From 50256826d79ab9c4e8d5ba40081720a111adf69d Mon Sep 17 00:00:00 2001 From: unidesk-code-queue-runner Date: Sat, 23 May 2026 15:29:57 +0000 Subject: [PATCH] fix: correct durable readiness attribution --- .../durable-readiness-status-matrix.json | 240 ++++++++++++++++++ web/hwlab-cloud-web/live-status.mjs | 71 ++++-- .../scripts/live-status-contract.test.mjs | 126 ++++++++- 3 files changed, 420 insertions(+), 17 deletions(-) create mode 100644 fixtures/cloud-web/durable-readiness-status-matrix.json diff --git a/fixtures/cloud-web/durable-readiness-status-matrix.json b/fixtures/cloud-web/durable-readiness-status-matrix.json new file mode 100644 index 00000000..639ad60c --- /dev/null +++ b/fixtures/cloud-web/durable-readiness-status-matrix.json @@ -0,0 +1,240 @@ +{ + "runtime": { + "durableReady": { + "runtime": { + "adapter": "postgres", + "durable": true, + "durableRequested": true, + "durableCapable": true, + "ready": true, + "status": "ready", + "blocker": null, + "liveRuntimeEvidence": true, + "connection": { + "queryAttempted": true, + "queryResult": "durable_readiness_ready" + }, + "durabilityContract": { + "ready": true, + "status": "ready", + "blockedLayer": null, + "blocker": null, + "liveRuntimeEvidence": true, + "secretMaterialRead": false + }, + "gates": { + "ssl": { "checked": true, "ready": true, "status": "ready", "blocker": null }, + "auth": { "checked": true, "ready": true, "status": "ready", "blocker": null }, + "schema": { "checked": true, "ready": true, "status": "ready", "blocker": null }, + "migration": { "checked": true, "ready": true, "status": "ready", "blocker": null }, + "durability": { "checked": true, "ready": true, "status": "ready", "blocker": null } + } + }, + "readiness": { + "contractVersion": "v3", + "ready": true, + "status": "ok", + "blockerCodes": [], + "blockers": [], + "components": { + "db": "ready", + "codeAgent": "ready", + "runtime": "ready" + }, + "durability": { + "status": "ready", + "ready": true, + "adapter": "postgres", + "durable": true, + "durableRequested": true, + "durableCapable": true, + "runtimeStatus": "ready", + "runtimeReady": true, + "liveRuntimeEvidence": true, + "dbLiveEvidenceObserved": true, + "dbLiveEvidenceIsDurabilityEvidence": false, + "blockedLayer": null, + "blocker": null, + "queryAttempted": true, + "queryResult": "durable_readiness_ready", + "secretMaterialRead": false + } + }, + "blockerCodes": [] + }, + "durableQueryBlocked": { + "status": "degraded", + "ready": false, + "runtime": { + "adapter": "postgres", + "durable": true, + "durableRequested": true, + "ready": false, + "status": "blocked", + "blocker": "runtime_durable_adapter_query_blocked", + "liveRuntimeEvidence": false, + "connection": { + "queryAttempted": true, + "queryResult": "query_blocked" + }, + "durabilityContract": { + "ready": false, + "status": "blocked", + "blockedLayer": "durability_query", + "blocker": "runtime_durable_adapter_query_blocked" + } + }, + "readiness": { + "ready": false, + "status": "blocked", + "durability": { + "status": "blocked", + "ready": false, + "blocker": "runtime_durable_adapter_query_blocked", + "blockedLayer": "durability_query", + "queryResult": "query_blocked" + } + }, + "blockerCodes": ["runtime_durable_adapter_query_blocked"] + }, + "authBlocked": { + "status": "degraded", + "ready": false, + "runtime": { + "adapter": "postgres", + "durable": true, + "durableRequested": true, + "ready": false, + "status": "blocked", + "blocker": "runtime_durable_adapter_auth_blocked", + "liveRuntimeEvidence": false, + "connection": { + "queryAttempted": true, + "queryResult": "auth_blocked" + }, + "durabilityContract": { + "ready": false, + "status": "blocked", + "blockedLayer": "auth", + "blocker": "runtime_durable_adapter_auth_blocked" + } + }, + "readiness": { + "ready": false, + "status": "blocked", + "durability": { + "status": "blocked", + "ready": false, + "blocker": "runtime_durable_adapter_auth_blocked", + "blockedLayer": "auth", + "queryResult": "auth_blocked" + } + }, + "blockerCodes": ["runtime_durable_adapter_auth_blocked"] + }, + "schemaBlocked": { + "status": "degraded", + "ready": false, + "runtime": { + "adapter": "postgres", + "durable": true, + "durableRequested": true, + "ready": false, + "status": "blocked", + "blocker": "runtime_durable_adapter_schema_blocked", + "liveRuntimeEvidence": false, + "connection": { + "queryAttempted": true, + "queryResult": "schema_blocked" + }, + "durabilityContract": { + "ready": false, + "status": "blocked", + "blockedLayer": "schema", + "blocker": "runtime_durable_adapter_schema_blocked" + } + }, + "readiness": { + "ready": false, + "status": "blocked", + "durability": { + "status": "blocked", + "ready": false, + "blocker": "runtime_durable_adapter_schema_blocked", + "blockedLayer": "schema", + "queryResult": "schema_blocked" + } + }, + "blockerCodes": ["runtime_durable_adapter_schema_blocked"] + }, + "unknownMissingReadiness": { + "status": "unknown", + "ready": null + } + }, + "codeAgent": { + "readOnlySessionTools": { + "endpoint": "POST /v1/agent/chat", + "status": "available", + "ready": true, + "provider": "codex-readonly-runner", + "backend": "hwlab-cloud-api/controlled-readonly-session-registry", + "capabilityLevel": "read-only-session-tools", + "sessionMode": "controlled-readonly-session-registry", + "implementationType": "controlled-readonly-session-registry", + "trackingIssue": "#317", + "runner": { + "kind": "hwlab-readonly-runner", + "ready": true, + "writeCapable": false, + "durableSession": false, + "codexStdio": false + }, + "runnerLimitations": [ + "not-codex-stdio", + "not-write-capable", + "not-durable-session" + ], + "longLivedSessionGate": { + "status": "blocked", + "pass": false, + "sourceIssue": "pikasTech/HWLAB#317", + "blockers": [ + { + "code": "stdio_protocol_not_wired", + "sourceIssue": "pikasTech/HWLAB#317" + } + ] + } + } + }, + "m3": { + "controlReady": { + "status": "available", + "readiness": { + "status": "ready", + "controlReady": true, + "sourceKind": "DEV-LIVE", + "evidenceLevel": "DEV-LIVE" + } + }, + "trustedGreenSummary": { + "contractVersion": "m3-status-v1", + "status": "live", + "sourceKind": "DEV-LIVE", + "summary": "M3 trusted green fixture", + "patchPanel": { + "serviceId": "hwlab-patch-panel", + "connectionActive": true + }, + "trust": { + "durableStatus": "green", + "blocker": null, + "operationId": "op_m3_fixture_green", + "traceId": "trc_m3_fixture_green", + "auditId": "aud_m3_fixture_green", + "evidenceId": "evd_m3_fixture_green" + } + } + } +} diff --git a/web/hwlab-cloud-web/live-status.mjs b/web/hwlab-cloud-web/live-status.mjs index 22158374..20d40a17 100644 --- a/web/hwlab-cloud-web/live-status.mjs +++ b/web/hwlab-cloud-web/live-status.mjs @@ -173,15 +173,6 @@ function classifyApiProbe(probe, context) { const payload = probe.data; const serviceId = payload?.serviceId ?? context.serviceId; - if (context.apiPath === "/v1" && payload?.codeAgent) { - const codeAgentProbe = classifyCodeAgentProbe({ restIndex: probe }); - if (codeAgentProbe?.kind === "readonly") { - return { - ...codeAgentProbe, - rawStatus: rawStatusFrom(payload) - }; - } - } const durableReason = durableRuntimeReason(payload); if (durableReason) { return readOnlyProbe({ @@ -194,6 +185,16 @@ function classifyApiProbe(probe, context) { }); } + if (context.apiPath === "/v1" && payload?.codeAgent) { + const codeAgentProbe = classifyCodeAgentProbe({ restIndex: probe }); + if (codeAgentProbe && codeAgentProbe.kind !== "pass" && codeAgentProbe.kind !== "unverified") { + return { + ...codeAgentProbe, + rawStatus: rawStatusFrom(payload) + }; + } + } + const blocker = firstBlocker(payload); if (blocker) { const reasonCode = String(blocker.code ?? blocker.reason ?? "blocked"); @@ -549,13 +550,23 @@ export function classifyRuntimeDurableReadiness(payload = {}, options = {}) { durability?.queryResult, dbRuntimeReadiness?.queryResult ); + const queryBlocker = runtimeDurableBlockerFromQueryResult(queryResult); + const readyQueryResult = isRuntimeDurableReadyQueryResult(queryResult); + const explicitDurabilityReady = durabilityReadySignal({ + runtime, + durability, + dbRuntimeReadiness, + gateBlocker, + queryResult, + readyQueryResult, + queryBlocker + }); const directBlocker = firstNonEmpty( - runtime?.blocker, + explicitDurabilityReady ? "" : runtime?.blocker, durability?.blocker, dbRuntimeReadiness?.blocker, - blockerCodeStartingWith(payload, "runtime_durable_adapter_") + explicitDurabilityReady ? "" : blockerCodeStartingWith(payload, "runtime_durable_adapter_") ); - const queryBlocker = runtimeDurableBlockerFromQueryResult(queryResult); const reasonCode = firstNonEmpty(directBlocker, gateBlocker?.blocker, queryBlocker); const blockedLayer = firstNonEmpty( runtime?.durabilityContract?.blockedLayer, @@ -575,8 +586,6 @@ export function classifyRuntimeDurableReadiness(payload = {}, options = {}) { durability?.liveRuntimeEvidence === true || dbRuntimeReadiness?.liveRuntimeEvidence === true; const rawStatus = rawStatusFrom(payload); - const rawStatusBlocked = [...ERROR_STATUSES, ...BLOCKED_STATUSES, ...RAW_DEGRADED_STATUSES].includes(normalizedStatus(rawStatus)); - const readyQueryResult = isRuntimeDurableReadyQueryResult(queryResult); const postgresRequired = options.requirePostgresAdapter === true; const adapterReady = !postgresRequired || adapter === "postgres"; const ready = adapterReady && @@ -585,7 +594,8 @@ export function classifyRuntimeDurableReadiness(payload = {}, options = {}) { durabilityReady && liveRuntimeEvidence && !reasonCode && - !rawStatusBlocked; + !blockedLayer && + (!queryResult || readyQueryResult); if (ready) { return { @@ -622,7 +632,7 @@ export function classifyRuntimeDurableReadiness(payload = {}, options = {}) { runtimeDurable ? "" : "runtime_durable_adapter_missing", runtimeReady ? "" : "runtime_durable_runtime_not_ready", liveRuntimeEvidence ? "" : "runtime_durable_live_evidence_missing", - rawStatusBlocked ? rawStatus : "", + isBlockedRuntimeStatus(rawStatus) ? rawStatus : "", "runtime_durable_adapter_not_ready" ); const layer = firstNonEmpty( @@ -658,6 +668,35 @@ export function classifyRuntimeDurableReadiness(payload = {}, options = {}) { }; } +function durabilityReadySignal({ + runtime, + durability, + dbRuntimeReadiness, + gateBlocker, + queryResult, + readyQueryResult, + queryBlocker +}) { + if (gateBlocker || queryBlocker) return false; + if (queryResult && !readyQueryResult) return false; + const ready = durability?.ready === true || + dbRuntimeReadiness?.ready === true || + runtime?.durabilityContract?.ready === true; + if (!ready) return false; + return !firstNonEmpty( + durability?.blocker, + dbRuntimeReadiness?.blocker, + runtime?.durabilityContract?.blocker, + durability?.blockedLayer, + dbRuntimeReadiness?.blockedLayer, + runtime?.durabilityContract?.blockedLayer + ); +} + +function isBlockedRuntimeStatus(status) { + return [...ERROR_STATUSES, ...BLOCKED_STATUSES, ...RAW_DEGRADED_STATUSES].includes(normalizedStatus(status)); +} + function firstBlockedRuntimeGate(gates = {}) { for (const layer of RUNTIME_DURABLE_GATE_LAYERS) { const gate = gates?.[layer]; diff --git a/web/hwlab-cloud-web/scripts/live-status-contract.test.mjs b/web/hwlab-cloud-web/scripts/live-status-contract.test.mjs index 4c9a3dfa..6613988a 100644 --- a/web/hwlab-cloud-web/scripts/live-status-contract.test.mjs +++ b/web/hwlab-cloud-web/scripts/live-status-contract.test.mjs @@ -1,7 +1,17 @@ import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; import test from "node:test"; -import { classifyWorkbenchLiveStatus } from "../live-status.mjs"; +import { + classifyRuntimeDurableReadiness, + classifyWorkbenchLiveStatus +} from "../live-status.mjs"; +import { wiringPresentationFromSummary } from "../wiring-status.mjs"; + +const statusMatrix = JSON.parse(readFileSync( + new URL("../../../fixtures/cloud-web/durable-readiness-status-matrix.json", import.meta.url), + "utf8" +)); const okApi = Object.freeze({ ok: true, @@ -609,6 +619,112 @@ test("keeps ready durable runtime green while attributing read-only mode to Code assert.doesNotMatch(healthLiveProbe.reason, /blocked/u); }); +test("fixture matrix keeps durable_readiness_ready green and attributes read-only mode to Code Agent", () => { + const restPayload = { + serviceId: "hwlab-cloud-api", + status: "ok", + ready: true, + ...statusMatrix.runtime.durableReady, + codeAgent: statusMatrix.codeAgent.readOnlySessionTools + }; + const durable = classifyRuntimeDurableReadiness(restPayload); + const status = classifyWorkbenchLiveStatus({ + healthLive: jsonProbe({ + serviceId: "hwlab-cloud-api", + status: "ok", + ready: true, + ...statusMatrix.runtime.durableReady + }), + restIndex: jsonProbe(restPayload), + health: okApi, + adapter: okApi, + m3Control: jsonProbe(statusMatrix.m3.controlReady), + m3Status: jsonProbe(statusMatrix.m3.trustedGreenSummary) + }); + + assert.equal(durable.ready, true); + assert.equal(durable.evidence.blocker, null); + assert.equal(durable.evidence.blockedLayer, null); + assert.equal(durable.evidence.queryResult, "durable_readiness_ready"); + assert.equal(status.kind, "readonly"); + assert.equal(status.reasonCode, "code_agent_read_only_session_tools"); + assert.equal(status.apiPath, "POST /v1/agent/chat"); + assert.match(status.detail, /#317 full pass not complete/u); + assert.doesNotMatch(status.detail, /runtime durable blocked|blocked at durability/u); +}); + +test("fixture matrix preserves runtime durable blocked attribution when durability is actually blocked", () => { + const status = classifyWorkbenchLiveStatus({ + healthLive: jsonProbe(statusMatrix.runtime.durableQueryBlocked), + restIndex: okRest, + health: okApi, + adapter: okApi, + m3Control: okM3Control, + m3Status: okM3Status + }); + const durable = classifyRuntimeDurableReadiness(statusMatrix.runtime.durableQueryBlocked); + + assert.equal(durable.ready, false); + assert.equal(durable.reasonCode, "runtime_durable_adapter_query_blocked"); + assert.equal(durable.evidence.blockedLayer, "durability_query"); + assert.equal(status.kind, "readonly"); + assert.equal(status.reasonCode, "runtime_durable_adapter_query_blocked"); + assert.match(status.detail, /runtime durable/u); + assert.match(status.detail, /query_blocked/u); +}); + +test("fixture matrix keeps auth and schema runtime blockers on their exact layers", () => { + for (const [name, expectedReason, expectedLayer, expectedQuery] of [ + ["authBlocked", "runtime_durable_adapter_auth_blocked", "auth", "auth_blocked"], + ["schemaBlocked", "runtime_durable_adapter_schema_blocked", "schema", "schema_blocked"] + ]) { + const durable = classifyRuntimeDurableReadiness(statusMatrix.runtime[name]); + const status = classifyWorkbenchLiveStatus({ + healthLive: jsonProbe(statusMatrix.runtime[name]), + restIndex: okRest, + health: okApi, + adapter: okApi, + m3Control: okM3Control, + m3Status: okM3Status + }); + + assert.equal(durable.reasonCode, expectedReason); + assert.equal(durable.evidence.blockedLayer, expectedLayer); + assert.equal(durable.evidence.queryResult, expectedQuery); + assert.equal(status.kind, "readonly"); + assert.equal(status.reasonCode, expectedReason); + } +}); + +test("fixture matrix treats unknown or missing readiness as unverified, not runtime durable blocked", () => { + const status = classifyWorkbenchLiveStatus({ + healthLive: jsonProbe(statusMatrix.runtime.unknownMissingReadiness) + }); + + assert.equal(status.kind, "unverified"); + assert.equal(status.label, "等待验证"); + assert.doesNotMatch(`${status.reasonCode} ${status.detail}`, /runtime durable blocked|blocked at durability/u); +}); + +test("fixture matrix keeps M3 trusted green summary aligned with live status and wiring presentation", () => { + const status = classifyWorkbenchLiveStatus({ + healthLive: okApi, + restIndex: codexStdioRest, + health: okApi, + adapter: okApi, + m3Control: jsonProbe(statusMatrix.m3.controlReady), + m3Status: jsonProbe(statusMatrix.m3.trustedGreenSummary) + }); + const wiring = wiringPresentationFromSummary(statusMatrix.m3.trustedGreenSummary); + const m3Probe = status.probes.find((probe) => probe.apiPath === "/v1/m3/status"); + + assert.equal(status.kind, "pass"); + assert.equal(m3Probe.kind, "pass"); + assert.equal(wiring.m3Live, true); + assert.equal(wiring.stateLabel, "实况已验证"); + assert.equal(wiring.badgeTone, "dev-live"); +}); + test("classifies M3 control blockers by cloud-api/gateway/box/patch-panel layer", () => { const status = classifyWorkbenchLiveStatus({ healthLive: okApi, @@ -673,3 +789,11 @@ test("includes trusted M3 evidence identifiers in probe attribution", () => { assert.match(m3Probe?.evidenceSummary ?? "", /audit=aud_m3_live/u); assert.match(m3Probe?.evidenceSummary ?? "", /evidence=evd_m3_live/u); }); + +function jsonProbe(data, status = 200) { + return { + ok: true, + status, + data + }; +}