Files
pikasTech-HWLAB/scripts/dev-m3-hardware-loop-smoke.test.mjs
T
2026-05-22 22:48:38 +00:00

156 lines
5.5 KiB
JavaScript

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);
});