fix: collapse internal gate review to live table

This commit is contained in:
Code Queue Review
2026-05-23 09:07:39 +00:00
parent 0c12cd71a5
commit 93373ac104
13 changed files with 1007 additions and 526 deletions
@@ -216,6 +216,13 @@ test("source/default smoke covers #276 two-column wiring long-table contract", (
assert.equal(check.evidence.includes("legacy 8-column table forbidden"), true);
});
test("source/default smoke covers #288 gate single-table contract", () => {
const report = runDevCloudWorkbenchStaticSmoke();
const check = report.checks.find((item) => item.id === "feedback-288-gate-single-table");
assert.equal(check?.status, "pass");
assert.equal(report.checks.some((item) => item.id === "feedback-119-gate-retains-diagnostics"), false);
});
test("live workbench identity passes only when runtime commit or image tag matches current source", () => {
assert.equal(
classifyLiveDeploymentIdentity(sourceIdentity, {
+222 -31
View File
@@ -89,12 +89,35 @@ const defaultHomepageForbiddenTerms = Object.freeze([
"waiting-for-dev-live"
]);
const diagnosticsRequiredTerms = Object.freeze([
"Gate / 诊断 / 验收",
"M0-M5",
"BLOCKED",
"DB live readiness",
"patch-panel DO1 -> DI1"
const gateReviewTableColumns = Object.freeze([
"类别",
"检查项",
"状态",
"归因对象",
"关键细节",
"证据/trace",
"更新时间",
"下一步"
]);
const gateReviewStatusLabels = Object.freeze([
"通过",
"阻塞",
"失败",
"待验证",
"信息"
]);
const legacyGateDashboardMarkers = Object.freeze([
"workspace-panel",
"gate-grid",
"gate-split",
"milestone-grid",
"trace-list",
"health-body",
"diagnostics-list",
"method-list",
"unblock-list"
]);
const gateRouteAliases = Object.freeze(["/gate", "/diagnostics/gate"]);
@@ -370,9 +393,9 @@ function runStaticSmoke() {
evidence: ["工作台", "内部复核", "使用说明", "资源树"]
});
addCheck(checks, blockers, "feedback-119-gate-retains-diagnostics", gateRetainsDiagnostics(files.html, files.app), "Internal Gate page retains BLOCKED/M0-M5 diagnostics and the DB live plus patch-panel DO1 -> DI1 unblock path.", {
addCheck(checks, blockers, "feedback-288-gate-single-table", gateSingleTableContract(files), "Internal review /gate is a single read-only table backed by the live diagnostics aggregation route.", {
blocker: "contract_blocker",
evidence: diagnosticsRequiredTerms
evidence: [...gateReviewTableColumns, "/v1/diagnostics/gate", "LIVE-BACKEND", ...gateReviewStatusLabels]
});
addCheck(checks, blockers, "internal-gate-route-aliases", gateRouteAliasesAreServed(files.app, artifactPublisher, buildScript, distContractScript), "/gate and /diagnostics/gate are direct internal diagnostic aliases, not default routes.", {
@@ -439,7 +462,7 @@ function runStaticSmoke() {
addCheck(checks, blockers, "same-origin-readonly-boundary", hasSameOriginReadOnlyBoundary(files.app), "Workbench data access stays same-origin and JSON-RPC diagnostics stay read-only.", {
blocker: "safety_blocker",
evidence: ["/health/live", "/v1", "/v1/agent/chat", "/json-rpc", ...readOnlyRpcMethods]
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.", {
@@ -839,8 +862,8 @@ async function runLiveDomOnlySmoke(args) {
liveMode: "dom-only-readonly-no-code-agent-post",
codeAgentBrowserJourneySkipped: true,
codeAgentPostSent: false,
retainedApiFields: ["runtime.commitId", "runtime.imageTag", "web asset hash/status", "DOM route/control/wiring observations"],
statement: "DOM-only live smoke preserves runtime and web-asset identity preflight semantics, continues with read-only browser DOM/help inspection even when identity is blocked, and never posts /v1/agent/chat, reads Secret values, mutates live DEV, or claims M3/M4/M5 acceptance."
retainedApiFields: ["runtime.commitId", "runtime.imageTag", "web asset hash/status", "DOM route/control/wiring observations", "gate single-table observations"],
statement: "DOM-only live smoke preserves runtime and web-asset identity preflight semantics, continues with read-only browser DOM/help/gate table inspection even when identity is blocked, and never posts /v1/agent/chat, reads Secret values, mutates live DEV, or claims M3/M4/M5 acceptance."
}
});
}
@@ -1376,7 +1399,8 @@ function baseReport({
"pikasTech/HWLAB#143",
"pikasTech/HWLAB#164",
"pikasTech/HWLAB#224",
"pikasTech/HWLAB#276"
"pikasTech/HWLAB#276",
"pikasTech/HWLAB#288"
],
mode,
url,
@@ -1504,7 +1528,8 @@ function readStaticFiles() {
styles: readText("web/hwlab-cloud-web/styles.css"),
app: readText("web/hwlab-cloud-web/app.mjs"),
runtime: readText("web/hwlab-cloud-web/runtime.mjs"),
panel: readText("web/hwlab-cloud-web/workbench-hardware-panel.mjs")
panel: readText("web/hwlab-cloud-web/workbench-hardware-panel.mjs"),
help: readText("web/hwlab-cloud-web/help.md")
};
}
@@ -1771,8 +1796,18 @@ function firstTagForDataView(html, view) {
function sectionSource(source, viewId) {
const start = source.search(new RegExp(`<section\\b[^>]*\\bdata-view=["']${escapeRegExp(viewId)}["'][^>]*>`, "u"));
if (start === -1) return "";
const next = source.slice(start + 1).search(/<section\b[^>]*\bdata-view=["'][^"']+["'][^>]*>/u);
return next === -1 ? source.slice(start) : source.slice(start, start + 1 + next);
const tagPattern = /<\/?section\b[^>]*>/giu;
tagPattern.lastIndex = start;
let depth = 0;
for (let match = tagPattern.exec(source); match; match = tagPattern.exec(source)) {
if (match[0].startsWith("</")) {
depth -= 1;
if (depth === 0) return source.slice(start, tagPattern.lastIndex);
} else {
depth += 1;
}
}
return source.slice(start);
}
function beforeSection(source, viewId) {
@@ -1846,10 +1881,29 @@ function hasCompletePrimaryNavigation(html) {
return navLabelsPresent && legacyLabelsAbsent && noLeftSideRecordsShortcut;
}
function gateRetainsDiagnostics(html, app) {
function gateSingleTableContract({ html, app, styles }) {
const gateHtml = sectionSource(html, "gate");
const gateSource = `${gateHtml}\n${app}`;
return diagnosticsRequiredTerms.every((term) => gateSource.includes(term));
const gateSource = `${gateHtml}\n${app}\n${styles}`;
const tableMatches = gateHtml.match(/<table\b[^>]*\bclass=["'][^"']*\bgate-table\b[^"']*["'][^>]*>/gu) ?? [];
const controlIds = ["gate-source-status", "gate-status-filter", "gate-search", "gate-refresh", "gate-review-body"];
const oldRenderFunctions = /function\s+(?:renderTrace|renderUnblockList|renderDiagnostics|renderGateDiagnostics|renderMethodList)\s*\(/u;
return (
/<h2\b[^>]*\bid=["']gate-title["'][^>]*>内部复核<\/h2>/u.test(gateHtml) &&
tableMatches.length === 1 &&
gateReviewTableColumns.every((column) => gateHtml.includes(`<th>${column}</th>`)) &&
controlIds.every((id) => gateHtml.includes(`id="${id}"`) || gateHtml.includes(`id='${id}'`)) &&
gateReviewStatusLabels.every((label) => gateSource.includes(label)) &&
/fetchJson\("\/v1\/diagnostics\/gate"/u.test(app) &&
/function\s+renderGateTable\s*\(/u.test(app) &&
/function\s+normalizeGateRow\s*\(/u.test(app) &&
/function\s+gateLiveBlockerRow\s*\(/u.test(app) &&
/sourceKind:\s*"LIVE-BACKEND"/u.test(app) &&
/live 后端聚合不可用;页面不会回退到 SOURCE 静态拓扑。/u.test(app) &&
/\.gate-table\b/u.test(styles) &&
/\.gate-controls\b/u.test(styles) &&
legacyGateDashboardMarkers.every((marker) => !gateHtml.includes(marker)) &&
!oldRenderFunctions.test(app)
);
}
function gateRouteAliasesAreServed(app, artifactPublisher, buildScript, distContractScript) {
@@ -1998,7 +2052,7 @@ function hasScrollLockContract(styles) {
/body\s*>\s*\[data-app-shell\]\s*\{[^\}]*min-height:\s*0;/su.test(styles) &&
/\.workbench-shell\s*\{[^\}]*height:\s*100(?:d)?vh;[^\}]*overflow:\s*hidden;/su.test(styles) &&
/\.view\s*\{[^\}]*min-height:\s*0;[^\}]*overflow:\s*auto;/su.test(styles) &&
/\.(?:resource-tree|compact-list|conversation-list|trace-list|task-list|hardware-list)[^\{]*\{[^\}]*min-height:\s*0;[^\}]*overflow:\s*auto;/su.test(styles)
/\.(?:resource-tree|compact-list|conversation-list|task-list|hardware-list)[^\{]*\{[^\}]*min-height:\s*0;[^\}]*overflow:\s*auto;/su.test(styles)
);
}
@@ -2063,6 +2117,7 @@ function hasSameOriginReadOnlyBoundary(app) {
/fetchJson\("\/health\/live"\)/u.test(app) &&
/fetchJson\("\/v1"\)/u.test(app) &&
/fetchJson\("\/v1\/m3\/status"\)/u.test(app) &&
/fetchJson\("\/v1\/diagnostics\/gate"/u.test(app) &&
/fetchJson\("\/v1\/agent\/chat"/u.test(app) &&
/fetchJson\("\/json-rpc"/u.test(app) &&
JSON.stringify(rpcCalls) === JSON.stringify([...readOnlyRpcMethods].sort()) &&
@@ -2254,7 +2309,7 @@ function sourceEvidenceNotDevLive(source) {
gateSummary.auditEvents.every((event) => event.dryRun === true) &&
gateSummary.evidenceRecords.every((record) => record.dryRun === true) &&
gateSummary.safety.allowNetwork === false &&
["来源 SOURCE", "演练 DRY-RUN", "开发实况 DEV-LIVE", "阻塞 BLOCKED"].every((label) => source.includes(label))
["`SOURCE`", "`DRY-RUN`", "`DEV-LIVE`", "`BLOCKED`"].every((label) => source.includes(label))
);
}
@@ -2875,13 +2930,34 @@ async function inspectLiveGateRoute(url, options = {}) {
try {
browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
const gateBackendRequests = [];
page.on("request", (request) => {
if (new URL(request.url()).pathname === "/v1/diagnostics/gate") {
gateBackendRequests.push({ method: request.method(), urlPath: "/v1/diagnostics/gate" });
}
});
const postGuard = options.noCodeAgentPost ? await installNoCodeAgentPostGuard(page) : null;
await page.goto(new URL("/gate", url).toString(), { waitUntil: "domcontentloaded", timeout: 12000 });
const gate = await page.evaluate(() => {
await page.waitForFunction(() => {
const rows = document.querySelectorAll("#gate-review-body tr");
const source = document.querySelector("#gate-source-status")?.textContent ?? "";
return rows.length > 0 && !/等待 live 后端|读取 live 后端中/u.test(source);
}, null, { timeout: 10000 });
const gate = await page.evaluate(({ columns, statuses, legacyMarkers }) => {
const workspace = document.querySelector('[data-view="workspace"]');
const gateView = document.querySelector('[data-view="gate"]');
const help = document.querySelector('[data-view="help"]');
const text = document.body.textContent ?? "";
const gateText = gateView?.textContent ?? "";
const tableHeaders = [...(gateView?.querySelectorAll(".gate-table thead th") ?? [])]
.map((cell) => cell.textContent?.trim() ?? "");
const tableRows = [...(gateView?.querySelectorAll("#gate-review-body tr") ?? [])].map((row) =>
[...row.querySelectorAll("td")].map((cell) => cell.textContent?.replace(/\s+/gu, " ").trim() ?? "")
);
const tableStatuses = [...(gateView?.querySelectorAll("#gate-review-body tr[data-status]") ?? [])]
.map((row) => row.getAttribute("data-status"))
.filter(Boolean);
const blockedRows = [...(gateView?.querySelectorAll('#gate-review-body tr[data-status="阻塞"], #gate-review-body tr[data-status-key="blocked"]') ?? [])];
const blockedRowsGreen = blockedRows.some((row) => row.querySelector(".tone-pass, .tone-ok, .tone-healthy"));
return {
pathname: window.location.pathname,
hash: window.location.hash,
@@ -2889,26 +2965,67 @@ async function inspectLiveGateRoute(url, options = {}) {
workspaceHidden: workspace ? workspace.hidden : null,
gateHidden: gateView ? gateView.hidden : null,
helpHidden: help ? help.hidden : null,
diagnosticsTermsPresent: ["Gate / 诊断 / 验收", "M0-M5", "BLOCKED", "DB live readiness", "patch-panel DO1 -> DI1"].every((term) => text.includes(term))
heading: document.querySelector("#gate-title")?.textContent?.trim() ?? "",
sourceStatus: document.querySelector("#gate-source-status")?.textContent?.trim() ?? "",
tableCount: gateView?.querySelectorAll("table").length ?? 0,
mainTableCount: gateView?.querySelectorAll(".gate-table").length ?? 0,
tableHeaders,
tableRows,
tableStatuses,
controls: {
statusFilter: Boolean(gateView?.querySelector("#gate-status-filter")),
search: Boolean(gateView?.querySelector("#gate-search")),
refresh: Boolean(gateView?.querySelector("#gate-refresh")),
controlCount: gateView?.querySelectorAll("select, input, button").length ?? 0
},
requiredHeadersPresent: columns.every((column) => tableHeaders.includes(column)),
statusLabelsChinese: tableStatuses.length > 0 && tableStatuses.every((status) => statuses.includes(status)),
rowsPresent: tableRows.length > 0,
liveBackendOrBlockerVisible: /live 后端|\/v1\/diagnostics\/gate|LIVE-BACKEND/u.test(gateText),
legacyLayoutAbsent: legacyMarkers.every((marker) => !gateView?.querySelector(`.${marker}, #${marker}`)),
userOperationsAbsent:
!gateView?.querySelector("#command-form, #m3-control-form, [data-side-tab], [data-side-tab-jump], #command-input, #command-send"),
blockedRowsGreen,
oldDashboardTermsAbsent: !/(M0-M5|Gate \/ 诊断 \/ 验收|执行轨迹)/u.test(gateText)
};
}, {
columns: gateReviewTableColumns,
statuses: gateReviewStatusLabels,
legacyMarkers: legacyGateDashboardMarkers
});
gate.backendRouteRequested = gateBackendRequests.some((request) => request.method === "GET");
const pass =
gate.pathname === "/gate" &&
gate.title === "HWLAB 云工作台" &&
gate.workspaceHidden === true &&
gate.gateHidden === false &&
(gate.helpHidden === true || gate.helpHidden === null) &&
gate.diagnosticsTermsPresent &&
gate.heading === "内部复核" &&
gate.tableCount === 1 &&
gate.mainTableCount === 1 &&
gate.requiredHeadersPresent &&
gate.rowsPresent &&
gate.statusLabelsChinese &&
gate.liveBackendOrBlockerVisible &&
gate.legacyLayoutAbsent &&
gate.userOperationsAbsent &&
!gate.blockedRowsGreen &&
gate.oldDashboardTermsAbsent &&
gate.backendRouteRequested &&
(postGuard ? postGuard.attempts.length === 0 : true);
return {
status: pass ? "pass" : "blocked",
summary: pass
? "Direct /gate route opens the internal diagnostics view without becoming the default workbench."
: "Direct /gate route did not satisfy the internal diagnostics route contract.",
? "Direct /gate route opens the internal single-table review view backed by the live diagnostics route."
: "Direct /gate route did not satisfy the internal single-table review contract.",
evidence: [
`pathname=${gate.pathname}`,
`gateHidden=${gate.gateHidden}`,
`diagnosticsTermsPresent=${gate.diagnosticsTermsPresent}`,
`heading=${gate.heading}`,
`tableCount=${gate.tableCount}`,
`rows=${gate.tableRows.length}`,
`statuses=${gate.tableStatuses.join(",") || "none"}`,
`backendRouteRequested=${gate.backendRouteRequested}`,
...(postGuard ? [`codeAgentPostAttempts=${postGuard.attempts.length}`] : [])
],
observations: {
@@ -3317,21 +3434,18 @@ async function inspectDefaultApiStatus(page) {
return page.evaluate(() => {
const liveStatus = document.querySelector("#live-status")?.textContent?.trim() ?? "";
const liveDetail = document.querySelector("#live-detail")?.textContent?.trim() ?? "";
const methodText = [...document.querySelectorAll("#method-list .badge")].map((element) => element.textContent?.trim() ?? "");
const firstScreenText = document.querySelector(".topbar")?.textContent?.replace(/\s+/gu, " ").trim() ?? "";
const forbidden = /Gate|诊断|验收|BLOCKED|M0-M5|执行轨迹|runtime_durable_adapter_query_blocked|DB live readiness/u;
return {
liveStatus,
liveDetail,
methodText,
firstScreenText,
degradedStatusVisible:
(liveStatus === "可信记录受阻 / 只读模式" || liveStatus === "API 错误 / 只读模式") &&
(
liveDetail.startsWith("/health/live 可响应,但可信记录持久化尚未就绪:") ||
liveDetail === "同源 API 返回 blocked/degraded/failed 或 ready=false;当前保持只读查看和本地任务整理。"
) &&
methodText.includes("system.health"),
),
defaultCopyIsWorkbenchSafe: !forbidden.test(`${liveStatus} ${liveDetail} ${firstScreenText}`)
};
});
@@ -4213,6 +4327,61 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {}
});
return true;
}
if (request.method === "GET" && url.pathname === "/v1/diagnostics/gate") {
const timestamp = "2026-05-22T00:00:00.000Z";
jsonResponse(response, 200, {
serviceId: "hwlab-cloud-api",
route: "/v1/diagnostics/gate",
contractVersion: "gate-diagnostics-table-v1",
status: "blocked",
sourceKind: "LIVE-BACKEND",
liveBackend: true,
observedAt: timestamp,
rowCount: 3,
columns: [...gateReviewTableColumns],
rows: [
localGateReviewRow({
category: "健康检查",
check: "/health/live readiness",
statusKey: "blocked",
owner: "hwlab-cloud-api",
detail: "Local fixture mirrors degraded live backend readiness for browser contract verification.",
evidence: ["GET /health/live", "blocker=runtime_durable_adapter_query_blocked"],
updatedAt: timestamp,
next: "DEV live 验证必须读取真实 /v1/diagnostics/gate。"
}),
localGateReviewRow({
category: "证据",
check: "audit.event.query",
statusKey: "pending",
owner: "runtime-store / audit",
detail: "Local fixture has no audit records; this is not DEV-LIVE evidence.",
evidence: ["POST /json-rpc audit.event.query"],
updatedAt: timestamp,
next: "等待真实 live audit。"
}),
localGateReviewRow({
category: "路由",
check: "single-table contract",
statusKey: "info",
owner: "cloud-web",
detail: "Browser smoke only validates the read-only table surface.",
evidence: ["GET /v1/diagnostics/gate", "LIVE-BACKEND fixture shape"],
updatedAt: timestamp,
next: "上线后以 DEV live route 结果为准。"
})
],
safety: {
frontendHardcodedRows: false,
sourceFixturePromoted: false,
localStorageUsed: false,
blockedIsGreen: false,
hardwareControlSurface: false,
codeAgentConversationSurface: false
}
});
return true;
}
if (request.method === "POST" && url.pathname === "/json-rpc") {
const body = await readJsonBody(request);
jsonResponse(response, 200, {
@@ -4419,6 +4588,28 @@ async function hardwarePanelText(page) {
return page.evaluate(() => document.querySelector("#hardware-list")?.textContent?.replace(/\s+/gu, " ").trim() ?? "");
}
function localGateReviewRow({ category, check, statusKey, owner, detail, evidence, updatedAt, next }) {
const label = {
pass: "通过",
blocked: "阻塞",
failed: "失败",
pending: "待验证",
info: "信息"
}[statusKey] ?? "阻塞";
return {
category,
check,
status: label,
statusKey,
owner,
detail,
evidence,
updatedAt,
next,
sourceKind: "LIVE-BACKEND"
};
}
function delay(ms) {
const normalized = Number.isFinite(Number(ms)) && Number(ms) > 0 ? Number(ms) : 0;
return new Promise((resolve) => setTimeout(resolve, normalized));
+1
View File
@@ -331,6 +331,7 @@ export function checkFrontendNoDirectRuntimeCalls({ appSource, htmlSource = "",
"/help.md",
"/health/live",
"/v1",
"/v1/diagnostics/gate",
"/v1/agent/chat",
"/v1/m3/io",
"/v1/m3/status",
+26 -1
View File
@@ -2451,7 +2451,32 @@ function assertDevCloudWorkbenchGateRoute(gate, label) {
assert.equal(gate.workspaceHidden, true, `${label}.workspaceHidden`);
assert.equal(gate.gateHidden, false, `${label}.gateHidden`);
assert.equal(gate.helpHidden, true, `${label}.helpHidden`);
assert.equal(gate.diagnosticsTermsPresent, true, `${label}.diagnosticsTermsPresent`);
assert.equal(gate.heading, "内部复核", `${label}.heading`);
assert.equal(gate.tableCount, 1, `${label}.tableCount`);
assert.equal(gate.mainTableCount, 1, `${label}.mainTableCount`);
assert.equal(gate.requiredHeadersPresent, true, `${label}.requiredHeadersPresent`);
assert.equal(gate.rowsPresent, true, `${label}.rowsPresent`);
assert.equal(gate.statusLabelsChinese, true, `${label}.statusLabelsChinese`);
assert.equal(gate.liveBackendOrBlockerVisible, true, `${label}.liveBackendOrBlockerVisible`);
assert.equal(gate.legacyLayoutAbsent, true, `${label}.legacyLayoutAbsent`);
assert.equal(gate.userOperationsAbsent, true, `${label}.userOperationsAbsent`);
assert.equal(gate.blockedRowsGreen, false, `${label}.blockedRowsGreen`);
assert.equal(gate.oldDashboardTermsAbsent, true, `${label}.oldDashboardTermsAbsent`);
assert.equal(gate.backendRouteRequested, true, `${label}.backendRouteRequested`);
assertStringArray(gate.tableHeaders, `${label}.tableHeaders`, { minLength: 8 });
for (const column of ["类别", "检查项", "状态", "归因对象", "关键细节", "证据/trace", "更新时间", "下一步"]) {
assert.ok(gate.tableHeaders.includes(column), `${label}.tableHeaders missing ${column}`);
}
assertStringArray(gate.tableStatuses, `${label}.tableStatuses`, { minLength: 1 });
for (const status of gate.tableStatuses) {
assert.ok(["通过", "阻塞", "失败", "待验证", "信息"].includes(status), `${label}.tableStatuses unexpected ${status}`);
}
assertArray(gate.tableRows, `${label}.tableRows`);
assertObject(gate.controls, `${label}.controls`);
assert.equal(gate.controls.statusFilter, true, `${label}.controls.statusFilter`);
assert.equal(gate.controls.search, true, `${label}.controls.search`);
assert.equal(gate.controls.refresh, true, `${label}.controls.refresh`);
assert.ok(gate.controls.controlCount <= 4, `${label}.controls.controlCount`);
if (Object.hasOwn(gate, "codeAgentPostGuard")) {
assertObject(gate.codeAgentPostGuard, `${label}.codeAgentPostGuard`);
assert.equal(gate.codeAgentPostGuard.installed, true, `${label}.codeAgentPostGuard.installed`);