import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { mkdir, readFile, rm } from "node:fs/promises"; import path from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; import { candidateTargetsFromKubernetes, classifyDirectTargetBlockerScope, createLiveOperationPlan, hasRequiredM3Connection, kubernetesServiceIdentity, parseArgs, requireSafetyGates } from "./dev-m3-hardware-loop-smoke.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const testOutputDir = "tmp/dev-m3-hardware-loop-smoke-test"; function runSmoke(args) { return execFileSync("node", ["scripts/dev-m3-hardware-loop-smoke.mjs", ...args], { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); } test("default M3 smoke is source/read-only and never claims DEV-LIVE", async () => { await mkdir(path.join(repoRoot, testOutputDir), { recursive: true }); const output = path.join(testOutputDir, "default-source-readonly.json"); try { const stdout = runSmoke(["--output", output]); assert.match(stdout, /mode=dry-run/u); const report = JSON.parse(await readFile(path.join(repoRoot, output), "utf8")); assert.equal(report.dryRunPlan.mode, "plan-only"); assert.equal(report.dryRunPlan.dryRunCallsLiveEndpoints, false); assert.equal(report.dryRunPlan.liveWriteWillRun, false); assert.equal(report.liveOperation.status, "not_run"); assert.equal(report.liveOperation.operationId, "not_observed"); assert.equal(report.summary.status, "blocked"); assert.match(report.summary.result, /did not call DEV endpoints/u); assert.equal( report.liveChecks.find((check) => check.id === "direct-call-do-write-di-read")?.status, "not_run" ); } finally { await rm(path.join(repoRoot, output), { force: true }); } }); test("--dry-run aliases the same source/read-only no-write path", async () => { await mkdir(path.join(repoRoot, testOutputDir), { recursive: true }); const output = path.join(testOutputDir, "dry-run-source-readonly.json"); try { runSmoke(["--dry-run", "--output", output]); const report = JSON.parse(await readFile(path.join(repoRoot, output), "utf8")); assert.equal(report.dryRunPlan.mode, "plan-only"); assert.equal(report.dryRunPlan.liveWriteWillRun, false); assert.ok( report.dryRunPlan.failureClassifications.includes("patch_panel_wiring_missing") ); assert.equal(report.liveOperation.status, "not_run"); } finally { await rm(path.join(repoRoot, output), { force: true }); } }); test("M3 safety gates keep incomplete live commands source/read-only", () => { assert.deepEqual(requireSafetyGates(parseArgs([])), { mode: "dry-run", liveWriteAllowed: false }); assert.deepEqual(requireSafetyGates(parseArgs(["--live", "--confirm-dev"])), { mode: "dry-run", liveWriteAllowed: false }); assert.deepEqual(requireSafetyGates(parseArgs(["--dry-run", "--live", "--confirm-dev", "--expect-non-prod"])), { mode: "dry-run", liveWriteAllowed: false }); assert.deepEqual(requireSafetyGates(parseArgs(["--live", "--confirm-dev", "--expect-non-prod"])), { mode: "live", liveWriteAllowed: true }); assert.deepEqual(requireSafetyGates(parseArgs(["--live", "--confirm-dev", "--confirmed-non-production"])), { mode: "live", liveWriteAllowed: true }); }); test("M3 blocker scopes classify missing identities and patch-panel route gaps", () => { assert.equal(classifyDirectTargetBlockerScope("m3-service-discovery"), "target_missing"); assert.equal(classifyDirectTargetBlockerScope("m3-direct-target-missing"), "target_missing"); assert.equal(classifyDirectTargetBlockerScope("m3-box-simu-identity"), "identity_not_distinct"); assert.equal(classifyDirectTargetBlockerScope("m3-gateway-simu-identity"), "identity_not_distinct"); assert.equal(classifyDirectTargetBlockerScope("m3-patch-panel-wiring"), "patch_panel_wiring_missing"); const plan = createLiveOperationPlan(); assert.equal(plan.evidenceLevel, "DRY-RUN"); assert.equal(plan.liveWriteWillRun, false); assert.ok(plan.failureClassifications.includes("identity_not_distinct")); assert.ok(plan.failureClassifications.includes("patch_panel_wiring_missing")); assert.match(plan.refusalPolicy.join("\n"), /source\/read-only/u); }); test("patch-panel route preflight rejects missing res_boxsimu_1 DO1 to res_boxsimu_2 DI1 wiring", () => { const wrongRoute = hasRequiredM3Connection({ status: { activeConnections: [ { fromResourceId: "res_boxsimu_1", fromPort: "DO1", toResourceId: "res_boxsimu_1", toPort: "DI1" } ] }, wiring: { connections: [ { from: { resourceId: "res_boxsimu_1", port: "DO1" }, to: { resourceId: "res_boxsimu_1", port: "DI1" }, mode: "exclusive" } ] } }); assert.equal(Boolean(wrongRoute.active), false); assert.equal(Boolean(wrongRoute.configured), false); const requiredRoute = hasRequiredM3Connection({ status: { activeConnections: [ { fromResourceId: "res_boxsimu_1", fromPort: "DO1", toResourceId: "res_boxsimu_2", toPort: "DI1" } ] }, wiring: { connections: [ { from: { resourceId: "res_boxsimu_1", port: "DO1" }, to: { resourceId: "res_boxsimu_2", port: "DI1" }, mode: "exclusive" } ] } }); assert.equal(Boolean(requiredRoute.active), true); assert.equal(Boolean(requiredRoute.configured), true); }); test("Kubernetes discovery preserves current indexed M3 service identities", () => { const endpointSlice = (serviceName, address, port, targetRef, labels = {}) => ({ metadata: { name: `${serviceName}-slice`, labels: { "kubernetes.io/service-name": serviceName, ...labels } }, ports: [{ name: "http", port }], endpoints: [ { addresses: [address], conditions: { ready: true }, targetRef: { name: targetRef } } ] }); const discovery = { status: "observed", source: "kubernetes-endpointslices", services: Object.fromEntries( [ endpointSlice("hwlab-box-simu-1", "10.0.0.11", 7201, "hwlab-box-simu-0", { "hwlab.pikastech.local/instance-id": "res_boxsimu_1" }), endpointSlice("hwlab-box-simu-2", "10.0.0.12", 7201, "hwlab-box-simu-1", { "hwlab.pikastech.local/instance-id": "res_boxsimu_2" }), endpointSlice("hwlab-gateway-simu-1", "10.0.0.21", 7101, "hwlab-gateway-simu-0", { "hwlab.pikastech.local/instance-id": "gwsimu_1" }), endpointSlice("hwlab-gateway-simu-2", "10.0.0.22", 7101, "hwlab-gateway-simu-1", { "hwlab.pikastech.local/instance-id": "gwsimu_2" }), endpointSlice("hwlab-patch-panel", "10.0.0.31", 7301, "hwlab-patch-panel-abc") ].map((item) => { const identity = kubernetesServiceIdentity(item); const port = item.ports[0].port; const endpoints = item.endpoints.flatMap((endpoint) => endpoint.addresses.map((address) => ({ url: `http://${address}:${port}`, address, port, targetRef: endpoint.targetRef.name, serviceName: identity.serviceName, serviceId: identity.serviceId, targetId: identity.targetId, instanceId: identity.instanceId })) ); return [ identity.serviceName, { endpointSlice: item.metadata.name, ...identity, port, count: endpoints.length, endpoints } ]; }) ) }; const candidates = candidateTargetsFromKubernetes(discovery); assert.deepEqual( candidates.map((candidate) => [ candidate.id, candidate.serviceName, candidate.serviceId, candidate.instanceId, candidate.baseUrl ]), [ ["box-simu-1", "hwlab-box-simu-1", "hwlab-box-simu", "res_boxsimu_1", "http://10.0.0.11:7201"], ["box-simu-2", "hwlab-box-simu-2", "hwlab-box-simu", "res_boxsimu_2", "http://10.0.0.12:7201"], ["gateway-simu-1", "hwlab-gateway-simu-1", "hwlab-gateway-simu", "gwsimu_1", "http://10.0.0.21:7101"], ["gateway-simu-2", "hwlab-gateway-simu-2", "hwlab-gateway-simu", "gwsimu_2", "http://10.0.0.22:7101"], ["patch-panel", "hwlab-patch-panel", "hwlab-patch-panel", null, "http://10.0.0.31:7301"] ] ); });