test: guard degraded dev gate acceptance

This commit is contained in:
Code Queue Review
2026-05-23 03:18:25 +00:00
parent 96e802b2a2
commit 74177c8cc4
8 changed files with 615 additions and 79 deletions
+146 -5
View File
@@ -391,6 +391,13 @@ async function runLiveSmoke(args) {
const blockers = [];
const sourceIdentity = observeSourceIdentity();
const runtimeIdentity = await observeLiveRuntimeIdentity(runtime.endpoints.api);
const apiRuntimeReadiness = classifyLiveApiRuntimeReadiness(runtimeIdentity);
addCheck(checks, blockers, "live-api-runtime-readiness", apiRuntimeReadiness.status, apiRuntimeReadiness.summary, {
blocker: "runtime_blocker",
evidence: apiRuntimeReadiness.evidence,
observations: apiRuntimeReadiness
});
const expectedRuntimeIdentity = observeExpectedRuntimeIdentity("hwlab-cloud-api");
const deploymentIdentity = classifyLiveDeploymentIdentity(sourceIdentity, runtimeIdentity, expectedRuntimeIdentity);
addCheck(checks, blockers, "live-runtime-current-main", deploymentIdentity.status, deploymentIdentity.summary, {
@@ -523,14 +530,18 @@ async function runLiveSmoke(args) {
});
}
const status = blockers.length === 0 ? "pass" : "blocked";
const status = blockers.length === 0
? apiRuntimeReadiness.status === "degraded" ? "degraded" : "pass"
: "blocked";
return baseReport({
mode: "live",
url: args.url,
status,
checks,
blockers,
evidenceLevel: status === "pass" ? "DEV-LIVE-BROWSER" : "BLOCKED",
evidenceLevel: status === "pass"
? "DEV-LIVE-BROWSER"
: status === "degraded" ? "DEV-LIVE-BROWSER-DEGRADED" : "BLOCKED",
devLive: status === "pass",
runtimeIdentity,
sourceIdentity,
@@ -545,7 +556,9 @@ async function runLiveSmoke(args) {
sourceIsDevLive: false,
liveMode: "browser-user-journey-with-code-agent-post",
retainedApiFields: ["status", "provider", "model", "backend", "traceId", "hasReply", "error.code", "error.missingEnv", "error.providerStatus"],
statement: "Live smoke opens the deployed workbench and sends one controlled Code Agent message; it does not call hardware write APIs and must not be used to infer M3 DEV-LIVE hardware-loop acceptance."
statement: status === "degraded"
? "Live smoke records deployed UI usable in degraded/read-only mode only; API health/runtime durability is degraded, so this is not full DEV-LIVE acceptance and must not be used to infer M3/M4/M5 acceptance."
: "Live smoke opens the deployed workbench and sends one controlled Code Agent message; it does not call hardware write APIs and must not be used to infer M3 DEV-LIVE hardware-loop acceptance."
}
});
}
@@ -567,6 +580,7 @@ function baseReport({
webAssetIdentity = null
}) {
const liveJourneyPassed = mode === "live" && status === "pass";
const liveJourneyDegraded = mode === "live" && status === "degraded";
return {
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-cloud-workbench-live.json",
@@ -584,7 +598,9 @@ function baseReport({
activeEndpoint: runtime.endpoints.api,
activeBrowserEndpoint: runtime.endpoints.frontend,
deprecatedEndpoint: null,
summary: "Current deployed browser user journey report for the 16666 Cloud Workbench; it does not substitute for M3/M4/M5 hardware acceptance."
summary: liveJourneyDegraded
? "Current deployed browser user journey report for the 16666 Cloud Workbench; deployed UI usable in degraded/read-only mode and does not substitute for M3/M4/M5 DEV-LIVE acceptance."
: "Current deployed browser user journey report for the 16666 Cloud Workbench; it does not substitute for M3/M4/M5 hardware acceptance."
},
task: "DC-DCSN-P0-2026-003",
refs: [
@@ -643,7 +659,7 @@ function baseReport({
summary: "The journey evidence is collected from the real DEV browser endpoint, not a dry-run fixture."
},
devPreconditions: {
status: liveJourneyPassed ? "pass" : "blocked",
status: liveJourneyPassed ? "pass" : liveJourneyDegraded ? "degraded" : "blocked",
requirements: [
"GET http://74.48.78.17:16666/ serves the Cloud Workbench HTML.",
"Browser DOM exposes the default workbench and core controls.",
@@ -655,6 +671,8 @@ function baseReport({
],
summary: liveJourneyPassed
? "Deployed 16666 browser journey and same-origin Code Agent chat completed."
: liveJourneyDegraded
? "Deployed UI usable in degraded/read-only mode; API health/runtime durability is degraded, so this is not full DEV-LIVE acceptance."
: mode === "live"
? "Deployed browser journey is blocked; see checks and blockers."
: "Static/local fixture evidence does not establish the deployed 16666 browser journey."
@@ -1417,6 +1435,75 @@ function staticSafety() {
};
}
export function classifyLiveApiRuntimeReadiness(runtimeIdentity) {
const healthStatus = sanitizeRuntimeString(runtimeIdentity?.healthStatus);
const runtime = runtimeIdentity?.runtime ?? {};
const readiness = runtimeIdentity?.readiness ?? {};
const durability = readiness?.durability ?? {};
const blockerCodes = Array.isArray(runtimeIdentity?.blockerCodes)
? runtimeIdentity.blockerCodes.map((item) => sanitizeRuntimeString(item))
: [];
const ready = runtimeIdentity?.ready === true || readiness?.ready === true;
const runtimeReady = runtime?.ready === true;
const durabilityReady = durability?.ready === true;
const runtimeDurable = runtime?.durable === true;
const durableBlocked = runtime?.blocker === "runtime_durable_adapter_query_blocked" ||
durability?.blocker === "runtime_durable_adapter_query_blocked" ||
blockerCodes.includes("runtime_durable_adapter_query_blocked") ||
durability?.blockedLayer === "durability_query" ||
runtime?.connection?.queryResult === "query_blocked" ||
durability?.queryResult === "query_blocked";
const degraded = healthStatus === "degraded" ||
ready === false ||
runtimeReady === false ||
durabilityReady === false ||
runtimeDurable === false ||
durableBlocked;
if (!degraded) {
return {
status: "pass",
healthStatus,
ready,
runtimeDurable,
runtimeReady,
durabilityReady,
durableBlocked,
blockerCodes,
evidence: [
`api.status=${healthStatus}`,
`ready=${ready}`,
`runtime.durable=${runtimeDurable}`,
`runtime.ready=${runtimeReady}`,
`durability.ready=${durabilityReady}`
],
summary: "Live API health and runtime durability are ready for browser journey evidence."
};
}
const blockedEvidence = [
`api.status=${healthStatus}`,
`ready=${ready}`,
`runtime.durable=${runtimeDurable}`,
`runtime.ready=${runtimeReady}`,
`durability.ready=${durabilityReady}`,
durableBlocked ? "runtime_durable_adapter_query_blocked" : null,
...blockerCodes.map((code) => `blocker=${code}`)
].filter(Boolean);
return {
status: "degraded",
healthStatus,
ready,
runtimeDurable,
runtimeReady,
durabilityReady,
durableBlocked,
blockerCodes,
evidence: blockedEvidence,
summary: "Live API is reachable but degraded/read-only; runtime durability is not ready, so this cannot be full DEV-LIVE acceptance."
};
}
async function observeLiveRuntimeIdentity(apiEndpoint) {
const endpoint = new URL("/health/live", apiEndpoint).toString();
const controller = new AbortController();
@@ -1442,10 +1529,14 @@ async function observeLiveRuntimeIdentity(apiEndpoint) {
serviceId: sanitizeRuntimeString(body.serviceId),
environment: sanitizeRuntimeString(body.environment),
healthStatus: sanitizeRuntimeString(body.status),
ready: body.ready === true,
commitId: sanitizeRevision(body.commit?.id ?? body.commitId),
commitSource: sanitizeRuntimeString(body.commit?.source),
imageTag: sanitizeRuntimeString(body.image?.tag),
observedAt: sanitizeTimestamp(body.observedAt),
runtime: summarizeRuntimeHealth(body.runtime),
readiness: summarizeReadinessHealth(body.readiness),
blockerCodes: sanitizeStringList(body.blockerCodes),
summary: "Live runtime identity was observed through the existing read-only health endpoint and is not inferred from source git HEAD."
};
} catch (error) {
@@ -1466,15 +1557,65 @@ function notObservedRuntimeIdentity(reason, endpoint = new URL("/health/live", r
serviceId: "not_observed",
environment: "not_observed",
healthStatus: "not_observed",
ready: false,
commitId: "not_observed",
commitSource: "not_observed",
imageTag: "not_observed",
observedAt: new Date().toISOString(),
runtime: {
durable: false,
ready: false,
status: "not_observed",
blocker: "not_observed"
},
readiness: {
ready: false,
status: "not_observed",
durability: {
ready: false,
status: "not_observed",
blocker: "not_observed",
blockedLayer: "not_observed",
queryResult: "not_observed"
}
},
blockerCodes: [],
reason: oneLine(reason),
summary: "Live runtime identity is recorded as not_observed; source identity must not be treated as the deployed runtime revision."
};
}
function summarizeRuntimeHealth(runtimeHealth) {
return {
durable: runtimeHealth?.durable === true,
ready: runtimeHealth?.ready === true,
status: sanitizeRuntimeString(runtimeHealth?.status),
blocker: sanitizeRuntimeString(runtimeHealth?.blocker),
connection: {
queryAttempted: runtimeHealth?.connection?.queryAttempted === true,
queryResult: sanitizeRuntimeString(runtimeHealth?.connection?.queryResult)
}
};
}
function summarizeReadinessHealth(readiness) {
return {
ready: readiness?.ready === true,
status: sanitizeRuntimeString(readiness?.status),
durability: {
ready: readiness?.durability?.ready === true,
status: sanitizeRuntimeString(readiness?.durability?.status),
blocker: sanitizeRuntimeString(readiness?.durability?.blocker),
blockedLayer: sanitizeRuntimeString(readiness?.durability?.blockedLayer),
queryResult: sanitizeRuntimeString(readiness?.durability?.queryResult)
}
};
}
function sanitizeStringList(value) {
return Array.isArray(value) ? value.map((item) => sanitizeRuntimeString(item)) : [];
}
function sanitizeRevision(value) {
if (typeof value !== "string") return "unknown";
const trimmed = value.trim().toLowerCase();
@@ -109,15 +109,15 @@ const frontendDevFact = Object.freeze({
"web/hwlab-cloud-web/styles.css"
],
evidence: [
`Cloud Web /health/live accepted revision ${latestFrontendRevision}`,
"Cloud Workbench public browser endpoint is the active frontend route only",
`Cloud Web /health/live observed revision ${latestFrontendRevision}`,
"Cloud Workbench public browser endpoint is usable in degraded/read-only mode only",
"Frontend load/revision evidence cannot satisfy DB, M3 hardware-loop, M4 agent-loop, or M5 MVP e2e acceptance"
],
commands: [
"node web/hwlab-cloud-web/scripts/check.mjs",
"node scripts/dev-cloud-workbench-smoke.mjs --static"
],
summary: `#99/#108 Cloud Workbench revision ${latestFrontendRevision} is the latest accepted DEV frontend fact, but it is frontend-only evidence.`
summary: `#99/#108 Cloud Workbench revision ${latestFrontendRevision} is the latest DEV frontend visibility fact; API/runtime durability remains degraded, so it is degraded/read-only UI evidence only.`
});
function issue(id) {
@@ -949,7 +949,7 @@ function buildCurrentDevLayering(reports, blockers) {
label: "Frontend DEV revision",
status: "pass",
evidenceLevel: "DEV-LIVE",
summary: `${activeBrowserRoot} serves the accepted Cloud Workbench frontend revision ${latestFrontendRevision}; this is browser/frontend evidence only.`,
summary: `${activeBrowserRoot} serves the Cloud Workbench frontend revision ${latestFrontendRevision}; deployed UI is usable in degraded/read-only mode only and this is browser/frontend evidence only.`,
evidence: frontendDevFact.evidence,
nextRequired: "Keep frontend revision proof separate from DB live readiness, M3 hardware-loop evidence, M4 agent-loop evidence, and M5 acceptance."
},
@@ -958,7 +958,7 @@ function buildCurrentDevLayering(reports, blockers) {
status: m2EndpointLive || edgeLive ? "pass" : "blocked",
evidenceLevel: m2EndpointLive || edgeLive ? "DEV-LIVE" : "BLOCKED",
summary: m2EndpointLive
? `${activeBrowserRoot}, ${DEV_ENDPOINT}/health, and ${activeApiLiveEndpoint} returned accepted HWLAB DEV responses in the active M2 read-only smoke.`
? `${activeBrowserRoot}, ${DEV_ENDPOINT}/health, and ${activeApiLiveEndpoint} returned observed HWLAB DEV route responses in the active M2 read-only smoke.`
: `No active read-only public endpoint report proves both ${activeBrowserRoot} and ${activeApiLiveEndpoint}.`,
evidence: publicEndpointEvidence(reports.devM2Smoke),
nextRequired: "Keep this separated from DB readiness, M3/M4 loop evidence, and M5 acceptance."
@@ -1299,7 +1299,7 @@ export async function buildReport() {
green: dod.green,
reason: dod.green
? "All #9 DEV DoD checks are green."
: `Frontend revision ${latestFrontendRevision} and EDGE/ROUTE DEV-LIVE evidence exist on :16666/:16667, but M5 remains blocked by artifact source drift, DB live degradation, missing M3 trusted loop operation evidence, and blocked M4 agent-loop preflight.`
: `Frontend revision ${latestFrontendRevision} is usable in degraded/read-only mode and EDGE/ROUTE DEV-LIVE evidence exists on :16666/:16667, but M5 remains blocked by artifact source drift, API/runtime durability degradation, missing M3 trusted loop operation evidence, and blocked M4 agent-loop preflight.`
},
latestFrontendDevFact: {
...frontendDevFact,
@@ -1382,7 +1382,7 @@ ${report.overall.reason}
## Frontend DEV Fact
The latest accepted #99/#108 frontend DEV fact is revision \`${report.latestFrontendDevFact.revision}\` at \`${report.latestFrontendDevFact.endpoint}\`. This is ${report.latestFrontendDevFact.evidenceLevel} browser/frontend evidence only and does not promote M3, M4, or M5.
The latest #99/#108 frontend DEV visibility fact is revision \`${report.latestFrontendDevFact.revision}\` at \`${report.latestFrontendDevFact.endpoint}\`. It means the deployed UI is usable in degraded/read-only mode only; this is ${report.latestFrontendDevFact.evidenceLevel} browser/frontend evidence and does not promote M3, M4, or M5.
## Current DEV Layering
@@ -21,6 +21,76 @@ function cloneBaseAggregatorReport() {
return JSON.parse(JSON.stringify(baseAggregatorReport));
}
function fullAcceptanceReport() {
const report = cloneBaseReport();
report.status = "pass";
report.evidenceLevel = "DEV-LIVE-BROWSER";
report.devLive = true;
report.devPreconditions.status = "pass";
report.devPreconditions.summary = "Deployed 16666 browser journey and same-origin Code Agent chat completed.";
report.reportLifecycle.summary = "Current deployed browser user journey report for the 16666 Cloud Workbench; it does not substitute for M3/M4/M5 hardware acceptance.";
report.safety.statement = "Live smoke opens the deployed workbench and sends one controlled Code Agent message; it does not call hardware write APIs and must not be used to infer M3 DEV-LIVE hardware-loop acceptance.";
Object.assign(report.runtimeIdentity, {
healthStatus: "ok",
ready: true,
runtime: {
durable: true,
ready: true,
status: "ok",
blocker: "none",
connection: {
queryAttempted: true,
queryResult: "ok"
}
},
readiness: {
ready: true,
status: "ok",
durability: {
ready: true,
status: "ready",
blocker: "none",
blockedLayer: "none",
queryResult: "ok"
}
},
blockerCodes: []
});
const readiness = report.checks.find((check) => check.id === "live-api-runtime-readiness");
if (readiness) {
Object.assign(readiness, {
status: "pass",
summary: "Live API health and runtime durability are ready for browser journey evidence.",
evidence: [
"api.status=ok",
"ready=true",
"runtime.durable=true",
"runtime.ready=true",
"durability.ready=true"
],
observations: {
status: "pass",
healthStatus: "ok",
ready: true,
runtimeDurable: true,
runtimeReady: true,
durabilityReady: true,
durableBlocked: false,
blockerCodes: [],
evidence: [
"api.status=ok",
"ready=true",
"runtime.durable=true",
"runtime.ready=true",
"durability.ready=true"
],
summary: "Live API health and runtime durability are ready for browser journey evidence."
}
});
}
return report;
}
function preflightBlockedReport() {
const report = cloneBaseReport();
report.status = "blocked";
@@ -177,6 +247,51 @@ test("accepts the sanitized deployed workbench evidence contract", async () => {
});
});
test("accepts full workbench acceptance only when API health and runtime durability are ready", async () => {
await withReport(fullAcceptanceReport(), async (reportPath) => {
const result = runValidator(reportPath);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /validated 1 dev-gate report JSON file/);
});
});
test("rejects full workbench acceptance when API health is degraded", async () => {
const report = fullAcceptanceReport();
report.runtimeIdentity.healthStatus = "degraded";
report.checks.find((check) => check.id === "live-api-runtime-readiness").observations.healthStatus = "degraded";
await assertRejected(report, /degraded API or runtime durability blocker cannot be summarized as full DEV-LIVE accepted/);
});
test("rejects full workbench acceptance when runtime durability query is blocked", async () => {
const report = fullAcceptanceReport();
Object.assign(report.runtimeIdentity.runtime, {
durable: false,
ready: false,
status: "degraded",
blocker: "runtime_durable_adapter_query_blocked",
connection: {
queryAttempted: true,
queryResult: "query_blocked"
}
});
Object.assign(report.runtimeIdentity.readiness.durability, {
ready: false,
status: "blocked",
blocker: "runtime_durable_adapter_query_blocked",
blockedLayer: "durability_query",
queryResult: "query_blocked"
});
report.runtimeIdentity.blockerCodes = ["runtime_durable_adapter_query_blocked"];
await assertRejected(report, /degraded API or runtime durability blocker cannot be summarized as full DEV-LIVE accepted/);
});
test("rejects degraded workbench evidence with full-acceptance wording", async () => {
const report = cloneBaseReport();
report.devPreconditions.summary = "M3/M4/M5 DEV-LIVE accepted.";
report.safety.statement = "Full DEV-LIVE accepted.";
await assertRejected(report, /degraded API\/runtime report must not claim full M3\/M4\/M5 DEV-LIVE acceptance/);
});
test("accepts preflight-blocked workbench evidence when Code Agent POST is marked not_sent", async () => {
await withReport(preflightBlockedReport(), async (reportPath) => {
const result = runValidator(reportPath);
+214 -24
View File
@@ -1351,13 +1351,24 @@ async function validateDevCloudWorkbenchLiveReport(report, label, raw) {
assert.equal(report.devOnly, true, `${label}.devOnly`);
assert.equal(report.prodDisabled, true, `${label}.prodDisabled`);
assertStatus(report.status, `${label}.status`);
assert.equal(report.status, report.blockers.length === 0 ? "pass" : "blocked", `${label}.status must match blockers`);
assert.equal(
report.blockers.length === 0 ? ["pass", "degraded"].includes(report.status) : report.status === "blocked",
true,
`${label}.status must match blockers`
);
assertTimestamp(report.generatedAt, `${label}.generatedAt`);
assert.equal(report.mode, "live", `${label}.mode`);
assert.equal(report.url, "http://74.48.78.17:16666/", `${label}.url`);
assert.equal(report.evidenceLevel, report.status === "pass" ? "DEV-LIVE-BROWSER" : "BLOCKED", `${label}.evidenceLevel`);
assert.equal(
report.evidenceLevel,
report.status === "pass"
? "DEV-LIVE-BROWSER"
: report.status === "degraded" ? "DEV-LIVE-BROWSER-DEGRADED" : "BLOCKED",
`${label}.evidenceLevel`
);
assert.equal(report.devLive, report.status === "pass", `${label}.devLive`);
assertDevCloudWorkbenchIdentity(report, label);
const apiRuntimeReadiness = assertDevCloudWorkbenchApiRuntimeReadiness(report, label);
assertDevCloudWorkbenchEndpoints(report, label);
assertObject(report.sourceContract, `${label}.sourceContract`);
@@ -1424,6 +1435,7 @@ async function validateDevCloudWorkbenchLiveReport(report, label, raw) {
for (const requiredCheck of [
"live-http-html",
"live-browser-dom",
"live-api-runtime-readiness",
"live-code-agent-browser-journey"
]) {
assert.ok(checks.has(requiredCheck), `${label}.checks missing ${requiredCheck}`);
@@ -1445,8 +1457,24 @@ async function validateDevCloudWorkbenchLiveReport(report, label, raw) {
assert.equal(dom.coreControlsVisible[control], true, `${label}.live-browser-dom.coreControlsVisible.${control}`);
}
const apiRuntimeReadinessCheck = checks.get("live-api-runtime-readiness");
assert.equal(
apiRuntimeReadinessCheck.status,
apiRuntimeReadiness.status,
`${label}.live-api-runtime-readiness.status`
);
assert.deepEqual(
apiRuntimeReadinessCheck.observations,
apiRuntimeReadiness,
`${label}.live-api-runtime-readiness.observations`
);
const journey = checks.get("live-code-agent-browser-journey");
assert.equal(journey.status, report.status, `${label}.live-code-agent-browser-journey.status`);
assert.equal(
journey.status,
report.status === "degraded" ? "pass" : report.status,
`${label}.live-code-agent-browser-journey.status`
);
assertObject(journey.observations, `${label}.live-code-agent-browser-journey.observations`);
assertObject(journey.observations.request, `${label}.live-code-agent-browser-journey.request`);
assert.equal(journey.observations.request.method, "POST", `${label}.live-code-agent-browser-journey.request.method`);
@@ -1486,6 +1514,9 @@ async function validateDevCloudWorkbenchLiveReport(report, label, raw) {
assert.equal(report.safety.liveMode, "browser-user-journey-with-code-agent-post", `${label}.safety.liveMode`);
assertCodeAgentRetainedApiFields(report.safety.retainedApiFields, `${label}.safety.retainedApiFields`);
assertString(report.safety.statement, `${label}.safety.statement`);
if (report.status === "degraded") {
assertDevCloudWorkbenchDegradedSummary(report, label, apiRuntimeReadiness);
}
assertFreshDevCloudWorkbenchLiveEvidence(report, label, raw, journey);
}
@@ -1537,6 +1568,157 @@ function assertDevCloudWorkbenchCodeAgentBlocked(report, label, journey) {
assertString(report.safety.statement, `${label}.safety.statement`);
}
function assertDevCloudWorkbenchApiRuntimeReadiness(report, label) {
assertObject(report.runtimeIdentity, `${label}.runtimeIdentity`);
if (Object.hasOwn(report.runtimeIdentity, "ready")) {
assertBoolean(report.runtimeIdentity.ready, `${label}.runtimeIdentity.ready`);
}
if (Object.hasOwn(report.runtimeIdentity, "runtime")) {
assertObject(report.runtimeIdentity.runtime, `${label}.runtimeIdentity.runtime`);
for (const field of ["durable", "ready"]) {
if (Object.hasOwn(report.runtimeIdentity.runtime, field)) {
assertBoolean(report.runtimeIdentity.runtime[field], `${label}.runtimeIdentity.runtime.${field}`);
}
}
if (Object.hasOwn(report.runtimeIdentity.runtime, "status")) {
assertString(report.runtimeIdentity.runtime.status, `${label}.runtimeIdentity.runtime.status`);
}
if (Object.hasOwn(report.runtimeIdentity.runtime, "blocker")) {
assertString(report.runtimeIdentity.runtime.blocker, `${label}.runtimeIdentity.runtime.blocker`);
}
}
if (Object.hasOwn(report.runtimeIdentity, "readiness")) {
assertObject(report.runtimeIdentity.readiness, `${label}.runtimeIdentity.readiness`);
if (Object.hasOwn(report.runtimeIdentity.readiness, "ready")) {
assertBoolean(report.runtimeIdentity.readiness.ready, `${label}.runtimeIdentity.readiness.ready`);
}
if (Object.hasOwn(report.runtimeIdentity.readiness, "status")) {
assertString(report.runtimeIdentity.readiness.status, `${label}.runtimeIdentity.readiness.status`);
}
if (Object.hasOwn(report.runtimeIdentity.readiness, "durability")) {
assertObject(report.runtimeIdentity.readiness.durability, `${label}.runtimeIdentity.readiness.durability`);
if (Object.hasOwn(report.runtimeIdentity.readiness.durability, "ready")) {
assertBoolean(report.runtimeIdentity.readiness.durability.ready, `${label}.runtimeIdentity.readiness.durability.ready`);
}
for (const field of ["status", "blocker", "blockedLayer", "queryResult"]) {
if (Object.hasOwn(report.runtimeIdentity.readiness.durability, field)) {
assertString(report.runtimeIdentity.readiness.durability[field], `${label}.runtimeIdentity.readiness.durability.${field}`);
}
}
}
}
if (Object.hasOwn(report.runtimeIdentity, "blockerCodes")) {
assertStringArray(report.runtimeIdentity.blockerCodes, `${label}.runtimeIdentity.blockerCodes`);
}
const runtime = report.runtimeIdentity.runtime ?? {};
const readiness = report.runtimeIdentity.readiness ?? {};
const durability = readiness.durability ?? {};
const blockerCodes = Array.isArray(report.runtimeIdentity.blockerCodes) ? report.runtimeIdentity.blockerCodes : [];
const durableBlocked = runtime.blocker === "runtime_durable_adapter_query_blocked" ||
durability.blocker === "runtime_durable_adapter_query_blocked" ||
blockerCodes.includes("runtime_durable_adapter_query_blocked") ||
durability.blockedLayer === "durability_query" ||
runtime.connection?.queryResult === "query_blocked" ||
durability.queryResult === "query_blocked";
const degraded = report.runtimeIdentity.healthStatus === "degraded" ||
report.runtimeIdentity.ready === false ||
runtime.ready === false ||
durability.ready === false ||
runtime.durable === false ||
durableBlocked;
const status = degraded ? "degraded" : "pass";
const result = {
status,
healthStatus: report.runtimeIdentity.healthStatus,
ready: report.runtimeIdentity.ready === true,
runtimeDurable: runtime.durable === true,
runtimeReady: runtime.ready === true,
durabilityReady: durability.ready === true,
durableBlocked,
blockerCodes,
evidence: [
`api.status=${report.runtimeIdentity.healthStatus}`,
`ready=${report.runtimeIdentity.ready === true}`,
`runtime.durable=${runtime.durable === true}`,
`runtime.ready=${runtime.ready === true}`,
`durability.ready=${durability.ready === true}`,
durableBlocked ? "runtime_durable_adapter_query_blocked" : null,
...blockerCodes.map((code) => `blocker=${code}`)
].filter(Boolean),
summary: degraded
? "Live API is reachable but degraded/read-only; runtime durability is not ready, so this cannot be full DEV-LIVE acceptance."
: "Live API health and runtime durability are ready for browser journey evidence."
};
if (["pass", "degraded"].includes(report.status)) {
assert.equal(report.runtimeIdentity.status, "observed", `${label}.runtimeIdentity.status`);
assert.equal(report.runtimeIdentity.source, "health-live", `${label}.runtimeIdentity.source`);
assert.equal(report.runtimeIdentity.endpoint, `${ACTIVE_DEV_PUBLIC_ENDPOINT}/health/live`, `${label}.runtimeIdentity.endpoint`);
assert.equal(report.runtimeIdentity.serviceId, "hwlab-cloud-api", `${label}.runtimeIdentity.serviceId`);
assert.equal(report.runtimeIdentity.environment, "dev", `${label}.runtimeIdentity.environment`);
assert.match(report.runtimeIdentity.commitId, /^[a-f0-9]{7,40}$/, `${label}.runtimeIdentity.commitId`);
assert.notEqual(report.runtimeIdentity.commitId, "unknown", `${label}.runtimeIdentity.commitId`);
assert.notEqual(report.runtimeIdentity.commitSource, "source-git-head", `${label}.runtimeIdentity.commitSource`);
assert.notEqual(report.runtimeIdentity.commitSource, "hard-coded", `${label}.runtimeIdentity.commitSource`);
assert.notEqual(report.runtimeIdentity.commitSource, "literal", `${label}.runtimeIdentity.commitSource`);
const generatedAtMs = Date.parse(report.generatedAt);
const runtimeObservedAtMs = Date.parse(report.runtimeIdentity.observedAt);
assert.ok(
Math.abs(generatedAtMs - runtimeObservedAtMs) <= devCloudWorkbenchRuntimeIdentityMaxSkewMs,
`${label}.runtimeIdentity.observedAt must be fresh relative to generatedAt`
);
}
if (report.status === "pass") {
assert.equal(
degraded,
false,
`${label}.runtimeIdentity degraded API or runtime durability blocker cannot be summarized as full DEV-LIVE accepted`
);
}
if (degraded) {
assert.ok(
["degraded", "blocked"].includes(report.status),
`${label}.status must be degraded or blocked when API health or runtime durability is degraded`
);
assert.equal(report.devLive, false, `${label}.devLive must remain false for degraded/read-only workbench evidence`);
assert.equal(
report.evidenceLevel,
report.status === "degraded" ? "DEV-LIVE-BROWSER-DEGRADED" : "BLOCKED",
`${label}.evidenceLevel`
);
}
return result;
}
function assertDevCloudWorkbenchDegradedSummary(report, label, apiRuntimeReadiness) {
assert.equal(apiRuntimeReadiness.status, "degraded", `${label}.live-api-runtime-readiness.status`);
assert.equal(report.devPreconditions.status, "degraded", `${label}.devPreconditions.status`);
const summaryText = [
report.reportLifecycle.summary,
report.devPreconditions.summary,
report.safety.statement
].join(" ");
assert.match(
summaryText,
/degraded\/read-only mode|degraded\/read-only|read-only mode/u,
`${label} degraded API/runtime report must be summarized as deployed UI usable in degraded/read-only mode`
);
assert.doesNotMatch(
summaryText,
/full DEV-LIVE accepted|full DEV-LIVE acceptance passed|M3\/M4\/M5 DEV-LIVE accepted|M[345]\s+(?:DEV-LIVE\s+)?(?:accepted|acceptance passed|green)/u,
`${label} degraded API/runtime report must not claim full M3/M4/M5 DEV-LIVE acceptance`
);
}
function assertCodeAgentRetainedApiFields(value, label) {
assertArray(value, label);
for (const field of ["status", "provider", "model", "backend", "traceId", "hasReply", "error.code", "error.missingEnv"]) {
@@ -1763,31 +1945,39 @@ function assertFreshDevCloudWorkbenchLiveEvidence(report, label, raw, journey) {
`${label} deployed workbench report must not contain deprecated public endpoint evidence`
);
if (report.status !== "pass") {
if (!["pass", "degraded"].includes(report.status)) {
return;
}
assert.equal(report.devLive, true, `${label}.devLive`);
assert.equal(report.evidenceLevel, "DEV-LIVE-BROWSER", `${label}.evidenceLevel`);
assert.equal(report.safety.sourceIsDevLive, false, `${label}.safety.sourceIsDevLive`);
assert.equal(report.runtimeIdentity.status, "observed", `${label}.runtimeIdentity.status`);
assert.equal(report.runtimeIdentity.source, "health-live", `${label}.runtimeIdentity.source`);
assert.equal(report.runtimeIdentity.endpoint, `${ACTIVE_DEV_PUBLIC_ENDPOINT}/health/live`, `${label}.runtimeIdentity.endpoint`);
assert.equal(report.runtimeIdentity.serviceId, "hwlab-cloud-api", `${label}.runtimeIdentity.serviceId`);
assert.equal(report.runtimeIdentity.environment, "dev", `${label}.runtimeIdentity.environment`);
assert.match(report.runtimeIdentity.commitId, /^[a-f0-9]{7,40}$/, `${label}.runtimeIdentity.commitId`);
assert.notEqual(report.runtimeIdentity.commitId, "unknown", `${label}.runtimeIdentity.commitId`);
assert.notEqual(report.runtimeIdentity.commitSource, "source-git-head", `${label}.runtimeIdentity.commitSource`);
assert.notEqual(report.runtimeIdentity.commitSource, "hard-coded", `${label}.runtimeIdentity.commitSource`);
assert.notEqual(report.runtimeIdentity.commitSource, "literal", `${label}.runtimeIdentity.commitSource`);
const generatedAtMs = Date.parse(report.generatedAt);
const runtimeObservedAtMs = Date.parse(report.runtimeIdentity.observedAt);
assert.ok(
Math.abs(generatedAtMs - runtimeObservedAtMs) <= devCloudWorkbenchRuntimeIdentityMaxSkewMs,
`${label}.runtimeIdentity.observedAt must be fresh relative to generatedAt`
assert.equal(report.devLive, report.status === "pass", `${label}.devLive`);
assert.equal(
report.evidenceLevel,
report.status === "pass" ? "DEV-LIVE-BROWSER" : "DEV-LIVE-BROWSER-DEGRADED",
`${label}.evidenceLevel`
);
assert.equal(report.safety.sourceIsDevLive, false, `${label}.safety.sourceIsDevLive`);
if (report.status === "pass") {
assert.notEqual(report.runtimeIdentity.healthStatus, "degraded", `${label}.runtimeIdentity.healthStatus`);
assert.equal(report.runtimeIdentity.ready, true, `${label}.runtimeIdentity.ready`);
assert.equal(report.runtimeIdentity.runtime?.durable, true, `${label}.runtimeIdentity.runtime.durable`);
assert.equal(report.runtimeIdentity.runtime?.ready, true, `${label}.runtimeIdentity.runtime.ready`);
assert.equal(report.runtimeIdentity.readiness?.durability?.ready, true, `${label}.runtimeIdentity.readiness.durability.ready`);
assert.notEqual(
report.runtimeIdentity.runtime?.blocker,
"runtime_durable_adapter_query_blocked",
`${label}.runtimeIdentity.runtime.blocker`
);
assert.notEqual(
report.runtimeIdentity.readiness?.durability?.blocker,
"runtime_durable_adapter_query_blocked",
`${label}.runtimeIdentity.readiness.durability.blocker`
);
assert.equal(
(report.runtimeIdentity.blockerCodes ?? []).includes("runtime_durable_adapter_query_blocked"),
false,
`${label}.runtimeIdentity.blockerCodes`
);
}
assert.equal(
report.sourceIdentity.reportCommitId === "unknown" || report.sourceIdentity.worktreeState === "clean",
true,