Merge PR #229: show blocked workbench readiness
Host commander review: small frontend/smoke hardening for the deployed user workbench. Root workbench now derives status from health/live, /v1, RPC readiness, and runtime durability, showing user-facing read-only blocked copy when durable runtime is not ready while keeping raw blocker/Gate/internal wording out of default status. Runner validation was source/local only; no live deploy, restart, Secret access, or M3/M4/M5 acceptance claim.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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, /编写任务/);
|
||||
|
||||
Reference in New Issue
Block a user