test: add m3 hardware loop smoke
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
# M3 Hardware Trusted Loop Local Contract
|
||||
|
||||
This contract smoke proves the local M3 wiring path for:
|
||||
|
||||
```text
|
||||
box-simu-1 DO1 -> box-simu-2 DI1
|
||||
```
|
||||
|
||||
The fixture is `fixtures/mvp/m3-hardware-loop/topology.json`. It contains two
|
||||
`hwlab-box-simu` resources, two `hwlab-gateway-simu` sessions, and one
|
||||
`hwlab-patch-panel` status object. The active wiring config has one exclusive
|
||||
connection from `res_boxsimu_1:DO1` to `res_boxsimu_2:DI1`.
|
||||
|
||||
Run the smoke locally:
|
||||
|
||||
```sh
|
||||
node scripts/m3-hardware-loop-smoke.mjs
|
||||
```
|
||||
|
||||
## What It Checks
|
||||
|
||||
- DEV-only topology with two boxes, two gateways, and one active patch panel.
|
||||
- Patch-panel-only propagation; box local loopback is not accepted as a
|
||||
cross-device substitute.
|
||||
- Direct local call into the existing patch-panel model routes
|
||||
`box-simu-1 DO1` to `box-simu-2 DI1`.
|
||||
- An unwired local signal does not cross devices.
|
||||
- The expected audit event includes the L0 audit fields:
|
||||
`auditId`, `traceId`, `actorType`, `actorId`, `action`, `targetType`,
|
||||
`targetId`, `serviceId`, `environment`, and `occurredAt`, plus project,
|
||||
gateway session, operation, outcome, and metadata fields.
|
||||
|
||||
## Boundary
|
||||
|
||||
This is a local contract smoke only. It does not deploy DEV or PROD services,
|
||||
does not connect to real hardware, does not read secrets or tokens, and does
|
||||
not write `audit_events` rows to a database. The audit event in the fixture is
|
||||
an expected record shape for the future real flow.
|
||||
|
||||
The later real DEV M3 path must replace the in-process direct call with live
|
||||
service boundaries: cloud request validation, gateway session ownership,
|
||||
patch-panel state application, real or simulator box observation, persisted
|
||||
audit events, and evidence records. That future work must preserve the same
|
||||
wiring contract and must keep routing decisions under `hwlab-patch-panel`
|
||||
ownership instead of mutating another box directly.
|
||||
@@ -0,0 +1,15 @@
|
||||
# M3 Hardware Loop Fixture
|
||||
|
||||
`topology.json` is the local contract fixture for the M3 hardware trusted loop
|
||||
smoke. It models two simulated boxes, two simulated gateways, and one patch
|
||||
panel.
|
||||
|
||||
The only active cross-device wiring is:
|
||||
|
||||
```text
|
||||
box-simu-1 DO1 -> box-simu-2 DI1
|
||||
```
|
||||
|
||||
The smoke script reads this fixture and calls the patch-panel model directly.
|
||||
It does not start services, deploy DEV/PROD, access real hardware, read
|
||||
secrets, or write audit rows to a database.
|
||||
@@ -0,0 +1,264 @@
|
||||
{
|
||||
"topologyId": "top_m3_hardware_loop",
|
||||
"projectId": "prj_m3_hardware_loop",
|
||||
"environment": "dev",
|
||||
"contract": {
|
||||
"name": "M3 hardware trusted loop local contract smoke",
|
||||
"signal": "box-simu-1 DO1 -> box-simu-2 DI1",
|
||||
"mode": "local-direct-call",
|
||||
"databaseWriteRequired": false,
|
||||
"realHardwareRequired": false,
|
||||
"cloudApiRequired": false
|
||||
},
|
||||
"services": {
|
||||
"boxes": [
|
||||
{
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"boxId": "boxsimu_1",
|
||||
"aliases": ["box-simu-1"],
|
||||
"projectId": "prj_m3_hardware_loop",
|
||||
"gatewaySessionId": "gws_gwsimu_1",
|
||||
"live": true,
|
||||
"resource": {
|
||||
"resourceId": "res_boxsimu_1",
|
||||
"projectId": "prj_m3_hardware_loop",
|
||||
"gatewaySessionId": "gws_gwsimu_1",
|
||||
"boxId": "boxsimu_1",
|
||||
"resourceType": "simulator_endpoint",
|
||||
"name": "box-simu-1 simulator endpoint",
|
||||
"state": "available",
|
||||
"environment": "dev",
|
||||
"metadata": {
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"simulatorName": "box-simu-1",
|
||||
"ports": ["DO1", "DI1"],
|
||||
"propagation": "patch-panel-only"
|
||||
},
|
||||
"createdAt": "2026-05-21T00:00:00.000Z",
|
||||
"updatedAt": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
"ports": {
|
||||
"DO1": {
|
||||
"port": "DO1",
|
||||
"direction": "output",
|
||||
"value": false,
|
||||
"source": "local",
|
||||
"updatedAt": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
"DI1": {
|
||||
"port": "DI1",
|
||||
"direction": "input",
|
||||
"value": false,
|
||||
"source": "local",
|
||||
"updatedAt": "2026-05-21T00:00:00.000Z"
|
||||
}
|
||||
},
|
||||
"constraints": {
|
||||
"crossDevicePropagation": "patch-panel-only",
|
||||
"localLoopbackEnabled": false
|
||||
},
|
||||
"updatedAt": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"boxId": "boxsimu_2",
|
||||
"aliases": ["box-simu-2"],
|
||||
"projectId": "prj_m3_hardware_loop",
|
||||
"gatewaySessionId": "gws_gwsimu_2",
|
||||
"live": true,
|
||||
"resource": {
|
||||
"resourceId": "res_boxsimu_2",
|
||||
"projectId": "prj_m3_hardware_loop",
|
||||
"gatewaySessionId": "gws_gwsimu_2",
|
||||
"boxId": "boxsimu_2",
|
||||
"resourceType": "simulator_endpoint",
|
||||
"name": "box-simu-2 simulator endpoint",
|
||||
"state": "available",
|
||||
"environment": "dev",
|
||||
"metadata": {
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"simulatorName": "box-simu-2",
|
||||
"ports": ["DO1", "DI1"],
|
||||
"propagation": "patch-panel-only"
|
||||
},
|
||||
"createdAt": "2026-05-21T00:00:00.000Z",
|
||||
"updatedAt": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
"ports": {
|
||||
"DO1": {
|
||||
"port": "DO1",
|
||||
"direction": "output",
|
||||
"value": false,
|
||||
"source": "local",
|
||||
"updatedAt": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
"DI1": {
|
||||
"port": "DI1",
|
||||
"direction": "input",
|
||||
"value": false,
|
||||
"source": "local",
|
||||
"updatedAt": "2026-05-21T00:00:00.000Z"
|
||||
}
|
||||
},
|
||||
"constraints": {
|
||||
"crossDevicePropagation": "patch-panel-only",
|
||||
"localLoopbackEnabled": false
|
||||
},
|
||||
"updatedAt": "2026-05-21T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"gateways": [
|
||||
{
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"gatewayId": "gwsimu_1",
|
||||
"aliases": ["gateway-simu-1"],
|
||||
"projectId": "prj_m3_hardware_loop",
|
||||
"live": true,
|
||||
"session": {
|
||||
"gatewaySessionId": "gws_gwsimu_1",
|
||||
"projectId": "prj_m3_hardware_loop",
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"gatewayId": "gwsimu_1",
|
||||
"endpoint": "http://127.0.0.1:7201",
|
||||
"status": "connected",
|
||||
"environment": "dev",
|
||||
"startedAt": "2026-05-21T00:00:00.000Z",
|
||||
"lastSeenAt": "2026-05-21T00:00:00.000Z",
|
||||
"labels": {
|
||||
"simulator": "true"
|
||||
}
|
||||
},
|
||||
"boxes": ["res_boxsimu_1"],
|
||||
"updatedAt": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"gatewayId": "gwsimu_2",
|
||||
"aliases": ["gateway-simu-2"],
|
||||
"projectId": "prj_m3_hardware_loop",
|
||||
"live": true,
|
||||
"session": {
|
||||
"gatewaySessionId": "gws_gwsimu_2",
|
||||
"projectId": "prj_m3_hardware_loop",
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"gatewayId": "gwsimu_2",
|
||||
"endpoint": "http://127.0.0.1:7202",
|
||||
"status": "connected",
|
||||
"environment": "dev",
|
||||
"startedAt": "2026-05-21T00:00:00.000Z",
|
||||
"lastSeenAt": "2026-05-21T00:00:00.000Z",
|
||||
"labels": {
|
||||
"simulator": "true"
|
||||
}
|
||||
},
|
||||
"boxes": ["res_boxsimu_2"],
|
||||
"updatedAt": "2026-05-21T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"patchPanel": {
|
||||
"patchPanelStatusId": "pps_m3_hardware_loop",
|
||||
"projectId": "prj_m3_hardware_loop",
|
||||
"gatewaySessionId": "gws_gwsimu_1",
|
||||
"wiringConfigId": "wir_m3_do1_di1",
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"state": "active",
|
||||
"environment": "dev",
|
||||
"activeConnections": [
|
||||
{
|
||||
"fromResourceId": "res_boxsimu_1",
|
||||
"fromPort": "DO1",
|
||||
"toResourceId": "res_boxsimu_2",
|
||||
"toPort": "DI1"
|
||||
}
|
||||
],
|
||||
"observedAt": "2026-05-21T00:00:00.000Z",
|
||||
"metadata": {
|
||||
"propagation": "patch-panel-only",
|
||||
"connectionCount": 1,
|
||||
"signal": "box-simu-1 DO1 -> box-simu-2 DI1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"wiringConfig": {
|
||||
"wiringConfigId": "wir_m3_do1_di1",
|
||||
"projectId": "prj_m3_hardware_loop",
|
||||
"gatewaySessionId": "gws_gwsimu_1",
|
||||
"name": "M3 simulated DO1 to DI1 patch wiring",
|
||||
"status": "active",
|
||||
"connections": [
|
||||
{
|
||||
"from": {
|
||||
"resourceId": "res_boxsimu_1",
|
||||
"port": "DO1"
|
||||
},
|
||||
"to": {
|
||||
"resourceId": "res_boxsimu_2",
|
||||
"port": "DI1"
|
||||
},
|
||||
"mode": "exclusive"
|
||||
}
|
||||
],
|
||||
"constraints": {
|
||||
"propagation": "patch-panel-only",
|
||||
"localLoopbackSubstituteAllowed": false
|
||||
},
|
||||
"createdAt": "2026-05-21T00:00:00.000Z",
|
||||
"updatedAt": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
"directCall": {
|
||||
"operationId": "op_m3_hardware_loop_smoke",
|
||||
"traceId": "trc_m3_hardware_loop_smoke",
|
||||
"source": {
|
||||
"boxAlias": "box-simu-1",
|
||||
"resourceId": "res_boxsimu_1",
|
||||
"port": "DO1",
|
||||
"value": true
|
||||
},
|
||||
"target": {
|
||||
"boxAlias": "box-simu-2",
|
||||
"resourceId": "res_boxsimu_2",
|
||||
"port": "DI1",
|
||||
"expectedValue": true
|
||||
},
|
||||
"expected": {
|
||||
"accepted": true,
|
||||
"propagatedBy": "hwlab-patch-panel",
|
||||
"deliveryCount": 1,
|
||||
"observedAt": "2026-05-21T00:00:01.000Z"
|
||||
}
|
||||
},
|
||||
"expectedAuditEvent": {
|
||||
"auditId": "aud_m3_hardware_loop_smoke",
|
||||
"traceId": "trc_m3_hardware_loop_smoke",
|
||||
"actorType": "system",
|
||||
"actorId": "sys_m3_hardware_loop_smoke",
|
||||
"action": "hardware.loop.smoke",
|
||||
"targetType": "wiring_config",
|
||||
"targetId": "wir_m3_do1_di1",
|
||||
"projectId": "prj_m3_hardware_loop",
|
||||
"gatewaySessionId": "gws_gwsimu_1",
|
||||
"operationId": "op_m3_hardware_loop_smoke",
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"environment": "dev",
|
||||
"outcome": "succeeded",
|
||||
"metadata": {
|
||||
"databaseWriteRequired": false,
|
||||
"directCall": true,
|
||||
"source": "box-simu-1 DO1",
|
||||
"target": "box-simu-2 DI1",
|
||||
"propagation": "patch-panel-only"
|
||||
},
|
||||
"occurredAt": "2026-05-21T00:00:01.000Z"
|
||||
},
|
||||
"constraints": {
|
||||
"patchPanelIsOnlyCrossDeviceChannel": true,
|
||||
"boxSimuLocalLoopbackForCrossDeviceSync": false,
|
||||
"realDevHardwareMutationAllowed": false,
|
||||
"cloudApiImplementationMutationAllowed": false
|
||||
},
|
||||
"dryRunHints": {
|
||||
"cliTarget": "fixtures/mvp/m3-hardware-loop/topology.json",
|
||||
"expectedServices": ["hwlab-box-simu", "hwlab-gateway-simu", "hwlab-patch-panel"],
|
||||
"command": "node scripts/m3-hardware-loop-smoke.mjs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { createPatchPanel } from "../internal/patchpanel/model.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const fixturePath = path.resolve(
|
||||
repoRoot,
|
||||
process.argv[2] ?? "fixtures/mvp/m3-hardware-loop/topology.json"
|
||||
);
|
||||
|
||||
const REQUIRED_AUDIT_FIELDS = Object.freeze([
|
||||
"auditId",
|
||||
"traceId",
|
||||
"actorType",
|
||||
"actorId",
|
||||
"action",
|
||||
"targetType",
|
||||
"targetId",
|
||||
"serviceId",
|
||||
"environment",
|
||||
"occurredAt"
|
||||
]);
|
||||
|
||||
const RECOMMENDED_AUDIT_FIELDS = Object.freeze([
|
||||
"projectId",
|
||||
"gatewaySessionId",
|
||||
"operationId",
|
||||
"outcome",
|
||||
"metadata"
|
||||
]);
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function assertPort(box, port) {
|
||||
assert.ok(box.ports, `${box.boxId} missing ports`);
|
||||
assert.ok(Object.hasOwn(box.ports, port), `${box.boxId} missing port ${port}`);
|
||||
assert.equal(box.ports[port].port, port, `${box.boxId}.${port} port name`);
|
||||
}
|
||||
|
||||
function assertTimestamp(value, name) {
|
||||
assert.equal(typeof value, "string", `${name} must be a string`);
|
||||
assert.ok(!Number.isNaN(Date.parse(value)), `${name} must be an RFC 3339 timestamp`);
|
||||
}
|
||||
|
||||
function assertOnePatchPanel(topology) {
|
||||
const patchPanel = topology.services?.patchPanel;
|
||||
assert.ok(patchPanel, "topology requires one patch panel");
|
||||
assert.equal(patchPanel.serviceId, "hwlab-patch-panel");
|
||||
assert.equal(patchPanel.state, "active");
|
||||
assert.equal(patchPanel.environment, "dev");
|
||||
assert.equal(patchPanel.projectId, topology.projectId);
|
||||
assert.equal(patchPanel.wiringConfigId, topology.wiringConfig.wiringConfigId);
|
||||
assert.equal(patchPanel.metadata?.propagation, "patch-panel-only");
|
||||
assert.equal(patchPanel.activeConnections.length, topology.wiringConfig.connections.length);
|
||||
return patchPanel;
|
||||
}
|
||||
|
||||
function indexBoxes(boxes) {
|
||||
const byResourceId = new Map();
|
||||
const byAlias = new Map();
|
||||
|
||||
for (const box of boxes) {
|
||||
assert.equal(box.serviceId, "hwlab-box-simu");
|
||||
assert.equal(box.live, true, `${box.boxId} must be live`);
|
||||
assert.equal(box.projectId, box.resource.projectId, `${box.boxId} project must match resource`);
|
||||
assert.equal(box.environment ?? box.resource.environment, "dev", `${box.boxId} environment`);
|
||||
assert.equal(box.constraints.crossDevicePropagation, "patch-panel-only");
|
||||
assert.equal(box.constraints.localLoopbackEnabled, false);
|
||||
assert.equal(box.resource.metadata.propagation, "patch-panel-only");
|
||||
assertPort(box, "DO1");
|
||||
assertPort(box, "DI1");
|
||||
|
||||
byResourceId.set(box.resource.resourceId, box);
|
||||
for (const alias of box.aliases ?? []) {
|
||||
byAlias.set(alias, box);
|
||||
}
|
||||
}
|
||||
|
||||
return { byResourceId, byAlias };
|
||||
}
|
||||
|
||||
function assertGateways(topology, resourceIds) {
|
||||
const gateways = topology.services?.gateways;
|
||||
assert.ok(Array.isArray(gateways), "topology requires gateways");
|
||||
assert.equal(gateways.length, 2, "topology requires exactly two gateway simulators");
|
||||
|
||||
const seenSessions = new Set();
|
||||
for (const gateway of gateways) {
|
||||
assert.equal(gateway.serviceId, "hwlab-gateway-simu");
|
||||
assert.equal(gateway.live, true, `${gateway.gatewayId} must be live`);
|
||||
assert.equal(gateway.projectId, topology.projectId);
|
||||
assert.equal(gateway.session.serviceId, "hwlab-gateway-simu");
|
||||
assert.equal(gateway.session.status, "connected");
|
||||
assert.equal(gateway.session.environment, "dev");
|
||||
assert.equal(gateway.session.projectId, topology.projectId);
|
||||
seenSessions.add(gateway.session.gatewaySessionId);
|
||||
|
||||
assert.ok(Array.isArray(gateway.boxes) && gateway.boxes.length >= 1, `${gateway.gatewayId} boxes`);
|
||||
for (const resourceId of gateway.boxes) {
|
||||
assert.ok(resourceIds.has(resourceId), `${gateway.gatewayId} references known box resource`);
|
||||
}
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
seenSessions.has(topology.wiringConfig.gatewaySessionId),
|
||||
"wiring gateway session must belong to a gateway simulator"
|
||||
);
|
||||
}
|
||||
|
||||
function assertWiringMatchesPatchPanel(wiringConfig, patchPanel) {
|
||||
assert.equal(wiringConfig.status, "active");
|
||||
assert.equal(wiringConfig.constraints.propagation, "patch-panel-only");
|
||||
assert.equal(wiringConfig.constraints.localLoopbackSubstituteAllowed, false);
|
||||
assert.equal(wiringConfig.connections.length, 1, "M3 smoke expects one DO1->DI1 connection");
|
||||
|
||||
const connection = wiringConfig.connections[0];
|
||||
const activeConnection = patchPanel.activeConnections[0];
|
||||
assert.deepEqual(activeConnection, {
|
||||
fromResourceId: connection.from.resourceId,
|
||||
fromPort: connection.from.port,
|
||||
toResourceId: connection.to.resourceId,
|
||||
toPort: connection.to.port
|
||||
});
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
function assertAuditEvent(topology) {
|
||||
const auditEvent = topology.expectedAuditEvent;
|
||||
assert.ok(auditEvent && typeof auditEvent === "object", "expectedAuditEvent is required");
|
||||
|
||||
for (const field of REQUIRED_AUDIT_FIELDS) {
|
||||
assert.ok(Object.hasOwn(auditEvent, field), `expected audit.${field}`);
|
||||
}
|
||||
for (const field of RECOMMENDED_AUDIT_FIELDS) {
|
||||
assert.ok(Object.hasOwn(auditEvent, field), `expected audit.${field}`);
|
||||
}
|
||||
|
||||
assert.equal(auditEvent.traceId, topology.directCall.traceId);
|
||||
assert.equal(auditEvent.projectId, topology.projectId);
|
||||
assert.equal(auditEvent.gatewaySessionId, topology.wiringConfig.gatewaySessionId);
|
||||
assert.equal(auditEvent.operationId, topology.directCall.operationId);
|
||||
assert.equal(auditEvent.targetType, "wiring_config");
|
||||
assert.equal(auditEvent.targetId, topology.wiringConfig.wiringConfigId);
|
||||
assert.equal(auditEvent.serviceId, "hwlab-patch-panel");
|
||||
assert.equal(auditEvent.environment, "dev");
|
||||
assert.equal(auditEvent.outcome, "succeeded");
|
||||
assert.equal(auditEvent.metadata.databaseWriteRequired, false);
|
||||
assert.equal(auditEvent.metadata.directCall, true);
|
||||
assertTimestamp(auditEvent.occurredAt, "audit.occurredAt");
|
||||
}
|
||||
|
||||
function applyOutput(box, port, value, updatedAt) {
|
||||
assertPort(box, port);
|
||||
assert.equal(box.ports[port].direction, "output", `${box.boxId}.${port} must be an output`);
|
||||
box.ports[port] = {
|
||||
...box.ports[port],
|
||||
value,
|
||||
source: "local",
|
||||
updatedAt
|
||||
};
|
||||
}
|
||||
|
||||
function applyDeliveries(boxesByResourceId, deliveries) {
|
||||
for (const delivery of deliveries) {
|
||||
const box = boxesByResourceId.get(delivery.resourceId);
|
||||
assert.ok(box, `delivery target ${delivery.resourceId} exists`);
|
||||
assertPort(box, delivery.port);
|
||||
assert.equal(box.ports[delivery.port].direction, "input", `${box.boxId}.${delivery.port} must be an input`);
|
||||
box.ports[delivery.port] = {
|
||||
...box.ports[delivery.port],
|
||||
value: delivery.value,
|
||||
source: delivery.sourceResourceId,
|
||||
sourcePort: delivery.sourcePort,
|
||||
propagatedBy: "hwlab-patch-panel",
|
||||
updatedAt: delivery.observedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const raw = await readFile(fixturePath, "utf8");
|
||||
const topology = JSON.parse(raw);
|
||||
|
||||
assert.equal(topology.environment, "dev");
|
||||
assert.equal(topology.contract.mode, "local-direct-call");
|
||||
assert.equal(topology.contract.databaseWriteRequired, false);
|
||||
assert.equal(topology.contract.realHardwareRequired, false);
|
||||
assert.equal(topology.contract.cloudApiRequired, false);
|
||||
assert.equal(topology.constraints.patchPanelIsOnlyCrossDeviceChannel, true);
|
||||
assert.equal(topology.constraints.boxSimuLocalLoopbackForCrossDeviceSync, false);
|
||||
assert.equal(topology.constraints.realDevHardwareMutationAllowed, false);
|
||||
assert.equal(topology.constraints.cloudApiImplementationMutationAllowed, false);
|
||||
|
||||
const boxes = topology.services?.boxes;
|
||||
assert.ok(Array.isArray(boxes), "topology requires boxes");
|
||||
assert.equal(boxes.length, 2, "topology requires exactly two box simulators");
|
||||
|
||||
const { byResourceId, byAlias } = indexBoxes(boxes);
|
||||
assertGateways(topology, new Set(byResourceId.keys()));
|
||||
|
||||
const patchPanelStatus = assertOnePatchPanel(topology);
|
||||
const connection = assertWiringMatchesPatchPanel(topology.wiringConfig, patchPanelStatus);
|
||||
assertAuditEvent(topology);
|
||||
|
||||
const directCall = topology.directCall;
|
||||
const sourceBox = byAlias.get(directCall.source.boxAlias);
|
||||
const targetBox = byAlias.get(directCall.target.boxAlias);
|
||||
assert.ok(sourceBox, "box-simu-1 alias must exist");
|
||||
assert.ok(targetBox, "box-simu-2 alias must exist");
|
||||
assert.equal(connection.from.resourceId, sourceBox.resource.resourceId);
|
||||
assert.equal(connection.from.port, "DO1");
|
||||
assert.equal(connection.to.resourceId, targetBox.resource.resourceId);
|
||||
assert.equal(connection.to.port, "DI1");
|
||||
assert.equal(directCall.source.resourceId, sourceBox.resource.resourceId);
|
||||
assert.equal(directCall.source.port, "DO1");
|
||||
assert.equal(directCall.target.resourceId, targetBox.resource.resourceId);
|
||||
assert.equal(directCall.target.port, "DI1");
|
||||
|
||||
const localBoxes = new Map([...byResourceId].map(([resourceId, box]) => [resourceId, clone(box)]));
|
||||
applyOutput(
|
||||
localBoxes.get(directCall.source.resourceId),
|
||||
directCall.source.port,
|
||||
directCall.source.value,
|
||||
directCall.expected.observedAt
|
||||
);
|
||||
|
||||
const patchPanel = createPatchPanel({
|
||||
wiringConfig: topology.wiringConfig,
|
||||
now: () => directCall.expected.observedAt
|
||||
});
|
||||
|
||||
const routeResult = patchPanel.routeSignal({
|
||||
fromResourceId: directCall.source.resourceId,
|
||||
fromPort: directCall.source.port,
|
||||
value: directCall.source.value
|
||||
});
|
||||
|
||||
assert.equal(routeResult.accepted, directCall.expected.accepted);
|
||||
assert.equal(routeResult.propagatedBy, directCall.expected.propagatedBy);
|
||||
assert.equal(routeResult.deliveryCount, directCall.expected.deliveryCount);
|
||||
assert.deepEqual(routeResult.deliveries, [
|
||||
{
|
||||
resourceId: directCall.target.resourceId,
|
||||
port: directCall.target.port,
|
||||
value: directCall.target.expectedValue,
|
||||
sourceResourceId: directCall.source.resourceId,
|
||||
sourcePort: directCall.source.port,
|
||||
observedAt: directCall.expected.observedAt
|
||||
}
|
||||
]);
|
||||
|
||||
applyDeliveries(localBoxes, routeResult.deliveries);
|
||||
|
||||
const targetState = localBoxes.get(directCall.target.resourceId).ports[directCall.target.port];
|
||||
assert.equal(targetState.value, directCall.target.expectedValue);
|
||||
assert.equal(targetState.propagatedBy, "hwlab-patch-panel");
|
||||
assert.equal(targetState.source, directCall.source.resourceId);
|
||||
assert.equal(targetState.sourcePort, directCall.source.port);
|
||||
|
||||
const bypassProbe = patchPanel.routeSignal({
|
||||
fromResourceId: directCall.source.resourceId,
|
||||
fromPort: "DI1",
|
||||
value: true
|
||||
});
|
||||
assert.equal(bypassProbe.deliveryCount, 0, "unwired direct box signal must not cross devices");
|
||||
|
||||
console.log("M3 hardware loop smoke passed");
|
||||
console.log("topology: 2 box simulators, 2 gateway simulators, 1 patch panel");
|
||||
console.log("wiring: box-simu-1 DO1 -> box-simu-2 DI1 via hwlab-patch-panel");
|
||||
console.log(`audit expectation: ${[...REQUIRED_AUDIT_FIELDS, ...RECOMMENDED_AUDIT_FIELDS].join(", ")}`);
|
||||
Reference in New Issue
Block a user