import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { classifyCodeAgentBrowserJourney, classifyLiveDeploymentIdentity, classifyLiveWebAssetIdentity, parseSmokeArgs, runDevCloudWorkbenchLayoutSmoke, runDevCloudWorkbenchQuickPromptsFixtureSmoke, runDevCloudWorkbenchSessionContinuityFixtureSmoke, runDevCloudWorkbenchStaticSmoke, runDevCloudWorkbenchTimeoutFixtureSmoke, sanitizeAgentChatBody } from "./src/dev-cloud-workbench-smoke-lib.mjs"; import { classifyCodeAgentBrowserFailure, classifyCodeAgentChatReadiness } from "./src/code-agent-response-contract.mjs"; import { decideSmokeReportWrite, smokeCliExitCode } from "./dev-cloud-workbench-smoke.mjs"; import { compactLayoutSmokeCliOutput, parseLayoutSmokeArgs } from "./dev-cloud-workbench-layout-smoke.mjs"; import { checkProfiles } from "./src/check-plan.mjs"; const sourceIdentity = Object.freeze({ status: "observed", commitId: "ff4b3f928f76a0b46cd7998a2a865f3e1caf02c8", shortCommitId: "ff4b3f928f76", reportCommitId: "ff4b3f928f76", worktreeState: "clean", dirty: false }); const expectedRuntimeIdentity = Object.freeze({ status: "observed", serviceId: "hwlab-cloud-api", source: "deploy/artifact-catalog.dev.json+deploy/deploy.json:runtime-config", commitId: "7de6edd", imageTag: "7de6edd", image: "127.0.0.1:5000/hwlab/hwlab-cloud-api:7de6edd" }); const rootPackage = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); const cloudWebPackage = JSON.parse(readFileSync(new URL("../web/hwlab-cloud-web/package.json", import.meta.url), "utf8")); const cloudWebCheckSource = readFileSync(new URL("../web/hwlab-cloud-web/scripts/check.ts", import.meta.url), "utf8"); test("workbench smoke defaults to SOURCE mode and requires live confirmation before DEV-LIVE provider calls", () => { const defaultArgs = parseSmokeArgs([]); assert.equal(defaultArgs.mode, "source"); assert.equal(defaultArgs.url, "http://74.48.78.17:16666/"); assert.equal(defaultArgs.confirmDevLive, false); const sourceArgs = parseSmokeArgs(["--source"]); assert.equal(sourceArgs.mode, "source"); const staticAliasArgs = parseSmokeArgs(["--static"]); assert.equal(staticAliasArgs.mode, "source"); const liveArgs = parseSmokeArgs(["--live", "--confirm-dev-live"]); assert.equal(liveArgs.mode, "live"); assert.equal(liveArgs.confirmDevLive, true); assert.equal(liveArgs.url, "http://74.48.78.17:16666/"); assert.throws( () => parseSmokeArgs(["--live"]), /requires explicit --live --confirm-dev-live/u ); }); test("source/default workbench report cannot claim DEV-LIVE and documents the confirmed live command", () => { const report = runDevCloudWorkbenchStaticSmoke(); assert.equal(report.mode, "source"); assert.equal(report.evidenceLevel, "SOURCE"); assert.equal(report.devLive, false); assert.equal(report.safety.devLive, false); assert.equal(report.validationCommands.includes("node scripts/dev-cloud-workbench-smoke.mjs --source"), true); assert.equal( report.validationCommands.includes("node scripts/dev-cloud-workbench-smoke.mjs --live --confirm-dev-live --url http://74.48.78.17:16666/ --report /tmp/hwlab-dev-gate/dev-cloud-workbench-live.json"), true ); assert.equal( report.validationCommands.includes("node scripts/dev-cloud-workbench-smoke.mjs --live --url http://74.48.78.17:16666/ --report /tmp/hwlab-dev-gate/dev-cloud-workbench-live.json"), false ); assert.equal(report.checks.find((check) => check.id === "code-agent-long-timeout-contract")?.status, "pass"); const traceDisclosure = report.checks.find((check) => check.id === "code-agent-trace-replay-disclosure"); assert.equal(traceDisclosure?.status, "pass"); assert.deepEqual(traceDisclosure?.evidence, ["显示全部可读事件 / 完整 trace 回放中", "复制 JSON", "下载 trace", "traceDetailsOpen", "traceScrollPositions", "internal scroll for full trace"]); }); test("Code Agent browser failure classifier emits Chinese timeout/provider/browser categories with traceId", () => { const timeout = classifyCodeAgentBrowserFailure(new Error("request timed out after 180000ms"), { traceId: "trc_timeout" }); assert.equal(timeout.category, "timeout"); assert.match(timeout.chineseSummary, /超时/u); assert.match(timeout.chineseSummary, /trc_timeout/u); const provider = classifyCodeAgentBrowserFailure(new Error("OpenAI provider returned HTTP 502"), { traceId: "trc_provider" }); assert.equal(provider.category, "provider"); assert.match(provider.chineseSummary, /Provider 故障/u); assert.match(provider.chineseSummary, /trc_provider/u); const runnerBusy = classifyCodeAgentBrowserFailure(new Error("session_busy: runner already busy"), { traceId: "trc_busy" }); assert.equal(runnerBusy.category, "runner_busy"); assert.equal(runnerBusy.blocker, "runner-busy"); assert.match(runnerBusy.chineseSummary, /Runner 忙碌/u); assert.match(runnerBusy.chineseSummary, /trc_busy/u); const sessionBlocked = classifyCodeAgentBrowserFailure(new Error("session_expired: session blocked"), { traceId: "trc_session" }); assert.equal(sessionBlocked.category, "session_blocked"); assert.equal(sessionBlocked.blocker, "session-blocked"); assert.match(sessionBlocked.chineseSummary, /Session 受阻/u); assert.match(sessionBlocked.chineseSummary, /trc_session/u); const apiError = classifyCodeAgentBrowserFailure(new Error("API HTTP 500 request_failed"), { traceId: "trc_api" }); assert.equal(apiError.category, "api_error"); assert.equal(apiError.blocker, "api-error"); assert.match(apiError.chineseSummary, /API 错误/u); assert.match(apiError.chineseSummary, /trc_api/u); const browser = classifyCodeAgentBrowserFailure(new Error("locator #command-input not found"), { traceId: "trc_browser" }); assert.equal(browser.category, "browser"); assert.match(browser.chineseSummary, /浏览器故障/u); assert.match(browser.chineseSummary, /trc_browser/u); }); test("workbench smoke CLI treats skipped browser evidence as blocked and refuses to write reports", () => { const report = { status: "skip", mode: "static-mobile-browser", summary: "Mobile browser smoke skipped because Playwright is unavailable." }; const decision = decideSmokeReportWrite( { reportPath: "/tmp/hwlab-dev-gate/dev-cloud-workbench-mobile.json" }, report, { repoRoot: "/repo", cwd: "/repo" } ); assert.equal(decision.status, "blocked"); assert.equal(decision.write, false); assert.match(decision.summary, /skipped and does not contain required browser evidence/u); assert.equal(smokeCliExitCode(report, decision), 2); }); test("workbench smoke CLI refuses to overwrite live report with non-live fixture evidence", () => { const report = { status: "pass", mode: "local-agent-fixture-browser" }; const decision = decideSmokeReportWrite( { reportPath: "/tmp/hwlab-dev-gate/dev-cloud-workbench-live.json" }, report, { repoRoot: "/repo", cwd: "/repo" } ); assert.equal(decision.status, "blocked"); assert.equal(decision.write, false); assert.match(decision.summary, /Refusing to overwrite \/tmp\/hwlab-dev-gate\/dev-cloud-workbench-live\.json/u); assert.equal(smokeCliExitCode(report, decision), 2); }); test("workbench smoke CLI allows live evidence to write the live report", () => { const report = { status: "pass", mode: "live" }; const decision = decideSmokeReportWrite( { reportPath: "/tmp/hwlab-dev-gate/dev-cloud-workbench-live.json" }, report, { repoRoot: "/repo", cwd: "/repo" } ); assert.equal(decision.status, "pass"); assert.equal(decision.write, true); assert.equal(decision.reportPath, "/tmp/hwlab-dev-gate/dev-cloud-workbench-live.json"); assert.equal(smokeCliExitCode(report, decision), 0); }); test("smoke args include a DOM-only live read-only mode", () => { const args = parseSmokeArgs(["--dom-only", "--url", "http://74.48.78.17:16666/"]); assert.equal(args.confirmDevLive, false); assert.equal(args.mode, "dom-only"); assert.equal(args.url, "http://74.48.78.17:16666/"); assert.equal(args.urlExplicit, true); }); test("smoke args include reusable layout-only browser mode", () => { const local = parseSmokeArgs(["--layout"]); assert.equal(local.mode, "layout"); assert.equal(local.urlExplicit, undefined); const build = parseSmokeArgs(["--layout", "--build"]); assert.equal(build.mode, "layout"); assert.equal(build.build, true); const live = parseSmokeArgs(["--layout", "--url", "http://74.48.78.17:16666/"]); assert.equal(live.mode, "layout"); assert.equal(live.url, "http://74.48.78.17:16666/"); assert.equal(live.urlExplicit, true); }); test("dedicated layout smoke CLI supports static build and DEV live shorthand", () => { const local = parseLayoutSmokeArgs(["--static"]); assert.equal(local.mode, "layout"); assert.equal(local.urlExplicit, undefined); const build = parseLayoutSmokeArgs(["--build", "--report", "/tmp/hwlab-dev-gate/dev-cloud-workbench-layout.json"]); assert.equal(build.mode, "layout"); assert.equal(build.build, true); assert.equal(build.reportPath, "/tmp/hwlab-dev-gate/dev-cloud-workbench-layout.json"); const liveDefault = parseLayoutSmokeArgs(["--live"]); assert.equal(liveDefault.mode, "layout"); assert.equal(liveDefault.url, "http://74.48.78.17:16666/"); assert.equal(liveDefault.urlExplicit, true); const liveExplicit = parseLayoutSmokeArgs(["--live", "--url", "http://74.48.78.17:16666/"]); assert.equal(liveExplicit.mode, "layout"); assert.equal(liveExplicit.url, "http://74.48.78.17:16666/"); assert.equal(liveExplicit.urlExplicit, true); }); test("source/default smoke covers #352 resource explorer removal contract", () => { const report = runDevCloudWorkbenchStaticSmoke(); const check = report.checks.find((item) => item.id === "feedback-352-resource-explorer-removed"); assert.equal(check?.status, "pass"); assert.equal(check.evidence.includes("aside#resource-explorer absent"), true); assert.equal(check.evidence.includes("#explorer-resize absent"), true); }); test("source/default smoke covers Device Pod right-sidebar contract", () => { const report = runDevCloudWorkbenchStaticSmoke(); const summary = report.checks.find((item) => item.id === "device-pod-summary-sidebar"); const events = report.checks.find((item) => item.id === "device-pod-event-stream"); const service = report.checks.find((item) => item.id === "device-pod-fake-service"); assert.equal(summary?.status, "pass"); assert.equal(events?.status, "pass"); assert.equal(service?.status, "pass"); assert.equal(summary.evidence.includes("summary-tile"), true); assert.equal(summary.evidence.includes("device-detail-dialog"), true); assert.equal(events.evidence.includes("device-event-text"), true); }); test("source/default smoke covers #288 gate single-table contract", () => { const report = runDevCloudWorkbenchStaticSmoke(); const check = report.checks.find((item) => item.id === "feedback-288-gate-single-table"); assert.equal(check?.status, "pass"); assert.equal(report.checks.some((item) => item.id === "feedback-119-gate-retains-diagnostics"), false); }); test("live workbench identity passes only when runtime commit or image tag matches current source", () => { assert.equal( classifyLiveDeploymentIdentity(sourceIdentity, { status: "observed", commitId: "7de6edd2c41f", imageTag: "c7de474" }, expectedRuntimeIdentity).status, "pass" ); assert.equal( classifyLiveDeploymentIdentity(sourceIdentity, { status: "observed", commitId: "unknown", imageTag: "7de6edd" }, expectedRuntimeIdentity).status, "pass" ); const stale = classifyLiveDeploymentIdentity(sourceIdentity, { status: "observed", commitId: "c7de474", imageTag: "c7de474" }, expectedRuntimeIdentity); assert.equal(stale.status, "blocked"); assert.match(stale.summary, /Deployment drift/u); assert.match(stale.reason, /does not match current source desired runtime identity/u); }); test("live workbench identity blocks when source identity is dirty or runtime identity is absent", () => { const dirty = classifyLiveDeploymentIdentity( { ...sourceIdentity, worktreeState: "dirty", dirty: true, reportCommitId: "unknown" }, { status: "observed", commitId: "7de6edd", imageTag: "7de6edd" }, expectedRuntimeIdentity ); assert.equal(dirty.status, "blocked"); assert.match(dirty.reason, /not cleanly attributable/u); const missingRuntime = classifyLiveDeploymentIdentity(sourceIdentity, { status: "not_observed", commitId: "not_observed", imageTag: "not_observed" }, expectedRuntimeIdentity); assert.equal(missingRuntime.status, "blocked"); assert.match(missingRuntime.reason, /not observed/u); }); test("live workbench identity uses deploy desired runtime identity instead of git HEAD", () => { const deployed = classifyLiveDeploymentIdentity(sourceIdentity, { status: "observed", commitId: "7de6edd", imageTag: "7de6edd" }, expectedRuntimeIdentity); assert.equal(deployed.status, "pass"); assert.equal(deployed.expectedCommit, "7de6edd"); assert.equal(deployed.expectedReportCommitId, "ff4b3f928f76"); assert.equal(deployed.expectedSource, "deploy/artifact-catalog.dev.json+deploy/deploy.json:runtime-config"); }); test("live web asset identity blocks stale deployed primary assets", () => { const pass = classifyLiveWebAssetIdentity([ { path: "index.html", status: "match" }, { path: "styles.css", status: "match" }, { path: "app.js", status: "match" } ]); assert.equal(pass.status, "pass"); const stale = classifyLiveWebAssetIdentity([ { path: "index.html", status: "match" }, { path: "styles.css", status: "mismatch" }, { path: "app.js", status: "mismatch" } ]); assert.equal(stale.status, "blocked"); assert.deepEqual(stale.mismatches, ["styles.css", "app.js"]); assert.match(stale.summary, /Deployment drift/u); }); test("Code Agent browser classifier blocks provider 502 response shapes", () => { const summary = sanitizeAgentChatBody({ status: "completed", provider: "openai-responses", model: "gpt-5.5", backend: "hwlab-cloud-api/openai-responses", traceId: "trc_provider_502", reply: { content: "must not count" }, error: { code: "provider_unavailable", message: "OpenAI Responses returned HTTP 502: request rejected", providerStatus: 502, missingEnv: [] } }, { httpStatus: 502 }); assert.equal(summary.status, "failed"); assert.equal(summary.hasReply, false); assert.equal(summary.error.code, "provider_unavailable"); assert.equal(summary.error.providerStatus, 502); const classification = classifyCodeAgentBrowserJourney({ responseSummary: summary, httpOk: false, httpStatus: 502, ui: { agentChatStatus: "DEV-LIVE 回复", completedMessageVisible: true, failedMessageVisible: false } }); assert.equal(classification.status, "blocked"); assert.equal(classification.blocker, "provider-upstream"); }); test("Code Agent browser classifier treats sanitized 服务受阻 UI as blocked provider evidence", () => { const summary = sanitizeAgentChatBody({ status: "failed", provider: "openai-responses", model: "gpt-5.5", backend: "hwlab-cloud-api/openai-responses", traceId: "trc_provider_502", error: { code: "provider_unavailable", message: "OpenAI Responses returned HTTP 502: request rejected", providerStatus: 502, missingEnv: [] } }, { httpStatus: 200 }); const classification = classifyCodeAgentBrowserJourney({ responseSummary: summary, httpOk: true, httpStatus: 200, ui: { agentChatStatus: "服务受阻", completedMessageVisible: false, failedMessageVisible: true } }); assert.equal(classification.status, "blocked"); assert.equal(classification.blocker, "provider-upstream"); }); test("Code Agent browser classifier distinguishes runner busy, session blocked, and API error payloads", () => { const base = { status: "failed", provider: "codex-readonly-runner", model: "read-only-tools", backend: "hwlab-cloud-api/codex-readonly-runner", traceId: "trc_runtime_block" }; const runnerBusy = classifyCodeAgentBrowserJourney({ responseSummary: sanitizeAgentChatBody({ ...base, error: { code: "session_busy", message: "Code Agent runner session is already busy" } }, { httpStatus: 200 }), httpOk: true, httpStatus: 200, ui: { agentChatStatus: "服务受阻", completedMessageVisible: false, failedMessageVisible: true } }); assert.equal(runnerBusy.blocker, "runner-busy"); assert.equal(runnerBusy.category, "runner_busy"); const sessionBlocked = classifyCodeAgentBrowserJourney({ responseSummary: sanitizeAgentChatBody({ ...base, error: { code: "session_expired", message: "Code Agent runner session expired" } }, { httpStatus: 200 }), httpOk: true, httpStatus: 200, ui: { agentChatStatus: "服务受阻", completedMessageVisible: false, failedMessageVisible: true } }); assert.equal(sessionBlocked.blocker, "session-blocked"); assert.equal(sessionBlocked.category, "session_blocked"); const codexRuntimeBlocked = classifyCodeAgentBrowserJourney({ responseSummary: sanitizeAgentChatBody({ ...base, error: { code: "codex_stdio_failed", message: "Codex app-server turn finished with status=failed" } }, { httpStatus: 200 }), httpOk: true, httpStatus: 200, ui: { agentChatStatus: "Runner 受阻", completedMessageVisible: false, failedMessageVisible: true } }); assert.equal(codexRuntimeBlocked.blocker, "runner-blocked"); assert.equal(codexRuntimeBlocked.category, "runner_blocked"); const apiError = classifyCodeAgentBrowserJourney({ responseSummary: sanitizeAgentChatBody({ status: "failed", traceId: "trc_proxy_timeout", error: { code: "cloud_api_proxy_failed", message: "cloud web proxy failed" } }, { httpStatus: 502 }), httpOk: false, httpStatus: 502, ui: { agentChatStatus: "后端失败", completedMessageVisible: false, failedMessageVisible: true } }); assert.equal(apiError.blocker, "api-error"); assert.equal(apiError.category, "api_error"); }); test("Code Agent readiness classifier blocks completed payloads over any non-2xx HTTP status", () => { const completedPayload = { conversationId: "cnv_completed_non_2xx", sessionId: "cnv_completed_non_2xx", messageId: "msg_completed_non_2xx", status: "completed", provider: "openai-responses", model: "gpt-5.5", backend: "hwlab-cloud-api/openai-responses", traceId: "trc_completed_non_2xx", reply: { content: "must only count over HTTP 2xx" } }; const accepted = classifyCodeAgentChatReadiness(completedPayload, { realDevLive: true, httpStatus: 200 }); assert.equal(accepted.status, "blocked"); assert.equal(accepted.devLiveReplyPass, false); assert.equal(accepted.blocker, "untrusted-completion"); for (const httpStatus of [400, 401, 403, 429, 500, 502, 503, 504]) { const blocked = classifyCodeAgentChatReadiness(completedPayload, { realDevLive: true, httpStatus }); assert.equal(blocked.status, "blocked", `HTTP ${httpStatus} must block`); assert.equal(blocked.blocker, "provider-upstream", `HTTP ${httpStatus} blocker`); assert.equal(blocked.devLiveReplyPass, false, `HTTP ${httpStatus} must not pass DEV-LIVE`); assert.equal(blocked.providerStatus, httpStatus, `HTTP ${httpStatus} providerStatus`); assert.match(blocked.reason, /HTTP non-2xx|upstream response/u, `HTTP ${httpStatus} reason`); } }); test("Code Agent browser classifier blocks completed payloads without backend evidence", () => { const summary = sanitizeAgentChatBody({ status: "completed", provider: "openai-responses", model: "gpt-5.5", traceId: "trc_missing_backend", reply: { content: "must not count" } }, { httpStatus: 200 }); const classification = classifyCodeAgentBrowserJourney({ responseSummary: summary, httpOk: true, httpStatus: 200, ui: { agentChatStatus: "DEV-LIVE 回复", completedMessageVisible: true, failedMessageVisible: false } }); assert.equal(classification.status, "blocked"); assert.equal(classification.blocker, "untrusted-completion"); }); test("local Code Agent timeout fixture keeps bounded timeout state, trace context, and retry input", async () => { const report = await runDevCloudWorkbenchTimeoutFixtureSmoke({ responseDelayMs: 120, timeoutConfigMs: 50 }); if (report.status === "skip") { assert.match(report.summary, /Playwright is unavailable/u); return; } assert.equal(report.status, "pass", JSON.stringify(report.blockers, null, 2)); assert.equal(report.evidenceLevel, "SOURCE"); assert.equal(report.devLive, false); const timeoutCheck = report.checks.find((check) => check.id === "local-agent-timeout-fixture-failed-state"); assert.equal(timeoutCheck?.status, "pass"); assert.equal(timeoutCheck.observations.ui.agentChatStatus, "等待超时"); assert.equal(timeoutCheck.observations.ui.traceHasTraceId, true); assert.equal(timeoutCheck.observations.ui.retryInputPreserved, true); assert.equal(timeoutCheck.observations.ui.completedMessageVisible, false); }); test("local session continuity fixture reuses Code Agent context and retries with degraded-copy guard", async () => { const report = await runDevCloudWorkbenchSessionContinuityFixtureSmoke(); if (report.status === "skip") { assert.match(report.summary, /Playwright is unavailable/u); return; } assert.equal(report.status, "pass", JSON.stringify(report.blockers, null, 2)); assert.equal(report.evidenceLevel, "SOURCE"); assert.equal(report.devLive, false); assert.equal(report.checks.find((check) => check.id === "local-agent-session-continuity-two-turns")?.status, "pass"); assert.equal(report.checks.find((check) => check.id === "local-agent-session-continuity-retry")?.status, "pass"); assert.equal(report.checks.find((check) => check.id === "local-agent-session-continuity-degraded-copy")?.status, "pass"); const continuity = report.checks.find((check) => check.id === "local-agent-session-continuity-two-turns")?.observations; assert.equal(continuity.secondRequest.conversationId, continuity.firstRequest.conversationId); assert.equal(continuity.secondRequest.sessionId, "ses_source_fixture_continuity"); assert.equal(continuity.secondRequest.threadId, "thread_source_fixture_continuity"); const retry = report.checks.find((check) => check.id === "local-agent-session-continuity-retry")?.observations; assert.equal(retry.retryRequest.message, retry.failedRequest.message); assert.equal(retry.retryRequest.sessionId, retry.failedRequest.sessionId); assert.equal(retry.retryRequest.threadId, retry.failedRequest.threadId); }); test("Code Agent quick prompt fixture fills input, does not autosend writes, and fits mobile", async () => { const report = await runDevCloudWorkbenchQuickPromptsFixtureSmoke(); if (report.status === "skip") { assert.match(report.summary, /Playwright is unavailable/u); return; } assert.equal(report.status, "pass", JSON.stringify(report.blockers, null, 2)); assert.equal(report.evidenceLevel, "SOURCE"); assert.equal(report.devLive, false); assert.equal(report.safety.codeAgentPostSentByQuickPrompt, false); assert.equal(report.safety.hardwareWriteApis, false); const fill = report.checks.find((check) => check.id === "quick-prompts-fill-input"); const noAutosend = report.checks.find((check) => check.id === "quick-prompts-write-no-autosend"); const copy = report.checks.find((check) => check.id === "quick-prompts-copy-boundary"); const layout = report.checks.find((check) => check.id === "quick-prompts-mobile-layout"); assert.equal(fill?.status, "pass"); assert.equal(noAutosend?.status, "pass"); assert.equal(copy?.status, "pass"); assert.equal(layout?.status, "pass"); assert.equal(noAutosend.observations.every((item) => item.agentPostCountDelta === 0), true); assert.equal(noAutosend.observations.every((item) => item.explicitSendRequired === true), true); assert.equal(copy.observations.every((item) => item.copyBoundaryOk === true), true); assert.equal(layout.observations.some((item) => item.viewport.width === 390 && item.layoutOk), true); }); test("layout smoke verifies desktop and mobile default workbench geometry without resource explorer", async () => { const report = await runDevCloudWorkbenchLayoutSmoke(); if (report.status === "skip") { assert.match(report.summary, /Playwright is unavailable/u); return; } assert.equal(report.status, "pass", JSON.stringify(report.blockers, null, 2)); assert.equal(report.issue, "pikasTech/HWLAB#273"); assert.equal(report.taskId, "dev-cloud-workbench-layout"); assert.equal(report.acceptanceLevel, "dev_cloud_workbench_layout"); assert.equal(report.sourceMode, "source-static"); assert.equal(report.evidenceLevel, "SOURCE"); assert.equal(report.devLive, false); assert.equal(report.devPreconditions.status, "not_applicable"); assert.equal(report.localSmoke.commands.includes("npm run web:check"), true); for (const command of [ "node --check scripts/dev-cloud-workbench-layout-smoke.mjs", "npm run web:layout", "npm run web:layout:build", "npm run web:layout:live" ]) { assert.equal(report.validationCommands.includes(command), true, `missing ${command}`); } for (const id of [ "layout-desktop-default", "layout-narrow-desktop-default", "layout-mobile-default", "layout-feedback-352-resource-explorer-removed", "layout-feedback-437-live-build-overlay", "layout-left-sidebar-collapse", "layout-gate-desktop", "layout-gate-narrow-desktop", "layout-gate-mobile" ]) { assert.equal(report.checks.find((check) => check.id === id)?.status, "pass", id); } assert.deepEqual( report.viewports.map((viewport) => `${viewport.width}x${viewport.height}`), ["1366x768", "1024x768", "390x844"] ); assert.equal(Array.isArray(report.failures), true); assert.equal(Array.isArray(report.skipped), true); assert.equal(report.artifacts.reportPath, null); assert.match(report.reportLifecycle.summary, /cannot be used as DEV-LIVE evidence/u); assert.match(report.devPreconditions.summary, /not required for SOURCE\/static gates/u); assert.equal(report.safety.hitTestMethod.includes("elementsFromPoint"), true); assert.match(report.safety.statement, /does not send Code Agent chat, call Device Pod or hardware write APIs/u); assert.equal(report.checks.find((check) => check.id === "layout-device-pod-summary-sidebar")?.status, "pass"); assert.equal(report.checks.find((check) => check.id === "layout-device-pod-event-stream")?.status, "pass"); assert.equal(report.checks.find((check) => check.id === "layout-device-pod-detail-dialog")?.status, "pass"); assert.equal(report.checks.find((check) => check.id === "layout-feedback-352-resource-explorer-removed")?.status, "pass"); assert.equal(report.checks.find((check) => check.id === "layout-feedback-437-live-build-overlay")?.status, "pass"); assert.equal(report.checks.find((check) => check.id === "layout-issue-288-future-single-table-gate")?.status, "skip"); const desktopDefault = report.checks.find((check) => check.id === "layout-desktop-default")?.observations; const resourceExplorerRemoval = report.checks.find((check) => check.id === "layout-feedback-352-resource-explorer-removed")?.observations; assert.equal(resourceExplorerRemoval.forbiddenSelectors.includes("#explorer-resize"), true); assert.equal(resourceExplorerRemoval.forbiddenCopy.includes("常用能力"), true); assert.equal(resourceExplorerRemoval.coverage.desktop.covered, true); assert.equal(resourceExplorerRemoval.coverage.mobile.removedVisibleCopyAbsent, true); assert.equal(desktopDefault.removedSelectorsAbsent, true); assert.equal(desktopDefault.removedVisibleCopyAbsent, true); assert.equal(desktopDefault.resourceExplorerRemovalGuard, true); assert.equal(desktopDefault.keyTargetsReachable, true); assert.equal(desktopDefault.devicePod.summaryOk, true); assert.equal(desktopDefault.devicePod.noInternalDisclosure, true); assert.equal(desktopDefault.devicePod.noLegacySelectors, true); assert.equal(desktopDefault.devicePod.detailDialogOk, true); assert.equal(desktopDefault.devicePod.eventStreamOk, true); assert.equal(desktopDefault.devicePod.summaryTileCount >= 2, true); assert.equal(desktopDefault.noHorizontalOverflow.right, true); assert.equal(desktopDefault.liveBuildLayout.overlayPositioned, true); assert.equal(desktopDefault.liveBuildLayout.dialogVisible, true); assert.equal(desktopDefault.liveBuildLayout.dialogViewportContained, true); assert.equal(desktopDefault.liveBuildLayout.stableGeometry, true); assert.equal(desktopDefault.liveBuildLayout.closedByButton, true); assert.equal(Object.hasOwn(desktopDefault.boxes.shell, "text"), false); assert.equal(Object.hasOwn(desktopDefault.semanticOverlapChecks[0].boxes[".right-sidebar"], "text"), false); assert.equal(desktopDefault.failures.length, 0); assert.equal(desktopDefault.semanticOverlapChecks.every((check) => !check.overlaps), true); assert.equal(desktopDefault.overflowChecks.every((check) => check.ok), true); const mobileDefault = report.checks.find((check) => check.id === "layout-mobile-default")?.observations; assert.equal(mobileDefault.removedSelectorsAbsent, true); assert.equal(mobileDefault.removedVisibleCopyAbsent, true); assert.equal(mobileDefault.resourceExplorerRemovalGuard, true); assert.equal(mobileDefault.keyTargetsReachable, true); assert.equal(mobileDefault.devicePod.summaryOk, true); assert.equal(mobileDefault.devicePod.noInternalDisclosure, true); assert.equal(mobileDefault.devicePod.noLegacySelectors, true); assert.equal(mobileDefault.devicePod.detailDialogOk, true); assert.equal(mobileDefault.devicePod.eventStreamOk, true); assert.equal(mobileDefault.noHorizontalOverflow.right, true); assert.equal(mobileDefault.liveBuildLayout.overlayPositioned, true); assert.equal(mobileDefault.liveBuildLayout.dialogVisible, true); assert.equal(mobileDefault.liveBuildLayout.dialogViewportContained, true); assert.equal(mobileDefault.liveBuildLayout.stableGeometry, true); const leftCollapse = report.checks.find((check) => check.id === "layout-left-sidebar-collapse")?.observations; assert.equal(leftCollapse.status, "pass"); assert.equal(leftCollapse.coverage.desktop.widthReclaimed, true); assert.equal(leftCollapse.coverage["narrow-desktop"].widthReclaimed, true); assert.equal(leftCollapse.coverage.mobile.widthReclaimed, true); assert.equal(leftCollapse.coverage.desktop.collapsed.keyTargetsReachable, true); assert.equal(leftCollapse.coverage.mobile.collapsed.keyTargetsReachable, true); assert.equal(leftCollapse.coverage.desktop.restoredCollapsed.collapsed, true); assert.equal(leftCollapse.coverage.mobile.expanded.routeButtonsVisible, true); const compact = compactLayoutSmokeCliOutput(report); assert.equal(compact.status, "pass"); assert.equal(compact.artifacts.screenshotCount >= 18, true); assert.equal(compact.failures.length, 0); assert.equal(compact.blockers.length, 0); assert.equal(JSON.stringify(compact).includes('"checks"'), false); assert.equal(JSON.stringify(compact).includes('"boxes"'), false); assert.equal(JSON.stringify(compact).includes('"text"'), false); }); test("repo-owned web checks expose source build and DEV live layout smoke gates", () => { const checkCommands = checkProfiles.check.map((task) => task.command.join(" ")); assert.match(rootPackage.scripts["web:check"], /cd web\/hwlab-cloud-web && bun run check/u); assert.match(rootPackage.scripts["web:check"], /bun test web\/hwlab-cloud-web\/message-markdown\.test\.ts/u); assert.equal( rootPackage.scripts["web:layout"], "node scripts/dev-cloud-workbench-layout-smoke.mjs --static --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout.json" ); assert.equal( rootPackage.scripts["web:layout:build"], "node scripts/dev-cloud-workbench-layout-smoke.mjs --build --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout-build.json" ); assert.equal( rootPackage.scripts["web:layout:live"], "node scripts/dev-cloud-workbench-layout-smoke.mjs --live --url http://74.48.78.17:16666/ --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout-live.json" ); assert.ok(checkCommands.some((command) => command === "node --check scripts/dev-cloud-workbench-layout-smoke.mjs")); assert.ok(checkProfiles.check.some((task) => task.cwd === "web/hwlab-cloud-web" && task.command.join(" ") === "bun run check")); assert.doesNotMatch(checkCommands.join("\n"), /gate-summary\.mjs|export-web-gate-summary/u); assert.equal(cloudWebPackage.scripts.layout.includes("--static"), true); assert.equal(cloudWebPackage.scripts["layout:build"].includes("--build"), true); assert.equal(cloudWebPackage.scripts["layout:live"].includes("--live --url http://74.48.78.17:16666/"), true); assert.match(cloudWebCheckSource, /runJavaScriptSyntaxCheck/u); assert.match(cloudWebCheckSource, /Device Pod summary sidebar/u); assert.match(cloudWebCheckSource, /device-detail-dialog/u); assert.match(cloudWebCheckSource, /device-event-text/u); assert.match(cloudWebCheckSource, /isCodeAgentTimeoutError/u); assert.doesNotMatch(cloudWebCheckSource, /m3-readonly-contract|workbench-hardware-panel-contract|gate-summary\.mjs/u); });