fix: harden M3 simulator identities and wiring
This commit is contained in:
@@ -8,12 +8,22 @@ import {
|
|||||||
import { createHealth, createBoxState, selectInstanceValue } from "../../internal/sim/model.mjs";
|
import { createHealth, createBoxState, selectInstanceValue } from "../../internal/sim/model.mjs";
|
||||||
import { createJsonServer, listen, parsePort, readJson } from "../../internal/sim/http.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(
|
const gatewaySessionId = selectInstanceValue(
|
||||||
process.env.HWLAB_GATEWAY_SESSION_ID,
|
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 port = parsePort(process.env.PORT, 7201);
|
||||||
const state = createBoxState({ boxId, gatewaySessionId });
|
const state = createBoxState({ boxId, gatewaySessionId });
|
||||||
if (resourceId) {
|
if (resourceId) {
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import {
|
|||||||
import { fetchJson, createJsonServer, listen, parsePort, readJson } from "../../internal/sim/http.mjs";
|
import { fetchJson, createJsonServer, listen, parsePort, readJson } from "../../internal/sim/http.mjs";
|
||||||
import { createRpcErrorBody, ERROR_CODES } from "../../internal/protocol/index.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 port = parsePort(process.env.PORT, 7101);
|
||||||
const endpoint = `http://127.0.0.1:${port}`;
|
const endpoint = `http://127.0.0.1:${port}`;
|
||||||
const boxUrls = parseBoxUrls(process.env.HWLAB_BOX_URLS);
|
const boxUrls = parseBoxUrls(process.env.HWLAB_BOX_URLS);
|
||||||
@@ -117,9 +120,9 @@ function parseList(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseBoxes({ resources, ids, urls, instanceScoped = false }) {
|
function parseBoxes({ resources, ids, urls, instanceScoped = false }) {
|
||||||
const explicitResources = instanceScoped ? selectScopedList(resources) : parseList(resources);
|
const explicitResources = instanceScoped ? selectScopedList(resources, "HWLAB_BOX_RESOURCES") : parseList(resources);
|
||||||
const explicitIds = instanceScoped ? selectScopedList(ids) : parseList(ids);
|
const explicitIds = instanceScoped ? selectScopedList(ids, "HWLAB_BOX_IDS") : parseList(ids);
|
||||||
const explicitUrls = instanceScoped ? selectScopedList(urls.join(",")) : urls;
|
const explicitUrls = instanceScoped ? selectScopedList(urls.join(","), "HWLAB_BOX_URLS") : urls;
|
||||||
const count = Math.max(explicitResources.length, explicitIds.length, explicitUrls.length, 1);
|
const count = Math.max(explicitResources.length, explicitIds.length, explicitUrls.length, 1);
|
||||||
const parsed = [];
|
const parsed = [];
|
||||||
|
|
||||||
@@ -140,12 +143,15 @@ function parseBoxes({ resources, ids, urls, instanceScoped = false }) {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectScopedList(value) {
|
function selectScopedList(value, label = "scoped list") {
|
||||||
const items = parseList(value);
|
const items = parseList(value);
|
||||||
if (items.length <= 1) {
|
if (items.length <= 1) {
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
const selected = selectInstanceValue(items.join(","), "");
|
const selected = selectInstanceValue(items.join(","), "", {
|
||||||
|
requireIndexForMultiple: true,
|
||||||
|
label
|
||||||
|
});
|
||||||
return selected ? [selected] : [];
|
return selected ? [selected] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ const topology = createM3Topology();
|
|||||||
const endpointMap = parseEndpointMap(process.env.HWLAB_PATCH_PANEL_ENDPOINT_MAP) ?? {
|
const endpointMap = parseEndpointMap(process.env.HWLAB_PATCH_PANEL_ENDPOINT_MAP) ?? {
|
||||||
res_boxsimu_2: "http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201"
|
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
|
const runtime = wiringConfigPath
|
||||||
? await createPatchPanelRuntimeFromFile({ path: wiringConfigPath })
|
? await createPatchPanelRuntimeFromFile({ path: wiringConfigPath })
|
||||||
: createPatchPanelRuntime({
|
: createPatchPanelRuntime({
|
||||||
wiringConfig: topology.wiringConfig,
|
wiringConfig: manifestWiringConfig ?? topology.wiringConfig,
|
||||||
endpointMap,
|
endpointMap,
|
||||||
configSource: "internal:m3-topology"
|
configSource: manifestWiringConfig ? "env:HWLAB_PATCH_PANEL_WIRING_CONFIG" : "internal:m3-topology"
|
||||||
});
|
});
|
||||||
|
|
||||||
const routes = new Map();
|
const routes = new Map();
|
||||||
@@ -51,3 +52,12 @@ function parseEndpointMap(value) {
|
|||||||
|
|
||||||
return JSON.parse(source);
|
return JSON.parse(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseWiringConfig(value) {
|
||||||
|
const source = String(value ?? "").trim();
|
||||||
|
if (!source) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(source);
|
||||||
|
}
|
||||||
|
|||||||
@@ -301,9 +301,17 @@
|
|||||||
"healthPath": "/health/live",
|
"healthPath": "/health/live",
|
||||||
"profile": "dev",
|
"profile": "dev",
|
||||||
"replicas": 1,
|
"replicas": 1,
|
||||||
|
"m3Route": {
|
||||||
|
"fromResourceId": "res_boxsimu_1",
|
||||||
|
"fromPort": "DO1",
|
||||||
|
"patchPanelServiceId": "hwlab-patch-panel",
|
||||||
|
"toResourceId": "res_boxsimu_2",
|
||||||
|
"toPort": "DI1"
|
||||||
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"HWLAB_PATCH_PANEL_MODE": "dev-skeleton",
|
"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_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"
|
"HWLAB_ENVIRONMENT": "dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -353,6 +353,33 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["indexed-workloads"]
|
"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": {
|
"env": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
|
|||||||
@@ -543,6 +543,10 @@
|
|||||||
{
|
{
|
||||||
"name": "HWLAB_PATCH_PANEL_ENDPOINT_MAP",
|
"name": "HWLAB_PATCH_PANEL_ENDPOINT_MAP",
|
||||||
"value": "{\"res_boxsimu_2\":\"http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201\"}"
|
"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": {
|
"readinessProbe": {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { createM3Topology } from "./fixture.mjs";
|
||||||
import { createWiringConfig } from "./model.mjs";
|
import { createWiringConfig } from "./model.mjs";
|
||||||
import { createPatchPanelRuntime } from "./runtime.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.equal(result.deliveryCount, 1);
|
||||||
assert.ok(result.diagnostics.some((diagnostic) => diagnostic.code === "target_endpoint_missing"));
|
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");
|
||||||
|
});
|
||||||
|
|||||||
+17
-3
@@ -92,7 +92,16 @@ function isoNow() {
|
|||||||
return new Date().toISOString();
|
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 ?? "")
|
const items = String(value ?? "")
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
@@ -110,12 +119,17 @@ export function selectInstanceValue(value, fallback, { index = process.env.HWLAB
|
|||||||
return items[parsedIndex];
|
return items[parsedIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
const hostname = process.env.HOSTNAME ?? "";
|
const ordinal = Number.parseInt(String(hostname ?? "").match(/-(\d+)$/)?.[1] ?? "", 10);
|
||||||
const ordinal = Number.parseInt(hostname.match(/-(\d+)$/)?.[1] ?? "", 10);
|
|
||||||
if (Number.isInteger(ordinal) && ordinal >= 0 && ordinal < items.length) {
|
if (Number.isInteger(ordinal) && ordinal >= 0 && ordinal < items.length) {
|
||||||
return items[ordinal];
|
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];
|
return items[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
);
|
||||||
|
});
|
||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"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",
|
"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",
|
"dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs",
|
||||||
"cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs",
|
"cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs",
|
||||||
"m1:smoke": "node scripts/m1-contract-smoke.mjs",
|
"m1:smoke": "node scripts/m1-contract-smoke.mjs",
|
||||||
|
|||||||
@@ -419,6 +419,21 @@ async function collectReadOnlySupplementalEvidence() {
|
|||||||
const patchPanelEndpointMap = patchPanel?.env.HWLAB_PATCH_PANEL_ENDPOINT_MAP
|
const patchPanelEndpointMap = patchPanel?.env.HWLAB_PATCH_PANEL_ENDPOINT_MAP
|
||||||
? JSON.parse(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 =
|
const boxReadyForM3 =
|
||||||
box?.kind === "StatefulSet" &&
|
box?.kind === "StatefulSet" &&
|
||||||
box?.replicas === 2 &&
|
box?.replicas === 2 &&
|
||||||
@@ -436,7 +451,13 @@ async function collectReadOnlySupplementalEvidence() {
|
|||||||
]);
|
]);
|
||||||
const patchPanelReadyForM3 =
|
const patchPanelReadyForM3 =
|
||||||
patchPanel?.replicas === 1 &&
|
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;
|
const manifestReadyForM3 = boxReadyForM3 && gatewayReadyForM3 && patchPanelReadyForM3 && hasInstanceServices;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -466,7 +487,8 @@ async function collectReadOnlySupplementalEvidence() {
|
|||||||
},
|
},
|
||||||
"hwlab-patch-panel": {
|
"hwlab-patch-panel": {
|
||||||
replicas: patchPanel?.replicas ?? null,
|
replicas: patchPanel?.replicas ?? null,
|
||||||
env: patchPanel?.env ?? null
|
env: patchPanel?.env ?? null,
|
||||||
|
firstClassWiringConfig: patchPanelWiringConfig
|
||||||
},
|
},
|
||||||
instanceServices: [...serviceMap.keys()].filter((name) =>
|
instanceServices: [...serviceMap.keys()].filter((name) =>
|
||||||
name.startsWith("hwlab-box-simu-") || name.startsWith("hwlab-gateway-simu-")
|
name.startsWith("hwlab-box-simu-") || name.startsWith("hwlab-gateway-simu-")
|
||||||
|
|||||||
@@ -144,9 +144,11 @@ function validateSource(ctx, manifest) {
|
|||||||
const cloudApi = servicesById.get("hwlab-cloud-api");
|
const cloudApi = servicesById.get("hwlab-cloud-api");
|
||||||
const tunnelClient = servicesById.get("hwlab-tunnel-client");
|
const tunnelClient = servicesById.get("hwlab-tunnel-client");
|
||||||
const cli = servicesById.get("hwlab-cli");
|
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");
|
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 ?? {});
|
validateCloudApiDbSource(ctx, cloudApi?.env ?? {});
|
||||||
validateCloudApiCodeAgentSource(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, 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_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");
|
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 };
|
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) {
|
function validateCloudApiDbSource(ctx, env) {
|
||||||
const alias = DEV_DB_ENV_CONTRACT.dns;
|
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");
|
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");
|
||||||
|
|||||||
@@ -155,7 +155,9 @@ assert.equal(deployServicesById.has("hwlab-agent-worker"), true);
|
|||||||
assert.equal(deployServicesById.has("hwlab-agent-skills"), true);
|
assert.equal(deployServicesById.has("hwlab-agent-skills"), true);
|
||||||
|
|
||||||
const cloudApi = deployServicesById.get("hwlab-cloud-api");
|
const cloudApi = deployServicesById.get("hwlab-cloud-api");
|
||||||
|
const patchPanel = deployServicesById.get("hwlab-patch-panel");
|
||||||
const cloudApiWorkloadEnv = getWorkloadEnv(k8sWorkloads, "hwlab-cloud-api");
|
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.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.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");
|
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");
|
assert.equal(cloudApi.env.OPENAI_API_KEY, codeAgentSecretRefPlaceholder(), "cloud-api Code Agent OpenAI key must be a Secret reference placeholder");
|
||||||
assertCodeAgentProviderWorkloadContract(cloudApiWorkloadEnv);
|
assertCodeAgentProviderWorkloadContract(cloudApiWorkloadEnv);
|
||||||
|
assertM3PatchPanelDeployContract(patchPanel, patchPanelWorkloadEnv);
|
||||||
assertDbForbiddenInvalidHostContract();
|
assertDbForbiddenInvalidHostContract();
|
||||||
assertValidationDoesNotExposeSecretValues(cloudApi.env, cloudApiWorkloadEnv);
|
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");
|
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() {
|
function assertDbForbiddenInvalidHostContract() {
|
||||||
const invalidHost = parseDbUrlContract("postgres://user:password@hwlab-dev-db.invalid:5432/hwlab");
|
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");
|
assert.equal(invalidHost.ok, false, "DEV DB contract must reject .invalid runtime hosts");
|
||||||
|
|||||||
@@ -9,6 +9,32 @@ const namespace = "hwlab-dev";
|
|||||||
const m3EndpointMap = Object.freeze({
|
const m3EndpointMap = Object.freeze({
|
||||||
res_boxsimu_2: "http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201"
|
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({
|
const expected = Object.freeze({
|
||||||
"hwlab-box-simu": {
|
"hwlab-box-simu": {
|
||||||
kind: "StatefulSet",
|
kind: "StatefulSet",
|
||||||
@@ -39,8 +65,16 @@ const expected = Object.freeze({
|
|||||||
"hwlab-patch-panel": {
|
"hwlab-patch-panel": {
|
||||||
kind: "Deployment",
|
kind: "Deployment",
|
||||||
replicas: 1,
|
replicas: 1,
|
||||||
|
m3Route: {
|
||||||
|
fromResourceId: "res_boxsimu_1",
|
||||||
|
fromPort: "DO1",
|
||||||
|
patchPanelServiceId: "hwlab-patch-panel",
|
||||||
|
toResourceId: "res_boxsimu_2",
|
||||||
|
toPort: "DI1"
|
||||||
|
},
|
||||||
env: {
|
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`);
|
assertSameMembers(service.instanceNames ?? [], requirement.instanceNames, `${serviceId} deploy instanceNames`);
|
||||||
assert.equal(service.identityStrategy, "indexed-workloads", `${serviceId} deploy identityStrategy`);
|
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)) {
|
for (const [name, values] of Object.entries(requirement.env)) {
|
||||||
if (Array.isArray(values)) {
|
if (Array.isArray(values)) {
|
||||||
@@ -245,6 +282,25 @@ function assertM3FixtureWiring(topology) {
|
|||||||
], "M3 fixture wiring config");
|
], "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([
|
const [deploy, workloads, services, topology] = await Promise.all([
|
||||||
readJson("deploy/deploy.json"),
|
readJson("deploy/deploy.json"),
|
||||||
readJson("deploy/k8s/base/workloads.yaml"),
|
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")
|
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);
|
assertDeployManifestCardinality(deploy);
|
||||||
assertDeployManifestInstanceMappings(deploy);
|
assertDeployManifestInstanceMappings(deploy);
|
||||||
assertDevK8sCardinality(workloads);
|
assertDevK8sCardinality(workloads);
|
||||||
assertDevK8sInstanceServices(services);
|
assertDevK8sInstanceServices(services);
|
||||||
assertM3FixtureWiring(topology);
|
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");
|
console.log("validated DEV M3 manifest cardinality: 2 distinct box-simu, 2 distinct gateway-simu, 1 patch-panel, M3 DO1->DI1 wiring");
|
||||||
|
|||||||
Reference in New Issue
Block a user