From 0492db35b8401244cf0f8fba0c1d5be990c1f3e2 Mon Sep 17 00:00:00 2001 From: Code Queue Review Date: Sat, 23 May 2026 02:29:16 +0000 Subject: [PATCH] fix: show blocked workbench readiness --- scripts/src/dev-cloud-workbench-smoke-lib.mjs | 49 ++++++++++--- web/hwlab-cloud-web/app.mjs | 72 +++++++++++++++++-- web/hwlab-cloud-web/scripts/check.mjs | 18 ++++- 3 files changed, 124 insertions(+), 15 deletions(-) diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index bdf21c7c..e7bf481b 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -295,7 +295,7 @@ function runStaticSmoke() { addCheck(checks, blockers, "api-degraded-default-status", hasApiDegradedDefaultStatus(files), "Default workbench status distinguishes reachable degraded API from fully OK without exposing backend blocker details.", { blocker: "observability_blocker", - evidence: ["healthLive.data.status === degraded", "API 降级 / 只读模式", "no Gate/diagnostics/blocker copy in default status classifier"] + evidence: ["healthLive.data.status === degraded", "ready false", "runtime_durable_adapter_query_blocked", "API 降级 / 只读模式", "可信记录受阻 / 只读模式", "no Gate/diagnostics/blocker copy in default status classifier"] }); addCheck(checks, blockers, "code-agent-chat-primary-flow", hasCodeAgentChatContract(files), "Center Agent conversation sends to the controlled Code Agent chat endpoint and no longer uses 添加草稿 as the primary flow.", { @@ -1090,13 +1090,27 @@ function hasSameOriginReadOnlyBoundary(app) { function hasApiDegradedDefaultStatus({ app }) { const statusBody = functionBody(app, "workbenchApiSurfaceStatus"); + const readinessBody = functionBody(app, "workbenchSurfaceReadiness"); + const durableBlockBody = functionBody(app, "hasDurableRuntimeQueryBlock"); return ( /function\s+workbenchApiSurfaceStatus\s*\(/u.test(app) && - /live\.healthLive\?\.data\?\.status/u.test(statusBody) && - /healthLiveStatus === "degraded"/u.test(statusBody) && + /function\s+workbenchSurfaceReadiness\s*\(/u.test(app) && + /function\s+hasDurableRuntimeQueryBlock\s*\(/u.test(app) && + /surface\.durableQueryBlocked/u.test(statusBody) && + /surface\.degraded/u.test(statusBody) && + /function\s+readinessBooleanValues\s*\(/u.test(app) && + /payload\.ready/u.test(functionBody(app, "readinessBooleanValues")) && + /payload\.readiness\?\.ready/u.test(functionBody(app, "readinessBooleanValues")) && + /readyValues\.includes\(false\)/u.test(readinessBody) && + /runtime_durable_adapter_query_blocked/u.test(durableBlockBody) && + /payload\.runtime\?\.blocker/u.test(durableBlockBody) && + /payload\.readiness\?\.durability\?\.blocker/u.test(durableBlockBody) && + /blockedLayer === "durability_query"/u.test(durableBlockBody) && + /可信记录受阻 \/ 只读模式/u.test(statusBody) && /API 降级 \/ 只读模式/u.test(statusBody) && /同源 API 可响应但处于降级状态;当前保持只读查看和本地任务整理。/u.test(statusBody) && - !/blocker|DB live readiness|Gate|诊断|验收|M0-M5|执行轨迹/u.test(statusBody) + /同源 API 可响应,但可信记录持久化尚未就绪;当前只保留只读查看和本地任务整理。/u.test(statusBody) && + !/runtime_durable_adapter_query_blocked|DB live readiness|Gate|诊断|验收|M0-M5|执行轨迹/u.test(statusBody) ); } @@ -1770,8 +1784,8 @@ async function inspectDefaultApiStatus(page) { methodText, firstScreenText, degradedStatusVisible: - liveStatus === "API 降级 / 只读模式" && - liveDetail === "同源 API 可响应但处于降级状态;当前保持只读查看和本地任务整理。" && + liveStatus === "可信记录受阻 / 只读模式" && + liveDetail === "同源 API 可响应,但可信记录持久化尚未就绪;当前只保留只读查看和本地任务整理。" && methodText.includes("system.health"), defaultCopyIsWorkbenchSafe: !forbidden.test(`${liveStatus} ${liveDetail} ${firstScreenText}`) }; @@ -1841,7 +1855,7 @@ export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() { await page.goto(server.url, { waitUntil: "networkidle", timeout: 15000 }); await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 }); await page.waitForFunction( - () => document.querySelector("#live-status")?.textContent?.trim() === "API 降级 / 只读模式", + () => document.querySelector("#live-status")?.textContent?.trim() === "可信记录受阻 / 只读模式", null, { timeout: 8000 } ); @@ -2143,17 +2157,36 @@ async function handleLocalAgentFixtureApi({ request, response, url }) { jsonResponse(response, 200, { serviceId: "hwlab-cloud-api", status: "degraded", + ready: false, sourceKind: "SOURCE", codeAgent: localSourceFixtureAvailability(), methods: [...readOnlyRpcMethods], runtime: { durable: false, + ready: false, status: "degraded", + blocker: "runtime_durable_adapter_query_blocked", + connection: { + queryAttempted: true, + queryResult: "query_blocked" + }, counts: { gatewaySessions: gateSummary.gatewaySessionCount, boxResources: gateSummary.boxResourceCount } - } + }, + readiness: { + status: "degraded", + ready: false, + durability: { + status: "blocked", + ready: false, + blocker: "runtime_durable_adapter_query_blocked", + blockedLayer: "durability_query", + queryResult: "query_blocked" + } + }, + blockerCodes: ["runtime_durable_adapter_query_blocked"] }); return true; } diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index e736ca2d..b6e32b2a 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -528,8 +528,8 @@ function renderLiveSurface(live) { } function workbenchApiSurfaceStatus(live, coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter]) { - const reachable = coreProbes.some((probe) => probe.ok); - if (!reachable) { + const surface = workbenchSurfaceReadiness(live, coreProbes); + if (!surface.reachable) { return { label: "离线查看", tone: "source", @@ -538,8 +538,16 @@ function workbenchApiSurfaceStatus(live, coreProbes = [live.healthLive, live.res }; } - const healthLiveStatus = String(live.healthLive?.data?.status ?? "").trim().toLowerCase(); - if (live.healthLive?.ok && healthLiveStatus === "degraded") { + if (surface.durableQueryBlocked) { + return { + label: "可信记录受阻 / 只读模式", + tone: "blocked", + methodTone: "degraded", + detail: "同源 API 可响应,但可信记录持久化尚未就绪;当前只保留只读查看和本地任务整理。" + }; + } + + if (surface.degraded) { return { label: "API 降级 / 只读模式", tone: "degraded", @@ -556,6 +564,62 @@ function workbenchApiSurfaceStatus(live, coreProbes = [live.healthLive, live.res }; } +function workbenchSurfaceReadiness(live, coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter]) { + const payloads = coreProbes + .filter((probe) => probe?.ok) + .map((probe) => probe.data) + .filter((payload) => payload && typeof payload === "object"); + const statuses = payloads.flatMap(readinessStatusValues).map(normalizedStatusValue); + const readyValues = payloads.flatMap(readinessBooleanValues); + return { + reachable: payloads.length > 0, + degraded: + statuses.some((status) => ["blocked", "degraded", "failed"].includes(status)) || + readyValues.includes(false), + durableQueryBlocked: payloads.some(hasDurableRuntimeQueryBlock) + }; +} + +function readinessStatusValues(payload) { + return [ + payload.status, + payload.readiness?.status, + payload.readiness?.durability?.status, + payload.runtime?.status + ].filter((value) => value !== undefined && value !== null); +} + +function readinessBooleanValues(payload) { + return [ + payload.ready, + payload.readiness?.ready, + payload.readiness?.durability?.ready, + payload.runtime?.ready + ].filter((value) => typeof value === "boolean"); +} + +function hasDurableRuntimeQueryBlock(payload) { + const codes = [ + ...(Array.isArray(payload.blockerCodes) ? payload.blockerCodes : []), + ...(Array.isArray(payload.readiness?.blockerCodes) ? payload.readiness.blockerCodes : []), + ...(Array.isArray(payload.blockers) ? payload.blockers.map((item) => item?.code) : []), + ...(Array.isArray(payload.readiness?.blockers) ? payload.readiness.blockers.map((item) => item?.code) : []), + payload.runtime?.blocker, + payload.readiness?.durability?.blocker + ].map(normalizedStatusValue); + const queryResult = normalizedStatusValue(payload.runtime?.connection?.queryResult ?? payload.readiness?.durability?.queryResult); + const blockedLayer = normalizedStatusValue(payload.runtime?.durabilityContract?.blockedLayer ?? payload.readiness?.durability?.blockedLayer); + return ( + codes.includes("runtime_durable_adapter_query_blocked") || + queryResult === "query_blocked" || + blockedLayer === "durability_query" + ); +} + +function normalizedStatusValue(value) { + return String(value ?? "").trim().toLowerCase(); +} + function renderResourceTree() { const items = [ { diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 24e57c59..fe7186c2 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -287,11 +287,23 @@ assert.match(markedLicense, /MarkedJS/); assert.match(markedLicense, /Permission is hereby granted, free of charge/); assert.match(html, /live-status/); assert.match(app, /function workbenchApiSurfaceStatus/); -assert.match(functionBody(app, "workbenchApiSurfaceStatus"), /live\.healthLive\?\.data\?\.status/); -assert.match(functionBody(app, "workbenchApiSurfaceStatus"), /healthLiveStatus === "degraded"/); +assert.match(app, /function workbenchSurfaceReadiness/); +assert.match(app, /function hasDurableRuntimeQueryBlock/); +assert.match(functionBody(app, "workbenchApiSurfaceStatus"), /surface\.durableQueryBlocked/); +assert.match(functionBody(app, "workbenchApiSurfaceStatus"), /surface\.degraded/); +assert.match(app, /function readinessBooleanValues/); +assert.match(functionBody(app, "readinessBooleanValues"), /payload\.ready/); +assert.match(functionBody(app, "readinessBooleanValues"), /payload\.readiness\?\.ready/); +assert.match(functionBody(app, "workbenchSurfaceReadiness"), /readyValues\.includes\(false\)/); +assert.match(functionBody(app, "hasDurableRuntimeQueryBlock"), /runtime_durable_adapter_query_blocked/); +assert.match(functionBody(app, "hasDurableRuntimeQueryBlock"), /payload\.runtime\?\.blocker/); +assert.match(functionBody(app, "hasDurableRuntimeQueryBlock"), /payload\.readiness\?\.durability\?\.blocker/); +assert.match(functionBody(app, "hasDurableRuntimeQueryBlock"), /blockedLayer === "durability_query"/); +assert.match(app, /可信记录受阻 \/ 只读模式/); assert.match(app, /API 降级 \/ 只读模式/); assert.match(app, /同源 API 可响应但处于降级状态;当前保持只读查看和本地任务整理。/); -assert.doesNotMatch(functionBody(app, "workbenchApiSurfaceStatus"), /blocker|DB live readiness|Gate|诊断|验收|M0-M5|执行轨迹/u); +assert.match(app, /同源 API 可响应,但可信记录持久化尚未就绪;当前只保留只读查看和本地任务整理。/); +assert.doesNotMatch(functionBody(app, "workbenchApiSurfaceStatus"), /runtime_durable_adapter_query_blocked|DB live readiness|Gate|诊断|验收|M0-M5|执行轨迹/u); assert.match(html, /method-list/); assert.match(html, /quick-actions/); assert.match(html, /编写任务/);