import fs from "node:fs"; import { execFileSync } from "node:child_process"; import { createHash } from "node:crypto"; import http from "node:http"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { buildDevicePodRestPayload } from "../../internal/device-pod/fake-data.mjs"; import { importPlaywright, launchChromium, playwrightDependencyCommand, playwrightDependencySummary } from "./browser-launcher.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 defaultLayoutProgressIntervalMs = 10000; const defaultLayoutViewportTimeoutMs = 15000; const defaultLayoutCleanupTimeoutMs = 3000; 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", "react-markdown", "remark", "remark-gfm", "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/components/shared/MessageMarkdown.tsx", "src/hooks/useAuth.ts", "src/services/api/client.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"]); 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(); } 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 === "--viewport-timeout-ms") { index += 1; args.viewportTimeoutMs = parsePositiveIntegerArg("--viewport-timeout-ms", argv[index]); } else if (arg === "--progress-interval-ms") { index += 1; args.progressIntervalMs = parsePositiveIntegerArg("--progress-interval-ms", argv[index], { allowZero: true }); } else if (arg === "--cleanup-timeout-ms") { index += 1; args.cleanupTimeoutMs = parsePositiveIntegerArg("--cleanup-timeout-ms", argv[index]); } else if (arg === "--no-progress") { args.progressEnabled = false; } 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 parsePositiveIntegerArg(flag, value, options = {}) { if (!value) throw new Error(`${flag} requires a value`); const parsed = Number(value); const valid = Number.isInteger(parsed) && (options.allowZero ? parsed >= 0 : parsed > 0); if (!valid) throw new Error(`${flag} must be a ${options.allowZero ? "non-negative" : "positive"} integer`); return parsed; } 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: ["
", "/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, shared markdown rendering, and UI components are separated.", { blocker: "contract_blocker", evidence: ["services/api/client.ts", "state/workbench.ts", "state/conversation.ts", "components/shared/MessageMarkdown.tsx"] }); 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 progress = createLayoutSmokeProgress(args); const viewportTimeoutMs = args.viewportTimeoutMs ?? defaultLayoutViewportTimeoutMs; const cleanupTimeoutMs = args.cleanupTimeoutMs ?? defaultLayoutCleanupTimeoutMs; const artifactRoot = path.resolve( repoRoot, args.artifactDir ?? path.join("tmp", "dev-cloud-workbench-layout-smoke", layoutArtifactTimestamp()) ); const layoutViewports = workbenchLayoutViewports(); let chromium; try { progress.log("import-playwright", { sourceMode: layoutSourceMode }); ({ chromium } = await importPlaywright()); } catch (error) { progress.stop(); 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) { progress.log("build-start", { command: "bun run web/hwlab-cloud-web/scripts/build.ts" }); try { execFileSync(resolveBunCommand(), ["run", "web/hwlab-cloud-web/scripts/build.ts"], { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 30000 }); } catch (error) { progress.stop(); throw error; } progress.log("build-done"); } let browser; let server = null; let url = useLiveUrl ? args.url : null; let cleanupStarted = false; const cleanupResources = async (reason) => { if (cleanupStarted) return; cleanupStarted = true; progress.log("cleanup-start", { reason, browser: Boolean(browser), server: Boolean(server), timeoutMs: cleanupTimeoutMs }); if (browser) await closeWithTimeout(() => browser.close(), cleanupTimeoutMs, "browser.close", progress); if (server) await closeWithTimeout(() => server.close(), cleanupTimeoutMs, "static-server.close", progress); progress.log("cleanup-done", { reason }); }; const signalHandlers = registerLayoutSmokeSignalCleanup(cleanupResources, progress); try { progress.log("artifact-dir", { artifactRoot }); await fs.promises.mkdir(artifactRoot, { recursive: true }); progress.log(useLiveUrl ? "use-live-url" : "start-static-server", { url: useLiveUrl ? args.url : null }); server = useLiveUrl ? null : await startStaticWebServer({ rootDir: args.build === true ? path.join(webRoot, "dist") : webRoot, authFixture: true, liveBuildsFixture: true }); url = useLiveUrl ? args.url : server.url; progress.log(useLiveUrl ? "live-url-ready" : "static-server-ready", { url }); progress.log("launch-browser"); browser = await launchChromium(chromium); progress.log("browser-launched", browser.hwlabLaunchPlan ?? {}); const checks = []; const screenshots = []; for (const viewport of layoutViewports) { progress.log("viewport-start", { viewport: viewport.id, timeoutMs: viewportTimeoutMs }); const check = await withLayoutTimeout( inspectMinimalWorkbenchLayout(browser, url, viewport, { artifactRoot }), viewportTimeoutMs, `layout viewport ${viewport.id}` ); progress.log("viewport-done", { viewport: viewport.id, status: check.status }); 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 { removeLayoutSmokeSignalCleanup(signalHandlers); await cleanupResources("finally"); progress.stop(); } } 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 createLayoutSmokeProgress(args = {}) { const enabled = args.progressEnabled !== false; const intervalMs = args.progressIntervalMs ?? defaultLayoutProgressIntervalMs; let lastStage = "start"; let interval = null; const write = (stage, details = {}) => { if (!enabled) return; const payload = { event: "layout-smoke-progress", stage, generatedAt: new Date().toISOString(), ...details }; process.stderr.write(`[dev-cloud-workbench-layout-smoke] ${JSON.stringify(payload)}\n`); }; if (enabled && intervalMs > 0) { interval = setInterval(() => write("heartbeat", { lastStage }), intervalMs); interval.unref?.(); } return { log(stage, details = {}) { lastStage = stage; write(stage, details); }, stop() { if (interval) clearInterval(interval); interval = null; } }; } function registerLayoutSmokeSignalCleanup(cleanupResources, progress) { const handlers = []; for (const signal of ["SIGINT", "SIGTERM"]) { const handler = () => { progress.log("signal", { signal }); cleanupResources(signal) .catch((error) => progress.log("signal-cleanup-failed", { signal, summary: error instanceof Error ? error.message : String(error) })) .finally(() => process.exit(signal === "SIGINT" ? 130 : 143)); }; process.once(signal, handler); handlers.push({ signal, handler }); } return handlers; } function removeLayoutSmokeSignalCleanup(handlers) { for (const { signal, handler } of handlers) { process.off(signal, handler); } } function withLayoutTimeout(promise, timeoutMs, label) { let timeout = null; const timer = new Promise((_, reject) => { timeout = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs); }); return Promise.race([promise, timer]).finally(() => { if (timeout) clearTimeout(timeout); }); } async function closeWithTimeout(closeFn, timeoutMs, label, progress) { try { await withLayoutTimeout(Promise.resolve().then(closeFn), timeoutMs, label); progress.log("cleanup-resource-done", { resource: label }); } catch (error) { progress.log("cleanup-resource-timeout", { resource: label, summary: error instanceof Error ? error.message : String(error) }); } } 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 /