test: add m3 source contract dry-run plan

This commit is contained in:
Code Queue Review
2026-05-22 18:06:24 +00:00
parent ab252f524e
commit 8ff93656ed
8 changed files with 663 additions and 24 deletions
+284 -9
View File
@@ -19,6 +19,9 @@ const issue = "pikasTech/HWLAB#38";
const deployWorkloadsPath = "deploy/k8s/base/workloads.yaml";
const deployServicesPath = "deploy/k8s/base/services.yaml";
const requiredManifestCardinalityCommand = "node scripts/validate-dev-m3-cardinality.mjs";
const dryRunPlanCommand = "node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run";
const liveSmokeCommand = "node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod";
const legacyLiveSmokeCommand = "node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production";
const requiredM3BoxIds = Object.freeze(["boxsimu_1", "boxsimu_2"]);
const requiredM3BoxResources = Object.freeze(["res_boxsimu_1", "res_boxsimu_2"]);
const requiredM3Connection = Object.freeze({
@@ -84,10 +87,15 @@ function parseArgs(argv) {
flags.add(arg);
}
return { flags, values };
return {
flags,
values,
dryRun: flags.has("--dry-run") || flags.has("--plan"),
live: flags.has("--live") || flags.has("--allow-live")
};
}
function requireSafetyGates(flags) {
function requireSafetyGates({ flags, dryRun, live }) {
const forbiddenFlags = [
"--prod",
"--real-hardware",
@@ -104,13 +112,32 @@ function requireSafetyGates(flags) {
assert.ok(!flags.has(flag), `${flag} is forbidden for DEV M3 smoke`);
}
const live = flags.has("--live") || flags.has("--allow-live");
assert.ok(live, "DEV M3 smoke requires --live or --allow-live");
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"
);
if (dryRun) {
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("--confirmed-non-production"),
"live DEV smoke requires --confirmed-non-production"
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
};
}
function isoNow() {
@@ -484,6 +511,152 @@ function joinUrl(baseUrl, routePath) {
return new URL(routePath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
}
function endpointReference(target, routePath) {
const baseUrl = process.env[target.urlEnv];
if (baseUrl) {
return {
url: joinUrl(baseUrl, routePath),
urlSource: `env:${target.urlEnv}`
};
}
return {
url: `$${target.urlEnv}${routePath}`,
urlSource: `required-env:${target.urlEnv}`
};
}
function plannedServiceEndpoint(target, routePath, { method = "GET", phase, mutatesDevState = false }) {
const endpoint = endpointReference(target, routePath);
return {
phase,
targetId: target.id,
serviceId: target.serviceId,
method,
path: routePath,
...endpoint,
mutatesDevState,
calledInDryRun: false
};
}
function createLiveOperationPlan() {
const box1 = serviceTargets.find((target) => target.id === "box-simu-1");
const box2 = serviceTargets.find((target) => target.id === "box-simu-2");
const patchPanel = serviceTargets.find((target) => target.id === "patch-panel");
const endpointPlan = [
{
phase: "dev-ingress-precondition",
targetId: "dev-api-edge",
serviceId: "hwlab-cloud-api",
method: "GET",
path: "/health/live",
url: joinUrl(DEV_ENDPOINT, "/health/live"),
urlSource: "frozen-dev-endpoint",
mutatesDevState: false,
calledInDryRun: false
},
{
phase: "dev-ingress-precondition",
targetId: "dev-api-edge-json-rpc",
serviceId: "hwlab-cloud-api",
method: "POST",
path: "/json-rpc",
url: joinUrl(DEV_ENDPOINT, "/json-rpc"),
urlSource: "frozen-dev-endpoint",
mutatesDevState: false,
calledInDryRun: false,
requestShape: "system.health"
},
{
phase: "dev-ingress-precondition",
targetId: "dev-frontend",
serviceId: "hwlab-cloud-web",
method: "GET",
path: "/health/live",
url: joinUrl(DEV_FRONTEND_ENDPOINT, "/health/live"),
urlSource: "frozen-dev-frontend-endpoint",
mutatesDevState: false,
calledInDryRun: false
}
];
for (const target of serviceTargets) {
endpointPlan.push(
plannedServiceEndpoint(target, "/health/live", {
phase: "direct-target-precondition"
}),
plannedServiceEndpoint(target, target.statusPath, {
phase: "direct-target-precondition"
})
);
if (target.wiringPath) {
endpointPlan.push(
plannedServiceEndpoint(target, target.wiringPath, {
phase: "patch-panel-route-precondition"
})
);
}
}
endpointPlan.push(
plannedServiceEndpoint(box1, "/ports/write", {
method: "POST",
phase: "live-write-source-do1",
mutatesDevState: true
}),
plannedServiceEndpoint(patchPanel, patchPanel.routePath, {
method: "POST",
phase: "live-write-patch-panel-route",
mutatesDevState: true
}),
plannedServiceEndpoint(box2, "/status", {
phase: "live-read-target-di1"
})
);
return {
mode: "plan-only",
evidenceLevel: "DRY-RUN",
route: "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1",
dryRunCallsLiveEndpoints: false,
liveWriteWillRun: false,
liveWriteRequiresHumanApproval: true,
liveCommand: liveSmokeCommand,
legacyLiveCommand: legacyLiveSmokeCommand,
liveWritePreconditions: [
"operator has authorized a bounded DEV M3 live smoke window",
"command includes --live, --confirm-dev, and --expect-non-prod",
"PROD, real hardware, secret reads, service restarts, and force pushes remain forbidden",
"DEV ingress identifies HWLAB DEV on the frozen 16666/16667 boundary",
"two distinct hwlab-box-simu targets report res_boxsimu_1 and res_boxsimu_2",
"two distinct hwlab-gateway-simu targets report distinct DEV gateway sessions",
"one hwlab-patch-panel reports active wiring for res_boxsimu_1:DO1 -> res_boxsimu_2:DI1",
"cross-device propagation is owned by hwlab-patch-panel; box loopback, SOURCE, LOCAL, fixture, and DRY-RUN output cannot be marked DEV-LIVE"
],
endpointPlan,
evidenceFields: [
"operationId",
"traceId",
"auditId",
"evidenceId",
"sourceResourceId=res_boxsimu_1",
"sourcePort=DO1",
"routeOwner=hwlab-patch-panel",
"routeResponse.propagatedBy=hwlab-patch-panel",
"targetResourceId=res_boxsimu_2",
"targetPort=DI1",
"targetState.ports.DI1.propagatedBy=hwlab-patch-panel"
],
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",
"a dry-run, source, local, fixture, or read-only blocker report leaves liveOperation.status as not_run or blocked, never pass"
]
};
}
function isHwlabDevIdentity(probe) {
const body = probe.json;
if (!probe.ok || !body || typeof body !== "object" || Array.isArray(body)) {
@@ -1075,6 +1248,7 @@ async function runLiveM3Targets(targets) {
probes.push({ id: "patch-panel", kind: "signals.route", probe: route });
assert.ok(route.ok, "patch-panel signal route failed");
assert.equal(route.json?.accepted, true, "patch-panel signal route accepted");
assert.equal(route.json?.propagatedBy, "hwlab-patch-panel", "patch-panel signal route owner");
assert.equal(route.json?.deliveryCount, 1, "patch-panel signal route delivery count");
const after = await probeJson(joinUrl(box2Target.baseUrl, "/status"));
@@ -1122,7 +1296,9 @@ function baseReport({ commitId, observedAt }) {
"node --check scripts/dev-m3-hardware-loop-smoke.mjs",
"node --check scripts/validate-dev-m3-cardinality.mjs",
requiredManifestCardinalityCommand,
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production",
dryRunPlanCommand,
liveSmokeCommand,
legacyLiveSmokeCommand,
"node --check scripts/validate-dev-gate-report.mjs",
"node scripts/validate-dev-gate-report.mjs"
],
@@ -1133,13 +1309,18 @@ function baseReport({ commitId, observedAt }) {
environment: ENVIRONMENT_DEV,
requiredBoxSimulators: 2,
requiredGatewaySimulators: 2,
requiredPatchPanels: 1,
requiredRoute: "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1",
realHardwareAllowed: false,
prodAllowed: false
},
safetyGates: {
liveFlagRequired: true,
confirmDevRequired: true,
expectNonProdRequired: true,
confirmedNonProductionRequired: true,
dryRunPlanSupported: true,
dryRunCallsLiveEndpoints: false,
prodForbidden: true,
realHardwareForbidden: true,
secretReadForbidden: true,
@@ -1193,6 +1374,94 @@ function baseReport({ commitId, observedAt }) {
};
}
async function runDryRunPlan({ values }) {
await ensureContractFiles();
const reportPath = resolveReportPath(values.get("output"));
const observedAt = isoNow();
const report = baseReport({
commitId: currentCommit(),
observedAt
});
const plan = createLiveOperationPlan();
report.dryRunPlan = plan;
report.readOnlySupplementalEvidence = await collectReadOnlySupplementalEvidence();
report.liveChecks.push(
{
id: "dry-run-live-write-plan",
status: "not_run",
summary: "Plan-only mode enumerated live write prerequisites and endpoints without calling DEV endpoints or mutating DEV state.",
evidence: [
`endpointCount=${plan.endpointPlan.length}`,
`mutatingEndpointCount=${plan.endpointPlan.filter((endpoint) => endpoint.mutatesDevState).length}`,
"dryRunCallsLiveEndpoints=false"
]
},
{
id: "two-box-simu-online",
status: "not_run",
summary: "Plan-only mode requires two distinct live DEV box-simu identities before any write; no DEV target was probed.",
evidence: ["required=res_boxsimu_1,res_boxsimu_2"]
},
{
id: "two-gateway-simu-online",
status: "not_run",
summary: "Plan-only mode requires two distinct live DEV gateway-simu identities before any write; no DEV target was probed.",
evidence: ["required=gwsimu_1,gwsimu_2"]
},
{
id: "patch-panel-healthy",
status: "not_run",
summary: "Plan-only mode requires a callable DEV hwlab-patch-panel before any write; no DEV target was probed.",
evidence: ["required=hwlab-patch-panel"]
},
{
id: "wiring-do1-di1-applied",
status: "not_run",
summary: "Plan-only mode requires patch-panel-owned res_boxsimu_1:DO1 -> res_boxsimu_2:DI1 wiring before any write.",
evidence: [`route=${plan.route}`]
},
{
id: "direct-call-do-write-di-read",
status: "not_run",
summary: "Plan-only mode did not send /ports/write or /signals/route; this output is DRY-RUN only and cannot be labeled DEV-LIVE.",
evidence: ["No live DEV write was sent."]
},
{
id: "audit-evidence-traceable",
status: "not_run",
summary: "Plan-only mode listed the required operation, trace, audit, and evidence fields but did not create them.",
evidence: plan.evidenceFields
}
);
report.liveOperation = {
status: "not_run",
operationId: "not_observed",
traceId: "not_observed",
auditId: "not_observed",
evidenceId: "not_observed",
summary: "Dry-run plan only; no DEV-LIVE operation was attempted or claimed."
};
report.blockers.push({
type: "safety_blocker",
scope: "m3-live-write-authorization",
status: "open",
classification: "dry_run_plan_only",
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",
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."
};
await writeReport(report, reportPath);
console.log(`[dev-m3-smoke] mode=dry-run status=blocked report=${relativePath(reportPath)}`);
console.log("[dev-m3-smoke] dry-run: no DEV endpoint calls and no live write were sent");
}
function addNotRunM3Checks(report, reason) {
for (const id of [
"two-box-simu-online",
@@ -1223,8 +1492,14 @@ async function writeReport(report, reportPath) {
}
async function main() {
const { flags, values } = parseArgs(process.argv.slice(2));
requireSafetyGates(flags);
const args = parseArgs(process.argv.slice(2));
const safety = requireSafetyGates(args);
if (safety.mode === "dry-run") {
await runDryRunPlan(args);
return;
}
const { values } = args;
await ensureContractFiles();
const reportPath = resolveReportPath(values.get("output"));