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 { classifyDirectTargetBlockerScope, createLiveOperationPlan, hasRequiredM3Connection, 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); });