From 5583e4b3a69bb28197155b07adcf6bd6a8af21f7 Mon Sep 17 00:00:00 2001 From: HWLAB Code Queue Date: Thu, 21 May 2026 15:11:20 +0000 Subject: [PATCH] test: add m3 hardware loop smoke --- docs/m3-hardware-loop.md | 45 ++++ fixtures/mvp/m3-hardware-loop/README.md | 15 ++ fixtures/mvp/m3-hardware-loop/topology.json | 264 +++++++++++++++++++ scripts/m3-hardware-loop-smoke.mjs | 276 ++++++++++++++++++++ 4 files changed, 600 insertions(+) create mode 100644 docs/m3-hardware-loop.md create mode 100644 fixtures/mvp/m3-hardware-loop/README.md create mode 100644 fixtures/mvp/m3-hardware-loop/topology.json create mode 100644 scripts/m3-hardware-loop-smoke.mjs diff --git a/docs/m3-hardware-loop.md b/docs/m3-hardware-loop.md new file mode 100644 index 00000000..c0c608ab --- /dev/null +++ b/docs/m3-hardware-loop.md @@ -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. diff --git a/fixtures/mvp/m3-hardware-loop/README.md b/fixtures/mvp/m3-hardware-loop/README.md new file mode 100644 index 00000000..ee3e03f9 --- /dev/null +++ b/fixtures/mvp/m3-hardware-loop/README.md @@ -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. diff --git a/fixtures/mvp/m3-hardware-loop/topology.json b/fixtures/mvp/m3-hardware-loop/topology.json new file mode 100644 index 00000000..b9957d6b --- /dev/null +++ b/fixtures/mvp/m3-hardware-loop/topology.json @@ -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" + } +} diff --git a/scripts/m3-hardware-loop-smoke.mjs b/scripts/m3-hardware-loop-smoke.mjs new file mode 100644 index 00000000..9e320c30 --- /dev/null +++ b/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(", ")}`);