83e5f52c98
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.
676 lines
19 KiB
JavaScript
676 lines
19 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import test from "node:test";
|
||
|
||
import { classifyWorkbenchLiveStatus } from "../live-status.mjs";
|
||
|
||
const okApi = Object.freeze({
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
serviceId: "hwlab-cloud-api",
|
||
status: "ok",
|
||
ready: true,
|
||
readiness: {
|
||
status: "ok",
|
||
ready: true,
|
||
durability: {
|
||
status: "ready",
|
||
ready: true
|
||
}
|
||
},
|
||
runtime: {
|
||
status: "ready",
|
||
durable: true,
|
||
ready: true,
|
||
liveRuntimeEvidence: true
|
||
}
|
||
}
|
||
});
|
||
|
||
const okRest = Object.freeze({
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
serviceId: "hwlab-cloud-api",
|
||
status: "ok",
|
||
ready: true,
|
||
readiness: {
|
||
status: "ok",
|
||
ready: true,
|
||
durability: {
|
||
status: "ready",
|
||
ready: true,
|
||
queryResult: "durable_readiness_ready"
|
||
}
|
||
},
|
||
runtime: {
|
||
adapter: "postgres",
|
||
status: "ready",
|
||
durable: true,
|
||
ready: true,
|
||
liveRuntimeEvidence: true,
|
||
connection: {
|
||
queryResult: "durable_readiness_ready"
|
||
}
|
||
},
|
||
codeAgent: {
|
||
endpoint: "POST /v1/agent/chat",
|
||
status: "available",
|
||
ready: true,
|
||
provider: "openai-responses",
|
||
backend: "hwlab-cloud-api/openai-responses",
|
||
capabilityLevel: "available"
|
||
}
|
||
}
|
||
});
|
||
|
||
const codexStdioRest = Object.freeze({
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
serviceId: "hwlab-cloud-api",
|
||
status: "ok",
|
||
ready: true,
|
||
readiness: {
|
||
status: "ok",
|
||
ready: true,
|
||
durability: {
|
||
status: "ready",
|
||
ready: true,
|
||
queryResult: "durable_readiness_ready"
|
||
}
|
||
},
|
||
runtime: {
|
||
adapter: "postgres",
|
||
status: "ready",
|
||
durable: true,
|
||
ready: true,
|
||
liveRuntimeEvidence: true,
|
||
connection: {
|
||
queryResult: "durable_readiness_ready"
|
||
}
|
||
},
|
||
codeAgent: {
|
||
endpoint: "POST /v1/agent/chat",
|
||
status: "codex-stdio-feasible",
|
||
ready: true,
|
||
provider: "codex-stdio",
|
||
backend: "hwlab-cloud-api/codex-mcp-stdio",
|
||
capabilityLevel: "long-lived-codex-stdio-session",
|
||
agentKind: "codex-stdio-feasible",
|
||
codexStdioFeasibility: {
|
||
canStartLongLivedCodexStdio: true
|
||
},
|
||
longLivedSessionGate: {
|
||
status: "pass",
|
||
pass: true
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
const readonlyOnlyRest = Object.freeze({
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
serviceId: "hwlab-cloud-api",
|
||
status: "degraded",
|
||
ready: false,
|
||
readiness: {
|
||
status: "ok",
|
||
ready: true,
|
||
durability: {
|
||
status: "ready",
|
||
ready: true,
|
||
queryResult: "durable_readiness_ready"
|
||
}
|
||
},
|
||
runtime: {
|
||
adapter: "postgres",
|
||
status: "ready",
|
||
durable: true,
|
||
ready: true,
|
||
liveRuntimeEvidence: true,
|
||
connection: {
|
||
queryResult: "durable_readiness_ready"
|
||
}
|
||
},
|
||
codeAgent: {
|
||
endpoint: "POST /v1/agent/chat",
|
||
status: "partial",
|
||
ready: false,
|
||
partialReady: true,
|
||
agentKind: "controlled-readonly-session-registry",
|
||
provider: "openai-responses",
|
||
backend: "hwlab-cloud-api/openai-responses",
|
||
capabilityLevel: "read-only-session-tools",
|
||
runner: {
|
||
kind: "hwlab-readonly-runner",
|
||
ready: true,
|
||
writeCapable: false
|
||
},
|
||
blockerCodes: ["codex_stdio_supervisor_disabled", "provider_token_boundary"],
|
||
codexStdioFeasibility: {
|
||
status: "blocked",
|
||
blockers: [{ code: "codex_stdio_supervisor_disabled" }]
|
||
},
|
||
longLivedSessionGate: {
|
||
status: "blocked",
|
||
pass: false
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
const okM3Control = Object.freeze({
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
status: "available",
|
||
readiness: {
|
||
status: "ready",
|
||
controlReady: true,
|
||
sourceKind: "DEV-LIVE",
|
||
evidenceLevel: "DEV-LIVE"
|
||
}
|
||
}
|
||
});
|
||
|
||
const okM3Status = Object.freeze({
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
status: "live",
|
||
sourceKind: "DEV-LIVE",
|
||
summary: "M3 硬件状态与可信持久化均为 DEV-LIVE。"
|
||
}
|
||
});
|
||
|
||
test("classifies fully ready workbench runtime as API 正常", () => {
|
||
const status = classifyWorkbenchLiveStatus({
|
||
healthLive: okApi,
|
||
restIndex: codexStdioRest,
|
||
health: okApi,
|
||
adapter: okApi,
|
||
m3Control: okM3Control,
|
||
m3Status: okM3Status
|
||
});
|
||
|
||
assert.equal(status.kind, "pass");
|
||
assert.equal(status.label, "API 正常");
|
||
assert.match(status.detail, /hwlab-cloud-api \/health\/live/u);
|
||
assert.equal(status.internalRawStatuses.includes("degraded"), false);
|
||
});
|
||
|
||
test("classifies controlled readonly Code Agent as read-only, not full pass", () => {
|
||
const status = classifyWorkbenchLiveStatus({
|
||
healthLive: okApi,
|
||
restIndex: readonlyOnlyRest,
|
||
health: okApi,
|
||
adapter: okApi,
|
||
m3Control: okM3Control,
|
||
m3Status: okM3Status
|
||
});
|
||
|
||
assert.equal(status.kind, "readonly");
|
||
assert.equal(status.label, "只读模式");
|
||
assert.equal(status.reasonCode, "codex_stdio_blocked_readonly_available");
|
||
assert.match(status.detail, /controlled-readonly-session-registry/u);
|
||
});
|
||
|
||
test("classifies concrete API failure with service path and HTTP status", () => {
|
||
const status = classifyWorkbenchLiveStatus({
|
||
healthLive: {
|
||
ok: false,
|
||
path: "/health/live",
|
||
status: 503,
|
||
error: "/health/live HTTP 503:runtime unavailable"
|
||
},
|
||
restIndex: okRest,
|
||
health: okApi,
|
||
adapter: okApi,
|
||
m3Control: okM3Control,
|
||
m3Status: okM3Status
|
||
});
|
||
|
||
assert.equal(status.kind, "error");
|
||
assert.equal(status.label, "API 错误");
|
||
assert.equal(status.serviceId, "hwlab-cloud-api");
|
||
assert.equal(status.apiPath, "/health/live");
|
||
assert.equal(status.reasonCode, "HTTP 503");
|
||
assert.match(status.detail, /错误:hwlab-cloud-api \/health\/live \/ HTTP 503/u);
|
||
});
|
||
|
||
test("classifies durable runtime degraded as explicit read-only mode", () => {
|
||
const status = classifyWorkbenchLiveStatus({
|
||
healthLive: {
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
serviceId: "hwlab-cloud-api",
|
||
status: "degraded",
|
||
ready: false,
|
||
runtime: {
|
||
durable: true,
|
||
ready: false,
|
||
liveRuntimeEvidence: false,
|
||
status: "blocked",
|
||
blocker: "runtime_durable_adapter_schema_blocked",
|
||
connection: {
|
||
queryResult: "schema_blocked"
|
||
}
|
||
},
|
||
readiness: {
|
||
status: "degraded",
|
||
ready: false,
|
||
durability: {
|
||
status: "blocked",
|
||
ready: false,
|
||
blocker: "runtime_durable_adapter_schema_blocked",
|
||
blockedLayer: "schema",
|
||
queryResult: "schema_blocked"
|
||
}
|
||
},
|
||
blockerCodes: ["runtime_durable_adapter_schema_blocked"]
|
||
}
|
||
},
|
||
restIndex: okRest,
|
||
health: okApi,
|
||
adapter: okApi,
|
||
m3Control: okM3Control,
|
||
m3Status: okM3Status
|
||
});
|
||
|
||
assert.equal(status.kind, "readonly");
|
||
assert.equal(status.label, "只读模式");
|
||
assert.equal(status.reasonCode, "runtime_durable_adapter_schema_blocked");
|
||
assert.match(status.detail, /只读:hwlab-cloud-api \/health\/live/u);
|
||
assert.match(status.detail, /schema_blocked/u);
|
||
assert.deepEqual(status.internalRawStatuses.filter((item) => item === "degraded"), ["degraded"]);
|
||
});
|
||
|
||
test("does not classify ready queryResult durable runtime as blocked", () => {
|
||
const status = classifyWorkbenchLiveStatus({
|
||
healthLive: {
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
serviceId: "hwlab-cloud-api",
|
||
status: "ready",
|
||
ready: true,
|
||
runtime: {
|
||
adapter: "postgres",
|
||
durable: true,
|
||
ready: true,
|
||
liveRuntimeEvidence: true,
|
||
status: "ready",
|
||
blocker: null,
|
||
connection: {
|
||
queryResult: "durable_readiness_ready"
|
||
}
|
||
},
|
||
readiness: {
|
||
status: "ready",
|
||
ready: true,
|
||
durability: {
|
||
status: "ready",
|
||
ready: true,
|
||
blocker: null,
|
||
blockedLayer: null,
|
||
queryResult: "durable_readiness_ready"
|
||
}
|
||
},
|
||
blockerCodes: []
|
||
}
|
||
},
|
||
restIndex: okRest,
|
||
health: okApi,
|
||
adapter: okApi,
|
||
m3Control: okM3Control,
|
||
m3Status: okM3Status
|
||
});
|
||
|
||
assert.equal(status.kind, "pass");
|
||
assert.equal(status.reasonCode, "ready");
|
||
assert.equal(status.probes.find((probe) => probe.apiPath === "/health/live")?.reasonCode, "ready");
|
||
});
|
||
|
||
test("classifies auth and migration durable blockers with shared runtime reason", () => {
|
||
for (const [reasonCode, queryResult, blockedLayer] of [
|
||
["runtime_durable_adapter_auth_blocked", "auth_blocked", "auth"],
|
||
["runtime_durable_adapter_migration_blocked", "migration_blocked", "migration"]
|
||
]) {
|
||
const status = classifyWorkbenchLiveStatus({
|
||
healthLive: {
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
serviceId: "hwlab-cloud-api",
|
||
status: "degraded",
|
||
ready: false,
|
||
runtime: {
|
||
adapter: "postgres",
|
||
durable: false,
|
||
durableRequested: true,
|
||
ready: false,
|
||
liveRuntimeEvidence: false,
|
||
status: "blocked",
|
||
blocker: reasonCode,
|
||
connection: { queryResult },
|
||
durabilityContract: { blockedLayer },
|
||
gates: {
|
||
[blockedLayer]: {
|
||
checked: true,
|
||
ready: false,
|
||
status: "blocked",
|
||
blocker: reasonCode
|
||
}
|
||
}
|
||
},
|
||
readiness: {
|
||
ready: false,
|
||
status: "blocked",
|
||
durability: {
|
||
ready: false,
|
||
status: "blocked",
|
||
blocker: reasonCode,
|
||
blockedLayer,
|
||
queryResult
|
||
}
|
||
},
|
||
blockerCodes: [reasonCode]
|
||
}
|
||
},
|
||
restIndex: okRest,
|
||
health: okApi,
|
||
adapter: okApi,
|
||
m3Control: okM3Control,
|
||
m3Status: okM3Status
|
||
});
|
||
|
||
assert.equal(status.kind, "readonly");
|
||
assert.equal(status.reasonCode, reasonCode);
|
||
assert.match(status.detail, new RegExp(queryResult, "u"));
|
||
}
|
||
});
|
||
|
||
test("classifies missing probes as unverified instead of degraded", () => {
|
||
const status = classifyWorkbenchLiveStatus({});
|
||
|
||
assert.equal(status.kind, "unverified");
|
||
assert.equal(status.label, "等待验证");
|
||
assert.match(status.detail, /等待验证/u);
|
||
assert.doesNotMatch(`${status.label} ${status.detail}`, /降级/u);
|
||
});
|
||
|
||
test("classifies memory or non-durable runtime as read-only with explicit reason", () => {
|
||
const status = classifyWorkbenchLiveStatus({
|
||
healthLive: okApi,
|
||
restIndex: okRest,
|
||
health: {
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
serviceId: "hwlab-cloud-api",
|
||
status: "degraded",
|
||
runtime: {
|
||
adapter: "memory",
|
||
durable: false
|
||
}
|
||
}
|
||
},
|
||
adapter: okApi,
|
||
m3Control: okM3Control,
|
||
m3Status: okM3Status
|
||
});
|
||
|
||
assert.equal(status.kind, "readonly");
|
||
assert.equal(status.label, "只读模式");
|
||
assert.equal(status.reasonCode, "runtime_durable_false");
|
||
assert.match(status.detail, /runtime durable=false/u);
|
||
});
|
||
|
||
test("classifies raw degraded without durable/read-only reason as API error", () => {
|
||
const status = classifyWorkbenchLiveStatus({
|
||
healthLive: {
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
serviceId: "hwlab-cloud-api",
|
||
status: "degraded",
|
||
ready: false,
|
||
traceId: "trc_degraded_without_attribution"
|
||
}
|
||
},
|
||
restIndex: okRest,
|
||
health: okApi,
|
||
adapter: okApi,
|
||
m3Control: okM3Control,
|
||
m3Status: okM3Status
|
||
});
|
||
|
||
assert.equal(status.kind, "error");
|
||
assert.equal(status.label, "API 错误");
|
||
assert.equal(status.reasonCode, "ready_false_without_blocker");
|
||
assert.match(status.detail, /API readiness is not pass and no concrete blocker was provided/u);
|
||
assert.match(status.detail, /trace=trc_degraded_without_attribution/u);
|
||
assert.equal(status.internalRawStatuses.includes("degraded"), true);
|
||
assert.doesNotMatch(`${status.label} ${status.detail}`, /API 降级/u);
|
||
assert.doesNotMatch(`${status.label} ${status.detail}`, /API returned degraded/u);
|
||
});
|
||
|
||
test("classifies Code Agent provider timeout or fallback with concrete attribution", () => {
|
||
const providerTimeout = classifyWorkbenchLiveStatus({
|
||
healthLive: okApi,
|
||
restIndex: {
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
serviceId: "hwlab-cloud-api",
|
||
status: "ok",
|
||
ready: true,
|
||
codeAgent: {
|
||
endpoint: "POST /v1/agent/chat",
|
||
status: "blocked",
|
||
ready: false,
|
||
provider: "openai-responses",
|
||
backend: "hwlab-cloud-api/openai-responses",
|
||
traceId: "trc_provider_timeout",
|
||
error: {
|
||
providerStatus: 504
|
||
},
|
||
summary: "/v1/agent/chat timeout"
|
||
}
|
||
}
|
||
},
|
||
health: okApi,
|
||
adapter: okApi,
|
||
m3Control: okM3Control,
|
||
m3Status: okM3Status
|
||
});
|
||
assert.equal(providerTimeout.kind, "error");
|
||
assert.equal(providerTimeout.apiPath, "POST /v1/agent/chat");
|
||
assert.equal(providerTimeout.reasonCode, "provider_http_504");
|
||
assert.match(providerTimeout.detail, /hwlab-cloud-api POST \/v1\/agent\/chat/u);
|
||
assert.match(providerTimeout.detail, /trace=trc_provider_timeout/u);
|
||
|
||
const fallback = classifyWorkbenchLiveStatus({
|
||
healthLive: okApi,
|
||
restIndex: {
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
serviceId: "hwlab-cloud-api",
|
||
status: "ok",
|
||
ready: true,
|
||
codeAgent: {
|
||
endpoint: "POST /v1/agent/chat",
|
||
status: "available",
|
||
ready: true,
|
||
provider: "openai-responses",
|
||
backend: "hwlab-cloud-api/openai-responses",
|
||
capabilityLevel: "text-chat-only",
|
||
runner: {
|
||
kind: "openai-responses-fallback"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
health: okApi,
|
||
adapter: okApi,
|
||
m3Control: okM3Control,
|
||
m3Status: okM3Status
|
||
});
|
||
assert.equal(fallback.kind, "readonly");
|
||
assert.equal(fallback.label, "只读模式");
|
||
assert.equal(fallback.reasonCode, "fallback_text_chat_only");
|
||
assert.match(fallback.detail, /capability=text-chat-only/u);
|
||
});
|
||
|
||
test("keeps ready durable runtime green while attributing read-only mode to Code Agent session-tools", () => {
|
||
const status = classifyWorkbenchLiveStatus({
|
||
healthLive: {
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
serviceId: "hwlab-cloud-api",
|
||
status: "ok",
|
||
ready: true,
|
||
runtime: {
|
||
status: "ready",
|
||
ready: true,
|
||
durable: true,
|
||
liveRuntimeEvidence: true,
|
||
blocker: null,
|
||
connection: {
|
||
queryResult: "durable_readiness_ready"
|
||
}
|
||
},
|
||
readiness: {
|
||
status: "ready",
|
||
ready: true,
|
||
durability: {
|
||
status: "ready",
|
||
ready: true,
|
||
runtimeStatus: "ready",
|
||
blockedLayer: null,
|
||
blocker: null,
|
||
queryResult: "durable_readiness_ready"
|
||
}
|
||
},
|
||
codeAgent: {
|
||
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",
|
||
trackingIssue: "#317",
|
||
runnerLimitations: ["not-codex-stdio", "not-write-capable", "not-durable-session"]
|
||
}
|
||
}
|
||
},
|
||
restIndex: {
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
serviceId: "hwlab-cloud-api",
|
||
status: "ok",
|
||
ready: true,
|
||
codeAgent: {
|
||
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",
|
||
trackingIssue: "#317",
|
||
runnerLimitations: ["not-codex-stdio", "not-write-capable", "not-durable-session"]
|
||
}
|
||
}
|
||
},
|
||
health: okApi,
|
||
adapter: okApi,
|
||
m3Control: okM3Control,
|
||
m3Status: okM3Status
|
||
});
|
||
|
||
assert.equal(status.kind, "readonly");
|
||
assert.equal(status.label, "只读模式");
|
||
assert.equal(status.serviceId, "hwlab-cloud-api");
|
||
assert.equal(status.apiPath, "POST /v1/agent/chat");
|
||
assert.equal(status.reasonCode, "code_agent_read_only_session_tools");
|
||
assert.match(status.detail, /capability=read-only-session-tools/u);
|
||
assert.match(status.detail, /#317 full pass not complete/u);
|
||
assert.doesNotMatch(status.detail, /runtime durable blocked/u);
|
||
assert.doesNotMatch(status.detail, /blocked at durability/u);
|
||
const healthLiveProbe = status.probes.find((probe) => probe.apiPath === "/health/live");
|
||
assert.equal(healthLiveProbe.kind, "pass");
|
||
assert.doesNotMatch(healthLiveProbe.reason, /blocked/u);
|
||
});
|
||
|
||
test("classifies M3 control blockers by cloud-api/gateway/box/patch-panel layer", () => {
|
||
const status = classifyWorkbenchLiveStatus({
|
||
healthLive: okApi,
|
||
restIndex: okRest,
|
||
health: okApi,
|
||
adapter: okApi,
|
||
m3Control: {
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
status: "blocked",
|
||
traceId: "trc_m3_wiring_missing",
|
||
readiness: {
|
||
status: "blocked",
|
||
controlReady: false,
|
||
blocker: {
|
||
code: "m3_wiring_missing",
|
||
layer: "hwlab-patch-panel",
|
||
zh: "hwlab-patch-panel route unavailable"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
m3Status: okM3Status
|
||
});
|
||
|
||
assert.equal(status.kind, "error");
|
||
assert.equal(status.serviceId, "hwlab-patch-panel");
|
||
assert.equal(status.apiPath, "/v1/m3/io");
|
||
assert.equal(status.reasonCode, "m3_wiring_missing");
|
||
assert.match(status.detail, /hwlab-patch-panel \/v1\/m3\/io/u);
|
||
assert.match(status.detail, /trace=trc_m3_wiring_missing/u);
|
||
});
|
||
|
||
test("includes trusted M3 evidence identifiers in probe attribution", () => {
|
||
const status = classifyWorkbenchLiveStatus({
|
||
healthLive: okApi,
|
||
restIndex: okRest,
|
||
health: okApi,
|
||
adapter: okApi,
|
||
m3Control: okM3Control,
|
||
m3Status: {
|
||
ok: true,
|
||
status: 200,
|
||
data: {
|
||
status: "live",
|
||
sourceKind: "DEV-LIVE",
|
||
summary: "M3 硬件状态与可信持久化均为 DEV-LIVE。",
|
||
trust: {
|
||
operationId: "op_m3_live",
|
||
traceId: "trc_m3_live",
|
||
auditId: "aud_m3_live",
|
||
evidenceId: "evd_m3_live"
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
const m3Probe = status.probes.find((probe) => probe.apiPath === "/v1/m3/status");
|
||
assert.equal(status.kind, "pass");
|
||
assert.match(m3Probe?.evidenceSummary ?? "", /operation=op_m3_live/u);
|
||
assert.match(m3Probe?.evidenceSummary ?? "", /audit=aud_m3_live/u);
|
||
assert.match(m3Probe?.evidenceSummary ?? "", /evidence=evd_m3_live/u);
|
||
});
|