|
|
|
@@ -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));
|
|
|
|
|