Files
pikasTech-HWLAB/web/hwlab-cloud-web/scripts/check.mjs
T

1428 lines
81 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { loadMvpGateSummary } from "../../../internal/mvp-gate/summary.mjs";
import {
runDevCloudWorkbenchLocalAgentFixtureSmoke,
runDevCloudWorkbenchLayoutSmoke,
runDevCloudWorkbenchMobileSmoke,
runDevCloudWorkbenchAuthFixtureSmoke,
runDevCloudWorkbenchQuickPromptsFixtureSmoke,
runDevCloudWorkbenchStaticSmoke
} from "../../../scripts/src/dev-cloud-workbench-smoke-lib.mjs";
import { gateSummary } from "../gate-summary.mjs";
import { runtime } from "../runtime.mjs";
import { runM3ControlPanelGuard } from "./m3-control-panel-guard.mjs";
import { runCloudWebM3ReadonlyContract } from "./m3-readonly-contract.mjs";
import { runWorkbenchHardwarePanelContract } from "./workbench-hardware-panel-contract.mjs";
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = path.resolve(rootDir, "../..");
const requiredFiles = [
"index.html",
"styles.css",
"auth.mjs",
"app.mjs",
"code-agent-facts.mjs",
"code-agent-status.mjs",
"code-agent-m3-evidence.mjs",
"live-status.mjs",
"gate-summary.mjs",
"runtime.mjs",
"wiring-status.mjs",
"workbench-hardware-panel.mjs",
"help.md",
"third_party/marked/marked.esm.js",
"third_party/marked/LICENSE"
];
for (const file of requiredFiles) {
const filePath = path.resolve(rootDir, file);
if (!fs.existsSync(filePath)) {
throw new Error(`missing web asset: ${file}`);
}
}
const html = fs.readFileSync(path.resolve(rootDir, "index.html"), "utf8");
const styles = fs.readFileSync(path.resolve(rootDir, "styles.css"), "utf8");
const auth = fs.readFileSync(path.resolve(rootDir, "auth.mjs"), "utf8");
const app = fs.readFileSync(path.resolve(rootDir, "app.mjs"), "utf8");
const codeAgentFacts = fs.readFileSync(path.resolve(rootDir, "code-agent-facts.mjs"), "utf8");
const codeAgentStatus = fs.readFileSync(path.resolve(rootDir, "code-agent-status.mjs"), "utf8");
const liveStatus = fs.readFileSync(path.resolve(rootDir, "live-status.mjs"), "utf8");
const wiringStatus = fs.readFileSync(path.resolve(rootDir, "wiring-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");
const markedLicense = fs.readFileSync(path.resolve(rootDir, "third_party/marked/LICENSE"), "utf8");
const artifactPublisher = fs.readFileSync(path.resolve(repoRoot, "scripts/artifact-publish.mjs"), "utf8");
const cloudWebRoutes = fs.readFileSync(path.resolve(repoRoot, "internal/dev-entrypoint/cloud-web-routes.mjs"), "utf8");
const cloudWebProxy = fs.readFileSync(path.resolve(repoRoot, "internal/dev-entrypoint/cloud-web-proxy.mjs"), "utf8");
const cloudApiServer = fs.readFileSync(path.resolve(repoRoot, "internal/cloud/server.mjs"), "utf8");
const cloudJsonRpc = fs.readFileSync(path.resolve(repoRoot, "internal/cloud/json-rpc.mjs"), "utf8");
const gatewayDemoRegistry = fs.readFileSync(path.resolve(repoRoot, "internal/cloud/gateway-demo-registry.mjs"), "utf8");
const gatewayMain = fs.readFileSync(path.resolve(repoRoot, "cmd/hwlab-gateway/main.mjs"), "utf8");
const gatewayDemoSmoke = fs.readFileSync(path.resolve(repoRoot, "scripts/gateway-outbound-demo-smoke.mjs"), "utf8");
const gatewayNonblockingProbe = fs.readFileSync(path.resolve(repoRoot, "scripts/gateway-outbound-nonblocking-probe.mjs"), "utf8");
const simHttp = fs.readFileSync(path.resolve(repoRoot, "internal/sim/http.mjs"), "utf8");
const codexStdioSession = fs.readFileSync(path.resolve(repoRoot, "internal/cloud/codex-stdio-session.mjs"), "utf8");
const gatewayShellTool = fs.readFileSync(path.resolve(repoRoot, "tools/hwlab-gateway-shell.mjs"), "utf8");
const frontendSource = `${html}\n${auth}\n${app}\n${artifactPublisher}`;
const requiredTrustedRecordTerms = Object.freeze([
"Code Agent 对话记录",
"接线/operation",
"audit 记录",
"evidence 证据",
"trace/messageId",
"audit.event.query + SOURCE 回退",
"evidence.record.query + SOURCE 回退",
"conversationId、traceId 和 messageId",
"provider/model/trace/conversation",
"失败原因=",
"runtime.durable=false;只读 audit/evidence 不能标为 durable ready 或 DEV-LIVE。",
"只读 audit 缺少 M3 patch-panel live report / operation / trace / evidence 绑定",
"只读 evidence 缺少 M3 patch-panel live report / operation / trace / audit 绑定",
"当前显示 SOURCE 回退,不能冒充 DEV-LIVE"
]);
const workbenchSmoke = runDevCloudWorkbenchStaticSmoke();
const helpSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "help-md-contract");
const outerScrollSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "outer-scroll-contract");
const mobileLayoutSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "mobile-workbench-layout-contract");
const resourceExplorerRemovalSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "feedback-352-resource-explorer-removed");
const conversationUxSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "code-agent-conversation-ux-states");
const codeAgentTimeoutSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "code-agent-long-timeout-contract");
const codeAgentQuickPromptsSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "code-agent-quick-prompts-contract");
const mobileWorkbenchSmoke = await runDevCloudWorkbenchMobileSmoke();
const mobileOuterScrollSmokeCheck = mobileWorkbenchSmoke.checks.find((check) => check.id === "mobile-outer-scroll-lock");
const mobileHelpSmokeCheck = mobileWorkbenchSmoke.checks.find((check) => check.id === "mobile-help-markdown-route");
const localAgentFixtureSmoke = await runDevCloudWorkbenchLocalAgentFixtureSmoke();
const localAgentNo4500Check = localAgentFixtureSmoke.checks.find((check) => check.id === "local-agent-fixture-no-4500ms-permanent-failure");
const localAgentMobilePendingCheck = localAgentFixtureSmoke.checks.find((check) => check.id === "local-agent-fixture-mobile-pending-layout");
const quickPromptsFixtureSmoke = await runDevCloudWorkbenchQuickPromptsFixtureSmoke();
const quickPromptsFillCheck = quickPromptsFixtureSmoke.checks.find((check) => check.id === "quick-prompts-fill-input");
const quickPromptsNoAutosendCheck = quickPromptsFixtureSmoke.checks.find((check) => check.id === "quick-prompts-write-no-autosend");
const quickPromptsCopyCheck = quickPromptsFixtureSmoke.checks.find((check) => check.id === "quick-prompts-copy-boundary");
const quickPromptsLayoutCheck = quickPromptsFixtureSmoke.checks.find((check) => check.id === "quick-prompts-mobile-layout");
const authFixtureSmoke = await runDevCloudWorkbenchAuthFixtureSmoke();
const authSuccessDesktopCheck = authFixtureSmoke.checks.find((check) => check.id === "auth-login-success-desktop");
const authFailureDesktopCheck = authFixtureSmoke.checks.find((check) => check.id === "auth-login-failure-desktop");
const authRefreshDesktopCheck = authFixtureSmoke.checks.find((check) => check.id === "auth-refresh-session-desktop");
const authLogoutDesktopCheck = authFixtureSmoke.checks.find((check) => check.id === "auth-logout-expiry-desktop");
const authSuccessMobileCheck = authFixtureSmoke.checks.find((check) => check.id === "auth-login-success-mobile");
const authFailureMobileCheck = authFixtureSmoke.checks.find((check) => check.id === "auth-login-failure-mobile");
const authRefreshMobileCheck = authFixtureSmoke.checks.find((check) => check.id === "auth-refresh-session-mobile");
const authLogoutMobileCheck = authFixtureSmoke.checks.find((check) => check.id === "auth-logout-expiry-mobile");
const layoutSmokeReportPath = path.resolve(repoRoot, "tmp/dev-cloud-workbench-layout-web-check.json");
const layoutSmoke = await runDevCloudWorkbenchLayoutSmoke({ reportPath: layoutSmokeReportPath });
fs.mkdirSync(path.dirname(layoutSmokeReportPath), { recursive: true });
layoutSmoke.artifacts = { ...(layoutSmoke.artifacts ?? {}), reportPath: layoutSmokeReportPath };
fs.writeFileSync(layoutSmokeReportPath, `${JSON.stringify(layoutSmoke, null, 2)}\n`, "utf8");
if (process.env.HWLAB_WEB_CHECK_LEGACY !== "1") {
runMinimalWebCheck();
process.exit(0);
}
assert.notEqual(
mobileWorkbenchSmoke.status,
"skip",
[
mobileWorkbenchSmoke.summary,
"Install repository dependencies and run `node scripts/dev-cloud-workbench-smoke.mjs --mobile` to generate the required mobile outer-scroll evidence."
].filter(Boolean).join(" ")
);
assert.notEqual(
layoutSmoke.status,
"skip",
[
layoutSmoke.summary,
"Install repository dependencies and run `node scripts/dev-cloud-workbench-layout-smoke.mjs --static` to generate the required layout evidence."
].filter(Boolean).join(" ")
);
function runMinimalWebCheck() {
assert.ok(isBrowserSmokePassOrEnvironmentSkip(mobileWorkbenchSmoke), browserSmokeSkipSummary(mobileWorkbenchSmoke, "mobile browser smoke"));
assert.ok(isBrowserSmokePassOrEnvironmentSkip(layoutSmoke), browserSmokeSkipSummary(layoutSmoke, "layout browser smoke"));
assert.equal(workbenchSmoke.status, "pass", JSON.stringify(workbenchSmoke.blockers, null, 2));
assert.equal(workbenchSmoke.mode, "source");
assert.equal(workbenchSmoke.evidenceLevel, "SOURCE");
assert.equal(workbenchSmoke.devLive, false);
assert.equal(helpSmokeCheck?.status, "pass", "help Markdown route must remain available");
assert.equal(outerScrollSmokeCheck?.status, "pass", "desktop outer-scroll contract must remain locked");
assert.equal(mobileLayoutSmokeCheck?.status, "pass", "mobile workbench layout contract must remain present");
assert.equal(resourceExplorerRemovalSmokeCheck?.status, "pass", "resource explorer removal contract must remain present");
assert.equal(conversationUxSmokeCheck?.status, "pass", "conversation UX smoke must remain distinct");
assert.equal(codeAgentTimeoutSmokeCheck?.status, "pass", "Code Agent timeout contract must remain present");
assert.equal(codeAgentQuickPromptsSmokeCheck?.status, "pass", "quick prompt contract must remain present");
assertBrowserSmokeChecks(mobileWorkbenchSmoke, [
[mobileOuterScrollSmokeCheck, "mobile outer-scroll lock must work"],
[mobileHelpSmokeCheck, "mobile help route must work"]
]);
assertBrowserSmokeChecks(localAgentFixtureSmoke, [
[localAgentNo4500Check, "local fixture must prove no 4500ms permanent failure"],
[localAgentMobilePendingCheck, "local fixture must preserve mobile pending layout"]
]);
assertBrowserSmokeChecks(quickPromptsFixtureSmoke, [
[quickPromptsFillCheck, "quick prompts must fill input"],
[quickPromptsNoAutosendCheck, "write quick prompts must not auto-send"],
[quickPromptsCopyCheck, "quick prompt copy must stay safe"],
[quickPromptsLayoutCheck, "quick prompt strip must stay within layout contract"]
]);
assert.ok(isBrowserSmokePassOrEnvironmentSkip(layoutSmoke), JSON.stringify(compactLayoutSmokeFailures(layoutSmoke), null, 2));
assert.equal(layoutSmoke.sourceMode, "source-static");
assert.equal(layoutSmoke.evidenceLevel, "SOURCE");
assert.equal(layoutSmoke.devLive, false);
}
function assertBrowserSmokeChecks(report, checks = []) {
assert.ok(isBrowserSmokePassOrEnvironmentSkip(report), browserSmokeSkipSummary(report, report?.mode ?? "browser smoke"));
assert.equal(report.evidenceLevel, "SOURCE");
assert.equal(report.devLive, false);
if (report.status === "skip" || isBrowserSmokeEnvironmentBlocked(report)) return;
for (const [check, message] of checks) {
assert.equal(check?.status, "pass", message);
}
}
function isBrowserSmokePassOrEnvironmentSkip(report) {
if (report?.status === "pass") return true;
if (isBrowserSmokeEnvironmentBlocked(report)) return true;
if (report?.status !== "skip") return false;
const blockers = Array.isArray(report.blockers) ? report.blockers : [];
const checks = Array.isArray(report.checks) ? report.checks : [];
return blockers.every((blocker) => blocker?.type === "environment_blocker") &&
checks.every((check) => check?.status === "skip") &&
/Playwright|browser/i.test(`${report.summary ?? ""} ${JSON.stringify(blockers)}`);
}
function isBrowserSmokeEnvironmentBlocked(report) {
if (report?.status !== "blocked") return false;
return /Executable doesn't exist|Please run the following command to download new browsers|does not support chromium|Playwright is unavailable|browser could not run|browserType\.launch|Host system is missing dependencies/i.test(browserSmokeEnvironmentText(report));
}
function browserSmokeEnvironmentText(report) {
return `${report?.summary ?? ""} ${JSON.stringify(report?.blockers ?? [])} ${JSON.stringify(report?.checks ?? [])}`;
}
function browserSmokeSkipSummary(report, label) {
return JSON.stringify({
label,
status: report?.status ?? null,
summary: report?.summary ?? null,
blockers: report?.blockers ?? [],
checks: (report?.checks ?? []).map((check) => ({ id: check.id, status: check.status, summary: check.summary }))
}, null, 2);
}
assert.equal(workbenchSmoke.status, "pass", JSON.stringify(workbenchSmoke.blockers, null, 2));
assert.equal(workbenchSmoke.mode, "source");
assert.equal(workbenchSmoke.evidenceLevel, "SOURCE");
assert.equal(workbenchSmoke.devLive, false);
assert.equal(helpSmokeCheck?.status, "pass", "PR #114 help Markdown route must be present and ready");
assert.equal(outerScrollSmokeCheck?.status, "pass", "desktop workbench shell must lock outer page scrolling");
assert.equal(mobileLayoutSmokeCheck?.status, "pass", "mobile 390x844 workbench layout contract must be present");
assert.equal(resourceExplorerRemovalSmokeCheck?.status, "pass", "#352 resource explorer removal source contract must be present");
assert.equal(conversationUxSmokeCheck?.status, "pass", "Code Agent conversation UX states must remain distinct");
assert.equal(codeAgentTimeoutSmokeCheck?.status, "pass", "Code Agent chat must not reuse the 4500ms light API timeout");
assert.equal(codeAgentQuickPromptsSmokeCheck?.status, "pass", "Code Agent quick prompts must remain fill-only with explicit send semantics for writes");
assert.equal(workbenchSmoke.help.status, "pass", "smoke report must not leave #114 help contract pending");
assert.notEqual(mobileWorkbenchSmoke.status, "blocked", JSON.stringify(mobileWorkbenchSmoke.blockers, null, 2));
assert.equal(mobileWorkbenchSmoke.evidenceLevel, "SOURCE");
assert.equal(mobileWorkbenchSmoke.devLive, false);
assert.ok(
mobileOuterScrollSmokeCheck,
"mobile smoke must include the mobile-outer-scroll-lock check; run `node scripts/dev-cloud-workbench-smoke.mjs --mobile` with Playwright installed."
);
assert.equal(mobileOuterScrollSmokeCheck.status, "pass", "mobile smoke must prove outer page scroll remains locked");
assert.ok(
mobileHelpSmokeCheck,
"mobile smoke must include the mobile-help-markdown-route check; run `node scripts/dev-cloud-workbench-smoke.mjs --mobile` with Playwright installed."
);
assert.equal(mobileHelpSmokeCheck.status, "pass", "mobile help route must remain non-default Markdown content");
assert.equal(localAgentFixtureSmoke.status, "pass", JSON.stringify(localAgentFixtureSmoke.blockers, null, 2));
assert.equal(localAgentFixtureSmoke.evidenceLevel, "SOURCE");
assert.equal(localAgentFixtureSmoke.devLive, false);
assert.equal(localAgentNo4500Check?.status, "pass", "local Code Agent fixture must prove the 4500ms browser timeout path is not permanent");
assert.equal(localAgentMobilePendingCheck?.status, "pass", "local Code Agent fixture must keep mobile pending state visible with trace/session context");
assert.equal(quickPromptsFixtureSmoke.status, "pass", JSON.stringify(quickPromptsFixtureSmoke.blockers, null, 2));
assert.equal(quickPromptsFixtureSmoke.evidenceLevel, "SOURCE");
assert.equal(quickPromptsFixtureSmoke.devLive, false);
assert.equal(quickPromptsFillCheck?.status, "pass", "quick prompt buttons must fill and focus the Code Agent input");
assert.equal(quickPromptsNoAutosendCheck?.status, "pass", "HWLAB API write quick prompts must not auto-send");
assert.equal(quickPromptsCopyCheck?.status, "pass", "quick prompt labels must use Codex natural-language routing copy");
assert.equal(quickPromptsLayoutCheck?.status, "pass", "quick prompt strip must stay contained on desktop and 390x844 mobile");
assert.equal(authFixtureSmoke.status, "pass", JSON.stringify(authFixtureSmoke.blockers, null, 2));
assert.equal(authFixtureSmoke.evidenceLevel, "SOURCE");
assert.equal(authFixtureSmoke.devLive, false);
assert.equal(authSuccessDesktopCheck?.status, "pass", "desktop login must accept admin/hwlab2026 and enter the Cloud Workbench");
assert.equal(authFailureDesktopCheck?.status, "pass", "desktop login failure must show a bounded Chinese error and keep workbench hidden");
assert.equal(authRefreshDesktopCheck?.status, "pass", "desktop refresh must preserve authenticated session state");
assert.equal(authLogoutDesktopCheck?.status, "pass", "desktop logout and expired session must return to login");
assert.equal(authSuccessMobileCheck?.status, "pass", "390x844 mobile login must enter workbench without outer scroll or hidden controls");
assert.equal(authFailureMobileCheck?.status, "pass", "390x844 mobile login failure must keep form visible and error contained");
assert.equal(authRefreshMobileCheck?.status, "pass", "390x844 mobile refresh must preserve authenticated session state");
assert.equal(authLogoutMobileCheck?.status, "pass", "390x844 mobile logout and expired session must return to login");
assert.equal(
layoutSmoke.status,
"pass",
JSON.stringify(compactLayoutSmokeFailures(layoutSmoke), null, 2)
);
assert.equal(layoutSmoke.sourceMode, "source-static");
assert.equal(layoutSmoke.evidenceLevel, "SOURCE");
assert.equal(layoutSmoke.devLive, false);
assert.equal(layoutSmoke.safety.layoutOnly, true);
assert.equal(layoutSmoke.safety.codeAgentPostSent, false);
assert.equal(layoutSmoke.safety.hardwareWriteApis, false);
assert.equal(layoutSmoke.failures.length, 0);
assert.equal(layoutSmoke.viewports.map((viewport) => `${viewport.width}x${viewport.height}`).join(","), "1366x768,1024x768,390x844");
assert.equal(
layoutSmoke.checks.find((check) => check.id === "layout-issue-287-future-hardware-status-tabs")?.status,
"pass",
"#287 hardware status tabs must remain covered by layout smoke"
);
assert.equal(
layoutSmoke.checks.find((check) => check.id === "layout-hardware-tabs-center-hit")?.status,
"pass",
"hardware status tabs must be center-hit reachable on desktop and 390x844 mobile"
);
assert.equal(
layoutSmoke.checks.find((check) => check.id === "layout-wiring-readable-two-column")?.status,
"pass",
"#276 wiring table must stay readable, non-overlapping, and horizontally contained"
);
assert.equal(
layoutSmoke.checks.find((check) => check.id === "layout-feedback-352-resource-explorer-removed")?.status,
"pass",
"#352 resource explorer removal must remain covered by desktop and 390x844 layout smoke"
);
assert.equal(
layoutSmoke.checks.find((check) => check.id === "layout-feedback-437-live-build-overlay")?.status,
"pass",
"#437 live build details overlay must not move the topbar, workspace, or conversation geometry"
);
assert.equal(
layoutSmoke.checks.find((check) => check.id === "layout-left-sidebar-collapse")?.status,
"pass",
"#278 left activity rail collapse/expand must remain covered by desktop and 390x844 layout smoke"
);
assert.equal(
layoutSmoke.checks.find((check) => check.id === "layout-issue-288-future-single-table-gate")?.status,
"skip",
"#288 single-table gate is future coverage; current /gate route remains covered by layout smoke"
);
for (const layoutCheckId of [
"layout-desktop-default",
"layout-narrow-desktop-default",
"layout-mobile-default",
"layout-feedback-352-resource-explorer-removed",
"layout-feedback-437-live-build-overlay",
"layout-left-sidebar-collapse",
"layout-right-sidebar-resize-desktop",
"layout-right-sidebar-resize-narrow-desktop",
"layout-right-sidebar-resize-mobile",
"layout-gate-desktop",
"layout-gate-narrow-desktop",
"layout-gate-mobile"
]) {
assert.equal(layoutSmoke.checks.find((check) => check.id === layoutCheckId)?.status, "pass", `${layoutCheckId} must pass in web:check layout gate`);
}
if (mobileWorkbenchSmoke.status === "pass") {
assert.equal(mobileWorkbenchSmoke.viewport.width, 390);
assert.equal(mobileWorkbenchSmoke.viewport.height, 844);
assert.equal(mobileWorkbenchSmoke.checks.every((check) => check.status === "pass"), true);
}
assert.match(html, /HWLAB 云工作台/);
for (const chineseWorkbenchText of [
"硬件资源",
"Agent 对话",
"可信记录",
"内部复核",
"使用说明"
]) {
assert.match(`${html}\n${app}`, new RegExp(chineseWorkbenchText));
}
for (const legacyEnglishTitle of [
/Agent Conversation \/ Trace Workspace/,
/Resource Tree/,
/Current Blockers/,
/Control Drafts/,
/Trusted Records/,
/Same-Origin Read Probe/,
/Allowed Read RPC/,
/Open Blockers/,
/Light Validation/,
/DEV Health Contract/,
/Secondary Entry/,
/Runtime Surface/
]) {
assert.doesNotMatch(`${html}\n${app}`, legacyEnglishTitle);
}
assert.match(html, /M3 实况状态/);
assert.match(html, /硬件状态工作区/);
assert.match(html, /id="hardware-source-status"/);
assert.match(app, /function renderHardwareStatus/);
assert.match(app, /wiringPresentationFromSummary/);
assert.match(wiringStatus, /function wiringPresentationFromSummary/);
assert.match(wiringStatus, /function trustedM3StatusEvidenceFrom/);
assert.match(functionBody(wiringStatus, "trustedM3StatusEvidenceFrom"), /contractVersion !== "m3-status-v1"/);
assert.match(functionBody(wiringStatus, "trustedM3StatusEvidenceFrom"), /sourceKind !== "DEV-LIVE"/);
assert.match(functionBody(wiringStatus, "trustedM3StatusEvidenceFrom"), /trust\.durableStatus !== "green"/);
assert.match(functionBody(wiringStatus, "trustedM3StatusEvidenceFrom"), /trust\.blocker != null/);
assert.match(functionBody(wiringStatus, "trustedM3StatusEvidenceFrom"), /LIVE_M3_ID_FIELDS\.every/);
assert.match(functionBody(wiringStatus, "wiringPresentationFromSummary"), /接线实况 \/ 持久化受阻/);
assert.match(functionBody(wiringStatus, "wiringPresentationFromSummary"), /durableStatus !== "green"/);
assert.match(functionBody(wiringStatus, "wiringPresentationFromSummary"), /blockedTrust != null/);
assert.match(functionBody(wiringStatus, "wiringPresentationFromSummary"), /\/v1\/m3\/status 聚合 \/ 实况可信记录/);
assert.match(app, /function initHardwareTabs/);
assert.match(app, /function hardwareKvGroup/);
assert.match(app, /function sourceFallbackM3Status/);
assert.match(app, /function trustedM3Link/);
assert.match(app, /function hasTrustedM3LiveEvidence/);
assert.match(app, /function hasTrustedPatchPanelLiveReport/);
assert.match(app, /function linkStatusLabel/);
assert.match(app, /function normalizePort/);
assert.match(app, /LIVE_M3_ID_FIELDS/);
assert.match(app, /patchPanelLiveReport/);
assert.match(functionBody(app, "sourceFallbackM3Status"), /unverified_cloud_api_m3_status/);
assert.match(functionBody(app, "sourceFallbackM3Status"), /SOURCE 拓扑不能冒充实况硬件状态/);
assert.doesNotMatch(functionBody(app, "renderHardwareStatus"), /hardwareRow\(\s*\{/u);
assert.match(functionBody(app, "renderHardwareStatus"), /Key\/Value groups/u);
assert.doesNotMatch(app, /sourceKind:\s*counts\s*\?\s*"DEV-LIVE"/);
assert.doesNotMatch(app, /counts\s*&&\s*(?:liveLink|sourceLink|m3Link)[\s\S]{0,80}\?\s*"DEV-LIVE"/);
assert.match(app, /function renderRecords/);
assert.match(app, /function codeAgentRecordCards/);
assert.match(app, /function operationRecordCards/);
assert.match(app, /function auditRecordCards/);
assert.match(app, /function evidenceRecordCards/);
assert.match(app, /function auditCard/);
assert.match(app, /function evidenceCard/);
assert.match(app, /function liveRecordTone/);
assert.match(app, /function liveRecordDurabilityBlocker/);
assert.match(app, /function isDurableRuntimeReady/);
assert.match(app, /runtimeSummary\?\.durable !== true/);
assert.match(app, /summary\.liveRuntimeEvidence !== true/);
assert.match(app, /auditCard\(event, liveRecordTone\(event, live\), liveRecordDurabilityBlocker\(live\)\)/);
assert.match(app, /evidenceCard\(record, liveRecordTone\(record, live\), liveRecordDurabilityBlocker\(live\)\)/);
assert.match(app, /function recordGroup/);
assert.match(app, /function recordField/);
assert.match(app, /function safeFailureReason/);
assert.match(app, /function sourceKindLabel/);
assert.match(app, /function liveFailureCard/);
assert.match(app, /state\.liveSurface/);
assert.match(app, /renderRecords\(state\.liveSurface\)/);
assert.match(app, /traceId: error\.traceId \|\| traceId/);
assert.match(app, /error\.traceId = (?:traceId|response\.data\?\.traceId \|\| traceId)/);
assert.match(app, /DEFAULT_API_TIMEOUT_MS\s*=\s*4500/);
assert.match(app, /DEFAULT_LIVE_SURFACE_TIMEOUT_MS\s*=\s*12000/);
assert.match(app, /LIVE_SURFACE_TIMEOUT_MS\s*=\s*resolveTimeoutMs\("liveSurfaceTimeoutMs"/);
assert.match(app, /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*600000/);
assert.match(app, /DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS\s*=\s*60000/);
assert.match(app, /CODE_AGENT_TIMEOUT_STORAGE_KEY/);
assert.match(functionBody(app, "clampTimeoutMs"), /value === null \|\| value === undefined/);
assert.match(functionBody(app, "clampTimeoutMs"), /typeof value === "string" && value\.trim\(\) === ""/);
assert.match(functionBody(app, "sendAgentMessage"), /timeoutMs:\s*CODE_AGENT_SUBMIT_TIMEOUT_MS/);
assert.match(functionBody(app, "sendAgentMessage"), /shortConnection:\s*true/);
assert.match(functionBody(app, "waitForAgentMessageResult"), /idleMs\s*>=\s*CODE_AGENT_TIMEOUT_MS/);
assert.match(html, /id="code-agent-timeout"/);
assert.match(styles, /\.agent-timeout-control\s*{/);
assert.match(app, /activityRef:\s*\(\)\s*=>\s*state\.currentRequest/);
assert.match(functionBody(app, "fetchJson"), /timeoutMs\s*=\s*API_TIMEOUT_MS/);
assert.match(functionBody(app, "fetchJson"), /无新事件/);
assert.match(functionBody(app, "liveSurfaceFetch"), /timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/);
assert.match(functionBody(app, "liveSurfaceRpc"), /timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/);
assert.match(functionBody(app, "loadLiveSurface"), /liveSurfaceFetch\("\/v1\/m3\/status"\)/);
assert.match(functionBody(app, "loadLiveSurface"), /liveSurfaceRpc\("system\.health"\)/);
assert.match(functionBody(app, "callRpc"), /timeoutMs:\s*options\?\.timeoutMs \?\? API_TIMEOUT_MS/);
for (const hardwareTerm of [
"Gateway 在线",
"BOX 在线",
"链路状态",
"接线盘连接",
"runtime durable",
"DISABLED 后续能力",
"SOURCE",
"DEV-LIVE",
"unverified_cloud_api_m3_status",
"/v1/m3/status",
"仍在等待完整可信记录",
"页面不直连模拟器也不伪造硬件状态"
]) {
assert.match(app, new RegExp(escapeRegExp(hardwareTerm)), `missing hardware term: ${hardwareTerm}`);
}
for (const hardwareHtmlTerm of ["Gateway-SIMU", "BOX-SIMU-1", "BOX-SIMU-2", "Patch Panel"]) {
assert.match(html, new RegExp(escapeRegExp(hardwareHtmlTerm)), `missing hardware HTML term: ${hardwareHtmlTerm}`);
}
assert.match(app, /function hardwareEvidenceLabel/);
assert.match(styles, /\.record-group\s*{/);
assert.match(styles, /\.record-group-title\s*{/);
assert.match(styles, /\.hardware-section\s*{/);
assert.match(styles, /\.hardware-status-tabs\s*{/);
assert.match(styles, /\.kv-row\s*{/);
assert.match(styles, /\.hardware-row\s*{/);
assert.match(app, /new Set\(\["\/gate", "\/diagnostics\/gate"\]\)/);
assert.match(html, /href="\/styles\.css"/);
assert.match(html, /src="\/app\.mjs"/);
for (const workbenchElement of [
"activity-rail",
"left-sidebar-toggle",
"conversation-list",
"agent-chat-status",
"code-agent-summary",
"code-agent-summary-label",
"code-agent-summary-capability",
"code-agent-summary-trace",
"right-sidebar",
"right-sidebar-resize",
"command-form",
"command-send",
"hardware-list",
"control-list",
"wiring-body",
"records-list",
"gate-review-body"
]) {
assert.match(html, new RegExp(workbenchElement));
}
for (const removedSelectorTerm of [
"id=\"resource-explorer\"",
"id=\"explorer-toggle\"",
"id=\"explorer-resize\"",
"id=\"resource-tree\"",
"explorer-capabilities",
"capability-count",
"tree-count",
"quick-actions",
"data-focus-command"
]) {
assert.doesNotMatch(html, new RegExp(escapeRegExp(removedSelectorTerm)), `removed resource explorer selector returned: ${removedSelectorTerm}`);
}
for (const removedUiText of [
"资源浏览与硬件资源树",
"资源浏览",
"硬件资源树",
"只读工作区",
"常用能力",
"收起资源树",
"展开资源树",
"拖拽调整左侧资源树宽度",
"资源查看",
"查看接线",
"查看状态",
"编写任务"
]) {
assert.doesNotMatch(`${html}\n${app}\n${styles}`, new RegExp(escapeRegExp(removedUiText)), `removed resource explorer copy returned: ${removedUiText}`);
}
assert.match(html, /id="hardware-sidebar"/);
assert.match(html, /id="activity-rail"/);
assert.match(html, /id="left-sidebar-toggle"/);
assert.match(html, /aria-label="折叠左侧导航"/);
assert.match(html, /title="折叠左侧导航"/);
assert.match(html, /aria-controls="activity-rail"/);
assert.match(html, /aria-expanded="true"/);
assert.match(html, /id="right-sidebar-resize"/);
assert.match(html, /aria-label="拖拽调整右侧硬件状态栏宽度"/);
assert.match(html, /aria-valuemin="560"/);
assert.match(html, /aria-valuemax="740"/);
assert.match(html, /aria-valuenow="728"/);
assert.doesNotMatch(app, /setExplorerCollapsed|initExplorerResize|clampExplorerWidth|syncMobileExplorer|collapseExplorerAfterMobileAction/);
assert.match(app, /LAYOUT_STORAGE_KEY\s*=\s*"hwlab\.workbench\.layout\.v1"/);
assert.match(app, /leftSidebarCollapsed:\s*payload\.leftSidebarCollapsed === true/);
assert.match(app, /leftSidebarCollapsed:\s*state\.layout\.leftSidebarCollapsed/);
assert.match(app, /function initLeftSidebarToggle/);
assert.match(app, /function setLeftSidebarCollapsed/);
assert.match(app, /is-left-sidebar-collapsed/);
assert.match(app, /展开左侧导航/);
assert.match(app, /折叠左侧导航/);
assert.match(app, /function initRightSidebarResize/);
assert.match(app, /function clampRightSidebarWidth/);
assert.match(app, /function rightSidebarResizeBounds/);
assert.match(app, /setPointerCapture/);
assert.match(app, /localStorage\?\.setItem\(\s*LAYOUT_STORAGE_KEY/);
assert.match(app, /rightSidebarWidth:\s*state\.layout\.rightSidebarWidth/);
assert.match(app, /ArrowLeft/);
assert.match(app, /ArrowRight/);
assert.match(app, /event\.key === "Home"/);
assert.match(app, /event\.key === "End"/);
assert.doesNotMatch(styles, /--explorer-width|--explorer-min-width|--explorer-max-width|\.explorer|\.explorer-resize|\.quick-actions|\.resource-tree|\.tree-panel|\.tree-row|\.tree-label/);
assert.match(styles, /--right-sidebar-width:\s*728px/);
assert.match(styles, /--right-sidebar-min-width:\s*560px/);
assert.match(styles, /--right-sidebar-max-width:\s*740px/);
assert.match(styles, /--right-width:\s*clamp\(var\(--right-sidebar-min-width\),\s*var\(--right-sidebar-width\),\s*var\(--right-sidebar-max-width\)\)/);
assert.match(styles, /--right-width-expanded:\s*clamp\(var\(--right-sidebar-min-width\),\s*var\(--right-sidebar-width\),\s*var\(--right-sidebar-max-width\)\)/);
assert.match(styles, /--rail-collapsed-width:\s*44px/);
assert.match(styles, /\.workbench-shell\.is-left-sidebar-collapsed\s*{[^}]*--rail-width:\s*var\(--rail-collapsed-width\);/s);
assert.match(styles, /\.workbench-shell\.is-left-sidebar-collapsed \.activity-rail \.rail-button:not\(\.sidebar-toggle\),\s*\n\.workbench-shell\.is-left-sidebar-collapsed \.rail-spacer\s*{[^}]*display:\s*none;/s);
assert.match(styles, /\.sidebar-toggle\[aria-expanded="false"\]\s*{/);
assert.match(styles, /\.right-sidebar-resize\s*{[^}]*cursor:\s*col-resize;/s);
assert.match(styles, /@media \(max-width: 1240px\)[\s\S]*?\.right-sidebar-resize\s*{[\s\S]*?display:\s*none;/);
for (const viewId of ["workspace", "gate"]) {
assert.match(html, new RegExp(`data-view="${viewId}"`));
}
assert.match(html, /data-route="help"/);
assert.match(html, /data-view="help"/);
assert.match(html, /<section class="view help-view"[\s\S]*?hidden/);
assert.match(html, /id="help-content"/);
assert.match(html, /data-help-route-policy="non-default-internal-help"/);
assert.match(app, /from "\.\/third_party\/marked\/marked\.esm\.js"/);
assert.match(app, /marked\.parse\(/);
assert.doesNotMatch(app, /function\s+parseMarkdown|markedRegex|markdownRegex/);
assert.match(app, /fetch\("\/help\.md"/);
assert.match(app, /帮助内容加载失败/);
assert.match(functionBody(app, "routeFromLocation"), /replace\(\/\^#\\\/\?\//);
assert.match(functionBody(app, "routeFromLocation"), /helpPathnames\(\)\.has/);
assert.match(functionBody(app, "routeFromLocation"), /return "help"/);
assert.match(app, /return "workspace";/);
assert.match(styles, /\.help-view\s*{[^}]*overflow:\s*hidden;/s);
assert.match(styles, /\.help-panel\s*{[^}]*overflow:\s*hidden;/s);
assert.match(styles, /\.help-content\s*{[^}]*overflow:\s*auto;/s);
assert.match(styles, /body\s*>\s*\[data-app-shell\]\s*{[^}]*height:\s*100%;[^}]*overflow:\s*hidden;[^}]*overscroll-behavior:\s*contain;/s);
assert.match(styles, /\.workbench-shell\s*{[^}]*height:\s*100dvh;[^}]*max-height:\s*100dvh;[^}]*overflow:\s*hidden;[^}]*overscroll-behavior:\s*contain;/s);
assert.match(helpMarkdown, /^# 云工作台内部使用说明/m);
for (const helpTerm of [
"活动栏与默认工作区",
"Agent 对话与输入区",
"完整中文标签",
"内部复核",
"BOX-SIMU",
"Gateway-SIMU",
"hwlab-patch-panel",
"控制",
"接线",
"可信记录",
"诊断",
"Gate",
"SOURCE",
"LOCAL",
"DRY-RUN",
"DEV-LIVE",
"BLOCKED",
"res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1",
"same-origin `/v1`",
"`/v1/diagnostics/gate`",
"只读 `/json-rpc`",
"`/health/live`",
"`:16666`",
"`:16667`"
]) {
assert.match(helpMarkdown, new RegExp(escapeRegExp(helpTerm)), `missing help term: ${helpTerm}`);
}
const defaultFirstViewportText = visibleTextFromHtml(defaultFirstViewportHtml(html));
for (const backendTerm of ["Gate", "诊断", "验收", "BLOCKED", "SOURCE", "DRY-RUN", "DEV-LIVE", "M0-M5", "M3 基本", "trace/evidence", "执行轨迹", "OPENAI_API_KEY", "Secret", "secretRef", "hwlab-code-agent-provider", "m3-route-required", "waiting-for-dev-live"]) {
assert.equal(textHasForbiddenTerm(defaultFirstViewportText, backendTerm), false, `default workbench must not expose backend term ${backendTerm}`);
}
for (const fullNavLabel of ["工作台", "内部复核", "使用说明"]) {
assert.match(html, new RegExp(`>${escapeRegExp(fullNavLabel)}<`), `missing full nav label ${fullNavLabel}`);
}
for (const oneCharNavLabel of [">台<", ">证<", ">诊<", ">帮<"]) {
assert.doesNotMatch(html, new RegExp(escapeRegExp(oneCharNavLabel)), `legacy one-character nav label must be removed: ${oneCharNavLabel}`);
}
assert.doesNotMatch(html, /data-side-tab-jump="records"/u, "trusted records must not remain a left-side standalone shortcut");
const wiringPanel = html.match(/<section\b[^>]*\bid="panel-wiring"[\s\S]*?<\/section>/u)?.[0] ?? "";
assert.match(wiringPanel, /data-wiring-layout="two-column-long-table"/u, "wiring panel must use the #276 two-column long-table layout");
assert.equal((wiringPanel.match(/<th\b/gu) ?? []).length, 2, "wiring table must have exactly two device header columns");
assert.equal((wiringPanel.match(/<td\b/gu) ?? []).length, 2, "wiring table static IO row must have exactly two port cells");
for (const wiringTerm of ["res_boxsimu_1", "res_boxsimu_2", "DO1", "DI1", "hwlab-patch-panel", "状态", "证据来源", "轨迹/证据"]) {
assert.match(wiringPanel, new RegExp(escapeRegExp(wiringTerm)), `missing wiring term ${wiringTerm}`);
}
for (const legacyWideHeader of ["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "轨迹/证据"]) {
assert.doesNotMatch(wiringPanel, new RegExp(`<th>${escapeRegExp(legacyWideHeader)}<\\/th>`), `legacy wide wiring column must not return: ${legacyWideHeader}`);
}
const gateHtml = sectionSource(html, "gate");
assert.match(gateHtml, /<h2 id="gate-title">内部复核<\/h2>/);
assert.match(gateHtml, /id="gate-source-status"/);
assert.match(gateHtml, /id="gate-status-filter"/);
assert.match(gateHtml, /id="gate-search"/);
assert.match(gateHtml, /id="gate-refresh"/);
assert.match(gateHtml, /class="gate-table"/);
assert.equal((gateHtml.match(/class="gate-table"/gu) ?? []).length, 1, "/gate must contain one main table");
for (const gateHeader of ["类别", "检查项", "状态", "归因对象", "关键细节", "证据/trace", "更新时间", "下一步"]) {
assert.match(gateHtml, new RegExp(`<th>${escapeRegExp(gateHeader)}<\\/th>`), `missing gate table column ${gateHeader}`);
}
for (const gateStatus of ["通过", "阻塞", "失败", "待验证", "信息"]) {
assert.match(`${gateHtml}\n${app}`, new RegExp(escapeRegExp(gateStatus)), `missing gate status ${gateStatus}`);
}
for (const forbiddenGateMarker of ["workspace-panel", "gate-grid", "gate-split", "milestone-grid", "trace-list", "health-body", "diagnostics-list", "method-list", "unblock-list"]) {
assert.doesNotMatch(gateHtml, new RegExp(escapeRegExp(forbiddenGateMarker)), `/gate must not contain legacy dashboard marker ${forbiddenGateMarker}`);
}
assert.match(app, /fetchJson\("\/v1\/diagnostics\/gate"/);
assert.match(app, /function renderGateTable/);
assert.match(app, /function normalizeGateRow/);
assert.match(app, /function gateLiveBlockerRow/);
assert.match(app, /sourceKind:\s*"LIVE-BACKEND"/);
assert.match(app, /live 后端聚合不可用;页面不会回退到 SOURCE 静态拓扑。/);
assert.doesNotMatch(app, /function\s+(?:renderTrace|renderUnblockList|renderDiagnostics|renderGateDiagnostics|renderMethodList)\s*\(/);
assert.match(styles, /\.gate-table\s*{/);
assert.match(styles, /\.gate-controls\s*{/);
assert.doesNotMatch(styles, /\.gate-grid\s*{/);
assert.doesNotMatch(styles, /\.milestone-grid\s*{/);
assert.doesNotMatch(styles, /\.trace-list\s*{/);
for (const trustedRecordTerm of requiredTrustedRecordTerms) {
assert.match(app, new RegExp(escapeRegExp(trustedRecordTerm)), `missing trusted-record term: ${trustedRecordTerm}`);
}
assert.match(app, /function copyButton/);
assert.match(functionBody(app, "renderWiringList"), /M3_TRUSTED_ROUTE\.fromResourceId/);
assert.match(functionBody(app, "renderWiringList"), /M3_TRUSTED_ROUTE\.patchPanelServiceId/);
assert.doesNotMatch(functionBody(app, "renderWiringList"), /activeConnections\.map/u);
assert.doesNotMatch(functionBody(app, "renderWiringList"), /link\.fromResourceId/u);
assert.match(buildScript, /buildCloudWebDist/);
assert.match(distContractScript, /"help\.md"/);
assert.match(distContractScript, /"code-agent-m3-evidence\.mjs"/);
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/);
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, /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.match(liveStatus, /evidenceSummary/);
assert.match(liveStatus, /function evidenceSummaryFrom/);
assert.match(liveStatus, /api_degraded_without_attribution/);
assert.match(liveStatus, /API readiness is not pass and no concrete blocker was provided/);
assert.match(liveStatus, /trust\?\.evidenceId/);
assert.doesNotMatch(liveStatus, /API returned \$\{status\}/);
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, /placeholder="输入要发送给 Code Agent 的消息;不会直接触发硬件变更。"/);
assert.match(html, />发送<\/button>/);
assert.doesNotMatch(html, />添加草稿<\/button>/);
assert.doesNotMatch(html, /主流程[\s\S]*添加草稿/u);
assert.match(app, /sendAgentMessage/);
assert.match(app, /fetchJson\("\/v1\/agent\/chat"/);
assert.match(app, /fetchJson\("\/v1\/m3\/io"/);
assert.match(app, /fetchJson\("\/v1\/m3\/status"\)/);
assert.match(html, /id="m3-control-form"/);
assert.match(html, /id="m3-control-status"/);
assert.match(html, />写入 DO1<\/button>/);
assert.match(html, />读取 DI1<\/button>/);
assert.match(app, /function runM3IoAction/);
assert.match(app, /function renderM3ControlStatus/);
assert.match(app, /frontendBypass=false/);
assert.match(app, /按钮只走受控后端/);
assert.match(app, /页面不直连模拟器,也不伪造硬件状态。/);
assert.match(styles, /\.m3-control-form\s*{/);
assert.match(html, /data-hardware-tab="overview"[\s\S]*>总览<\/button>/);
assert.match(html, /data-hardware-tab="gateways"[\s\S]*>Gateway-SIMU<\/button>/);
assert.match(html, /data-hardware-tab="box1"[\s\S]*>BOX-SIMU-1<\/button>/);
assert.match(html, /data-hardware-tab="box2"[\s\S]*>BOX-SIMU-2<\/button>/);
assert.match(html, /data-hardware-tab="patch"[\s\S]*>Patch Panel<\/button>/);
assert.match(app, /function initHardwareTabs/);
assert.match(app, /function hardwareKvGroup/);
assert.match(app, /function m3LinkStatus/);
assert.match(app, /sourceFallbackM3Status/);
for (const hardwareLabel of ["链路状态", "接线盘连接", "在线", "更新时间", "阻塞原因", "接线状态", "连接已激活"]) {
assert.match(app, new RegExp(escapeRegExp(hardwareLabel)), `missing Chinese hardware label ${hardwareLabel}`);
}
assert.match(styles, /\.hardware-status-tabs\s*\{/);
assert.match(styles, /\.kv-list\s*\{/);
assert.match(styles, /\.right-sidebar \.table-wrap\s*\{[^}]*overflow-x:\s*hidden;/s);
assert.match(app, /Code Agent 调用失败/);
assert.match(app, /normalizeBlockedAgentResult/);
assert.match(app, /isBlockedAgentResponse/);
assert.match(app, /isHttpNon2xxStatus/);
assert.match(app, /providerStatus/);
assert.match(app, /codeAgentAvailability/);
assert.match(app, /codeAgentAvailabilityFrom/);
assert.match(app, /sanitizeCodeAgentAvailability/);
assert.match(app, /latestCompletedAgentMessage/);
assert.match(app, /服务受阻/);
assert.match(app, /provider_unavailable/);
assert.match(app, /OPENAI_API_KEY/);
assert.match(app, /只有真实完成回复才显示为完成/);
assert.match(app, /不能因为只有会话编号就当成实况完成/);
assert.match(app, /TRUSTED_CODE_AGENT_PROVIDERS/);
assert.match(app, /UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN/);
assert.match(app, /isRealCompletedChatResult/);
assert.match(app, /isRealCompletedChatMessage/);
assert.match(app, /hasRealCodeAgentEvidence/);
assert.match(app, /isTrustedCodeAgentProvider\(value\?\.provider\)/);
assert.match(app, /Boolean\(value\?\.model\)/);
assert.match(app, /Boolean\(value\?\.backend\)/);
assert.match(app, /Boolean\(value\?\.traceId\)/);
assert.match(app, /Boolean\(value\?\.conversationId \|\| value\?\.sessionId\)/);
assert.match(app, /value\?\.session\?\.status === "idle"/);
assert.match(app, /typeof value\?\.session\?\.idleTimeoutMs === "number"/);
assert.match(app, /Boolean\(value\?\.session\?\.lastTraceId\)/);
assert.match(app, /Boolean\(value\?\.sessionReuse\)/);
assert.match(app, /value\?\.sessionMode === CODEX_APP_SERVER_SESSION_MODE/);
assert.match(app, /value\?\.implementationType === CODEX_APP_SERVER_IMPLEMENTATION_TYPE/);
assert.match(app, /threadIdFrom\(value\)/);
assert.match(app, /hasProviderTrace\(value\)/);
assert.match(app, /value\?\.capabilityLevel === "long-lived-codex-stdio-session"/);
assert.match(app, /value\?\.session\?\.codexStdio === true/);
assert.match(app, /value\?\.runner\?\.codexStdio === true/);
assert.match(app, /value\?\.runner\?\.writeCapable === true/);
assert.match(app, /value\?\.runner\?\.durableSession === true/);
assert.match(app, /providerTrace:\s*result\.providerTrace/);
assert.match(app, /CODEX_APP_SERVER_RUNNER_KIND/);
assert.match(app, /CODEX_APP_SERVER_PROTOCOL/);
assert.match(app, /value\?\.providerTrace\?\.protocol === CODEX_APP_SERVER_PROTOCOL/);
assert.match(app, /providerTrace\.command/);
assert.match(app, /providerTrace\.terminalStatus/);
assert.match(app, /session:\s*result\.session/);
assert.match(app, /sessionMode:\s*result\.sessionMode/);
assert.match(app, /sessionReuse:\s*result\.sessionReuse/);
assert.match(app, /implementationType:\s*result\.implementationType/);
assert.match(app, /runnerLimitations:\s*result\.runnerLimitations/);
assert.match(app, /codexStdioFeasibility:\s*result\.codexStdioFeasibility/);
assert.match(app, /longLivedSessionGate:\s*result\.longLivedSessionGate/);
assert.match(app, /conversationFacts:\s*result\.conversationFacts/);
assert.match(app, /"Prefer":\s*"respond-async"/);
assert.match(app, /DEFAULT_CODE_AGENT_CANCEL_TIMEOUT_MS\s*=\s*30000/);
assert.match(app, /DEFAULT_GATEWAY_SHELL_TIMEOUT_MS\s*=\s*120000/);
assert.match(app, /GATEWAY_SHELL_TIMEOUT_STORAGE_KEY/);
assert.match(app, /CODE_AGENT_PROVIDER_PROFILE_STORAGE_KEY/);
assert.match(app, /DEFAULT_CODE_AGENT_PROVIDER_PROFILE\s*=\s*"deepseek"/);
assert.match(app, /CODE_AGENT_PROVIDER_PROFILES\s*=\s*Object\.freeze\(\["deepseek",\s*"codex-api"\]\)/);
assert.match(html, /id="code-agent-provider-profile"/);
assert.match(html, /value="deepseek" selected/);
assert.match(html, /value="codex-api"/);
assert.match(functionBody(app, "sendAgentMessage"), /providerProfile:\s*CODE_AGENT_PROVIDER_PROFILE/);
assert.match(app, /gatewayShellTimeoutMs:\s*GATEWAY_SHELL_TIMEOUT_MS/);
assert.match(html, /id="gateway-shell-timeout"/);
assert.match(app, /CODE_AGENT_CANCEL_TIMEOUT_MS/);
assert.match(functionBody(app, "cancelAgentRequest"), /timeoutMs:\s*CODE_AGENT_CANCEL_TIMEOUT_MS/);
assert.match(app, /function stopRunningTraceStream/);
assert.match(functionBody(app, "cancelAgentMessage"), /stopRunningTraceStream\(traceId\)/);
assert.match(app, /function waitForAgentMessageResult/);
assert.match(app, /\/v1\/agent\/chat\/result\/\$\{encodeURIComponent\(traceId\)\}/);
assert.match(app, /function isTransientCodeAgentResultPollError/);
assert.match(functionBody(app, "isTransientCodeAgentResultPollError"), /\[408,\s*425,\s*429,\s*500,\s*502,\s*503,\s*504\]/);
assert.match(functionBody(app, "waitForAgentMessageResult"), /isTransientCodeAgentResultPollError\(response\)/);
assert.match(app, /function refreshTraceAfterResultPollError/);
assert.match(app, /function messageTracePanel/);
assert.match(app, /function messageTraceToolbar/);
assert.match(app, /function messageTraceCountText/);
assert.match(app, /显示全部可读事件\s+\$\{readableTotal\}\s+\/\s+已载入原始\s+\$\{loadedTotal\}\s+\/\s+后端原始\s+\$\{rawTotal\}/);
assert.match(app, /完整 trace 回放中\s+\/\s+当前可读事件\s+\$\{readableTotal\}\s+\/\s+已载入原始\s+\$\{loadedTotal\}\s+\/\s+后端原始\s+\$\{rawTotal\}/);
assert.doesNotMatch(app, /压缩窗口\s+\$\{readableTotal\}/);
assert.match(app, /CODE_AGENT_SESSION_STORAGE_KEY\s*=\s*"hwlab\.workbench\.codeAgentSession\.v1"/);
assert.match(app, /function restoreCodeAgentSessionState/);
assert.match(app, /function persistCodeAgentSessionState/);
assert.match(app, /window\.localStorage\?\.setItem\(CODE_AGENT_SESSION_STORAGE_KEY/);
assert.match(app, /window\.localStorage\?\.removeItem\(CODE_AGENT_SESSION_STORAGE_KEY/);
assert.match(app, /refreshRestoredCodeAgentTraces/);
assert.match(app, /FULL_TRACE_REPLAY_TIMEOUT_MS\s*=\s*15000/);
assert.match(app, /fullTraceReplayInFlight:\s*new Set\(\)/);
assert.match(app, /function maybeReplayFullTraceForMessage/);
assert.match(app, /function replayFullTrace/);
assert.match(app, /eventsCompacted:\s*snapshot\.eventsCompacted === true/);
assert.match(app, /eventWindow:\s*snapshot\.eventWindow/);
assert.match(app, /assistantStreams:\s*Array\.isArray\(snapshot\.assistantStreams\)/);
assert.match(app, /function traceAssistantStreamRows/);
assert.match(app, /function traceAssistantStreamRow/);
assert.match(app, /function traceAssistantStreamBody/);
assert.match(app, /function traceAssistantStreamChunkCount/);
assert.match(app, /fullTraceLoaded:/);
assert.match(app, /eventCount:\s*Number\.isInteger\(trace\?\.eventCount\)/);
assert.match(app, /traceDetailsOpen:\s*new Map\(\)/);
assert.match(app, /conversationRenderVersion:\s*0/);
assert.match(app, /conversationScrollUserActiveUntil:\s*0/);
assert.match(app, /conversationScrollPosition:\s*\{\s*top:\s*0,\s*left:\s*0\s*\}/);
assert.match(app, /traceScrollPositions:\s*new Map\(\)/);
assert.match(app, /traceScrollUserActiveUntil:\s*new Map\(\)/);
assert.match(app, /state\.traceDetailsOpen\.clear\(\)/);
assert.match(app, /state\.conversationRenderVersion \+= 1/);
assert.match(app, /state\.conversationScrollPosition\s*=\s*\{\s*top:\s*0,\s*left:\s*0\s*\}/);
assert.match(app, /state\.traceScrollPositions\.clear\(\)/);
assert.match(app, /state\.traceScrollUserActiveUntil\.clear\(\)/);
assert.match(app, /state\.fullTraceReplayInFlight\.clear\(\)/);
assert.match(app, /function messageTraceUiKey/);
assert.match(app, /function defaultTraceDetailsOpen/);
assert.match(app, /initConversationScrollMemory\(\)/);
assert.match(app, /function initConversationScrollMemory/);
assert.match(app, /function markConversationScrollIntent/);
assert.match(app, /function markTraceScrollIntent/);
assert.match(app, /function scrollBottomGap/);
assert.match(app, /function scrollTopForPosition/);
assert.match(app, /function captureConversationScrollPosition/);
assert.match(app, /function restoreConversationScrollPosition/);
assert.match(app, /function captureTraceScrollPositions/);
assert.match(app, /function restoreTraceScrollPositions/);
assert.match(app, /function rememberTraceScrollPosition/);
assert.match(functionBody(app, "renderConversation"), /captureConversationScrollPosition\(\)/);
assert.match(functionBody(app, "renderConversation"), /restoreConversationScrollPosition\(\)/);
assert.match(functionBody(app, "renderConversation"), /captureTraceScrollPositions\(\)/);
assert.match(functionBody(app, "renderConversation"), /restoreTraceScrollPositions\(\)/);
assert.match(functionBody(app, "renderConversation"), /requestAnimationFrame/);
assert.match(functionBody(app, "renderConversation"), /renderVersion !== state\.conversationRenderVersion/);
assert.match(functionBody(app, "restoreTraceScrollPositions"), /isTraceScrollUserActive/);
assert.match(functionBody(app, "scrollTopForPosition"), /SCROLL_BOTTOM_PIN_PX/);
assert.match(functionBody(app, "messageTracePanel"), /state\.traceDetailsOpen\.get\(traceUiKey\)/);
assert.match(functionBody(app, "messageTracePanel"), /defaultTraceDetailsOpen\(message\)/);
assert.match(functionBody(app, "messageTracePanel"), /state\.traceDetailsOpen\.set\(traceUiKey,\s*details\.open\)/);
assert.match(functionBody(app, "messageTracePanel"), /list\.dataset\.traceUiKey\s*=\s*traceUiKey/);
assert.match(functionBody(app, "messageTracePanel"), /markTraceScrollIntent\(traceUiKey\)/);
assert.match(functionBody(app, "messageTracePanel"), /rememberTraceScrollPosition\(traceUiKey,\s*list\)/);
assert.match(functionBody(app, "messageTracePanel"), /list\.dataset\.traceMode\s*=\s*"all"/);
assert.match(functionBody(app, "messageTracePanel"), /renderTraceEventList\(list,\s*rows\)/);
assert.doesNotMatch(app, /CODE_AGENT_TRACE_PREVIEW_LIMIT|tracePreviewRows|收起为最近|展开全部|data-trace-mode="tail"/);
assert.match(app, /复制 JSON/);
assert.match(app, /下载 trace/);
assert.match(app, /function renderTraceEventList/);
assert.match(app, /function installWorkbenchTestHooks/);
assert.match(app, /hwlab-test-hooks/);
assert.match(app, /function traceDisplayRows/);
assert.match(app, /function traceDisplayRow/);
assert.match(app, /function traceToolOutputSummaryRows/);
assert.match(app, /function traceToolOutputSummaryRow/);
assert.match(app, /function parseGatewayJsonRpcOutputs/);
assert.match(app, /function parseGatewayJsonRpcOutput/);
assert.match(app, /tool gateway\.shell/);
assert.match(app, /function traceNoiseSummaryRow/);
assert.match(styles, /max-height:\s*min\(520px,\s*54dvh\)/);
assert.match(app, /assistant message x\$\{visibleEvents\.length\}/);
assert.match(app, /assistant message x\$\{chunkCount\}/);
assert.match(app, /chunks=\$\{assistantChunks\.length\} assistant message chunks/);
assert.match(app, /chunks=\$\{chunkCount\} assistant message chunks/);
assert.doesNotMatch(app, /compressed=\$\{assistantChunks\.length\} assistant chunks/);
assert.match(app, /message=\$\{text\}/);
assert.match(app, /waiting first assistant token/);
assert.match(app, /function compactTraceTextTail/);
assert.match(app, /function isAssistantChunkTraceEvent/);
assert.match(app, /function downloadTraceJson/);
assert.match(styles, /\.message-trace-toolbar\s*\{/);
assert.match(styles, /\.message-trace-count\s*\{/);
assert.match(styles, /\.message-trace-action\s*\{/);
assert.doesNotMatch(styles, /\.message-trace-events-full/);
assert.match(app, /function messageRuntimePathPanel/);
assert.match(app, /codeAgentRuntimePathFromMessage/);
assert.match(app, /function runnerTraceHeadline/);
assert.match(app, /function traceEventMeta/);
assert.match(app, /function subscribeRunnerTrace/);
assert.match(app, /function pollRunnerTrace/);
assert.match(app, /TRACE_STREAM_FALLBACK_MS\s*=\s*2500/);
assert.match(app, /fallbackStop\s*=\s*pollRunnerTrace\(traceId,\s*messageId\)/);
assert.match(app, /TRACE_POLL_INTERVAL_MS/);
assert.match(app, /function updateMessageTrace/);
assert.match(app, /function runnerTraceFromSnapshot/);
assert.match(app, /function reconcileCodeAgentResult/);
assert.match(app, /function shouldReconcileCodeAgentResult/);
assert.match(app, /function reconcileRestoredCodeAgentResults/);
assert.match(app, /traceHasTerminalResultEvent/);
assert.match(app, /new EventSource\(`\/v1\/agent\/chat\/trace\/\$\{encodeURIComponent\(traceId\)\}\/stream`\)/);
assert.match(app, /source\.addEventListener\("runnerTrace"/);
assert.doesNotMatch(app, /providerTraceSummary\(message\.providerTrace\)/);
assert.doesNotMatch(app, /function messageEvidenceFields/);
assert.match(app, /codeAgentSummaryRow\("sessionId\/status", summary\.sessionIdStatus/);
assert.match(app, /codeAgentSummaryRow\("workspace", summary\.workspace/);
assert.match(app, /codeAgentSummaryRow\("sandbox", summary\.sandbox/);
assert.match(app, /codeAgentSummaryRow\("runnerKind", summary\.runnerKind/);
assert.match(app, /codeAgentSummaryRow\("protocol", summary\.protocol/);
assert.match(app, /codeAgentSummaryRow\("implementationType", summary\.implementationType/);
assert.match(app, /codeAgentSummaryRow\("providerTrace\.command", summary\.providerTraceCommand/);
assert.match(app, /codeAgentSummaryRow\("providerTrace\.terminalStatus", summary\.providerTraceTerminalStatus/);
assert.match(app, /codeAgentSummaryRow\("运行路径语义", summary\.runtimePathLabel/);
assert.match(app, /codeAgentSummaryRow\("toolCalls", summary\.toolCalls/);
assert.match(app, /codeAgentSummaryRow\("skills", summary\.skills/);
assert.match(app, /codeAgentSummaryRow\("conversation facts", conversationFactsSummary\(summary\.latestMessage\?\.conversationFacts\)/);
assert.match(app, /codeAgentSummaryRow\("fact traces", conversationFactTracesSummary\(summary\.latestMessage\?\.conversationFacts\)/);
assert.match(app, /codeAgentSummaryRow\("runnerTrace", summary\.runnerTrace/);
assert.match(app, /function sessionSummary/);
assert.match(app, /function conversationFactsSummary/);
assert.match(app, /function codeAgentAvailabilitySummary/);
assert.doesNotMatch(app, /function longLivedSessionGateSummary/);
assert.doesNotMatch(app, /function conversationFactRows/);
assert.match(app, /not-codex-stdio/);
assert.match(app, /not-write-capable/);
assert.match(app, /process-local-session-registry/);
assert.match(app, /Code Agent 完成证据不足/);
assert.match(app, /本次不会标记为真实完成/);
assert.match(app, /untrusted_completion/);
assert.match(app, /function classifyCodeAgentCompletion/);
assert.match(app, /function isSourceFixtureChatResult/);
assert.match(app, /function isSourceFixtureCompletion/);
assert.match(app, /function isSourceFixtureCompletedChatMessage/);
assert.match(app, /Code Agent SOURCE 回复/);
assert.match(app, /SOURCE fixture 只可显示为 SOURCE 回复,不能冒充 DEV-LIVE/);
assert.match(app, /function messageTracePanel/);
assert.match(app, /function subscribeRunnerTrace/);
assert.match(app, /new EventSource\(`\/v1\/agent\/chat\/trace\/\$\{encodeURIComponent\(traceId\)\}\/stream`\)/);
assert.match(app, /source\.addEventListener\("runnerTrace"/);
assert.match(app, /function pollRunnerTrace/);
assert.match(app, /function updateMessageTrace/);
assert.match(app, /function runnerTraceFromSnapshot/);
assert.match(app, /function reconcileCodeAgentResult/);
assert.match(app, /function shouldReconcileCodeAgentResult/);
assert.doesNotMatch(app, /function messageEvidencePanel/);
assert.doesNotMatch(app, /function messageAttributionPanel/);
assert.doesNotMatch(app, /function codeAgentCapabilityFactsPanel/);
assert.doesNotMatch(app, /codeAgentAttributionFromMessage/);
assert.doesNotMatch(app, /codeAgentFactsFromMessage/);
assert.doesNotMatch(app, /compactHwlabApiFact/);
assert.match(app, /function renderCodeAgentSummary/);
assert.match(app, /currentCodeAgentStatusSummary/);
assert.match(app, /classifyCodeAgentStatusSummary/);
assert.match(app, /codeAgent\.status/);
assert.match(app, /当前部署 revision/);
assert.match(codeAgentFacts, /字段缺失:证据不足/);
assert.match(codeAgentFacts, /codeAgentAttributionFromMessage/);
assert.match(codeAgentFacts, /operationId", fact\.operationId/);
assert.match(codeAgentFacts, /auditId", fact\.auditId/);
assert.match(codeAgentFacts, /evidenceId", fact\.evidenceId/);
assert.match(codeAgentFacts, /blocker", fact\.blocker/);
assert.match(codeAgentStatus, /long-lived-session/);
assert.match(codeAgentStatus, /currentDeploymentRevision/);
assert.match(codeAgentStatus, /unsafeGreenForNonReady/);
assert.match(codeAgentStatus, /provider_config_blocked/);
assert.doesNotMatch(codeAgentStatus, /sourceMain|SOURCE main|待部署 commit|targetCommit/u);
assert.match(app, /sourceKind:\s*completion\.sourceKind/);
assert.match(app, /function\s+agentStatusTone\s*\(/);
assert.match(app, /m3_io_result/);
assert.doesNotMatch(app, /status === "failed" \? "dev-live"/);
assert.doesNotMatch(`${html}\n${app}`, /provider_unavailable[\s\S]{0,120}tone-[\w-]*dev-live/iu);
assert.doesNotMatch(app, /tone:\s*state\.conversationId\s*\?\s*"dev-live"/);
assert.doesNotMatch(`${html}\n${app}`, /sk-[A-Za-z0-9._-]{8,}/u);
assert.match(styles, /\.message-card\.status-completed\s*{/);
assert.match(styles, /\.message-card\.status-source\s*{/);
assert.match(styles, /\.message-user\s*{/);
assert.match(styles, /\.message-agent,\s*\n\.message-system\s*{/);
assert.match(styles, /\.message-pending-context\s*{/);
assert.match(styles, /\.message-session-context\s*{/);
assert.match(styles, /\.message-compact-details\s*{/);
assert.match(styles, /\.workbench-dialog-layer\s*{/);
assert.match(styles, /\.workbench-dialog\s*{/);
assert.match(app, /function openWorkbenchDialog/);
assert.match(app, /document\.body\.append\(layer\)/);
assert.match(styles, /\.message-m3-evidence\s*{/);
assert.match(styles, /\.message-m3-rows\s*{/);
assert.match(styles, /\.message-m3-row\s*{/);
assert.match(styles, /\.copy-chip\s*{/);
assert.match(styles, /\.message-trace\s*{/);
assert.match(styles, /\.message-trace-events\s*{/);
assert.match(styles, /\.message-trace-line\s*{/);
assert.match(styles, /\.message-trace-body\s*{/);
assert.doesNotMatch(styles, /\.message-attribution\s*{/);
assert.doesNotMatch(styles, /\.message-evidence\s*{/);
assert.doesNotMatch(styles, /\.code-agent-facts\s*{/);
assert.match(styles, /\.code-agent-summary\s*{/);
assert.match(styles, /\.tone-border-warn/);
assert.match(styles, /\.tone-border-blocked/);
assert.match(styles, /\.tone-border-ok/);
assert.match(styles, /\.hwlab-api-fact-code\s*{/);
assert.match(styles, /\.hwlab-api-fact-code\s*{[^}]*user-select:\s*all;/s);
assert.match(styles, /\.rail-button\s*{[^}]*min-width:\s*0;[^}]*min-height:\s*36px;[^}]*text-align:\s*center;[^}]*word-break:\s*keep-all;/s);
assert.match(styles, /\.side-tab\s*{[^}]*min-width:\s*0;[^}]*min-height:\s*34px;[^}]*overflow-wrap:\s*anywhere;[^}]*word-break:\s*keep-all;/s);
assert.match(styles, /\.(?:status-dot|state-tag|badge)[^{]*{[^}]*max-width:\s*100%;[^}]*line-height:\s*1\.25;[^}]*white-space:\s*normal;[^}]*overflow-wrap:\s*anywhere;/s);
assert.match(styles, /\.probe-card\s*{[^}]*min-width:\s*0;/s);
assert.match(styles, /\.probe-card strong\s*{[^}]*line-height:\s*1\.2;[^}]*overflow-wrap:\s*anywhere;/s);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.message-m3-rows\s*{[\s\S]*?grid-template-columns:\s*1fr;/);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.message-runtime-path,\s*\n\s*\.message-session-context,\s*\n\s*\.message-trace,\s*\n\s*\.message-m3-evidence,\s*\n\s*\.message-actions\s*{[\s\S]*?grid-column:\s*1;/);
for (const userFacingFunctionName of [
"codeAgentStatusMessage",
"codeAgentPromptText",
"codeAgentControlSummary",
"codeAgentBlockedSummary",
"codeAgentAvailabilitySummary",
"failureMessage",
"safeFailureReason"
]) {
assert.doesNotMatch(functionBody(app, userFacingFunctionName), /OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef|m3-route-required|waiting-for-dev-live|BLOCKED 凭证缺口/u, `${userFacingFunctionName} must keep default copy sanitized`);
}
assert.match(app, /state\.conversationId/);
assert.match(app, /conversationId/);
assert.match(app, /state\.threadId/);
assert.match(app, /threadIdForNextRequest/);
assert.match(app, /threadId:\s*requestedThreadId/);
assert.match(app, /function\s+completedSessionContinuity/);
assert.match(app, /function\s+codeAgentContinuityBlocker/);
assert.match(app, /code_agent_session_evidence_missing/);
assert.match(app, /Code Agent completed 但会话证据缺失/);
assert.match(app, /继续当前会话/);
assert.match(app, /新会话\/会话降级/);
assert.match(app, /messageId/);
assert.match(app, /createdAt/);
assert.match(app, /updatedAt/);
assert.match(app, /error\?\.message/);
assert.match(app, /DEFAULT_API_TIMEOUT_MS\s*=\s*4500/);
assert.match(app, /DEFAULT_LIVE_SURFACE_TIMEOUT_MS\s*=\s*12000/);
assert.match(app, /LIVE_SURFACE_TIMEOUT_MS\s*=\s*resolveTimeoutMs\("liveSurfaceTimeoutMs"/);
assert.match(app, /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*600000/);
assert.match(app, /CODE_AGENT_SUBMIT_TIMEOUT_MS/);
assert.match(app, /CODE_AGENT_TIMEOUT_MS/);
assert.match(functionBody(app, "sendAgentMessage"), /timeoutMs:\s*CODE_AGENT_SUBMIT_TIMEOUT_MS/);
assert.match(functionBody(app, "waitForAgentMessageResult"), /CODE_AGENT_TIMEOUT_MS/);
assert.match(functionBody(app, "liveSurfaceFetch"), /timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/);
assert.match(functionBody(app, "liveSurfaceRpc"), /timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/);
assert.match(functionBody(app, "loadLiveSurface"), /liveSurfaceFetch\("\/health\/live"\)/);
assert.match(functionBody(app, "loadLiveSurface"), /liveSurfaceRpc\("system\.health"\)/);
assert.match(app, /HWLAB_CLOUD_WEB_CONFIG/);
assert.match(app, /sourceKind:\s*"PENDING"/);
assert.match(app, /旧 4500ms/);
assert.match(app, /function messagePendingContextPanel/);
assert.match(app, /message-pending-context/);
assert.match(app, /function\s+restartRunningAgentMessage/);
assert.match(app, /先取消当前 trace,再用同一输入重新发送/);
assert.match(app, /canRetryTerminalAgentMessage/);
assert.match(app, /结构化 blocker/);
assert.match(app, /Code Agent 超过 \$\{response\.timeoutMs\}ms 无新事件/);
assert.match(app, /agentFailurePresentation/);
assert.match(app, /agentStatusLabel/);
assert.match(app, /Code Agent 无新事件超时/);
assert.match(app, /Code Agent Runner 忙碌/);
assert.match(app, /Code Agent Session 受阻/);
assert.match(app, /Code Agent API 错误/);
assert.match(app, /等待超时/);
assert.match(app, /Provider 不可用/);
assert.match(app, /Runner 忙碌/);
assert.match(app, /Session 受阻/);
assert.match(app, /API 错误/);
assert.match(app, /后端失败/);
assert.doesNotMatch(app, /发送失败/);
assert.doesNotMatch(html, /M3 Diagnostics Console/);
assert.match(styles, /html,\s*\nbody\s*{[^}]*height:\s*100%;[^}]*overflow:\s*hidden;/s);
assert.match(styles, /body\s*>\s*\[data-app-shell\]\s*{[^}]*min-height:\s*0;/s);
assert.match(styles, /\.workbench-shell\s*{[^}]*height:\s*100(?:d)?vh;[^}]*overflow:\s*hidden;/s);
assert.match(styles, /\.view\s*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s);
assert.match(styles, /\.(?:compact-list|conversation-list|hardware-list)[^{]*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s);
assert.doesNotMatch(html, /task-panel|task-list|下一步任务草稿/);
assert.doesNotMatch(app, /taskList|renderTaskList/);
assert.doesNotMatch(styles, /\.task-panel|\.task-list/);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.activity-rail\s*{[\s\S]*?grid-row:\s*1 \/ 3;/);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.center-workspace\s*{[\s\S]*?grid-row:\s*1;/);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.right-sidebar\s*{[\s\S]*?grid-row:\s*2;/);
assert.match(styles, /@media \(max-width: 520px\)[\s\S]*?\.command-bar\s*{[\s\S]*?grid-template-columns:\s*minmax\(0, 1fr\) auto auto;/);
assert.match(app, /fetchJson\("\/v1"\)/);
assert.match(app, /fetchJson\("\/v1\/diagnostics\/gate"/);
assert.match(app, /fetchJson\("\/v1\/live-builds"\)/);
assert.match(html, /id="live-build-latest"/);
assert.match(html, /id="live-build-list"/);
assert.match(html, /id="live-build-toggle"[^>]*aria-haspopup="dialog"[^>]*aria-expanded="false"/);
assert.match(styles, /\.live-build-summary\s*{/);
assert.match(app, /initLiveBuildOverlay/);
assert.match(app, /openWorkbenchDialog/);
assert.match(app, /aria-expanded/);
assert.match(app, /formatBeijingTime/);
assert.match(app, /Asia\/Shanghai/);
assert.match(app, /最新镜像构建时间/);
assert.match(app, /构建时间不可用/);
assert.match(app, /revision \$\{shortToken\(latest\.revision\)\}/);
assert.match(app, /revision \$\{shortToken\(service\.revision\)\}/);
assert.match(app, /liveBuildSourceLabel/);
assert.match(app, /liveBuildDesiredStateText/);
assert.match(app, /外部镜像或非 HWLAB 构建产物/);
assert.doesNotMatch(app, /artifact report|artifact-report/);
assert.match(app, /live health/);
assert.match(styles, /\.live-build-summary-label\s*{[^}]*pointer-events:\s*none;/s);
assert.match(styles, /\.agent-quick-prompts\s*{[^}]*grid-column:\s*1 \/ -1;[^}]*order:\s*2;/s);
assert.match(styles, /\.live-build-summary-label\s*{[^}]*min-width:\s*0;[^}]*max-width:\s*100%;[^}]*overflow:\s*hidden;[^}]*text-overflow:\s*ellipsis;[^}]*white-space:\s*nowrap;/s);
assert.match(styles, /\.live-build-list\s*{[^}]*min-width:\s*0;[^}]*max-height:\s*min\(352px,\s*calc\(58dvh - 54px\)\);[^}]*overflow:\s*auto;/s);
assert.match(styles, /\.live-build-row-head\s*{[^}]*grid-template-columns:\s*minmax\(0,\s*0\.8fr\)\s*minmax\(0,\s*1\.2fr\);/s);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.live-build-row-head\s*{[\s\S]*?grid-template-columns:\s*1fr;/);
assert.match(styles, /\.live-build-meta span\s*{[^}]*overflow-wrap:\s*anywhere;/s);
assert.match(app, /fetchJson\("\/health\/live"\)/);
assert.match(app, /callRpc\("system\.health"\)/);
assert.match(app, /callRpc\("cloud\.adapter\.describe"\)/);
assert.match(app, /callRpc\("audit\.event\.query"/);
assert.match(app, /callRpc\("evidence\.record\.query"/);
assert.match(artifactPublisher, /HWLAB_API_BASE_URL/);
assert.match(artifactPublisher, /HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS/);
assert.match(artifactPublisher, /cloudApiProxyTimeoutMs/);
assert.match(cloudWebProxy, /setTimeout\(\(\) => \{/);
assert.match(cloudWebProxy, /cloud api proxy timed out after/);
assert.match(cloudWebProxy, /"prefer"/);
assert.match(cloudWebProxy, /"x-hwlab-short-connection"/);
assert.match(artifactPublisher, /readOnlyRpcMethods/);
assert.match(artifactPublisher, /requestUpstream/);
assert.match(artifactPublisher, /proxyCloudApiRequest/);
assert.match(artifactPublisher, /isCloudWebSseRoute\(url\.pathname\)/);
assert.match(cloudWebProxy, /text\\\/event-stream/);
assert.match(cloudWebProxy, /x-accel-buffering/);
assert.match(cloudWebProxy, /flushHeaders/);
assert.match(cloudWebProxy, /response\.write\(chunk\)/);
assert.match(cloudWebProxy, /response\.headersSent\)[\s\S]*?response\.destroy\(error\);[\s\S]*?settle\(resolve\);[\s\S]*?return;/);
assert.match(artifactPublisher, /response\.headersSent \|\| response\.writableEnded/);
assert.match(artifactPublisher, /if \(!response\.writableEnded\) response\.destroy\(error\);/);
assert.doesNotMatch(artifactPublisher, /await fetch\(target/);
assert.match(artifactPublisher, /cloudWebProxyRoutePolicy\(request\.method,\s*url\.pathname\)/);
assert.match(cloudWebRoutes, /"\/v1\/agent\/chat"/);
assert.match(cloudWebRoutes, /"\/v1\/agent\/chat\/cancel"/);
assert.match(cloudWebRoutes, /"\/v1\/m3\/io"/);
assert.match(cloudWebRoutes, /"POST \/v1\/agent\/chat"/);
assert.match(cloudWebRoutes, /"POST \/v1\/agent\/chat\/cancel"/);
assert.match(cloudWebRoutes, /isPublicCodeAgentPollRoute/);
assert.match(cloudWebRoutes, /\/v1\/agent\/chat\/result\//);
assert.match(cloudWebRoutes, /\/v1\/agent\/chat\/trace\//);
assert.match(cloudWebRoutes, /const GET_PROXY_PREFIXES = Object\.freeze\(\["\/v1\/"\]\)/);
assert.match(cloudWebRoutes, /const GET_PROXY_ROUTES = new Set\(\["\/v1"\]\)/);
assert.match(cloudWebRoutes, /authRequired: proxy && !publicRoute/);
assert.match(codexStdioSession, /turn:completed:transport_closed_after_assistant/);
assert.match(codexStdioSession, /turn:completed:idle_timeout_after_assistant/);
assert.match(codexStdioSession, /firstNonEmpty\(finalResponse,\s*assistantText\)/);
assert.match(codexStdioSession, /CODEX_STDIO_FIRST_TOKEN_PROGRESS_MS/);
assert.match(codexStdioSession, /turn:waiting:first_assistant_token/);
assert.match(codexStdioSession, /progressOnly:\s*true/);
assert.match(codexStdioSession, /\["assistant-message", "turn\/completed"\]\.includes\(activity\.waitingFor\)/);
assert.match(codexStdioSession, /does not reset the backend idle timer/);
assert.match(codexStdioSession, /--powershell-stdin/);
assert.match(codexStdioSession, /gatewayShellTimeoutMs/);
assert.match(codexStdioSession, /Read-HwlabText/);
assert.match(codexStdioSession, /ConvertTo-HwlabJson/);
assert.match(codexStdioSession, /-EncodedCommand/);
assert.match(codexStdioSession, /do not hand-build compound cmd \/c, cd &&, pipes, or nested quotes/);
assert.match(codexStdioSession, /Set-Location -LiteralPath/);
assert.match(codexStdioSession, /Windows filesystem inventory/);
assert.match(codexStdioSession, /Select-Object -First/);
assert.match(codexStdioSession, /ConvertTo-Json -Compress/);
assert.match(codexStdioSession, /C:\\\\Users\\\\liang\\\\\.agents\\\\skills\\\\keil/);
assert.match(codexStdioSession, /C:\\\\Users\\\\liang\\\\\.agents\\\\skills\\\\serial-monitor/);
assert.match(codexStdioSession, /monitor start/);
assert.match(codexStdioSession, /async job flow/);
assert.match(gatewayShellTool, /function powershellEncodedCommand/);
assert.match(gatewayShellTool, /DEFAULT_GATEWAY_SHELL_TIMEOUT_MS\s*=\s*120000/);
assert.match(gatewayShellTool, /REQUEST_TIMEOUT_GRACE_MS/);
assert.match(gatewayShellTool, /op_gateway_shell_cli_\$\{randomUUID\(\)\}/);
assert.match(gatewayShellTool, /operationId,/);
assert.match(gatewayShellTool, /Read-HwlabText/);
assert.match(gatewayShellTool, /Buffer\.from\(`\$\{prologue\}\$\{source\}`,\s*"utf16le"\)\.toString\("base64"\)/);
assert.match(gatewayShellTool, /--powershell-stdin/);
assert.match(gatewayShellTool, /--cwd WINDOWS_PATH/);
assert.match(gatewayShellTool, /powershell\.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand/);
assert.doesNotMatch(gatewayShellTool, /keil-find|keil-build|keil-job-status/);
assert.match(cloudJsonRpc, /Math\.max\(configured \?\? 0,\s*requested \?\? 0,\s*120000\)/);
assert.match(cloudJsonRpc, /op_gateway_shell_\$\{randomUUID\(\)\}/);
assert.match(gatewayDemoRegistry, /DEFAULT_DISPATCH_TIMEOUT_MS\s*=\s*120000/);
assert.match(gatewayDemoRegistry, /inflightCount/);
assert.match(gatewayDemoRegistry, /maxInflightRequests/);
assert.match(gatewayMain, /HWLAB_GATEWAY_MAX_INFLIGHT/);
assert.match(gatewayMain, /function startCloudRequest/);
assert.match(gatewayMain, /Promise\.resolve\(\)\s*\n\s*\.then\(\(\) => handleCloudRequest/);
assert.doesNotMatch(gatewayMain, /await handleCloudRequest\(response\.body\.request\)/);
assert.match(gatewayDemoSmoke, /quick request was blocked by slow request/);
assert.match(gatewayDemoSmoke, /function requestJson/);
assert.doesNotMatch(functionBody(gatewayDemoSmoke, "getJson"), /\bfetch\(/);
assert.doesNotMatch(functionBody(gatewayDemoSmoke, "postJson"), /\bfetch\(/);
assert.match(gatewayNonblockingProbe, /quick request was blocked by slow request/);
assert.match(simHttp, /request as httpRequest/);
assert.match(functionBody(simHttp, "fetchJson"), /httpRequest/);
assert.doesNotMatch(functionBody(simHttp, "fetchJson"), /\bfetch\(/);
assert.match(cloudApiServer, /HWLAB_GATEWAY_DISPATCH_TIMEOUT_MS,\s*120000/);
assert.match(codexStdioSession, /detached:\s*childDetached/);
assert.match(codexStdioSession, /function terminateChildProcess/);
assert.match(codexStdioSession, /process\.kill\(-child\.pid,\s*"SIGTERM"\)/);
assert.match(codexStdioSession, /process\.kill\(-child\.pid,\s*"SIGKILL"\)/);
assert.match(cloudApiServer, /function isCodeAgentResultCanceled/);
assert.match(cloudApiServer, /options\.codeAgentChatResults\?\.set\(traceId,\s*payload\)/);
assert.match(cloudApiServer, /if \(isCodeAgentResultCanceled\(results\.get\(traceId\)\)\) return;/);
assert.match(artifactPublisher, /"system\.health"/);
assert.match(artifactPublisher, /"cloud\.adapter\.describe"/);
assert.match(artifactPublisher, /"audit\.event\.query"/);
assert.match(artifactPublisher, /"evidence\.record\.query"/);
assert.match(artifactPublisher, /readonly_rpc_required/);
assert.match(artifactPublisher, /cloud_api_proxy_failed/);
assert.match(artifactPublisher, /cloud_api_proxy_timeout/);
assert.match(artifactPublisher, /Code Agent 代理等待 cloud-api 超过/);
assert.match(artifactPublisher, /输入已保留,可稍后重试/);
assert.match(artifactPublisher, /retryable:\s*true/);
assert.match(artifactPublisher, /blocker:\s*\{/);
assert.match(artifactPublisher, /const traceId = request\.headers\["x-trace-id"\] \|\| null/);
assert.match(artifactPublisher, /traceId,\s*\n\s*route: url\.pathname/);
assert.match(artifactPublisher, /url\.pathname\.replace\(\/\\\/\+\$\/,\s*""\) \|\| "\/"/);
assert.match(artifactPublisher, /routePath === "\/gate"/);
assert.match(artifactPublisher, /routePath === "\/diagnostics\/gate"/);
assert.match(artifactPublisher, /routePath === "\/help"/);
assert.match(app, /degraded/);
assert.deepEqual(runtime.serviceRoute, [
"browser/CLI/gateway",
"master hwlab-edge-proxy",
"frp",
"D601 hwlab-dev/hwlab-router",
"cloud-web/cloud-api"
]);
assert.doesNotMatch(app, /callRpc\("hardware\./);
assert.doesNotMatch(app, /hardware\.operation\.request/);
assert.doesNotMatch(app, /hardware\.invoke\.shell/);
assert.doesNotMatch(app, /audit\.event\.write/);
assert.doesNotMatch(app, /evidence\.record\.write/);
assert.doesNotMatch(app, /https?:\/\/[^"']*(?:gateway-simu|box-simu|patch-panel)[^"']*/iu);
assert.doesNotMatch(frontendSource, /--live --confirm-dev --confirmed-non-production/);
assert.doesNotMatch(frontendSource, /74\.48\.78\.17:666[67]\b|:666[67]\b/);
const sourceSummary = loadMvpGateSummary(repoRoot);
assert.equal(gateSummary.endpoint, sourceSummary.endpoint);
assert.equal(gateSummary.gateStatus, sourceSummary.gateStatus);
assert.equal(gateSummary.milestones.length, 6);
assert.deepEqual(
gateSummary.milestones.map((item) => item.id),
["M0", "M1", "M2", "M3", "M4", "M5"]
);
assert.equal(gateSummary.artifactCount, 13);
assert.equal(gateSummary.healthCount, 14);
assert.equal(gateSummary.gatewaySessionCount, 2);
assert.equal(gateSummary.boxResourceCount, 2);
assert.equal(gateSummary.topology.patchPanel.serviceId, "hwlab-patch-panel");
assert.deepEqual(gateSummary.topology.gateways.map((gateway) => gateway.gatewayId), ["gwsimu_1", "gwsimu_2"]);
assert.deepEqual(gateSummary.topology.boxResources.map((resource) => resource.resourceId), ["res_boxsimu_1", "res_boxsimu_2"]);
assert.deepEqual(gateSummary.topology.patchPanel.activeConnections, [
{
fromResourceId: "res_boxsimu_1",
fromPort: "DO1",
toResourceId: "res_boxsimu_2",
toPort: "DI1"
}
]);
assert.doesNotMatch(
JSON.stringify(gateSummary.topology),
/res_m5-control-relay|res_m5-target-board|out1|reset/u,
"16666 default topology must not render M5/reset wiring"
);
assert.equal(gateSummary.m5DryRunTopology.patchPanel.patchPanelStatusId, "pps_m5-0001");
assert.equal(gateSummary.agent.agentServiceId, "hwlab-agent-mgr");
assert.equal(gateSummary.agent.workerServiceId, "hwlab-agent-worker");
assert.equal(gateSummary.operations.length, 2);
assert.ok(gateSummary.operations.every((operation) => operation.dryRun === true));
assert.ok(gateSummary.operations.every((operation) => operation.requestedAt && operation.updatedAt));
assert.equal(gateSummary.auditEvents.length, 7);
assert.ok(gateSummary.auditEvents.every((event) => event.dryRun === true));
assert.ok(gateSummary.auditEvents.every((event) => event.traceId && event.occurredAt));
assert.equal(gateSummary.evidenceRecords.length, 2);
assert.ok(gateSummary.evidenceRecords.every((record) => record.traceId && record.createdAt && record.dryRun === true));
assert.equal(gateSummary.safety.allowNetwork, false);
runCloudWebM3ReadonlyContract();
runM3ControlPanelGuard();
runWorkbenchHardwarePanelContract();
console.log("hwlab-cloud-web check ok: workbench shell, M3 control guard, hardware evidence panel, and read-only diagnostics contract are present");
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function sectionSource(source, viewId) {
const start = source.search(new RegExp(`<section\\b[^>]*\\bdata-view=["']${escapeRegExp(viewId)}["'][^>]*>`, "u"));
if (start === -1) return "";
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) {
const index = source.search(new RegExp(`<section\\b[^>]*\\bdata-view=["']${escapeRegExp(viewId)}["'][^>]*>`, "u"));
return index === -1 ? source : source.slice(0, index);
}
function defaultFirstViewportHtml(source) {
return `${beforeSection(source, "help")}\n${rightSidebarDefaultHtml(source)}`;
}
function rightSidebarDefaultHtml(source) {
const start = source.search(/<aside\b[^>]*\bclass=["'][^"']*\bright-sidebar\b[^"']*["'][^>]*>/u);
if (start === -1) return "";
const wiringStart = source.slice(start).search(/<section\b[^>]*\bid=["']panel-wiring["'][^>]*>/u);
if (wiringStart !== -1) return source.slice(start, start + wiringStart);
const end = source.slice(start).search(/<\/aside>/u);
return end === -1 ? source.slice(start) : source.slice(start, start + end);
}
function visibleTextFromHtml(fragment) {
return fragment
.replace(/<script\b[\s\S]*?<\/script>/giu, " ")
.replace(/<style\b[\s\S]*?<\/style>/giu, " ")
.replace(/<[^>]+>/gu, " ")
.replace(/&nbsp;/gu, " ")
.replace(/&gt;/gu, ">")
.replace(/&lt;/gu, "<")
.replace(/&amp;/gu, "&")
.replace(/\s+/gu, " ")
.trim();
}
function textHasForbiddenTerm(text, term) {
if (/^[A-Za-z][A-Za-z0-9-]*$/u.test(term)) {
return new RegExp(`\\b${escapeRegExp(term)}\\b`, "u").test(text);
}
return text.includes(term);
}
function compactLayoutSmokeFailures(report) {
return {
status: report.status,
sourceMode: report.sourceMode,
artifacts: report.artifacts,
blockers: (report.blockers ?? []).map(compactLayoutFailure),
failures: (report.failures ?? []).map(compactLayoutFailure)
};
}
function compactLayoutFailure(failure) {
return {
checkId: failure.checkId ?? failure.scope ?? null,
viewport: failure.viewport ?? null,
selector: failure.selector ?? null,
failureType: failure.failureType ?? "blocked",
summary: failure.summary ?? null,
artifact: failure.artifact ?? null
};
}
function functionBody(source, functionName) {
const match = source.match(new RegExp(`(?:async\\s+)?function\\s+${escapeRegExp(functionName)}\\s*\\(`, "u"));
if (!match || typeof match.index !== "number") return "";
let signatureIndex = match.index + match[0].length - 1;
let signatureDepth = 0;
let bodyStart = -1;
for (let index = signatureIndex; index < source.length; index += 1) {
const char = source[index];
if (char === "(") signatureDepth += 1;
if (char === ")") {
signatureDepth -= 1;
continue;
}
if (signatureDepth === 0 && char === "{") {
bodyStart = index;
break;
}
}
if (bodyStart === -1) return "";
let depth = 0;
for (let index = bodyStart; index < source.length; index += 1) {
const char = source[index];
if (char === "{") depth += 1;
if (char === "}") {
depth -= 1;
if (depth === 0) return source.slice(match.index, index + 1);
}
}
return "";
}