feat: add l2 l3 simulator patch panel skeleton

This commit is contained in:
HWLAB Code Queue
2026-05-21 14:40:12 +00:00
parent b1d31380cb
commit 77f70ff4ad
11 changed files with 757 additions and 0 deletions
+80
View File
@@ -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"]
}
};
}
+111
View File
@@ -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
};
}
+52
View File
@@ -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());
});