Files
pikasTech-HWLAB/scripts/dev-cloud-workbench-smoke.test.mjs
T
2026-05-25 20:14:05 +08:00

844 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
classifyCodeAgentBrowserJourney,
classifyLiveDeploymentIdentity,
classifyLiveWebAssetIdentity,
parseSmokeArgs,
runDevCloudWorkbenchLayoutSmoke,
runDevCloudWorkbenchQuickPromptsFixtureSmoke,
runDevCloudWorkbenchSessionContinuityFixtureSmoke,
runDevCloudWorkbenchSlowBlockerFixtureSmoke,
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";
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",
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.mjs", 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, ["显示全部可读事件 / 压缩窗口", "复制 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 #276 two-column wiring long-table contract", () => {
const report = runDevCloudWorkbenchStaticSmoke();
const check = report.checks.find((item) => item.id === "feedback-276-wiring-long-table");
assert.equal(check?.status, "pass");
assert.deepEqual(
check.evidence.filter((item) => ["res_boxsimu_1", "res_boxsimu_2", "DO1 对 DI1"].includes(item)),
["res_boxsimu_1", "res_boxsimu_2", "DO1 对 DI1"]
);
assert.equal(check.evidence.includes("legacy 8-column table forbidden"), true);
assert.equal(check.evidence.includes("trace/evidence header forbidden"), true);
assert.equal(check.evidence.includes("状态/证据/trace kept in summary/detail"), 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("source/default smoke covers Code Agent quick prompt fill-only contract", () => {
const report = runDevCloudWorkbenchStaticSmoke();
const check = report.checks.find((item) => item.id === "code-agent-quick-prompts-contract");
assert.equal(check?.status, "pass");
assert.equal(check.evidence.includes("CodexDO1=true 后读 DI1"), true);
assert.equal(check.evidence.includes("CodexDO1=false 复核 DI1"), true);
assert.equal(check.evidence.some((item) => /我点击发送后才确认执行这次写入请求/u.test(item)), true);
});
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");
});
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.mjs", status: "match" }
]);
assert.equal(pass.status, "pass");
const stale = classifyLiveWebAssetIdentity([
{ path: "index.html", status: "match" },
{ path: "styles.css", status: "mismatch" },
{ path: "app.mjs", status: "mismatch" }
]);
assert.equal(stale.status, "blocked");
assert.deepEqual(stale.mismatches, ["styles.css", "app.mjs"]);
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 accepts only completed HTTP success plus completed UI and real provider evidence", () => {
const summary = sanitizeAgentChatBody({
status: "completed",
provider: "codex-stdio",
model: "gpt-5.5",
backend: "hwlab-cloud-api/codex-mcp-stdio",
traceId: "trc_completed",
reply: {
content: "ok"
},
longLivedSessionGate: {
status: "pass",
pass: true
}
}, { httpStatus: 200 });
const pass = classifyCodeAgentBrowserJourney({
responseSummary: summary,
httpOk: true,
httpStatus: 200,
ui: {
agentChatStatus: "DEV-LIVE 回复",
completedMessageVisible: true,
failedMessageVisible: false
}
});
assert.equal(pass.status, "pass");
const blocked = classifyCodeAgentBrowserJourney({
responseSummary: summary,
httpOk: false,
httpStatus: 500,
ui: {
agentChatStatus: "DEV-LIVE 回复",
completedMessageVisible: true,
failedMessageVisible: false
}
});
assert.equal(blocked.status, "blocked");
assert.equal(blocked.blocker, "provider-upstream");
});
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 slow structured blocker fixture preserves trace, pending state, and retry input", async () => {
const report = await runDevCloudWorkbenchSlowBlockerFixtureSmoke({
responseDelayMs: 5200
});
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 blockerCheck = report.checks.find((check) => check.id === "local-agent-fixture-slow-structured-blocker");
assert.equal(blockerCheck?.status, "pass");
assert.equal(blockerCheck.observations.ui.agentChatStatus, "Runner 受阻");
assert.equal(blockerCheck.observations.ui.failedMessageVisible, true);
assert.equal(blockerCheck.observations.ui.failedMessageHasTrace, true);
assert.equal(blockerCheck.observations.ui.retryInputPreserved, true);
assert.equal(blockerCheck.observations.ui.retryInputValue, "列出你能使用的所有skill");
assert.equal(blockerCheck.observations.prompt.legacyWindow.permanentFailureAround4500ms, false);
assert.equal(blockerCheck.observations.prompt.legacyWindow.pendingChineseVisible, true);
assert.equal(blockerCheck.observations.prompt.response.provider, "codex-stdio");
assert.equal(blockerCheck.observations.prompt.response.traceId, blockerCheck.observations.prompt.traceId);
assert.equal(blockerCheck.observations.prompt.response.error.code, "skills_unavailable");
assert.equal(blockerCheck.observations.prompt.response.skills.status, "blocked");
assert.equal(blockerCheck.observations.prompt.classification.blocker, "runner-blocked");
});
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 M3 IO/u);
assert.equal(report.checks.find((check) => check.id === "layout-issue-287-future-hardware-status-tabs")?.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.wiring.longTableOk, true);
assert.equal(desktopDefault.wiring.noHorizontalScroll, true);
assert.equal(desktopDefault.wiring.headers.length, 2);
assert.deepEqual(desktopDefault.wiring.devicePair, ["res_boxsimu_1", "res_boxsimu_2"]);
assert.equal(desktopDefault.wiring.ioPairs.some((row) => row.join("|") === "DO1|DI1"), true);
assert.deepEqual(desktopDefault.wiring.legacyHeaderHits, []);
assert.deepEqual(desktopDefault.wiring.metadataColumnHits, []);
assert.equal(desktopDefault.wiring.metadataInSummary, true);
assert.equal(desktopDefault.wiring.metadataInDetails, true);
assert.equal(desktopDefault.wiring.horizontalScroll.panelScrollWidth <= desktopDefault.wiring.horizontalScroll.panelClientWidth + 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["#m3-control-form"], "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.wiring.longTableOk, true);
assert.equal(mobileDefault.wiring.noHorizontalScroll, true);
assert.deepEqual(mobileDefault.wiring.devicePair, ["res_boxsimu_1", "res_boxsimu_2"]);
assert.equal(mobileDefault.wiring.ioPairs.some((row) => row.join("|") === "DO1|DI1"), true);
assert.deepEqual(mobileDefault.wiring.legacyHeaderHits, []);
assert.deepEqual(mobileDefault.wiring.metadataColumnHits, []);
assert.equal(mobileDefault.wiring.metadataInSummary, true);
assert.equal(mobileDefault.wiring.metadataInDetails, 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, 21);
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", () => {
assert.equal(rootPackage.scripts["web:check"], "node web/hwlab-cloud-web/scripts/check.mjs");
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.match(rootPackage.scripts.check, /node --check scripts\/dev-cloud-workbench-layout-smoke\.mjs/u);
assert.match(rootPackage.scripts.check, /node web\/hwlab-cloud-web\/scripts\/check\.mjs/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, /runDevCloudWorkbenchLayoutSmoke/u);
assert.match(cloudWebCheckSource, /tmp\/dev-cloud-workbench-layout-web-check\.json/u);
assert.match(cloudWebCheckSource, /layout-feedback-352-resource-explorer-removed/u);
assert.match(cloudWebCheckSource, /layout-feedback-437-live-build-overlay/u);
assert.match(cloudWebCheckSource, /compactLayoutSmokeFailures/u);
});