feat: add cloud workbench login entry

This commit is contained in:
Code Queue Review
2026-05-23 18:08:51 +00:00
parent fcfc2b01b0
commit 6861239a47
8 changed files with 1210 additions and 11 deletions
+533 -6
View File
@@ -72,6 +72,7 @@ const readOnlyRpcMethods = Object.freeze([
const requiredWebAssets = Object.freeze([
"index.html",
"styles.css",
"auth.mjs",
"app.mjs",
"code-agent-facts.mjs",
"code-agent-status.mjs",
@@ -86,9 +87,10 @@ const requiredWebAssets = Object.freeze([
"third_party/marked/LICENSE"
]);
const liveIdentityWebAssets = Object.freeze(["index.html", "styles.css", "app.mjs", "code-agent-facts.mjs", "code-agent-status.mjs", "live-status.mjs", "wiring-status.mjs"]);
const liveIdentityWebAssets = Object.freeze(["index.html", "styles.css", "auth.mjs", "app.mjs", "code-agent-facts.mjs", "code-agent-status.mjs", "live-status.mjs", "wiring-status.mjs"]);
const chineseWorkbenchLabels = Object.freeze([
"云工作台登录",
"HWLAB 云工作台",
"硬件资源",
"Agent 对话",
@@ -98,6 +100,11 @@ const chineseWorkbenchLabels = Object.freeze([
"使用说明"
]);
const defaultAuthCredentials = Object.freeze({
username: "admin",
password: "hwlab2026"
});
const defaultHomepageForbiddenTerms = Object.freeze([
"Gate",
"诊断",
@@ -370,6 +377,7 @@ export async function runDevCloudWorkbenchTimeoutFixtureSmoke(options = {}) {
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 === "m3-status-fixture") return runM3StatusFixtureSmoke();
@@ -440,6 +448,8 @@ export function parseSmokeArgs(argv) {
args.mode = "local-agent-fixture";
} else if (arg === "--quick-prompts-fixture") {
args.mode = "quick-prompts-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") {
@@ -476,9 +486,14 @@ function runStaticSmoke() {
evidence: requiredWebAssets.map((file) => `web/hwlab-cloud-web/${file}`)
});
addCheck(checks, blockers, "default-workbench-shell", hasDefaultWorkbench(files), "Default route is the VS Code-style Cloud Workbench.", {
addCheck(checks, blockers, "default-login-entry", hasDefaultLoginEntry(files, artifactPublisher), "Default 16666 route shows the Chinese login page before the Cloud Workbench is exposed.", {
blocker: "runtime_blocker",
evidence: ["workspace view is visible by default", "Gate view is hidden by default", ...workbenchMarkers]
evidence: ["云工作台登录", "账号或密码不正确,请重新输入。", "data-app-shell hidden", "auth.mjs", "HWLAB_CLOUD_WEB_AUTH_USERNAME", "HWLAB_CLOUD_WEB_AUTH_PASSWORD"]
});
addCheck(checks, blockers, "default-workbench-shell", hasDefaultWorkbench(files), "Authenticated route is the VS Code-style Cloud Workbench.", {
blocker: "runtime_blocker",
evidence: ["workspace view exists behind login", "Gate view is hidden by default after auth", ...workbenchMarkers]
});
addCheck(checks, blockers, "secondary-gate-status-help", secondaryViewsAreNotDefault(files), "Gate/status/diagnostics/help surfaces are not accepted as the default homepage.", {
@@ -1657,6 +1672,7 @@ function readStaticFiles() {
return {
html: readText("web/hwlab-cloud-web/index.html"),
styles: readText("web/hwlab-cloud-web/styles.css"),
auth: readText("web/hwlab-cloud-web/auth.mjs"),
app: readText("web/hwlab-cloud-web/app.mjs"),
codeAgentFacts: readText("web/hwlab-cloud-web/code-agent-facts.mjs"),
codeAgentStatus: readText("web/hwlab-cloud-web/code-agent-status.mjs"),
@@ -1902,15 +1918,50 @@ function assetsExist() {
return requiredWebAssets.every((file) => fs.existsSync(path.join(webRoot, file)));
}
function hasDefaultLoginEntry({ html, app, auth, styles }, artifactPublisher = "") {
const loginShellTag = html.match(/<section\b[^>]*\bid=["']login-shell["'][^>]*>/u)?.[0] ?? "";
const appShellTag = html.match(/<main\b[^>]*\bdata-app-shell\b[^>]*>/u)?.[0] ?? "";
const loginHtml = html.match(/<section\b[^>]*\bid=["']login-shell["'][\s\S]*?<\/section>/u)?.[0] ?? "";
const defaultShellText = visibleTextFromHtml(loginHtml);
const authSource = `${auth}\n${app}\n${styles}\n${artifactPublisher}`;
return (
loginShellTag.includes("data-auth-login") &&
!/\bhidden\b/u.test(loginShellTag) &&
/\bhidden\b/u.test(appShellTag) &&
/data-auth-state=["']checking["']/u.test(html) &&
["云工作台登录", "请输入账号和密码。", "用户名", "密码", "登录"].every((term) => defaultShellText.includes(term)) &&
!defaultShellText.includes("Gate") &&
!defaultShellText.includes("诊断") &&
!defaultShellText.includes("验收") &&
!defaultShellText.includes("后厨") &&
/DEFAULT_AUTH_USERNAME\s*=\s*["']admin["']/u.test(auth) &&
/DEFAULT_AUTH_PASSWORD\s*=\s*["']hwlab2026["']/u.test(auth) &&
/HWLAB_CLOUD_WEB_CONFIG\?\.auth/u.test(auth) &&
/AUTH_STORAGE_KEY\s*=\s*["']hwlab\.cloudWorkbench\.auth\.v1["']/u.test(auth) &&
/账号或密码不正确,请重新输入。/u.test(auth) &&
/ensureWorkbenchAuth/u.test(app) &&
/await\s+ensureWorkbenchAuth/u.test(app) &&
/initWorkbenchLogout/u.test(app) &&
/HWLAB_CLOUD_WEB_AUTH_USERNAME/u.test(authSource) &&
/HWLAB_CLOUD_WEB_AUTH_PASSWORD/u.test(authSource) &&
/auth_required/u.test(authSource) &&
/\.login-shell\s*\{[^}]*height:\s*100dvh;[^}]*overflow:\s*hidden;/su.test(styles) &&
/\.login-panel\s*\{[^}]*width:\s*min\(100%,\s*360px\);/su.test(styles) &&
/\.workbench-shell\[hidden\]/u.test(styles)
);
}
function hasDefaultWorkbench({ html, app }) {
const workspaceTag = firstTagForDataView(html, "workspace");
const gateTag = firstTagForDataView(html, "gate");
const appShellTag = html.match(/<main\b[^>]*\bdata-app-shell\b[^>]*>/u)?.[0] ?? "";
return (
labelsPresent(html, workbenchMarkers) &&
/<title>\s*HWLAB 云工作台\s*<\/title>/u.test(html) &&
/data-route=["']workspace["']/u.test(html) &&
workspaceTag !== null &&
!/\bhidden\b/u.test(workspaceTag) &&
/\bhidden\b/u.test(appShellTag) &&
gateTag !== null &&
/\bhidden\b/u.test(gateTag) &&
finalRouteFallback(app) === "workspace"
@@ -2975,6 +3026,9 @@ async function inspectLiveDom(url, options = {}) {
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
const postGuard = options.noCodeAgentPost ? await installNoCodeAgentPostGuard(page) : null;
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 8000 });
const login = await inspectLoginPage(page);
await loginWithDefaultCredentials(page);
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
const dom = await page.evaluate(async (forbiddenTerms) => {
const workspace = document.querySelector('[data-view="workspace"]');
const gate = document.querySelector('[data-view="gate"]');
@@ -3075,6 +3129,9 @@ async function inspectLiveDom(url, options = {}) {
};
}, [...defaultHomepageForbiddenTerms]);
const pass =
login.loginVisible &&
login.workbenchHidden &&
login.copyOk &&
dom.title === "HWLAB 云工作台" &&
dom.bodyOverflow === "hidden" &&
dom.htmlOverflow === "hidden" &&
@@ -3114,6 +3171,8 @@ async function inspectLiveDom(url, options = {}) {
status: pass ? "pass" : "blocked",
summary: pass ? "Browser DOM confirms the default workbench, core controls, and outer scroll lock." : "Browser DOM did not satisfy the workbench contract.",
evidence: [
`loginVisible=${login.loginVisible}`,
`workbenchHiddenBeforeLogin=${login.workbenchHidden}`,
`title=${dom.title}`,
`bodyOverflow=${dom.bodyOverflow}`,
`htmlOverflow=${dom.htmlOverflow}`,
@@ -3128,6 +3187,7 @@ async function inspectLiveDom(url, options = {}) {
...(postGuard ? [`codeAgentPostAttempts=${postGuard.attempts.length}`] : [])
],
observations: {
login,
...dom,
...(postGuard ? { codeAgentPostGuard: postGuard.observation() } : {})
}
@@ -3161,6 +3221,7 @@ async function inspectLiveHelpRoute(url, options = {}) {
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
const postGuard = options.noCodeAgentPost ? await installNoCodeAgentPostGuard(page) : null;
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 12000 });
await loginWithDefaultCredentials(page);
await page.locator('[data-route="help"]').click();
await page.waitForFunction(() => document.querySelector("#help-content")?.dataset.helpState === "ready", null, { timeout: 8000 });
const help = await inspectHelpMarkdownRoute(page);
@@ -3217,6 +3278,7 @@ async function inspectLiveGateRoute(url, options = {}) {
});
const postGuard = options.noCodeAgentPost ? await installNoCodeAgentPostGuard(page) : null;
await page.goto(new URL("/gate", url).toString(), { waitUntil: "domcontentloaded", timeout: 12000 });
await loginWithDefaultCredentials(page);
await page.waitForFunction(() => {
const rows = document.querySelectorAll("#gate-review-body tr");
const source = document.querySelector("#gate-source-status")?.textContent ?? "";
@@ -3323,6 +3385,395 @@ async function inspectLiveGateRoute(url, options = {}) {
}
}
export async function runDevCloudWorkbenchAuthFixtureSmoke() {
let chromium;
try {
({ chromium } = await importPlaywright());
} catch (error) {
const summary = `Auth fixture smoke skipped because Playwright is unavailable: ${error.message}`;
return {
status: "skip",
task: "DC-DCSN-P0-2026-003",
mode: "auth-fixture-browser",
evidenceLevel: "SOURCE",
devLive: false,
summary,
checks: [
{
id: "auth-browser-dependency",
status: "skip",
summary: playwrightDependencySummary,
evidence: [playwrightDependencyCommand, "package.json dependencies.playwright"]
}
],
blockers: [
{
type: "environment_blocker",
scope: "auth-browser-dependency",
status: "open",
summary
}
],
safety: staticSafety()
};
}
const server = await startStaticWebServer({ authFixture: true });
let browser;
try {
browser = await chromium.launch({ headless: true });
const desktop = await inspectAuthFixtureViewport(browser, server.url, {
width: 1366,
height: 768,
isMobile: false
});
const mobile = await inspectAuthFixtureViewport(browser, server.url, {
width: 390,
height: 844,
isMobile: true
});
const checks = [
{
id: "auth-login-success-desktop",
status: desktop.success.pass ? "pass" : "blocked",
viewport: desktop.viewport,
summary: "Desktop login accepts admin/hwlab2026 and enters the current Cloud Workbench.",
observations: desktop.success
},
{
id: "auth-login-failure-desktop",
status: desktop.failure.pass ? "pass" : "blocked",
viewport: desktop.viewport,
summary: "Desktop login failure stays on the Chinese login page with a bounded error message.",
observations: desktop.failure
},
{
id: "auth-refresh-session-desktop",
status: desktop.refresh.pass ? "pass" : "blocked",
viewport: desktop.viewport,
summary: "Desktop refresh preserves the authenticated workbench session.",
observations: desktop.refresh
},
{
id: "auth-logout-expiry-desktop",
status: desktop.logout.pass && desktop.expired.pass ? "pass" : "blocked",
viewport: desktop.viewport,
summary: "Desktop logout and expired session both return to the login page.",
observations: {
logout: desktop.logout,
expired: desktop.expired
}
},
{
id: "auth-login-success-mobile",
status: mobile.success.pass ? "pass" : "blocked",
viewport: mobile.viewport,
summary: "390x844 mobile login enters the workbench without outer scrolling or hidden controls.",
observations: mobile.success
},
{
id: "auth-login-failure-mobile",
status: mobile.failure.pass ? "pass" : "blocked",
viewport: mobile.viewport,
summary: "390x844 mobile login failure keeps the form visible and error text contained.",
observations: mobile.failure
},
{
id: "auth-refresh-session-mobile",
status: mobile.refresh.pass ? "pass" : "blocked",
viewport: mobile.viewport,
summary: "390x844 mobile refresh preserves the authenticated session and workbench layout.",
observations: mobile.refresh
},
{
id: "auth-logout-expiry-mobile",
status: mobile.logout.pass && mobile.expired.pass ? "pass" : "blocked",
viewport: mobile.viewport,
summary: "390x844 mobile logout and expired session return to login without outer scroll.",
observations: {
logout: mobile.logout,
expired: mobile.expired
}
}
];
const blockers = checks
.filter((check) => check.status !== "pass")
.map((check) => ({
type: "runtime_blocker",
scope: check.id,
status: "open",
summary: check.summary,
viewport: check.viewport
}));
return {
status: blockers.length === 0 ? "pass" : "blocked",
task: "DC-DCSN-P0-2026-003",
mode: "auth-fixture-browser",
url: server.url,
generatedAt: new Date().toISOString(),
evidenceLevel: "SOURCE",
devLive: false,
summary: "Local browser fixture validates the Cloud Workbench login entry, failure message, session refresh, logout, expiry, and desktop/mobile layout.",
refs: ["pikasTech/HWLAB#357", "pikasTech/HWLAB#108"],
checks,
blockers,
safety: {
...staticSafety(),
localFixtureOnly: true,
hardwareWriteApis: false,
codeAgentPostSent: false,
statement: "Auth fixture uses local same-origin /auth endpoints only; it does not call hardware write APIs, post Code Agent chat, read Secrets, or deploy DEV."
}
};
} catch (error) {
return {
status: "blocked",
task: "DC-DCSN-P0-2026-003",
mode: "auth-fixture-browser",
url: server.url,
generatedAt: new Date().toISOString(),
evidenceLevel: "SOURCE",
devLive: false,
summary: `Auth fixture smoke failed: ${error.message}`,
checks: [],
blockers: [
{
type: "runtime_blocker",
scope: "auth-fixture-browser",
status: "open",
summary: error.message
}
],
safety: staticSafety()
};
} finally {
if (browser) await browser.close();
await server.close();
}
}
async function inspectAuthFixtureViewport(browser, url, viewport) {
const context = await browser.newContext({
viewport: { width: viewport.width, height: viewport.height },
deviceScaleFactor: 1,
isMobile: viewport.isMobile === true
});
try {
const failurePage = await context.newPage();
await failurePage.goto(url, { waitUntil: "networkidle", timeout: 15000 });
const initial = await inspectLoginPage(failurePage);
await failurePage.locator("#login-username").fill(defaultAuthCredentials.username);
await failurePage.locator("#login-password").fill("wrong-password");
await failurePage.locator("#login-submit").click();
await failurePage.waitForFunction(() => !document.querySelector("#login-error")?.hidden, null, { timeout: 8000 });
const failure = await inspectFailedLoginPage(failurePage, viewport);
await failurePage.close();
const successPage = await context.newPage();
await successPage.goto(url, { waitUntil: "networkidle", timeout: 15000 });
await loginWithDefaultCredentials(successPage);
const success = await inspectAuthenticatedWorkbench(successPage, viewport);
await successPage.reload({ waitUntil: "networkidle", timeout: 15000 });
const refresh = await inspectAuthenticatedWorkbench(successPage, viewport);
await successPage.locator("#logout-button").click();
await successPage.waitForFunction(() => document.querySelector("#login-shell")?.hidden === false, null, { timeout: 8000 });
const logout = await inspectLoginPage(successPage, viewport);
await successPage.close();
const expiredPage = await context.newPage();
await expiredPage.addInitScript(({ username }) => {
window.localStorage.setItem("hwlab.cloudWorkbench.auth.v1", JSON.stringify({
version: 1,
username,
issuedAt: Date.now() - 120000,
expiresAt: Date.now() - 1000
}));
}, { username: defaultAuthCredentials.username });
await expiredPage.goto(url, { waitUntil: "networkidle", timeout: 15000 });
const expired = await inspectLoginPage(expiredPage, viewport);
await expiredPage.close();
return {
viewport: { width: viewport.width, height: viewport.height },
initial,
failure,
success,
refresh,
logout,
expired
};
} finally {
await context.close();
}
}
async function loginWithDefaultCredentials(page) {
const loginVisible = await page.locator("#login-shell").evaluate((element) => element.hidden === false).catch(() => false);
if (!loginVisible) return;
await page.locator("#login-username").fill(defaultAuthCredentials.username);
await page.locator("#login-password").fill(defaultAuthCredentials.password);
await page.locator("#login-submit").click();
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
}
async function inspectLoginPage(page, viewport = null) {
return page.evaluate((viewport) => {
const login = document.querySelector("#login-shell");
const panel = document.querySelector(".login-panel");
const appShell = document.querySelector("[data-app-shell]");
const title = document.querySelector("#login-title")?.textContent?.trim() ?? "";
const text = login?.textContent?.replace(/\s+/gu, " ").trim() ?? "";
const panelBox = panel?.getBoundingClientRect();
const inputBoxes = [...document.querySelectorAll(".login-form input, #login-submit")].map((element) => {
const box = element.getBoundingClientRect();
return {
id: element.id,
width: box.width,
height: box.height,
left: box.left,
right: box.right,
scrollWidth: element.scrollWidth,
clientWidth: element.clientWidth
};
});
const forbiddenTerms = ["Gate", "诊断", "验收", "后厨", "BLOCKED", "SOURCE", "DEV-LIVE", "trace", "Secret"];
const rootScrollLocked =
getComputedStyle(document.documentElement).overflow === "hidden" &&
getComputedStyle(document.body).overflow === "hidden" &&
document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 &&
document.body.scrollHeight <= document.body.clientHeight + 2;
const noHorizontalOverflow =
document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2 &&
document.body.scrollWidth <= document.body.clientWidth + 2 &&
inputBoxes.every((box) => box.scrollWidth <= box.clientWidth + 2);
const panelInViewport = Boolean(panelBox) &&
panelBox.left >= -1 &&
panelBox.right <= window.innerWidth + 1 &&
panelBox.top >= -1 &&
panelBox.bottom <= window.innerHeight + 1;
const copyOk =
title === "云工作台登录" &&
["请输入账号和密码。", "用户名", "密码", "登录"].every((term) => text.includes(term)) &&
forbiddenTerms.every((term) => !text.includes(term));
const loginVisible = Boolean(login && login.hidden === false && panelBox && panelBox.width > 0 && panelBox.height > 0);
const workbenchHidden = appShell ? appShell.hidden === true : false;
return {
viewport: viewport ?? { width: window.innerWidth, height: window.innerHeight },
title,
textSample: text.slice(0, 180),
bodyAuthState: document.body.dataset.authState ?? "",
loginVisible,
workbenchHidden,
copyOk,
rootScrollLocked,
noHorizontalOverflow,
panelInViewport,
inputBoxes,
pass: loginVisible && workbenchHidden && copyOk && rootScrollLocked && noHorizontalOverflow && panelInViewport
};
}, viewport);
}
async function inspectFailedLoginPage(page, viewport) {
return page.evaluate((viewport) => {
const error = document.querySelector("#login-error");
const password = document.querySelector("#login-password");
const login = document.querySelector("#login-shell");
const appShell = document.querySelector("[data-app-shell]");
const errorBox = error?.getBoundingClientRect();
const message = error?.textContent?.replace(/\s+/gu, " ").trim() ?? "";
const internalLeak = /stack|trace|secret|token|HWLAB_CLOUD|Exception|Error:/iu.test(message);
const rootScrollLocked =
getComputedStyle(document.documentElement).overflow === "hidden" &&
getComputedStyle(document.body).overflow === "hidden" &&
document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 &&
document.body.scrollHeight <= document.body.clientHeight + 2;
const errorContained = Boolean(errorBox) &&
errorBox.width > 0 &&
error.scrollWidth <= error.clientWidth + 2 &&
errorBox.left >= 0 &&
errorBox.right <= window.innerWidth + 1;
const pass =
login?.hidden === false &&
appShell?.hidden === true &&
message === "账号或密码不正确,请重新输入。" &&
document.activeElement === password &&
!internalLeak &&
rootScrollLocked &&
errorContained;
return {
viewport,
pass,
message,
internalLeak,
passwordFocused: document.activeElement === password,
loginVisible: login?.hidden === false,
workbenchHidden: appShell?.hidden === true,
rootScrollLocked,
errorContained
};
}, viewport);
}
async function inspectAuthenticatedWorkbench(page, viewport) {
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
return page.evaluate((viewport) => {
const login = document.querySelector("#login-shell");
const appShell = document.querySelector("[data-app-shell]");
const visible = (selector) => {
const element = document.querySelector(selector);
if (!element) return false;
const box = element.getBoundingClientRect();
const style = getComputedStyle(element);
return box.width > 0 && box.height > 0 && style.visibility !== "hidden" && style.display !== "none";
};
window.scrollTo(0, 240);
document.documentElement.scrollTop = 240;
document.body.scrollTop = 240;
const rootAfterScrollAttempt = {
windowScrollY: window.scrollY,
htmlScrollTop: document.documentElement.scrollTop,
bodyScrollTop: document.body.scrollTop
};
const rootScrollLocked =
getComputedStyle(document.documentElement).overflow === "hidden" &&
getComputedStyle(document.body).overflow === "hidden" &&
document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 &&
document.body.scrollHeight <= document.body.clientHeight + 2 &&
rootAfterScrollAttempt.windowScrollY === 0 &&
rootAfterScrollAttempt.htmlScrollTop === 0 &&
rootAfterScrollAttempt.bodyScrollTop === 0;
const noHorizontalOverflow =
document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2 &&
document.body.scrollWidth <= document.body.clientWidth + 2;
const controlsVisible = {
commandInput: visible("#command-input"),
commandSend: visible("#command-send"),
logout: visible("#logout-button"),
hardwareArea: visible(".hardware-status") && visible("[data-hardware-tab='overview']"),
m3Control: visible("#m3-control-form")
};
const pass =
login?.hidden === true &&
appShell?.hidden === false &&
document.body.dataset.authState === "authenticated" &&
rootScrollLocked &&
noHorizontalOverflow &&
Object.values(controlsVisible).every(Boolean);
return {
viewport,
pass,
loginHidden: login?.hidden === true,
workbenchVisible: appShell?.hidden === false,
bodyAuthState: document.body.dataset.authState ?? "",
rootScrollLocked,
rootAfterScrollAttempt,
noHorizontalOverflow,
controlsVisible
};
}, viewport);
}
async function installNoCodeAgentPostGuard(page) {
const attempts = [];
await page.route("**/v1/agent/chat", async (route) => {
@@ -3363,6 +3814,7 @@ async function inspectLiveCodeAgentE2e(url) {
browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 12000 });
await loginWithDefaultCredentials(page);
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
const controls = await inspectJourneyControls(page);
if (!Object.values(controls).every(Boolean)) {
@@ -4003,6 +4455,7 @@ async function runLocalAgentFixtureSmoke({
});
await page.goto(server.url, { waitUntil: "networkidle", timeout: 15000 });
await loginWithDefaultCredentials(page);
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
await page.waitForFunction(
() => document.querySelector("#live-status")?.textContent?.trim() === "只读模式",
@@ -4217,15 +4670,19 @@ export async function runDevCloudWorkbenchMobileSmoke() {
isMobile: true
});
await page.goto(server.url, { waitUntil: "networkidle", timeout: 15000 });
await loginWithDefaultCredentials(page);
const closed = await inspectMobileWorkbench(page);
const workspaceScroll = await inspectWorkbenchScrollContract(page, { panelSelector: "#conversation-list" });
await page.click('[data-route="help"]');
await page.waitForFunction(() => document.querySelector("#help-content")?.dataset.helpState === "ready", null, { timeout: 8000 });
await page.waitForTimeout(150);
const help = await inspectHelpMarkdownRoute(page);
await page.goto(new URL("/#/help", server.url).toString(), { waitUntil: "networkidle", timeout: 15000 });
await loginWithDefaultCredentials(page);
await page.waitForFunction(() => document.querySelector("#help-content")?.dataset.helpState === "ready", null, { timeout: 8000 });
const slashHashHelp = await inspectHelpMarkdownRoute(page);
await page.goto(new URL("/help", server.url).toString(), { waitUntil: "networkidle", timeout: 15000 });
await loginWithDefaultCredentials(page);
await page.waitForFunction(() => document.querySelector("#help-content")?.dataset.helpState === "ready", null, { timeout: 8000 });
const directHelp = await inspectHelpMarkdownRoute(page);
await page.click('[data-route="workspace"]');
@@ -4259,7 +4716,7 @@ export async function runDevCloudWorkbenchMobileSmoke() {
},
{
id: "mobile-help-markdown-route",
status: help.nonDefaultMarkdownHelp && help.routeIsHashHelp && help.termsPresent ? "pass" : "blocked",
status: help.nonDefaultMarkdownHelp && (help.routeIsHashHelp || help.routeIsSlashHashHelp) && help.termsPresent ? "pass" : "blocked",
summary: "Help opens from the rail as a non-default hash route rendered from help.md by the vendored Markdown renderer.",
observations: help
},
@@ -4492,6 +4949,7 @@ async function inspectQuickPromptsViewport(browser, url, viewport) {
});
try {
await page.goto(url, { waitUntil: "networkidle", timeout: 15000 });
await loginWithDefaultCredentials(page);
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
const promptResults = [];
for (const prompt of codeAgentQuickPromptFixtures) {
@@ -4652,8 +5110,12 @@ async function inspectQuickPromptGeometry(page, viewport) {
async function startStaticWebServer(options = {}) {
const rootDir = path.resolve(options.rootDir ?? webRoot);
const authFixtureSessions = new Set();
const server = http.createServer(async (request, response) => {
const url = new URL(request.url ?? "/", "http://127.0.0.1");
if (options.authFixture && await handleAuthFixtureApi({ request, response, url, authFixtureSessions })) {
return;
}
if (options.m3StatusFixture && await handleM3StatusFixtureApi({ request, response, url, options })) {
return;
}
@@ -4693,6 +5155,64 @@ async function startStaticWebServer(options = {}) {
};
}
async function handleAuthFixtureApi({ request, response, url, authFixtureSessions }) {
if (url.pathname === "/auth/session" && request.method === "GET") {
const token = authFixtureCookie(request);
jsonResponse(response, 200, token && authFixtureSessions.has(token)
? {
authenticated: true,
user: { username: defaultAuthCredentials.username },
expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString()
}
: { authenticated: false });
return true;
}
if (url.pathname === "/auth/login" && request.method === "POST") {
const body = await readJsonBody(request);
if (body?.username !== defaultAuthCredentials.username || body?.password !== defaultAuthCredentials.password) {
jsonResponse(response, 401, { authenticated: false, error: "invalid_credentials" });
return true;
}
const token = `auth_fixture_${authFixtureSessions.size + 1}`;
authFixtureSessions.add(token);
const payload = JSON.stringify({
authenticated: true,
user: { username: defaultAuthCredentials.username },
expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString()
});
response.writeHead(200, {
"content-type": "application/json; charset=utf-8",
"set-cookie": `hwlab_cloud_web_session=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=3600`
});
response.end(payload);
return true;
}
if (url.pathname === "/auth/logout" && request.method === "POST") {
const token = authFixtureCookie(request);
if (token) authFixtureSessions.delete(token);
response.writeHead(200, {
"content-type": "application/json; charset=utf-8",
"set-cookie": "hwlab_cloud_web_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0"
});
response.end(JSON.stringify({ authenticated: false }));
return true;
}
if (url.pathname.startsWith("/auth/")) {
jsonResponse(response, 404, { authenticated: false, error: "not_found" });
return true;
}
return false;
}
function authFixtureCookie(request) {
const cookie = request.headers.cookie ?? "";
for (const part of cookie.split(";")) {
const [name, ...value] = part.trim().split("=");
if (name === "hwlab_cloud_web_session") return decodeURIComponent(value.join("=") || "");
}
return "";
}
async function handleQuickPromptsFixtureApi({ request, response, url }) {
if (request.method === "GET" && (url.pathname === "/health/live" || url.pathname === "/v1")) {
jsonResponse(response, 200, {
@@ -5177,6 +5697,7 @@ async function inspectM3StatusFixtureViewport(browser, url, viewport) {
});
});
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 10000 });
await loginWithDefaultCredentials(page);
await page.waitForFunction(() => document.querySelector("#hardware-source-status")?.textContent?.trim() === "实况受阻", null, { timeout: 8000 });
for (const tab of ["gateways", "box1", "box2", "patch", "overview"]) {
await page.locator(`[data-hardware-tab="${tab}"]`).click();
@@ -5264,6 +5785,7 @@ async function inspectM3StatusFixtureRefresh(browser, url) {
});
});
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 10000 });
await loginWithDefaultCredentials(page);
await page.waitForFunction(() => document.querySelector("#hardware-source-status")?.textContent?.trim() === "实况受阻", null, { timeout: 8000 });
await page.locator('[data-hardware-tab="box1"]').click();
const initialBox1 = await hardwarePanelText(page);
@@ -5537,6 +6059,7 @@ async function inspectWorkbenchLayoutViewport(browser, url, viewport, options =
const artifacts = { screenshots: [] };
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
await loginWithDefaultCredentials(page);
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
await page.waitForTimeout(150);
const defaultLayout = await inspectLayoutState(page, {
@@ -5563,6 +6086,7 @@ async function inspectWorkbenchGateLayout(browser, url, viewport, options = {})
});
try {
await page.goto(new URL("/gate", url).toString(), { waitUntil: "domcontentloaded", timeout: 15000 });
await loginWithDefaultCredentials(page);
await page.locator('[data-view="gate"]').waitFor({ state: "visible", timeout: 12000 });
await page.waitForTimeout(150);
const currentRoute = await inspectGateLayoutState(page, { viewport, mode: "gate-current" });
@@ -6904,8 +7428,11 @@ async function inspectHelpMarkdownRoute(page) {
helpContent.scrollTop = 0;
await new Promise((resolve) => requestAnimationFrame(resolve));
const before = helpContent.scrollTop;
helpContent.scrollTop = helpContent.scrollHeight;
await new Promise((resolve) => requestAnimationFrame(resolve));
for (let attempt = 0; attempt < 3; attempt += 1) {
helpContent.scrollTop = helpContent.scrollHeight;
await new Promise((resolve) => requestAnimationFrame(resolve));
if (helpContent.scrollTop > before) break;
}
return {
before,
after: helpContent.scrollTop,