fix: harden dev cd runtime postflight
Summary: - shared runtime durable readiness classifier for live-status and DEV postflight - structured postflight blockers propagated through dev-cd apply - contract coverage for ready queryResult, auth/schema/migration blockers, evidence persistence blockers, and bounded stdout Verification: - git diff --check - node --check changed scripts/tests/live-status modules - node --test scripts/src/dev-runtime-postflight.test.mjs scripts/src/dev-cd-apply.test.mjs web/hwlab-cloud-web/scripts/live-status-contract.test.mjs - node scripts/dev-runtime-postflight.mjs --check - node web/hwlab-cloud-web/scripts/check.mjs - npm run check No DEV apply/rollout/live health verification was run.
This commit is contained in:
@@ -56,6 +56,8 @@ test("live mode skips M3 writes when /health/live durable runtime is blocked", a
|
||||
assert.equal(report.actions.m3MutationAttempted, false);
|
||||
assert.equal(report.m3.status, "not_run");
|
||||
assert.equal(report.blockers.some((blocker) => blocker.scope === "api-health-live"), true);
|
||||
assertStructuredBlockers(report.blockers);
|
||||
assert.match(report.blockers.find((blocker) => blocker.scope === "api-health-live").reason, /auth_blocked/u);
|
||||
assert.equal(JSON.stringify(report).includes("postgresql://"), false);
|
||||
});
|
||||
|
||||
@@ -74,6 +76,7 @@ test("live mode requires /v1 durable readiness before M3 writes", async () => {
|
||||
assert.equal(report.conclusion.status, "blocked");
|
||||
assert.equal(report.actions.m3MutationAttempted, false);
|
||||
assert.equal(report.blockers.some((blocker) => blocker.scope === "api-v1"), true);
|
||||
assertStructuredBlockers(report.blockers);
|
||||
});
|
||||
|
||||
test("live mode requires /v1 to expose the controlled M3 IO route before M3 writes", async () => {
|
||||
@@ -100,6 +103,7 @@ test("live mode requires /v1 to expose the controlled M3 IO route before M3 writ
|
||||
blocker.scope === "api-v1" &&
|
||||
blocker.summary.includes("controlled M3 IO route")
|
||||
), true);
|
||||
assertStructuredBlockers(report.blockers);
|
||||
});
|
||||
|
||||
test("live mode does not mistake DB live evidence for durable runtime readiness", async () => {
|
||||
@@ -121,6 +125,51 @@ test("live mode does not mistake DB live evidence for durable runtime readiness"
|
||||
assert.equal(report.apiHealth.readiness.durabilityReady, false);
|
||||
assert.equal(report.actions.m3MutationAttempted, false);
|
||||
assert.equal(report.blockers.some((blocker) => blocker.scope === "api-health-live"), true);
|
||||
assertStructuredBlockers(report.blockers);
|
||||
});
|
||||
|
||||
test("live mode blocks auth, schema, and migration durable readiness before M3 writes", async () => {
|
||||
for (const [blocker, queryResult, blockedLayer] of [
|
||||
["runtime_durable_adapter_auth_blocked", "auth_blocked", "auth"],
|
||||
["runtime_durable_adapter_schema_blocked", "schema_blocked", "schema"],
|
||||
["runtime_durable_adapter_migration_blocked", "migration_blocked", "migration"]
|
||||
]) {
|
||||
const report = await buildDevRuntimePostflightReport(
|
||||
parseArgs(["--live", "--confirm-dev", "--confirmed-non-production"]),
|
||||
{
|
||||
httpGetJson: async () => ({ ok: true, status: 200, json: apiPayload({ ready: false, blocker, queryResult, blockedLayer }) }),
|
||||
m3Runner: async () => {
|
||||
throw new Error("M3 runner should not run when durable readiness is blocked");
|
||||
},
|
||||
now: () => "2026-05-23T00:00:00.000Z"
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(report.conclusion.status, "blocked");
|
||||
assert.equal(report.actions.m3MutationAttempted, false);
|
||||
assert.equal(report.m3.status, "not_run");
|
||||
assert.equal(report.apiHealth.runtimeDurableReadiness.classification, blocker);
|
||||
assert.equal(report.apiHealth.runtimeDurableReadiness.evidence.queryResult, queryResult);
|
||||
assert.equal(report.apiV1.runtimeDurableReadiness.classification, blocker);
|
||||
assertStructuredBlockers(report.blockers);
|
||||
}
|
||||
});
|
||||
|
||||
test("live mode treats durable_readiness_ready queryResult as ready, not blocked", async () => {
|
||||
const report = await buildDevRuntimePostflightReport(
|
||||
parseArgs(["--live", "--confirm-dev", "--confirmed-non-production"]),
|
||||
{
|
||||
httpGetJson: async () => ({ ok: true, status: 200, json: apiPayload({ ready: true, queryResult: "durable_readiness_ready" }) }),
|
||||
m3Runner: async () => m3GreenReport(),
|
||||
now: () => "2026-05-23T00:00:00.000Z"
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(report.conclusion.status, "pass");
|
||||
assert.equal(report.actions.m3MutationAttempted, true);
|
||||
assert.equal(report.apiHealth.runtimeDurableReadiness.ready, true);
|
||||
assert.equal(report.apiHealth.runtimeDurableReadiness.evidence.queryResult, "durable_readiness_ready");
|
||||
assert.equal(report.blockers.length, 0);
|
||||
});
|
||||
|
||||
test("live mode records M3 true/false durable green evidence", async () => {
|
||||
@@ -165,6 +214,7 @@ test("live mode blocks when M3 operation evidence is not durable green", async (
|
||||
assert.equal(report.conclusion.status, "blocked");
|
||||
assert.equal(report.blockers.some((blocker) => blocker.scope === "m3-durable-postflight"), true);
|
||||
assert.equal(report.m3.persistenceContract.ready, false);
|
||||
assertStructuredBlockers(report.blockers);
|
||||
});
|
||||
|
||||
test("live mode blocks when M3 true/false persisted sequence is incomplete", async () => {
|
||||
@@ -183,9 +233,29 @@ test("live mode blocks when M3 true/false persisted sequence is incomplete", asy
|
||||
assert.equal(report.m3.persistenceContract.ready, false);
|
||||
assert.equal(report.m3.persistenceContract.sequence.at(-1).status, "missing");
|
||||
assert.equal(report.blockers.some((blocker) => blocker.scope === "m3-durable-postflight"), true);
|
||||
assertStructuredBlockers(report.blockers);
|
||||
});
|
||||
|
||||
function apiPayload({ ready }) {
|
||||
function assertStructuredBlockers(blockers) {
|
||||
assert.ok(blockers.length > 0);
|
||||
for (const blocker of blockers) {
|
||||
assert.equal(typeof blocker.scope, "string");
|
||||
assert.equal(typeof blocker.reason, "string");
|
||||
assert.equal(typeof blocker.impact, "string");
|
||||
assert.equal(typeof blocker.safeNextAction, "string");
|
||||
assert.equal(typeof blocker.retryable, "boolean");
|
||||
assert.notEqual(blocker.reason, "");
|
||||
assert.notEqual(blocker.impact, "");
|
||||
assert.notEqual(blocker.safeNextAction, "");
|
||||
}
|
||||
}
|
||||
|
||||
function apiPayload({
|
||||
ready,
|
||||
blocker = "runtime_durable_adapter_auth_blocked",
|
||||
queryResult = ready ? "durable_readiness_ready" : "auth_blocked",
|
||||
blockedLayer = "auth"
|
||||
}) {
|
||||
return {
|
||||
serviceId: "hwlab-cloud-api",
|
||||
environment: "dev",
|
||||
@@ -200,8 +270,8 @@ function apiPayload({ ready }) {
|
||||
runtimeReadiness: {
|
||||
ready,
|
||||
status: ready ? "ready" : "blocked",
|
||||
blocker: ready ? null : "runtime_durable_adapter_auth_blocked",
|
||||
queryResult: ready ? "durable_readiness_ready" : "auth_blocked",
|
||||
blocker: ready ? null : blocker,
|
||||
queryResult,
|
||||
requiredEvidence: "runtime_adapter_schema_migration_read_query"
|
||||
}
|
||||
},
|
||||
@@ -211,18 +281,21 @@ function apiPayload({ ready }) {
|
||||
durableRequested: true,
|
||||
ready,
|
||||
status: ready ? "ready" : "blocked",
|
||||
blocker: ready ? null : "runtime_durable_adapter_auth_blocked",
|
||||
blocker: ready ? null : blocker,
|
||||
liveRuntimeEvidence: ready,
|
||||
connection: {
|
||||
queryResult
|
||||
},
|
||||
durabilityContract: {
|
||||
requiredEvidence: "runtime_adapter_schema_migration_read_query",
|
||||
blockedLayer: ready ? null : "auth"
|
||||
blockedLayer: ready ? null : blockedLayer
|
||||
},
|
||||
gates: {
|
||||
ssl: gate(true),
|
||||
auth: gate(ready, "runtime_durable_adapter_auth_blocked"),
|
||||
schema: gate(ready),
|
||||
migration: gate(ready),
|
||||
durability: gate(ready)
|
||||
auth: gate(ready || !["auth"].includes(blockedLayer), blockedLayer === "auth" ? blocker : null),
|
||||
schema: gate(ready || !["schema"].includes(blockedLayer), blockedLayer === "schema" ? blocker : null),
|
||||
migration: gate(ready || !["migration"].includes(blockedLayer), blockedLayer === "migration" ? blocker : null),
|
||||
durability: gate(ready, blocker)
|
||||
}
|
||||
},
|
||||
readiness: {
|
||||
@@ -230,11 +303,14 @@ function apiPayload({ ready }) {
|
||||
status: ready ? "ready" : "blocked",
|
||||
durability: {
|
||||
ready,
|
||||
blockedLayer: ready ? null : "auth"
|
||||
status: ready ? "ready" : "blocked",
|
||||
blocker: ready ? null : blocker,
|
||||
blockedLayer: ready ? null : blockedLayer,
|
||||
queryResult
|
||||
}
|
||||
},
|
||||
blockerCodes: ready ? []
|
||||
: ["runtime_durable_adapter_auth_blocked"],
|
||||
: [blocker],
|
||||
m3IoControl: {
|
||||
route: "/v1/m3/io",
|
||||
status: "available",
|
||||
|
||||
Reference in New Issue
Block a user