Merge pull request #182 from pikasTech/fix/m3-trusted-loop-execution-path-64-clean

test: harden M3 patch-panel preflight
This commit is contained in:
Lyon
2026-05-23 07:13:07 +08:00
committed by GitHub
5 changed files with 260 additions and 45 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
+1 -1
View File
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"validate": "node scripts/validate-contract.mjs && node scripts/deploy-contract-plan.mjs --check && node scripts/deploy-desired-state-plan.mjs --check",
"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/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/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.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/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-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/deploy-desired-state-plan.test.mjs scripts/src/dev-deploy-apply.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.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/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/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/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/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-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/dev-m3-hardware-loop-smoke.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/src/dev-deploy-apply.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.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",
"dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs",
"cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs",
"m1:smoke": "node scripts/m1-contract-smoke.mjs",
+75 -39
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";
@@ -33,6 +33,14 @@ const requiredM3Connection = Object.freeze({
const requiredM3GatewayIds = Object.freeze(["gwsimu_1", "gwsimu_2"]);
const runnerK3sKubeconfigPath = "/etc/rancher/k3s/k3s.yaml";
const m3FailureClassifications = Object.freeze({
targetMissing: "target_missing",
identityNotDistinct: "identity_not_distinct",
patchPanelWiringMissing: "patch_panel_wiring_missing",
operationFailed: "operation_failed",
evidenceMissing: "evidence_missing"
});
const serviceTargets = Object.freeze([
{
id: "box-simu-1",
@@ -90,11 +98,31 @@ function parseArgs(argv) {
return {
flags,
values,
dryRun: flags.has("--dry-run") || flags.has("--plan"),
live: flags.has("--live") || flags.has("--allow-live")
dryRun: flags.has("--dry-run") || flags.has("--plan") || flags.has("--source-read-only"),
live: flags.has("--live")
};
}
function classifyDirectTargetBlockerScope(scope) {
if (scope === "m3-patch-panel-wiring") {
return m3FailureClassifications.patchPanelWiringMissing;
}
if (scope === "m3-box-simu-identity" || scope === "m3-gateway-simu-identity") {
return m3FailureClassifications.identityNotDistinct;
}
if (scope === "m3-service-discovery" || scope === "m3-direct-target-missing") {
return m3FailureClassifications.targetMissing;
}
return m3FailureClassifications.operationFailed;
}
function classifyLiveOperationError(error) {
const message = error instanceof Error ? error.message : String(error);
return /\b(auditId|evidenceId)\b/u.test(message)
? m3FailureClassifications.evidenceMissing
: m3FailureClassifications.operationFailed;
}
function requireSafetyGates({ flags, dryRun, live }) {
const forbiddenFlags = [
"--prod",
@@ -112,28 +140,20 @@ function requireSafetyGates({ flags, dryRun, live }) {
assert.ok(!flags.has(flag), `${flag} is forbidden for DEV M3 smoke`);
}
assert.ok(
!(dryRun && live),
"DEV M3 smoke refuses to combine --dry-run/--plan with --live/--allow-live; plan mode never runs the M3 write path"
);
const confirmedLive =
live &&
flags.has("--confirm-dev") &&
(flags.has("--expect-non-prod") || flags.has("--confirmed-non-production")) &&
!dryRun &&
!flags.has("--allow-live");
if (dryRun) {
if (!confirmedLive) {
return {
mode: "dry-run",
liveWriteAllowed: false
};
}
assert.ok(
live,
"DEV M3 smoke refuses to run by default; use --dry-run for a no-write plan or --live --confirm-dev --expect-non-prod after approval"
);
assert.ok(flags.has("--confirm-dev"), "live DEV smoke requires --confirm-dev");
assert.ok(
flags.has("--expect-non-prod") || flags.has("--confirmed-non-production"),
"live DEV smoke requires --expect-non-prod (legacy --confirmed-non-production is accepted only as compatibility)"
);
return {
mode: "live",
liveWriteAllowed: true
@@ -648,10 +668,11 @@ function createLiveOperationPlan() {
"targetPort=DI1",
"targetState.ports.DI1.propagatedBy=hwlab-patch-panel"
],
failureClassifications: Object.values(m3FailureClassifications),
refusalPolicy: [
"no arguments defaults to refusal",
"--dry-run/--plan cannot be combined with --live/--allow-live",
"live mode refuses without --confirm-dev and --expect-non-prod",
"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"
]
};
@@ -963,7 +984,7 @@ async function resolveDirectM3Targets({ environmentTargets, kubernetesDiscovery
type: "observability_blocker",
scope: "m3-service-discovery",
status: "open",
classification: "runner_kubeconfig_readonly_gap",
classification: classifyDirectTargetBlockerScope("m3-service-discovery"),
sourceIssue: "pikasTech/HWLAB#46",
summary: `DEV ingress is reachable on frozen :16667, but direct M3 target discovery via ${kubernetesDiscovery.source} is ${kubernetesDiscovery.status}; environment URLs missing ${environmentTargets.missing.join(", ")}; discovery detail=${kubernetesDiscovery.summary}.`
});
@@ -972,7 +993,7 @@ async function resolveDirectM3Targets({ environmentTargets, kubernetesDiscovery
type: "observability_blocker",
scope: "m3-direct-target-missing",
status: "open",
classification: "direct_target_missing",
classification: classifyDirectTargetBlockerScope("m3-direct-target-missing"),
sourceIssue: "pikasTech/HWLAB#64",
summary: `Read-only service discovery did not expose enough callable DEV M3 direct targets: box-simu=${boxCandidates.length}/2, gateway-simu=${gatewayCandidates.length}/2, patch-panel=${patchCandidates.length}/1.`
});
@@ -983,7 +1004,7 @@ async function resolveDirectM3Targets({ environmentTargets, kubernetesDiscovery
type: "runtime_blocker",
scope: "m3-box-simu-identity",
status: "open",
classification: "direct_target_identity_gap",
classification: classifyDirectTargetBlockerScope("m3-box-simu-identity"),
sourceIssue: "pikasTech/HWLAB#64",
summary: `DEV exposes two box-simu endpoints, but live identities are not the required distinct resources res_boxsimu_1 and res_boxsimu_2; observed resources=${[...distinctBoxResources].join(", ") || "none"}.`
});
@@ -994,7 +1015,7 @@ async function resolveDirectM3Targets({ environmentTargets, kubernetesDiscovery
type: "runtime_blocker",
scope: "m3-gateway-simu-identity",
status: "open",
classification: "direct_target_identity_gap",
classification: classifyDirectTargetBlockerScope("m3-gateway-simu-identity"),
sourceIssue: "pikasTech/HWLAB#64",
summary: `DEV exposes two gateway-simu endpoints, but live gateway identities are not distinct; observed identities=${[...distinctGatewayIdentities].join(", ") || "none"}.`
});
@@ -1007,7 +1028,7 @@ async function resolveDirectM3Targets({ environmentTargets, kubernetesDiscovery
type: "runtime_blocker",
scope: "m3-patch-panel-wiring",
status: "open",
classification: "direct_target_m3_wiring_missing",
classification: classifyDirectTargetBlockerScope("m3-patch-panel-wiring"),
sourceIssue: "pikasTech/HWLAB#64",
summary: `DEV patch-panel is callable, but live wiring does not contain ${requiredM3Connection.fromResourceId}:${requiredM3Connection.fromPort} -> ${requiredM3Connection.toResourceId}:${requiredM3Connection.toPort}; active=${observedActive}; configured=${observedConfigured}.`
});
@@ -1256,8 +1277,9 @@ async function runLiveM3Targets(targets) {
assert.ok(after.ok, "box-simu-2 DI1 read failed");
assert.equal(after.json?.ports?.DI1?.value, true, "box-simu-2 DI1 must read true");
const auditId = route.json?.auditId ?? write.json?.auditId ?? after.json?.auditId;
const evidenceId = route.json?.evidenceId ?? write.json?.evidenceId ?? after.json?.evidenceId;
const deliveryResponse = route.json?.deliveries?.[0]?.response;
const auditId = route.json?.auditId ?? deliveryResponse?.auditId ?? write.json?.auditId ?? after.json?.auditId;
const evidenceId = route.json?.evidenceId ?? deliveryResponse?.evidenceId ?? write.json?.evidenceId ?? after.json?.evidenceId;
assert.equal(typeof auditId, "string", "live M3 result must include auditId");
assert.equal(typeof evidenceId, "string", "live M3 result must include evidenceId");
@@ -1319,6 +1341,7 @@ function baseReport({ commitId, observedAt }) {
confirmDevRequired: true,
expectNonProdRequired: true,
confirmedNonProductionRequired: true,
defaultSourceReadOnly: true,
dryRunPlanSupported: true,
dryRunCallsLiveEndpoints: false,
prodForbidden: true,
@@ -1447,12 +1470,12 @@ async function runDryRunPlan({ values }) {
type: "safety_blocker",
scope: "m3-live-write-authorization",
status: "open",
classification: "dry_run_plan_only",
classification: m3FailureClassifications.targetMissing,
summary: "Plan-only mode is intentionally non-mutating; run the bounded live smoke only after explicit DEV/non-PROD approval and after read-only preconditions identify the exact HWLAB targets."
});
report.summary = {
status: "blocked",
classification: "dry_run_plan_only",
classification: m3FailureClassifications.targetMissing,
observedAt,
result: "DRY-RUN plan emitted live write prerequisites, endpoint plan, and required evidence fields; it did not call DEV endpoints or produce DEV-LIVE evidence."
};
@@ -1537,12 +1560,12 @@ async function main() {
type: "network_blocker",
scope: "frp",
status: "open",
classification: "frp",
classification: m3FailureClassifications.targetMissing,
summary: "Blocked at the #33 DEV runtime readiness condition: public DEV ingress does not accept HWLAB health requests, so #36/M3 hardware-loop checks were not reached."
});
report.summary = {
status: "blocked",
classification: "frp",
classification: m3FailureClassifications.targetMissing,
observedAt,
result: "No live DEV M3 evidence was produced; fixture-backed local evidence was not used as a substitute."
};
@@ -1602,7 +1625,7 @@ async function main() {
report.blockers.push(...directTargets.blockers);
report.summary = {
status: "blocked",
classification: directTargets.blockers[0]?.classification ?? "direct_target_blocked",
classification: directTargets.blockers[0]?.classification ?? m3FailureClassifications.targetMissing,
observedAt,
result: "DEV ingress and direct targets were reachable, but M3 DEV-LIVE was not triggered because required identities or patch-panel wiring were missing."
};
@@ -1672,17 +1695,18 @@ async function main() {
`[dev-m3-smoke] operationId=${live.operationId} auditId=${live.auditId} evidenceId=${live.evidenceId}`
);
} catch (error) {
const classification = classifyLiveOperationError(error);
addNotRunM3Checks(report, "A live DEV M3 runtime contract check failed before all checks passed.");
report.blockers.push({
type: "runtime_blocker",
scope: "m3-hardware-loop-runtime",
status: "open",
classification: "hardware loop",
classification,
summary: error instanceof Error ? error.message : String(error)
});
report.summary = {
status: "blocked",
classification: "hardware loop",
classification,
observedAt,
result: "Live DEV M3 runtime was reachable but did not satisfy the M3 hardware-loop contract."
};
@@ -1692,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;
}
}
+155
View File
@@ -0,0 +1,155 @@
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);
});
+18
View File
@@ -1722,6 +1722,9 @@ async function validateDevM3Report(report, label) {
if (Object.hasOwn(report.safetyGates, "dryRunCallsLiveEndpoints")) {
assert.equal(report.safetyGates.dryRunCallsLiveEndpoints, false, `${label}.safetyGates.dryRunCallsLiveEndpoints`);
}
if (Object.hasOwn(report.safetyGates, "defaultSourceReadOnly")) {
assert.equal(report.safetyGates.defaultSourceReadOnly, true, `${label}.safetyGates.defaultSourceReadOnly`);
}
assertArray(report.liveChecks, `${label}.liveChecks`);
assert.ok(report.liveChecks.length >= 8, `${label}.liveChecks must include DEV M3 checks`);
@@ -1835,6 +1838,21 @@ async function validateDevM3Report(report, label) {
assert.equal(plan.route, "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1", `${label}.dryRunPlan.route`);
assert.equal(plan.dryRunCallsLiveEndpoints, false, `${label}.dryRunPlan.dryRunCallsLiveEndpoints`);
assert.equal(plan.liveWriteWillRun, false, `${label}.dryRunPlan.liveWriteWillRun`);
if (Object.hasOwn(plan, "failureClassifications")) {
assertStringArray(plan.failureClassifications, `${label}.dryRunPlan.failureClassifications`, { minLength: 5 });
for (const classification of [
"target_missing",
"identity_not_distinct",
"patch_panel_wiring_missing",
"operation_failed",
"evidence_missing"
]) {
assert.ok(
plan.failureClassifications.includes(classification),
`${label}.dryRunPlan.failureClassifications missing ${classification}`
);
}
}
assertStringArray(plan.liveWritePreconditions, `${label}.dryRunPlan.liveWritePreconditions`, { minLength: 4 });
assertStringArray(plan.evidenceFields, `${label}.dryRunPlan.evidenceFields`, { minLength: 4 });
assertArray(plan.endpointPlan, `${label}.dryRunPlan.endpointPlan`);