Files
pikasTech-HWLAB/scripts/src/dev-cloud-workbench-smoke-lib.mjs
T

7175 lines
300 KiB
JavaScript

import fs from "node:fs";
import { execFileSync, spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import http from "node:http";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { buildDevicePodRestPayload } from "../../internal/device-pod/fake-data.mjs";
import {
classifyCodeAgentBrowserJourney,
classifyCodeAgentBrowserFailure,
summarizeCodeAgentPayload
} from "./code-agent-response-contract.mjs";
import { tempReportPath } from "./report-paths.mjs";
export { classifyCodeAgentBrowserJourney };
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const domOnlyReportPath = tempReportPath("dev-cloud-workbench-dom-only.json");
const liveReportPath = tempReportPath("dev-cloud-workbench-live.json");
const webRoot = path.join(repoRoot, "web/hwlab-cloud-web");
const runtime = Object.freeze({
endpoints: Object.freeze({
frontend: "http://74.48.78.17:16666",
api: "http://74.48.78.17:16667",
edge: "http://74.48.78.17:16667"
})
});
const defaultLiveUrl = "http://74.48.78.17:16666/";
const helpOwner = "codex_1779444232735_1";
const legacyFailureWindowMs = 4500;
const codeAgentLongTimeoutMs = 1800000;
const localAgentFixtureDelayMs = 5200;
const sourceFixtureProjectId = "hwlab-device-pod-source-fixture";
const codeAgentE2ePrompts = Object.freeze([
{
id: "simple-chinese",
label: "简单中文提示",
text: "请用一句话说明当前 HWLAB 工作台可以做什么。"
},
{
id: "list-skills",
label: "列出技能提示",
text: "列出你能使用的所有skill"
}
]);
const codeAgentExternalNetworkPrompt = Object.freeze({
id: "github-access",
label: "外部网络提示",
text: "访问一下github看看"
});
const readOnlyRpcMethods = Object.freeze([
"system.health",
"cloud.adapter.describe"
]);
const chineseWorkbenchLabels = Object.freeze([
"云工作台登录",
"HWLAB 云工作台",
"Device Pod",
"设备目标看板",
"Agent 对话",
"事件流",
"内部复核",
"使用说明"
]);
const defaultAuthCredentials = Object.freeze({
username: "admin",
password: "hwlab2026"
});
const defaultHomepageForbiddenTerms = Object.freeze([
"Gate",
"诊断",
"验收",
"M0-M5",
"M3 基本",
"BLOCKED",
"SOURCE",
"DRY-RUN",
"DEV-LIVE",
"db-live",
"m3-hardware-loop-runtime",
"trace/evidence",
"执行轨迹",
"OPENAI_API_KEY",
"Secret",
"secretRef",
"hwlab-code-agent-provider",
"m3-route-required",
"waiting-for-dev-live"
]);
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"]);
const helpRouteAliases = Object.freeze(["/help"]);
const incompletePrimaryNavLabels = Object.freeze(["台", "证", "诊", "帮"]);
const removedResourceExplorerSourceTerms = Object.freeze([
"resource-explorer",
"explorer-toggle",
"explorer-resize",
"resource-tree",
"explorer-capabilities",
"capability-count",
"tree-count",
"data-focus-command",
"quick-actions",
"tree-panel",
"explorer-collapsed",
"--explorer-width",
"--explorer-min-width",
"--explorer-max-width"
]);
const removedResourceExplorerDomSelectors = Object.freeze([
"aside#resource-explorer",
"#resource-explorer",
"#explorer-toggle",
"#explorer-resize",
"#resource-tree",
".resource-tree",
"#explorer-capabilities",
".explorer-capabilities",
"#capability-count",
"#tree-count",
"[data-focus-command]",
".quick-actions",
".tree-panel",
".explorer-collapsed"
]);
const removedResourceExplorerCopyTerms = Object.freeze([
"资源浏览与硬件资源树",
"资源浏览",
"硬件资源树",
"只读工作区",
"常用能力",
"收起资源树",
"展开资源树",
"拖拽调整左侧资源树宽度",
"资源查看",
"查看接线",
"查看状态",
"编写任务"
]);
const workbenchMarkers = Object.freeze([
"data-app-shell",
"workbench-shell",
"activity-rail",
"left-sidebar-toggle",
"center-workspace",
"conversation-list",
"agent-chat-status",
"right-sidebar",
"device-pod-sidebar",
"device-pod-summary",
"device-pod-interfaces",
"device-event-text",
"command-form"
]);
const requiredCodeAgentEvidenceTerms = Object.freeze([
"运行 trace",
"message-trace",
"message-trace-events",
"toolCalls",
"skills",
"runnerTrace",
"conversation",
"trace",
"message",
"providerTrace",
"codex-stdio",
"codex-app-server-stdio-runner",
"codex-app-server-stdio-long-lived",
"repo-owned-codex-app-server-stdio-session",
"codex-app-server-jsonrpc-stdio",
"providerTrace.command",
"providerTrace.terminalStatus",
"message-runtime-path",
"threadId",
"not-codex-stdio",
"not-write-capable",
"process-local-session-registry",
"echo/mock/stub",
"untrusted_completion",
"SOURCE fixture",
"Code Agent SOURCE 回复"
]);
const blockedCodeAgentUiLabels = Object.freeze([
"后端失败",
"服务受阻",
"等待超时",
"Provider 不可用",
"Runner 忙碌",
"Session 受阻",
"Runner 受阻",
"API 错误",
"BLOCKED 凭证缺口"
]);
const forbiddenWritePatterns = Object.freeze([
/callRpc\(\s*["']hardware\./u,
/hardware\.operation\.request/u,
/method:\s*["']hardware\.invoke\.shell["']/u,
/fetchJson\(\s*["'][^"']*hardware\.invoke\.shell/u,
/audit\.event\.write/u,
/evidence\.record\.write/u,
/\/v1\/rpc\//u,
/\/wiring\/apply/u,
/\/wiring\/reload/u,
/--live --confirm-dev --confirmed-non-production/u
]);
const markdownRendererPackages = Object.freeze([
"marked",
"markdown-it",
"micromark",
"remark",
"remark-parse",
"unified",
"commonmark"
]);
const cloudWebAppSourceFiles = Object.freeze([
"src/main.tsx",
"src/App.tsx"
]);
const requiredWebAssets = Object.freeze([
"index.html",
"src/styles/workbench.css",
...cloudWebAppSourceFiles,
"src/components/layout/ActivityRail.tsx",
"src/components/auth/LoginShell.tsx",
"src/components/sessions/SessionSidebar.tsx",
"src/components/conversation/ConversationPanel.tsx",
"src/components/command-bar/CommandBar.tsx",
"src/components/device-pod/DevicePodSidebar.tsx",
"src/components/settings/SettingsView.tsx",
"src/components/skills/SkillsView.tsx",
"src/hooks/useAuth.ts",
"src/services/api/client.ts",
"src/services/markdown/render.ts",
"src/state/workbench.ts",
"src/state/conversation.ts",
"src/types/domain.ts",
"favicon.svg",
"favicon.ico",
"help.md",
"vite.config.ts"
]);
const liveIdentityWebAssets = Object.freeze(["index.html", "assets/index.css", "app.js"]);
const playwrightDependencyCommand = "npm install";
const playwrightDependencySummary =
"Install repository dependencies before running browser smokes: npm install provides the declared playwright package; the Code Queue image may also provide the same package globally.";
export function runDevCloudWorkbenchStaticSmoke() {
return runStaticSmoke();
}
export async function runDevCloudWorkbenchTimeoutFixtureSmoke(options = {}) {
return runLocalAgentFixtureSmoke({
mode: "local-agent-timeout-fixture-browser",
responseDelayMs: options.responseDelayMs ?? 120,
timeoutConfigMs: options.timeoutConfigMs ?? 50,
expectTimeout: true
});
}
export async function runDevCloudWorkbenchSmoke(argv = []) {
const args = parseSmokeArgs(argv);
if (args.mode === "live") return runLiveSmoke(args);
if (args.mode === "auth-fixture") return runDevCloudWorkbenchAuthFixtureSmoke();
if (args.mode === "local-agent-timeout-fixture") return runDevCloudWorkbenchTimeoutFixtureSmoke();
if (args.mode === "dom-only") return runLiveDomOnlySmoke(args);
if (args.mode === "layout") return runDevCloudWorkbenchLayoutSmoke(args);
if (args.mode === "local-agent-fixture") return runDevCloudWorkbenchLocalAgentFixtureSmoke();
if (args.mode === "session-continuity-fixture") return runDevCloudWorkbenchSessionContinuityFixtureSmoke();
if (args.mode === "mobile") return runDevCloudWorkbenchMobileSmoke();
return runStaticSmoke();
}
async function importPlaywright() {
try {
return normalizePlaywrightModule(await import("playwright"));
} catch (localError) {
const globalRoot = npmGlobalRoot();
if (globalRoot) {
try {
return normalizePlaywrightModule(await import(pathToFileURL(path.join(globalRoot, "playwright/index.js")).href));
} catch {
// Fall through to the original local resolution error so the remediation points at package.json.
}
}
throw localError;
}
}
function normalizePlaywrightModule(module) {
if (module?.chromium) return module;
if (module?.default?.chromium) return module.default;
return module;
}
async function launchChromium(chromium) {
try {
return await chromium.launch({ headless: true });
} catch (error) {
const executablePath = systemChromiumExecutablePath();
if (!executablePath || !isMissingPlaywrightBrowser(error)) throw error;
return chromium.launch({ headless: true, executablePath });
}
}
function isMissingPlaywrightBrowser(error) {
return /Executable doesn't exist|Please run the following command to download new browsers|playwright install/iu.test(
error instanceof Error ? error.message : String(error)
);
}
function systemChromiumExecutablePath() {
for (const candidate of ["chromium", "chromium-browser", "google-chrome", "google-chrome-stable"]) {
const found = spawnSync("command", ["-v", candidate], {
encoding: "utf8",
shell: true,
stdio: ["ignore", "pipe", "ignore"]
});
const executablePath = found.stdout.trim().split("\n")[0];
if (found.status === 0 && executablePath) return executablePath;
}
return "";
}
function npmGlobalRoot() {
try {
return execFileSync("npm", ["root", "-g"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
timeout: 5000
}).trim();
} catch {
return "";
}
}
export function parseSmokeArgs(argv) {
const args = { mode: "source", url: defaultLiveUrl, confirmDevLive: false };
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--source" || arg === "--static") {
args.mode = "source";
} else if (arg === "--live") {
args.mode = "live";
} else if (arg === "--confirm-dev-live") {
args.confirmDevLive = true;
} else if (arg === "--dom-only") {
args.mode = "dom-only";
} else if (arg === "--layout") {
args.mode = "layout";
} else if (arg === "--build") {
args.mode = "layout";
args.build = true;
} else if (arg === "--mobile") {
args.mode = "mobile";
args.mobile = true;
} else if (arg === "--local-agent-fixture") {
args.mode = "local-agent-fixture";
} else if (arg === "--session-continuity-fixture") {
args.mode = "session-continuity-fixture";
} else if (arg === "--auth-fixture") {
args.mode = "auth-fixture";
} else if (arg === "--local-agent-timeout-fixture") {
args.mode = "local-agent-timeout-fixture";
} else if (arg === "--url") {
index += 1;
if (!argv[index]) throw new Error("--url requires a value");
args.url = argv[index];
args.urlExplicit = true;
} else if (arg === "--report") {
index += 1;
if (!argv[index]) throw new Error("--report requires a value");
args.reportPath = argv[index];
} else if (arg === "--help") {
args.help = true;
} else {
throw new Error(`unknown argument ${arg}`);
}
}
if (args.mode === "live" && !args.confirmDevLive) {
throw new Error("DEV-LIVE mode requires explicit --live --confirm-dev-live; default/source mode never calls the live provider.");
}
return args;
}
function runStaticSmoke() {
const checks = [];
const blockers = [];
const files = readStaticFiles();
const source = `${files.html}\n${files.app}\n${files.styles}\n${files.packageJson}\n${files.tsconfig}\n${files.checkScript}\n${files.tscCheck}`;
addCheck(checks, blockers, "static-web-assets", assetsExist(), "Cloud Web source assets are present.", {
evidence: requiredWebAssets.map((file) => `web/hwlab-cloud-web/${file}`)
});
addCheck(checks, blockers, "react-shell-only-index", reactShellOnlyIndex(files), "Cloud Web index.html is a shell-only React mount entry.", {
blocker: "runtime_blocker",
evidence: ["<div id=\"root\"></div>", "/src/main.tsx", "no business DOM in index.html"]
});
addCheck(checks, blockers, "react-strict-typescript-entry", reactStrictTypeScriptEntry(files), "React runtime is mounted from TSX under strict TypeScript without explicit any in source.", {
blocker: "runtime_blocker",
evidence: ["createRoot", "React.StrictMode", "strict=true", "noImplicitAny=true", "tsc-check explicit any scan"]
});
addCheck(checks, blockers, "react-module-boundaries", reactModuleBoundaries(files), "React modules are split by layout/auth/sessions/conversation/command-bar/device-pod/settings/shared/hooks/services/state/types/styles.", {
blocker: "contract_blocker",
evidence: requiredWebAssets.filter((file) => file.startsWith("src/"))
});
addCheck(checks, blockers, "legacy-runtime-paths-removed", legacyRuntimePathsRemoved(), "Legacy vanilla runtime files are removed and cannot remain as a second frontend path.", {
blocker: "runtime_blocker",
evidence: legacyRuntimeFiles.map((file) => `removed:${file}`)
});
addCheck(checks, blockers, "react-core-workbench-selectors", reactCoreWorkbenchSelectors(files), "React runtime renders the authenticated shell, command bar, session sidebar, conversation, Device Pod sidebar, settings, skills, gate, and help selectors.", {
blocker: "contract_blocker",
evidence: ["data-app-shell", "#command-input", "#conversation-list", "#device-pod-sidebar", "#logout-button"]
});
addCheck(checks, blockers, "react-api-state-separation", reactApiStateSeparation(files), "API, workspace/session state, conversation mapping, markdown rendering, and UI components are separated.", {
blocker: "contract_blocker",
evidence: ["services/api/client.ts", "state/workbench.ts", "state/conversation.ts", "services/markdown/render.ts"]
});
addCheck(checks, blockers, "vite-build-and-dist-contract", viteBuildAndDistContract(files), "Vite build, route aliases, dist freshness, and root app.js output are the only deployable Cloud Web asset path.", {
blocker: "runtime_blocker",
evidence: ["vite.config.ts", "dist-contract.ts", "app.js", "assets/index.css", ...gateRouteAliases, ...helpRouteAliases]
});
addCheck(checks, blockers, "react-layout-contract", reactLayoutContract(files), "React CSS owns desktop, narrow, and 390px mobile layout without page-level scrolling.", {
blocker: "runtime_blocker",
evidence: ["html/body overflow hidden", "@media (max-width: 720px)", "390px mobile grid", "event stream overscroll contained"]
});
return baseReport({
mode: "source",
status: blockers.length === 0 ? "pass" : "blocked",
checks,
blockers,
evidenceLevel: "SOURCE",
devLive: false,
help: inspectReactHelpContract(files),
safety: staticSafety()
});
}
async function runLiveSmoke(args) {
const checks = [];
const blockers = [];
const sourceIdentity = observeSourceIdentity();
const runtimeIdentity = await observeLiveRuntimeIdentity(runtime.endpoints.api);
const apiRuntimeReadiness = classifyLiveApiRuntimeReadiness(runtimeIdentity);
addCheck(checks, blockers, "live-api-runtime-readiness", apiRuntimeReadiness.status, apiRuntimeReadiness.summary, {
blocker: "runtime_blocker",
evidence: apiRuntimeReadiness.evidence,
observations: apiRuntimeReadiness
});
const expectedRuntimeIdentity = observeExpectedRuntimeIdentity("hwlab-cloud-api");
const deploymentIdentity = classifyLiveDeploymentIdentity(sourceIdentity, runtimeIdentity, expectedRuntimeIdentity);
addCheck(checks, blockers, "live-runtime-current-main", deploymentIdentity.status, deploymentIdentity.summary, {
blocker: "observability_blocker",
evidence: deploymentIdentity.evidence,
observations: deploymentIdentity
});
const http = await fetchText(args.url);
addCheck(checks, blockers, "live-http-html", http.ok && http.status === 200 && /text\/html/u.test(http.contentType) && liveHtmlLooksLikeWorkbench(http.body), "Live URL serves the Cloud Workbench HTML.", {
blocker: "runtime_blocker",
evidence: [args.url, `HTTP ${http.status ?? "none"}`, http.contentType ?? "no content-type"]
});
const webAssetIdentity = http.ok
? await inspectLiveWebAssetIdentity(args.url, http.body)
: {
status: "blocked",
summary: "Live web asset identity could not be checked because the 16666 HTML fetch failed.",
assets: []
};
addCheck(checks, blockers, "live-web-assets-current-main", webAssetIdentity.status, webAssetIdentity.summary, {
blocker: "observability_blocker",
evidence: webAssetIdentity.assets.map((asset) => `${asset.path}: ${asset.status}`),
observations: webAssetIdentity
});
const identityBlocked = deploymentIdentity.status !== "pass" || webAssetIdentity.status !== "pass";
if (identityBlocked) {
addCheck(
checks,
blockers,
"live-code-agent-browser-journey",
"blocked",
"Browser Code Agent journey was not posted because deployed runtime/assets do not match current source identity.",
{
blocker: "observability_blocker",
evidence: [
"POST /v1/agent/chat not sent",
`runtimeIdentity=${deploymentIdentity.status}`,
`webAssetIdentity=${webAssetIdentity.status}`
],
observations: {
request: {
method: "POST",
status: "not_sent",
urlPath: "/v1/agent/chat"
},
response: blockedAgentChatResponse("deployment_identity_preflight"),
classification: {
status: "blocked",
reason: "deployment_identity_preflight",
codeAgentBrowserJourneySkipped: true
}
}
}
);
return baseReport({
mode: "live",
url: args.url,
status: "blocked",
checks,
blockers,
evidenceLevel: "BLOCKED",
devLive: false,
runtimeIdentity,
sourceIdentity,
deploymentIdentity,
expectedRuntimeIdentity,
webAssetIdentity,
safety: {
prodTouched: false,
servicesRestarted: false,
heavyE2E: false,
hardwareWriteApis: false,
sourceIsDevLive: false,
liveMode: "deployment-identity-preflight",
liveConfirmed: args.confirmDevLive === true,
codeAgentBrowserJourneySkipped: true,
prompts: codeAgentE2ePrompts.map((prompt) => ({ id: prompt.id, label: prompt.label })),
legacyFailureWindowMs,
codeAgentTimeoutMs: codeAgentLongTimeoutMs,
retainedApiFields: ["runtime.commitId", "runtime.imageTag", "web asset hash/status"],
statement: "Live smoke blocked before the browser chat journey because deployed runtime/assets do not match the current source identity; it did not post Code Agent chat, call hardware write APIs, restart services, or read secret values."
}
});
}
const dom = nonBlockingLegacyBrowserDiagnostic(
http.ok ? await inspectLiveDom(args.url) : { status: "skip", summary: "Browser DOM check skipped because HTTP fetch failed.", evidence: [] },
"Browser DOM check"
);
addCheck(checks, blockers, "live-browser-dom", dom.status, dom.summary, {
evidence: dom.evidence,
observations: dom.observations
});
const help = nonBlockingLegacyBrowserDiagnostic(
http.ok ? await inspectLiveHelpRoute(args.url) : { status: "skip", summary: "Browser Markdown help check skipped because HTTP fetch failed.", evidence: [] },
"Browser Markdown help check"
);
addCheck(checks, blockers, "live-help-markdown-route", help.status, help.summary, {
evidence: help.evidence,
observations: help.observations
});
const journey = http.ok ? await inspectLiveCodeAgentE2e(args.url) : { status: "skip", summary: "浏览器 Code Agent E2E 因 HTTP fetch 失败而跳过。", evidence: [] };
addCheck(checks, blockers, "live-code-agent-browser-journey", journey.status, journey.summary, {
blocker: ["pass", "degraded"].includes(journey.status) ? null : "runtime_blocker",
evidence: journey.evidence,
observations: journey.observations
});
if (journey.status === "skip") {
blockers.push({
type: "observability_blocker",
scope: "live-code-agent-browser-journey",
status: "open",
summary: journey.summary
});
}
const hasDegradedCheck = checks.some((check) => check.status === "degraded");
const status = blockers.length === 0
? apiRuntimeReadiness.status === "degraded" || hasDegradedCheck ? "degraded" : "pass"
: "blocked";
return baseReport({
mode: "live",
url: args.url,
status,
checks,
blockers,
evidenceLevel: status === "pass"
? "DEV-LIVE-BROWSER"
: status === "degraded" ? "DEV-LIVE-BROWSER-DEGRADED" : "BLOCKED",
devLive: status === "pass",
runtimeIdentity,
sourceIdentity,
deploymentIdentity,
expectedRuntimeIdentity,
webAssetIdentity,
safety: {
prodTouched: false,
servicesRestarted: false,
heavyE2E: false,
hardwareWriteApis: false,
sourceIsDevLive: false,
liveMode: "browser-user-journey-with-code-agent-post",
liveConfirmed: args.confirmDevLive === true,
prompts: codeAgentE2ePrompts.map((prompt) => ({ id: prompt.id, label: prompt.label })),
legacyFailureWindowMs,
codeAgentTimeoutMs: codeAgentLongTimeoutMs,
retainedApiFields: ["status", "provider", "model", "backend", "traceId", "hasReply", "error.code", "error.missingEnv", "error.providerStatus"],
statement: status === "degraded"
? "Live smoke records deployed UI usable in degraded/read-only mode only; API health/runtime durability is degraded, so this is not full DEV-LIVE acceptance and must not be used to infer Device Pod hardware acceptance."
: "Live smoke opens the deployed workbench and sends two controlled Code Agent messages; it does not call Device Pod or hardware write APIs."
}
});
}
async function runLiveDomOnlySmoke(args) {
const checks = [];
const blockers = [];
const sourceIdentity = observeSourceIdentity();
const runtimeIdentity = await observeLiveRuntimeIdentity(runtime.endpoints.api);
const apiRuntimeReadiness = classifyLiveApiRuntimeReadiness(runtimeIdentity);
addCheck(checks, blockers, "live-api-runtime-readiness", apiRuntimeReadiness.status, apiRuntimeReadiness.summary, {
blocker: "runtime_blocker",
evidence: apiRuntimeReadiness.evidence,
observations: apiRuntimeReadiness
});
const expectedRuntimeIdentity = observeExpectedRuntimeIdentity("hwlab-cloud-api");
const deploymentIdentity = classifyLiveDeploymentIdentity(sourceIdentity, runtimeIdentity, expectedRuntimeIdentity);
addCheck(checks, blockers, "live-runtime-current-main", deploymentIdentity.status, deploymentIdentity.summary, {
blocker: "observability_blocker",
evidence: deploymentIdentity.evidence,
observations: deploymentIdentity
});
const http = await fetchText(args.url);
addCheck(checks, blockers, "live-http-html", http.ok && http.status === 200 && /text\/html/u.test(http.contentType) && liveHtmlLooksLikeWorkbench(http.body), "Live URL serves the Cloud Workbench HTML.", {
blocker: "runtime_blocker",
evidence: [args.url, `HTTP ${http.status ?? "none"}`, http.contentType ?? "no content-type"]
});
const webAssetIdentity = http.ok
? await inspectLiveWebAssetIdentity(args.url, http.body)
: {
status: "blocked",
summary: "Live web asset identity could not be checked because the 16666 HTML fetch failed.",
assets: [],
mismatches: ["index.html"]
};
addCheck(checks, blockers, "live-web-assets-current-main", webAssetIdentity.status, webAssetIdentity.summary, {
blocker: "observability_blocker",
evidence: webAssetIdentity.assets.length > 0
? webAssetIdentity.assets.map((asset) => `${asset.path}: ${asset.status}`)
: ["web asset identity not observed"],
observations: webAssetIdentity
});
const dom = nonBlockingLegacyBrowserDiagnostic(
http.ok ? await inspectLiveDom(args.url, { noCodeAgentPost: true }) : { status: "skip", summary: "Browser DOM check skipped because HTTP fetch failed.", evidence: [] },
"Browser DOM check"
);
addCheck(checks, blockers, "live-browser-dom", dom.status, dom.summary, {
evidence: dom.evidence,
observations: dom.observations
});
const gate = nonBlockingLegacyBrowserDiagnostic(
http.ok ? await inspectLiveGateRoute(args.url, { noCodeAgentPost: true }) : { status: "skip", summary: "Browser Gate route check skipped because HTTP fetch failed.", evidence: [] },
"Browser Gate route check"
);
addCheck(checks, blockers, "live-gate-route", gate.status, gate.summary, {
evidence: gate.evidence,
observations: gate.observations
});
const help = nonBlockingLegacyBrowserDiagnostic(
http.ok ? await inspectLiveHelpRoute(args.url, { noCodeAgentPost: true }) : { status: "skip", summary: "Browser Markdown help check skipped because HTTP fetch failed.", evidence: [] },
"Browser Markdown help check"
);
addCheck(checks, blockers, "live-help-markdown-route", help.status, help.summary, {
evidence: help.evidence,
observations: help.observations
});
addDomOnlyCodeAgentCheck(checks);
const status = blockers.length === 0 ? "pass" : "blocked";
return baseReport({
mode: "dom-only",
url: args.url,
status,
checks,
blockers,
evidenceLevel: status === "pass" ? "DEV-LIVE-DOM-READONLY" : "BLOCKED",
devLive: false,
runtimeIdentity,
sourceIdentity,
deploymentIdentity,
expectedRuntimeIdentity,
webAssetIdentity,
safety: {
prodTouched: false,
servicesRestarted: false,
heavyE2E: false,
hardwareWriteApis: false,
sourceIsDevLive: false,
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", "gate single-table observations"],
statement: "DOM-only live smoke preserves runtime and web-asset identity preflight semantics; legacy browser DOM/help/gate probes are recorded as non-blocking diagnostics and never post /v1/agent/chat, read Secret values, mutate live DEV, or claim M3/M4/M5 acceptance."
}
});
}
export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
const useLiveUrl = args.live === true || args.urlExplicit === true;
const layoutSourceMode = useLiveUrl ? "dev-live" : "local-build";
const artifactRoot = path.resolve(
repoRoot,
args.artifactDir ?? path.join("tmp", "dev-cloud-workbench-layout-smoke", layoutArtifactTimestamp())
);
const layoutViewports = workbenchLayoutViewports();
let chromium;
try {
({ chromium } = await importPlaywright());
} catch (error) {
const summary = `Workbench layout smoke skipped because Playwright is unavailable: ${error.message}`;
return sanitizeLayoutReport({
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
$id: reportModeId("layout"),
reportVersion: "v1",
status: "skip",
issue: "pikasTech/HWLAB#273",
taskId: "dev-cloud-workbench-layout",
commitId: observeSourceIdentity().reportCommitId,
acceptanceLevel: "dev_cloud_workbench_layout",
devOnly: true,
prodDisabled: true,
reportLifecycle: layoutReportLifecycle(useLiveUrl, "blocked"),
task: "DC-DCSN-P0-2026-003",
mode: "layout-browser",
sourceMode: layoutSourceMode,
url: useLiveUrl ? args.url : null,
generatedAt: new Date().toISOString(),
sourceContract: layoutSourceContract(),
validationCommands: layoutValidationCommands(),
localSmoke: layoutLocalSmoke("skip"),
dryRun: layoutDryRun(),
devPreconditions: layoutDevPreconditions(useLiveUrl, "blocked"),
evidenceLevel: useLiveUrl ? "BLOCKED" : "SOURCE",
devLive: false,
summary,
viewports: layoutViewports.map(({ id, width, height }) => ({ id, width, height })),
checks: [
{
id: "layout-browser-dependency",
status: "skip",
summary: playwrightDependencySummary,
evidence: [playwrightDependencyCommand, "package.json dependencies.playwright"]
}
],
blockers: [
{
type: "environment_blocker",
scope: "layout-browser-dependency",
status: "open",
summary
}
],
safety: staticSafety(),
dependency: {
package: "playwright",
declaredIn: "package.json",
installCommand: playwrightDependencyCommand
},
artifacts: {
screenshotDir: artifactRoot,
reportPath: args.reportPath ?? null
},
safety: layoutSafety()
});
}
if (!useLiveUrl && args.build === true) {
execFileSync(resolveBunCommand(), ["run", "web/hwlab-cloud-web/scripts/build.ts"], {
cwd: repoRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
timeout: 30000
});
}
await fs.promises.mkdir(artifactRoot, { recursive: true });
const server = useLiveUrl
? null
: await startStaticWebServer({
rootDir: args.build === true ? path.join(webRoot, "dist") : webRoot,
authFixture: true,
liveBuildsFixture: true
});
const url = useLiveUrl ? args.url : server.url;
let browser;
try {
browser = await launchChromium(chromium);
const checks = [];
const screenshots = [];
for (const viewport of layoutViewports) {
const check = await inspectMinimalWorkbenchLayout(browser, url, viewport, { artifactRoot });
checks.push(check);
screenshots.push(...(check.artifacts?.screenshots ?? []));
}
const blockers = checks
.filter((check) => check.status !== "pass")
.filter((check) => check.status !== "skip")
.map((check) => ({
type: "runtime_blocker",
scope: check.id,
status: "open",
summary: check.summary,
viewport: check.viewport ?? null,
selector: firstFailure(check)?.selector ?? null,
failureType: firstFailure(check)?.failureType ?? "blocked"
}));
const failures = checks.flatMap((check) =>
(check.observations?.failures ?? []).map((failure) => ({
checkId: check.id,
viewport: check.viewport ?? failure.viewport ?? null,
selector: failure.selector ?? null,
failureType: failure.failureType ?? "blocked",
summary: failure.summary ?? check.summary,
...failure
}))
);
const skipped = [];
return sanitizeLayoutReport({
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
$id: reportModeId("layout"),
reportVersion: "v1",
status: blockers.length === 0 ? "pass" : "blocked",
issue: "pikasTech/HWLAB#273",
taskId: "dev-cloud-workbench-layout",
commitId: observeSourceIdentity().reportCommitId,
acceptanceLevel: "dev_cloud_workbench_layout",
devOnly: true,
prodDisabled: true,
reportLifecycle: layoutReportLifecycle(useLiveUrl, blockers.length === 0 ? "pass" : "blocked"),
task: "DC-DCSN-P0-2026-003",
mode: "layout-browser",
sourceMode: layoutSourceMode,
url,
generatedAt: new Date().toISOString(),
sourceContract: layoutSourceContract(),
validationCommands: layoutValidationCommands(),
localSmoke: layoutLocalSmoke(blockers.length === 0 ? "pass" : "blocked"),
dryRun: layoutDryRun(),
devPreconditions: layoutDevPreconditions(useLiveUrl, blockers.length === 0 ? "pass" : "blocked"),
evidenceLevel: useLiveUrl ? blockers.length === 0 ? "DEV-LIVE-LAYOUT" : "BLOCKED" : "SOURCE",
devLive: false,
summary: useLiveUrl
? "Live browser layout smoke verifies that the workbench opens and core controls are visible."
: "Vite local-build browser layout smoke verifies that the workbench opens and core controls are visible; detailed visual acceptance remains manual/live.",
refs: ["pikasTech/HWLAB#273"],
viewports: layoutViewports.map(({ id, width, height }) => ({ id, width, height })),
checks,
blockers,
failures,
skipped,
artifacts: {
screenshotDir: artifactRoot,
screenshots,
reportPath: args.reportPath ?? null
},
safety: layoutSafety()
});
} catch (error) {
if (isBrowserExecutionEnvironmentError(error)) {
const summary = `Workbench layout smoke skipped because the browser could not run: ${error.message}`;
return sanitizeLayoutReport({
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
$id: reportModeId("layout"),
reportVersion: "v1",
status: "skip",
issue: "pikasTech/HWLAB#273",
taskId: "dev-cloud-workbench-layout",
commitId: observeSourceIdentity().reportCommitId,
acceptanceLevel: "dev_cloud_workbench_layout",
devOnly: true,
prodDisabled: true,
reportLifecycle: layoutReportLifecycle(useLiveUrl, "blocked"),
task: "DC-DCSN-P0-2026-003",
mode: "layout-browser",
sourceMode: layoutSourceMode,
url,
generatedAt: new Date().toISOString(),
sourceContract: layoutSourceContract(),
validationCommands: layoutValidationCommands(),
localSmoke: layoutLocalSmoke("skip"),
dryRun: layoutDryRun(),
devPreconditions: layoutDevPreconditions(useLiveUrl, "blocked"),
evidenceLevel: useLiveUrl ? "BLOCKED" : "SOURCE",
devLive: false,
summary,
viewports: layoutViewports.map(({ id, width, height }) => ({ id, width, height })),
checks: [
{
id: "layout-browser-runtime",
status: "skip",
summary,
evidence: ["browser launch failed", "use G14 k3s/CI Playwright base image for browser smoke"]
}
],
blockers: [
{
type: "environment_blocker",
scope: "layout-browser-runtime",
status: "open",
summary
}
],
artifacts: {
screenshotDir: artifactRoot,
reportPath: args.reportPath ?? null
},
safety: layoutSafety()
});
}
return sanitizeLayoutReport({
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
$id: reportModeId("layout"),
reportVersion: "v1",
status: "blocked",
issue: "pikasTech/HWLAB#273",
taskId: "dev-cloud-workbench-layout",
commitId: observeSourceIdentity().reportCommitId,
acceptanceLevel: "dev_cloud_workbench_layout",
devOnly: true,
prodDisabled: true,
reportLifecycle: layoutReportLifecycle(useLiveUrl, "blocked"),
task: "DC-DCSN-P0-2026-003",
mode: "layout-browser",
sourceMode: layoutSourceMode,
url,
generatedAt: new Date().toISOString(),
sourceContract: layoutSourceContract(),
validationCommands: layoutValidationCommands(),
localSmoke: layoutLocalSmoke("blocked"),
dryRun: layoutDryRun(),
devPreconditions: layoutDevPreconditions(useLiveUrl, "blocked"),
evidenceLevel: useLiveUrl ? "BLOCKED" : "SOURCE",
devLive: false,
summary: `Workbench layout smoke failed: ${error.message}`,
viewports: layoutViewports.map(({ id, width, height }) => ({ id, width, height })),
checks: [],
blockers: [
{
type: "runtime_blocker",
scope: "layout-browser-smoke",
status: "open",
summary: error.message,
selector: null,
failureType: "blocked",
viewport: null
}
],
failures: [
{
selector: null,
failureType: "blocked",
viewport: null,
summary: error.message
}
],
artifacts: {
screenshotDir: artifactRoot,
screenshots: [],
reportPath: args.reportPath ?? null
},
safety: layoutSafety()
});
} finally {
if (browser) await browser.close();
if (server) await server.close();
}
}
function workbenchLayoutViewports() {
return [
{ id: "desktop", width: 1366, height: 768, isMobile: false },
{ id: "narrow-desktop", width: 1024, height: 768, isMobile: false },
{ id: "mobile", width: 390, height: 844, isMobile: true }
];
}
function layoutSafety() {
return {
...staticSafety(),
sourceIsDevLive: false,
layoutOnly: true,
codeAgentPostSent: false,
hardwareWriteApis: false,
hitTestMethod: "document.elementsFromPoint plus normal Playwright clicks without forced clicks",
statement: "Layout smoke opens the workbench, uses real Playwright clicks for sidebar controls, and samples hit targets with elementsFromPoint. It does not send Code Agent chat, call Device Pod or hardware write APIs, mutate DEV, or claim hardware acceptance."
};
}
function sanitizeLayoutReport(report) {
return stripLayoutObservationNoise(report);
}
function stripLayoutObservationNoise(value) {
if (Array.isArray(value)) return value.map(stripLayoutObservationNoise);
if (!value || typeof value !== "object") return value;
const cleaned = {};
const looksLikeElementBox =
Number.isFinite(value.left) &&
Number.isFinite(value.top) &&
Number.isFinite(value.right) &&
Number.isFinite(value.bottom) &&
Number.isFinite(value.width) &&
Number.isFinite(value.height);
for (const [key, item] of Object.entries(value)) {
if (looksLikeElementBox && key === "text") continue;
cleaned[key] = stripLayoutObservationNoise(item);
}
return cleaned;
}
function addDomOnlyCodeAgentCheck(checks) {
checks.push({
id: "live-code-agent-browser-journey",
status: "not_applicable",
summary: "DOM-only mode never posts the browser Code Agent journey; /v1/agent/chat is not sent.",
evidence: [
"POST /v1/agent/chat not_applicable",
"POST /v1/agent/chat not sent",
"DOM-only read-only mode"
],
observations: {
request: {
method: "POST",
status: "not_applicable",
urlPath: "/v1/agent/chat"
},
response: {
status: "not_applicable",
provider: "not_observed",
model: "not_observed",
backend: "not_observed",
traceId: "not_observed",
hasReply: false,
error: null
},
classification: {
status: "not_applicable",
reason: "dom_only_no_code_agent_post",
codeAgentBrowserJourneySkipped: true
},
networkEvents: []
}
});
}
function nonBlockingLegacyBrowserDiagnostic(result, label) {
if (result?.status === "pass") return result;
const originalStatus = typeof result?.status === "string" ? result.status : "unknown";
return {
...result,
status: "skip",
summary: `${label} recorded as a non-blocking legacy browser diagnostic; original status=${originalStatus}; ${result?.summary ?? "no summary"}`,
evidence: result?.evidence ?? [],
observations: {
...(result?.observations && typeof result.observations === "object" ? result.observations : {}),
legacyBrowserDiagnostic: {
originalStatus,
blockerDisabled: true
}
}
};
}
function layoutReportLifecycle(useLiveUrl, status) {
return {
version: "v1",
state: "active",
activeEndpoint: runtime.endpoints.api,
activeBrowserEndpoint: runtime.endpoints.frontend,
deprecatedEndpoint: null,
summary: useLiveUrl
? `DEV live layout smoke ${status}; this is UI layout/clickability evidence only and not hardware acceptance.`
: `Repository Vite local-build layout smoke ${status}; this runs without public DEV dependency and cannot be used as DEV-LIVE evidence.`
};
}
function layoutSourceContract() {
return {
status: "pass",
documents: [
"docs/reference/cloud-workbench.md",
"docs/reference/code-agent-chat-readiness.md"
],
summary: "Cloud Workbench layout smoke protects the #99 workbench route, #278 left navigation collapse/expand, #352 resource-explorer removal, Code Agent input, right Device Pod summary board, /gate current route, and outer-scroll lock without claiming hardware acceptance."
};
}
function layoutValidationCommands() {
return [
"node --check scripts/dev-cloud-workbench-layout-smoke.mjs",
"node --check scripts/src/dev-cloud-workbench-smoke-lib.mjs",
"npm run web:check",
"npm run web:layout",
"npm run web:layout:build",
"npm run web:layout:live"
];
}
function layoutLocalSmoke(status) {
return {
status,
commands: [
"npm run web:check",
"npm run web:layout",
"npm run web:layout:build"
],
evidence: [
"web:check runs strict React TypeScript, module-boundary, dist freshness, and unit checks as the repo-owned frontend gate.",
"web:layout runs the Vite local-build Playwright geometry and core-control visibility checks without public DEV dependency.",
"web:layout:build is the same Vite local-build browser smoke entry."
],
summary: "Vite local-build layout checks classify hidden core controls and browser startup regressions as layout blockers."
};
}
function layoutDryRun() {
return {
status: "not_applicable",
commands: ["npm run web:layout"],
evidence: ["Layout smoke is browser evidence, not a dry-run substitute."],
summary: "No dry-run mode is accepted for layout/clickability evidence."
};
}
function layoutDevPreconditions(useLiveUrl, status) {
return {
status: useLiveUrl ? status : "not_applicable",
requirements: [
"SOURCE/static PR checks must pass without public DEV dependency.",
"local-build mode must rebuild and inspect Cloud Web dist before publish/apply.",
"DEV live mode must target http://74.48.78.17:16666/ after a controlled DEV deployment or confirmed live revision.",
"The report must include viewport, selector, failureType, and artifact/report paths for failures.",
"Layout pass is not hardware acceptance and does not replace Device Pod runtime evidence."
],
commands: ["npm run web:layout:live"],
summary: useLiveUrl
? "DEV live layout smoke was executed as UI layout/clickability evidence only."
: "DEV live layout smoke is a post-deploy check and is intentionally not required for SOURCE/static gates."
};
}
function baseReport({
mode,
status,
checks,
blockers,
evidenceLevel,
devLive,
help = undefined,
safety,
url = null,
runtimeIdentity = null,
sourceIdentity = observeSourceIdentity(),
deploymentIdentity = null,
expectedRuntimeIdentity = null,
webAssetIdentity = null
}) {
const liveJourneyPassed = mode === "live" && status === "pass";
const liveJourneyDegraded = mode === "live" && status === "degraded";
const sourceOnlyMode = mode === "source";
return {
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
$id: reportModeId(mode),
reportVersion: "v1",
status,
issue: "pikasTech/HWLAB#7",
taskId: "dev-cloud-workbench-live",
commitId: sourceIdentity.reportCommitId,
acceptanceLevel: sourceOnlyMode ? "dev_cloud_workbench_source" : "dev_cloud_workbench_live",
devOnly: true,
prodDisabled: true,
reportLifecycle: {
version: "v1",
state: "active",
activeEndpoint: runtime.endpoints.api,
activeBrowserEndpoint: runtime.endpoints.frontend,
deprecatedEndpoint: null,
summary: sourceOnlyMode
? "SOURCE-only Workbench contract report; it does not call the live provider and cannot be used as DEV-LIVE evidence."
: liveJourneyDegraded
? "Current deployed browser user journey report for the 16666 Cloud Workbench; deployed UI usable in degraded/read-only mode and does not substitute for M3/M4/M5 DEV-LIVE acceptance."
: "Current deployed browser user journey report for the 16666 Cloud Workbench; it does not substitute for M3/M4/M5 hardware acceptance."
},
task: "DC-DCSN-P0-2026-003",
refs: [
"pikasTech/HWLAB#7",
"pikasTech/HWLAB#108",
"pikasTech/HWLAB#99",
"pikasTech/HWLAB#78",
"pikasTech/HWLAB#117",
"pikasTech/HWLAB#118",
"pikasTech/HWLAB#119",
"pikasTech/HWLAB#120",
"pikasTech/HWLAB#131",
"pikasTech/HWLAB#143",
"pikasTech/HWLAB#164",
"pikasTech/HWLAB#224",
"pikasTech/HWLAB#276",
"pikasTech/HWLAB#288"
],
mode,
url,
generatedAt: new Date().toISOString(),
evidenceLevel,
devLive,
sourceIdentity,
runtimeIdentity: runtimeIdentity ?? notObservedRuntimeIdentity("live runtime identity was not observed in this smoke mode"),
...(expectedRuntimeIdentity ? { expectedRuntimeIdentity } : {}),
...(deploymentIdentity ? { deploymentIdentity } : {}),
...(webAssetIdentity ? { webAssetIdentity } : {}),
sourceContract: {
status: "pass",
documents: [
"docs/reference/cloud-workbench.md",
"docs/reference/code-agent-chat-readiness.md"
],
summary: sourceOnlyMode
? "SOURCE mode verifies repository Workbench and Code Agent readiness contracts without calling the live provider."
: "The deployed browser journey is anchored to the Cloud Workbench and Code Agent readiness contracts."
},
validationCommands: [
"node --check scripts/dev-cloud-workbench-smoke.mjs",
"node --check scripts/src/dev-cloud-workbench-smoke-lib.mjs",
"node scripts/dev-cloud-workbench-smoke.mjs --source",
"node scripts/dev-cloud-workbench-smoke.mjs --layout",
"node scripts/dev-cloud-workbench-smoke.mjs --mobile",
"node scripts/dev-cloud-workbench-smoke.mjs --local-agent-fixture",
"node scripts/dev-cloud-workbench-smoke.mjs --session-continuity-fixture",
"node scripts/dev-cloud-workbench-smoke.mjs --local-agent-timeout-fixture",
"node scripts/dev-cloud-workbench-smoke.mjs --layout --url http://74.48.78.17:16666/",
`node scripts/dev-cloud-workbench-smoke.mjs --dom-only --url http://74.48.78.17:16666/ --report ${domOnlyReportPath}`,
`node scripts/dev-cloud-workbench-smoke.mjs --live --confirm-dev-live --url http://74.48.78.17:16666/ --report ${liveReportPath}`,
mode === "dom-only"
? `node scripts/validate-dev-gate-report.mjs ${domOnlyReportPath}`
: `node scripts/validate-dev-gate-report.mjs ${liveReportPath}`,
"node scripts/validate-dev-gate-report.mjs"
],
localSmoke: {
status: mode === "source" ? status : "not_run",
commands: [
"node scripts/dev-cloud-workbench-smoke.mjs --source",
"node scripts/dev-cloud-workbench-smoke.mjs --layout",
"node scripts/dev-cloud-workbench-smoke.mjs --mobile",
"node scripts/dev-cloud-workbench-smoke.mjs --local-agent-fixture",
"node scripts/dev-cloud-workbench-smoke.mjs --session-continuity-fixture",
"node scripts/dev-cloud-workbench-smoke.mjs --local-agent-timeout-fixture"
],
evidence: ["Source mode verifies the source contract and same-origin Code Agent wiring only.", "Layout browser mode verifies desktop/mobile sidebar collapse, hit targets, non-overlap, and outer-scroll lock at SOURCE level.", "Mobile browser mode verifies the 390x844 outer-scroll lock and help route at SOURCE level.", "Local fixture browser mode verifies both Code Agent prompts past the old 4500ms window at SOURCE level.", "Session continuity fixture verifies same-browser conversation/session/thread reuse and retry request context at SOURCE level.", "Local timeout fixture verifies bounded timeout UI classification without live acceptance."],
summary: "Source, layout, mobile, and local fixture workbench smokes are SOURCE-level evidence and do not prove the deployed browser journey."
},
dryRun: {
status: "not_applicable",
commands: ["node scripts/dev-cloud-workbench-smoke.mjs --source"],
evidence: ["No dry-run substitute is accepted for the deployed browser journey."],
summary: "The journey evidence is collected from the real DEV browser endpoint, not a dry-run fixture."
},
devPreconditions: {
status: liveJourneyPassed ? "pass" : liveJourneyDegraded ? "degraded" : "blocked",
requirements: [
"GET http://74.48.78.17:16666/ serves the Cloud Workbench HTML.",
"Browser DOM exposes the default workbench and core controls.",
"DOM-only mode may inspect read-only deployed DOM wiring after identity preflight blocks, but must not send Code Agent chat.",
"A controlled same-origin POST /v1/agent/chat returns completed UI-visible evidence or reports a blocker.",
"No hardware write API, service restart, PROD access, or secret read is performed."
],
commands: [
`node scripts/dev-cloud-workbench-smoke.mjs --dom-only --url http://74.48.78.17:16666/ --report ${domOnlyReportPath}`,
`node scripts/dev-cloud-workbench-smoke.mjs --live --confirm-dev-live --url http://74.48.78.17:16666/ --report ${liveReportPath}`
],
summary: liveJourneyPassed
? "Deployed 16666 browser journey and same-origin Code Agent chat completed."
: liveJourneyDegraded
? "Deployed UI usable in degraded/read-only mode; API health/runtime durability is degraded, so this is not full DEV-LIVE acceptance."
: mode === "dom-only"
? "Deployed browser DOM was inspected read-only; Code Agent chat was not posted."
: mode === "live"
? "Deployed browser journey is blocked; see checks and blockers."
: "Static/local fixture evidence does not establish the deployed 16666 browser journey."
},
endpoints: {
frontend: runtime.endpoints.frontend,
api: runtime.endpoints.api,
edge: runtime.endpoints.edge
},
checks,
blockers,
help,
safety
};
}
function reportModeId(mode) {
return mode === "layout"
? "https://hwlab.pikastech.local/dev-gate/dev-cloud-workbench-layout.json"
: mode === "dom-only"
? "https://hwlab.pikastech.local/dev-gate/dev-cloud-workbench-dom-only.json"
: "https://hwlab.pikastech.local/dev-gate/dev-cloud-workbench-live.json";
}
function addCheck(checks, blockers, id, result, summary, options = {}) {
const status = result === true ? "pass" : result === false ? "blocked" : result;
const check = {
id,
status,
summary,
...(options.owner ? { owner: options.owner } : {}),
...(options.evidence ? { evidence: options.evidence } : {}),
...(options.observations ? { observations: options.observations } : {})
};
checks.push(check);
if (status === "blocked" && options.blocker) {
blockers.push({
type: options.blocker,
scope: id,
status: "open",
summary
});
}
return check;
}
function readStaticFiles() {
return {
html: readText("web/hwlab-cloud-web/index.html"),
styles: readText("web/hwlab-cloud-web/src/styles/workbench.css"),
app: readCloudWebAppSource(),
packageJson: readText("web/hwlab-cloud-web/package.json"),
tsconfig: readText("web/hwlab-cloud-web/tsconfig.json"),
checkScript: readText("web/hwlab-cloud-web/scripts/check.ts"),
tscCheck: readText("web/hwlab-cloud-web/scripts/tsc-check.ts"),
distContract: readText("web/hwlab-cloud-web/scripts/dist-contract.ts"),
viteConfig: readText("web/hwlab-cloud-web/vite.config.ts"),
help: readText("web/hwlab-cloud-web/help.md"),
artifactPublisher: readText("scripts/artifact-publish.mjs"),
cloudApiServer: readText("internal/cloud/server.ts"),
cloudApiPayloads: readText("internal/cloud/server-rest-payloads.ts"),
devicePodData: readText("internal/device-pod/fake-data.mjs"),
devicePodService: readText("cmd/hwlab-device-pod/main.ts"),
protocol: readText("internal/protocol/index.mjs"),
deployJson: readText("deploy/deploy.json"),
artifactCatalog: readText("deploy/artifact-catalog.dev.json")
};
}
const legacyRuntimeFiles = Object.freeze([
"app.ts",
"app-device-pod.ts",
"app-conversation.ts",
"app-helpers.ts",
"app-skills.ts",
"app-session-tabs.ts",
"app-trace.ts",
"auth.ts",
"code-agent-facts.ts",
"code-agent-status.ts",
"composer-policy.ts",
"live-status.ts",
"message-markdown.ts",
"runtime.ts",
"styles.css",
"web-types.d.ts"
]);
function reactShellOnlyIndex({ html }) {
return /<div id="root"><\/div>/u.test(html) &&
/src="\/src\/main\.tsx"/u.test(html) &&
!/workbench-shell|device-pod-sidebar|command-input|conversation-list|session-tabs/u.test(html);
}
function reactStrictTypeScriptEntry({ app, tsconfig, tscCheck }) {
return /createRoot/u.test(app) &&
/React\.StrictMode/u.test(app) &&
/"strict"\s*:\s*true/u.test(tsconfig) &&
/"noImplicitAny"\s*:\s*true/u.test(tsconfig) &&
/"noUncheckedIndexedAccess"\s*:\s*true/u.test(tsconfig) &&
/explicit any/u.test(tscCheck) &&
!/(^|[^A-Za-z0-9_])(?:as\s+any|:\s*any\b|Array\s*<\s*any\s*>|<\s*any\s*>)/u.test(app);
}
function reactModuleBoundaries() {
return requiredWebAssets
.filter((file) => file.startsWith("src/"))
.every((file) => fs.existsSync(path.join(webRoot, file)));
}
function legacyRuntimePathsRemoved() {
return legacyRuntimeFiles.every((file) => !fs.existsSync(path.join(webRoot, file)));
}
function reactCoreWorkbenchSelectors({ app }) {
return [
"data-app-shell",
"id=\"command-input\"",
"id=\"conversation-list\"",
"id=\"device-pod-sidebar\"",
"id=\"device-event-scroll\"",
"id=\"logout-button\"",
"data-view=\"gate\"",
"data-view=\"help\"",
"SkillsView",
"SettingsView"
].every((term) => app.includes(term));
}
function reactApiStateSeparation({ app }) {
return [
"services/api/client.ts",
"state/workbench.ts",
"state/conversation.ts",
"services/markdown/render.ts"
].every((file) => fs.existsSync(path.join(webRoot, "src", file))) &&
/fetchJson/u.test(app) &&
/useWorkbenchStore/u.test(app) &&
/renderMessageMarkdown/u.test(app) &&
/selectConversation/u.test(app);
}
function viteBuildAndDistContract({ packageJson, distContract, viteConfig }) {
return /"build"\s*:\s*"bun run scripts\/build\.ts"/u.test(packageJson) &&
/vite.*build/u.test(distContract) &&
/entryFileNames:\s*"app\.js"/u.test(viteConfig) &&
/assets\/index\.css/u.test(distContract) &&
gateRouteAliases.every((route) => distContract.includes(`${route.replace(/^\//u, "")}/index.html`)) &&
helpRouteAliases.every((route) => distContract.includes(`${route.replace(/^\//u, "")}/index.html`));
}
function reactLayoutContract({ styles }) {
return /html, body, #root \{[^}]*overflow:\s*hidden;/su.test(styles) &&
/\.workbench-shell \{[^}]*overflow:\s*hidden;/su.test(styles) &&
/@media \(max-width: 720px\)/u.test(styles) &&
/grid-template-columns:\s*56px\s+minmax\(0,\s*1fr\)/u.test(styles) &&
/\.device-event-scroll\s*\{[\s\S]*?overscroll-behavior:\s*contain;/u.test(styles);
}
function inspectReactHelpContract({ app, help }) {
return {
status: /data-view="help"/u.test(app) && /fetchText\("\/help\.md"/u.test(app) && help.trim().length > 0 ? "pass" : "blocked",
route: "/help",
source: "web/hwlab-cloud-web/help.md"
};
}
function readText(relativePath) {
return fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
}
function readCloudWebAppSource() {
return collectCloudWebSourceFiles(path.join(webRoot, "src"))
.map((file) => fs.readFileSync(file, "utf8"))
.join("\n");
}
function collectCloudWebSourceFiles(dir) {
const files = [];
for (const entry of fs.readdirSync(dir)) {
const full = path.join(dir, entry);
const stat = fs.statSync(full);
if (stat.isDirectory()) files.push(...collectCloudWebSourceFiles(full));
else if (/\.(ts|tsx|css)$/u.test(full)) files.push(full);
}
return files.sort();
}
function resolveBunCommand() {
for (const candidate of [process.env.HWLAB_BUN_BINARY, process.env.BUN_BINARY, "/root/.bun/bin/bun", "bun"]) {
if (typeof candidate === "string" && candidate.trim()) return candidate;
}
return "bun";
}
function observeSourceIdentity() {
const head = runGit(["rev-parse", "--verify", "HEAD"]);
const shortHead = head ? runGit(["rev-parse", "--short=12", "HEAD"]) : null;
const ref = runGit(["rev-parse", "--abbrev-ref", "HEAD"]) ?? process.env.GITHUB_HEAD_REF ?? process.env.GITHUB_REF_NAME ?? "unknown";
const statusOutput = runGit(["status", "--porcelain"]);
const envIdentity = head ? null : environmentCommitIdentity();
const commitId = head ?? envIdentity?.commitId ?? "unknown";
const shortCommitId = shortHead ?? envIdentity?.commitId.slice(0, 12) ?? "unknown";
const dirty = statusOutput === null ? null : statusOutput.length > 0;
const worktreeState = dirty === null ? "unknown" : dirty ? "dirty" : "clean";
const source = head ? "git-head" : envIdentity ? "environment" : "unknown";
const reportCommitId = commitId !== "unknown" && worktreeState === "clean" ? shortCommitId : "unknown";
return {
status: commitId === "unknown" ? "unknown" : "observed",
source,
...(envIdentity ? { environmentKey: envIdentity.key } : {}),
commitId,
shortCommitId,
ref,
worktreeState,
dirty,
reportCommitId,
summary: worktreeState !== "clean"
? "Source HEAD was observed, but the worktree was not cleanly attributable when the report was generated; top-level commitId is unknown rather than pretending to identify uncommitted source."
: "Source identity was derived from the current worktree or source environment and is separate from the live runtime identity."
};
}
function environmentCommitIdentity() {
for (const key of [
"GITHUB_SHA",
"CI_COMMIT_SHA",
"BUILDKITE_COMMIT",
"CODEBUILD_RESOLVED_SOURCE_VERSION",
"SOURCE_COMMIT",
"HWLAB_SOURCE_COMMIT_ID"
]) {
const value = process.env[key];
if (typeof value === "string" && /^[a-f0-9]{7,40}$/iu.test(value.trim())) {
return { key, commitId: value.trim().toLowerCase() };
}
}
return null;
}
function runGit(args) {
try {
return execFileSync("git", args, {
cwd: repoRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
timeout: 5000
}).trim();
} catch {
return null;
}
}
export function classifyLiveDeploymentIdentity(sourceIdentity, runtimeIdentity, expectedRuntimeIdentity = null) {
const expectedCommit = sanitizeRevision(expectedRuntimeIdentity?.commitId ?? sourceIdentity?.commitId);
const expectedShort = expectedCommit === "unknown" ? "unknown" : expectedCommit.slice(0, 7);
const expectedReport = sourceIdentity?.reportCommitId ?? "unknown";
const expectedImageTag = sanitizeRuntimeString(expectedRuntimeIdentity?.imageTag ?? expectedShort).toLowerCase();
const observedRuntimeCommit = sanitizeRevision(runtimeIdentity?.commitId);
const observedImageTag = sanitizeRuntimeString(runtimeIdentity?.imageTag).toLowerCase();
const runtimeObserved = runtimeIdentity?.status === "observed";
const expectedObserved = expectedRuntimeIdentity === null || expectedRuntimeIdentity?.status === "observed";
const sourceClean =
sourceIdentity?.status === "observed" &&
sourceIdentity?.worktreeState === "clean" &&
expectedReport !== "unknown";
const accepted = new Set([...commitIdentityVariants(expectedCommit), ...commitIdentityVariants(expectedImageTag)]);
const runtimeMatches =
runtimeObserved &&
sourceClean &&
expectedObserved &&
expectedCommit !== "unknown" &&
(identityVariantMatches(accepted, observedRuntimeCommit) || identityVariantMatches(accepted, observedImageTag));
if (runtimeMatches) {
return {
status: "pass",
expectedCommit,
expectedReportCommitId: expectedReport,
expectedImageTag,
expectedSource: expectedRuntimeIdentity?.source ?? "source-git-head",
observedRuntimeCommit,
observedImageTag,
evidence: [
`expected=${expectedShort}`,
`expectedImageTag=${expectedImageTag}`,
`runtimeCommit=${observedRuntimeCommit}`,
`imageTag=${observedImageTag}`
],
summary: "Live API runtime identity matches the current source desired runtime identity."
};
}
const reason = !sourceClean
? "current source identity is not cleanly attributable"
: !expectedObserved || expectedCommit === "unknown"
? "source desired runtime identity was not observed"
: !runtimeObserved
? "live API runtime identity was not observed"
: "live API runtime commit/image tag does not match current source desired runtime identity";
return {
status: "blocked",
expectedCommit,
expectedReportCommitId: expectedReport,
expectedImageTag,
expectedSource: expectedRuntimeIdentity?.source ?? "source-git-head",
observedRuntimeCommit,
observedImageTag,
reason,
evidence: [
`expected=${expectedShort}`,
`expectedImageTag=${expectedImageTag}`,
`runtimeCommit=${observedRuntimeCommit}`,
`imageTag=${observedImageTag}`
],
summary: `Deployment drift: ${reason}.`
};
}
function observeExpectedRuntimeIdentity(serviceId) {
const deploy = readJsonOrNull("deploy/deploy.json");
const catalog = readJsonOrNull("deploy/artifact-catalog.dev.json");
const deployService = Array.isArray(deploy?.services)
? deploy.services.find((service) => service?.serviceId === serviceId)
: null;
const catalogService = Array.isArray(catalog?.services)
? catalog.services.find((service) => service?.serviceId === serviceId)
: null;
const image = sanitizeRuntimeString(catalogService?.image);
const imageTag = sanitizeRuntimeString(catalogService?.imageTag ?? imageTagFromImage(image));
const commitId = sanitizeRevision(catalogService?.commitId ?? catalog?.commitId ?? imageTag);
const sources = [];
if (catalogService) sources.push("deploy/artifact-catalog.dev.json");
if (deployService) sources.push("deploy/deploy.json:runtime-config");
const observed = commitId !== "unknown" || imageTag !== "unknown";
return {
status: observed ? "observed" : "not_observed",
serviceId,
source: sources.length > 0 ? sources.join("+") : "not_observed",
commitId,
imageTag,
image,
summary: observed
? "Expected DEV runtime identity was derived from current source deploy desired state, not from live health."
: "Expected DEV runtime identity was not found in current source deploy desired state."
};
}
function readJsonOrNull(relativePath) {
try {
return JSON.parse(readText(relativePath));
} catch {
return null;
}
}
function imageTagFromImage(image) {
if (typeof image !== "string") return "unknown";
const match = image.match(/:([^:/]+)$/u);
return match?.[1] ?? "unknown";
}
function commitIdentityVariants(commitId) {
if (typeof commitId !== "string" || !/^[a-f0-9]{7,40}$/u.test(commitId)) return new Set();
return new Set([commitId, commitId.slice(0, 12), commitId.slice(0, 7)]);
}
function identityVariantMatches(accepted, observed) {
if (typeof observed !== "string" || observed === "unknown" || observed === "not_observed") return false;
if (accepted.has(observed)) return true;
return [...accepted].some((expected) => expected.length >= 7 && observed.startsWith(expected));
}
async function inspectLiveWebAssetIdentity(baseUrl, fetchedIndexHtml) {
const assets = [];
for (const assetPath of liveIdentityWebAssets) {
const sourceText = assetPath === "app.js" ? await buildCloudWebAppBundleText() : readText(`web/hwlab-cloud-web/${assetPath}`);
const live = assetPath === "index.html"
? { ok: true, status: 200, contentType: "text/html; charset=utf-8", body: fetchedIndexHtml }
: await fetchText(new URL(assetPath, baseUrl).toString());
assets.push(compareLiveAsset(assetPath, live, sourceText));
}
return classifyLiveWebAssetIdentity(assets);
}
export function classifyLiveWebAssetIdentity(assets) {
const mismatches = assets.filter((asset) => asset.status !== "match");
return {
status: mismatches.length === 0 ? "pass" : "blocked",
assets,
mismatches: mismatches.map((asset) => asset.path),
summary: mismatches.length === 0
? "Live 16666 primary web assets match the current clean source files."
: `Deployment drift: live 16666 primary web assets differ from current source (${mismatches.map((asset) => asset.path).join(", ")}).`
};
}
function compareLiveAsset(assetPath, live, sourceText) {
const liveText = typeof live?.body === "string" ? live.body : "";
const sourceHash = hashText(sourceText);
const liveHash = hashText(liveText);
const fetched = live?.ok === true && live?.status === 200;
const matches = fetched && liveText === sourceText;
return {
path: assetPath,
status: matches ? "match" : fetched ? "mismatch" : "fetch_failed",
httpStatus: live?.status ?? null,
contentType: live?.contentType ?? "",
sourceHash,
liveHash,
sourceLength: sourceText.length,
liveLength: liveText.length
};
}
async function buildCloudWebAppBundleText() {
execFileSync(resolveBunCommand(), ["run", "build"], {
cwd: webRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
timeout: 60000
});
return fs.readFileSync(path.join(webRoot, "dist/app.js"), "utf8");
}
function hashText(value) {
return createHash("sha256").update(value).digest("hex").slice(0, 16);
}
function assetsExist() {
return requiredWebAssets.every((file) => fs.existsSync(path.join(webRoot, file)));
}
function hasDefaultWorkbench({ html, app }) {
const workspaceTag = firstTagForDataView(html, "workspace");
const gateTag = firstTagForDataView(html, "gate");
const appShellTag = html.match(/<main\b[^>]*\bdata-app-shell\b[^>]*>/u)?.[0] ?? "";
return (
labelsPresent(html, workbenchMarkers) &&
/<title>\s*HWLAB 云工作台\s*<\/title>/u.test(html) &&
/data-route=["']workspace["']/u.test(html) &&
workspaceTag !== null &&
!/\bhidden\b/u.test(workspaceTag) &&
/\bhidden\b/u.test(appShellTag) &&
gateTag !== null &&
/\bhidden\b/u.test(gateTag) &&
finalRouteFallback(app) === "workspace"
);
}
function secondaryViewsAreNotDefault({ html, app }) {
const finalFallback = finalRouteFallback(app);
const unhiddenSecondaryViews = [...html.matchAll(/<section\b[^>]*\bdata-view=["']([^"']+)["'][^>]*>/gu)]
.filter((match) => match[1] !== "workspace")
.filter((match) => !/\bhidden\b/u.test(match[0]));
return finalFallback === "workspace" && unhiddenSecondaryViews.length === 0;
}
function firstTagForDataView(html, view) {
return html.match(new RegExp(`<section\\b[^>]*\\bdata-view=["']${escapeRegExp(view)}["'][^>]*>`, "u"))?.[0] ?? null;
}
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 defaultHomepageHidesBackstage({ html }) {
const firstViewportText = visibleTextFromHtml(defaultFirstViewportHtml(html));
return defaultHomepageForbiddenTerms.every((term) => !textHasForbiddenTerm(firstViewportText, term));
}
function defaultWorkspaceIsSanitized({ html, app }) {
const defaultShellText = visibleTextFromHtml(defaultFirstViewportHtml(html));
const userFacingBodies = [
functionBody(app, "codeAgentStatusMessage"),
functionBody(app, "codeAgentPromptText"),
functionBody(app, "codeAgentControlSummary"),
functionBody(app, "codeAgentBlockedSummary"),
functionBody(app, "codeAgentAvailabilitySummary"),
functionBody(app, "failureMessage")
].join("\n");
return (
defaultHomepageForbiddenTerms.every((term) => !textHasForbiddenTerm(defaultShellText, term)) &&
!/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef|m3-route-required|waiting-for-dev-live|BLOCKED 凭证缺口|SOURCE fixture|DEV-LIVE/u.test(userFacingBodies) &&
/服务受阻/u.test(functionBody(app, "renderAgentChatStatus")) &&
/Code Agent 服务暂不可用/u.test(functionBody(app, "codeAgentBlockedSummary")) &&
/renderDevicePodPanel\(state\.liveSurface\)/u.test(app) &&
/默认右栏只显示 Device Pod 摘要和纯文本事件流/u.test(html) &&
!/Gate|诊断|验收|M0-M5|trace\/evidence|执行轨迹/u.test(defaultShellText)
);
}
function defaultFirstViewportHtml(html) {
return `${beforeSection(html, "help")}\n${rightSidebarDefaultHtml(html)}`;
}
function rightSidebarDefaultHtml(html) {
const start = html.search(/<aside\b[^>]*\bclass=["'][^"']*\bright-sidebar\b[^"']*["'][^>]*>/u);
if (start === -1) return "";
const wiringStart = html.slice(start).search(/<section\b[^>]*\bid=["']panel-wiring["'][^>]*>/u);
if (wiringStart !== -1) return html.slice(start, start + wiringStart);
const end = html.slice(start).search(/<\/aside>/u);
return end === -1 ? html.slice(start) : html.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 normalizeWiringHeaderText(text) {
return String(text ?? "")
.replace(/\s+/gu, " ")
.replace(/\s*\/\s*/gu, "/")
.trim()
.toLowerCase();
}
function hasCompletePrimaryNavigation(html) {
const navLabelsPresent =
["工作台", "内部复核", "使用说明"].every((label) => html.includes(`>${label}<`)) &&
!/>(?:资源树|收起资源树|展开资源树)<\/button>/u.test(html);
const legacyLabelsAbsent = incompletePrimaryNavLabels.every((label) => !html.includes(`>${label}<`));
const noLeftSideRecordsShortcut = !/data-side-tab-jump=["']records["']/u.test(html);
return navLabelsPresent && legacyLabelsAbsent && noLeftSideRecordsShortcut;
}
function gateSingleTableContract({ html, app, styles }) {
const gateHtml = sectionSource(html, "gate");
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) {
const buildCopiesAliases =
/\["gate", "diagnostics\/gate"\]/u.test(buildScript) ||
(/gate\/index\.html/u.test(distContractScript) && /diagnostics\/gate\/index\.html/u.test(distContractScript));
return (
/internalGatePathnames\(\)\.has/u.test(app) &&
gateRouteAliases.every((route) => app.includes(`"${route}"`)) &&
gateRouteAliases.every((route) => artifactPublisher.includes(`routePath === "${route}"`)) &&
buildCopiesAliases
);
}
function helpRouteAliasesAreServed(app, artifactPublisher, buildScript, distContractScript) {
const routeBody = functionBody(app, "routeFromLocation");
return (
/helpPathnames\(\)\.has/u.test(routeBody) &&
helpRouteAliases.every((route) => app.includes(`"${route}"`)) &&
helpRouteAliases.every((route) => artifactPublisher.includes(`routePath === "${route}"`)) &&
/buildCloudWebDist/u.test(buildScript) &&
/help\/index\.html/u.test(distContractScript) &&
/return "help"/u.test(routeBody) &&
finalRouteFallback(app) === "workspace"
);
}
function devicePodSummarySidebarContract({ html, app, styles }) {
const rightSidebar = elementSourceById(html, "aside", "device-pod-sidebar");
const activeSource = `${html}\n${app}`;
const forbiddenLegacy = /data-hardware-tab|m3-control|hardware-list|wiring-body|records-list|Gateway-SIMU|BOX-SIMU|Patch Panel|\/v1\/m3/iu;
return (
rightSidebar.length > 0 &&
[
"Device Pod",
"设备目标看板",
"id=\"device-pod-select\"",
"id=\"device-pod-summary\"",
"data-device-detail=\"pod\"",
"id=\"device-pod-interfaces\"",
"id=\"device-event-scroll\"",
"id=\"device-event-text\"",
"id=\"device-detail-dialog\""
].every((term) => rightSidebar.includes(term)) &&
!/<details\b/iu.test(rightSidebar) &&
!forbiddenLegacy.test(rightSidebar) &&
!forbiddenLegacy.test(activeSource) &&
/class="summary-tile"[^>]*data-device-detail="pod"[^>]*role="button"[^>]*tabindex="0"/u.test(rightSidebar) &&
/<dialog\b[\s\S]*id="device-detail-dialog"/u.test(rightSidebar) &&
/function\s+initDevicePodPanel\s*\(/u.test(app) &&
/function\s+renderDevicePodPanel\s*\(/u.test(app) &&
/function\s+renderDevicePodSummary\s*\(/u.test(app) &&
/function\s+renderDevicePodInterfaces\s*\(/u.test(app) &&
/function\s+deviceSummaryTile\s*\(/u.test(app) &&
/function\s+showDeviceDetail\s*\(/u.test(app) &&
/showModal\(\)/u.test(app) &&
/\.summary-tile\s*\{/u.test(styles) &&
/\.device-detail-dialog\s*\{/u.test(styles)
);
}
function devicePodEventStreamContract({ html, app, styles }) {
const rightSidebar = elementSourceById(html, "aside", "device-pod-sidebar");
const eventScrollStyle = styles.match(/\.device-event-scroll\s*\{[\s\S]*?\}/u)?.[0] ?? "";
return (
/id="device-event-scroll"/u.test(rightSidebar) &&
/<pre\b[^>]*id="device-event-text"/u.test(rightSidebar) &&
/等待 \/v1\/device-pods 事件流。/u.test(rightSidebar) &&
/function\s+renderDeviceEventStream\s*\(/u.test(app) &&
/function\s+markDeviceEventScrollIntent\s*\(/u.test(app) &&
/function\s+deviceEventUserActive\s*\(/u.test(app) &&
/function\s+setDeviceEventFollow\s*\(/u.test(app) &&
/function\s+renderDeviceEventUnreadHint\s*\(/u.test(app) &&
/textContent\s*=\s*nextText/u.test(functionBody(app, "renderDeviceEventStream")) &&
/state\.devicePod\.followEvents && wasNearBottom && !deviceEventUserActive\(\)/u.test(functionBody(app, "renderDeviceEventStream")) &&
/unreadEvents \+= nextLineCount - previousLineCount/u.test(functionBody(app, "renderDeviceEventStream")) &&
/overflow:\s*auto/u.test(eventScrollStyle) &&
/overscroll-behavior:\s*contain/u.test(eventScrollStyle) &&
!/<details\b|data-device-event-expand|accordion/iu.test(rightSidebar)
);
}
function devicePodExecutorAuthorityContract({ app, liveStatus, cloudApiPayloads, devicePodData, devicePodService, protocol, deployJson, artifactCatalog }) {
return (
["/v1/device-pods", "/status", "/events?limit=120", "/debug-probe/chip-id", "/io-probe/uart/1", "/io-probe/uart/1/tail?maxBytes=12000"].every((route) => app.includes(route)) &&
/function\s+classifyDevicePodProbe\s*\(/u.test(liveStatus) &&
/buildDevicePodCloudApiPayload/u.test(cloudApiPayloads) &&
/device_pod_authority_unavailable/u.test(cloudApiPayloads) &&
/buildDevicePodRestPayload/u.test(devicePodData) &&
/device-pod-executor-v1/u.test(devicePodService) &&
/fake:\s*false/u.test(devicePodService) &&
/hwlab-device-pod/u.test(protocol) &&
/hwlab-device-pod/u.test(deployJson) &&
/hwlab-device-pod/u.test(artifactCatalog)
);
}
function elementSourceById(source, tagName, id) {
const startPattern = new RegExp(`<${tagName}\\b[^>]*\\bid=["']${escapeRegExp(id)}["'][^>]*>`, "iu");
const startMatch = startPattern.exec(source);
if (!startMatch) return "";
const start = startMatch.index;
const tagPattern = new RegExp(`</?${tagName}\\b[^>]*>`, "giu");
tagPattern.lastIndex = start;
let depth = 0;
for (const match of source.slice(start).matchAll(tagPattern)) {
const absoluteEnd = start + match.index + match[0].length;
if (match[0].startsWith("</")) {
depth -= 1;
if (depth === 0) return source.slice(start, absoluteEnd);
} else {
depth += 1;
}
}
return source.slice(start);
}
function finalRouteFallback(app) {
const body = app.match(/function\s+routeFromLocation\s*\(\)\s*\{([\s\S]*?)\n\}/u)?.[1] ?? "";
const returns = [...body.matchAll(/return\s+["']([^"']+)["']\s*;/gu)].map((match) => match[1]);
return returns.at(-1) ?? null;
}
function labelsPresent(source, labels) {
return labels.every((label) => source.includes(label));
}
function hasScrollLockContract(styles) {
return (
/html,\s*\nbody\s*\{[^\}]*height:\s*100%;[^\}]*overflow:\s*hidden;/su.test(styles) &&
/body\s*>\s*\[data-app-shell\]\s*\{[^\}]*min-height:\s*0;/su.test(styles) &&
/\.workbench-shell\s*\{[^\}]*height:\s*100vh;[^\}]*height:\s*100dvh;[^\}]*overflow:\s*hidden;/su.test(styles) &&
/\.view\s*\{[^\}]*min-height:\s*0;[^\}]*overflow:\s*auto;/su.test(styles) &&
/[^\{]*\.conversation-list[^\{]*\{[^\}]*min-height:\s*0;[^\}]*overflow:\s*auto;/su.test(styles) &&
/\.device-event-scroll\s*\{[^\}]*min-height:\s*[^;]+;[^\}]*overflow:\s*auto;/su.test(styles)
);
}
function hasMobileWorkbenchLayoutContract({ html, app, styles }) {
return (
!/id=["']resource-explorer["']/u.test(html) &&
!/id=["']explorer-toggle["']/u.test(html) &&
!/id=["']explorer-resize["']/u.test(html) &&
!/function\s+syncMobileExplorer\s*\(/u.test(app) &&
!/function\s+setExplorerCollapsed\s*\(/u.test(app) &&
!/function\s+collapseExplorerAfterMobileAction\s*\(/u.test(app) &&
/@media\s*\(max-width:\s*860px\)[\s\S]*?\.activity-rail\s*\{[\s\S]*?grid-row:\s*1\s*\/\s*3;/u.test(styles) &&
/@media\s*\(max-width:\s*860px\)[\s\S]*?\.center-workspace\s*\{[\s\S]*?grid-row:\s*1;/u.test(styles) &&
/@media\s*\(max-width:\s*860px\)[\s\S]*?\.right-sidebar\s*\{[\s\S]*?grid-row:\s*2;/u.test(styles) &&
/@media\s*\(max-width:\s*520px\)[\s\S]*?\.command-bar\s*\{[\s\S]*?grid-template-columns:\s*minmax\(0,\s*1fr\)\s+auto\s+auto;/u.test(styles)
);
}
function hasResourceExplorerRemovalContract({ html, app, styles }) {
const source = `${html}\n${app}\n${styles}`;
const removedSelectorsAbsent = removedResourceExplorerSourceTerms.every((term) => !source.includes(term));
const removedTextAbsent = removedResourceExplorerCopyTerms.every((term) => !source.includes(term));
return (
removedSelectorsAbsent &&
removedTextAbsent &&
/id=["']activity-rail["']/u.test(html) &&
/id=["']left-sidebar-toggle["']/u.test(html) &&
/aria-label=["']折叠左侧导航["']/u.test(html) &&
/title=["']折叠左侧导航["']/u.test(html) &&
/aria-controls=["']activity-rail["']/u.test(html) &&
/aria-expanded=["']true["']/u.test(html) &&
/LAYOUT_STORAGE_KEY\s*=\s*["']hwlab\.workbench\.layout\.v1["']/u.test(app) &&
/leftSidebarCollapsed:\s*payload\.leftSidebarCollapsed === true/u.test(app) &&
/leftSidebarCollapsed:\s*state\.layout\.leftSidebarCollapsed/u.test(app) &&
/function\s+initLeftSidebarToggle\s*\(/u.test(app) &&
/function\s+setLeftSidebarCollapsed\s*\(/u.test(app) &&
/is-left-sidebar-collapsed/u.test(app) &&
/展开左侧导航/u.test(app) &&
/折叠左侧导航/u.test(app) &&
/setPointerCapture/u.test(app) &&
/localStorage\?\.setItem\(\s*LAYOUT_STORAGE_KEY/u.test(app) &&
/event\.key === "Home"/u.test(app) &&
/event\.key === "End"/u.test(app) &&
/ArrowLeft/u.test(app) &&
/ArrowRight/u.test(app) &&
/--rail-collapsed-width:\s*44px/u.test(styles) &&
/\.workbench-shell\s*\{[^}]*grid-template-columns:\s*var\(--rail-width\)\s+minmax\(420px,\s*1fr\)\s+var\(--right-width-expanded\);/su.test(styles) &&
/\.workbench-shell\.is-left-sidebar-collapsed\s*\{[^}]*--rail-width:\s*var\(--rail-collapsed-width\);/su.test(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;/su.test(styles) &&
/@media\s*\(max-width:\s*1240px\)[\s\S]*?\.workbench-shell\s*\{[\s\S]*?grid-template-columns:\s*var\(--rail-width\)\s+minmax\(0,\s*1fr\);/u.test(styles) &&
/id=["']device-pod-sidebar["']/u.test(html) &&
/id=["']right-sidebar-resize["']/u.test(html) &&
/aria-label=["']拖拽调整右侧 Device Pod 看板宽度["']/u.test(html) &&
/aria-valuemin=["']560["']/u.test(html) &&
/aria-valuemax=["']740["']/u.test(html) &&
/aria-valuenow=["']728["']/u.test(html) &&
/RIGHT_SIDEBAR_DEFAULT_WIDTH\s*=\s*728/u.test(app) &&
/RIGHT_SIDEBAR_MIN_WIDTH\s*=\s*560/u.test(app) &&
/RIGHT_SIDEBAR_MAX_WIDTH\s*=\s*740/u.test(app) &&
/function\s+initRightSidebarResize\s*\(/u.test(app) &&
/function\s+clampRightSidebarWidth\s*\(/u.test(app) &&
/function\s+rightSidebarResizeBounds\s*\(/u.test(app) &&
/setRightSidebarWidth\(drag\.startWidth \+ drag\.startX - event\.clientX/u.test(app) &&
/rightSidebarWidth:\s*state\.layout\.rightSidebarWidth/u.test(app) &&
/--right-sidebar-width:\s*728px/u.test(styles) &&
/--right-sidebar-min-width:\s*560px/u.test(styles) &&
/--right-sidebar-max-width:\s*740px/u.test(styles) &&
/--right-width:\s*clamp\(var\(--right-sidebar-min-width\),\s*var\(--right-sidebar-width\),\s*var\(--right-sidebar-max-width\)\)/u.test(styles) &&
/--right-width-expanded:\s*clamp\(var\(--right-sidebar-min-width\),\s*var\(--right-sidebar-width\),\s*var\(--right-sidebar-max-width\)\)/u.test(styles) &&
/\.right-sidebar-resize\s*\{[^}]*cursor:\s*col-resize;/su.test(styles) &&
/@media\s*\(max-width:\s*1240px\)[\s\S]*?\.right-sidebar-resize\s*\{[\s\S]*?display:\s*none;/u.test(styles) &&
/\.device-event-scroll\s*\{[^}]*overflow:\s*auto;[^}]*overscroll-behavior:\s*contain;/su.test(styles)
);
}
function hasStableRouteControls({ html, styles }) {
return (
["工作台", "内部复核", "使用说明", "Device Pod", "设备目标看板", "事件流"].every((label) => html.includes(label)) &&
!/>(?:资源树|收起资源树|展开资源树)<\/button>/u.test(html) &&
/\.rail-button\s*\{[^}]*min-width:\s*0;[^}]*min-height:\s*36px;[^}]*text-align:\s*center;[^}]*word-break:\s*keep-all;/su.test(styles) &&
/@media\s*\(max-width:\s*860px\)[\s\S]*?\.rail-button\s*\{[\s\S]*?min-height:\s*42px;/u.test(styles) &&
/\.summary-tile\s*\{[^}]*min-width:\s*0;/su.test(styles) &&
/\.summary-detail\s*\{[^}]*overflow-wrap:\s*anywhere;/su.test(styles) &&
/\.(?:status-dot|state-tag|badge)[^{]*\{[^}]*max-width:\s*100%;[^}]*line-height:\s*1\.25;[^}]*white-space:\s*normal;[^}]*overflow-wrap:\s*anywhere;/su.test(styles) &&
/\.probe-card\s*\{[^}]*min-width:\s*0;/su.test(styles) &&
/\.probe-card strong\s*\{[^}]*line-height:\s*1\.2;[^}]*overflow-wrap:\s*anywhere;/su.test(styles) &&
/@media\s*\(max-width:\s*860px\)[\s\S]*?\.message-runtime-path,\s*\n\s*\.message-session-context,\s*\n\s*\.message-trace,\s*\n\s*\.message-actions\s*\{[\s\S]*?grid-column:\s*1;/u.test(styles)
);
}
function hasSameOriginReadOnlyBoundary(app) {
const rpcCalls = [...app.matchAll(/callRpc\(\s*["']([^"']+)["']/gu)].map((match) => match[1]).sort();
return (
/fetchJson\("\/health\/live"\)/u.test(app) &&
/fetchJson\("\/v1"\)/u.test(app) &&
/fetchJson\("\/v1\/live-builds"\)/u.test(app) &&
/fetchJson\("\/v1\/device-pods"\)/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()) &&
!/fetchJson\(\s*["']https?:\/\//u.test(app)
);
}
function hasApiLiveStatusAttribution({ app, liveStatus }) {
const statusBody = functionBody(app, "workbenchApiSurfaceStatus");
return (
/function\s+workbenchApiSurfaceStatus\s*\(/u.test(app) &&
/classifyWorkbenchLiveStatus/u.test(statusBody) &&
/export function classifyWorkbenchLiveStatus/u.test(liveStatus) &&
/label:\s*"API 正常"/u.test(liveStatus) &&
/label:\s*"API 错误"/u.test(liveStatus) &&
/label:\s*"等待验证"/u.test(liveStatus) &&
/label:\s*"只读模式"/u.test(liveStatus) &&
/internalRawStatuses/u.test(liveStatus) &&
/classifyCodeAgentProbe/u.test(liveStatus) &&
/classifyDevicePodProbe/u.test(liveStatus) &&
/\/v1\/agent\/chat/u.test(liveStatus) &&
/\/v1\/device-pods/u.test(liveStatus) &&
/device_pod_ready/u.test(liveStatus) &&
/runtime_durable_adapter_/u.test(liveStatus) &&
/fallback_text_chat_only/u.test(liveStatus) &&
/provider_http_/u.test(liveStatus) &&
!/surface\.degraded/u.test(statusBody) &&
!/API 错误 \/ 只读模式/u.test(app) &&
!/可信记录受阻 \/ 只读模式/u.test(app) &&
!/API 降级 \/ 只读模式/u.test(app) &&
!/runtime_durable_adapter_query_blocked|DB live readiness|Gate|诊断|验收|M0-M5|执行轨迹/u.test(statusBody)
);
}
function hasCodeAgentChatContract({ html, app }) {
return (
/id=["']agent-chat-status["']/u.test(html) &&
/id=["']command-send["']/u.test(html) &&
/>发送<\/button>/u.test(html) &&
!/>添加草稿<\/button>/u.test(html) &&
/placeholder=["']输入要发送给 Code Agent 的消息;不会直接触发硬件变更。["']/u.test(html) &&
/fetchJson\("\/v1\/agent\/chat"/u.test(app) &&
/conversationId/u.test(app) &&
/messageId/u.test(app) &&
/updatedAt/u.test(app) &&
/error\?\.message/u.test(app) &&
/Code Agent 调用失败/u.test(app)
);
}
function hasCodeAgentReadinessVisibility({ html, app, liveStatus = "" }) {
const source = `${html}\n${app}`;
const userFacingBodies = [
functionBody(app, "codeAgentStatusMessage"),
functionBody(app, "codeAgentPromptText"),
functionBody(app, "codeAgentControlSummary"),
functionBody(app, "codeAgentBlockedSummary"),
functionBody(app, "codeAgentAvailabilitySummary"),
functionBody(app, "failureMessage")
].join("\n");
return (
/codeAgentAvailability/u.test(app) &&
/codeAgentAvailabilityFrom/u.test(app) &&
/sanitizeCodeAgentAvailability/u.test(app) &&
/latestCompletedAgentMessage/u.test(app) &&
blockedCodeAgentUiLabels.some((label) => app.includes(label)) &&
/provider_unavailable/u.test(app) &&
/OPENAI_API_KEY/u.test(app) &&
/只有真实完成回复才显示为完成/u.test(app) &&
/不能因为只有会话编号就当成实况完成/u.test(app) &&
/isTrustedCodeAgentProvider/u.test(app) &&
/Boolean\(value\?\.model\)/u.test(app) &&
/Boolean\(value\?\.backend\)/u.test(app) &&
/Boolean\(value\?\.traceId\)/u.test(app) &&
/Boolean\(value\?\.conversationId \|\| value\?\.sessionId\)/u.test(app) &&
/isRealCompletedChatMessage/u.test(app) &&
/isRealCompletedChatResult/u.test(app) &&
/hasRealCodeAgentEvidence/u.test(app) &&
/codeAgentBlockerDetail/u.test(app) &&
/codex_stdio_blocked_readonly_session_available/u.test(liveStatus) &&
/codex_stdio_supervisor_disabled/u.test(app) &&
!/status === "failed" \? "dev-live"/u.test(app) &&
!/tone:\s*state\.conversationId\s*\?\s*"dev-live"/u.test(app) &&
!/provider_unavailable[\s\S]{0,120}tone-[\w-]*dev-live/iu.test(source) &&
!/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef/u.test(userFacingBodies) &&
!/sk-[A-Za-z0-9._-]{8,}/u.test(source)
);
}
function hasCodeAgentCompletedEvidenceVisibility({ app }) {
const realEvidenceBody = functionBody(app, "hasRealCodeAgentEvidence");
const runnerEvidenceBody = functionBody(app, "isCodexRunnerCapableEvidence");
return (
requiredCodeAgentEvidenceTerms.every((term) => app.includes(term)) &&
/conversationId:\s*result\.conversationId \|\| result\.sessionId \|\| state\.conversationId/u.test(app) &&
/providerTrace:\s*result\.providerTrace/u.test(app) &&
/codeAgentRuntimePathFromMessage/u.test(app) &&
/function\s+messageRuntimePathPanel\s*\(/u.test(app) &&
/providerTrace\.command/u.test(app) &&
/providerTrace\.terminalStatus/u.test(app) &&
/function\s+messageTracePanel\s*\(/u.test(app) &&
/function\s+subscribeRunnerTrace\s*\(/u.test(app) &&
/new EventSource\(`\/v1\/agent\/chat\/trace\/\$\{encodeURIComponent\(traceId\)\}\/stream`\)/u.test(app) &&
/source\.addEventListener\("runnerTrace"/u.test(app) &&
/function\s+pollRunnerTrace\s*\(/u.test(app) &&
/function\s+updateMessageTrace\s*\(/u.test(app) &&
/function\s+runnerTraceFromSnapshot\s*\(/u.test(app) &&
/function\s+runnerTraceHeadline\s*\(/u.test(app) &&
/function\s+traceEventMeta\s*\(/u.test(app) &&
!/function\s+messageAttributionPanel\s*\(/u.test(app) &&
!/function\s+messageEvidencePanel\s*\(/u.test(app) &&
!/codeAgentAttributionFromMessage/u.test(app) &&
!/codeAgentFactsFromMessage/u.test(app) &&
/copyButton/u.test(app) &&
/isTrustedCodeAgentProvider\(value\?\.provider\)/u.test(realEvidenceBody) &&
/isCodexRunnerCapableEvidence\(value\)/u.test(realEvidenceBody) &&
/CODEX_RUNNER_CAPABLE_PROVIDERS/u.test(app) &&
/CODEX_APP_SERVER_RUNNER_KIND/u.test(runnerEvidenceBody) &&
/CODEX_APP_SERVER_SESSION_MODE/u.test(runnerEvidenceBody) &&
/CODEX_APP_SERVER_IMPLEMENTATION_TYPE/u.test(runnerEvidenceBody) &&
/CODEX_APP_SERVER_PROTOCOL/u.test(runnerEvidenceBody) &&
/long-lived-codex-stdio-session/u.test(runnerEvidenceBody) &&
/threadIdFrom\(value\)/u.test(runnerEvidenceBody) &&
/hasProviderTrace\(value\)/u.test(runnerEvidenceBody) &&
/isTextFallbackChatResult/u.test(app) &&
/value\?\.session\?\.status === "idle"/u.test(runnerEvidenceBody) &&
/value\?\.longLivedSessionGate\?\.status === "pass"/u.test(runnerEvidenceBody) &&
/value\?\.session\?\.codexStdio === true/u.test(runnerEvidenceBody) &&
/value\?\.runner\?\.codexStdio === true/u.test(runnerEvidenceBody) &&
/value\?\.runner\?\.writeCapable === true/u.test(runnerEvidenceBody) &&
/value\?\.runner\?\.durableSession === true/u.test(runnerEvidenceBody) &&
/TRUSTED_CODE_AGENT_PROVIDERS/u.test(app) &&
/UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN/u.test(realEvidenceBody) &&
/value\?\.conversationId \|\| value\?\.sessionId/u.test(realEvidenceBody) &&
/codeAgentContinuityBlocker/u.test(app) &&
/code_agent_session_evidence_missing/u.test(app) &&
/isRealCompletedChatResult\(result\)/u.test(app) &&
/classifyCodeAgentCompletion\(result,\s*\{\s*blockedError\s*\}\)/u.test(app) &&
/status:\s*"completed"/u.test(functionBody(app, "classifyCodeAgentCompletion")) &&
/Code Agent 完成证据不足/u.test(app) &&
/本次不会标记为真实完成/u.test(app)
);
}
function hasCodeAgentLongTimeoutContract(files) {
const app = files.app;
return (
/DEFAULT_API_TIMEOUT_MS\s*=\s*4500/u.test(app) &&
/DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*1800000/u.test(app) &&
/MAX_CODE_AGENT_TIMEOUT_MS\s*=\s*2400000/u.test(app) &&
/DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS\s*=\s*60000/u.test(app) &&
/async function sendAgentMessage[\s\S]*?fetchJson\("\/v1\/agent\/chat"[\s\S]*?timeoutMs:\s*CODE_AGENT_SUBMIT_TIMEOUT_MS/u.test(app) &&
/async function sendAgentMessage[\s\S]*?shortConnection:\s*true/u.test(app) &&
/timeoutMs\s*=\s*API_TIMEOUT_MS/u.test(functionBody(app, "fetchJson")) &&
/timeoutName\s*=\s*path/u.test(functionBody(app, "fetchJson")) &&
/function\s+isCodeAgentTimeoutError\s*\(/u.test(app) &&
/isTimeoutFailure\(code,\s*message\)/u.test(functionBody(app, "isCodeAgentTimeoutError")) &&
/function\s+renderDrafts\s*\(/u.test(app) &&
/HWLAB_CLOUD_WEB_CONFIG/u.test(app) &&
/codeAgentTimeoutMs/u.test(app)
);
}
function hasWorkbenchLiveSurfaceTimeoutContract(files) {
const app = files.app;
const loadLiveSurfaceBody = functionBody(app, "loadLiveSurface");
return (
/DEFAULT_API_TIMEOUT_MS\s*=\s*4500/u.test(app) &&
/DEFAULT_LIVE_SURFACE_TIMEOUT_MS\s*=\s*12000/u.test(app) &&
/LIVE_SURFACE_TIMEOUT_MS\s*=\s*resolveTimeoutMs\("liveSurfaceTimeoutMs"/u.test(app) &&
/timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/u.test(functionBody(app, "liveSurfaceFetch")) &&
/timeoutName:\s*`工作台实况 \$\{path\}`/u.test(functionBody(app, "liveSurfaceFetch")) &&
/callRpc\(method,\s*params,\s*\{/u.test(functionBody(app, "liveSurfaceRpc")) &&
/timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/u.test(functionBody(app, "liveSurfaceRpc")) &&
/timeoutMs:\s*options\?\.timeoutMs \?\? API_TIMEOUT_MS/u.test(functionBody(app, "callRpc")) &&
/liveSurfaceFetch\("\/health\/live"\)/u.test(loadLiveSurfaceBody) &&
/liveSurfaceFetch\("\/v1\/device-pods"\)/u.test(loadLiveSurfaceBody) &&
/\/v1\/device-pods\/\$\{encodedPodId\}\/status/u.test(loadLiveSurfaceBody) &&
/\/v1\/device-pods\/\$\{encodedPodId\}\/events\?limit=120/u.test(loadLiveSurfaceBody) &&
/liveSurfaceRpc\("system\.health"\)/u.test(loadLiveSurfaceBody) &&
/timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/u.test(functionBody(app, "loadGateDiagnostics")) &&
!/\/v1\/m3|runM3IoAction|工作台 M3 状态刷新/u.test(loadLiveSurfaceBody)
);
}
function hasCodeAgentTraceReplayDisclosure({ app, styles }) {
const tracePanelBody = functionBody(app, "messageTracePanel");
const traceToolbarBody = functionBody(app, "messageTraceToolbar");
const renderConversationBody = functionBody(app, "renderConversation");
return (
/function\s+messageTraceCountText\s*\(/u.test(app) &&
/显示全部可读事件\s+\$\{readableTotal\}\s+\/\s+已载入原始\s+\$\{loadedTotal\}\s+\/\s+后端原始\s+\$\{rawTotal\}/u.test(app) &&
/完整 trace 回放中\s+\/\s+当前可读事件\s+\$\{readableTotal\}\s+\/\s+已载入原始\s+\$\{loadedTotal\}\s+\/\s+后端原始\s+\$\{rawTotal\}/u.test(app) &&
!/压缩窗口\s+\$\{readableTotal\}/u.test(app) &&
/CODE_AGENT_SESSION_STORAGE_KEY\s*=\s*"hwlab\.workbench\.codeAgentSession\.v1"/u.test(app) &&
/function\s+restoreCodeAgentSessionState\s*\(/u.test(app) &&
/function\s+persistCodeAgentSessionState\s*\(/u.test(app) &&
/window\.localStorage\?\.setItem\(CODE_AGENT_SESSION_STORAGE_KEY/u.test(app) &&
/window\.localStorage\?\.removeItem\(CODE_AGENT_SESSION_STORAGE_KEY/u.test(app) &&
/refreshRestoredCodeAgentTraces/u.test(app) &&
/function\s+maybeReplayFullTraceForMessage\s*\(/u.test(app) &&
/function\s+replayFullTrace\s*\(/u.test(app) &&
/function\s+renderTraceEventList\s*\(/u.test(app) &&
/messageTraceToolbar\(message,\s*trace,\s*events,\s*rows\)/u.test(tracePanelBody) &&
/list\.dataset\.traceMode\s*=\s*"all"/u.test(tracePanelBody) &&
/list\.dataset\.traceUiKey\s*=\s*traceUiKey/u.test(tracePanelBody) &&
/rememberTraceScrollPosition\(traceUiKey,\s*list\)/u.test(tracePanelBody) &&
/traceDetailsOpen:\s*new Map\(\)/u.test(app) &&
/traceScrollPositions:\s*new Map\(\)/u.test(app) &&
/traceScrollPinnedToBottom:\s*new Map\(\)/u.test(app) &&
/function\s+shouldFollowTraceBottom\s*\(/u.test(app) &&
/function\s+scrollTraceToBottom\s*\(/u.test(app) &&
/captureTraceScrollPositions\(\)/u.test(renderConversationBody) &&
/restoreTraceScrollPositions\(\)/u.test(renderConversationBody) &&
!/CODE_AGENT_TRACE_PREVIEW_LIMIT|tracePreviewEvents|展开全部|data-trace-mode="tail"/u.test(app) &&
/复制 JSON/u.test(traceToolbarBody) &&
/下载 trace/u.test(traceToolbarBody) &&
/function\s+messageTraceJson\s*\(/u.test(app) &&
/function\s+downloadTraceJson\s*\(/u.test(app) &&
/\.message-trace-toolbar\s*\{/u.test(styles) &&
/\.message-trace-count\s*\{/u.test(styles) &&
/\.message-trace-action\s*\{/u.test(styles) &&
/\.message-trace-events\s*\{[\s\S]*?max-height:\s*min\(520px,\s*54dvh\)[\s\S]*?overflow:\s*auto/u.test(styles)
);
}
function hasCodeAgentStatusSummaryContract({ html, app, styles, codeAgentStatus }) {
return (
/id=["']code-agent-summary["']/u.test(html) &&
/id=["']code-agent-summary-label["']/u.test(html) &&
/id=["']code-agent-summary-capability["']/u.test(html) &&
/id=["']code-agent-summary-trace["']/u.test(html) &&
/function\s+renderCodeAgentSummary\s*\(/u.test(app) &&
/classifyCodeAgentStatusSummary/u.test(app) &&
/currentCodeAgentStatusSummary/u.test(app) &&
/codeAgent\.status/u.test(app) &&
/providerTrace\.command/u.test(app) &&
/providerTrace\.terminalStatus/u.test(app) &&
/运行路径语义/u.test(app) &&
/当前部署 revision/u.test(app) &&
/fallback-text-chat-only/u.test(codeAgentStatus) &&
/stateless-one-shot/u.test(codeAgentStatus) &&
/read-only-session-tools/u.test(codeAgentStatus) &&
/currentDeploymentRevision/u.test(codeAgentStatus) &&
/unsafeGreenForNonReady/u.test(codeAgentStatus) &&
/provider_config_blocked/u.test(codeAgentStatus) &&
!/sourceMain|SOURCE main|待部署 commit|targetCommit/u.test(codeAgentStatus) &&
/\.code-agent-summary\s*\{/u.test(styles) &&
/\.tone-border-warn/u.test(styles) &&
/\.tone-border-blocked/u.test(styles) &&
/\.tone-border-ok/u.test(styles)
);
}
function hasCodeAgentConversationUxStates({ app, styles }) {
const submitBody = `${functionBody(app, "initCommandBar")}\n${functionBody(app, "submitAgentMessage")}`;
const renderStatusBody = functionBody(app, "renderAgentChatStatus");
const applyResultBody = functionBody(app, "applyCodeAgentResultToMessage");
const agentToneBody = functionBody(app, "agentStatusTone");
const statusToneContract =
/status === "completed" \? "dev-live" : status === "failed" \|\| status === "blocked" \? "blocked" : status === "running" \? "pending" : "source"/u.test(app) ||
/status === "completed" \? "dev-live" : \["failed",\s*"blocked",\s*"timeout",\s*"canceled",\s*"error"\]\.includes\(status\) \? "blocked" : status === "running" \? "pending" : "source"/u.test(app) ||
(
/function\s+agentStatusTone\s*\(/u.test(app) &&
/\["failed",\s*"blocked",\s*"timeout",\s*"canceled",\s*"error"\]\.includes\(status\)/u.test(agentToneBody)
);
return (
/function\s+classifyCodeAgentCompletion\s*\(/u.test(app) &&
/function\s+isSourceFixtureChatResult\s*\(/u.test(app) &&
/function\s+isSourceFixtureCompletion\s*\(/u.test(app) &&
/function\s+isSourceFixtureCompletedChatMessage\s*\(/u.test(app) &&
/function\s+messageTracePanel\s*\(/u.test(app) &&
/function\s+subscribeRunnerTrace\s*\(/u.test(app) &&
/function\s+updateMessageTrace\s*\(/u.test(app) &&
/function\s+messagePendingContextPanel\s*\(/u.test(app) &&
!/function\s+messageEvidencePanel\s*\(/u.test(app) &&
!/function\s+messageAttributionPanel\s*\(/u.test(app) &&
!/codeAgentAttributionFromMessage/u.test(app) &&
/DEFAULT_API_TIMEOUT_MS\s*=\s*4500/u.test(app) &&
/DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*1800000/u.test(app) &&
/MAX_CODE_AGENT_TIMEOUT_MS\s*=\s*2400000/u.test(app) &&
/timeoutMs:\s*CODE_AGENT_SUBMIT_TIMEOUT_MS/u.test(app) &&
/computeCodeAgentComposerState/u.test(app) &&
/composer\.submitMode\s*===\s*"steer"/u.test(submitBody) &&
/sendAgentSteer/u.test(app) &&
/fetchJson\("\/v1\/agent\/chat\/steer"/u.test(functionBody(app, "sendAgentSteer")) &&
/el\.commandInput\.disabled\s*=\s*composer\.disabled/u.test(renderStatusBody) &&
/el\.commandSend\.textContent\s*=\s*composer\.submitMode\s*===\s*"steer"\s*\?\s*"引导"\s*:\s*"发送"/u.test(renderStatusBody) &&
!/el\.commandInput\.disabled\s*=\s*status\s*===\s*"running"/u.test(renderStatusBody) &&
/HWLAB_CLOUD_WEB_CONFIG/u.test(app) &&
/正在处理这次 Code Agent 请求/u.test(app) &&
/旧 4500ms/u.test(app) &&
/结构化 blocker/u.test(app) &&
/sourceKind:\s*"PENDING"/u.test(app) &&
/Code Agent 超过.*无新事件/u.test(app) &&
/Code Agent SOURCE 回复/u.test(app) &&
/SOURCE fixture 只可显示为 SOURCE 回复,不能冒充 DEV-LIVE/u.test(app) &&
/sourceKind:\s*completion\.sourceKind/u.test(`${submitBody}\n${applyResultBody}`) &&
/completion\.replied/u.test(`${submitBody}\n${applyResultBody}`) &&
/function\s+applyCodeAgentResultToMessage\s*\(/u.test(app) &&
/function\s+reconcileCodeAgentResult\s*\(/u.test(app) &&
/function\s+shouldReconcileCodeAgentResult\s*\(/u.test(app) &&
/status-source/u.test(styles) &&
/message-user/u.test(styles) &&
/message-agent/u.test(styles) &&
/message-pending-context\s*\{/u.test(styles) &&
/message-session-context\s*\{/u.test(styles) &&
/copy-chip\s*\{/u.test(styles) &&
/message-trace\s*\{/u.test(styles) &&
/message-trace-events\s*\{/u.test(styles) &&
!/message-attribution\s*\{/u.test(styles) &&
!/message-evidence\s*\{/u.test(styles) &&
/width:\s*min\(100%,\s*780px\)/u.test(styles) &&
statusToneContract &&
!/sourceKind:\s*"SOURCE"[\s\S]{0,160}status:\s*"completed"/u.test(app)
);
}
function noForbiddenWriteSurface(source) {
return forbiddenWritePatterns.every((pattern) => !pattern.test(source));
}
function sourceEvidenceNotDevLive(source) {
return (
/data-static-source-fallback=["']FAKE["']/u.test(source) &&
/Device Pod|hwlab-device-pod/u.test(source) &&
!/sourceKind:\s*"SOURCE"[\s\S]{0,160}status:\s*"completed"/u.test(source) &&
!/data-static-source-fallback=["']FAKE["'][\s\S]{0,240}DEV-LIVE/iu.test(source)
);
}
function functionBody(source, functionName) {
const match = source.match(new RegExp(`(?:async\\s+)?function\\s+${escapeRegExp(functionName)}\\s*\\([^)]*\\)\\s*\\{`, "u"));
if (!match) return "";
let depth = 0;
for (let index = match.index + match[0].length - 1; 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 "";
}
function inspectHelpContract({ html, app }) {
const markdownFiles = findHelpMarkdownFiles();
const rendererPackages = findMarkdownRenderers();
const route = inspectHelpRoute(html, app);
const implemented = markdownFiles.includes("web/hwlab-cloud-web/help.md") && rendererPackages.length > 0 && route.nonDefaultRoute;
const status = route.defaultRouteRisk ? "blocked" : implemented ? "pass" : "pending";
return {
status,
blocker: route.defaultRouteRisk ? "runtime_blocker" : null,
summary: implemented ? "Markdown help route from PR #114 is present and remains non-default." : `Markdown help page contract is pending; owner=${helpOwner}.`,
observations: {
owner: helpOwner,
markdownHelpFilesPresent: markdownFiles.length > 0,
markdownHelpFiles: markdownFiles,
matureMarkdownRendererPresent: rendererPackages.length > 0,
markdownRenderers: rendererPackages,
nonDefaultRoute: route.nonDefaultRoute,
implemented,
route
}
};
}
function findHelpMarkdownFiles() {
const files = [
"web/hwlab-cloud-web/help.md",
"web/hwlab-cloud-web/help/README.md",
"web/hwlab-cloud-web/help/index.md",
"docs/reference/cloud-workbench.md"
];
return files.filter((relativePath) => fs.existsSync(path.join(repoRoot, relativePath)));
}
function findMarkdownRenderers() {
const renderService = readText("web/hwlab-cloud-web/src/services/markdown/render.ts");
const packages = ["package.json", "web/hwlab-cloud-web/package.json"]
.filter((file) => fs.existsSync(path.join(repoRoot, file)))
.flatMap((file) => {
const json = JSON.parse(readText(file));
return Object.keys({ ...(json.dependencies ?? {}), ...(json.devDependencies ?? {}) });
});
const source = `${readCloudWebAppSource()}\n${readText("web/hwlab-cloud-web/index.html")}`;
const imported = markdownRendererPackages.filter((name) => new RegExp(`from\\s+["']${escapeRegExp(name)}["']|import\\(["']${escapeRegExp(name)}["']\\)`, "u").test(source));
const renderers = [...packages, ...imported].filter((name) => markdownRendererPackages.includes(name));
if (/export function renderMessageMarkdown/u.test(renderService) && /escapeHtml/u.test(renderService)) renderers.push("react-render-service");
return [...new Set(renderers)].sort();
}
function inspectHelpRoute(html, app) {
const helpViewTag = firstTagForDataView(html, "help");
const explicitRoutePresent =
/data-route=["']help["']/u.test(html) ||
/data-view=["']help["']/u.test(html) ||
/#help\b/u.test(html + app) ||
/pathname[\s\S]{0,220}\/help/u.test(app);
const finalFallback = finalRouteFallback(app);
const helpViewHidden = helpViewTag ? /\bhidden\b/u.test(helpViewTag) : false;
return {
explicitRoutePresent,
finalFallback,
helpViewHidden,
routePolicy: html.match(/\bdata-help-route-policy=["']([^"']+)["']/u)?.[1] ?? null,
nonDefaultRoute:
explicitRoutePresent &&
finalFallback === "workspace" &&
(helpViewTag ? helpViewHidden : true) &&
/data-help-route-policy=["']non-default-internal-help["']/u.test(html),
defaultRouteRisk: finalFallback === "help" || (helpViewTag !== null && !helpViewHidden)
};
}
function staticSafety() {
return {
prodTouched: false,
servicesRestarted: false,
heavyE2E: false,
hardwareWriteApis: false,
devLive: false,
sourceIsDevLive: false,
statement: "Static repository/source evidence is not DEV-LIVE and cannot promote SOURCE, LOCAL, DRY-RUN, or fixture observations."
};
}
export function classifyLiveApiRuntimeReadiness(runtimeIdentity) {
const healthStatus = sanitizeRuntimeString(runtimeIdentity?.healthStatus);
const runtime = runtimeIdentity?.runtime ?? {};
const readiness = runtimeIdentity?.readiness ?? {};
const durability = readiness?.durability ?? {};
const blockerCodes = Array.isArray(runtimeIdentity?.blockerCodes)
? runtimeIdentity.blockerCodes.map((item) => sanitizeRuntimeString(item))
: [];
const ready = runtimeIdentity?.ready === true || readiness?.ready === true;
const runtimeReady = runtime?.ready === true;
const durabilityReady = durability?.ready === true;
const runtimeDurable = runtime?.durable === true;
const durableBlocked = runtime?.blocker === "runtime_durable_adapter_query_blocked" ||
durability?.blocker === "runtime_durable_adapter_query_blocked" ||
blockerCodes.includes("runtime_durable_adapter_query_blocked") ||
durability?.blockedLayer === "durability_query" ||
runtime?.connection?.queryResult === "query_blocked" ||
durability?.queryResult === "query_blocked";
const degraded = healthStatus === "degraded" ||
ready === false ||
runtimeReady === false ||
durabilityReady === false ||
runtimeDurable === false ||
durableBlocked;
if (!degraded) {
return {
status: "pass",
healthStatus,
ready,
runtimeDurable,
runtimeReady,
durabilityReady,
durableBlocked,
blockerCodes,
evidence: [
`api.status=${healthStatus}`,
`ready=${ready}`,
`runtime.durable=${runtimeDurable}`,
`runtime.ready=${runtimeReady}`,
`durability.ready=${durabilityReady}`
],
summary: "Live API health and runtime durability are ready for browser journey evidence."
};
}
const blockedEvidence = [
`api.status=${healthStatus}`,
`ready=${ready}`,
`runtime.durable=${runtimeDurable}`,
`runtime.ready=${runtimeReady}`,
`durability.ready=${durabilityReady}`,
durableBlocked ? "runtime_durable_adapter_query_blocked" : null,
...blockerCodes.map((code) => `blocker=${code}`)
].filter(Boolean);
return {
status: "degraded",
healthStatus,
ready,
runtimeDurable,
runtimeReady,
durabilityReady,
durableBlocked,
blockerCodes,
evidence: blockedEvidence,
summary: "Live API is reachable but degraded/read-only; runtime durability is not ready, so this cannot be full DEV-LIVE acceptance."
};
}
async function observeLiveRuntimeIdentity(apiEndpoint) {
const endpoint = new URL("/health/live", apiEndpoint).toString();
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 6000);
try {
const response = await fetch(endpoint, {
signal: controller.signal,
headers: { Accept: "application/json" }
});
let body = null;
try {
body = await response.json();
} catch {
body = null;
}
if (!response.ok || !body || typeof body !== "object") {
return notObservedRuntimeIdentity(`health-live returned HTTP ${response.status}`, endpoint);
}
return {
status: "observed",
source: "health-live",
endpoint,
serviceId: sanitizeRuntimeString(body.serviceId),
environment: sanitizeRuntimeString(body.environment),
healthStatus: sanitizeRuntimeString(body.status),
ready: body.ready === true,
commitId: sanitizeRevision(body.commit?.id ?? body.commitId),
commitSource: sanitizeRuntimeString(body.commit?.source),
imageTag: sanitizeRuntimeString(body.image?.tag),
observedAt: sanitizeTimestamp(body.observedAt),
runtime: summarizeRuntimeHealth(body.runtime),
readiness: summarizeReadinessHealth(body.readiness),
blockerCodes: sanitizeStringList(body.blockerCodes),
summary: "Live runtime identity was observed through the existing read-only health endpoint and is not inferred from source git HEAD."
};
} catch (error) {
return notObservedRuntimeIdentity(
error.name === "AbortError" ? "health-live request timed out" : `health-live request failed: ${oneLine(error.message)}`,
endpoint
);
} finally {
clearTimeout(timer);
}
}
function notObservedRuntimeIdentity(reason, endpoint = new URL("/health/live", runtime.endpoints.api).toString()) {
return {
status: "not_observed",
source: "health-live",
endpoint,
serviceId: "not_observed",
environment: "not_observed",
healthStatus: "not_observed",
ready: false,
commitId: "not_observed",
commitSource: "not_observed",
imageTag: "not_observed",
observedAt: new Date().toISOString(),
runtime: {
durable: false,
ready: false,
status: "not_observed",
blocker: "not_observed"
},
readiness: {
ready: false,
status: "not_observed",
durability: {
ready: false,
status: "not_observed",
blocker: "not_observed",
blockedLayer: "not_observed",
queryResult: "not_observed"
}
},
blockerCodes: [],
reason: oneLine(reason),
summary: "Live runtime identity is recorded as not_observed; source identity must not be treated as the deployed runtime revision."
};
}
function summarizeRuntimeHealth(runtimeHealth) {
return {
durable: runtimeHealth?.durable === true,
ready: runtimeHealth?.ready === true,
status: sanitizeRuntimeString(runtimeHealth?.status),
blocker: sanitizeRuntimeString(runtimeHealth?.blocker),
connection: {
queryAttempted: runtimeHealth?.connection?.queryAttempted === true,
queryResult: sanitizeRuntimeString(runtimeHealth?.connection?.queryResult)
}
};
}
function summarizeReadinessHealth(readiness) {
return {
ready: readiness?.ready === true,
status: sanitizeRuntimeString(readiness?.status),
durability: {
ready: readiness?.durability?.ready === true,
status: sanitizeRuntimeString(readiness?.durability?.status),
blocker: sanitizeRuntimeString(readiness?.durability?.blocker),
blockedLayer: sanitizeRuntimeString(readiness?.durability?.blockedLayer),
queryResult: sanitizeRuntimeString(readiness?.durability?.queryResult)
}
};
}
function sanitizeStringList(value) {
return Array.isArray(value) ? value.map((item) => sanitizeRuntimeString(item)) : [];
}
function sanitizeRevision(value) {
if (typeof value !== "string") return "unknown";
const trimmed = value.trim().toLowerCase();
return /^[a-f0-9]{7,40}$/u.test(trimmed) ? trimmed : "unknown";
}
function sanitizeTimestamp(value) {
return typeof value === "string" && !Number.isNaN(Date.parse(value)) ? value : new Date().toISOString();
}
function sanitizeRuntimeString(value) {
return typeof value === "string" && value.trim().length > 0 ? oneLine(value) : "unknown";
}
function oneLine(value) {
return String(value ?? "unknown").replace(/\s+/gu, " ").trim().slice(0, 240) || "unknown";
}
async function fetchText(url) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 6000);
try {
const response = await fetch(url, { signal: controller.signal, headers: { Accept: "text/html" } });
const body = await response.text();
return {
ok: response.ok,
status: response.status,
contentType: response.headers.get("content-type") ?? "",
body
};
} catch (error) {
return {
ok: false,
status: null,
contentType: "",
body: "",
error: error.name === "AbortError" ? "timeout" : error.message
};
} finally {
clearTimeout(timer);
}
}
function liveHtmlLooksLikeWorkbench(html) {
const workspaceTag = firstTagForDataView(html, "workspace");
const gateTag = firstTagForDataView(html, "gate");
const helpTag = firstTagForDataView(html, "help");
return (
/HWLAB 云工作台/u.test(html) &&
labelsPresent(html, workbenchMarkers) &&
workspaceTag !== null &&
!/\bhidden\b/u.test(workspaceTag) &&
gateTag !== null &&
/\bhidden\b/u.test(gateTag) &&
(helpTag === null || /\bhidden\b/u.test(helpTag))
);
}
async function inspectLiveDom(url, options = {}) {
let chromium;
try {
({ chromium } = await importPlaywright());
} catch (error) {
return {
status: "skip",
summary: `Browser DOM check skipped because Playwright is unavailable: ${error.message}`,
evidence: ["playwright import failed"]
};
}
let browser;
try {
browser = await launchChromium(chromium);
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
const postGuard = options.noCodeAgentPost ? await installNoCodeAgentPostGuard(page) : null;
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 8000 });
const login = await inspectLoginPage(page);
await loginWithDefaultCredentials(page);
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
const dom = await page.evaluate(async (forbiddenTerms) => {
const workspace = document.querySelector('[data-view="workspace"]');
const gate = document.querySelector('[data-view="gate"]');
const help = document.querySelector('[data-view="help"]');
const diagnostics = document.querySelector('[data-side-panel="diagnostics"]');
const bodyStyle = getComputedStyle(document.body);
const htmlStyle = getComputedStyle(document.documentElement);
const text = document.body.textContent ?? "";
const visible = (selector) => {
const element = document.querySelector(selector);
if (!element) return false;
const style = getComputedStyle(element);
const box = element.getBoundingClientRect();
return style.visibility !== "hidden" && style.display !== "none" && box.width > 0 && box.height > 0;
};
const visibleTextInFirstViewport = () => {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
const chunks = [];
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
const textContent = node.textContent?.replace(/\s+/gu, " ").trim();
if (!textContent) continue;
const parent = node.parentElement;
if (!parent) continue;
const style = getComputedStyle(parent);
if (style.visibility === "hidden" || style.display === "none") continue;
const box = parent.getBoundingClientRect();
const inViewport = box.width > 0 && box.height > 0 && box.bottom > 0 && box.right > 0 && box.top < window.innerHeight && box.left < window.innerWidth;
if (inViewport) chunks.push(textContent);
}
return chunks.join(" ").replace(/\s+/gu, " ").trim();
};
const textHasForbiddenTerm = (candidateText, term) => {
if (/^[A-Za-z][A-Za-z0-9-]*$/u.test(term)) {
const escaped = term.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
return new RegExp(`\\b${escaped}\\b`, "u").test(candidateText);
}
return candidateText.includes(term);
};
window.scrollTo(0, 240);
document.documentElement.scrollTop = 240;
document.body.scrollTop = 240;
await new Promise((resolve) => requestAnimationFrame(resolve));
const firstViewportText = visibleTextInFirstViewport();
const rootAfterScrollAttempt = {
windowScrollY: window.scrollY,
htmlScrollTop: document.documentElement.scrollTop,
bodyScrollTop: document.body.scrollTop
};
return {
title: document.title,
bodyOverflow: bodyStyle.overflow,
htmlOverflow: htmlStyle.overflow,
workspaceHidden: workspace ? workspace.hidden : null,
gateHidden: gate ? gate.hidden : null,
helpHidden: help ? help.hidden : null,
diagnosticsHidden: diagnostics ? diagnostics.hidden : null,
outerScrollLocked: document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2,
rootAfterScrollAttempt,
liveBuildLatest: document.querySelector("#live-build-latest")?.textContent?.replace(/\s+/gu, " ").trim() ?? "",
liveBuildDefaultVisible: visible("#live-build-latest"),
liveBuildSummaryOpen: document.querySelector("#live-build-summary")?.open === true,
liveBuildListVisibleWhenClosed: visible("#live-build-list"),
liveBuildDetailWhenOpen: (() => {
const details = document.querySelector("#live-build-summary");
const toggle = document.querySelector("#live-build-toggle");
const list = document.querySelector("#live-build-list");
if (!details || !toggle || !list) {
return {
visible: false,
rows: 0,
scrollContained: false,
overlay: false,
summaryUsesDialog: false,
closedByButton: false,
title: "",
text: "",
overflowX: 0
};
}
const wasOpen = details.open;
details.open = false;
toggle.click();
const dialogLayer = document.querySelector(".workbench-dialog-layer");
const dialog = document.querySelector('.workbench-dialog[role="dialog"][aria-modal="true"]');
const content = dialog?.querySelector(".workbench-dialog-content");
const dialogList = dialog?.querySelector(".live-build-list");
const rows = [...(dialogList?.querySelectorAll(".live-build-row") ?? [])];
const dialogLayerStyle = dialogLayer ? getComputedStyle(dialogLayer) : null;
const contentStyle = content ? getComputedStyle(content) : null;
const dialogBox = dialog?.getBoundingClientRect();
const visibleDialog = Boolean(dialogBox && dialogBox.width > 0 && dialogBox.height > 0);
const overlay = dialogLayerStyle?.position === "fixed";
const scrollContained = Boolean(
content && (
content.scrollHeight <= content.clientHeight + 2 ||
/auto|scroll/u.test(contentStyle?.overflowY ?? "")
)
);
const overflowX = dialogList
? Math.max(0, ...rows.map((row) => row.scrollWidth - row.clientWidth), dialogList.scrollWidth - dialogList.clientWidth)
: 0;
const text = dialogList?.textContent?.replace(/\s+/gu, " ").trim() ?? "";
dialog?.querySelector(".workbench-dialog-close")?.click();
const closedByButton = !document.querySelector('.workbench-dialog[role="dialog"][aria-modal="true"]');
details.open = wasOpen;
return {
visible: visibleDialog,
rows: rows.length,
scrollContained,
overlay,
summaryUsesDialog: toggle.getAttribute("aria-haspopup") === "dialog",
closedByButton,
title: dialog?.getAttribute("aria-label") ?? "",
text,
overflowX
};
})(),
firstViewportForbiddenPresent: forbiddenTerms.filter((term) => textHasForbiddenTerm(firstViewportText, term)),
firstViewportTextSample: firstViewportText.slice(0, 400),
labelsPresent: ["Device Pod", "设备目标看板", "Agent 对话", "事件流", "使用说明"].every((label) => text.includes(label)),
navLabelsPresent: ["工作台", "内部复核", "使用说明"].every((label) =>
[...document.querySelectorAll(".activity-rail button")].some((button) => button.textContent?.trim() === label)
) && ![...document.querySelectorAll(".activity-rail button")].some((button) => /资源树|收起资源树|展开资源树/u.test(button.textContent?.trim() ?? "")),
defaultBackstageHidden: !/(Gate|诊断|验收|BLOCKED|M0-M5|执行轨迹)/u.test(document.querySelector(".center-workspace")?.innerText ?? ""),
gateRouteAvailable: [...document.querySelectorAll("[data-route='gate']")].some((button) => button.textContent?.trim() === "内部复核"),
devicePod: (() => {
const sidebar = document.querySelector("#device-pod-sidebar");
const summaryTiles = [...document.querySelectorAll("#device-pod-sidebar .summary-tile")];
const summaryTile = document.querySelector('#device-pod-summary [data-device-detail="pod"]');
const dialog = document.querySelector("#device-detail-dialog");
const eventScroll = document.querySelector("#device-event-scroll");
const eventText = document.querySelector("#device-event-text");
let dialogOpened = false;
let dialogClosed = false;
if (summaryTile && dialog) {
summaryTile.click();
dialogOpened = dialog.open === true;
dialog.querySelector('button[value="close"]')?.click();
dialogClosed = dialog.open === false;
}
return {
summaryTileCount: summaryTiles.length,
summaryButtonsOnly: summaryTiles.length >= 2 && summaryTiles.every((tile) => tile.getAttribute("role") === "button" && tile.tabIndex === 0),
noInternalDisclosure: Boolean(sidebar && !sidebar.querySelector("details")),
noLegacySelectors: Boolean(sidebar && !sidebar.querySelector("[data-hardware-tab],#hardware-list,#panel-wiring,#records-list")),
dialogOpened,
dialogClosed,
eventStreamVisible: Boolean(eventScroll && eventText && eventText.textContent?.trim()),
eventScrollContained: Boolean(eventScroll && ["auto", "scroll"].includes(getComputedStyle(eventScroll).overflowY))
};
})(),
coreControlsVisible: {
commandInput: visible("#command-input"),
commandSend: visible("#command-send"),
agentChatStatus: visible("#agent-chat-status"),
conversationList: visible("#conversation-list"),
devicePodSelect: visible("#device-pod-select"),
devicePodSummary: visible("#device-pod-summary"),
devicePodEvents: visible("#device-event-scroll")
}
};
}, [...defaultHomepageForbiddenTerms]);
const pass =
login.loginVisible &&
login.workbenchHidden &&
login.copyOk &&
dom.title === "HWLAB 云工作台" &&
dom.bodyOverflow === "hidden" &&
dom.htmlOverflow === "hidden" &&
dom.workspaceHidden === false &&
dom.gateHidden === true &&
(dom.helpHidden === true || dom.helpHidden === null) &&
(dom.diagnosticsHidden === true || dom.diagnosticsHidden === null) &&
dom.outerScrollLocked &&
dom.rootAfterScrollAttempt.windowScrollY === 0 &&
dom.rootAfterScrollAttempt.htmlScrollTop === 0 &&
dom.rootAfterScrollAttempt.bodyScrollTop === 0 &&
dom.liveBuildDefaultVisible &&
/最新镜像构建时间:\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} 北京时间/u.test(dom.liveBuildLatest) &&
/hwlab-agent-mgr/u.test(dom.liveBuildLatest) &&
/tag f09ad05/u.test(dom.liveBuildLatest) &&
/commit f09ad05/u.test(dom.liveBuildLatest) &&
/revision f09ad05/u.test(dom.liveBuildLatest) &&
dom.liveBuildSummaryOpen === false &&
dom.liveBuildListVisibleWhenClosed === false &&
dom.liveBuildDetailWhenOpen.visible &&
dom.liveBuildDetailWhenOpen.summaryUsesDialog &&
dom.liveBuildDetailWhenOpen.rows >= 4 &&
dom.liveBuildDetailWhenOpen.scrollContained &&
dom.liveBuildDetailWhenOpen.overlay &&
dom.liveBuildDetailWhenOpen.closedByButton &&
dom.liveBuildDetailWhenOpen.overflowX <= 1 &&
/构建版本明细/u.test(dom.liveBuildDetailWhenOpen.title) &&
/hwlab-cloud-api/u.test(dom.liveBuildDetailWhenOpen.text) &&
/hwlab-agent-worker/u.test(dom.liveBuildDetailWhenOpen.text) &&
/构建时间不可用/u.test(dom.liveBuildDetailWhenOpen.text) &&
/外部镜像或非 HWLAB 构建产物/u.test(dom.liveBuildDetailWhenOpen.text) &&
/来源 live health/u.test(dom.liveBuildDetailWhenOpen.text) &&
dom.firstViewportForbiddenPresent.length === 0 &&
dom.labelsPresent &&
dom.navLabelsPresent &&
dom.defaultBackstageHidden &&
dom.gateRouteAvailable &&
dom.devicePod.summaryButtonsOnly &&
dom.devicePod.noInternalDisclosure &&
dom.devicePod.noLegacySelectors &&
dom.devicePod.dialogOpened &&
dom.devicePod.dialogClosed &&
dom.devicePod.eventStreamVisible &&
dom.devicePod.eventScrollContained &&
(postGuard ? postGuard.attempts.length === 0 : true) &&
Object.values(dom.coreControlsVisible).every(Boolean);
return {
status: pass ? "pass" : "blocked",
summary: pass ? "Browser DOM confirms the default workbench, core controls, and outer scroll lock." : "Browser DOM did not satisfy the workbench contract.",
evidence: [
`loginVisible=${login.loginVisible}`,
`workbenchHiddenBeforeLogin=${login.workbenchHidden}`,
`title=${dom.title}`,
`bodyOverflow=${dom.bodyOverflow}`,
`htmlOverflow=${dom.htmlOverflow}`,
`liveBuildLatest=${dom.liveBuildLatest}`,
`liveBuildRowsOpen=${dom.liveBuildDetailWhenOpen.rows}`,
`liveBuildOverflowX=${dom.liveBuildDetailWhenOpen.overflowX}`,
`firstViewportForbidden=${dom.firstViewportForbiddenPresent.join(",") || "none"}`,
`gateRouteAvailable=${dom.gateRouteAvailable}`,
`devicePodSummaryTiles=${dom.devicePod.summaryTileCount}`,
`devicePodDialogOpened=${dom.devicePod.dialogOpened}`,
`devicePodEventStream=${dom.devicePod.eventStreamVisible}`,
...(postGuard ? [`codeAgentPostAttempts=${postGuard.attempts.length}`] : [])
],
observations: {
login,
...dom,
...(postGuard ? { codeAgentPostGuard: postGuard.observation() } : {})
}
};
} catch (error) {
return {
status: "skip",
summary: `Browser DOM check skipped because the browser could not run: ${error.message}`,
evidence: ["browser launch or navigation failed"]
};
} finally {
if (browser) await browser.close();
}
}
async function inspectLiveHelpRoute(url, options = {}) {
let chromium;
try {
({ chromium } = await importPlaywright());
} catch (error) {
return {
status: "skip",
summary: `Browser Markdown help check skipped because Playwright is unavailable: ${error.message}`,
evidence: ["playwright import failed"]
};
}
let browser;
try {
browser = await launchChromium(chromium);
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
const postGuard = options.noCodeAgentPost ? await installNoCodeAgentPostGuard(page) : null;
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 12000 });
await loginWithDefaultCredentials(page);
await page.locator('[data-route="help"]').click();
await page.waitForFunction(() => document.querySelector("#help-content")?.dataset.helpState === "ready", null, { timeout: 8000 });
const help = await inspectHelpMarkdownRoute(page);
const pass = help.nonDefaultMarkdownHelp && help.termsPresent && (postGuard ? postGuard.attempts.length === 0 : true);
return {
status: pass ? "pass" : "blocked",
summary: pass
? "Desktop browser help route remains non-default, markdown-rendered, and scrollable."
: "Desktop browser help route did not satisfy the markdown workbench contract.",
evidence: [
`hash=${help.hash ?? "unknown"}`,
`heading=${help.heading ?? "unknown"}`,
`rootStillLocked=${help.rootStillLocked}`,
`helpPanelScrolls=${help.helpPanelScrolls}`,
...(postGuard ? [`codeAgentPostAttempts=${postGuard.attempts.length}`] : [])
],
observations: {
...help,
...(postGuard ? { codeAgentPostGuard: postGuard.observation() } : {})
}
};
} catch (error) {
return {
status: "blocked",
summary: `Desktop browser help route failed: ${error.message}`,
evidence: ["browser navigation or markdown render failed"]
};
} finally {
if (browser) await browser.close();
}
}
async function inspectLiveGateRoute(url, options = {}) {
let chromium;
try {
({ chromium } = await importPlaywright());
} catch (error) {
return {
status: "skip",
summary: `Browser Gate route check skipped because Playwright is unavailable: ${error.message}`,
evidence: ["playwright import failed"]
};
}
let browser;
try {
browser = await launchChromium(chromium);
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 });
await loginWithDefaultCredentials(page);
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 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,
title: document.title,
workspaceHidden: workspace ? workspace.hidden : null,
gateHidden: gateView ? gateView.hidden : null,
helpHidden: help ? help.hidden : null,
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, [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.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 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}`,
`heading=${gate.heading}`,
`tableCount=${gate.tableCount}`,
`rows=${gate.tableRows.length}`,
`statuses=${gate.tableStatuses.join(",") || "none"}`,
`backendRouteRequested=${gate.backendRouteRequested}`,
...(postGuard ? [`codeAgentPostAttempts=${postGuard.attempts.length}`] : [])
],
observations: {
...gate,
...(postGuard ? { codeAgentPostGuard: postGuard.observation() } : {})
}
};
} catch (error) {
return {
status: "blocked",
summary: `Browser Gate route failed: ${error.message}`,
evidence: ["browser navigation or route inspection failed"]
};
} finally {
if (browser) await browser.close();
}
}
export async function runDevCloudWorkbenchAuthFixtureSmoke() {
let chromium;
try {
({ chromium } = await importPlaywright());
} catch (error) {
const summary = `Auth fixture smoke skipped because Playwright is unavailable: ${error.message}`;
return {
status: "skip",
task: "DC-DCSN-P0-2026-003",
mode: "auth-fixture-browser",
evidenceLevel: "SOURCE",
devLive: false,
summary,
checks: [
{
id: "auth-browser-dependency",
status: "skip",
summary: playwrightDependencySummary,
evidence: [playwrightDependencyCommand, "package.json dependencies.playwright"]
}
],
blockers: [
{
type: "environment_blocker",
scope: "auth-browser-dependency",
status: "open",
summary
}
],
safety: staticSafety()
};
}
const server = await startStaticWebServer({ authFixture: true });
let browser;
try {
browser = await launchChromium(chromium);
const desktop = await inspectAuthFixtureViewport(browser, server.url, {
width: 1366,
height: 768,
isMobile: false
});
const mobile = await inspectAuthFixtureViewport(browser, server.url, {
width: 390,
height: 844,
isMobile: true
});
const checks = [
{
id: "auth-login-success-desktop",
status: desktop.success.pass ? "pass" : "blocked",
viewport: desktop.viewport,
summary: "Desktop login accepts admin/hwlab2026 and enters the current Cloud Workbench.",
observations: desktop.success
},
{
id: "auth-login-failure-desktop",
status: desktop.failure.pass ? "pass" : "blocked",
viewport: desktop.viewport,
summary: "Desktop login failure stays on the Chinese login page with a bounded error message.",
observations: desktop.failure
},
{
id: "auth-refresh-session-desktop",
status: desktop.refresh.pass ? "pass" : "blocked",
viewport: desktop.viewport,
summary: "Desktop refresh preserves the authenticated workbench session.",
observations: desktop.refresh
},
{
id: "auth-logout-expiry-desktop",
status: desktop.logout.pass && desktop.expired.pass ? "pass" : "blocked",
viewport: desktop.viewport,
summary: "Desktop logout and expired session both return to the login page.",
observations: {
logout: desktop.logout,
expired: desktop.expired
}
},
{
id: "auth-login-success-mobile",
status: mobile.success.pass ? "pass" : "blocked",
viewport: mobile.viewport,
summary: "390x844 mobile login enters the workbench without outer scrolling or hidden controls.",
observations: mobile.success
},
{
id: "auth-login-failure-mobile",
status: mobile.failure.pass ? "pass" : "blocked",
viewport: mobile.viewport,
summary: "390x844 mobile login failure keeps the form visible and error text contained.",
observations: mobile.failure
},
{
id: "auth-refresh-session-mobile",
status: mobile.refresh.pass ? "pass" : "blocked",
viewport: mobile.viewport,
summary: "390x844 mobile refresh preserves the authenticated session and workbench layout.",
observations: mobile.refresh
},
{
id: "auth-logout-expiry-mobile",
status: mobile.logout.pass && mobile.expired.pass ? "pass" : "blocked",
viewport: mobile.viewport,
summary: "390x844 mobile logout and expired session return to login without outer scroll.",
observations: {
logout: mobile.logout,
expired: mobile.expired
}
}
];
const blockers = checks
.filter((check) => check.status !== "pass")
.map((check) => ({
type: "runtime_blocker",
scope: check.id,
status: "open",
summary: check.summary,
viewport: check.viewport
}));
return {
status: blockers.length === 0 ? "pass" : "blocked",
task: "DC-DCSN-P0-2026-003",
mode: "auth-fixture-browser",
url: server.url,
generatedAt: new Date().toISOString(),
evidenceLevel: "SOURCE",
devLive: false,
summary: "Local browser fixture validates the Cloud Workbench login entry, failure message, session refresh, logout, expiry, and desktop/mobile layout.",
refs: ["pikasTech/HWLAB#357", "pikasTech/HWLAB#108"],
checks,
blockers,
safety: {
...staticSafety(),
localFixtureOnly: true,
hardwareWriteApis: false,
codeAgentPostSent: false,
statement: "Auth fixture uses local same-origin /auth endpoints only; it does not call hardware write APIs, post Code Agent chat, read Secrets, or deploy DEV."
}
};
} catch (error) {
return {
status: "blocked",
task: "DC-DCSN-P0-2026-003",
mode: "auth-fixture-browser",
url: server.url,
generatedAt: new Date().toISOString(),
evidenceLevel: "SOURCE",
devLive: false,
summary: `Auth fixture smoke failed: ${error.message}`,
checks: [],
blockers: [
{
type: "runtime_blocker",
scope: "auth-fixture-browser",
status: "open",
summary: error.message
}
],
safety: staticSafety()
};
} finally {
if (browser) await browser.close();
await server.close();
}
}
async function inspectAuthFixtureViewport(browser, url, viewport) {
const context = await browser.newContext({
viewport: { width: viewport.width, height: viewport.height },
deviceScaleFactor: 1,
isMobile: viewport.isMobile === true
});
try {
const failurePage = await context.newPage();
await failurePage.goto(url, { waitUntil: "networkidle", timeout: 15000 });
const initial = await inspectLoginPage(failurePage);
await failurePage.locator("#login-username").fill(defaultAuthCredentials.username);
await failurePage.locator("#login-password").fill("wrong-password");
await failurePage.locator("#login-submit").click();
await failurePage.waitForFunction(() => !document.querySelector("#login-error")?.hidden, null, { timeout: 8000 });
const failure = await inspectFailedLoginPage(failurePage, viewport);
await failurePage.close();
const successPage = await context.newPage();
await successPage.goto(url, { waitUntil: "networkidle", timeout: 15000 });
await loginWithDefaultCredentials(successPage);
const success = await inspectAuthenticatedWorkbench(successPage, viewport);
await successPage.reload({ waitUntil: "networkidle", timeout: 15000 });
const refresh = await inspectAuthenticatedWorkbench(successPage, viewport);
await successPage.locator("#logout-button").click();
await successPage.waitForFunction(() => document.querySelector("#login-shell")?.hidden === false, null, { timeout: 8000 });
const logout = await inspectLoginPage(successPage, viewport);
await successPage.close();
const expiredPage = await context.newPage();
await expiredPage.addInitScript(({ username }) => {
window.localStorage.setItem("hwlab.cloudWorkbench.auth.v1", JSON.stringify({
version: 1,
username,
issuedAt: Date.now() - 120000,
expiresAt: Date.now() - 1000
}));
}, { username: defaultAuthCredentials.username });
await expiredPage.goto(url, { waitUntil: "networkidle", timeout: 15000 });
const expired = await inspectLoginPage(expiredPage, viewport);
await expiredPage.close();
return {
viewport: { width: viewport.width, height: viewport.height },
initial,
failure,
success,
refresh,
logout,
expired
};
} finally {
await context.close();
}
}
async function loginWithDefaultCredentials(page) {
await page.waitForFunction(
() => document.querySelector("#command-input") !== null || document.querySelector("#login-username") !== null,
null,
{ timeout: 12000 }
).catch(() => {});
const loginVisible = await page.evaluate(() => {
const login = document.querySelector("#login-shell");
const username = document.querySelector("#login-username");
return Boolean(login && username && login.hidden === false);
});
if (!loginVisible) return;
await page.locator("#login-username").fill(defaultAuthCredentials.username);
await page.locator("#login-password").fill(defaultAuthCredentials.password);
await page.locator("#login-submit").click();
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
}
async function inspectLoginPage(page, viewport = null) {
return page.evaluate((viewport) => {
const login = document.querySelector("#login-shell");
const panel = document.querySelector(".login-panel");
const appShell = document.querySelector("[data-app-shell]");
const title = document.querySelector("#login-title")?.textContent?.trim() ?? "";
const text = login?.textContent?.replace(/\s+/gu, " ").trim() ?? "";
const panelBox = panel?.getBoundingClientRect();
const inputBoxes = [...document.querySelectorAll(".login-form input, #login-submit")].map((element) => {
const box = element.getBoundingClientRect();
return {
id: element.id,
width: box.width,
height: box.height,
left: box.left,
right: box.right,
scrollWidth: element.scrollWidth,
clientWidth: element.clientWidth
};
});
const forbiddenTerms = ["Gate", "诊断", "验收", "后厨", "BLOCKED", "SOURCE", "DEV-LIVE", "trace", "Secret"];
const rootScrollLocked =
getComputedStyle(document.documentElement).overflow === "hidden" &&
getComputedStyle(document.body).overflow === "hidden" &&
document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 &&
document.body.scrollHeight <= document.body.clientHeight + 2;
const noHorizontalOverflow =
document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2 &&
document.body.scrollWidth <= document.body.clientWidth + 2 &&
inputBoxes.every((box) => box.scrollWidth <= box.clientWidth + 2);
const panelInViewport = Boolean(panelBox) &&
panelBox.left >= -1 &&
panelBox.right <= window.innerWidth + 1 &&
panelBox.top >= -1 &&
panelBox.bottom <= window.innerHeight + 1;
const copyOk =
title === "云工作台登录" &&
["请输入账号和密码。", "用户名", "密码", "登录"].every((term) => text.includes(term)) &&
forbiddenTerms.every((term) => !text.includes(term));
const loginVisible = Boolean(login && login.hidden === false && panelBox && panelBox.width > 0 && panelBox.height > 0);
const workbenchHidden = appShell ? appShell.hidden === true : false;
return {
viewport: viewport ?? { width: window.innerWidth, height: window.innerHeight },
title,
textSample: text.slice(0, 180),
bodyAuthState: document.body.dataset.authState ?? "",
loginVisible,
workbenchHidden,
copyOk,
rootScrollLocked,
noHorizontalOverflow,
panelInViewport,
inputBoxes,
pass: loginVisible && workbenchHidden && copyOk && rootScrollLocked && noHorizontalOverflow && panelInViewport
};
}, viewport);
}
async function inspectFailedLoginPage(page, viewport) {
return page.evaluate((viewport) => {
const error = document.querySelector("#login-error");
const password = document.querySelector("#login-password");
const login = document.querySelector("#login-shell");
const appShell = document.querySelector("[data-app-shell]");
const errorBox = error?.getBoundingClientRect();
const message = error?.textContent?.replace(/\s+/gu, " ").trim() ?? "";
const internalLeak = /stack|trace|secret|token|HWLAB_CLOUD|Exception|Error:/iu.test(message);
const rootScrollLocked =
getComputedStyle(document.documentElement).overflow === "hidden" &&
getComputedStyle(document.body).overflow === "hidden" &&
document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 &&
document.body.scrollHeight <= document.body.clientHeight + 2;
const errorContained = Boolean(errorBox) &&
errorBox.width > 0 &&
error.scrollWidth <= error.clientWidth + 2 &&
errorBox.left >= 0 &&
errorBox.right <= window.innerWidth + 1;
const pass =
login?.hidden === false &&
appShell?.hidden === true &&
message === "账号或密码不正确,请重新输入。" &&
document.activeElement === password &&
!internalLeak &&
rootScrollLocked &&
errorContained;
return {
viewport,
pass,
message,
internalLeak,
passwordFocused: document.activeElement === password,
loginVisible: login?.hidden === false,
workbenchHidden: appShell?.hidden === true,
rootScrollLocked,
errorContained
};
}, viewport);
}
async function inspectAuthenticatedWorkbench(page, viewport) {
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
return page.evaluate((viewport) => {
const login = document.querySelector("#login-shell");
const appShell = document.querySelector("[data-app-shell]");
const visible = (selector) => {
const element = document.querySelector(selector);
if (!element) return false;
const box = element.getBoundingClientRect();
const style = getComputedStyle(element);
return box.width > 0 && box.height > 0 && style.visibility !== "hidden" && style.display !== "none";
};
window.scrollTo(0, 240);
document.documentElement.scrollTop = 240;
document.body.scrollTop = 240;
const rootAfterScrollAttempt = {
windowScrollY: window.scrollY,
htmlScrollTop: document.documentElement.scrollTop,
bodyScrollTop: document.body.scrollTop
};
const rootScrollLocked =
getComputedStyle(document.documentElement).overflow === "hidden" &&
getComputedStyle(document.body).overflow === "hidden" &&
document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 &&
document.body.scrollHeight <= document.body.clientHeight + 2 &&
rootAfterScrollAttempt.windowScrollY === 0 &&
rootAfterScrollAttempt.htmlScrollTop === 0 &&
rootAfterScrollAttempt.bodyScrollTop === 0;
const noHorizontalOverflow =
document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2 &&
document.body.scrollWidth <= document.body.clientWidth + 2;
const controlsVisible = {
commandInput: visible("#command-input"),
commandSend: visible("#command-send"),
logout: visible("#logout-button"),
devicePodSummary: visible("#device-pod-summary"),
deviceEventStream: visible("#device-event-scroll")
};
const pass =
login?.hidden === true &&
appShell?.hidden === false &&
document.body.dataset.authState === "authenticated" &&
rootScrollLocked &&
noHorizontalOverflow &&
Object.values(controlsVisible).every(Boolean);
return {
viewport,
pass,
loginHidden: login?.hidden === true,
workbenchVisible: appShell?.hidden === false,
bodyAuthState: document.body.dataset.authState ?? "",
rootScrollLocked,
rootAfterScrollAttempt,
noHorizontalOverflow,
controlsVisible
};
}, viewport);
}
async function installNoCodeAgentPostGuard(page) {
const attempts = [];
await page.route("**/v1/agent/chat", async (route) => {
const request = route.request();
const attempt = {
method: request.method(),
urlPath: new URL(request.url()).pathname,
status: "aborted_by_dom_only_guard"
};
attempts.push(attempt);
await route.abort("blockedbyclient");
});
return {
attempts,
observation: () => ({
installed: true,
blockedPath: "/v1/agent/chat",
attemptedCount: attempts.length,
attempts
})
};
}
async function inspectLiveCodeAgentE2e(url) {
let chromium;
try {
({ chromium } = await importPlaywright());
} catch (error) {
return {
status: "skip",
summary: `浏览器 Code Agent E2E 跳过:Playwright 不可用:${error.message}`,
evidence: ["playwright import failed"]
};
}
let browser;
try {
browser = await launchChromium(chromium);
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 12000 });
await loginWithDefaultCredentials(page);
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
const controls = await inspectJourneyControls(page);
if (!Object.values(controls).every(Boolean)) {
return {
status: "blocked",
summary: "浏览器故障:无法可靠定位 Workbench Code Agent 控件。",
evidence: ["selectors=#command-input,#command-send,#agent-chat-status,#conversation-list,#device-pod-summary,#device-event-scroll"],
observations: { controls }
};
}
const promptResults = [];
for (const prompt of codeAgentE2ePrompts) {
promptResults.push(await runCodeAgentPromptJourney(page, prompt));
}
const firstBlocked = promptResults.find((result) => result.status === "blocked");
const degraded = promptResults.some((result) => result.status === "degraded");
const status = firstBlocked ? "blocked" : degraded ? "degraded" : "pass";
return {
status,
summary: status === "pass"
? "DEV-LIVE 浏览器 E2E 已发送两个 Code Agent 提示,并确认旧 4500ms 窗口未形成永久失败。"
: status === "degraded"
? "DEV-LIVE 浏览器 E2E 未命中旧 4500ms 永久失败路径;至少一个提示在配置的长时间边界后返回清晰后端超时。"
: firstBlocked.classification?.chineseSummary ?? firstBlocked.summary,
evidence: [
`prompts=${promptResults.length}`,
`legacyFailureWindowMs=${legacyFailureWindowMs}`,
`codeAgentTimeoutMs=${codeAgentLongTimeoutMs}`,
...promptResults.map((result) => `${result.promptId}: status=${result.status}; blocker=${result.classification?.blocker ?? "none"}; traceId=${result.response?.traceId ?? result.traceId ?? "unknown"}`)
],
observations: {
controls,
request: promptResults.length === 1 ? promptResults[0].request : {
method: "POST",
status: promptResults.every((result) => result.request?.status === 200) ? 200 : "mixed",
urlPath: "/v1/agent/chat",
count: promptResults.length
},
response: promptResults[0]?.response ?? blockedAgentChatResponse("not_observed"),
classification: firstBlocked?.classification ?? promptResults.find((result) => result.status === "degraded")?.classification ?? {
status: "pass",
blocker: null,
reason: "both configured Code Agent browser prompts completed without a permanent 4500ms UI failure"
},
prompts: promptResults,
ui: promptResults.at(-1)?.ui ?? null,
networkEvents: promptResults.flatMap((result) => result.networkEvents ?? []),
legacyFailureWindowMs,
codeAgentTimeoutMs: codeAgentLongTimeoutMs
}
};
} catch (error) {
const classification = classifyCodeAgentBrowserFailure(error);
return {
status: "blocked",
summary: classification.chineseSummary,
evidence: ["browser navigation, selector, or POST failed", `category=${classification.category}`],
observations: { classification }
};
} finally {
if (browser) await browser.close();
}
}
async function runCodeAgentPromptJourney(page, prompt, options = {}) {
const agentRequests = [];
const agentResponses = [];
const requestListener = (request) => {
if (!request.url().includes("/v1/agent/chat")) return;
const body = parseJsonOrNull(request.postData() ?? "");
agentRequests.push({
method: request.method(),
urlPath: new URL(request.url()).pathname,
traceId: stringOrFallback(body?.traceId, request.headers()["x-trace-id"] ?? "unknown"),
message: typeof body?.message === "string" ? body.message : "",
messageLength: typeof body?.message === "string" ? body.message.length : 0,
conversationId: typeof body?.conversationId === "string" ? body.conversationId : null,
sessionId: typeof body?.sessionId === "string" ? body.sessionId : null,
threadId: typeof body?.threadId === "string" ? body.threadId : null
});
};
const responseListener = async (response) => {
if (!response.url().includes("/v1/agent/chat")) return;
let body = null;
try {
body = await response.json();
} catch {
body = null;
}
agentResponses.push({
method: response.request().method(),
status: response.status(),
urlPath: new URL(response.url()).pathname,
body: sanitizeAgentChatBody(body, { httpStatus: response.status() })
});
};
page.on("request", requestListener);
page.on("response", responseListener);
try {
const beforeMessageCount = await page.locator(".message-card").count();
await page.locator("#command-input").fill(prompt.text);
let responseSeenBeforeLegacyWindow = false;
const responsePromise = page.waitForResponse(
(response) => response.url().includes("/v1/agent/chat") && response.request().method() === "POST",
{ timeout: codeAgentLongTimeoutMs + 5000 }
).then((response) => {
responseSeenBeforeLegacyWindow = true;
return response;
});
await page.locator("#command-send").click();
await page.waitForTimeout(legacyFailureWindowMs + 350);
const legacyWindow = await inspectLegacyFailureWindow(page, { beforeMessageCount });
legacyWindow.responseSeenBeforeLegacyWindow = responseSeenBeforeLegacyWindow;
if (legacyWindow.permanentFailureAround4500ms && !responseSeenBeforeLegacyWindow) {
responsePromise.catch(() => null);
const ui = await inspectJourneyUi(page).catch(() => null);
const traceId = agentResponses.at(-1)?.body?.traceId ?? agentRequests.at(-1)?.traceId ?? "unknown";
return {
promptId: prompt.id,
promptLabel: prompt.label,
status: "blocked",
summary: `浏览器故障:前端在 ${legacyFailureWindowMs}ms 附近把 Code Agent 请求标为永久失败;traceId=${traceId}。`,
traceId,
request: {
method: "POST",
status: "not_observed",
urlPath: "/v1/agent/chat"
},
response: blockedAgentChatResponse("frontend-legacy-timeout"),
classification: {
status: "blocked",
blocker: "frontend-legacy-timeout",
category: "browser",
chineseSummary: `浏览器故障:前端在 ${legacyFailureWindowMs}ms 附近把 Code Agent 请求标为永久失败;traceId=${traceId}。`,
reason: "frontend marked Code Agent request as permanent failure around the old 4500ms timeout path"
},
ui,
legacyWindow,
requestEvents: agentRequests,
networkEvents: agentResponses
};
}
const response = await responsePromise;
let responseBody = null;
try {
responseBody = await response.json();
} catch {
responseBody = null;
}
if (options.sourceFixture === true) {
await page.waitForFunction(
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "SOURCE 回复",
null,
{ timeout: 8000 }
).catch(() => null);
} else {
await page.waitForTimeout(500);
}
const ui = await inspectJourneyUi(page);
const responseSummary = sanitizeAgentChatBody(responseBody, { httpStatus: response.status() });
let classification = classifyCodeAgentBrowserJourney({
responseSummary,
httpOk: response.ok(),
httpStatus: response.status(),
ui
});
if (
options.sourceFixture === true &&
response.ok() &&
responseSummary?.status === "completed" &&
responseSummary?.sourceKind === "SOURCE" &&
responseSummary?.hasReply === true &&
ui.agentChatStatus === "SOURCE 回复"
) {
classification = {
status: "pass",
blocker: null,
sourceOnly: true,
reason: "SOURCE fixture completed through the browser path; this is not DEV-LIVE evidence",
chineseSummary: "SOURCE fixture 完成:浏览器链路通过,但该证据不能标为 DEV-LIVE。"
};
}
const legacyWindowPass = !legacyWindow.permanentFailureAround4500ms;
const traceId = responseSummary?.traceId ?? agentResponses.at(-1)?.body?.traceId ?? agentRequests.at(-1)?.traceId ?? "unknown";
const backendTimeoutMs = backendTimeoutMsFromPayload(responseBody);
if (
legacyWindowPass &&
response.ok() &&
responseSummary?.status === "failed" &&
responseSummary?.error?.code === "provider_unavailable" &&
backendTimeoutMs !== null &&
backendTimeoutMs >= legacyFailureWindowMs
) {
classification = {
status: "degraded",
blocker: null,
terminalState: "backend-timeout",
backendTimeoutMs,
traceId,
chineseSummary: `后端超时:Code Agent 在配置的长时间边界后返回清晰超时,不是旧 ${legacyFailureWindowMs}ms 前端永久失败;traceId=${traceId}。`,
reason: "clear backend timeout after the configured longer bound"
};
}
const accepted = ["pass", "degraded"].includes(classification.status) && legacyWindowPass;
const returnedClassification = accepted ? classification : legacyWindowPass ? classification : {
status: "blocked",
blocker: "frontend-legacy-timeout",
category: "browser",
chineseSummary: `浏览器故障:前端在 ${legacyFailureWindowMs}ms 附近把 Code Agent 请求标为永久失败;traceId=${traceId}。`,
reason: "frontend marked heavy Code Agent request as permanent failure around the old 4500ms timeout path"
};
return {
promptId: prompt.id,
promptLabel: prompt.label,
status: accepted ? classification.status : "blocked",
summary: accepted
? classification.terminalState === "backend-timeout"
? `${prompt.label}返回清晰后端超时,且 ${legacyFailureWindowMs}ms 窗口没有永久失败。`
: `${prompt.label}完成,且 ${legacyFailureWindowMs}ms 窗口没有永久失败。`
: returnedClassification.chineseSummary ?? returnedClassification.reason,
traceId,
request: {
method: response.request().method(),
status: response.status(),
urlPath: new URL(response.url()).pathname
},
response: responseSummary,
classification: returnedClassification,
ui,
legacyWindow,
requestEvents: agentRequests,
networkEvents: agentResponses
};
} catch (error) {
const traceId = agentResponses.at(-1)?.body?.traceId ?? agentRequests.at(-1)?.traceId ?? null;
const classification = classifyCodeAgentBrowserFailure(error, { traceId });
return {
promptId: prompt.id,
promptLabel: prompt.label,
status: "blocked",
summary: classification.chineseSummary,
traceId: traceId ?? "unknown",
request: {
method: "POST",
status: "not_observed",
urlPath: "/v1/agent/chat"
},
response: blockedAgentChatResponse(classification.blocker),
classification,
ui: await inspectJourneyUi(page).catch(() => null),
legacyWindow: null,
requestEvents: agentRequests,
networkEvents: agentResponses
};
} finally {
page.off("request", requestListener);
page.off("response", responseListener);
}
}
async function inspectJourneyControls(page) {
return page.evaluate(() => {
const visible = (selector) => {
const element = document.querySelector(selector);
if (!element) return false;
const style = getComputedStyle(element);
const box = element.getBoundingClientRect();
return style.display !== "none" && style.visibility !== "hidden" && box.width > 0 && box.height > 0;
};
return {
appShell: visible("[data-app-shell]"),
workspace: document.querySelector('[data-view="workspace"]')?.hidden === false,
commandInput: visible("#command-input"),
commandSend: visible("#command-send"),
agentChatStatus: visible("#agent-chat-status"),
conversationList: visible("#conversation-list"),
devicePodSelect: visible("#device-pod-select"),
devicePodSummary: visible("#device-pod-summary"),
devicePodEvents: visible("#device-event-scroll")
};
});
}
async function inspectJourneyUi(page) {
return page.evaluate(() => {
const summarizeActions = (statusClass) => {
const cards = [...document.querySelectorAll(`.message-card.${statusClass}`)];
const labels = cards.flatMap((card) =>
[...card.querySelectorAll(".message-action")].map((button) => button.textContent?.replace(/\s+/gu, " ").trim() ?? "")
);
const panels = cards.flatMap((card) =>
[...card.querySelectorAll(".message-actions")].map((panel) => {
const box = panel.getBoundingClientRect();
return {
width: box.width,
left: box.left,
right: box.right,
scrollWidth: panel.scrollWidth,
clientWidth: panel.clientWidth,
contained: box.left >= -1 && box.right <= window.innerWidth + 1 && panel.scrollWidth <= panel.clientWidth + 2
};
})
);
return {
retryVisible: labels.includes("重试上一条"),
traceVisible: labels.includes("回放 trace"),
cancelVisible: labels.includes("取消当前请求"),
contained: panels.length > 0 && panels.every((panel) => panel.contained)
};
};
const failedText = [...document.querySelectorAll(".message-card.status-failed")]
.map((element) => element.textContent ?? "")
.join("\n");
const runtimePathPanel = document.querySelector(".message-card.message-agent .message-runtime-path");
const runtimePathTrigger = runtimePathPanel?.querySelector(".message-compact-summary");
const runtimePathText = runtimePathPanel?.textContent ?? "";
let runtimePathDialogText = "";
let runtimePathDialogLabel = "";
if (runtimePathTrigger instanceof HTMLElement) {
runtimePathTrigger.click();
const runtimePathDialog = document.querySelector('.workbench-dialog[role="dialog"][aria-modal="true"]');
runtimePathDialogText = runtimePathDialog?.textContent ?? "";
runtimePathDialogLabel = runtimePathDialog?.getAttribute("aria-label") ?? "";
runtimePathDialog?.querySelector(".workbench-dialog-close")?.click();
}
const pendingText = [...document.querySelectorAll(".message-card.status-running")]
.map((element) => element.textContent ?? "")
.join("\n");
const commandValue = document.querySelector("#command-input")?.value ?? "";
const pendingRows = [...document.querySelectorAll(".message-card.status-running .message-pending-row")].map((row) => {
const box = row.getBoundingClientRect();
return {
text: row.textContent?.replace(/\s+/gu, " ").trim() ?? "",
width: box.width,
scrollWidth: row.scrollWidth,
clientWidth: row.clientWidth,
contained: row.scrollWidth <= row.clientWidth + 2
};
});
const runningCardBoxes = [...document.querySelectorAll(".message-card.status-running")].map((card) => {
const box = card.getBoundingClientRect();
return {
width: box.width,
left: box.left,
right: box.right,
scrollWidth: card.scrollWidth,
clientWidth: card.clientWidth,
contained: box.left >= -1 && box.right <= window.innerWidth + 1 && card.scrollWidth <= card.clientWidth + 2
};
});
const runningActions = summarizeActions("status-running");
const failedActions = summarizeActions("status-failed");
const timeoutActions = summarizeActions("status-timeout");
const tracePanels = [...document.querySelectorAll(".message-card.message-agent .message-trace")];
const tracePanelMetrics = tracePanels.map((panel) => {
const countText = panel.querySelector(".message-trace-count")?.textContent?.replace(/\s+/gu, " ").trim() ?? "";
const actions = [...panel.querySelectorAll(".message-trace-action")].map((button) => button.textContent?.replace(/\s+/gu, " ").trim() ?? "");
const events = [...panel.querySelectorAll(".message-trace-events li")].map((item) => item.textContent?.replace(/\s+/gu, " ").trim() ?? "");
return {
countText,
actions,
eventCount: events.length,
traceMode: panel.querySelector(".message-trace-events")?.dataset.traceMode ?? "",
allTraceVisible: /显示全部可读事件\s+\d+\s*\/\s*已载入原始\s+\d+\s*\/\s*后端原始\s+\d+/u.test(countText),
copyJsonVisible: actions.includes("复制 JSON"),
downloadTraceVisible: actions.includes("下载 trace"),
fixedAllMode: panel.querySelector(".message-trace-events")?.dataset.traceMode === "all"
};
});
return {
title: document.title,
workspaceHidden: document.querySelector('[data-view="workspace"]')?.hidden ?? null,
gateHidden: document.querySelector('[data-view="gate"]')?.hidden ?? null,
helpHidden: document.querySelector('[data-view="help"]')?.hidden ?? null,
agentChatStatus: document.querySelector("#agent-chat-status")?.textContent?.trim() ?? null,
inputCleared: commandValue === "",
retryInputPreserved: commandValue.trim().length > 0,
retryInputValue: commandValue,
completedMessageVisible: Boolean(document.querySelector(".message-card.status-completed")),
sourceMessageVisible: Boolean(document.querySelector(".message-card.status-source")),
failedMessageVisible: Boolean(document.querySelector(".message-card.status-failed")),
failedMessageHasTrace: /trace=trc_/u.test(failedText),
failedMessageHasChineseTimeout: /无新事件|请求超时|保留输入|重试/u.test(failedText),
runningMessageVisible: Boolean(document.querySelector(".message-card.status-running")),
pendingMessageText: pendingText.replace(/\s+/gu, " ").trim(),
pendingMessageChinese: /正在处理|后端处理中|不会把旧 4500ms|结构化 blocker|真实超时/u.test(pendingText),
pendingMessageHasTrace: /traceId\s*trc_|trace=trc_|trc_/u.test(pendingText),
pendingMessageHasConversation: /conversation\s*cnv_|conversation=cnv_|cnv_/u.test(pendingText),
pendingMessageHasSession: /session\s*(等待后端分配|cnv_|ses_)/u.test(pendingText),
pendingTraceCopyVisible: pendingRows.some((row) => /traceId/u.test(row.text) && /复制/u.test(row.text)),
pendingRetryActionVisible: runningActions.retryVisible,
pendingTraceActionVisible: runningActions.traceVisible,
pendingCancelActionVisible: runningActions.cancelVisible,
pendingActionPanelContained: runningActions.contained,
failedRetryActionVisible: failedActions.retryVisible,
failedTraceActionVisible: failedActions.traceVisible,
failedActionPanelContained: failedActions.contained,
timeoutRetryActionVisible: timeoutActions.retryVisible,
timeoutTraceActionVisible: timeoutActions.traceVisible,
timeoutActionPanelContained: timeoutActions.contained,
pendingContextRowsContained: pendingRows.length > 0 && pendingRows.every((row) => row.contained),
runningCardsContained: runningCardBoxes.length > 0 && runningCardBoxes.every((box) => box.contained),
messageCount: document.querySelectorAll(".message-card").length,
completedMessageHasNonSensitiveMeta: Boolean(
[...document.querySelectorAll(".message-card.status-completed .message-meta")]
.some((element) => /codex-stdio|gpt-5\.5|runner=|workspace=|toolCalls=|trc_|trace\s+trc_/u.test(element.textContent ?? ""))
),
sourceMessageHasNonSensitiveMeta: Boolean(
[...document.querySelectorAll(".message-card.status-source .message-meta")]
.some((element) => /SOURCE|source-fixture|trc_/u.test(element.textContent ?? ""))
),
traceText: tracePanels
.map((element) => element.textContent ?? "")
.join("\n"),
traceHasTraceId: tracePanels
.some((element) => /trc_/u.test(element.textContent ?? "")),
traceShowsEvents: [...document.querySelectorAll(".message-card.message-agent .message-trace-events li")]
.some((element) => /#\d+|session|prompt|tool|assistant|timeout|blocked/u.test(element.textContent ?? "")),
tracePanelMetrics,
traceAllModeVisible: tracePanelMetrics.some((metric) => metric.allTraceVisible && metric.fixedAllMode),
traceCopyJsonVisible: tracePanelMetrics.some((metric) => metric.copyJsonVisible),
traceDownloadVisible: tracePanelMetrics.some((metric) => metric.downloadTraceVisible),
runtimePathVisible: Boolean(runtimePathPanel),
runtimePathCompactSummaryVisible: Boolean(runtimePathTrigger),
runtimePathUsesCompactDialog: runtimePathTrigger?.getAttribute("aria-haspopup") === "dialog" &&
/运行路径明细/u.test(`${runtimePathDialogLabel} ${runtimePathDialogText}`),
runtimePathShowsProviderFields: /provider|runnerKind|protocol|implementationType|providerTrace\.command|providerTrace\.terminalStatus/u.test(runtimePathDialogText),
runtimePathSummaryStaysCompact: !/providerTrace\.command|providerTrace\.terminalStatus|protocol/u.test(runtimePathText),
runtimePathFallbackNotFull: !/(OpenAI text fallback|source-fixture|SOURCE)[\s\S]{0,120}(真实 runner|repo-owned Codex app-server stdio)/u.test(runtimePathText),
noMainAttributionNoise: document.querySelectorAll(".message-card.message-agent .message-attribution, .message-card.message-agent .message-evidence, .message-card.message-agent .code-agent-facts").length === 0,
attributionFallbackNotRunnerControl: !/openai-responses-fallback[\s\S]{0,80}(真实 runner|Codex stdio 长会话|workspace-write)/u.test(
tracePanels
.map((element) => element.textContent ?? "")
.join("\n")
),
conversationFactsVisible: /conversationFacts|fact traces|fact skills|fact tools|turns=|sessionId=|workspace=/u.test(
[
document.querySelector("#code-agent-summary")
]
.map((element) => element?.textContent ?? "")
.join("\n")
)
};
});
}
async function inspectLegacyFailureWindow(page, { beforeMessageCount }) {
return page.evaluate(({ beforeMessageCount, legacyFailureWindowMs }) => {
const messages = [...document.querySelectorAll(".message-card")];
const newMessages = messages.slice(beforeMessageCount);
const failed = newMessages.filter((element) => element.classList.contains("status-failed"));
const latestAgent = [...newMessages].reverse().find((element) => element.classList.contains("message-agent"));
const agentChatStatus = document.querySelector("#agent-chat-status")?.textContent?.trim() ?? null;
const pendingText = latestAgent?.classList.contains("status-running")
? latestAgent.textContent?.replace(/\s+/gu, " ").trim() ?? ""
: "";
const pendingRows = latestAgent
? [...latestAgent.querySelectorAll(".message-pending-row")].map((row) => ({
text: row.textContent?.replace(/\s+/gu, " ").trim() ?? "",
scrollWidth: row.scrollWidth,
clientWidth: row.clientWidth,
contained: row.scrollWidth <= row.clientWidth + 2
}))
: [];
const pendingActionLabels = latestAgent
? [...latestAgent.querySelectorAll(".message-action")].map((button) => button.textContent?.replace(/\s+/gu, " ").trim() ?? "")
: [];
const pendingActionPanels = latestAgent
? [...latestAgent.querySelectorAll(".message-actions")].map((panel) => ({
scrollWidth: panel.scrollWidth,
clientWidth: panel.clientWidth,
contained: panel.scrollWidth <= panel.clientWidth + 2
}))
: [];
const pendingTraceEvents = latestAgent
? [...latestAgent.querySelectorAll(".message-trace-events li")].map((item) => item.textContent?.replace(/\s+/gu, " ").trim() ?? "")
: [];
const latestAgentBox = latestAgent?.getBoundingClientRect();
const pendingStillRunning =
latestAgent?.classList.contains("status-running") === true ||
agentChatStatus === "处理中" ||
agentChatStatus === "运行中";
return {
checkedAtMs: legacyFailureWindowMs,
beforeMessageCount,
messageCount: messages.length,
newMessageCount: newMessages.length,
agentChatStatus,
failedMessageVisible: failed.length > 0,
runningMessageVisible: Boolean(latestAgent?.classList.contains("status-running")),
pendingChineseVisible: /正在处理|后端处理中|不会把旧 4500ms|结构化 blocker|真实超时/u.test(pendingText),
pendingTraceVisible: /traceId\s*trc_|trace=trc_|trc_/u.test(pendingText),
pendingTraceShowsEvents: pendingTraceEvents.some((text) => /#\d+|session|prompt|tool|assistant/u.test(text)),
pendingConversationVisible: /conversation\s*cnv_|conversation=cnv_|cnv_/u.test(pendingText),
pendingSessionVisible: /session\s*(等待后端分配|cnv_|ses_)/u.test(pendingText),
pendingTraceCopyVisible: pendingRows.some((row) => /traceId/u.test(row.text) && /复制/u.test(row.text)),
pendingRetryActionVisible: pendingActionLabels.includes("重试上一条"),
pendingTraceActionVisible: pendingActionLabels.includes("回放 trace"),
pendingActionsContained: pendingActionPanels.length > 0 && pendingActionPanels.every((panel) => panel.contained),
pendingRowsContained: pendingRows.length > 0 && pendingRows.every((row) => row.contained),
runningCardContained: latestAgentBox
? latestAgentBox.left >= -1 &&
latestAgentBox.right <= window.innerWidth + 1 &&
latestAgent.scrollWidth <= latestAgent.clientWidth + 2
: false,
permanentFailureAround4500ms: failed.length > 0 && !pendingStillRunning
};
}, { beforeMessageCount, legacyFailureWindowMs });
}
function backendTimeoutMsFromPayload(payload) {
const message = String(payload?.error?.message ?? "");
const match = message.match(/(?:timed out after|timeout after|请求超时|请求超过|超过)\s*(\d+)\s*ms/iu);
if (!match) return null;
const value = Number.parseInt(match[1], 10);
return Number.isInteger(value) && value > 0 ? value : null;
}
async function inspectDefaultApiStatus(page) {
return page.evaluate(() => {
const liveStatus = document.querySelector("#live-status")?.textContent?.trim() ?? "";
const liveDetail = document.querySelector("#live-detail")?.textContent?.trim() ?? "";
const firstScreenText = document.querySelector(".topbar")?.textContent?.replace(/\s+/gu, " ").trim() ?? "";
const forbidden = /Gate|诊断|验收|BLOCKED|M0-M5|执行轨迹|DB live readiness/u;
return {
liveStatus,
liveDetail,
firstScreenText,
actionableStatusVisible:
["API 正常", "API 错误", "等待验证", "只读模式"].includes(liveStatus) &&
/^(通过|错误|等待验证|只读):/u.test(liveDetail) &&
/hwlab-[a-z-]+ .*(\/health\/live|\/v1|\/json-rpc|\/v1\/agent\/chat|\/v1\/m3\/io|\/v1\/m3\/status)/u.test(liveDetail) &&
!/API 降级|degraded\/read-only|blocked\/degraded\/failed/u.test(`${liveStatus} ${liveDetail}`),
defaultCopyIsWorkbenchSafe: !forbidden.test(`${liveStatus} ${liveDetail} ${firstScreenText}`)
};
});
}
export function sanitizeAgentChatBody(body, options = {}) {
return summarizeCodeAgentPayload(body, options);
}
function blockedAgentChatResponse(code) {
return {
status: "failed",
provider: "not_observed",
model: "not_observed",
backend: "not_observed",
traceId: "not_observed",
hasReply: false,
error: {
code,
missingEnv: []
}
};
}
export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() {
return runLocalAgentFixtureSmoke({
mode: "local-agent-fixture-browser",
responseDelayMs: localAgentFixtureDelayMs,
expectTimeout: false
});
}
export async function runDevCloudWorkbenchSessionContinuityFixtureSmoke(options = {}) {
let chromium;
try {
({ chromium } = await importPlaywright());
} catch (error) {
const summary = `Code Agent session continuity fixture skipped because Playwright is unavailable: ${error.message}`;
return {
status: "skip",
task: "DC-DCSN-P0-2026-003",
mode: "local-agent-session-continuity-fixture-browser",
evidenceLevel: "SOURCE",
devLive: false,
summary,
checks: [{
id: "local-agent-session-continuity-dependency",
status: "skip",
summary: playwrightDependencySummary,
evidence: [playwrightDependencyCommand, "package.json dependencies.playwright"]
}],
blockers: [{
type: "environment_blocker",
scope: "local-agent-session-continuity-dependency",
status: "open",
summary
}],
safety: staticSafety()
};
}
const server = await startStaticWebServer({
agentFixture: true,
agentFixtureMode: "session-continuity",
agentDelayMs: options.responseDelayMs ?? 80
});
let browser;
const requests = [];
try {
browser = await launchChromium(chromium);
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
page.on("request", (request) => {
const requestUrl = new URL(request.url());
if (request.method() !== "POST" || requestUrl.pathname !== "/v1/agent/chat") return;
const body = parseJsonOrNull(request.postData() ?? "");
requests.push({
message: body?.message ?? "",
conversationId: body?.conversationId ?? null,
sessionId: body?.sessionId ?? null,
threadId: body?.threadId ?? null,
traceId: body?.traceId ?? null,
hasConversationContext: Boolean(body?.conversationContext)
});
});
await page.goto(server.url, { waitUntil: "networkidle", timeout: 15000 });
await loginWithDefaultCredentials(page);
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
await page.waitForFunction(
() => document.querySelector("#live-status")?.textContent?.trim() === "只读模式",
null,
{ timeout: 8000 }
);
const first = await runCodeAgentPromptJourney(page, {
id: "continuity-first",
label: "第一轮",
text: "第一轮:请记住当前会话"
}, { sourceFixture: true });
const second = await runCodeAgentPromptJourney(page, {
id: "continuity-second",
label: "第二轮",
text: "第二轮:继续同一会话"
}, { sourceFixture: true });
await page.locator("#command-input").fill("触发 providerTrace 缺失");
await page.locator("#command-send").click();
await page.waitForFunction(
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "Session 受阻",
null,
{ timeout: 8000 }
);
await page.locator(".message-card.status-failed .message-action-retry").last().click();
await page.waitForFunction(
() => document.querySelectorAll(".message-card.status-failed").length >= 2,
null,
{ timeout: 8000 }
);
const ui = await inspectJourneyUi(page);
const text = await page.evaluate(() => document.body.textContent?.replace(/\s+/gu, " ").trim() ?? "");
const [firstRequest, secondRequest, failedRequest, retryRequest] = requests;
const continuityOk = Boolean(
firstRequest?.conversationId &&
secondRequest?.conversationId === firstRequest.conversationId &&
secondRequest?.sessionId === "ses_source_fixture_continuity" &&
secondRequest?.threadId === "thread_source_fixture_continuity"
);
const nativeContextOk = secondRequest?.hasConversationContext === false;
const retryOk = Boolean(
retryRequest?.message === failedRequest?.message &&
retryRequest?.conversationId === failedRequest?.conversationId &&
retryRequest?.sessionId === failedRequest?.sessionId &&
retryRequest?.threadId === failedRequest?.threadId
);
const degradedOk = /Code Agent completed 但会话证据缺失|会话证据缺失|新会话\/会话降级|按会话降级处理/u.test(text);
const checks = [
{
id: "local-agent-session-continuity-two-turns",
status: continuityOk && nativeContextOk && first.status === "pass" && second.status === "pass" ? "pass" : "blocked",
summary: "第二轮 /v1/agent/chat 请求复用第一轮建立的 conversationId、sessionId 和 threadId,且不携带 synthetic conversationContext。",
observations: { firstRequest, secondRequest, first, second }
},
{
id: "local-agent-session-continuity-retry",
status: retryOk ? "pass" : "blocked",
summary: "重试上一条会重发上一条用户输入,并携带同一 conversation/session/thread 上下文。",
observations: { failedRequest, retryRequest }
},
{
id: "local-agent-session-continuity-degraded-copy",
status: degradedOk && ui.failedRetryActionVisible && ui.failedTraceActionVisible ? "pass" : "blocked",
summary: "缺失 providerTrace/thread 的 completed 响应显示结构化会话降级/受阻文案,不静默冒充同一会话成功。",
observations: { degradedOk, ui, visibleTextMatched: degradedOk }
}
];
const blockers = checks
.filter((check) => check.status !== "pass")
.map((check) => ({
type: "observability_blocker",
scope: check.id,
status: "open",
summary: check.summary
}));
return {
status: blockers.length === 0 ? "pass" : "blocked",
task: "DC-DCSN-P0-2026-003",
issue: "pikasTech/HWLAB#438",
mode: "local-agent-session-continuity-fixture-browser",
url: server.url,
generatedAt: new Date().toISOString(),
evidenceLevel: "SOURCE",
devLive: false,
summary: "Local SOURCE fixture verifies browser Code Agent session continuity, retry context, and degraded missing providerTrace/thread copy without claiming DEV-LIVE.",
checks,
blockers,
safety: {
...staticSafety(),
localFixtureOnly: true,
fixtureTrustBoundary: "SOURCE fixture verifies browser request semantics only and never updates DEV live.",
observedPostCount: requests.length
}
};
} catch (error) {
return {
status: "blocked",
task: "DC-DCSN-P0-2026-003",
mode: "local-agent-session-continuity-fixture-browser",
evidenceLevel: "SOURCE",
devLive: false,
summary: `Code Agent session continuity fixture failed: ${error.message}`,
checks: [{
id: "local-agent-session-continuity-fixture-runtime",
status: "blocked",
summary: error.message,
observations: { requests }
}],
blockers: [{
type: "observability_blocker",
scope: "local-agent-session-continuity-fixture-runtime",
status: "open",
summary: error.message
}],
safety: staticSafety()
};
} finally {
if (browser) await browser.close();
await server.close();
}
}
async function runLocalAgentFixtureSmoke({
mode,
responseDelayMs = 0,
timeoutConfigMs = null,
expectTimeout = false
} = {}) {
let chromium;
try {
({ chromium } = await importPlaywright());
} catch (error) {
const summary = `Local Code Agent browser fixture smoke skipped because Playwright is unavailable: ${error.message}`;
return {
status: "skip",
task: "DC-DCSN-P0-2026-003",
mode,
evidenceLevel: "SOURCE",
devLive: false,
summary,
checks: [
{
id: "local-agent-fixture-browser-dependency",
status: "skip",
summary: playwrightDependencySummary,
evidence: [playwrightDependencyCommand, "package.json dependencies.playwright"]
}
],
blockers: [
{
type: "environment_blocker",
scope: "local-agent-fixture-browser-dependency",
status: "open",
summary
}
],
safety: staticSafety(),
dependency: {
package: "playwright",
declaredIn: "package.json",
installCommand: playwrightDependencyCommand
}
};
}
const server = await startStaticWebServer({ agentFixture: true, agentDelayMs: responseDelayMs, agentFixtureMode: "success" });
let browser;
try {
browser = await launchChromium(chromium);
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
if (timeoutConfigMs !== null) {
await page.addInitScript((timeoutMs) => {
globalThis.HWLAB_CLOUD_WEB_CONFIG = {
...(globalThis.HWLAB_CLOUD_WEB_CONFIG ?? {}),
timeouts: {
...(globalThis.HWLAB_CLOUD_WEB_CONFIG?.timeouts ?? {}),
codeAgentSubmitTimeoutMsMinMs: 1,
codeAgentSubmitTimeoutMs: timeoutMs,
codeAgentTimeoutMsMinMs: 1,
codeAgentTimeoutMs: timeoutMs
}
};
}, timeoutConfigMs);
}
const agentResponses = [];
page.on("response", async (response) => {
if (!response.url().includes("/v1/agent/chat")) return;
let body = null;
try {
body = await response.json();
} catch {
body = null;
}
agentResponses.push({
method: response.request().method(),
status: response.status(),
urlPath: new URL(response.url()).pathname,
body: sanitizeAgentChatBody(body, { httpStatus: response.status() })
});
});
await page.goto(server.url, { waitUntil: "networkidle", timeout: 15000 });
await loginWithDefaultCredentials(page);
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
await page.waitForFunction(
() => document.querySelector("#live-status")?.textContent?.trim() === "只读模式",
null,
{ timeout: 8000 }
);
const controls = await inspectJourneyControls(page);
const apiStatus = await inspectDefaultApiStatus(page);
let response = null;
let responseBody = null;
let responseSummary = null;
const promptResults = [];
if (expectTimeout) {
await page.locator("#command-input").fill(codeAgentE2ePrompts[0].text);
await page.locator("#command-send").click();
await page.waitForTimeout(Math.min(legacyFailureWindowMs + 350, Math.max(80, timeoutConfigMs ?? 0) + 80));
await page.waitForFunction(
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "等待超时",
null,
{ timeout: 8000 }
);
} else {
for (const prompt of codeAgentE2ePrompts) {
promptResults.push(await runCodeAgentPromptJourney(page, prompt, { sourceFixture: true }));
await page.waitForFunction(
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "SOURCE 回复",
null,
{ timeout: 8000 }
);
}
}
const ui = await inspectJourneyUi(page);
const mobilePending = !expectTimeout
? await inspectLocalAgentPendingViewport(browser, server.url, {
width: 390,
height: 844,
responseDelayMs,
timeoutConfigMs
})
: null;
responseSummary = response ? sanitizeAgentChatBody(responseBody, { httpStatus: response.status() }) : null;
const pass = expectTimeout
? (
ui.agentChatStatus === "等待超时" &&
ui.retryInputPreserved &&
(ui.failedMessageHasTrace || ui.traceHasTraceId) &&
(ui.failedMessageHasChineseTimeout || ui.agentChatStatus === "等待超时") &&
ui.timeoutRetryActionVisible &&
ui.timeoutTraceActionVisible &&
ui.timeoutActionPanelContained &&
!ui.completedMessageVisible
)
: (
promptResults.every((result) =>
result.status === "pass" &&
result.response?.status === "completed" &&
result.response?.sourceKind === "SOURCE" &&
result.response?.hasReply === true &&
result.classification?.sourceOnly === true &&
result.legacyWindow?.permanentFailureAround4500ms === false &&
result.legacyWindow?.runningMessageVisible === true &&
result.legacyWindow?.pendingChineseVisible === true &&
result.legacyWindow?.pendingTraceVisible === true &&
result.legacyWindow?.pendingTraceShowsEvents === true &&
result.legacyWindow?.pendingConversationVisible === true &&
result.legacyWindow?.pendingSessionVisible === true &&
result.legacyWindow?.pendingTraceCopyVisible === true &&
result.legacyWindow?.pendingRetryActionVisible === true &&
result.legacyWindow?.pendingTraceActionVisible === true &&
result.legacyWindow?.pendingActionsContained === true &&
result.legacyWindow?.pendingRowsContained === true &&
result.legacyWindow?.runningCardContained === true
) &&
ui.agentChatStatus === "SOURCE 回复" &&
ui.sourceMessageVisible &&
ui.sourceMessageHasNonSensitiveMeta &&
ui.traceHasTraceId &&
ui.traceShowsEvents &&
ui.traceAllModeVisible &&
ui.traceCopyJsonVisible &&
ui.traceDownloadVisible &&
ui.runtimePathVisible &&
ui.runtimePathCompactSummaryVisible &&
ui.runtimePathUsesCompactDialog &&
ui.runtimePathShowsProviderFields &&
ui.runtimePathSummaryStaysCompact &&
ui.runtimePathFallbackNotFull &&
ui.noMainAttributionNoise &&
ui.attributionFallbackNotRunnerControl &&
ui.conversationFactsVisible &&
mobilePending?.pass === true &&
!ui.completedMessageVisible &&
!ui.failedMessageVisible
);
const checks = [
{
id: "local-agent-fixture-controls",
status: Object.values(controls).every(Boolean) ? "pass" : "blocked",
summary: "Local SOURCE fixture browser smoke can locate the Code Agent input, send button, thread, and right-side evidence panels.",
observations: controls
},
{
id: "local-api-status-attribution",
status: apiStatus.actionableStatusVisible && apiStatus.defaultCopyIsWorkbenchSafe ? "pass" : "blocked",
summary: "Default workbench status renders reachable blocked /health/live as read-only with concrete service/API attribution and no raw degraded conclusion.",
observations: apiStatus
},
{
id: expectTimeout ? "local-agent-timeout-fixture-failed-state" : "local-agent-fixture-replied-state",
status: pass ? "pass" : "blocked",
summary: expectTimeout
? "Local SOURCE fixture proves a bounded Code Agent timeout stays user-visible/BLOCKED in the UI, preserves trace context, and keeps the user's input for retry."
: "Local SOURCE fixture sends input and reaches a user-visible replied state without marking the fixture as DEV-LIVE.",
observations: expectTimeout ? {
response: responseSummary,
ui,
networkEvents: agentResponses
} : {
prompts: promptResults,
ui,
mobilePending,
networkEvents: agentResponses
}
}
];
if (!expectTimeout) {
checks.push({
id: "local-agent-fixture-no-4500ms-permanent-failure",
status: promptResults.every((result) =>
result.legacyWindow?.permanentFailureAround4500ms === false &&
result.legacyWindow?.runningMessageVisible === true &&
result.legacyWindow?.pendingChineseVisible === true &&
result.legacyWindow?.pendingTraceVisible === true &&
result.legacyWindow?.pendingConversationVisible === true &&
result.legacyWindow?.pendingSessionVisible === true &&
result.legacyWindow?.pendingTraceCopyVisible === true &&
result.legacyWindow?.pendingRetryActionVisible === true &&
result.legacyWindow?.pendingTraceActionVisible === true &&
result.legacyWindow?.pendingActionsContained === true &&
result.legacyWindow?.pendingRowsContained === true &&
result.legacyWindow?.runningCardContained === true
) ? "pass" : "blocked",
summary: "Local SOURCE fixture delays each Code Agent response beyond 4500ms and the conversation shows Chinese pending state with trace/session evidence instead of permanent failure.",
observations: {
legacyFailureWindowMs,
fixtureDelayMs: responseDelayMs,
prompts: promptResults.map((result) => ({
promptId: result.promptId,
status: result.status,
legacyWindow: result.legacyWindow,
traceId: result.traceId
})),
mobilePending
}
});
checks.push({
id: "local-agent-fixture-mobile-pending-layout",
status: mobilePending?.pass === true ? "pass" : "blocked",
summary: "390x844 mobile view shows Chinese Code Agent pending status, trace/session context, and no pending-card text overflow before the delayed response arrives.",
observations: mobilePending
});
}
const blockers = checks
.filter((check) => check.status !== "pass")
.map((check) => ({
type: "observability_blocker",
scope: check.id,
status: "open",
summary: check.summary
}));
return {
status: blockers.length === 0 ? "pass" : "blocked",
task: "DC-DCSN-P0-2026-003",
mode,
url: server.url,
generatedAt: new Date().toISOString(),
evidenceLevel: "SOURCE",
devLive: false,
summary: expectTimeout
? "Local browser smoke uses a sanitized slow SOURCE fixture for /v1/agent/chat; it proves timeout UI classification only and does not claim DEV-LIVE."
: "Local browser smoke uses a sanitized SOURCE fixture for /v1/agent/chat; it proves the UI conversation path only and does not claim DEV-LIVE.",
checks,
blockers,
safety: {
...staticSafety(),
localFixtureOnly: true,
agentFixtureMode: "success",
fixtureTrustBoundary: "SOURCE fixture replies render as SOURCE 回复 and never as DEV-LIVE 回复.",
legacyFailureWindowMs,
fixtureDelayMs: responseDelayMs,
prompts: codeAgentE2ePrompts.map((prompt) => ({ id: prompt.id, label: prompt.label })),
preservedLiveFields: ["provider", "model", "backend", "conversationId", "traceId", "messageId", "providerTrace"]
}
};
} catch (error) {
return {
status: "blocked",
task: "DC-DCSN-P0-2026-003",
mode,
url: server.url,
generatedAt: new Date().toISOString(),
evidenceLevel: "SOURCE",
devLive: false,
summary: `Local Code Agent browser fixture smoke failed: ${error.message}`,
checks: [],
blockers: [
{
type: "observability_blocker",
scope: mode,
status: "open",
summary: error.message
}
],
safety: staticSafety()
};
} finally {
if (browser) await browser.close();
await server.close();
}
}
async function inspectLocalAgentPendingViewport(browser, url, { width, height, responseDelayMs, timeoutConfigMs }) {
const context = await browser.newContext({
viewport: { width, height },
deviceScaleFactor: 1,
isMobile: width <= 480
});
const page = await context.newPage();
try {
if (timeoutConfigMs !== null) {
await page.addInitScript((timeoutMs) => {
globalThis.HWLAB_CLOUD_WEB_CONFIG = {
...(globalThis.HWLAB_CLOUD_WEB_CONFIG ?? {}),
timeouts: {
...(globalThis.HWLAB_CLOUD_WEB_CONFIG?.timeouts ?? {}),
codeAgentSubmitTimeoutMsMinMs: 1,
codeAgentSubmitTimeoutMs: timeoutMs,
codeAgentTimeoutMsMinMs: 1,
codeAgentTimeoutMs: timeoutMs
}
};
}, timeoutConfigMs);
}
await page.goto(url, { waitUntil: "networkidle", timeout: 15000 });
await loginWithDefaultCredentials(page);
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
const beforeMessageCount = await page.locator(".message-card").count();
await page.locator("#command-input").fill(codeAgentE2ePrompts[1].text);
const responsePromise = page.waitForResponse(
(response) => response.url().includes("/v1/agent/chat") && response.request().method() === "POST",
{ timeout: responseDelayMs + 10000 }
).catch(() => null);
await page.locator("#command-send").click();
await page.waitForTimeout(legacyFailureWindowMs + 350);
const legacyWindow = await inspectLegacyFailureWindow(page, { beforeMessageCount });
const ui = await inspectJourneyUi(page);
const response = await responsePromise;
return {
viewport: { width, height },
pass:
legacyWindow.permanentFailureAround4500ms === false &&
legacyWindow.runningMessageVisible === true &&
legacyWindow.pendingChineseVisible === true &&
legacyWindow.pendingTraceVisible === true &&
legacyWindow.pendingTraceShowsEvents === true &&
legacyWindow.pendingConversationVisible === true &&
legacyWindow.pendingSessionVisible === true &&
legacyWindow.pendingTraceCopyVisible === true &&
legacyWindow.pendingRetryActionVisible === true &&
legacyWindow.pendingTraceActionVisible === true &&
legacyWindow.pendingActionsContained === true &&
legacyWindow.pendingRowsContained === true &&
legacyWindow.runningCardContained === true &&
ui.runningCardsContained === true &&
ui.pendingContextRowsContained === true &&
ui.pendingRetryActionVisible === true &&
ui.pendingTraceActionVisible === true &&
ui.pendingActionPanelContained === true,
legacyWindow,
ui: {
agentChatStatus: ui.agentChatStatus,
runningMessageVisible: ui.runningMessageVisible,
pendingMessageChinese: ui.pendingMessageChinese,
pendingMessageHasTrace: ui.pendingMessageHasTrace,
pendingMessageHasConversation: ui.pendingMessageHasConversation,
pendingMessageHasSession: ui.pendingMessageHasSession,
pendingTraceCopyVisible: ui.pendingTraceCopyVisible,
pendingRetryActionVisible: ui.pendingRetryActionVisible,
pendingTraceActionVisible: ui.pendingTraceActionVisible,
pendingActionPanelContained: ui.pendingActionPanelContained,
pendingContextRowsContained: ui.pendingContextRowsContained,
runningCardsContained: ui.runningCardsContained
},
responseObserved: Boolean(response)
};
} finally {
await page.close().catch(() => null);
await context.close();
}
}
export async function runDevCloudWorkbenchMobileSmoke() {
let chromium;
try {
({ chromium } = await importPlaywright());
} catch (error) {
const summary = `Mobile browser smoke skipped because Playwright is unavailable: ${error.message}`;
return {
status: "skip",
task: "DC-DCSN-P0-2026-003",
mode: "static-mobile-browser",
viewport: { width: 390, height: 844 },
evidenceLevel: "SOURCE",
devLive: false,
summary,
checks: [
{
id: "mobile-browser-dependency",
status: "skip",
summary: playwrightDependencySummary,
evidence: [playwrightDependencyCommand, "package.json dependencies.playwright"]
}
],
blockers: [
{
type: "environment_blocker",
scope: "mobile-browser-dependency",
status: "open",
summary
}
],
safety: staticSafety(),
dependency: {
package: "playwright",
declaredIn: "package.json",
installCommand: playwrightDependencyCommand
}
};
}
const server = await startStaticWebServer();
let browser;
try {
browser = await launchChromium(chromium);
const page = await browser.newPage({
viewport: { width: 390, height: 844 },
deviceScaleFactor: 1,
isMobile: true
});
await page.goto(server.url, { waitUntil: "networkidle", timeout: 15000 });
await loginWithDefaultCredentials(page);
const closed = await inspectMobileWorkbench(page);
const workspaceScroll = await inspectWorkbenchScrollContract(page, { panelSelector: "#conversation-list" });
await page.click('[data-route="help"]');
await page.waitForFunction(() => document.querySelector("#help-content")?.dataset.helpState === "ready", null, { timeout: 8000 });
await page.waitForTimeout(150);
const help = await inspectHelpMarkdownRoute(page);
await page.goto(new URL("/#/help", server.url).toString(), { waitUntil: "networkidle", timeout: 15000 });
await loginWithDefaultCredentials(page);
await page.waitForFunction(() => document.querySelector("#help-content")?.dataset.helpState === "ready", null, { timeout: 8000 });
const slashHashHelp = await inspectHelpMarkdownRoute(page);
await page.goto(new URL("/help", server.url).toString(), { waitUntil: "networkidle", timeout: 15000 });
await loginWithDefaultCredentials(page);
await page.waitForFunction(() => document.querySelector("#help-content")?.dataset.helpState === "ready", null, { timeout: 8000 });
const directHelp = await inspectHelpMarkdownRoute(page);
await page.click('[data-route="workspace"]');
await page.waitForFunction(() => document.querySelector('[data-view="workspace"]')?.hidden === false, null, { timeout: 8000 });
const checks = [
{
id: "mobile-default-workbench",
status: closed.defaultWorkbenchReachable ? "pass" : "blocked",
summary: "390x844 default route remains the user workbench with Gate/help as secondary entries.",
observations: {
title: closed.title,
workspaceHidden: closed.workspaceHidden,
gateHidden: closed.gateHidden,
helpHidden: closed.helpHidden,
removedSelectors: closed.removedSelectors,
visibleText: closed.visibleText
}
},
{
id: "mobile-outer-scroll-lock",
status: workspaceScroll.outerScrollLocked && help.rootStillLocked && help.helpPanelScrolls ? "pass" : "blocked",
summary: "390x844 page locks outer scrolling while the internal Markdown help pane owns overflow scrolling.",
observations: {
workspace: workspaceScroll,
help: {
rootStillLocked: help.rootStillLocked,
helpPanelScrolls: help.helpPanelScrolls,
helpScroll: help.helpScroll
}
}
},
{
id: "mobile-help-markdown-route",
status: help.nonDefaultMarkdownHelp && (help.routeIsHashHelp || help.routeIsSlashHashHelp) && help.termsPresent ? "pass" : "blocked",
summary: "Help opens from the rail as a non-default hash route rendered from help.md by the vendored Markdown renderer.",
observations: help
},
{
id: "mobile-slash-hash-help-route",
status: slashHashHelp.nonDefaultMarkdownHelp && slashHashHelp.routeIsSlashHashHelp && slashHashHelp.termsPresent ? "pass" : "blocked",
summary: "Direct #/help opens the user-facing help view.",
observations: slashHashHelp
},
{
id: "mobile-direct-help-route",
status: directHelp.nonDefaultMarkdownHelp && directHelp.routeIsDirectHelp && directHelp.termsPresent ? "pass" : "blocked",
summary: "Direct /help opens the user-facing help view instead of raw API 404 JSON.",
observations: directHelp
},
{
id: "mobile-default-hit-test",
status: closed.primaryControlsReachable ? "pass" : "blocked",
summary: "Primary workbench controls are center-click reachable without a resource tree drawer.",
observations: {
reachable: closed.reachable,
blocked: closed.blocked
}
},
{
id: "mobile-resource-explorer-removed",
status: closed.resourceExplorerRemoved ? "pass" : "blocked",
summary: "390x844 default DOM has no resource explorer, explorer resize handle, quick actions, or resource tree.",
observations: {
removedSelectors: closed.removedSelectors
}
}
];
const blockers = checks
.filter((check) => check.status !== "pass")
.map((check) => ({
type: "runtime_blocker",
scope: check.id,
status: "open",
summary: check.summary
}));
return {
status: blockers.length === 0 ? "pass" : "blocked",
task: "DC-DCSN-P0-2026-003",
mode: "static-mobile-browser",
viewport: { width: 390, height: 844 },
url: server.url,
generatedAt: new Date().toISOString(),
evidenceLevel: "SOURCE",
devLive: false,
summary: "Static local browser smoke only; deployment still requires post-deploy live smoke.",
checks,
blockers,
safety: staticSafety()
};
} catch (error) {
return {
status: "blocked",
task: "DC-DCSN-P0-2026-003",
mode: "static-mobile-browser",
viewport: { width: 390, height: 844 },
url: server.url,
generatedAt: new Date().toISOString(),
evidenceLevel: "SOURCE",
devLive: false,
summary: `Mobile browser smoke failed: ${error.message}`,
checks: [],
blockers: [
{
type: "runtime_blocker",
scope: "mobile-browser-smoke",
status: "open",
summary: error.message
}
],
safety: staticSafety()
};
} finally {
if (browser) await browser.close();
await server.close();
}
}
function longSourceFixtureTraceEvents(traceId, count = 18) {
const labels = [
"source-fixture:session",
"prompt:queued",
"prompt:sent",
"context:loaded",
"session:reused",
"assistant:started",
"tool:skills.inspect",
"tool:skills.inspect:completed",
"tool:workspace.read",
"tool:workspace.read:completed",
"assistant:delta",
"assistant:delta",
"assistant:delta",
"assistant:delta",
"assistant:finalizing",
"message:stored",
"conversation:facts-updated",
"assistant:completed"
];
return Array.from({ length: count }, (_, index) => {
const seq = index + 1;
const label = labels[index] ?? `assistant:event:${seq}`;
const toolName = label.startsWith("tool:") ? label.split(":").slice(1).join(":") : undefined;
return {
seq,
traceId,
type: label.split(":")[0] || "event",
stage: label.split(":")[1] || "observed",
status: seq === count ? "completed" : "observed",
label,
...(toolName ? { toolName } : {}),
elapsedMs: seq * 37
};
});
}
function sessionContinuityFixturePayload({ body, traceId, conversationId, messageId, timestamp, options }) {
const fixtureState = options.sessionContinuityState ?? {
sessionId: "ses_source_fixture_continuity",
threadId: "thread_source_fixture_continuity",
turn: 0,
traceIds: []
};
options.sessionContinuityState = fixtureState;
fixtureState.turn += 1;
fixtureState.traceIds.push(traceId);
const message = String(body?.message ?? "");
const missingProviderTrace = /providerTrace\s*缺失|缺失\s*providerTrace/u.test(message);
const sessionId = fixtureState.sessionId;
const threadId = missingProviderTrace ? null : fixtureState.threadId;
const turn = fixtureState.turn;
const toolName = turn > 1 ? "codex-app-server.thread/resume+turn/start" : "codex-app-server.thread/start+turn/start";
const events = longSourceFixtureTraceEvents(traceId, missingProviderTrace ? 6 : 18).map((event, index) => ({
...event,
label: index === 0 ? (turn > 1 ? "session:reused" : "session:created") : event.label,
sessionId,
...(threadId ? { threadId } : {})
}));
const session = {
sessionId,
conversationId,
conversationIds: [conversationId],
status: "idle",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
runnerKind: "codex-app-server-stdio-runner",
sessionMode: "codex-app-server-stdio-long-lived",
capabilityLevel: "long-lived-codex-stdio-session",
implementationType: "repo-owned-codex-app-server-stdio-session",
createdAt: timestamp,
updatedAt: timestamp,
idleTimeoutMs: 1800000,
expiresAt: "2026-05-22T00:30:00.000Z",
lastTraceId: traceId,
currentTraceId: null,
turn,
threadId,
reused: turn > 1,
durable: true,
longLivedSession: true,
codexStdio: true,
writeCapable: true,
secretMaterialStored: false,
valuesRedacted: true
};
const payload = {
conversationId,
sessionId,
messageId,
status: "completed",
createdAt: timestamp,
updatedAt: timestamp,
traceId,
provider: "codex-stdio",
model: "gpt-5.5",
backend: "local-source-fixture/codex-app-server-stdio",
projectId: stringOrFallback(body?.projectId, sourceFixtureProjectId),
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
session,
sessionMode: "codex-app-server-stdio-long-lived",
sessionReuse: {
conversationId,
sessionId,
threadId,
mapped: true,
reused: turn > 1,
turn,
previousTurns: Math.max(0, turn - 1),
workspace: "/workspace/hwlab",
status: "idle"
},
implementationType: "repo-owned-codex-app-server-stdio-session",
runnerLimitations: ["secret-values-redacted"],
codexStdioFeasibility: {
ready: true,
canStartLongLivedCodexStdio: true,
commandProbe: { ready: true },
blockers: [],
blockerCodes: []
},
longLivedSessionGate: {
status: "pass",
pass: true,
blockers: []
},
toolCalls: [{
id: `tool_${turn}`,
type: "codex-app-server-stdio",
name: toolName,
status: "completed",
cwd: "/workspace/hwlab",
command: "codex app-server --listen stdio://",
exitCode: 0,
stdout: [threadId ? `threadId=${threadId}` : "threadId=not_observed", `turnId=turn_source_fixture_${turn}`, "terminalStatus=completed"].join(" "),
stderrSummary: "",
outputTruncated: false,
traceId
}],
skills: { status: "not_requested", count: 0, totalCount: 0, items: [], blockers: [] },
runner: {
kind: "codex-app-server-stdio-runner",
provider: "codex-stdio",
backend: "hwlab-cloud-api/codex-app-server-stdio",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
session: "codex-app-server-stdio-long-lived",
sessionMode: "codex-app-server-stdio-long-lived",
sessionId,
turn,
sessionReused: turn > 1,
implementationType: "repo-owned-codex-app-server-stdio-session",
codexStdio: true,
longLivedSession: true,
durableSession: true,
writeCapable: true,
readOnly: false,
capabilityLevel: "long-lived-codex-stdio-session"
},
runnerTrace: {
traceId,
runnerKind: "codex-app-server-stdio-runner",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
sessionId,
threadId,
sessionStatus: "idle",
turn,
sessionReused: turn > 1,
events,
eventLabels: events.map((event) => event.label),
lastEvent: events.at(-1),
updatedAt: timestamp,
startedAt: timestamp,
elapsedMs: 120,
outputTruncated: false
},
conversationFacts: {
conversationId,
sessionId,
sessionStatus: "idle",
sessionMode: "codex-app-server-stdio-long-lived",
capabilityLevel: "long-lived-codex-stdio-session",
runnerKind: "codex-app-server-stdio-runner",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
turnCount: turn,
latestTraceId: traceId,
traceIds: [...fixtureState.traceIds],
latestSkills: null,
recentToolCalls: [{ name: toolName, status: "completed" }],
facts: [],
valuesRedacted: true,
secretMaterialStored: false
},
capabilityLevel: "long-lived-codex-stdio-session",
reply: {
messageId,
role: "assistant",
content: missingProviderTrace
? "本响应故意缺少 providerTrace/thread,用于验证前端会话降级显示。"
: `SOURCE fixture:第 ${turn} 轮已在同一 Code Agent conversation/session/thread 中处理。`,
createdAt: timestamp
},
usage: null
};
if (!missingProviderTrace) {
payload.sourceKind = "SOURCE";
payload.evidenceLevel = "SOURCE";
payload.providerTrace = {
source: "SOURCE-local-browser-fixture",
sourceKind: "SOURCE",
transport: "stdio",
protocol: "codex-app-server-jsonrpc-stdio",
command: "codex app-server --listen stdio://",
toolName,
threadId,
turnId: `turn_source_fixture_${turn}`,
terminalStatus: "completed",
valuesPrinted: false,
redacted: true
};
}
return payload;
}
async function startStaticWebServer(options = {}) {
const rootDir = path.resolve(options.rootDir ?? webRoot);
const authFixtureSessions = new Set();
const agentTraceStore = new Map();
const server = http.createServer(async (request, response) => {
const url = new URL(request.url ?? "/", "http://127.0.0.1");
if (options.authFixture && await handleAuthFixtureApi({ request, response, url, authFixtureSessions })) {
return;
}
if (options.agentFixture && await handleLocalAgentFixtureApi({ request, response, url, options, agentTraceStore })) {
return;
}
if (handleDevicePodFixtureApi({ request, response, url })) {
return;
}
if (options.liveBuildsFixture && request.method === "GET" && url.pathname === "/v1/live-builds") {
jsonResponse(response, 200, liveBuildsFixturePayload());
return;
}
let pathname = decodeURIComponent(url.pathname);
if (pathname === "/" || pathname === "/workbench" || pathname === "/gate" || pathname === "/diagnostics/gate" || pathname === "/help") {
pathname = "/index.html";
}
const filePath = path.resolve(rootDir, pathname.replace(/^\/+/, ""));
if (!filePath.startsWith(rootDir)) {
response.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
response.end("forbidden");
return;
}
try {
const body = await fs.promises.readFile(filePath);
response.writeHead(200, { "content-type": contentTypeFor(filePath) });
response.end(body);
} catch {
response.writeHead(404, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify({ ok: false, error: "not_found" }));
}
});
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", resolve);
});
const address = server.address();
return {
url: `http://127.0.0.1:${address.port}/`,
close: () => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()))
};
}
function startLocalAgentTrace(agentTraceStore, { traceId, conversationId, delayMs }) {
if (!agentTraceStore || agentTraceStore.has(traceId)) return;
const startedAt = Date.now();
const events = [];
const append = (event) => {
const normalizedEvent = {
traceId,
...event
};
events.push(normalizedEvent);
agentTraceStore.set(traceId, localAgentTraceSnapshot({
traceId,
conversationId,
startedAt,
events,
lastEvent: normalizedEvent
}));
};
append({ seq: 1, type: "session", stage: "created", status: "completed", label: "session:created" });
const scheduledEvents = [
[250, { seq: 2, type: "prompt", stage: "sent", status: "completed", label: "prompt:sent" }],
[900, { seq: 3, type: "runner", stage: "started", status: "running", label: "runner:started" }],
[1800, { seq: 4, type: "tool_call", stage: "tool_call", status: "running", label: "tool:fixture:running", toolName: "local.source.fixture" }],
[Math.min(Math.max(delayMs - 1200, 2400), 3400), { seq: 5, type: "assistant", stage: "waiting", status: "running", label: "assistant:waiting" }]
];
for (const [waitMs, event] of scheduledEvents) {
const timer = setTimeout(() => append(event), waitMs);
timer.unref?.();
}
}
function localAgentTraceSnapshot({ traceId, conversationId = null, startedAt = Date.now(), events = [], lastEvent = null }) {
return {
traceId,
runnerKind: "codex-app-server-stdio-runner",
sessionMode: "codex-app-server-stdio-long-lived",
sessionId: conversationId,
sessionStatus: events.length > 0 ? "running" : "pending",
status: events.length > 0 ? "running" : "pending",
elapsedMs: Math.max(0, Date.now() - startedAt),
waitingFor: events.length > 0 ? "fixture-response" : "fixture-trace",
events: [...events],
lastEvent,
updatedAt: new Date().toISOString()
};
}
async function handleAuthFixtureApi({ request, response, url, authFixtureSessions }) {
if (url.pathname === "/auth/session" && request.method === "GET") {
const token = authFixtureCookie(request);
jsonResponse(response, 200, token && authFixtureSessions.has(token)
? {
authenticated: true,
user: { username: defaultAuthCredentials.username },
expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString()
}
: { authenticated: false });
return true;
}
if (url.pathname === "/auth/login" && request.method === "POST") {
const body = await readJsonBody(request);
if (body?.username !== defaultAuthCredentials.username || body?.password !== defaultAuthCredentials.password) {
jsonResponse(response, 401, { authenticated: false, error: "invalid_credentials" });
return true;
}
const token = `auth_fixture_${authFixtureSessions.size + 1}`;
authFixtureSessions.add(token);
const payload = JSON.stringify({
authenticated: true,
user: { username: defaultAuthCredentials.username },
expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString()
});
response.writeHead(200, {
"content-type": "application/json; charset=utf-8",
"set-cookie": `hwlab_session=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=3600`
});
response.end(payload);
return true;
}
if (url.pathname === "/auth/logout" && request.method === "POST") {
const token = authFixtureCookie(request);
if (token) authFixtureSessions.delete(token);
response.writeHead(200, {
"content-type": "application/json; charset=utf-8",
"set-cookie": "hwlab_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0"
});
response.end(JSON.stringify({ authenticated: false }));
return true;
}
if (url.pathname.startsWith("/auth/")) {
jsonResponse(response, 404, { authenticated: false, error: "not_found" });
return true;
}
return false;
}
function authFixtureCookie(request) {
const cookie = request.headers.cookie ?? "";
for (const part of cookie.split(";")) {
const [name, ...value] = part.trim().split("=");
if (name === "hwlab_session") return decodeURIComponent(value.join("=") || "");
}
return "";
}
function handleDevicePodFixtureApi({ request, response, url }) {
if (request.method !== "GET" || !url.pathname.startsWith("/v1/device-pods")) return false;
const payload = buildDevicePodRestPayload(url.pathname, url.searchParams, {
sourceKind: "SOURCE",
observedAt: "2026-05-27T08:00:00.000Z"
});
if (!payload) return false;
jsonResponse(response, 200, payload);
return true;
}
function liveBuildsFixturePayload() {
return {
serviceId: "hwlab-cloud-api",
environment: "dev",
status: "ok",
contractVersion: "live-builds-v1",
observedAt: "2026-05-23T00:30:00.000Z",
source: {
kind: "live-health+deploy-artifact-catalog",
route: "/v1/live-builds",
healthPath: "/health/live"
},
latest: {
serviceId: "hwlab-agent-mgr",
name: "hwlab-agent-mgr",
kind: "hwlab",
status: "ok",
build: {
createdAt: "2026-05-23T00:20:00.000Z",
metadataSource: "runtime-env:HWLAB_BUILD_CREATED_AT",
liveHealthMissingReason: "health payload 缺少 build.createdAt / image.createdAt / buildCreatedAt,已使用与 live tag/revision 匹配的 catalog metadata"
},
image: {
reference: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:f09ad05",
tag: "f09ad05",
digest: "sha256:" + "1".repeat(64)
},
commit: {
id: "f09ad05dec5f2bbca12ae661b140a87dfcbf54a4",
source: "fixture"
},
revision: "f09ad05dec5f2bbca12ae661b140a87dfcbf54a4"
},
counts: {
total: 5,
hwlab: 4,
withBuildTime: 2,
unavailable: 2,
external: 1
},
services: [
{
serviceId: "hwlab-cloud-api",
name: "hwlab-cloud-api",
kind: "hwlab",
status: "ok",
build: {
createdAt: "2026-05-23T00:10:00.000Z",
metadataSource: "runtime-env:HWLAB_BUILD_CREATED_AT"
},
image: {
reference: "127.0.0.1:5000/hwlab/hwlab-cloud-api:f09ad05",
tag: "f09ad05",
digest: "sha256:" + "0".repeat(64)
},
commit: {
id: "f09ad05dec5f2bbca12ae661b140a87dfcbf54a4",
source: "fixture"
},
revision: "f09ad05dec5f2bbca12ae661b140a87dfcbf54a4"
},
{
serviceId: "hwlab-agent-mgr",
name: "hwlab-agent-mgr",
kind: "hwlab",
status: "ok",
build: {
createdAt: "2026-05-23T00:20:00.000Z",
metadataSource: "runtime-env:HWLAB_BUILD_CREATED_AT"
},
image: {
reference: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:f09ad05",
tag: "f09ad05",
digest: "sha256:" + "1".repeat(64)
},
commit: {
id: "f09ad05dec5f2bbca12ae661b140a87dfcbf54a4",
source: "fixture"
},
revision: "f09ad05dec5f2bbca12ae661b140a87dfcbf54a4"
},
{
serviceId: "hwlab-frpc",
name: "hwlab-frpc",
kind: "external",
status: "external",
build: {
createdAt: null,
metadataSource: "external-image",
unavailableReason: "外部镜像或非 HWLAB 构建产物"
},
image: {
reference: "127.0.0.1:5000/hwlab/frpc:v0.68.1",
tag: "v0.68.1",
digest: "unknown"
},
commit: {
id: "unknown",
source: "external-image"
},
revision: "unknown"
},
{
serviceId: "hwlab-agent-worker",
name: "hwlab-agent-worker",
kind: "hwlab",
status: "unavailable",
build: {
createdAt: null,
metadataSource: "unavailable",
unavailableReason: "构建时间不可用:hwlab-agent-worker 是 suspended Job template,当前 live desired replicas=0"
},
image: {
reference: "unknown",
tag: "unknown",
digest: "unknown"
},
commit: {
id: "unknown",
source: "unavailable"
},
revision: "unknown"
}
]
};
}
async function handleLocalAgentFixtureApi({ request, response, url, options = {}, agentTraceStore = null }) {
if (request.method === "GET" && url.pathname.startsWith("/v1/agent/chat/trace/")) {
const parts = url.pathname.split("/").filter(Boolean);
const traceId = decodeURIComponent(parts[4] ?? "");
if (parts[5] === "stream") {
jsonResponse(response, 503, {
status: "failed",
error: {
code: "trace_stream_unavailable_for_fixture",
message: "fixture intentionally forces EventSource fallback to polling"
},
traceId
});
return true;
}
jsonResponse(response, 200, agentTraceStore?.get(traceId) ?? localAgentTraceSnapshot({ traceId }));
return true;
}
if (request.method === "GET" && (url.pathname === "/health/live" || url.pathname === "/v1")) {
jsonResponse(response, 200, {
serviceId: "hwlab-cloud-api",
status: "degraded",
ready: false,
sourceKind: "SOURCE",
codeAgent: localSourceFixtureAvailability(),
methods: [...readOnlyRpcMethods],
runtime: {
durable: false,
ready: false,
status: "degraded",
blocker: "runtime_durable_adapter_query_blocked",
connection: {
queryAttempted: true,
queryResult: "query_blocked"
},
counts: {
devicePods: 1,
fakeDevicePods: 1
}
},
readiness: {
status: "degraded",
ready: false,
durability: {
status: "blocked",
ready: false,
blocker: "runtime_durable_adapter_query_blocked",
blockedLayer: "durability_query",
queryResult: "query_blocked"
}
},
blockerCodes: ["runtime_durable_adapter_query_blocked"]
});
return true;
}
if (request.method === "GET" && url.pathname === "/v1/live-builds") {
jsonResponse(response, 200, liveBuildsFixturePayload());
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 === "GET" && url.pathname === "/v1/live-builds") {
jsonResponse(response, 200, liveBuildsFixturePayload());
return true;
}
if (request.method === "POST" && url.pathname === "/json-rpc") {
const body = await readJsonBody(request);
jsonResponse(response, 200, {
jsonrpc: "2.0",
id: body?.id ?? "req_source_fixture",
result: localRpcFixtureResult(body?.method)
});
return true;
}
if (request.method === "POST" && url.pathname === "/v1/agent/chat") {
const body = await readJsonBody(request);
const timestamp = "2026-05-22T00:00:00.000Z";
const conversationId = stringOrFallback(body?.conversationId, "cnv_source_fixture_browser");
const traceId = stringOrFallback(body?.traceId, "trc_source_fixture_browser");
startLocalAgentTrace(agentTraceStore, {
traceId,
conversationId,
delayMs: options.agentDelayMs ?? 0
});
await delay(options.agentDelayMs ?? 0);
const normalizedMessage = String(body?.message ?? "");
const isSkillListPrompt = /列出你能使用的所有skill/u.test(normalizedMessage);
const isExternalNetworkPrompt = /github|https?:\/\/|外网|公网|访问/u.test(normalizedMessage);
const messageId = isExternalNetworkPrompt
? "msg_source_fixture_browser_external_network"
: isSkillListPrompt ? "msg_source_fixture_browser_skills" : "msg_source_fixture_browser_simple";
if (options.agentFixtureMode === "session-continuity") {
jsonResponse(response, 200, sessionContinuityFixturePayload({
body,
traceId,
conversationId,
messageId,
timestamp,
options
}));
return true;
}
const fixtureSkills = isSkillListPrompt
? {
status: "ready",
items: [
{ name: "source-fixture-skill" }
],
count: 1,
totalCount: 1,
blockers: []
}
: {
status: "not_requested",
count: 0,
totalCount: 0,
items: [],
blockers: []
};
const fixtureTraceEvents = longSourceFixtureTraceEvents(traceId);
jsonResponse(response, 200, {
conversationId,
sessionId: conversationId,
messageId,
status: "completed",
sourceKind: "SOURCE",
evidenceLevel: "SOURCE",
createdAt: timestamp,
updatedAt: timestamp,
traceId,
provider: "source-fixture",
model: "gpt-source-fixture",
backend: "local-source-fixture",
projectId: stringOrFallback(body?.projectId, sourceFixtureProjectId),
runner: {
kind: "openai-responses-fallback",
codexStdio: false,
longLivedSession: false,
writeCapable: false,
durableSession: false
},
capabilityLevel: "text-chat-only",
sessionMode: "provider-text-request",
toolCalls: [],
skills: fixtureSkills,
runnerTrace: {
traceId,
runnerKind: "openai-responses-fallback",
events: fixtureTraceEvents,
lastEvent: fixtureTraceEvents.at(-1)
},
conversationFacts: {
conversationId,
sessionId: conversationId,
sessionStatus: "idle",
sessionMode: "provider-text-request",
capabilityLevel: "text-chat-only",
runnerKind: "openai-responses-fallback",
workspace: "/workspace/hwlab",
sandbox: "none",
turnCount: isSkillListPrompt ? 2 : 1,
latestTraceId: traceId,
traceIds: isSkillListPrompt ? ["trc_source_fixture_browser_simple", traceId] : [traceId],
latestSkills: isSkillListPrompt
? {
status: "ready",
count: 1,
totalCount: 1,
names: ["source-fixture-skill"],
blockers: []
}
: null,
recentToolCalls: isSkillListPrompt
? [{ name: "skills.discover", status: "completed" }]
: [],
facts: [],
valuesRedacted: true,
secretMaterialStored: false,
summary: isSkillListPrompt
? `turns=2 / sessionId=${conversationId} / workspace=/workspace/hwlab / skills=1 (source-fixture-skill) / traceIds=trc_source_fixture_browser_simple,${traceId}`
: `turns=1 / sessionId=${conversationId} / workspace=/workspace/hwlab / skills=not_requested / traceIds=${traceId}`
},
reply: {
messageId,
role: "assistant",
content: isSkillListPrompt
? "SOURCE fixture 回复:可用 skill 列表需要由真实运行时返回;本地只验证浏览器长请求链路。"
: "SOURCE fixture 回复:HWLAB 工作台可以整理任务、查看资源,并保持硬件变更受控。",
createdAt: timestamp
},
usage: null,
providerTrace: {
source: "SOURCE-local-browser-fixture",
sourceKind: "SOURCE",
responseId: "rsp_source_fixture_sanitized",
redacted: true
}
});
return true;
}
return false;
}
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));
}
function localSourceFixtureAvailability() {
return {
endpoint: "POST /v1/agent/chat",
provider: "source-fixture",
model: "gpt-source-fixture",
backend: "local-source-fixture",
mode: "local-source-fixture",
status: "available",
sourceKind: "SOURCE",
evidenceLevel: "SOURCE",
ready: true,
summary: "Local browser smoke SOURCE fixture; it is not DEV-LIVE provider evidence.",
missingEnv: [],
secretRefs: [],
safety: {
localFixtureOnly: true,
secretsRead: false,
valuesRedacted: true
}
};
}
function localRpcFixtureResult(method) {
if (method === "system.health" || method === "cloud.adapter.describe") {
return {
status: "degraded",
sourceKind: "SOURCE",
methods: [...readOnlyRpcMethods],
runtime: {
durable: false,
counts: {
devicePods: 1,
fakeDevicePods: 1
}
}
};
}
if (method === "audit.event.query") {
return { count: 0, events: [] };
}
if (method === "evidence.record.query") {
return { count: 0, records: [] };
}
return { status: "not_implemented", sourceKind: "SOURCE" };
}
async function readJsonBody(request) {
const chunks = [];
for await (const chunk of request) {
chunks.push(chunk);
}
const text = Buffer.concat(chunks).toString("utf8");
if (!text.trim()) return null;
try {
return JSON.parse(text);
} catch {
return null;
}
}
function parseJsonOrNull(text) {
try {
return JSON.parse(text);
} catch {
return null;
}
}
function jsonResponse(response, status, body) {
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify(body));
}
function stringOrFallback(value, fallback) {
return typeof value === "string" && value.trim() ? value.trim() : fallback;
}
function contentTypeFor(filePath) {
if (filePath.endsWith(".html")) return "text/html; charset=utf-8";
if (filePath.endsWith(".css")) return "text/css; charset=utf-8";
if (filePath.endsWith(".mjs") || filePath.endsWith(".js")) return "text/javascript; charset=utf-8";
if (filePath.endsWith(".md")) return "text/markdown; charset=utf-8";
return "application/octet-stream";
}
async function inspectMobileWorkbench(page) {
return page.evaluate(async () => {
const visibleTextFor = (selector) => document.querySelector(selector)?.textContent?.replace(/\s+/gu, " ").trim() ?? "";
const ownsHit = (element, hit) => hit === element || element.contains(hit);
const inspectSelector = (selector, label) => {
const element = document.querySelector(selector);
if (!element) return { selector, label, ok: false, missing: true };
const box = element.getBoundingClientRect();
const cx = box.left + box.width / 2;
const cy = box.top + box.height / 2;
const hit = document.elementFromPoint(cx, cy);
return {
selector,
label,
ok: box.width > 0 && box.height > 0 && cx >= 0 && cx <= window.innerWidth && cy >= 0 && cy <= window.innerHeight && ownsHit(element, hit),
center: { x: cx, y: cy },
box: { left: box.left, top: box.top, right: box.right, bottom: box.bottom, width: box.width, height: box.height },
hit: hit ? `${hit.tagName.toLowerCase()}${hit.id ? `#${hit.id}` : ""}` : "none"
};
};
const defaultTargets = [
inspectSelector('[data-route="workspace"]', "工作台"),
inspectSelector('[data-route="gate"]', "内部复核"),
inspectSelector('[data-route="help"]', "使用说明"),
inspectSelector("#command-input", "Agent 输入"),
inspectSelector("#command-send", "发送"),
inspectSelector("#command-clear", "清空"),
inspectSelector("#device-pod-select", "Device Pod 选择"),
inspectSelector('#device-pod-summary [data-device-detail="pod"]', "Pod Summary"),
inspectSelector("#device-event-scroll", "事件流")
];
const text = document.body.textContent ?? "";
const workspace = document.querySelector('[data-view="workspace"]');
const gate = document.querySelector('[data-view="gate"]');
const help = document.querySelector('[data-view="help"]');
const removedSelectors = {
resourceExplorer: document.querySelector("#resource-explorer") === null,
explorerResize: document.querySelector("#explorer-resize") === null,
explorerToggle: document.querySelector("#explorer-toggle") === null,
quickActions: document.querySelector(".quick-actions") === null,
resourceTree: document.querySelector("#resource-tree") === null
};
return {
title: document.title,
workspaceHidden: workspace ? workspace.hidden : null,
gateHidden: gate ? gate.hidden : null,
helpHidden: help ? help.hidden : null,
removedSelectors,
visibleText: {
bodyHasWorkbench: text.includes("用户工作台") && text.includes("Agent 对话") && text.includes("Device Pod"),
gateIsSecondaryLabel: text.includes("内部复核"),
helpIsSecondaryLabel: text.includes("使用说明"),
visibleWorkspaceText: visibleTextFor(".center-workspace").slice(0, 160)
},
defaultWorkbenchReachable:
document.title === "HWLAB 云工作台" &&
workspace?.hidden === false &&
gate?.hidden === true &&
help?.hidden === true &&
text.includes("用户工作台") &&
text.includes("Agent 对话") &&
text.includes("Device Pod") &&
text.includes("设备目标看板") &&
text.includes("事件流") &&
text.includes("内部复核") &&
text.includes("使用说明"),
primaryControlsReachable: defaultTargets.every((target) => target.ok),
resourceExplorerRemoved: Object.values(removedSelectors).every(Boolean),
devicePodSummaryVisible: Boolean(document.querySelector("#device-pod-summary .summary-tile")),
deviceEventStreamVisible: Boolean(document.querySelector("#device-event-text")),
reachable: defaultTargets,
blocked: defaultTargets.filter((target) => !target.ok)
};
});
}
async function inspectWorkbenchLayoutViewport(browser, url, viewport, options = {}) {
const page = await browser.newPage({
viewport: { width: viewport.width, height: viewport.height },
deviceScaleFactor: 1,
isMobile: viewport.isMobile === true
});
const artifacts = { screenshots: [] };
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
await loginWithDefaultCredentials(page);
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
await page.locator("#device-pod-summary .summary-tile").waitFor({ state: "visible", timeout: 12000 });
await page.waitForTimeout(150);
const defaultLayout = await inspectLayoutState(page, {
mode: viewport.isMobile ? "mobile-default" : viewport.width <= 1240 ? "narrow-desktop-default" : "desktop-default",
viewport,
compareTo: null
});
artifacts.screenshots.push(...await captureLayoutScreenshots(page, options.artifactRoot, viewport, defaultLayout.mode));
const leftCollapse = await inspectLeftSidebarCollapse(page, { viewport, artifactRoot: options.artifactRoot });
const rightResize = viewport.width > 1240
? await inspectRightSidebarResizeDesktop(page, { viewport })
: await inspectRightSidebarResizeDisabled(page, { viewport });
const gate = await inspectWorkbenchGateLayout(browser, url, viewport, options);
return { default: defaultLayout, leftCollapse, rightResize, gate, artifacts };
} finally {
await page.close();
}
}
async function inspectWorkbenchGateLayout(browser, url, viewport, options = {}) {
const page = await browser.newPage({
viewport: { width: viewport.width, height: viewport.height },
deviceScaleFactor: 1,
isMobile: viewport.isMobile === true
});
try {
await page.goto(new URL("/gate", url).toString(), { waitUntil: "domcontentloaded", timeout: 15000 });
await loginWithDefaultCredentials(page);
await page.locator('[data-view="gate"]').waitFor({ state: "visible", timeout: 12000 });
await page.waitForTimeout(150);
const currentRoute = await inspectGateLayoutState(page, { viewport, mode: "gate-current" });
currentRoute.artifacts = {
screenshots: await captureGateScreenshots(page, options.artifactRoot, viewport)
};
return { currentRoute };
} finally {
await page.close();
}
}
async function inspectMinimalWorkbenchLayout(browser, url, viewport, options = {}) {
const page = await browser.newPage({
viewport: { width: viewport.width, height: viewport.height },
deviceScaleFactor: 1,
isMobile: viewport.isMobile === true
});
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
await loginWithDefaultCredentials(page);
const observations = await page.evaluate(() => {
const visible = (selector) => {
const element = document.querySelector(selector);
if (!element) return false;
const box = element.getBoundingClientRect();
const style = getComputedStyle(element);
return style.display !== "none" && style.visibility !== "hidden" && box.width > 0 && box.height > 0;
};
return {
authState: document.body.dataset.authState ?? "",
appShellVisible: visible("[data-app-shell]"),
commandInputVisible: visible("#command-input"),
conversationVisible: visible("#conversation-list"),
rightSidebarVisible: visible("#device-pod-sidebar"),
rightSidebarToggleVisible: visible("#right-sidebar-toggle")
};
});
const pass = observations.authState === "authenticated" &&
observations.appShellVisible &&
observations.commandInputVisible &&
observations.conversationVisible &&
observations.rightSidebarVisible &&
observations.rightSidebarToggleVisible;
const screenshots = [await captureElementScreenshot(page, "[data-app-shell]", options.artifactRoot, viewport, `minimal-${viewport.id}-shell`)].filter(Boolean);
return {
id: `layout-minimal-${viewport.id}`,
status: pass ? "pass" : "blocked",
viewport: { id: viewport.id, width: viewport.width, height: viewport.height },
summary: `${viewport.id} workbench opens after auth and exposes core conversation plus Device Pod controls.`,
observations,
artifacts: { screenshots },
failures: pass ? [] : [
{
selector: "[data-app-shell], #command-input, #conversation-list, #device-pod-sidebar, #right-sidebar-toggle",
failureType: "minimal-layout-core-hidden",
summary: "Minimal workbench smoke could not see the authenticated shell or core controls.",
observations
}
]
};
} finally {
await page.close();
}
}
async function inspectRightSidebarResizeDesktop(page, { viewport }) {
const before = await page.evaluate(() => {
const shell = document.querySelector("[data-app-shell]");
const right = document.querySelector("#device-pod-sidebar");
const center = document.querySelector(".center-workspace");
const handle = document.querySelector("#right-sidebar-resize");
return {
rightWidth: right?.getBoundingClientRect().width ?? 0,
centerWidth: center?.getBoundingClientRect().width ?? 0,
cssWidth: Number.parseFloat(getComputedStyle(shell).getPropertyValue("--right-sidebar-width")),
storage: localStorage.getItem("hwlab.workbench.layout.v1"),
handle: {
ariaLabel: handle?.getAttribute("aria-label") ?? "",
title: handle?.getAttribute("title") ?? "",
role: handle?.getAttribute("role") ?? "",
tabIndex: handle?.tabIndex ?? null,
valueMin: handle?.getAttribute("aria-valuemin") ?? "",
valueMax: handle?.getAttribute("aria-valuemax") ?? "",
valueNow: handle?.getAttribute("aria-valuenow") ?? "",
valueText: handle?.getAttribute("aria-valuetext") ?? ""
}
};
});
const min = Number(before.handle.valueMin);
const max = Number(before.handle.valueMax);
const growTarget = Math.min(max, before.rightWidth + 56);
const shrinkTarget = Math.max(min, before.rightWidth - 48);
const dragTarget = growTarget - before.rightWidth >= 24 ? growTarget : shrinkTarget;
await dragRightSidebarResizeHandle(page, dragTarget);
const afterDrag = await rightSidebarResizeMetrics(page);
await page.locator("#right-sidebar-resize").press("ArrowRight");
const afterArrowRight = await rightSidebarResizeMetrics(page);
await page.locator("#right-sidebar-resize").press("End");
const afterEnd = await rightSidebarResizeMetrics(page);
await page.locator("#right-sidebar-resize").press("Home");
const afterHome = await rightSidebarResizeMetrics(page);
await page.locator("#right-sidebar-resize").press("ArrowLeft");
const afterArrowLeft = await rightSidebarResizeMetrics(page);
await page.reload({ waitUntil: "domcontentloaded", timeout: 15000 });
await page.locator("#right-sidebar-resize").waitFor({ state: "visible", timeout: 12000 });
await page.waitForTimeout(150);
const restoredFromStorage = await rightSidebarResizeMetrics(page);
const expectedArrowLeft = Math.min(afterHome.bounds.max, afterHome.bounds.min + 16);
const pass =
before.handle.role === "separator" &&
before.handle.ariaLabel === "拖拽调整右侧 Device Pod 看板宽度" &&
before.handle.title.includes("左右方向键") &&
before.handle.tabIndex === 0 &&
Number.isFinite(min) &&
Number.isFinite(max) &&
max >= min &&
Math.abs(afterDrag.rightWidth - before.rightWidth) >= 20 &&
afterDrag.rightWidth >= min &&
afterDrag.rightWidth <= max &&
afterArrowRight.rightWidth <= afterDrag.rightWidth - 12 &&
Math.abs(afterEnd.rightWidth - afterEnd.bounds.max) <= 2 &&
Math.abs(afterHome.rightWidth - afterHome.bounds.min) <= 2 &&
Math.abs(afterArrowLeft.rightWidth - expectedArrowLeft) <= 2 &&
afterArrowLeft.storage?.version === 1 &&
afterArrowLeft.storage?.rightSidebarWidth === afterArrowLeft.rightWidth &&
Math.abs(restoredFromStorage.rightWidth - afterArrowLeft.rightWidth) <= 2 &&
restoredFromStorage.keyTargetsReachable &&
restoredFromStorage.rootScrollLocked &&
restoredFromStorage.noHorizontalOverflow;
return { viewport, pass, targetWidth: dragTarget, before, afterDrag, afterArrowRight, afterEnd, afterHome, afterArrowLeft, restoredFromStorage };
}
async function dragRightSidebarResizeHandle(page, targetWidth) {
const start = await page.evaluate(() => {
const right = document.querySelector("#device-pod-sidebar");
const handle = document.querySelector("#right-sidebar-resize");
const rightBox = right.getBoundingClientRect();
const handleBox = handle.getBoundingClientRect();
return {
startX: handleBox.left + handleBox.width / 2,
startY: handleBox.top + Math.min(80, handleBox.height / 2),
currentWidth: rightBox.width
};
});
const targetX = start.startX - (targetWidth - start.currentWidth);
await page.mouse.move(start.startX, start.startY);
await page.mouse.down();
await page.mouse.move(targetX, start.startY, { steps: 8 });
await page.mouse.up();
}
async function inspectLeftSidebarCollapse(page, { viewport, artifactRoot }) {
const before = await leftSidebarCollapseMetrics(page);
const beforeScreenshot = await captureElementScreenshot(
page,
"[data-app-shell]",
artifactRoot,
viewport,
"left-sidebar-expanded-before"
);
await page.locator("#left-sidebar-toggle").click();
await page.waitForTimeout(100);
const collapsed = await leftSidebarCollapseMetrics(page);
const collapsedScreenshot = await captureElementScreenshot(
page,
"[data-app-shell]",
artifactRoot,
viewport,
"left-sidebar-collapsed"
);
await page.reload({ waitUntil: "domcontentloaded", timeout: 15000 });
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
await page.waitForTimeout(150);
const restoredCollapsed = await leftSidebarCollapseMetrics(page);
await page.locator("#left-sidebar-toggle").click();
await page.waitForTimeout(100);
const expanded = await leftSidebarCollapseMetrics(page);
const expandedScreenshot = await captureElementScreenshot(
page,
"[data-app-shell]",
artifactRoot,
viewport,
"left-sidebar-expanded-after"
);
const widthReclaimed = collapsed.centerWidth >= before.centerWidth + Math.min(12, Math.max(0, before.railWidth - collapsed.railWidth - 2));
const pass =
before.toggle.ariaExpanded === "true" &&
before.toggle.ariaLabel === "折叠左侧导航" &&
before.toggle.title === "折叠左侧导航" &&
before.routeButtonsVisible &&
collapsed.toggle.ariaExpanded === "false" &&
collapsed.toggle.ariaLabel === "展开左侧导航" &&
collapsed.toggle.title === "展开左侧导航" &&
collapsed.railWidth < before.railWidth - 12 &&
widthReclaimed &&
collapsed.routeButtonsVisible === false &&
collapsed.keyTargetsReachable &&
collapsed.resourceExplorerRemoved &&
collapsed.rootScrollLocked &&
collapsed.noHorizontalOverflow &&
restoredCollapsed.collapsed === true &&
restoredCollapsed.storage?.leftSidebarCollapsed === true &&
restoredCollapsed.keyTargetsReachable &&
expanded.toggle.ariaExpanded === "true" &&
expanded.toggle.ariaLabel === "折叠左侧导航" &&
expanded.railWidth >= before.railWidth - 2 &&
Math.abs(expanded.centerWidth - before.centerWidth) <= 3 &&
expanded.routeButtonsVisible &&
expanded.storage?.leftSidebarCollapsed === false &&
expanded.keyTargetsReachable &&
expanded.resourceExplorerRemoved &&
expanded.rootScrollLocked &&
expanded.noHorizontalOverflow;
return {
viewport,
pass,
widthReclaimed,
before,
collapsed,
restoredCollapsed,
expanded,
artifacts: {
screenshots: [beforeScreenshot, collapsedScreenshot, expandedScreenshot].filter(Boolean)
},
failures: pass ? [] : [
{
selector: "#left-sidebar-toggle",
failureType: "left-sidebar-collapse-regression",
viewport,
summary: "#278 left sidebar collapse/expand did not reclaim space, persist state, or preserve key workbench controls."
}
]
};
}
async function leftSidebarCollapseMetrics(page) {
return page.evaluate(async () => {
const shell = document.querySelector("[data-app-shell]");
const rail = document.querySelector("#activity-rail");
const center = document.querySelector(".center-workspace");
const right = document.querySelector("#device-pod-sidebar");
const toggle = document.querySelector("#left-sidebar-toggle");
const routeButtons = [...document.querySelectorAll(".activity-rail [data-route]")];
const ownsHit = (element, hit) => hit === element || element.contains(hit);
const hitLabel = (hit) => hit ? `${hit.tagName.toLowerCase()}${hit.id ? `#${hit.id}` : ""}` : "none";
const inspectTarget = async (selector) => {
const element = document.querySelector(selector);
if (!element) return { selector, ok: false, missing: true };
element.scrollIntoView({ block: "center", inline: "nearest" });
await new Promise((resolve) => requestAnimationFrame(resolve));
const box = element.getBoundingClientRect();
const x = box.left + box.width / 2;
const y = box.top + box.height / 2;
const stack = document.elementsFromPoint(x, y);
const style = getComputedStyle(element);
return {
selector,
ok: style.display !== "none" && style.visibility !== "hidden" && box.width > 0 && box.height > 0 && x >= 0 && x <= window.innerWidth && y >= 0 && y <= window.innerHeight && stack.some((candidate) => ownsHit(element, candidate)),
hit: hitLabel(stack[0] ?? null)
};
};
const keyTargets = [];
for (const selector of ["#left-sidebar-toggle", "#command-input", "#command-send", "#device-pod-select", '#device-pod-summary [data-device-detail="pod"]', "#device-event-scroll"]) {
keyTargets.push(await inspectTarget(selector));
}
let storage = null;
try { storage = JSON.parse(localStorage.getItem("hwlab.workbench.layout.v1") ?? "null"); } catch { storage = "parse_failed"; }
const railBox = rail?.getBoundingClientRect();
const centerBox = center?.getBoundingClientRect();
const rightBox = right?.getBoundingClientRect();
return {
viewport: { width: window.innerWidth, height: window.innerHeight },
collapsed: shell?.classList.contains("is-left-sidebar-collapsed") === true,
storage,
shellColumns: shell ? getComputedStyle(shell).gridTemplateColumns : "",
railWidth: railBox?.width ?? 0,
centerWidth: centerBox?.width ?? 0,
rightWidth: rightBox?.width ?? 0,
routeButtonsVisible: routeButtons.some((button) => getComputedStyle(button).display !== "none" && button.getBoundingClientRect().width > 0 && button.getBoundingClientRect().height > 0),
toggle: {
text: toggle?.textContent?.trim() ?? "",
ariaExpanded: toggle?.getAttribute("aria-expanded") ?? "",
ariaLabel: toggle?.getAttribute("aria-label") ?? "",
title: toggle?.getAttribute("title") ?? "",
ariaControls: toggle?.getAttribute("aria-controls") ?? ""
},
keyTargets,
keyTargetsReachable: keyTargets.every((target) => target.ok),
resourceExplorerRemoved: document.querySelector("#resource-explorer") === null && document.querySelector("#explorer-resize") === null && document.querySelector("#resource-tree") === null,
noHorizontalOverflow: document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2 && document.body.scrollWidth <= document.body.clientWidth + 2 && (!shell || shell.scrollWidth <= shell.clientWidth + 2) && (!right || right.scrollWidth <= right.clientWidth + 2),
rootScrollLocked: getComputedStyle(document.documentElement).overflow === "hidden" && getComputedStyle(document.body).overflow === "hidden" && document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 && document.body.scrollHeight <= document.body.clientHeight + 2
};
});
}
async function rightSidebarResizeMetrics(page) {
return page.evaluate(async () => {
const shell = document.querySelector("[data-app-shell]");
const center = document.querySelector(".center-workspace");
const right = document.querySelector("#device-pod-sidebar");
const handle = document.querySelector("#right-sidebar-resize");
const ownsHit = (element, hit) => hit === element || element.contains(hit);
const targetSelectors = ["#command-input", "#command-send", "#device-pod-select", '#device-pod-summary [data-device-detail="pod"]', "#device-event-scroll"];
const keyTargets = [];
for (const selector of targetSelectors) {
const element = document.querySelector(selector);
if (!element) { keyTargets.push({ selector, ok: false, missing: true }); continue; }
element.scrollIntoView({ block: "center", inline: "nearest" });
await new Promise((resolve) => requestAnimationFrame(resolve));
const box = element.getBoundingClientRect();
const x = box.left + box.width / 2;
const y = box.top + box.height / 2;
const stack = document.elementsFromPoint(x, y);
keyTargets.push({ selector, ok: box.width > 0 && box.height > 0 && x >= 0 && x <= window.innerWidth && y >= 0 && y <= window.innerHeight && stack.some((candidate) => ownsHit(element, candidate)) });
}
let storage = null;
try { storage = JSON.parse(localStorage.getItem("hwlab.workbench.layout.v1") ?? "null"); } catch { storage = "parse_failed"; }
return {
resourceExplorerPresent: Boolean(document.querySelector("#resource-explorer")),
explorerResizePresent: Boolean(document.querySelector("#explorer-resize")),
explorerTogglePresent: Boolean(document.querySelector("#explorer-toggle")),
centerWidth: center?.getBoundingClientRect().width ?? 0,
rightWidth: right?.getBoundingClientRect().width ?? 0,
cssWidth: Number.parseFloat(getComputedStyle(shell).getPropertyValue("--right-sidebar-width")),
bounds: { min: Number(handle?.getAttribute("aria-valuemin")), max: Number(handle?.getAttribute("aria-valuemax")) },
handleTabIndex: handle?.tabIndex ?? null,
handleAriaDisabled: handle?.getAttribute("aria-disabled") ?? "",
handleAriaHidden: handle?.getAttribute("aria-hidden") ?? "",
handleAriaValueNow: Number(handle?.getAttribute("aria-valuenow")),
handleAriaValueText: handle?.getAttribute("aria-valuetext") ?? "",
storage,
keyTargets,
keyTargetsReachable: keyTargets.every((target) => target.ok),
noHorizontalOverflow: document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2 && document.body.scrollWidth <= document.body.clientWidth + 2 && (!right || right.scrollWidth <= right.clientWidth + 2),
rootScrollLocked: getComputedStyle(document.documentElement).overflow === "hidden" && getComputedStyle(document.body).overflow === "hidden" && document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 && document.body.scrollHeight <= document.body.clientHeight + 2
};
});
}
async function inspectRightSidebarResizeDisabled(page, { viewport }) {
const observation = await page.evaluate(() => {
const handle = document.querySelector("#right-sidebar-resize");
const box = handle?.getBoundingClientRect();
const style = handle ? getComputedStyle(handle) : null;
const right = document.querySelector("#device-pod-sidebar");
const command = document.querySelector("#command-form");
const devicePod = document.querySelector("#device-pod-summary");
return {
viewport: { width: window.innerWidth, height: window.innerHeight },
handlePresent: Boolean(handle),
handleDisplay: style?.display ?? "missing",
handleWidth: box?.width ?? 0,
handleTabIndex: handle?.tabIndex ?? null,
handleAriaDisabled: handle?.getAttribute("aria-disabled") ?? "",
handleAriaHidden: handle?.getAttribute("aria-hidden") ?? "",
rightWidth: right?.getBoundingClientRect().width ?? 0,
commandVisible: Boolean(command && command.getBoundingClientRect().width > 0 && command.getBoundingClientRect().height > 0),
devicePodVisible: Boolean(devicePod && devicePod.getBoundingClientRect().width > 0 && devicePod.getBoundingClientRect().height > 0),
noHorizontalOverflow: document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2 && document.body.scrollWidth <= document.body.clientWidth + 2 && (!right || right.scrollWidth <= right.clientWidth + 2),
rootScrollLocked: getComputedStyle(document.documentElement).overflow === "hidden" && getComputedStyle(document.body).overflow === "hidden" && document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 && document.body.scrollHeight <= document.body.clientHeight + 2
};
});
return {
viewport,
pass: observation.handlePresent && observation.handleDisplay === "none" && observation.handleTabIndex === -1 && observation.handleAriaDisabled === "true" && observation.handleAriaHidden === "true" && observation.commandVisible && observation.devicePodVisible && observation.noHorizontalOverflow && observation.rootScrollLocked,
...observation
};
}
async function inspectLayoutState(page, { mode, viewport, compareTo }) {
return page.evaluate(async ({ mode, viewport, compareTo, resourceExplorerGuard }) => {
const failures = [];
const selectorLabels = [
["#command-input", "Code Agent 输入"],
["#command-send", "发送"],
["#command-clear", "清空"],
["#device-pod-select", "Device Pod 选择"],
['#device-pod-summary [data-device-detail="pod"]', "Pod Summary"],
["#device-pod-interfaces .summary-tile", "接口 summary"],
["#device-event-scroll", "纯文本事件流"],
["#device-event-follow", "事件流跟随按钮"]
];
const overflowSelectors = ["body", "[data-app-shell]", ".workbench-shell", ".center-workspace", "#command-form", ".input-shell", ".right-sidebar", "#device-pod-summary", "#device-pod-interfaces", "#device-event-scroll"];
const overlapPairs = [["#command-form", ".right-sidebar", "Code Agent command form vs right sidebar"]];
const boxForElement = (element) => {
if (!element) return null;
const box = element.getBoundingClientRect();
const style = getComputedStyle(element);
return { left: box.left, top: box.top, right: box.right, bottom: box.bottom, width: box.width, height: box.height, display: style.display, visibility: style.visibility, overflow: style.overflow, overflowY: style.overflowY, overflowX: style.overflowX, scrollHeight: element.scrollHeight, clientWidth: element.clientWidth, clientHeight: element.clientHeight, scrollWidth: element.scrollWidth, text: element.textContent?.replace(/\s+/gu, " ").trim().slice(0, 120) ?? "" };
};
const boxFor = (selector) => boxForElement(document.querySelector(selector));
const boxesOverlap = (a, b, gap = 0) => {
if (!a || !b || a.width <= 0 || a.height <= 0 || b.width <= 0 || b.height <= 0) return false;
return !(a.right + gap <= b.left || b.right + gap <= a.left || a.bottom + gap <= b.top || b.bottom + gap <= a.top);
};
const ownsHit = (element, hit) => hit === element || element.contains(hit);
const hitLabel = (hit) => hit ? `${hit.tagName.toLowerCase()}${hit.id ? `#${hit.id}` : ""}${hit.className ? `.${String(hit.className).trim().replace(/\s+/gu, ".")}` : ""}` : "none";
const inspectHitTarget = async (selector, label) => {
const element = document.querySelector(selector);
if (!element) return { selector, label, ok: false, missing: true };
element.scrollIntoView({ block: "center", inline: "nearest" });
await new Promise((resolve) => requestAnimationFrame(resolve));
const box = element.getBoundingClientRect();
const cx = box.left + box.width / 2;
const cy = box.top + box.height / 2;
const stack = document.elementsFromPoint(cx, cy);
const style = getComputedStyle(element);
const ok = style.display !== "none" && style.visibility !== "hidden" && box.width > 0 && box.height > 0 && cx >= 0 && cx <= window.innerWidth && cy >= 0 && cy <= window.innerHeight && stack.some((candidate) => ownsHit(element, candidate));
return { selector, label, ok, center: { x: cx, y: cy }, box: { left: box.left, top: box.top, right: box.right, bottom: box.bottom, width: box.width, height: box.height }, hit: hitLabel(stack[0] ?? null), hitStack: stack.slice(0, 5).map(hitLabel) };
};
const visibleTextForElement = (root) => {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
const parts = [];
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
const parent = node.parentElement;
if (!parent || parent.closest("[hidden], script, style, noscript")) continue;
const style = getComputedStyle(parent);
if (style.display === "none" || style.visibility === "hidden") continue;
const box = parent.getBoundingClientRect();
if (box.width <= 0 || box.height <= 0) continue;
const text = node.textContent?.replace(/\s+/gu, " ").trim();
if (text) parts.push(text);
}
return parts.join(" ").replace(/\s+/gu, " ").trim();
};
window.scrollTo(0, 240);
document.documentElement.scrollTop = 240;
document.body.scrollTop = 240;
await new Promise((resolve) => requestAnimationFrame(resolve));
const isDesktop = viewport.width >= 861;
const boxes = {
shell: boxFor("[data-app-shell]"),
rail: boxFor(".activity-rail"),
center: boxFor(".center-workspace"),
right: boxFor(".right-sidebar"),
commandBar: boxFor("#command-form"),
commandInput: boxFor("#command-input"),
commandSend: boxFor("#command-send"),
devicePodSidebar: boxFor("#device-pod-sidebar"),
devicePodSummary: boxFor("#device-pod-summary"),
devicePodInterfaces: boxFor("#device-pod-interfaces"),
deviceEventScroll: boxFor("#device-event-scroll"),
topbar: boxFor(".topbar")
};
const noHorizontalOverflow = {
html: document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2,
body: document.body.scrollWidth <= document.body.clientWidth + 2,
shell: boxes.shell ? boxes.shell.scrollWidth <= boxes.shell.clientWidth + 2 : false,
right: boxes.right ? boxes.right.scrollWidth <= boxes.right.clientWidth + 2 : false,
devicePod: (() => {
const element = document.querySelector("#device-pod-sidebar");
return element ? element.scrollWidth <= element.clientWidth + 2 : false;
})()
};
const liveBuildLayout = { ok: true, exists: Boolean(document.querySelector("#live-build-summary")), overlayPositioned: true, dialogVisible: true, dialogViewportContained: true, stableGeometry: true, closedByButton: true };
const rightPanelStacked = Boolean(boxes.right && boxes.center && boxes.right.top >= boxes.center.bottom - 2);
const rightWidthTarget = isDesktop ? (rightPanelStacked ? boxes.right?.width >= 560 && boxes.right?.width <= viewport.width : boxes.right?.width >= 560 && boxes.right?.width <= 740) : boxes.right?.width <= viewport.width && boxes.right?.width >= Math.max(0, viewport.width - 80);
const rootAfterScrollAttempt = { windowScrollY: window.scrollY, htmlScrollTop: document.documentElement.scrollTop, bodyScrollTop: document.body.scrollTop };
const rootScrollLocked = getComputedStyle(document.documentElement).overflow === "hidden" && getComputedStyle(document.body).overflow === "hidden" && document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 && document.body.scrollHeight <= document.body.clientHeight + 2 && rootAfterScrollAttempt.windowScrollY === 0 && rootAfterScrollAttempt.htmlScrollTop === 0 && rootAfterScrollAttempt.bodyScrollTop === 0;
const keyTargets = [];
for (const [selector, label] of selectorLabels) keyTargets.push(await inspectHitTarget(selector, label));
const rightResizeHandle = await inspectHitTarget("#right-sidebar-resize", "右侧 Device Pod 看板宽度调整手柄");
const blockedKeyTargets = keyTargets.filter((target) => !target.ok);
for (const target of blockedKeyTargets) {
failures.push({ failureType: target.missing ? "blocked/skip" : "covered-hit-target", selector: target.selector, viewport, summary: `${target.label} is not center-click reachable; hit=${target.hit ?? "none"}`, target });
}
const centerWidthDelta = compareTo?.boxes?.center?.width && boxes.center ? boxes.center.width - compareTo.boxes.center.width : null;
const rightWidthDelta = compareTo?.boxes?.right?.width && boxes.right ? boxes.right.width - compareTo.boxes.right.width : null;
const overlapChecks = { railCenter: boxesOverlap(boxes.rail, boxes.center, -1), centerRight: isDesktop && boxesOverlap(boxes.center, boxes.right, -1), topbarCommandBar: boxesOverlap(boxes.topbar, boxes.commandBar, -1) };
const semanticOverlapChecks = overlapPairs.map(([aSelector, bSelector, label]) => {
const a = boxFor(aSelector);
const b = boxFor(bSelector);
const overlaps = boxesOverlap(a, b, -1);
if (overlaps) failures.push({ failureType: "overlap", selector: `${aSelector} <-> ${bSelector}`, viewport, summary: `${label} overlap`, boxes: { [aSelector]: a, [bSelector]: b } });
return { label, selectors: [aSelector, bSelector], overlaps, boxes: { [aSelector]: a, [bSelector]: b } };
});
const noIncoherentOverlap = Object.values(overlapChecks).every((value) => value === false);
if (!noIncoherentOverlap) {
for (const [key, value] of Object.entries(overlapChecks)) if (value) failures.push({ failureType: "overlap", selector: key, viewport, summary: `layout region overlap detected: ${key}` });
}
const overflowChecks = overflowSelectors.map((selector) => {
const element = selector === "body" ? document.body : document.querySelector(selector);
if (!element) return { selector, exists: false, ok: true, skipped: true };
const style = getComputedStyle(element);
const horizontalOverflow = element.scrollWidth > element.clientWidth + 2;
const verticalOverflow = element.scrollHeight > element.clientHeight + 2;
const isRootSelector = selector === "body" || selector === "[data-app-shell]" || selector === ".workbench-shell";
const ok = isRootSelector ? !horizontalOverflow && !verticalOverflow : !horizontalOverflow || ["auto", "scroll"].includes(style.overflowX);
const result = { selector, exists: true, ok, horizontalOverflow, verticalOverflow, clientWidth: element.clientWidth, scrollWidth: element.scrollWidth, clientHeight: element.clientHeight, scrollHeight: element.scrollHeight, overflowX: style.overflowX, overflowY: style.overflowY };
if (!ok) failures.push({ failureType: "overflow", selector, viewport, summary: `${selector} overflow regression: scrollWidth=${element.scrollWidth}, clientWidth=${element.clientWidth}, scrollHeight=${element.scrollHeight}, clientHeight=${element.clientHeight}`, overflow: result });
return result;
});
const overflowOk = overflowChecks.every((check) => check.ok);
const rightSidebar = document.querySelector("#device-pod-sidebar");
const summaryTiles = [...document.querySelectorAll("#device-pod-sidebar .summary-tile")];
const detailDialog = document.querySelector("#device-detail-dialog");
const podSummaryTile = document.querySelector('#device-pod-summary [data-device-detail="pod"]');
const eventScroll = document.querySelector("#device-event-scroll");
const eventText = document.querySelector("#device-event-text");
let detailDialogOpened = false;
let detailDialogClosed = false;
if (podSummaryTile && detailDialog) {
podSummaryTile.click();
await new Promise((resolve) => requestAnimationFrame(resolve));
detailDialogOpened = detailDialog.open === true;
detailDialog.querySelector('button[value="close"]')?.click();
await new Promise((resolve) => requestAnimationFrame(resolve));
detailDialogClosed = detailDialog.open === false;
}
const visibleWorkbenchText = visibleTextForElement(document.body);
const removedSelectorStates = Object.fromEntries(resourceExplorerGuard.domSelectors.map((selector) => [selector, document.querySelector(selector) === null]));
const legacyRightSelectorsAbsent = !rightSidebar?.querySelector("details,[data-hardware-tab],#hardware-list,#m3-control-form,#panel-wiring,#records-list");
const removedSelectorsAbsent = Object.values(removedSelectorStates).every(Boolean);
const removedVisibleCopyHits = resourceExplorerGuard.copyTerms.filter((term) => visibleWorkbenchText.includes(term));
const removedVisibleCopyAbsent = removedVisibleCopyHits.length === 0;
const resourceExplorerRemovalGuard = removedSelectorsAbsent && removedVisibleCopyAbsent;
const devicePod = {
summaryOk: Boolean(rightSidebar && summaryTiles.length >= 2 && summaryTiles.every((tile) => tile.getAttribute("role") === "button" && tile.tabIndex === 0)),
noInternalDisclosure: Boolean(rightSidebar && !rightSidebar.querySelector("details")),
noLegacySelectors: legacyRightSelectorsAbsent,
detailDialogOk: Boolean(detailDialog && detailDialogOpened && detailDialogClosed),
eventStreamOk: Boolean(eventScroll && eventText && getComputedStyle(eventScroll).overflowY !== "visible" && eventText.textContent?.trim()),
summaryTileCount: summaryTiles.length,
eventTextSample: eventText?.textContent?.replace(/\s+/gu, " ").trim().slice(0, 180) ?? ""
};
devicePod.ok = devicePod.summaryOk && devicePod.noInternalDisclosure && devicePod.noLegacySelectors && devicePod.detailDialogOk && devicePod.eventStreamOk;
if (!resourceExplorerRemovalGuard) failures.push({ failureType: "blocked/skip", selector: "#resource-explorer,#explorer-resize", viewport, summary: "Removed resource explorer DOM or legacy copy returned in the default workbench.", removedSelectorStates, removedVisibleCopyHits });
if (!rootScrollLocked) failures.push({ failureType: "outer-scroll-regression", selector: "html/body/.workbench-shell", viewport, summary: "Outer html/body/workbench shell scrolled or exceeded viewport after scroll attempt.", rootAfterScrollAttempt, html: boxForElement(document.documentElement), body: boxForElement(document.body), shell: boxes.shell });
if (!rightWidthTarget) failures.push({ failureType: "overflow", selector: ".right-sidebar", viewport, summary: "Device Pod summary board width is outside the expected desktop, stacked desktop, or mobile boundary.", rightPanelStacked, rightBox: boxes.right });
if (!devicePod.ok) failures.push({ failureType: "blocked/skip", selector: "#device-pod-sidebar", viewport, summary: "Device Pod sidebar must show summary tiles only, no internal disclosure, and open details in a dialog.", devicePod });
const pass = rootScrollLocked && noIncoherentOverlap && semanticOverlapChecks.every((check) => !check.overlaps) && overflowOk && Object.values(noHorizontalOverflow).every(Boolean) && rightWidthTarget && blockedKeyTargets.length === 0 && resourceExplorerRemovalGuard && liveBuildLayout.ok && devicePod.ok;
return {
mode,
viewport,
pass,
removedSelectorStates,
removedSelectorsAbsent,
removedVisibleCopyAbsent,
removedVisibleCopyHits,
resourceExplorerRemovalGuard,
rootScrollLocked,
rootAfterScrollAttempt,
boxes,
centerWidthDelta,
rightWidthDelta,
rightPanelStacked,
overlapChecks,
noHorizontalOverflow,
horizontalOverflowFree: Object.values(noHorizontalOverflow).every(Boolean),
liveBuildLayout,
rightWidthTarget,
noIncoherentOverlap,
keyTargetsReachable: blockedKeyTargets.length === 0,
blockedKeyTargets,
rightResizeHandle,
devicePod,
devicePodSummaryBoardOk: devicePod.summaryOk && devicePod.noInternalDisclosure && devicePod.noLegacySelectors,
devicePodEventStreamOk: devicePod.eventStreamOk,
devicePodDetailDialogOk: devicePod.detailDialogOk,
liveBuildOverlayStable: liveBuildLayout.ok,
semanticOverlapChecks,
overflowChecks,
issue288SingleTableReady: false,
skip: [],
failures
};
}, { mode, viewport, compareTo, resourceExplorerGuard: { domSelectors: [...removedResourceExplorerDomSelectors], copyTerms: [...removedResourceExplorerCopyTerms] } });
}
async function inspectGateLayoutState(page, { mode, viewport }) {
return page.evaluate(async ({ mode, viewport }) => {
const failures = [];
const skip = [];
const boxForElement = (element) => {
if (!element) return null;
const box = element.getBoundingClientRect();
const style = getComputedStyle(element);
return {
left: box.left,
top: box.top,
right: box.right,
bottom: box.bottom,
width: box.width,
height: box.height,
display: style.display,
visibility: style.visibility,
overflowX: style.overflowX,
overflowY: style.overflowY,
clientWidth: element.clientWidth,
scrollWidth: element.scrollWidth,
clientHeight: element.clientHeight,
scrollHeight: element.scrollHeight,
text: element.textContent?.replace(/\s+/gu, " ").trim().slice(0, 160) ?? ""
};
};
const boxFor = (selector) => boxForElement(document.querySelector(selector));
const boxesOverlap = (a, b, gap = 0) => {
if (!a || !b || a.width <= 0 || a.height <= 0 || b.width <= 0 || b.height <= 0) return false;
return !(a.right + gap <= b.left || b.right + gap <= a.left || a.bottom + gap <= b.top || b.bottom + gap <= a.top);
};
const ownsHit = (element, hit) => hit === element || element.contains(hit);
const hitLabel = (hit) => hit ? `${hit.tagName.toLowerCase()}${hit.id ? `#${hit.id}` : ""}${hit.className ? `.${String(hit.className).trim().replace(/\s+/gu, ".")}` : ""}` : "none";
const hitTarget = (selector, label) => {
const element = document.querySelector(selector);
if (!element) {
const missing = { selector, label, ok: false, missing: true };
failures.push({
failureType: "blocked/skip",
selector,
viewport,
summary: `${label} is missing on /gate`,
target: missing
});
return missing;
}
const box = element.getBoundingClientRect();
const cx = box.left + box.width / 2;
const cy = box.top + Math.min(box.height / 2, 24);
const stack = document.elementsFromPoint(cx, cy);
const ok =
box.width > 0 &&
box.height > 0 &&
cx >= 0 &&
cx <= window.innerWidth &&
cy >= 0 &&
cy <= window.innerHeight &&
stack.some((candidate) => ownsHit(element, candidate));
const target = {
selector,
label,
ok,
center: { x: cx, y: cy },
hit: hitLabel(stack[0] ?? null),
hitStack: stack.slice(0, 5).map(hitLabel),
box: { left: box.left, top: box.top, right: box.right, bottom: box.bottom, width: box.width, height: box.height }
};
if (!ok) {
failures.push({
failureType: "covered-hit-target",
selector,
viewport,
summary: `${label} is not user-reachable on /gate; hit=${target.hit}`,
target
});
}
return target;
};
window.scrollTo(0, 240);
document.documentElement.scrollTop = 240;
document.body.scrollTop = 240;
await new Promise((resolve) => requestAnimationFrame(resolve));
const gate = document.querySelector('[data-view="gate"]');
const workspace = document.querySelector('[data-view="workspace"]');
const shell = document.querySelector("[data-app-shell]");
const html = boxForElement(document.documentElement);
const body = boxForElement(document.body);
const shellBox = boxFor("[data-app-shell]");
const gateBox = boxFor('[data-view="gate"]');
const rootAfterScrollAttempt = {
windowScrollY: window.scrollY,
htmlScrollTop: document.documentElement.scrollTop,
bodyScrollTop: document.body.scrollTop
};
const rootScrollLocked =
getComputedStyle(document.documentElement).overflow === "hidden" &&
getComputedStyle(document.body).overflow === "hidden" &&
document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 &&
document.body.scrollHeight <= document.body.clientHeight + 2 &&
rootAfterScrollAttempt.windowScrollY === 0 &&
rootAfterScrollAttempt.htmlScrollTop === 0 &&
rootAfterScrollAttempt.bodyScrollTop === 0;
if (!rootScrollLocked) {
failures.push({
failureType: "outer-scroll-regression",
selector: "html/body/.workbench-shell",
viewport,
summary: "/gate route allowed page-level scrolling.",
rootAfterScrollAttempt,
html,
body,
shell: shellBox
});
}
const hitTargets = [
hitTarget('[data-route="workspace"]', "工作台"),
hitTarget('[data-route="gate"]', "内部复核"),
hitTarget('[data-route="help"]', "使用说明")
];
const gateChildren = [...document.querySelectorAll('[data-view="gate"] > *')].map((element, index) => ({
selector: `[data-view="gate"] > :nth-child(${index + 1})`,
box: boxForElement(element)
}));
const visibleOverlapPairs = [];
for (let i = 0; i < gateChildren.length; i += 1) {
for (let j = i + 1; j < gateChildren.length; j += 1) {
const a = gateChildren[i];
const b = gateChildren[j];
if (boxesOverlap(a.box, b.box, -1)) {
visibleOverlapPairs.push([a.selector, b.selector]);
failures.push({
failureType: "overlap",
selector: `${a.selector} <-> ${b.selector}`,
viewport,
summary: "/gate visible sections overlap.",
boxes: { [a.selector]: a.box, [b.selector]: b.box }
});
}
}
}
const gateHorizontalOverflow = Boolean(gate && gate.scrollWidth > gate.clientWidth + 2);
if (gateHorizontalOverflow) {
failures.push({
failureType: "overflow",
selector: '[data-view="gate"]',
viewport,
summary: `/gate horizontal overflow: scrollWidth=${gate.scrollWidth}, clientWidth=${gate.clientWidth}`,
overflow: {
scrollWidth: gate.scrollWidth,
clientWidth: gate.clientWidth,
overflowX: getComputedStyle(gate).overflowX
}
});
}
const tableCount = document.querySelectorAll('[data-view="gate"] table').length;
const workspacePanels = document.querySelectorAll('[data-view="gate"] .workspace-panel').length;
const complexLayoutCount = document.querySelectorAll('[data-view="gate"] .gate-grid, [data-view="gate"] .gate-split, [data-view="gate"] .milestone-grid').length;
const issue288SingleTableReady = tableCount === 1 && workspacePanels <= 1 && complexLayoutCount === 0;
if (!issue288SingleTableReady) {
skip.push({
failureType: "skip",
selector: '[data-view="gate"]',
viewport,
summary: `#288 single-table gate page not yet implemented: tableCount=${tableCount}, workspacePanels=${workspacePanels}, complexLayoutCount=${complexLayoutCount}`
});
}
const pass =
window.location.pathname.replace(/\/+$/, "") === "/gate" &&
gate?.hidden === false &&
workspace?.hidden === true &&
shell !== null &&
rootScrollLocked &&
hitTargets.every((target) => target.ok) &&
!gateHorizontalOverflow &&
visibleOverlapPairs.length === 0;
return {
mode,
viewport,
pass,
pathname: window.location.pathname,
workspaceHidden: workspace ? workspace.hidden : null,
gateHidden: gate ? gate.hidden : null,
rootScrollLocked,
rootAfterScrollAttempt,
boxes: {
html,
body,
shell: shellBox,
gate: gateBox
},
hitTargets,
visibleOverlapPairs,
gateHorizontalOverflow,
tableCount,
workspacePanels,
complexLayoutCount,
issue288SingleTableReady,
issue287HardwareTabsReady: false,
failures,
skip
};
}, { mode, viewport });
}
async function captureLayoutScreenshots(page, artifactRoot, viewport, stateId) {
if (!artifactRoot) return [];
return [
await captureElementScreenshot(page, "[data-app-shell]", artifactRoot, viewport, `${stateId}-shell`),
await captureElementScreenshot(page, ".center-workspace", artifactRoot, viewport, `${stateId}-center-workspace`),
await captureElementScreenshot(page, "#command-form", artifactRoot, viewport, `${stateId}-command-form`),
await captureElementScreenshot(page, "#device-pod-sidebar", artifactRoot, viewport, `${stateId}-device-pod-sidebar`),
await captureElementScreenshot(page, "#device-pod-summary", artifactRoot, viewport, `${stateId}-device-pod-summary`),
await captureElementScreenshot(page, "#device-event-scroll", artifactRoot, viewport, `${stateId}-device-event-stream`)
].filter(Boolean);
}
async function captureGateScreenshots(page, artifactRoot, viewport) {
if (!artifactRoot) return [];
return [
await captureElementScreenshot(page, '[data-view="gate"]', artifactRoot, viewport, "gate-current")
].filter(Boolean);
}
async function captureElementScreenshot(page, selector, artifactRoot, viewport, label) {
const locator = page.locator(selector).first();
const count = await locator.count();
if (count === 0) return null;
const safeLabel = label.replace(/[^a-z0-9_-]+/giu, "-").replace(/^-|-$/gu, "").toLowerCase();
const filePath = path.join(artifactRoot, `${viewport.width}x${viewport.height}-${safeLabel}.png`);
try {
await locator.screenshot({ path: filePath, timeout: 5000 });
return {
selector,
viewport: { width: viewport.width, height: viewport.height },
label,
path: filePath
};
} catch (error) {
return {
selector,
viewport: { width: viewport.width, height: viewport.height },
label,
path: filePath,
status: "blocked",
failureType: "screenshot-diff",
summary: `screenshot capture failed: ${error.message}`
};
}
}
function firstFailure(check) {
return check?.observations?.failures?.[0] ?? null;
}
function allViewportCoverage(viewportResults, fieldName) {
return Object.values(viewportResults).every((result) =>
Boolean(result.default?.[fieldName] ?? result.gate?.currentRoute?.[fieldName])
);
}
function summarizeResourceExplorerRemovalCoverage(viewportResults) {
const coverageEntries = Object.entries(viewportResults).map(([id, result]) => {
const observation = result.default;
return [
id,
{
viewport: observation?.viewport ?? null,
covered: Boolean(observation?.resourceExplorerRemovalGuard),
removedSelectorsAbsent: observation?.removedSelectorsAbsent === true,
removedVisibleCopyAbsent: observation?.removedVisibleCopyAbsent === true,
removedVisibleCopyHits: observation?.removedVisibleCopyHits ?? [],
removedSelectorStates: observation?.removedSelectorStates ?? {}
}
];
});
const coverage = Object.fromEntries(coverageEntries);
const failures = coverageEntries
.filter(([, item]) => !item.covered)
.map(([id, item]) => ({
checkId: "layout-feedback-352-resource-explorer-removed",
viewport: item.viewport,
selector: "#resource-explorer,#explorer-resize",
failureType: "blocked/skip",
summary: `#352 resource explorer removal guard failed in ${id} viewport.`,
removedSelectorsAbsent: item.removedSelectorsAbsent,
removedVisibleCopyAbsent: item.removedVisibleCopyAbsent,
removedVisibleCopyHits: item.removedVisibleCopyHits,
removedSelectorStates: item.removedSelectorStates
}));
return {
status: failures.length === 0 ? "pass" : "blocked",
forbiddenSelectors: [...removedResourceExplorerDomSelectors],
forbiddenCopy: [...removedResourceExplorerCopyTerms],
coverage,
failures
};
}
function summarizeLeftSidebarCollapseCoverage(viewportResults) {
const coverageEntries = Object.entries(viewportResults).map(([id, result]) => {
const observation = result.leftCollapse;
return [
id,
{
viewport: observation?.viewport ?? null,
covered: observation?.pass === true,
widthReclaimed: observation?.widthReclaimed === true,
before: observation?.before
? {
railWidth: observation.before.railWidth,
centerWidth: observation.before.centerWidth,
toggle: observation.before.toggle,
routeButtonsVisible: observation.before.routeButtonsVisible
}
: null,
collapsed: observation?.collapsed
? {
railWidth: observation.collapsed.railWidth,
centerWidth: observation.collapsed.centerWidth,
toggle: observation.collapsed.toggle,
routeButtonsVisible: observation.collapsed.routeButtonsVisible,
keyTargetsReachable: observation.collapsed.keyTargetsReachable,
resourceExplorerRemoved: observation.collapsed.resourceExplorerRemoved,
rootScrollLocked: observation.collapsed.rootScrollLocked,
noHorizontalOverflow: observation.collapsed.noHorizontalOverflow
}
: null,
restoredCollapsed: observation?.restoredCollapsed
? {
collapsed: observation.restoredCollapsed.collapsed,
storage: observation.restoredCollapsed.storage,
keyTargetsReachable: observation.restoredCollapsed.keyTargetsReachable
}
: null,
expanded: observation?.expanded
? {
railWidth: observation.expanded.railWidth,
centerWidth: observation.expanded.centerWidth,
toggle: observation.expanded.toggle,
routeButtonsVisible: observation.expanded.routeButtonsVisible,
storage: observation.expanded.storage,
keyTargetsReachable: observation.expanded.keyTargetsReachable,
resourceExplorerRemoved: observation.expanded.resourceExplorerRemoved,
rootScrollLocked: observation.expanded.rootScrollLocked,
noHorizontalOverflow: observation.expanded.noHorizontalOverflow
}
: null
}
];
});
const coverage = Object.fromEntries(coverageEntries);
const failures = coverageEntries
.filter(([, item]) => !item.covered)
.map(([id, item]) => ({
checkId: "layout-left-sidebar-collapse",
viewport: item.viewport,
selector: "#left-sidebar-toggle",
failureType: "left-sidebar-collapse-regression",
summary: `#278 left activity rail collapse guard failed in ${id} viewport.`,
observation: item
}));
return {
status: failures.length === 0 ? "pass" : "blocked",
coverage,
failures
};
}
function summarizeCoverageGap(viewportResults, issueKey) {
const fieldName = {
issue288: "issue288SingleTableReady",
devicePodSummary: "devicePodSummaryBoardOk",
devicePodEvents: "devicePodEventStreamOk",
devicePodDialog: "devicePodDetailDialogOk",
liveBuildOverlay: "liveBuildOverlayStable"
}[issueKey] ?? "issue287HardwareTabsReady";
return {
status: allViewportCoverage(viewportResults, fieldName) ? "pass" : "skip",
coverage: Object.fromEntries(
Object.entries(viewportResults).map(([id, result]) => [
id,
{
viewport: result.default?.viewport ?? result.gate?.currentRoute?.viewport ?? null,
covered: Boolean(result.default?.[fieldName] ?? result.gate?.currentRoute?.[fieldName]),
currentVisibleContainersChecked: true,
skip: [
...(result.default?.skip ?? []),
...(result.gate?.currentRoute?.skip ?? [])
].filter((item) => issueKey === "issue288"
? item.summary?.includes("#288")
: issueKey === "issue287"
? item.summary?.includes("#287")
: false)
}
])
)
};
}
function layoutArtifactTimestamp(date = new Date()) {
return date.toISOString().replace(/[-:]/gu, "").replace(/\.\d{3}Z$/u, "Z");
}
async function inspectWorkbenchScrollContract(page, { panelSelector }) {
return page.evaluate(async ({ panelSelector }) => {
function metrics(selector) {
const element = selector === "html" ? document.documentElement : selector === "body" ? document.body : document.querySelector(selector);
if (!element) return null;
const style = getComputedStyle(element);
return {
selector,
overflow: style.overflow,
overflowY: style.overflowY,
height: element.getBoundingClientRect().height,
clientHeight: element.clientHeight,
scrollHeight: element.scrollHeight,
scrollTop: element.scrollTop
};
}
window.scrollTo(0, 240);
document.documentElement.scrollTop = 240;
document.body.scrollTop = 240;
await new Promise((resolve) => requestAnimationFrame(resolve));
const rootAfterScrollAttempt = {
windowScrollY: window.scrollY,
htmlScrollTop: document.documentElement.scrollTop,
bodyScrollTop: document.body.scrollTop
};
const panel = document.querySelector(panelSelector);
const beforePanelScroll = panel?.scrollTop ?? null;
if (panel) {
panel.scrollTop = 0;
await new Promise((resolve) => requestAnimationFrame(resolve));
panel.scrollTop = panel.scrollHeight;
await new Promise((resolve) => requestAnimationFrame(resolve));
}
const afterPanelScroll = panel?.scrollTop ?? null;
const html = metrics("html");
const body = metrics("body");
const shell = metrics("[data-app-shell]");
const panelMetrics = metrics(panelSelector);
const rootScrollLocked =
html?.overflow === "hidden" &&
body?.overflow === "hidden" &&
Math.abs((shell?.height ?? 0) - window.innerHeight) <= 2 &&
document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 &&
document.body.scrollHeight <= document.body.clientHeight + 2 &&
window.scrollY === 0 &&
document.documentElement.scrollTop === 0 &&
document.body.scrollTop === 0;
return {
viewport: { width: window.innerWidth, height: window.innerHeight },
outerScrollLocked: rootScrollLocked,
panelScrolls: Boolean(panelMetrics && panelMetrics.scrollHeight > panelMetrics.clientHeight && afterPanelScroll > beforePanelScroll),
rootAfterScrollAttempt,
html,
body,
shell,
panel: panelMetrics
};
}, { panelSelector });
}
async function inspectHelpMarkdownRoute(page) {
return page.evaluate(async () => {
const workspace = document.querySelector('[data-view="workspace"]');
const gate = document.querySelector('[data-view="gate"]');
const help = document.querySelector('[data-view="help"]');
const helpContent = document.querySelector("#help-content");
const heading = helpContent?.querySelector("h1");
const renderedLists = helpContent?.querySelectorAll("li").length ?? 0;
const codeCount = helpContent?.querySelectorAll("code").length ?? 0;
const text = helpContent?.textContent ?? "";
const helpScroll = await (async () => {
if (!helpContent) return null;
helpContent.scrollTop = 0;
await new Promise((resolve) => requestAnimationFrame(resolve));
const before = helpContent.scrollTop;
for (let attempt = 0; attempt < 3; attempt += 1) {
helpContent.scrollTop = helpContent.scrollHeight;
await new Promise((resolve) => requestAnimationFrame(resolve));
if (helpContent.scrollTop > before) break;
}
return {
before,
after: helpContent.scrollTop,
clientHeight: helpContent.clientHeight,
scrollHeight: helpContent.scrollHeight,
overflowY: getComputedStyle(helpContent).overflowY
};
})();
const rootStillLocked =
getComputedStyle(document.documentElement).overflow === "hidden" &&
getComputedStyle(document.body).overflow === "hidden" &&
document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 &&
document.body.scrollHeight <= document.body.clientHeight + 2;
return {
hash: window.location.hash,
pathname: window.location.pathname,
helpState: helpContent?.dataset.helpState ?? null,
workspaceHidden: workspace ? workspace.hidden : null,
gateHidden: gate ? gate.hidden : null,
helpHidden: help ? help.hidden : null,
heading: heading?.textContent?.trim() ?? null,
renderedLists,
codeCount,
hasRawMarkdownHeading: Boolean(helpContent?.textContent?.includes("# 云工作台内部使用说明")),
termsPresent:
["活动栏与默认工作区", "Agent 对话与输入区", "same-origin", "/v1", ":16666"].every((term) => text.includes(term)) &&
codeCount >= 8,
helpPanelScrolls: Boolean(helpScroll && helpScroll.scrollHeight > helpScroll.clientHeight && helpScroll.after > helpScroll.before),
rootStillLocked,
helpScroll,
routeIsHashHelp: window.location.hash === "#help",
routeIsSlashHashHelp: window.location.hash === "#/help",
routeIsDirectHelp: (window.location.pathname.replace(/\/+$/, "") || "/") === "/help",
nonDefaultMarkdownHelp:
(window.location.hash === "#help" || window.location.hash === "#/help" || (window.location.pathname.replace(/\/+$/, "") || "/") === "/help") &&
workspace?.hidden === true &&
gate?.hidden === true &&
help?.hidden === false &&
helpContent?.dataset.helpState === "ready" &&
heading?.textContent?.trim() === "云工作台内部使用说明" &&
renderedLists >= 8 &&
codeCount >= 8 &&
!helpContent?.textContent?.includes("# 云工作台内部使用说明") &&
rootStillLocked &&
Boolean(helpScroll && helpScroll.scrollHeight > helpScroll.clientHeight && helpScroll.after > helpScroll.before)
};
});
}
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
}
function isBrowserExecutionEnvironmentError(error) {
const message = String(error?.message ?? error ?? "");
return /Executable doesn't exist|Please run the following command to download new browsers|does not support chromium|Host system is missing dependencies|browserType\.launch|Failed to launch|No usable sandbox|Missing X server|Target page, context or browser has been closed/iu.test(message);
}
export function printSmokeHelp() {
return {
status: "usage",
command: `node scripts/dev-cloud-workbench-smoke.mjs --source | --layout [--url http://74.48.78.17:16666/] | --mobile | --local-agent-fixture | --session-continuity-fixture | --local-agent-timeout-fixture | --dom-only --url http://74.48.78.17:16666/ [--report ${domOnlyReportPath}] | --live --confirm-dev-live --url http://74.48.78.17:16666/ [--report ${liveReportPath}]`,
notes: [
"Default/source mode reads repository files and emits SOURCE-level evidence only; it never calls the live provider and must not claim DEV-LIVE.",
"--layout runs desktop and mobile browser geometry/hit-target checks for sidebar collapse/expand; with --url it remains layout-only live evidence and does not claim M3 acceptance.",
"--mobile runs a local static 390x844 browser hit-test and still emits SOURCE-level evidence only.",
"--local-agent-fixture runs two local browser send/reply paths with a sanitized SOURCE fixture delayed beyond 4500ms and does not claim DEV-LIVE.",
"--session-continuity-fixture verifies same-browser conversation/session/thread reuse, retry context, and degraded missing providerTrace/thread copy without claiming DEV-LIVE.",
"--local-agent-timeout-fixture runs a local slow SOURCE fixture to verify bounded timeout UI classification and retry preservation without live acceptance.",
"--dom-only preserves runtime/web-asset identity preflight, inspects read-only deployed DOM/help wiring, and never posts /v1/agent/chat.",
"DEV-LIVE mode requires --live --confirm-dev-live, opens the deployed workbench in a browser, sends the simple Chinese prompt and 列出你能使用的所有skill, and retains only non-sensitive response fields."
]
};
}