test: add M3 source contract dry-run plan
Host commander merge: clean PR, no live write, preserves DRY-RUN vs DEV-LIVE boundary for #78/#7/#164.
This commit is contained in:
@@ -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"));
|
||||
|
||||
@@ -522,7 +522,10 @@ function collectM3Evidence(reports) {
|
||||
`auditId=${m3.liveOperation?.auditId ?? "not_observed"}`,
|
||||
`evidenceId=${m3.liveOperation?.evidenceId ?? "not_observed"}`
|
||||
],
|
||||
commands: ["node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production"],
|
||||
commands: [
|
||||
"node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run",
|
||||
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod"
|
||||
],
|
||||
summary: liveSummary
|
||||
})
|
||||
];
|
||||
|
||||
@@ -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"]) {
|
||||
|
||||
@@ -41,6 +41,9 @@ async function main() {
|
||||
assertIncludes(doc, "deploy/artifact-catalog.dev.json", "m3 runbook");
|
||||
assertIncludes(doc, "pikasTech/HWLAB#63", "m3 runbook");
|
||||
assertIncludes(doc, "Do not promote SOURCE / LOCAL / DRY-RUN / fixture output to `DEV-LIVE`.", "m3 runbook");
|
||||
assertIncludes(doc, "node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run", "m3 runbook");
|
||||
assertIncludes(doc, "node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod", "m3 runbook");
|
||||
assertIncludes(doc, "liveOperation.status: \"not_run\"", "m3 runbook");
|
||||
assertNotIncludes(boundary, "http://74.48.78.17:6666/", "m3 current boundary");
|
||||
assertNotIncludes(boundary, "http://74.48.78.17:6667/", "m3 current boundary");
|
||||
assertIncludes(boundary, "http://74.48.78.17:16666/", "m3 current boundary");
|
||||
|
||||
Reference in New Issue
Block a user