Merge pull request #327 from pikasTech/fix/live-status-attribution-277-main

fix: classify workbench live status attribution
This commit is contained in:
Lyon
2026-05-23 18:32:11 +08:00
committed by GitHub
9 changed files with 996 additions and 195 deletions
+18
View File
@@ -83,6 +83,24 @@ The frontend may show read-only DEV observations when they are tied to the
exact route or component. It must not upgrade source fixtures, dry-run output,
front-end state, or edge-only health into M3 DEV-LIVE.
The default workbench runtime label must not expose raw `degraded` as the
user-facing conclusion. `live-status` maps current probes into four Chinese
states:
| State | Visible label | Required attribution |
| --- | --- | --- |
| Pass | `API 正常` | The exact service and API that proved readiness, normally `hwlab-cloud-api /health/live`. |
| Error | `API 错误` | Concrete service/API/reason, for example `hwlab-cloud-api /v1/agent/chat provider_http_504` or `hwlab-patch-panel /v1/m3/io m3_wiring_missing`. |
| Unverified | `等待验证` | The service/API still waiting for observation; it must not be treated as pass. |
| Read-only | `只读模式` | Explicit permission, fallback, DB persistence, or runtime durable reason, for example `runtime_durable_adapter_schema_blocked`, `runtime_durable_false`, or `fallback_text_chat_only`. |
Raw `degraded` may be retained in internal details, reports, and `/gate`, but
the main workbench status must translate it into one of these actionable
states. Real `/v1/agent/chat` failures, provider timeouts, text-chat-only
fallback, runner blockers, and M3 `cloud-api -> gateway-simu -> box-simu ->
hwlab-patch-panel` failures must name the responsible microservice/API and the
error category.
如果 `/v1/agent/chat` 返回 `provider_unavailable` 或报告缺少
`OPENAI_API_KEY`,可见状态必须保持 `BLOCKED/credential`。在同源 DEV 请求按
[Code Agent readiness](code-agent-chat-readiness.md) 合同返回 completed
+1 -1
View File
File diff suppressed because one or more lines are too long
+59 -34
View File
@@ -46,6 +46,7 @@ const requiredWebAssets = Object.freeze([
"index.html",
"styles.css",
"app.mjs",
"live-status.mjs",
"runtime.mjs",
"gate-summary.mjs",
"workbench-hardware-panel.mjs",
@@ -54,7 +55,7 @@ const requiredWebAssets = Object.freeze([
"third_party/marked/LICENSE"
]);
const liveIdentityWebAssets = Object.freeze(["index.html", "styles.css", "app.mjs"]);
const liveIdentityWebAssets = Object.freeze(["index.html", "styles.css", "app.mjs", "live-status.mjs"]);
const chineseWorkbenchLabels = Object.freeze([
"HWLAB 云工作台",
@@ -465,9 +466,9 @@ function runStaticSmoke() {
evidence: ["/health/live", "/v1", "/v1/diagnostics/gate", "/v1/agent/chat", "/json-rpc", ...readOnlyRpcMethods]
});
addCheck(checks, blockers, "api-degraded-default-status", hasApiDegradedDefaultStatus(files), "Default workbench status distinguishes reachable blocked API from fully OK with concrete blocker copy.", {
addCheck(checks, blockers, "api-live-status-attribution", hasApiLiveStatusAttribution(files), "Default workbench status maps raw API health into pass, error, unverified, or read-only with concrete service/API attribution.", {
blocker: "observability_blocker",
evidence: ["healthLive.data.status === degraded", "ready false", "runtime_durable_adapter_query_blocked", "runtime_durable_adapter_auth_blocked", "API 错误 / 只读模式", "可信记录受阻 / 只读模式", "no Gate/diagnostics copy in default status classifier"]
evidence: ["API 正常", "API 错误", "等待验证", "只读模式", "hwlab-cloud-api /health/live", "/v1/agent/chat", "/v1/m3/io", "raw degraded kept as internalRawStatuses"]
});
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.", {
@@ -1548,6 +1549,7 @@ function readStaticFiles() {
html: readText("web/hwlab-cloud-web/index.html"),
styles: readText("web/hwlab-cloud-web/styles.css"),
app: readText("web/hwlab-cloud-web/app.mjs"),
liveStatus: readText("web/hwlab-cloud-web/live-status.mjs"),
runtime: readText("web/hwlab-cloud-web/runtime.mjs"),
panel: readText("web/hwlab-cloud-web/workbench-hardware-panel.mjs"),
help: readText("web/hwlab-cloud-web/help.md")
@@ -2166,30 +2168,32 @@ function hasSameOriginReadOnlyBoundary(app) {
);
}
function hasApiDegradedDefaultStatus({ app }) {
function hasApiLiveStatusAttribution({ app, liveStatus }) {
const statusBody = functionBody(app, "workbenchApiSurfaceStatus");
const readinessBody = functionBody(app, "workbenchSurfaceReadiness");
const durableBlockBody = functionBody(app, "hasDurableRuntimeQueryBlock");
return (
/function\s+workbenchApiSurfaceStatus\s*\(/u.test(app) &&
/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) &&
/runtime_durable_adapter_/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) &&
/apiDurableBlockerDetail\(coreProbes\)/u.test(statusBody) &&
/apiProbeErrorDetail\(coreProbes\)/u.test(statusBody) &&
/\/health\/live 可响应,但可信记录持久化尚未就绪:/u.test(app) &&
/classifyWorkbenchLiveStatus/u.test(statusBody) &&
/export function classifyWorkbenchLiveStatus/u.test(liveStatus) &&
/label:\s*"API 正常"/u.test(liveStatus) &&
/label:\s*"API 错误"/u.test(liveStatus) &&
/label:\s*"等待验证"/u.test(liveStatus) &&
/label:\s*"只读模式"/u.test(liveStatus) &&
/internalRawStatuses/u.test(liveStatus) &&
/classifyCodeAgentProbe/u.test(liveStatus) &&
/classifyM3ControlProbe/u.test(liveStatus) &&
/classifyM3StatusProbe/u.test(liveStatus) &&
/\/v1\/agent\/chat/u.test(liveStatus) &&
/\/v1\/m3\/io/u.test(liveStatus) &&
/serviceForM3Layer/u.test(liveStatus) &&
/hwlab-patch-panel/u.test(liveStatus) &&
/hwlab-gateway-simu/u.test(liveStatus) &&
/hwlab-box-simu/u.test(liveStatus) &&
/runtime_durable_adapter_/u.test(liveStatus) &&
/fallback_text_chat_only/u.test(liveStatus) &&
/provider_http_/u.test(liveStatus) &&
!/surface\.degraded/u.test(statusBody) &&
!/API 错误 \/ 只读模式/u.test(app) &&
!/可信记录受阻 \/ 只读模式/u.test(app) &&
!/API 降级 \/ 只读模式/u.test(app) &&
!/runtime_durable_adapter_query_blocked|DB live readiness|Gate|诊断|验收|M0-M5|执行轨迹/u.test(statusBody)
);
@@ -3486,12 +3490,11 @@ async function inspectDefaultApiStatus(page) {
liveStatus,
liveDetail,
firstScreenText,
degradedStatusVisible:
(liveStatus === "可信记录受阻 / 只读模式" || liveStatus === "API 错误 / 只读模式") &&
(
liveDetail.startsWith("/health/live 可响应,但可信记录持久化尚未就绪:") ||
liveDetail === "同源 API 返回 blocked/degraded/failed 或 ready=false;当前保持只读查看和本地任务整理。"
),
actionableStatusVisible:
["API 正常", "API 错误", "等待验证", "只读模式"].includes(liveStatus) &&
/^(通过|错误|等待验证|只读):/u.test(liveDetail) &&
/hwlab-[a-z-]+ .*(\/health\/live|\/v1|\/json-rpc|\/v1\/agent\/chat|\/v1\/m3\/io|\/v1\/m3\/status)/u.test(liveDetail) &&
!/API 降级|degraded\/read-only|blocked\/degraded\/failed/u.test(`${liveStatus} ${liveDetail}`),
defaultCopyIsWorkbenchSafe: !forbidden.test(`${liveStatus} ${liveDetail} ${firstScreenText}`)
};
});
@@ -3603,7 +3606,7 @@ async function runLocalAgentFixtureSmoke({
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() === "可信记录受阻 / 只读模式",
() => document.querySelector("#live-status")?.textContent?.trim() === "只读模式",
null,
{ timeout: 8000 }
);
@@ -3665,9 +3668,9 @@ async function runLocalAgentFixtureSmoke({
observations: controls
},
{
id: "local-api-degraded-default-status",
status: apiStatus.degradedStatusVisible && apiStatus.defaultCopyIsWorkbenchSafe ? "pass" : "blocked",
summary: "Default workbench status renders reachable blocked /health/live as concrete read-only status without first-screen backend route details.",
id: "local-api-status-attribution",
status: apiStatus.actionableStatusVisible && apiStatus.defaultCopyIsWorkbenchSafe ? "pass" : "blocked",
summary: "Default workbench status renders reachable blocked /health/live as read-only with concrete service/API attribution and no raw degraded conclusion.",
observations: apiStatus
},
{
@@ -4373,6 +4376,28 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {}
});
return true;
}
if (request.method === "GET" && url.pathname === "/v1/m3/io") {
jsonResponse(response, 200, {
status: "available",
route: "/v1/m3/io",
readiness: {
status: "ready",
controlReady: true,
sourceKind: "DEV-LIVE",
evidenceLevel: "DEV-LIVE"
}
});
return true;
}
if (request.method === "GET" && url.pathname === "/v1/m3/status") {
jsonResponse(response, 200, m3StatusFixturePayload({
doValue: false,
diValue: false,
observedAt: "2026-05-22T00:00:00.000Z",
sequence: 1
}));
return true;
}
if (request.method === "GET" && url.pathname === "/v1/diagnostics/gate") {
const timestamp = "2026-05-22T00:00:00.000Z";
jsonResponse(response, 200, {
+13 -141
View File
@@ -1,4 +1,5 @@
import { gateSummary } from "./gate-summary.mjs";
import { classifyWorkbenchLiveStatus } from "./live-status.mjs";
import { marked } from "./third_party/marked/marked.esm.js";
const DEFAULT_API_TIMEOUT_MS = 4500;
@@ -628,9 +629,9 @@ function renderStaticWorkbench() {
}
function renderProbePending() {
el.liveStatus.textContent = "只读模式";
el.liveStatus.className = "tone-source";
el.liveDetail.textContent = "可整理任务、查看资源、核对接线和可信记录;不会触发硬件变更。";
el.liveStatus.textContent = "等待验证";
el.liveStatus.className = "tone-pending";
el.liveDetail.textContent = "正在验证 hwlab-cloud-api /health/live、/v1、/v1/agent/chat 与 /v1/m3/io;未完成前不标为通过。";
}
async function sendAgentMessage(message, conversationId, traceId = nextProtocolId("trc")) {
@@ -937,148 +938,19 @@ function renderLiveSurface(live) {
}
function workbenchApiSurfaceStatus(live, coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter]) {
const surface = workbenchSurfaceReadiness(live, coreProbes);
if (!surface.reachable) {
return {
label: "离线查看",
tone: "source",
methodTone: "degraded",
detail: "同源 API 暂不可用,当前仍可使用本地草稿和已加载资源。"
};
}
if (surface.durableQueryBlocked) {
return {
label: "可信记录受阻 / 只读模式",
tone: "blocked",
methodTone: "degraded",
detail: apiDurableBlockerDetail(coreProbes)
};
}
if (surface.degraded) {
return {
label: "API 错误 / 只读模式",
tone: "blocked",
methodTone: "degraded",
detail: apiProbeErrorDetail(coreProbes)
};
}
const status = classifyWorkbenchLiveStatus({
...live,
healthLive: live.healthLive ?? coreProbes[0],
restIndex: live.restIndex ?? coreProbes[1],
health: live.health ?? coreProbes[2],
adapter: live.adapter ?? coreProbes[3]
});
return {
label: "只读连接可用",
tone: "dev-live",
methodTone: "live",
detail: null
...status,
methodTone: status.kind === "pass" ? "live" : status.kind
};
}
function apiDurableBlockerDetail(coreProbes = []) {
const payloads = coreProbes
.filter((probe) => probe?.ok)
.map((probe) => probe.data)
.filter((payload) => payload && typeof payload === "object");
const blocker = safeDurableBlockerLabel(payloads.map(durableRuntimeBlockerCode).find(Boolean));
return `/health/live 可响应,但可信记录持久化尚未就绪:${blocker};当前只保留只读查看和本地任务整理。`;
}
function safeDurableBlockerLabel(blocker) {
const normalized = normalizedStatusValue(blocker);
if (normalized.includes("auth")) return "runtime durable adapter auth blocked";
if (normalized.includes("schema")) return "runtime durable adapter schema blocked";
if (normalized.includes("migration")) return "runtime durable adapter migration blocked";
if (normalized.includes("ssl")) return "runtime durable adapter ssl blocked";
if (normalized.includes("query")) return "runtime durable adapter query blocked";
return "runtime durable adapter blocked";
}
function apiProbeErrorDetail(coreProbes = []) {
const failedProbe = coreProbes.find((probe) => probe && probe.ok !== true);
if (failedProbe?.error?.message) {
return `同源 API 探测失败:${failedProbe.error.message};当前保持只读查看和本地任务整理。`;
}
return "同源 API 返回 blocked/degraded/failed 或 ready=false;当前保持只读查看和本地任务整理。";
}
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 legacyQueryBlockerCode = "runtime_durable_adapter_query_blocked";
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(legacyQueryBlockerCode) ||
codes.some((code) => code.startsWith("runtime_durable_adapter_") && code.endsWith("_blocked")) ||
queryResult.endsWith("_blocked") ||
blockedLayer === "durability_query" ||
blockedLayer === "durability_auth" ||
blockedLayer === "durability_schema" ||
blockedLayer === "durability_migration" ||
blockedLayer === "durability_ssl"
);
}
function durableRuntimeBlockerCode(payload) {
if (!payload || typeof payload !== "object") return null;
const candidates = [
...(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,
payload.runtime?.connection?.queryResult,
payload.readiness?.durability?.queryResult,
payload.runtime?.durabilityContract?.blockedLayer,
payload.readiness?.durability?.blockedLayer
];
return candidates.find((value) => normalizedStatusValue(value).includes("durable") || normalizedStatusValue(value).endsWith("_blocked")) ?? null;
}
function normalizedStatusValue(value) {
return String(value ?? "").trim().toLowerCase();
}
function renderResourceTree() {
const items = [
{
+2 -2
View File
@@ -68,8 +68,8 @@
</div>
<div class="probe-card user-card" aria-live="polite">
<span class="metric-label">工作区状态</span>
<strong id="live-status" class="tone-source">只读模式</strong>
<small id="live-detail">可整理任务、查看资源、核对接线和可信记录;不会触发硬件变更</small>
<strong id="live-status" class="tone-pending">等待验证</strong>
<small id="live-detail">正在验证 hwlab-cloud-api /health/live、/v1、/v1/agent/chat 与 /v1/m3/io;未完成前不标为通过</small>
</div>
</header>
+562
View File
@@ -0,0 +1,562 @@
const PASS_STATUSES = Object.freeze(["ok", "ready", "pass", "passed", "healthy", "available", "live"]);
const ERROR_STATUSES = Object.freeze(["error", "failed", "failure"]);
const RAW_DEGRADED_STATUSES = Object.freeze(["degraded"]);
const BLOCKED_STATUSES = Object.freeze(["blocked", "unavailable", "rejected"]);
const UNVERIFIED_STATUSES = Object.freeze(["", "unknown", "pending", "loading", "probing", "unverified", "not_observed"]);
const READ_ONLY_REASON_CODES = Object.freeze([
"read_only",
"read-only",
"readonly",
"permission",
"permissions",
"forbidden",
"unauthorized",
"runtime_durable",
"runtime-durable",
"durability",
"durable",
"db_persistence",
"persistence",
"not-codex-stdio",
"not-write-capable",
"not-durable-session",
"text-chat-only",
"fallback:text-chat-only",
"security_blocked"
]);
export function classifyWorkbenchLiveStatus(live = {}) {
const probes = collectLiveStatusProbes(live);
const reachable = probes.some((probe) => probe.reachable);
if (!reachable) {
const failedProbe = probes.find((probe) => !probe.reachable) ?? probes[0];
return statusResult({
kind: "unverified",
label: "等待验证",
tone: "pending",
serviceId: failedProbe?.serviceId ?? "hwlab-cloud-api",
apiPath: failedProbe?.apiPath ?? "/health/live",
reasonCode: failedProbe?.reasonCode ?? "api_unverified",
detail: failedProbe
? `等待验证:${probeAttribution(failedProbe)}`
: "等待同源 API 验证;主工作台不会把未探测状态当作通过。",
probes
});
}
const errorProbe = probes.find((probe) => probe.kind === "error");
if (errorProbe) {
return statusResult({
kind: "error",
label: "API 错误",
tone: "blocked",
serviceId: errorProbe.serviceId,
apiPath: errorProbe.apiPath,
reasonCode: errorProbe.reasonCode,
traceId: errorProbe.traceId,
detail: `错误:${probeAttribution(errorProbe)}`,
probes
});
}
const readOnlyProbe = probes.find((probe) => probe.kind === "readonly");
if (readOnlyProbe) {
return statusResult({
kind: "readonly",
label: "只读模式",
tone: "source",
serviceId: readOnlyProbe.serviceId,
apiPath: readOnlyProbe.apiPath,
reasonCode: readOnlyProbe.reasonCode,
traceId: readOnlyProbe.traceId,
detail: `只读:${probeAttribution(readOnlyProbe)}`,
probes
});
}
const pendingProbe = probes.find((probe) => probe.kind === "unverified");
if (pendingProbe) {
return statusResult({
kind: "unverified",
label: "等待验证",
tone: "pending",
serviceId: pendingProbe.serviceId,
apiPath: pendingProbe.apiPath,
reasonCode: pendingProbe.reasonCode,
traceId: pendingProbe.traceId,
detail: `等待验证:${probeAttribution(pendingProbe)}`,
probes
});
}
return statusResult({
kind: "pass",
label: "API 正常",
tone: "dev-live",
serviceId: "hwlab-cloud-api",
apiPath: "/health/live",
reasonCode: "ready",
detail: "通过:hwlab-cloud-api /health/live ready;关键同源 API 已完成当前工作台只读验证。",
probes
});
}
export function collectLiveStatusProbes(live = {}) {
return [
classifyApiProbe(live.healthLive, {
serviceId: "hwlab-cloud-api",
apiPath: "/health/live",
fallbackReasonCode: "health_live_unverified"
}),
classifyApiProbe(live.restIndex, {
serviceId: "hwlab-cloud-api",
apiPath: "/v1",
fallbackReasonCode: "rest_index_unverified"
}),
classifyApiProbe(live.health, {
serviceId: "hwlab-cloud-api",
apiPath: "/json-rpc system.health",
fallbackReasonCode: "system_health_unverified"
}),
classifyApiProbe(live.adapter, {
serviceId: "hwlab-cloud-api",
apiPath: "/json-rpc cloud.adapter.describe",
fallbackReasonCode: "adapter_describe_unverified"
}),
classifyCodeAgentProbe(live),
classifyM3ControlProbe(live),
classifyM3StatusProbe(live)
].filter(Boolean);
}
function classifyApiProbe(probe, context) {
if (!probe) {
return unverifiedProbe({
...context,
reasonCode: context.fallbackReasonCode,
reason: "not_observed"
});
}
if (probe.ok !== true) {
return errorProbe({
...context,
reasonCode: probe.timeout ? "timeout" : probe.status ? `HTTP ${probe.status}` : context.fallbackReasonCode,
reason: probe.error ?? "request_failed",
httpStatus: probe.status,
traceId: traceIdFrom(probe)
});
}
const payload = probe.data;
const serviceId = payload?.serviceId ?? context.serviceId;
const durableReason = durableRuntimeReason(payload);
if (durableReason) {
return readOnlyProbe({
...context,
serviceId,
...durableReason,
rawStatus: rawStatusFrom(payload),
traceId: traceIdFrom(payload)
});
}
const blocker = firstBlocker(payload);
if (blocker) {
const reasonCode = String(blocker.code ?? blocker.reason ?? "blocked");
const reason = blocker.summary ?? blocker.message ?? blocker.reason ?? reasonCode;
if (isReadOnlyReason(reasonCode) || isReadOnlyReason(reason)) {
return readOnlyProbe({
...context,
serviceId,
reasonCode,
reason,
rawStatus: rawStatusFrom(payload),
traceId: traceIdFrom(payload)
});
}
return errorProbe({
...context,
serviceId,
reasonCode,
reason,
rawStatus: rawStatusFrom(payload),
traceId: traceIdFrom(payload)
});
}
const status = normalizedStatus(rawStatusFrom(payload));
if (ERROR_STATUSES.includes(status)) {
return errorProbe({
...context,
serviceId,
reasonCode: status,
reason: "API returned error status",
rawStatus: rawStatusFrom(payload),
traceId: traceIdFrom(payload)
});
}
if (RAW_DEGRADED_STATUSES.includes(status) || BLOCKED_STATUSES.includes(status) || readinessFalse(payload)) {
return errorProbe({
...context,
serviceId,
reasonCode: readinessFalse(payload) ? "ready_false_without_blocker" : status,
reason: readinessFalse(payload) ? "API returned ready=false without a concrete blocker" : `API returned ${status}`,
rawStatus: rawStatusFrom(payload),
traceId: traceIdFrom(payload)
});
}
if (UNVERIFIED_STATUSES.includes(status)) {
return unverifiedProbe({
...context,
serviceId,
reasonCode: context.fallbackReasonCode,
reason: "status_not_observed",
rawStatus: rawStatusFrom(payload),
traceId: traceIdFrom(payload)
});
}
return passProbe({
...context,
serviceId,
reasonCode: status || "ready",
reason: "ready",
rawStatus: rawStatusFrom(payload),
traceId: traceIdFrom(payload)
});
}
function classifyCodeAgentProbe(live = {}) {
const availability = live.restIndex?.data?.codeAgent ?? live.healthLive?.data?.codeAgent;
if (!availability) {
return unverifiedProbe({
serviceId: "hwlab-cloud-api",
apiPath: "/v1/agent/chat",
reasonCode: "code_agent_unverified",
reason: "availability_not_observed"
});
}
const serviceId = "hwlab-cloud-api";
const apiPath = availability.endpoint ?? "POST /v1/agent/chat";
const provider = availability.provider ?? "not_observed";
const backend = availability.backend ?? "not_observed";
const capability = availability.capabilityLevel ?? availability.runner?.capabilityLevel;
const providerStatus = availability.error?.providerStatus ?? availability.providerStatus;
if (availability.status === "blocked" || availability.ready === false) {
return errorProbe({
serviceId,
apiPath,
reasonCode: providerStatus ? `provider_http_${providerStatus}` : availability.reason ?? availability.blocker ?? "code_agent_blocked",
reason: availability.summary ?? `provider=${provider}; backend=${backend}; capability=${capability ?? "unknown"}`,
traceId: traceIdFrom(availability),
meta: { provider, backend, capability }
});
}
if (capability === "text-chat-only" || availability.runner?.kind === "openai-responses-fallback") {
return readOnlyProbe({
serviceId,
apiPath,
reasonCode: "fallback_text_chat_only",
reason: `provider=${provider}; backend=${backend}; capability=text-chat-only`,
traceId: traceIdFrom(availability),
meta: { provider, backend, capability }
});
}
if (availability.runner?.ready === true && availability.runner?.writeCapable === false) {
return readOnlyProbe({
serviceId,
apiPath,
reasonCode: "codex_readonly_runner",
reason: `runner=${availability.runner.kind ?? "hwlab-readonly-runner"}; capability=${capability ?? "read-only-tools"}`,
traceId: traceIdFrom(availability),
meta: { provider, backend, capability }
});
}
if (availability.status === "available" || availability.ready === true) {
return passProbe({
serviceId,
apiPath,
reasonCode: capability ?? "available",
reason: `provider=${provider}; backend=${backend}`,
traceId: traceIdFrom(availability),
meta: { provider, backend, capability }
});
}
return unverifiedProbe({
serviceId,
apiPath,
reasonCode: "code_agent_unverified",
reason: `provider=${provider}; backend=${backend}`,
traceId: traceIdFrom(availability),
meta: { provider, backend, capability }
});
}
function classifyM3ControlProbe(live = {}) {
const probe = live.m3Control;
const context = {
serviceId: "hwlab-cloud-api",
apiPath: "/v1/m3/io",
fallbackReasonCode: "m3_io_unverified"
};
if (!probe) {
return unverifiedProbe({ ...context, reasonCode: context.fallbackReasonCode, reason: "not_observed" });
}
if (probe.ok !== true) {
return errorProbe({
...context,
reasonCode: probe.timeout ? "timeout" : probe.status ? `HTTP ${probe.status}` : "m3_io_request_failed",
reason: probe.error ?? "M3 IO readiness request failed",
httpStatus: probe.status,
traceId: traceIdFrom(probe)
});
}
const data = probe.data;
const readiness = data?.readiness ?? {};
if (data?.status === "available" && readiness.status === "ready" && readiness.controlReady === true) {
return passProbe({
...context,
reasonCode: "m3_io_ready",
reason: "cloud-api -> gateway-simu -> box-simu -> hwlab-patch-panel readiness ready",
rawStatus: data.status,
traceId: traceIdFrom(data)
});
}
const blocker = readiness.blocker ?? data?.blocker;
return errorProbe({
...context,
serviceId: serviceForM3Layer(blocker?.layer) ?? context.serviceId,
reasonCode: blocker?.code ?? data?.status ?? "m3_io_not_ready",
reason: blocker?.zh ?? data?.blockedReason ?? "M3 IO readiness is not ready",
rawStatus: data?.status,
traceId: traceIdFrom(data)
});
}
function classifyM3StatusProbe(live = {}) {
const probe = live.m3Status;
const context = {
serviceId: "hwlab-cloud-api",
apiPath: "/v1/m3/status",
fallbackReasonCode: "m3_status_unverified"
};
if (!probe) {
return unverifiedProbe({ ...context, reasonCode: context.fallbackReasonCode, reason: "not_observed" });
}
if (probe.ok !== true) {
return errorProbe({
...context,
reasonCode: probe.timeout ? "timeout" : probe.status ? `HTTP ${probe.status}` : "m3_status_request_failed",
reason: probe.error ?? "M3 status aggregation failed",
httpStatus: probe.status,
traceId: traceIdFrom(probe)
});
}
const data = probe.data;
const blocker = data?.blocker;
if (data?.status === "error") {
return errorProbe({
...context,
serviceId: serviceForM3Layer(blocker?.layer) ?? context.serviceId,
reasonCode: blocker?.code ?? "m3_status_error",
reason: blocker?.zh ?? data?.summary ?? "M3 status returned error",
rawStatus: data.status,
traceId: traceIdFrom(data)
});
}
if (data?.status === "blocked" && blocker) {
const serviceId = serviceForM3Layer(blocker.layer) ?? (isReadOnlyReason(blocker.code) ? context.serviceId : context.serviceId);
const factory = isReadOnlyReason(blocker.code) || isReadOnlyReason(blocker.zh) ? readOnlyProbe : errorProbe;
return factory({
...context,
serviceId,
reasonCode: blocker.code ?? "m3_status_blocked",
reason: blocker.zh ?? data.summary ?? "M3 status blocked",
rawStatus: data.status,
traceId: traceIdFrom(data)
});
}
if (data?.status === "blocked") {
return unverifiedProbe({
...context,
reasonCode: "m3_status_waiting_trusted_record",
reason: data.summary ?? "M3 status is waiting for trusted persistence evidence",
rawStatus: data.status,
traceId: traceIdFrom(data)
});
}
if (data?.status === "live" || data?.sourceKind === "DEV-LIVE") {
return passProbe({
...context,
reasonCode: data.status ?? "dev_live",
reason: data.summary ?? "M3 status observed through cloud-api",
rawStatus: data.status,
traceId: traceIdFrom(data)
});
}
return unverifiedProbe({
...context,
reasonCode: "m3_status_unverified",
reason: data?.summary ?? "M3 status not yet verified",
rawStatus: data?.status,
traceId: traceIdFrom(data)
});
}
function durableRuntimeReason(payload) {
const runtime = payload?.runtime;
const durability = payload?.readiness?.durability;
const reasonCode = firstNonEmpty(
runtime?.blocker,
durability?.blocker,
runtime?.connection?.queryResult,
durability?.queryResult,
runtime?.durabilityContract?.blockedLayer,
durability?.blockedLayer,
blockerCodeStartingWith(payload, "runtime_durable_adapter_")
);
const blockedLayer = firstNonEmpty(runtime?.durabilityContract?.blockedLayer, durability?.blockedLayer);
const runtimeDurable = runtime?.durable === true || durability?.durable === true;
const runtimeExplicitlyNonDurable = runtime?.durable === false || durability?.durable === false;
const runtimeReady = runtime?.ready === true || durability?.ready === true;
const liveRuntimeEvidence = runtime?.liveRuntimeEvidence === true || durability?.liveRuntimeEvidence === true;
if (!reasonCode && runtimeDurable && runtimeReady && liveRuntimeEvidence) return null;
if (runtimeExplicitlyNonDurable) {
return {
reasonCode: "runtime_durable_false",
reason: `runtime durable=false; adapter=${firstNonEmpty(runtime?.adapter, durability?.adapter, "unknown")}`,
layer: "runtime-durable"
};
}
if (
reasonCode &&
(
isReadOnlyReason(reasonCode) ||
String(reasonCode).endsWith("_blocked") ||
normalizedStatus(blockedLayer).includes("durability")
)
) {
return {
reasonCode,
reason: `runtime durable blocked at ${blockedLayer || "durability"}; queryResult=${firstNonEmpty(runtime?.connection?.queryResult, durability?.queryResult, "unknown")}`,
layer: blockedLayer || "runtime-durable"
};
}
return null;
}
function firstBlocker(payload) {
const blockers = [
...(Array.isArray(payload?.blockers) ? payload.blockers : []),
...(Array.isArray(payload?.readiness?.blockers) ? payload.readiness.blockers : [])
].filter(Boolean);
if (blockers.length > 0) return blockers[0];
const code = firstNonEmpty(
...(Array.isArray(payload?.blockerCodes) ? payload.blockerCodes : []),
...(Array.isArray(payload?.readiness?.blockerCodes) ? payload.readiness.blockerCodes : [])
);
return code ? { code, summary: code } : null;
}
function blockerCodeStartingWith(payload, prefix) {
return [
...(Array.isArray(payload?.blockerCodes) ? payload.blockerCodes : []),
...(Array.isArray(payload?.readiness?.blockerCodes) ? payload.readiness.blockerCodes : [])
].find((code) => String(code ?? "").startsWith(prefix));
}
function readinessFalse(payload) {
return [
payload?.ready,
payload?.readiness?.ready,
payload?.readiness?.durability?.ready,
payload?.runtime?.ready
].some((value) => value === false);
}
function rawStatusFrom(payload) {
return payload?.status ?? payload?.readiness?.status ?? payload?.runtime?.status ?? payload?.readiness?.durability?.status ?? "";
}
function statusResult(result) {
return {
serviceId: null,
apiPath: null,
reasonCode: null,
traceId: null,
internalRawStatuses: result.probes
.map((probe) => probe.rawStatus)
.filter((status) => status !== undefined && status !== null && String(status).trim() !== ""),
...result
};
}
function passProbe(options) {
return probe({ kind: "pass", reachable: true, ...options });
}
function readOnlyProbe(options) {
return probe({ kind: "readonly", reachable: true, ...options });
}
function errorProbe(options) {
return probe({ kind: "error", reachable: Boolean(options.httpStatus ? options.httpStatus >= 200 && options.httpStatus < 600 : true), ...options });
}
function unverifiedProbe(options) {
return probe({ kind: "unverified", reachable: false, ...options });
}
function probe(options) {
return {
kind: options.kind,
reachable: options.reachable,
serviceId: options.serviceId,
apiPath: options.apiPath,
reasonCode: String(options.reasonCode ?? options.kind),
reason: String(options.reason ?? options.reasonCode ?? options.kind),
httpStatus: options.httpStatus ?? null,
rawStatus: options.rawStatus ?? null,
traceId: options.traceId ?? null,
meta: options.meta ?? null
};
}
function probeAttribution(probe) {
return [
`${probe.serviceId} ${probe.apiPath}`,
probe.httpStatus ? `HTTP ${probe.httpStatus}` : null,
probe.reasonCode,
probe.reason && probe.reason !== probe.reasonCode ? probe.reason : null,
probe.traceId ? `trace=${probe.traceId}` : null
].filter(Boolean).join(" / ");
}
function normalizedStatus(value) {
return String(value ?? "").trim().toLowerCase();
}
function isReadOnlyReason(value) {
const text = normalizedStatus(value);
return READ_ONLY_REASON_CODES.some((code) => text.includes(code));
}
function traceIdFrom(value) {
return firstNonEmpty(value?.traceId, value?.meta?.traceId, value?.request?.traceId, value?.operation?.traceId);
}
function serviceForM3Layer(layer) {
const text = normalizedStatus(layer);
if (!text) return null;
if (text.includes("patch")) return "hwlab-patch-panel";
if (text.includes("gateway")) return "hwlab-gateway-simu";
if (text.includes("box") || text.includes("resource")) return "hwlab-box-simu";
if (text.includes("runtime") || text.includes("durable")) return "hwlab-cloud-api";
return null;
}
function firstNonEmpty(...values) {
for (const value of values) {
if (value !== undefined && value !== null && String(value).trim() !== "") return String(value).trim();
}
return "";
}
+27 -17
View File
@@ -21,6 +21,7 @@ const requiredFiles = [
"index.html",
"styles.css",
"app.mjs",
"live-status.mjs",
"gate-summary.mjs",
"runtime.mjs",
"workbench-hardware-panel.mjs",
@@ -39,6 +40,7 @@ for (const file of requiredFiles) {
const html = fs.readFileSync(path.resolve(rootDir, "index.html"), "utf8");
const styles = fs.readFileSync(path.resolve(rootDir, "styles.css"), "utf8");
const app = fs.readFileSync(path.resolve(rootDir, "app.mjs"), "utf8");
const liveStatus = fs.readFileSync(path.resolve(rootDir, "live-status.mjs"), "utf8");
const helpMarkdown = fs.readFileSync(path.resolve(rootDir, "help.md"), "utf8");
const buildScript = fs.readFileSync(path.resolve(rootDir, "scripts/build.mjs"), "utf8");
const distContractScript = fs.readFileSync(path.resolve(rootDir, "scripts/dist-contract.mjs"), "utf8");
@@ -432,6 +434,7 @@ assert.match(buildScript, /buildCloudWebDist/);
assert.match(distContractScript, /"help\.md"/);
assert.match(distContractScript, /"third_party\/marked\/marked\.esm\.js"/);
assert.match(distContractScript, /"third_party\/marked\/LICENSE"/);
assert.match(distContractScript, /"live-status\.mjs"/);
assert.match(distContractScript, /gate\/index\.html/);
assert.match(distContractScript, /diagnostics\/gate\/index\.html/);
assert.match(distContractScript, /help\/index\.html/);
@@ -439,23 +442,30 @@ 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(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"), /runtime_durable_adapter_/);
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 返回 blocked\/degraded\/failed 或 ready=false;当前保持只读查看和本地任务整理。/);
assert.match(app, /\/health\/live 可响应,但可信记录持久化尚未就绪:/);
assert.match(app, /classifyWorkbenchLiveStatus/);
assert.match(liveStatus, /export function classifyWorkbenchLiveStatus/);
assert.match(liveStatus, /label:\s*"API 正常"/);
assert.match(liveStatus, /label:\s*"API 错误"/);
assert.match(liveStatus, /label:\s*"等待验证"/);
assert.match(liveStatus, /label:\s*"只读模式"/);
assert.match(liveStatus, /internalRawStatuses/);
assert.match(liveStatus, /function classifyCodeAgentProbe/);
assert.match(liveStatus, /function classifyM3ControlProbe/);
assert.match(liveStatus, /function classifyM3StatusProbe/);
assert.match(liveStatus, /\/v1\/agent\/chat/);
assert.match(liveStatus, /\/v1\/m3\/io/);
assert.match(liveStatus, /\/v1\/m3\/status/);
assert.match(liveStatus, /hwlab-cloud-api/);
assert.match(liveStatus, /hwlab-patch-panel/);
assert.match(liveStatus, /hwlab-gateway-simu/);
assert.match(liveStatus, /hwlab-box-simu/);
assert.match(liveStatus, /runtime_durable_adapter_/);
assert.match(liveStatus, /fallback_text_chat_only/);
assert.match(liveStatus, /provider_http_/);
assert.match(liveStatus, /rawStatus/);
assert.doesNotMatch(functionBody(app, "workbenchApiSurfaceStatus"), /surface\.degraded/);
assert.doesNotMatch(app, /API 错误 \/ 只读模式/);
assert.doesNotMatch(app, /可信记录受阻 \/ 只读模式/);
assert.doesNotMatch(app, /API 降级 \/ 只读模式/);
assert.doesNotMatch(functionBody(app, "workbenchApiSurfaceStatus"), /runtime_durable_adapter_query_blocked|DB live readiness|Gate|诊断|验收|M0-M5|执行轨迹/u);
assert.match(html, /quick-actions/);
@@ -9,6 +9,7 @@ export const cloudWebDistRuntimeFiles = Object.freeze([
"index.html",
"styles.css",
"app.mjs",
"live-status.mjs",
"gate-summary.mjs",
"runtime.mjs",
"workbench-hardware-panel.mjs",
@@ -0,0 +1,313 @@
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,
codeAgent: {
endpoint: "POST /v1/agent/chat",
status: "available",
ready: true,
provider: "openai-responses",
backend: "hwlab-cloud-api/openai-responses",
capabilityLevel: "available"
}
}
});
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: okRest,
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 concrete API failure with service path and HTTP status", () => {
const status = classifyWorkbenchLiveStatus({
healthLive: {
ok: false,
path: "/health/live",
status: 503,
error: "/health/live HTTP 503runtime 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("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
}
},
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, /ready=false without a concrete blocker/u);
assert.equal(status.internalRawStatuses.includes("degraded"), true);
assert.doesNotMatch(`${status.label} ${status.detail}`, /API 降级/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",
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);
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"
}
}
},
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");
});
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",
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);
});