test: tighten M3 preflight blocker classifications

This commit is contained in:
Code Queue Review
2026-05-22 22:48:38 +00:00
parent 0ff408696a
commit 012a89bfe1
3 changed files with 128 additions and 13 deletions
+11 -5
View File
@@ -92,8 +92,9 @@ node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-pr
```
The legacy `--confirmed-non-production` flag is accepted as compatibility, but
new handoffs should use `--expect-non-prod`. The script refuses the default
mode and refuses `--dry-run` combined with `--live`.
new handoffs should use `--expect-non-prod`. Missing or incomplete live
authorization stays on the source/read-only plan path, and `--dry-run` combined
with `--live` remains no-write.
The live smoke writes `reports/dev-gate/dev-m3-hardware-loop.json`. It first
checks the frozen DEV endpoint, `http://74.48.78.17:16667`, and stops at the
@@ -113,9 +114,14 @@ HWLAB_DEV_PATCH_PANEL_URL
```
Those targets must be two distinct HWLAB DEV simulator services for each
simulator kind, plus one HWLAB DEV patch panel, not UniDesk substitutes. The
script then checks two box simulators, two gateway simulators, active
patch-panel wiring for
box/gateway role, plus one callable HWLAB DEV patch panel, not UniDesk
substitutes. Missing direct targets are classified as `target_missing`,
duplicated box or gateway identities as `identity_not_distinct`, and missing
`res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1` wiring as
`patch_panel_wiring_missing`. Each of those stop conditions keeps
`liveOperation.status` at `not_run` and prevents the DEV write/read smoke from
starting. The script then checks two box simulators, two gateway simulators,
active patch-panel wiring for
`res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1`, a live direct
call that reads `DI1=true`, and returned operation, trace, audit, and evidence
identifiers. Fixture-backed local M3 output is never accepted as live DEV
+21 -8
View File
@@ -7,7 +7,7 @@ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { request as httpRequest } from "node:http";
import { request as httpsRequest } from "node:https";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
import { activeReportLifecycle } from "../internal/dev-report-lifecycle.mjs";
import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
@@ -143,7 +143,7 @@ function requireSafetyGates({ flags, dryRun, live }) {
const confirmedLive =
live &&
flags.has("--confirm-dev") &&
flags.has("--confirmed-non-production") &&
(flags.has("--expect-non-prod") || flags.has("--confirmed-non-production")) &&
!dryRun &&
!flags.has("--allow-live");
@@ -671,7 +671,8 @@ function createLiveOperationPlan() {
failureClassifications: Object.values(m3FailureClassifications),
refusalPolicy: [
"no arguments defaults to refusal",
"commands missing any of --live --confirm-dev --confirmed-non-production stay source/read-only",
"commands missing --live, --confirm-dev, or non-production confirmation stay source/read-only",
"non-production confirmation is --expect-non-prod; legacy --confirmed-non-production remains accepted",
"a dry-run, source, local, fixture, or read-only blocker report leaves liveOperation.status as not_run or blocked, never pass"
]
};
@@ -1715,9 +1716,21 @@ async function main() {
}
}
try {
await main();
} catch (error) {
console.error(`[dev-m3-smoke] ${error instanceof Error ? error.message : String(error)}`);
process.exitCode = 1;
export {
classifyDirectTargetBlockerScope,
classifyLiveOperationError,
createLiveOperationPlan,
hasRequiredM3Connection,
m3FailureClassifications,
parseArgs,
requireSafetyGates
};
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
try {
await main();
} catch (error) {
console.error(`[dev-m3-smoke] ${error instanceof Error ? error.message : String(error)}`);
process.exitCode = 1;
}
}
@@ -5,6 +5,14 @@ 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";
@@ -57,3 +65,91 @@ test("--dry-run aliases the same source/read-only no-write path", async () => {
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);
});