fix: correct durable readiness attribution
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user