diff --git a/cmd/hwlab-box-simu/main.mjs b/cmd/hwlab-box-simu/main.mjs index 5920dd0a..05340bf6 100644 --- a/cmd/hwlab-box-simu/main.mjs +++ b/cmd/hwlab-box-simu/main.mjs @@ -8,12 +8,22 @@ import { import { createHealth, createBoxState, selectInstanceValue } from "../../internal/sim/model.mjs"; import { createJsonServer, listen, parsePort, readJson } from "../../internal/sim/http.mjs"; -const boxId = selectInstanceValue(process.env.HWLAB_BOX_ID, "boxsim_alpha"); +const boxId = selectInstanceValue(process.env.HWLAB_BOX_ID, "boxsim_alpha", { + requireIndexForMultiple: true, + label: "HWLAB_BOX_ID" +}); const gatewaySessionId = selectInstanceValue( process.env.HWLAB_GATEWAY_SESSION_ID, - "gws_mvp_simu" + "gws_mvp_simu", + { + requireIndexForMultiple: true, + label: "HWLAB_GATEWAY_SESSION_ID" + } ); -const resourceId = selectInstanceValue(process.env.HWLAB_BOX_RESOURCE_ID, ""); +const resourceId = selectInstanceValue(process.env.HWLAB_BOX_RESOURCE_ID, "", { + requireIndexForMultiple: true, + label: "HWLAB_BOX_RESOURCE_ID" +}); const port = parsePort(process.env.PORT, 7201); const state = createBoxState({ boxId, gatewaySessionId }); if (resourceId) { diff --git a/cmd/hwlab-gateway-simu/main.mjs b/cmd/hwlab-gateway-simu/main.mjs index 9840adbc..44da4d41 100644 --- a/cmd/hwlab-gateway-simu/main.mjs +++ b/cmd/hwlab-gateway-simu/main.mjs @@ -9,7 +9,10 @@ import { import { fetchJson, createJsonServer, listen, parsePort, readJson } from "../../internal/sim/http.mjs"; import { createRpcErrorBody, ERROR_CODES } from "../../internal/protocol/index.mjs"; -const gatewayId = selectInstanceValue(process.env.HWLAB_GATEWAY_ID, "gateway-a"); +const gatewayId = selectInstanceValue(process.env.HWLAB_GATEWAY_ID, "gateway-a", { + requireIndexForMultiple: true, + label: "HWLAB_GATEWAY_ID" +}); const port = parsePort(process.env.PORT, 7101); const endpoint = `http://127.0.0.1:${port}`; const boxUrls = parseBoxUrls(process.env.HWLAB_BOX_URLS); @@ -117,9 +120,9 @@ function parseList(value) { } function parseBoxes({ resources, ids, urls, instanceScoped = false }) { - const explicitResources = instanceScoped ? selectScopedList(resources) : parseList(resources); - const explicitIds = instanceScoped ? selectScopedList(ids) : parseList(ids); - const explicitUrls = instanceScoped ? selectScopedList(urls.join(",")) : urls; + const explicitResources = instanceScoped ? selectScopedList(resources, "HWLAB_BOX_RESOURCES") : parseList(resources); + const explicitIds = instanceScoped ? selectScopedList(ids, "HWLAB_BOX_IDS") : parseList(ids); + const explicitUrls = instanceScoped ? selectScopedList(urls.join(","), "HWLAB_BOX_URLS") : urls; const count = Math.max(explicitResources.length, explicitIds.length, explicitUrls.length, 1); const parsed = []; @@ -140,12 +143,15 @@ function parseBoxes({ resources, ids, urls, instanceScoped = false }) { return parsed; } -function selectScopedList(value) { +function selectScopedList(value, label = "scoped list") { const items = parseList(value); if (items.length <= 1) { return items; } - const selected = selectInstanceValue(items.join(","), ""); + const selected = selectInstanceValue(items.join(","), "", { + requireIndexForMultiple: true, + label + }); return selected ? [selected] : []; } diff --git a/cmd/hwlab-patch-panel/main.mjs b/cmd/hwlab-patch-panel/main.mjs index c83ef3fc..f62e1e9d 100644 --- a/cmd/hwlab-patch-panel/main.mjs +++ b/cmd/hwlab-patch-panel/main.mjs @@ -13,12 +13,13 @@ const topology = createM3Topology(); const endpointMap = parseEndpointMap(process.env.HWLAB_PATCH_PANEL_ENDPOINT_MAP) ?? { res_boxsimu_2: "http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201" }; +const manifestWiringConfig = parseWiringConfig(process.env.HWLAB_PATCH_PANEL_WIRING_CONFIG); const runtime = wiringConfigPath ? await createPatchPanelRuntimeFromFile({ path: wiringConfigPath }) : createPatchPanelRuntime({ - wiringConfig: topology.wiringConfig, + wiringConfig: manifestWiringConfig ?? topology.wiringConfig, endpointMap, - configSource: "internal:m3-topology" + configSource: manifestWiringConfig ? "env:HWLAB_PATCH_PANEL_WIRING_CONFIG" : "internal:m3-topology" }); const routes = new Map(); @@ -51,3 +52,12 @@ function parseEndpointMap(value) { return JSON.parse(source); } + +function parseWiringConfig(value) { + const source = String(value ?? "").trim(); + if (!source) { + return null; + } + + return JSON.parse(source); +} diff --git a/deploy/deploy.json b/deploy/deploy.json index 3c35e001..42b58aec 100644 --- a/deploy/deploy.json +++ b/deploy/deploy.json @@ -301,9 +301,17 @@ "healthPath": "/health/live", "profile": "dev", "replicas": 1, + "m3Route": { + "fromResourceId": "res_boxsimu_1", + "fromPort": "DO1", + "patchPanelServiceId": "hwlab-patch-panel", + "toResourceId": "res_boxsimu_2", + "toPort": "DI1" + }, "env": { "HWLAB_PATCH_PANEL_MODE": "dev-skeleton", "HWLAB_PATCH_PANEL_ENDPOINT_MAP": "{\"res_boxsimu_2\":\"http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201\"}", + "HWLAB_PATCH_PANEL_WIRING_CONFIG": "{\"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\"}", "HWLAB_ENVIRONMENT": "dev" } }, diff --git a/deploy/deploy.schema.json b/deploy/deploy.schema.json index 96487245..dad77c95 100644 --- a/deploy/deploy.schema.json +++ b/deploy/deploy.schema.json @@ -353,6 +353,33 @@ "type": "string", "enum": ["indexed-workloads"] }, + "m3Route": { + "type": "object", + "required": ["fromResourceId", "fromPort", "patchPanelServiceId", "toResourceId", "toPort"], + "properties": { + "fromResourceId": { + "type": "string", + "const": "res_boxsimu_1" + }, + "fromPort": { + "type": "string", + "const": "DO1" + }, + "patchPanelServiceId": { + "type": "string", + "const": "hwlab-patch-panel" + }, + "toResourceId": { + "type": "string", + "const": "res_boxsimu_2" + }, + "toPort": { + "type": "string", + "const": "DI1" + } + }, + "additionalProperties": false + }, "env": { "type": "object", "additionalProperties": { diff --git a/deploy/k8s/base/workloads.yaml b/deploy/k8s/base/workloads.yaml index 0a9f88fa..6022ca45 100644 --- a/deploy/k8s/base/workloads.yaml +++ b/deploy/k8s/base/workloads.yaml @@ -543,6 +543,10 @@ { "name": "HWLAB_PATCH_PANEL_ENDPOINT_MAP", "value": "{\"res_boxsimu_2\":\"http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201\"}" + }, + { + "name": "HWLAB_PATCH_PANEL_WIRING_CONFIG", + "value": "{\"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\"}" } ], "readinessProbe": { diff --git a/internal/patchpanel/runtime.test.mjs b/internal/patchpanel/runtime.test.mjs index bb6282a3..1082c582 100644 --- a/internal/patchpanel/runtime.test.mjs +++ b/internal/patchpanel/runtime.test.mjs @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; +import { createM3Topology } from "./fixture.mjs"; import { createWiringConfig } from "./model.mjs"; import { createPatchPanelRuntime } from "./runtime.mjs"; @@ -110,3 +111,55 @@ test("runtime reports missing target endpoint as readable diagnostics", async () assert.equal(result.deliveryCount, 1); assert.ok(result.diagnostics.some((diagnostic) => diagnostic.code === "target_endpoint_missing")); }); + +test("runtime applies first-class DEV M3 wiring config for res_boxsimu_1 DO1 to res_boxsimu_2 DI1", async () => { + const topology = createM3Topology(); + const writes = []; + const runtime = createPatchPanelRuntime({ + wiringConfig: topology.wiringConfig, + endpointMap: { + res_boxsimu_2: "http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201" + }, + configSource: "env:HWLAB_PATCH_PANEL_WIRING_CONFIG", + now: () => "2026-05-21T00:00:01.000Z", + requestJson: async (url, body) => { + writes.push({ url, body }); + return { + status: 200, + json: { + accepted: true + } + }; + } + }); + + assert.equal(runtime.status().state, "active"); + assert.equal(runtime.status().metadata.configSource, "env:HWLAB_PATCH_PANEL_WIRING_CONFIG"); + assert.deepEqual(runtime.status().activeConnections, [ + { + fromResourceId: "res_boxsimu_1", + fromPort: "DO1", + toResourceId: "res_boxsimu_2", + toPort: "DI1" + } + ]); + + const result = await runtime.routeSignal({ + fromResourceId: "res_boxsimu_1", + fromPort: "DO1", + value: true + }); + + assert.equal(result.accepted, true); + assert.equal(result.propagatedBy, "hwlab-patch-panel"); + assert.equal(result.deliveryCount, 1); + assert.equal(writes.length, 1); + assert.equal( + writes[0].url, + "http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201/internal/ports/write" + ); + assert.equal(writes[0].body.port, "DI1"); + assert.equal(writes[0].body.sourceResourceId, "res_boxsimu_1"); + assert.equal(writes[0].body.sourcePort, "DO1"); + assert.equal(writes[0].body.propagatedBy, "hwlab-patch-panel"); +}); diff --git a/internal/sim/model.mjs b/internal/sim/model.mjs index 53c0e5de..1b070246 100644 --- a/internal/sim/model.mjs +++ b/internal/sim/model.mjs @@ -92,7 +92,16 @@ function isoNow() { return new Date().toISOString(); } -export function selectInstanceValue(value, fallback, { index = process.env.HWLAB_INSTANCE_INDEX } = {}) { +export function selectInstanceValue( + value, + fallback, + { + index = process.env.HWLAB_INSTANCE_INDEX, + hostname = process.env.HOSTNAME, + requireIndexForMultiple = false, + label = "instance value" + } = {} +) { const items = String(value ?? "") .split(",") .map((item) => item.trim()) @@ -110,12 +119,17 @@ export function selectInstanceValue(value, fallback, { index = process.env.HWLAB return items[parsedIndex]; } - const hostname = process.env.HOSTNAME ?? ""; - const ordinal = Number.parseInt(hostname.match(/-(\d+)$/)?.[1] ?? "", 10); + const ordinal = Number.parseInt(String(hostname ?? "").match(/-(\d+)$/)?.[1] ?? "", 10); if (Number.isInteger(ordinal) && ordinal >= 0 && ordinal < items.length) { return items[ordinal]; } + if (requireIndexForMultiple) { + throw new Error( + `${label} declares ${items.length} indexed values, but neither HWLAB_INSTANCE_INDEX nor a StatefulSet ordinal HOSTNAME selected one` + ); + } + return items[0]; } diff --git a/internal/sim/model.test.mjs b/internal/sim/model.test.mjs new file mode 100644 index 00000000..f975a77c --- /dev/null +++ b/internal/sim/model.test.mjs @@ -0,0 +1,53 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { selectInstanceValue } from "./model.mjs"; + +test("indexed simulator identity selection uses explicit HWLAB_INSTANCE_INDEX", () => { + assert.equal( + selectInstanceValue("boxsimu_1,boxsimu_2", "fallback", { + index: "1", + hostname: "deployment-shaped-pod", + requireIndexForMultiple: true, + label: "HWLAB_BOX_ID" + }), + "boxsimu_2" + ); +}); + +test("indexed simulator identity selection uses StatefulSet hostname ordinal", () => { + assert.equal( + selectInstanceValue("res_boxsimu_1,res_boxsimu_2", "fallback", { + index: "", + hostname: "hwlab-box-simu-1", + requireIndexForMultiple: true, + label: "HWLAB_BOX_RESOURCE_ID" + }), + "res_boxsimu_2" + ); +}); + +test("multi-value simulator identities fail closed without an instance index", () => { + assert.throws( + () => + selectInstanceValue("gwsimu_1,gwsimu_2", "fallback", { + index: "", + hostname: "hwlab-gateway-simu-random-suffix", + requireIndexForMultiple: true, + label: "HWLAB_GATEWAY_ID" + }), + /HWLAB_GATEWAY_ID declares 2 indexed values/u + ); +}); + +test("single simulator identity value still works without an index", () => { + assert.equal( + selectInstanceValue("boxsimu_1", "fallback", { + index: "", + hostname: "deployment-shaped-pod", + requireIndexForMultiple: true, + label: "HWLAB_BOX_ID" + }), + "boxsimu_1" + ); +}); diff --git a/package.json b/package.json index 68a9358f..8ce95052 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "validate": "node scripts/validate-contract.mjs && node scripts/deploy-contract-plan.mjs --check && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-migration.mjs --check && node --test scripts/artifact-runtime-readiness-guard.test.mjs", - "check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-migration.mjs --check && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node --test scripts/artifact-runtime-readiness-guard.test.mjs scripts/dev-m3-hardware-loop-smoke.test.mjs scripts/dev-cloud-workbench-smoke.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/src/dev-deploy-apply.test.mjs scripts/src/dev-runtime-migration.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/db/runtime-store.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", + "check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-migration.mjs --check && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node --test scripts/artifact-runtime-readiness-guard.test.mjs scripts/dev-m3-hardware-loop-smoke.test.mjs scripts/dev-cloud-workbench-smoke.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/src/dev-deploy-apply.test.mjs scripts/src/dev-runtime-migration.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/db/runtime-store.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs internal/sim/model.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", "dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs", "cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs", "m1:smoke": "node scripts/m1-contract-smoke.mjs", diff --git a/scripts/dev-m3-hardware-loop-smoke.mjs b/scripts/dev-m3-hardware-loop-smoke.mjs index 615079bd..27f7ba96 100644 --- a/scripts/dev-m3-hardware-loop-smoke.mjs +++ b/scripts/dev-m3-hardware-loop-smoke.mjs @@ -419,6 +419,21 @@ async function collectReadOnlySupplementalEvidence() { const patchPanelEndpointMap = patchPanel?.env.HWLAB_PATCH_PANEL_ENDPOINT_MAP ? JSON.parse(patchPanel.env.HWLAB_PATCH_PANEL_ENDPOINT_MAP) : {}; + const patchPanelWiringConfig = patchPanel?.env.HWLAB_PATCH_PANEL_WIRING_CONFIG + ? JSON.parse(patchPanel.env.HWLAB_PATCH_PANEL_WIRING_CONFIG) + : null; + const patchPanelHasManifestRoute = + patchPanelWiringConfig?.status === "active" && + patchPanelWiringConfig?.constraints?.propagation === "patch-panel-only" && + patchPanelWiringConfig?.constraints?.localLoopbackSubstituteAllowed === false && + (patchPanelWiringConfig?.connections ?? []).some( + (connection) => + connection.from?.resourceId === requiredM3Connection.fromResourceId && + connection.from?.port === requiredM3Connection.fromPort && + connection.to?.resourceId === requiredM3Connection.toResourceId && + connection.to?.port === requiredM3Connection.toPort && + connection.mode === "exclusive" + ); const boxReadyForM3 = box?.kind === "StatefulSet" && box?.replicas === 2 && @@ -436,7 +451,13 @@ async function collectReadOnlySupplementalEvidence() { ]); const patchPanelReadyForM3 = patchPanel?.replicas === 1 && - patchPanelEndpointMap.res_boxsimu_2 === "http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201"; + patchPanel?.m3Route?.fromResourceId === requiredM3Connection.fromResourceId && + patchPanel?.m3Route?.fromPort === requiredM3Connection.fromPort && + patchPanel?.m3Route?.patchPanelServiceId === "hwlab-patch-panel" && + patchPanel?.m3Route?.toResourceId === requiredM3Connection.toResourceId && + patchPanel?.m3Route?.toPort === requiredM3Connection.toPort && + patchPanelEndpointMap.res_boxsimu_2 === "http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201" && + patchPanelHasManifestRoute; const manifestReadyForM3 = boxReadyForM3 && gatewayReadyForM3 && patchPanelReadyForM3 && hasInstanceServices; return [ @@ -466,7 +487,8 @@ async function collectReadOnlySupplementalEvidence() { }, "hwlab-patch-panel": { replicas: patchPanel?.replicas ?? null, - env: patchPanel?.env ?? null + env: patchPanel?.env ?? null, + firstClassWiringConfig: patchPanelWiringConfig }, instanceServices: [...serviceMap.keys()].filter((name) => name.startsWith("hwlab-box-simu-") || name.startsWith("hwlab-gateway-simu-") diff --git a/scripts/src/deploy-contract-plan.mjs b/scripts/src/deploy-contract-plan.mjs index 26821d64..e2301726 100644 --- a/scripts/src/deploy-contract-plan.mjs +++ b/scripts/src/deploy-contract-plan.mjs @@ -144,9 +144,11 @@ function validateSource(ctx, manifest) { const cloudApi = servicesById.get("hwlab-cloud-api"); const tunnelClient = servicesById.get("hwlab-tunnel-client"); const cli = servicesById.get("hwlab-cli"); + const patchPanel = servicesById.get("hwlab-patch-panel"); expectEqual(ctx, cloudApi?.env?.HWLAB_PUBLIC_ENDPOINT, endpoints.api?.url, "$.services.hwlab-cloud-api.env.HWLAB_PUBLIC_ENDPOINT", "cloud API public endpoint env"); validateCloudApiDbSource(ctx, cloudApi?.env ?? {}); validateCloudApiCodeAgentSource(ctx, cloudApi?.env ?? {}); + validateM3PatchPanelSource(ctx, patchPanel); expectEqual(ctx, cli?.env?.HWLAB_CLI_ENDPOINT, endpoints.api?.url, "$.services.hwlab-cli.env.HWLAB_CLI_ENDPOINT", "CLI endpoint env"); expectEqual(ctx, tunnelClient?.env?.HWLAB_FRP_PUBLIC_PORT, String(expectedPorts.api), "$.services.hwlab-tunnel-client.env.HWLAB_FRP_PUBLIC_PORT", "tunnel API public port env"); expectEqual(ctx, tunnelClient?.env?.HWLAB_FRP_WEB_PUBLIC_PORT, String(expectedPorts.frontend), "$.services.hwlab-tunnel-client.env.HWLAB_FRP_WEB_PUBLIC_PORT", "tunnel frontend public port env"); @@ -189,6 +191,78 @@ function validateSource(ctx, manifest) { return { manifest, endpoints, servicesById, proxies, serviceMappings, healthPath: manifest.health?.path }; } +function requiredM3Route() { + return { + fromResourceId: "res_boxsimu_1", + fromPort: "DO1", + patchPanelServiceId: "hwlab-patch-panel", + toResourceId: "res_boxsimu_2", + toPort: "DI1" + }; +} + +function parseJsonEnv(ctx, value, jsonPath, label) { + try { + return JSON.parse(String(value ?? "")); + } catch (error) { + addDiagnostic(ctx, "invalid_json", jsonPath, `${label} must be JSON`, { + actual: error instanceof Error ? error.message : String(error) + }); + return null; + } +} + +function validateM3PatchPanelSource(ctx, patchPanel) { + const route = requiredM3Route(); + expectEqual(ctx, patchPanel?.m3Route?.fromResourceId, route.fromResourceId, "$.services.hwlab-patch-panel.m3Route.fromResourceId", "patch panel first-class M3 source resource"); + expectEqual(ctx, patchPanel?.m3Route?.fromPort, route.fromPort, "$.services.hwlab-patch-panel.m3Route.fromPort", "patch panel first-class M3 source port"); + expectEqual(ctx, patchPanel?.m3Route?.patchPanelServiceId, route.patchPanelServiceId, "$.services.hwlab-patch-panel.m3Route.patchPanelServiceId", "patch panel first-class M3 route owner"); + expectEqual(ctx, patchPanel?.m3Route?.toResourceId, route.toResourceId, "$.services.hwlab-patch-panel.m3Route.toResourceId", "patch panel first-class M3 target resource"); + expectEqual(ctx, patchPanel?.m3Route?.toPort, route.toPort, "$.services.hwlab-patch-panel.m3Route.toPort", "patch panel first-class M3 target port"); + + const endpointMap = parseJsonEnv( + ctx, + patchPanel?.env?.HWLAB_PATCH_PANEL_ENDPOINT_MAP, + "$.services.hwlab-patch-panel.env.HWLAB_PATCH_PANEL_ENDPOINT_MAP", + "patch panel endpoint map" + ); + expectEqual( + ctx, + endpointMap?.res_boxsimu_2, + "http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201", + "$.services.hwlab-patch-panel.env.HWLAB_PATCH_PANEL_ENDPOINT_MAP.res_boxsimu_2", + "patch panel M3 target endpoint" + ); + + const wiring = parseJsonEnv( + ctx, + patchPanel?.env?.HWLAB_PATCH_PANEL_WIRING_CONFIG, + "$.services.hwlab-patch-panel.env.HWLAB_PATCH_PANEL_WIRING_CONFIG", + "patch panel wiring config" + ); + const connection = wiring?.connections?.[0]; + expectEqual(ctx, wiring?.status, "active", "$.services.hwlab-patch-panel.env.HWLAB_PATCH_PANEL_WIRING_CONFIG.status", "patch panel M3 wiring status"); + expectEqual( + ctx, + wiring?.constraints?.propagation, + "patch-panel-only", + "$.services.hwlab-patch-panel.env.HWLAB_PATCH_PANEL_WIRING_CONFIG.constraints.propagation", + "patch panel M3 wiring propagation" + ); + expectEqual( + ctx, + wiring?.constraints?.localLoopbackSubstituteAllowed, + false, + "$.services.hwlab-patch-panel.env.HWLAB_PATCH_PANEL_WIRING_CONFIG.constraints.localLoopbackSubstituteAllowed", + "patch panel M3 wiring forbids loopback substitute" + ); + expectEqual(ctx, connection?.from?.resourceId, route.fromResourceId, "$.services.hwlab-patch-panel.env.HWLAB_PATCH_PANEL_WIRING_CONFIG.connections[0].from.resourceId", "patch panel M3 source resource"); + expectEqual(ctx, connection?.from?.port, route.fromPort, "$.services.hwlab-patch-panel.env.HWLAB_PATCH_PANEL_WIRING_CONFIG.connections[0].from.port", "patch panel M3 source port"); + expectEqual(ctx, connection?.to?.resourceId, route.toResourceId, "$.services.hwlab-patch-panel.env.HWLAB_PATCH_PANEL_WIRING_CONFIG.connections[0].to.resourceId", "patch panel M3 target resource"); + expectEqual(ctx, connection?.to?.port, route.toPort, "$.services.hwlab-patch-panel.env.HWLAB_PATCH_PANEL_WIRING_CONFIG.connections[0].to.port", "patch panel M3 target port"); + expectEqual(ctx, connection?.mode, "exclusive", "$.services.hwlab-patch-panel.env.HWLAB_PATCH_PANEL_WIRING_CONFIG.connections[0].mode", "patch panel M3 route mode"); +} + function validateCloudApiDbSource(ctx, env) { const alias = DEV_DB_ENV_CONTRACT.dns; expectEqual(ctx, env.HWLAB_CLOUD_DB_URL, "secretRef:hwlab-cloud-api-dev-db/database-url", "$.services.hwlab-cloud-api.env.HWLAB_CLOUD_DB_URL", "cloud API DB URL Secret reference"); diff --git a/scripts/validate-contract.mjs b/scripts/validate-contract.mjs index 8c093a59..4d0408cb 100644 --- a/scripts/validate-contract.mjs +++ b/scripts/validate-contract.mjs @@ -155,7 +155,9 @@ assert.equal(deployServicesById.has("hwlab-agent-worker"), true); assert.equal(deployServicesById.has("hwlab-agent-skills"), true); const cloudApi = deployServicesById.get("hwlab-cloud-api"); +const patchPanel = deployServicesById.get("hwlab-patch-panel"); const cloudApiWorkloadEnv = getWorkloadEnv(k8sWorkloads, "hwlab-cloud-api"); +const patchPanelWorkloadEnv = getWorkloadEnv(k8sWorkloads, "hwlab-patch-panel"); assert.equal(deployManifest.health.path, "/health/live", "deploy health source path"); assert.equal(deployManifest.publicEndpoints.frontend.url, "http://74.48.78.17:16666", "deploy frontend endpoint"); assert.equal(deployManifest.publicEndpoints.api.url, "http://74.48.78.17:16667", "deploy api endpoint"); @@ -208,6 +210,7 @@ assert.notEqual( ); assert.equal(cloudApi.env.OPENAI_API_KEY, codeAgentSecretRefPlaceholder(), "cloud-api Code Agent OpenAI key must be a Secret reference placeholder"); assertCodeAgentProviderWorkloadContract(cloudApiWorkloadEnv); +assertM3PatchPanelDeployContract(patchPanel, patchPanelWorkloadEnv); assertDbForbiddenInvalidHostContract(); assertValidationDoesNotExposeSecretValues(cloudApi.env, cloudApiWorkloadEnv); @@ -262,6 +265,49 @@ function assertCodeAgentProviderWorkloadContract(env) { assert.equal(apiKey?.key, secretRef.secretKey, "cloud-api workload Code Agent OpenAI Secret key"); } +function assertM3PatchPanelDeployContract(manifestService, workloadEnv) { + const route = { + fromResourceId: "res_boxsimu_1", + fromPort: "DO1", + patchPanelServiceId: "hwlab-patch-panel", + toResourceId: "res_boxsimu_2", + toPort: "DI1" + }; + assert.deepEqual(manifestService.m3Route, route, "patch-panel deploy manifest first-class M3 route"); + const manifestEndpointMap = JSON.parse(manifestService.env.HWLAB_PATCH_PANEL_ENDPOINT_MAP); + const workloadEndpointMap = JSON.parse(workloadEnv.HWLAB_PATCH_PANEL_ENDPOINT_MAP?.value ?? "{}"); + assert.equal( + manifestEndpointMap.res_boxsimu_2, + "http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201", + "patch-panel deploy manifest M3 endpoint" + ); + assert.deepEqual(workloadEndpointMap, manifestEndpointMap, "patch-panel workload M3 endpoint map"); + + const manifestWiring = JSON.parse(manifestService.env.HWLAB_PATCH_PANEL_WIRING_CONFIG); + const workloadWiring = JSON.parse(workloadEnv.HWLAB_PATCH_PANEL_WIRING_CONFIG?.value ?? "{}"); + assert.deepEqual(workloadWiring, manifestWiring, "patch-panel workload first-class M3 wiring config"); + assert.equal(manifestWiring.status, "active", "patch-panel M3 wiring status"); + assert.equal(manifestWiring.constraints.propagation, "patch-panel-only", "patch-panel M3 wiring propagation"); + assert.equal( + manifestWiring.constraints.localLoopbackSubstituteAllowed, + false, + "patch-panel M3 wiring loopback substitute" + ); + assert.deepEqual(manifestWiring.connections, [ + { + from: { + resourceId: route.fromResourceId, + port: route.fromPort + }, + to: { + resourceId: route.toResourceId, + port: route.toPort + }, + mode: "exclusive" + } + ], "patch-panel M3 wiring route"); +} + function assertDbForbiddenInvalidHostContract() { const invalidHost = parseDbUrlContract("postgres://user:password@hwlab-dev-db.invalid:5432/hwlab"); assert.equal(invalidHost.ok, false, "DEV DB contract must reject .invalid runtime hosts"); diff --git a/scripts/validate-dev-m3-cardinality.mjs b/scripts/validate-dev-m3-cardinality.mjs index a730e043..6f670314 100644 --- a/scripts/validate-dev-m3-cardinality.mjs +++ b/scripts/validate-dev-m3-cardinality.mjs @@ -9,6 +9,32 @@ const namespace = "hwlab-dev"; const m3EndpointMap = Object.freeze({ res_boxsimu_2: "http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201" }); +const m3WiringConfig = Object.freeze({ + 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" +}); const expected = Object.freeze({ "hwlab-box-simu": { kind: "StatefulSet", @@ -39,8 +65,16 @@ const expected = Object.freeze({ "hwlab-patch-panel": { kind: "Deployment", replicas: 1, + m3Route: { + fromResourceId: "res_boxsimu_1", + fromPort: "DO1", + patchPanelServiceId: "hwlab-patch-panel", + toResourceId: "res_boxsimu_2", + toPort: "DI1" + }, env: { - HWLAB_PATCH_PANEL_ENDPOINT_MAP: m3EndpointMap + HWLAB_PATCH_PANEL_ENDPOINT_MAP: m3EndpointMap, + HWLAB_PATCH_PANEL_WIRING_CONFIG: m3WiringConfig } } }); @@ -197,6 +231,9 @@ function assertDeployManifestCardinality(deploy) { assertSameMembers(service.instanceNames ?? [], requirement.instanceNames, `${serviceId} deploy instanceNames`); assert.equal(service.identityStrategy, "indexed-workloads", `${serviceId} deploy identityStrategy`); } + if (requirement.m3Route) { + assert.deepEqual(service.m3Route, requirement.m3Route, `${serviceId} deploy first-class M3 route`); + } for (const [name, values] of Object.entries(requirement.env)) { if (Array.isArray(values)) { @@ -245,6 +282,25 @@ function assertM3FixtureWiring(topology) { ], "M3 fixture wiring config"); } +function assertM3RouteConfig(value, label) { + assert.deepEqual(value, m3WiringConfig, label); + assert.deepEqual(value.connections, [ + { + from: { + resourceId: "res_boxsimu_1", + port: "DO1" + }, + to: { + resourceId: "res_boxsimu_2", + port: "DI1" + }, + mode: "exclusive" + } + ], `${label} required route`); + assert.equal(value.constraints.propagation, "patch-panel-only", `${label} propagation`); + assert.equal(value.constraints.localLoopbackSubstituteAllowed, false, `${label} loopback substitute`); +} + const [deploy, workloads, services, topology] = await Promise.all([ readJson("deploy/deploy.json"), readJson("deploy/k8s/base/workloads.yaml"), @@ -252,10 +308,25 @@ const [deploy, workloads, services, topology] = await Promise.all([ readJson("fixtures/mvp/m3-hardware-loop/topology.json") ]); +const patchPanelWorkloadEnv = envMap( + workloadByName(workloads) + .get("hwlab-patch-panel") + ?.spec?.template?.spec?.containers?.find((item) => item.name === "hwlab-patch-panel") + ?.env +); + assertDeployManifestCardinality(deploy); assertDeployManifestInstanceMappings(deploy); assertDevK8sCardinality(workloads); assertDevK8sInstanceServices(services); assertM3FixtureWiring(topology); +assertM3RouteConfig( + parseEnvValue(patchPanelWorkloadEnv.HWLAB_PATCH_PANEL_WIRING_CONFIG), + "DEV workload first-class M3 patch-panel wiring config" +); +assertM3RouteConfig( + parseEnvValue(deploy.services.find((service) => service.serviceId === "hwlab-patch-panel")?.env?.HWLAB_PATCH_PANEL_WIRING_CONFIG), + "deploy manifest first-class M3 patch-panel wiring config" +); console.log("validated DEV M3 manifest cardinality: 2 distinct box-simu, 2 distinct gateway-simu, 1 patch-panel, M3 DO1->DI1 wiring");