feat: add l2 l3 simulator patch panel skeleton
This commit is contained in:
@@ -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());
|
||||
});
|
||||
Reference in New Issue
Block a user