feat: add l2 l3 simulator patch panel skeleton
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHealth, createBoxState } from "../../internal/sim/model.mjs";
|
||||
import { createJsonServer, listen, parsePort, readJson } from "../../internal/sim/http.mjs";
|
||||
|
||||
const boxId = process.env.HWLAB_BOX_ID ?? "boxsim_alpha";
|
||||
const port = parsePort(process.env.PORT, 7201);
|
||||
const state = createBoxState({ boxId });
|
||||
|
||||
const routes = new Map();
|
||||
|
||||
routes.set("GET /health/live", () => createHealth({ serviceId: "hwlab-box-simu", name: boxId }));
|
||||
routes.set("GET /status", () => state);
|
||||
routes.set("POST /ports/write", async ({ req }) => {
|
||||
const body = await readJson(req);
|
||||
if (!state.ports[body.port]) {
|
||||
throw new Error(`unknown port ${body.port}`);
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
state.ports[body.port] = {
|
||||
port: body.port,
|
||||
value: body.value ?? null,
|
||||
source: body.source ?? "local",
|
||||
updatedAt: now
|
||||
};
|
||||
state.updatedAt = now;
|
||||
|
||||
return {
|
||||
accepted: true,
|
||||
boxId,
|
||||
port: body.port,
|
||||
crossDevicePropagation: "patch-panel-only",
|
||||
localLoopbackEnabled: false
|
||||
};
|
||||
});
|
||||
|
||||
listen(createJsonServer({ routes }), port);
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env node
|
||||
import { createGatewayState, createHealth } from "../../internal/sim/model.mjs";
|
||||
import { createJsonServer, listen, parsePort } from "../../internal/sim/http.mjs";
|
||||
|
||||
const gatewayId = process.env.HWLAB_GATEWAY_ID ?? "gwsimu_east";
|
||||
const port = parsePort(process.env.PORT, 7101);
|
||||
const endpoint = `http://127.0.0.1:${port}`;
|
||||
const boxes = (process.env.HWLAB_BOX_RESOURCES ?? "res_boxsim_alpha")
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
const state = createGatewayState({ gatewayId, endpoint, boxes });
|
||||
|
||||
const routes = new Map();
|
||||
|
||||
routes.set("GET /health/live", () =>
|
||||
createHealth({ serviceId: "hwlab-gateway-simu", name: gatewayId })
|
||||
);
|
||||
routes.set("GET /status", () => state);
|
||||
routes.set("GET /boxes", () => ({
|
||||
gatewayId,
|
||||
boxes: state.boxes
|
||||
}));
|
||||
|
||||
listen(createJsonServer({ routes }), port);
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
import { createGatewayState, createHealth } from "../../internal/sim/model.mjs";
|
||||
import { createJsonServer, listen, parsePort } from "../../internal/sim/http.mjs";
|
||||
|
||||
const gatewayId = process.env.HWLAB_GATEWAY_ID ?? "gateway_dev_stub";
|
||||
const port = parsePort(process.env.PORT, 7001);
|
||||
const state = createGatewayState({
|
||||
gatewayId,
|
||||
serviceId: "hwlab-gateway",
|
||||
endpoint: `http://127.0.0.1:${port}`,
|
||||
boxes: []
|
||||
});
|
||||
state.session.status = "starting";
|
||||
|
||||
const routes = new Map();
|
||||
|
||||
routes.set("GET /health/live", () => createHealth({ serviceId: "hwlab-gateway", name: gatewayId }));
|
||||
routes.set("GET /status", () => ({
|
||||
...state,
|
||||
note: "L2/L3 skeleton only; real hardware integration is intentionally absent for MVP simulator work."
|
||||
}));
|
||||
|
||||
listen(createJsonServer({ routes }), port);
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
import { createMvpTopology } from "../../internal/patchpanel/fixture.mjs";
|
||||
import { createPatchPanel } from "../../internal/patchpanel/model.mjs";
|
||||
import { createHealth } from "../../internal/sim/model.mjs";
|
||||
import { createJsonServer, listen, parsePort, readJson } from "../../internal/sim/http.mjs";
|
||||
|
||||
const port = parsePort(process.env.PORT, 7301);
|
||||
const topology = createMvpTopology();
|
||||
const patchPanel = createPatchPanel({ wiringConfig: topology.wiringConfig });
|
||||
|
||||
const routes = new Map();
|
||||
|
||||
routes.set("GET /health/live", () =>
|
||||
createHealth({ serviceId: "hwlab-patch-panel", name: "hwlab-patch-panel" })
|
||||
);
|
||||
routes.set("GET /status", () => patchPanel.status());
|
||||
routes.set("GET /wiring", () => patchPanel.wiringConfig);
|
||||
routes.set("POST /signals/route", async ({ req }) => patchPanel.routeSignal(await readJson(req)));
|
||||
|
||||
listen(createJsonServer({ routes }), port);
|
||||
@@ -0,0 +1,9 @@
|
||||
# MVP Simulated Topology
|
||||
|
||||
`topology.json` is the L2/L3 dry-run fixture for the simulator skeleton. It
|
||||
describes two `hwlab-box-simu` instances, two `hwlab-gateway-simu` instances,
|
||||
and one `hwlab-patch-panel`.
|
||||
|
||||
Cross-device propagation is represented only through the patch panel wiring
|
||||
config. Box simulators keep local port state but do not provide loopback as a
|
||||
cross-device synchronization substitute.
|
||||
@@ -0,0 +1,226 @@
|
||||
{
|
||||
"topologyId": "top_mvp_l2_l3_sim_patch",
|
||||
"projectId": "prj_mvp_topology",
|
||||
"environment": "dev",
|
||||
"services": {
|
||||
"boxes": [
|
||||
{
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"boxId": "boxsim_alpha",
|
||||
"projectId": "prj_mvp_topology",
|
||||
"gatewaySessionId": "gws_gwsimu_east",
|
||||
"live": true,
|
||||
"resource": {
|
||||
"resourceId": "res_boxsim_alpha",
|
||||
"projectId": "prj_mvp_topology",
|
||||
"gatewaySessionId": "gws_gwsimu_east",
|
||||
"boxId": "boxsim_alpha",
|
||||
"resourceType": "simulator_endpoint",
|
||||
"name": "boxsim_alpha simulator endpoint",
|
||||
"state": "available",
|
||||
"environment": "dev",
|
||||
"metadata": {
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"ports": ["uart0", "eth0", "gpio0"],
|
||||
"propagation": "patch-panel-only"
|
||||
},
|
||||
"createdAt": "2026-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2026-01-01T00:00:00.000Z"
|
||||
},
|
||||
"ports": {
|
||||
"uart0": {
|
||||
"port": "uart0",
|
||||
"value": null,
|
||||
"source": "local",
|
||||
"updatedAt": "2026-01-01T00:00:00.000Z"
|
||||
},
|
||||
"eth0": {
|
||||
"port": "eth0",
|
||||
"value": null,
|
||||
"source": "local",
|
||||
"updatedAt": "2026-01-01T00:00:00.000Z"
|
||||
},
|
||||
"gpio0": {
|
||||
"port": "gpio0",
|
||||
"value": null,
|
||||
"source": "local",
|
||||
"updatedAt": "2026-01-01T00:00:00.000Z"
|
||||
}
|
||||
},
|
||||
"constraints": {
|
||||
"crossDevicePropagation": "patch-panel-only",
|
||||
"localLoopbackEnabled": false
|
||||
},
|
||||
"updatedAt": "2026-01-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"boxId": "boxsim_beta",
|
||||
"projectId": "prj_mvp_topology",
|
||||
"gatewaySessionId": "gws_gwsimu_west",
|
||||
"live": true,
|
||||
"resource": {
|
||||
"resourceId": "res_boxsim_beta",
|
||||
"projectId": "prj_mvp_topology",
|
||||
"gatewaySessionId": "gws_gwsimu_west",
|
||||
"boxId": "boxsim_beta",
|
||||
"resourceType": "simulator_endpoint",
|
||||
"name": "boxsim_beta simulator endpoint",
|
||||
"state": "available",
|
||||
"environment": "dev",
|
||||
"metadata": {
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"ports": ["uart0", "eth0", "gpio0"],
|
||||
"propagation": "patch-panel-only"
|
||||
},
|
||||
"createdAt": "2026-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2026-01-01T00:00:00.000Z"
|
||||
},
|
||||
"ports": {
|
||||
"uart0": {
|
||||
"port": "uart0",
|
||||
"value": null,
|
||||
"source": "local",
|
||||
"updatedAt": "2026-01-01T00:00:00.000Z"
|
||||
},
|
||||
"eth0": {
|
||||
"port": "eth0",
|
||||
"value": null,
|
||||
"source": "local",
|
||||
"updatedAt": "2026-01-01T00:00:00.000Z"
|
||||
},
|
||||
"gpio0": {
|
||||
"port": "gpio0",
|
||||
"value": null,
|
||||
"source": "local",
|
||||
"updatedAt": "2026-01-01T00:00:00.000Z"
|
||||
}
|
||||
},
|
||||
"constraints": {
|
||||
"crossDevicePropagation": "patch-panel-only",
|
||||
"localLoopbackEnabled": false
|
||||
},
|
||||
"updatedAt": "2026-01-01T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"gateways": [
|
||||
{
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"gatewayId": "gwsimu_east",
|
||||
"projectId": "prj_mvp_topology",
|
||||
"live": true,
|
||||
"session": {
|
||||
"gatewaySessionId": "gws_gwsimu_east",
|
||||
"projectId": "prj_mvp_topology",
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"gatewayId": "gwsimu_east",
|
||||
"endpoint": "http://127.0.0.1:7101",
|
||||
"status": "connected",
|
||||
"environment": "dev",
|
||||
"startedAt": "2026-01-01T00:00:00.000Z",
|
||||
"lastSeenAt": "2026-01-01T00:00:00.000Z",
|
||||
"labels": {
|
||||
"simulator": "true"
|
||||
}
|
||||
},
|
||||
"boxes": ["res_boxsim_alpha"],
|
||||
"updatedAt": "2026-01-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"gatewayId": "gwsimu_west",
|
||||
"projectId": "prj_mvp_topology",
|
||||
"live": true,
|
||||
"session": {
|
||||
"gatewaySessionId": "gws_gwsimu_west",
|
||||
"projectId": "prj_mvp_topology",
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"gatewayId": "gwsimu_west",
|
||||
"endpoint": "http://127.0.0.1:7102",
|
||||
"status": "connected",
|
||||
"environment": "dev",
|
||||
"startedAt": "2026-01-01T00:00:00.000Z",
|
||||
"lastSeenAt": "2026-01-01T00:00:00.000Z",
|
||||
"labels": {
|
||||
"simulator": "true"
|
||||
}
|
||||
},
|
||||
"boxes": ["res_boxsim_beta"],
|
||||
"updatedAt": "2026-01-01T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"patchPanel": {
|
||||
"patchPanelStatusId": "pps_mvp_patch_panel",
|
||||
"projectId": "prj_mvp_topology",
|
||||
"gatewaySessionId": "gws_gwsimu_east",
|
||||
"wiringConfigId": "wir_mvp_patch_panel",
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"state": "active",
|
||||
"environment": "dev",
|
||||
"activeConnections": [
|
||||
{
|
||||
"fromResourceId": "res_boxsim_alpha",
|
||||
"fromPort": "uart0",
|
||||
"toResourceId": "res_boxsim_beta",
|
||||
"toPort": "uart0"
|
||||
},
|
||||
{
|
||||
"fromResourceId": "res_boxsim_alpha",
|
||||
"fromPort": "gpio0",
|
||||
"toResourceId": "res_boxsim_beta",
|
||||
"toPort": "gpio0"
|
||||
}
|
||||
],
|
||||
"observedAt": "2026-01-01T00:00:00.000Z",
|
||||
"metadata": {
|
||||
"propagation": "patch-panel-only",
|
||||
"connectionCount": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"wiringConfig": {
|
||||
"wiringConfigId": "wir_mvp_patch_panel",
|
||||
"projectId": "prj_mvp_topology",
|
||||
"gatewaySessionId": "gws_gwsimu_east",
|
||||
"name": "MVP simulated patch panel wiring",
|
||||
"status": "active",
|
||||
"connections": [
|
||||
{
|
||||
"from": {
|
||||
"resourceId": "res_boxsim_alpha",
|
||||
"port": "uart0"
|
||||
},
|
||||
"to": {
|
||||
"resourceId": "res_boxsim_beta",
|
||||
"port": "uart0"
|
||||
},
|
||||
"mode": "exclusive"
|
||||
},
|
||||
{
|
||||
"from": {
|
||||
"resourceId": "res_boxsim_alpha",
|
||||
"port": "gpio0"
|
||||
},
|
||||
"to": {
|
||||
"resourceId": "res_boxsim_beta",
|
||||
"port": "gpio0"
|
||||
},
|
||||
"mode": "monitor"
|
||||
}
|
||||
],
|
||||
"constraints": {
|
||||
"propagation": "patch-panel-only",
|
||||
"localLoopbackSubstituteAllowed": false
|
||||
},
|
||||
"createdAt": "2026-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2026-01-01T00:00:00.000Z"
|
||||
},
|
||||
"constraints": {
|
||||
"patchPanelIsOnlyCrossDeviceChannel": true,
|
||||
"boxSimuLocalLoopbackForCrossDeviceSync": false
|
||||
},
|
||||
"dryRunHints": {
|
||||
"cliTarget": "fixtures/mvp-topology/topology.json",
|
||||
"expectedServices": ["hwlab-box-simu", "hwlab-gateway-simu", "hwlab-patch-panel"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { createBoxState, createGatewayState } from "../sim/model.mjs";
|
||||
import { createPatchPanelState, createWiringConfig } from "./model.mjs";
|
||||
|
||||
export const MVP_BOX_IDS = Object.freeze(["boxsim_alpha", "boxsim_beta"]);
|
||||
export const MVP_GATEWAY_IDS = Object.freeze(["gwsimu_east", "gwsimu_west"]);
|
||||
|
||||
export function createMvpTopology({ now = "2026-01-01T00:00:00.000Z" } = {}) {
|
||||
const gatewaySessionIds = MVP_GATEWAY_IDS.map((gatewayId) => `gws_${gatewayId}`);
|
||||
const boxes = MVP_BOX_IDS.map((boxId, index) =>
|
||||
createBoxState({
|
||||
boxId,
|
||||
gatewaySessionId: gatewaySessionIds[index],
|
||||
now
|
||||
})
|
||||
);
|
||||
|
||||
const gateways = MVP_GATEWAY_IDS.map((gatewayId, index) =>
|
||||
createGatewayState({
|
||||
gatewayId,
|
||||
endpoint: `http://127.0.0.1:${7101 + index}`,
|
||||
boxes: [boxes[index].resource.resourceId],
|
||||
now
|
||||
})
|
||||
);
|
||||
|
||||
const wiringConfig = createWiringConfig({
|
||||
gatewaySessionId: gatewaySessionIds[0],
|
||||
connections: [
|
||||
{
|
||||
from: {
|
||||
resourceId: boxes[0].resource.resourceId,
|
||||
port: "uart0"
|
||||
},
|
||||
to: {
|
||||
resourceId: boxes[1].resource.resourceId,
|
||||
port: "uart0"
|
||||
},
|
||||
mode: "exclusive"
|
||||
},
|
||||
{
|
||||
from: {
|
||||
resourceId: boxes[0].resource.resourceId,
|
||||
port: "gpio0"
|
||||
},
|
||||
to: {
|
||||
resourceId: boxes[1].resource.resourceId,
|
||||
port: "gpio0"
|
||||
},
|
||||
mode: "monitor"
|
||||
}
|
||||
],
|
||||
now
|
||||
});
|
||||
|
||||
const patchPanel = createPatchPanelState({
|
||||
gatewaySessionId: gatewaySessionIds[0],
|
||||
wiringConfig,
|
||||
now
|
||||
});
|
||||
|
||||
return {
|
||||
topologyId: "top_mvp_l2_l3_sim_patch",
|
||||
projectId: "prj_mvp_topology",
|
||||
environment: "dev",
|
||||
services: {
|
||||
boxes,
|
||||
gateways,
|
||||
patchPanel
|
||||
},
|
||||
wiringConfig,
|
||||
constraints: {
|
||||
patchPanelIsOnlyCrossDeviceChannel: true,
|
||||
boxSimuLocalLoopbackForCrossDeviceSync: false
|
||||
},
|
||||
dryRunHints: {
|
||||
cliTarget: "fixtures/mvp-topology/topology.json",
|
||||
expectedServices: ["hwlab-box-simu", "hwlab-gateway-simu", "hwlab-patch-panel"]
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { ENVIRONMENT_DEV } from "../protocol/index.mjs";
|
||||
import { DEFAULT_GATEWAY_SESSION_ID, DEFAULT_PROJECT_ID } from "../sim/model.mjs";
|
||||
|
||||
export function createWiringConfig({
|
||||
wiringConfigId = "wir_mvp_patch_panel",
|
||||
gatewaySessionId = DEFAULT_GATEWAY_SESSION_ID,
|
||||
connections,
|
||||
now = new Date().toISOString()
|
||||
}) {
|
||||
if (!Array.isArray(connections) || connections.length === 0) {
|
||||
throw new Error("wiring config requires at least one connection");
|
||||
}
|
||||
|
||||
return {
|
||||
wiringConfigId,
|
||||
projectId: DEFAULT_PROJECT_ID,
|
||||
gatewaySessionId,
|
||||
name: "MVP simulated patch panel wiring",
|
||||
status: "active",
|
||||
connections,
|
||||
constraints: {
|
||||
propagation: "patch-panel-only",
|
||||
localLoopbackSubstituteAllowed: false
|
||||
},
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
};
|
||||
}
|
||||
|
||||
export function createPatchPanelState({
|
||||
patchPanelStatusId = "pps_mvp_patch_panel",
|
||||
gatewaySessionId = DEFAULT_GATEWAY_SESSION_ID,
|
||||
wiringConfig,
|
||||
now = new Date().toISOString()
|
||||
}) {
|
||||
return {
|
||||
patchPanelStatusId,
|
||||
projectId: DEFAULT_PROJECT_ID,
|
||||
gatewaySessionId,
|
||||
wiringConfigId: wiringConfig.wiringConfigId,
|
||||
serviceId: "hwlab-patch-panel",
|
||||
state: "active",
|
||||
environment: ENVIRONMENT_DEV,
|
||||
activeConnections: wiringConfig.connections.map((connection) => ({
|
||||
fromResourceId: connection.from.resourceId,
|
||||
fromPort: connection.from.port,
|
||||
toResourceId: connection.to.resourceId,
|
||||
toPort: connection.to.port
|
||||
})),
|
||||
observedAt: now,
|
||||
metadata: {
|
||||
propagation: "patch-panel-only",
|
||||
connectionCount: wiringConfig.connections.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createPatchPanel({ wiringConfig, now = () => new Date().toISOString() }) {
|
||||
const signals = new Map();
|
||||
|
||||
function routeSignal({ fromResourceId, fromPort, value }) {
|
||||
const observedAt = now();
|
||||
const deliveries = [];
|
||||
|
||||
for (const connection of wiringConfig.connections) {
|
||||
const forwardMatch =
|
||||
connection.from.resourceId === fromResourceId && connection.from.port === fromPort;
|
||||
const reverseMatch =
|
||||
connection.to.resourceId === fromResourceId && connection.to.port === fromPort;
|
||||
|
||||
if (!forwardMatch && !reverseMatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const target = forwardMatch ? connection.to : connection.from;
|
||||
const key = `${target.resourceId}:${target.port}`;
|
||||
const delivery = {
|
||||
resourceId: target.resourceId,
|
||||
port: target.port,
|
||||
value,
|
||||
sourceResourceId: fromResourceId,
|
||||
sourcePort: fromPort,
|
||||
observedAt
|
||||
};
|
||||
signals.set(key, delivery);
|
||||
deliveries.push(delivery);
|
||||
}
|
||||
|
||||
return {
|
||||
accepted: true,
|
||||
propagatedBy: "hwlab-patch-panel",
|
||||
deliveryCount: deliveries.length,
|
||||
deliveries,
|
||||
observedAt
|
||||
};
|
||||
}
|
||||
|
||||
function status() {
|
||||
return createPatchPanelState({
|
||||
wiringConfig,
|
||||
now: now()
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
wiringConfig,
|
||||
routeSignal,
|
||||
status,
|
||||
signals
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
import { createMvpTopology } from "./fixture.mjs";
|
||||
import { createPatchPanel } from "./model.mjs";
|
||||
|
||||
test("MVP topology includes two box simulators, two gateway simulators, and one patch panel", () => {
|
||||
const topology = createMvpTopology();
|
||||
|
||||
assert.equal(topology.services.boxes.length, 2);
|
||||
assert.equal(topology.services.gateways.length, 2);
|
||||
assert.equal(topology.services.patchPanel.serviceId, "hwlab-patch-panel");
|
||||
assert.equal(topology.constraints.patchPanelIsOnlyCrossDeviceChannel, true);
|
||||
|
||||
for (const box of topology.services.boxes) {
|
||||
assert.equal(box.serviceId, "hwlab-box-simu");
|
||||
assert.equal(box.constraints.localLoopbackEnabled, false);
|
||||
assert.equal(box.constraints.crossDevicePropagation, "patch-panel-only");
|
||||
}
|
||||
});
|
||||
|
||||
test("patch panel routes cross-device signals and records deliveries", () => {
|
||||
const topology = createMvpTopology();
|
||||
const patchPanel = createPatchPanel({
|
||||
wiringConfig: topology.wiringConfig,
|
||||
now: () => "2026-01-01T00:00:01.000Z"
|
||||
});
|
||||
|
||||
const result = patchPanel.routeSignal({
|
||||
fromResourceId: "res_boxsim_alpha",
|
||||
fromPort: "uart0",
|
||||
value: "boot>"
|
||||
});
|
||||
|
||||
assert.equal(result.accepted, true);
|
||||
assert.equal(result.propagatedBy, "hwlab-patch-panel");
|
||||
assert.deepEqual(result.deliveries, [
|
||||
{
|
||||
resourceId: "res_boxsim_beta",
|
||||
port: "uart0",
|
||||
value: "boot>",
|
||||
sourceResourceId: "res_boxsim_alpha",
|
||||
sourcePort: "uart0",
|
||||
observedAt: "2026-01-01T00:00:01.000Z"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test("static dry-run fixture matches generated MVP topology", async () => {
|
||||
const raw = await readFile(new URL("../../fixtures/mvp-topology/topology.json", import.meta.url), "utf8");
|
||||
assert.deepEqual(JSON.parse(raw), createMvpTopology());
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { createServer } from "node:http";
|
||||
|
||||
export function jsonResponse(res, statusCode, body) {
|
||||
const payload = JSON.stringify(body, null, 2);
|
||||
res.writeHead(statusCode, {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-length": Buffer.byteLength(payload)
|
||||
});
|
||||
res.end(payload);
|
||||
}
|
||||
|
||||
export async function readJson(req) {
|
||||
const chunks = [];
|
||||
for await (const chunk of req) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
||||
if (!raw) {
|
||||
return {};
|
||||
}
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
export function createJsonServer({ routes }) {
|
||||
return createServer(async (req, res) => {
|
||||
try {
|
||||
const url = new URL(req.url ?? "/", "http://localhost");
|
||||
const key = `${req.method ?? "GET"} ${url.pathname}`;
|
||||
const handler = routes.get(key);
|
||||
|
||||
if (!handler) {
|
||||
jsonResponse(res, 404, {
|
||||
error: "not_found",
|
||||
path: url.pathname
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const body = await handler({ req, url });
|
||||
jsonResponse(res, 200, body);
|
||||
} catch (error) {
|
||||
jsonResponse(res, 500, {
|
||||
error: "internal_error",
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function parsePort(value, fallback) {
|
||||
const parsed = Number.parseInt(value ?? "", 10);
|
||||
if (Number.isInteger(parsed) && parsed > 0 && parsed < 65536) {
|
||||
return parsed;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function listen(server, port, host = "0.0.0.0") {
|
||||
server.listen(port, host, () => {
|
||||
const address = server.address();
|
||||
const bound = typeof address === "object" && address ? `${address.address}:${address.port}` : String(address);
|
||||
console.log(`listening on ${bound}`);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { ENVIRONMENT_DEV } from "../protocol/index.mjs";
|
||||
|
||||
export const DEFAULT_PROJECT_ID = "prj_mvp_topology";
|
||||
export const DEFAULT_GATEWAY_SESSION_ID = "gws_mvp_simu";
|
||||
|
||||
export const SIM_PORTS = Object.freeze(["uart0", "eth0", "gpio0"]);
|
||||
|
||||
function isoNow() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
export function createBoxResource({ boxId, gatewaySessionId = DEFAULT_GATEWAY_SESSION_ID, now = isoNow() }) {
|
||||
return {
|
||||
resourceId: `res_${boxId}`,
|
||||
projectId: DEFAULT_PROJECT_ID,
|
||||
gatewaySessionId,
|
||||
boxId,
|
||||
resourceType: "simulator_endpoint",
|
||||
name: `${boxId} simulator endpoint`,
|
||||
state: "available",
|
||||
environment: ENVIRONMENT_DEV,
|
||||
metadata: {
|
||||
serviceId: "hwlab-box-simu",
|
||||
ports: SIM_PORTS,
|
||||
propagation: "patch-panel-only"
|
||||
},
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
};
|
||||
}
|
||||
|
||||
export function createGatewaySession({
|
||||
gatewayId,
|
||||
serviceId = "hwlab-gateway-simu",
|
||||
endpoint,
|
||||
now = isoNow()
|
||||
}) {
|
||||
return {
|
||||
gatewaySessionId: `gws_${gatewayId}`,
|
||||
projectId: DEFAULT_PROJECT_ID,
|
||||
serviceId,
|
||||
gatewayId,
|
||||
endpoint,
|
||||
status: "connected",
|
||||
environment: ENVIRONMENT_DEV,
|
||||
startedAt: now,
|
||||
lastSeenAt: now,
|
||||
labels: {
|
||||
simulator: serviceId === "hwlab-gateway-simu" ? "true" : "false"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createBoxState({ boxId, gatewaySessionId = DEFAULT_GATEWAY_SESSION_ID, now = isoNow() }) {
|
||||
const ports = Object.fromEntries(
|
||||
SIM_PORTS.map((port) => [
|
||||
port,
|
||||
{
|
||||
port,
|
||||
value: null,
|
||||
source: "local",
|
||||
updatedAt: now
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
return {
|
||||
serviceId: "hwlab-box-simu",
|
||||
boxId,
|
||||
projectId: DEFAULT_PROJECT_ID,
|
||||
gatewaySessionId,
|
||||
live: true,
|
||||
resource: createBoxResource({ boxId, gatewaySessionId, now }),
|
||||
ports,
|
||||
constraints: {
|
||||
crossDevicePropagation: "patch-panel-only",
|
||||
localLoopbackEnabled: false
|
||||
},
|
||||
updatedAt: now
|
||||
};
|
||||
}
|
||||
|
||||
export function createGatewayState({
|
||||
gatewayId,
|
||||
serviceId = "hwlab-gateway-simu",
|
||||
endpoint,
|
||||
boxes = [],
|
||||
now = isoNow()
|
||||
}) {
|
||||
return {
|
||||
serviceId,
|
||||
gatewayId,
|
||||
projectId: DEFAULT_PROJECT_ID,
|
||||
live: true,
|
||||
session: createGatewaySession({ gatewayId, serviceId, endpoint, now }),
|
||||
boxes,
|
||||
updatedAt: now
|
||||
};
|
||||
}
|
||||
|
||||
export function createHealth({ serviceId, name, now = isoNow() }) {
|
||||
return {
|
||||
serviceId,
|
||||
name,
|
||||
environment: ENVIRONMENT_DEV,
|
||||
live: true,
|
||||
observedAt: now
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user