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
+67 -2
View File
@@ -85,8 +85,11 @@ const requiredDevM3ValidationCommands = [
"node --check scripts/dev-m3-hardware-loop-smoke.mjs",
"node --check scripts/validate-dev-m3-cardinality.mjs",
"node scripts/validate-dev-m3-cardinality.mjs",
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production"
"node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run",
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod"
];
const legacyDevM3LiveCommand =
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production";
const requiredDevM3Docs = [
"docs/dev-acceptance-matrix.md",
"docs/m3-hardware-loop.md"
@@ -1334,6 +1337,10 @@ async function validateDevM3Report(report, label) {
`${label}.validationCommands missing ${requiredCommand}`
);
}
assert.ok(
report.validationCommands.includes(legacyDevM3LiveCommand),
`${label}.validationCommands missing legacy compatibility command ${legacyDevM3LiveCommand}`
);
assertObject(report.runtimeTarget, `${label}.runtimeTarget`);
assert.equal(report.runtimeTarget.endpoint, "http://74.48.78.17:16667", `${label}.runtimeTarget.endpoint`);
@@ -1348,6 +1355,16 @@ async function validateDevM3Report(report, label) {
assert.equal(report.runtimeTarget.environment, "dev", `${label}.runtimeTarget.environment`);
assert.equal(report.runtimeTarget.requiredBoxSimulators, 2, `${label}.runtimeTarget.requiredBoxSimulators`);
assert.equal(report.runtimeTarget.requiredGatewaySimulators, 2, `${label}.runtimeTarget.requiredGatewaySimulators`);
if (Object.hasOwn(report.runtimeTarget, "requiredPatchPanels")) {
assert.equal(report.runtimeTarget.requiredPatchPanels, 1, `${label}.runtimeTarget.requiredPatchPanels`);
}
if (Object.hasOwn(report.runtimeTarget, "requiredRoute")) {
assert.equal(
report.runtimeTarget.requiredRoute,
"res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1",
`${label}.runtimeTarget.requiredRoute`
);
}
assert.equal(report.runtimeTarget.realHardwareAllowed, false, `${label}.runtimeTarget.realHardwareAllowed`);
assert.equal(report.runtimeTarget.prodAllowed, false, `${label}.runtimeTarget.prodAllowed`);
@@ -1355,7 +1372,9 @@ async function validateDevM3Report(report, label) {
for (const field of [
"liveFlagRequired",
"confirmDevRequired",
"expectNonProdRequired",
"confirmedNonProductionRequired",
"dryRunPlanSupported",
"prodForbidden",
"realHardwareForbidden",
"secretReadForbidden",
@@ -1364,6 +1383,9 @@ async function validateDevM3Report(report, label) {
]) {
assert.equal(report.safetyGates[field], true, `${label}.safetyGates.${field}`);
}
if (Object.hasOwn(report.safetyGates, "dryRunCallsLiveEndpoints")) {
assert.equal(report.safetyGates.dryRunCallsLiveEndpoints, false, `${label}.safetyGates.dryRunCallsLiveEndpoints`);
}
assertArray(report.liveChecks, `${label}.liveChecks`);
assert.ok(report.liveChecks.length >= 8, `${label}.liveChecks must include DEV M3 checks`);
@@ -1449,7 +1471,14 @@ async function validateDevM3Report(report, label) {
["pass", "gap", "source-ready", "manifest-ready"].includes(evidence.status),
`${evidenceLabel}.status must be pass, gap, source-ready, or manifest-ready`
);
assertRepoRelativePath(evidence.source, `${evidenceLabel}.source`);
if (Array.isArray(evidence.source)) {
assertStringArray(evidence.source, `${evidenceLabel}.source`, { minLength: 1 });
for (const [sourceIndex, sourcePath] of evidence.source.entries()) {
assertRepoRelativePath(sourcePath, `${evidenceLabel}.source[${sourceIndex}]`);
}
} else {
assertRepoRelativePath(evidence.source, `${evidenceLabel}.source`);
}
assertString(evidence.summary, `${evidenceLabel}.summary`);
assertObject(evidence.evidence, `${evidenceLabel}.evidence`);
assertString(evidence.requiredFollowUp, `${evidenceLabel}.requiredFollowUp`);
@@ -1462,6 +1491,42 @@ async function validateDevM3Report(report, label) {
}
}
if (Object.hasOwn(report, "dryRunPlan")) {
const plan = report.dryRunPlan;
assertObject(plan, `${label}.dryRunPlan`);
assert.equal(plan.mode, "plan-only", `${label}.dryRunPlan.mode`);
assert.equal(plan.evidenceLevel, "DRY-RUN", `${label}.dryRunPlan.evidenceLevel`);
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`);
assertStringArray(plan.liveWritePreconditions, `${label}.dryRunPlan.liveWritePreconditions`, { minLength: 4 });
assertStringArray(plan.evidenceFields, `${label}.dryRunPlan.evidenceFields`, { minLength: 4 });
assertArray(plan.endpointPlan, `${label}.dryRunPlan.endpointPlan`);
assert.ok(plan.endpointPlan.length >= 8, `${label}.dryRunPlan.endpointPlan must enumerate precondition and live endpoints`);
assert.ok(
plan.endpointPlan.some((endpoint) => endpoint.serviceId === "hwlab-patch-panel" && endpoint.path === "/signals/route" && endpoint.mutatesDevState === true),
`${label}.dryRunPlan.endpointPlan must include the patch-panel route write endpoint`
);
assert.ok(
plan.endpointPlan.some((endpoint) => endpoint.serviceId === "hwlab-box-simu" && endpoint.path === "/ports/write" && endpoint.mutatesDevState === true),
`${label}.dryRunPlan.endpointPlan must include the source box DO1 write endpoint`
);
assert.ok(
plan.endpointPlan.every((endpoint) => endpoint.calledInDryRun === false),
`${label}.dryRunPlan.endpointPlan endpoints must not be called in dry-run`
);
for (const requiredField of [
"operationId",
"traceId",
"auditId",
"evidenceId",
"routeResponse.propagatedBy=hwlab-patch-panel",
"targetState.ports.DI1.propagatedBy=hwlab-patch-panel"
]) {
assert.ok(plan.evidenceFields.includes(requiredField), `${label}.dryRunPlan.evidenceFields missing ${requiredField}`);
}
}
assertObject(report.liveOperation, `${label}.liveOperation`);
assertStatus(report.liveOperation.status, `${label}.liveOperation.status`);
for (const field of ["operationId", "traceId", "auditId", "evidenceId", "summary"]) {