import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { assertCloudWebDistFresh, buildCloudWebDist, readCloudWebAppSource } from "./dist-contract.ts"; const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const repoRoot = path.resolve(rootDir, "../.."); const requiredFiles = Object.freeze([ "index.html", "styles.css", "app.ts", "app-skills.ts", "app-session-tabs.ts", "app-device-pod.ts", "app-conversation.ts", "app-trace.ts", "app-helpers.ts", "composer-policy.ts", "auth.ts", "code-agent-facts.ts", "code-agent-status.ts", "message-markdown.ts", "live-status.ts", "runtime.ts", "favicon.svg", "favicon.ico", "help.md", "third_party/marked/marked.esm.js", "third_party/marked/LICENSE" ]); for (const file of requiredFiles) { const filePath = path.resolve(rootDir, file); if (!fs.existsSync(filePath)) throw new Error(`missing web asset: ${file}`); } console.log("hwlab-cloud-web check: runtime assets present"); await buildCloudWebDist(rootDir); console.log("hwlab-cloud-web check: dist bundle built"); await assertCloudWebDistFresh(rootDir); console.log("hwlab-cloud-web check: dist freshness verified"); const html = readWeb("index.html"); const styles = readWeb("styles.css"); const app = readCloudWebAppSource(rootDir); assertTraceHelpersBound(readWeb("app-trace.ts")); const liveStatus = readWeb("live-status.ts"); const helpMarkdown = readWeb("help.md"); const faviconSvg = readWeb("favicon.svg"); const faviconIco = readWeb("favicon.ico"); const distContractScript = readWeb("scripts/dist-contract.ts"); const cloudApiServer = readRepo("internal/cloud/server.ts"); const cloudApiPayloads = readRepo("internal/cloud/server-rest-payloads.ts"); const devicePodData = readRepo("internal/device-pod/fake-data.mjs"); const devicePodService = readRepo("cmd/hwlab-device-pod/main.ts"); const protocol = readRepo("internal/protocol/index.mjs"); const deployJson = readRepo("deploy/deploy.json"); const artifactCatalog = readRepo("deploy/artifact-catalog.dev.json"); console.log("hwlab-cloud-web check: source files loaded"); const rightSidebar = sectionById(html, "aside", "device-pod-sidebar"); console.log("hwlab-cloud-web check: right sidebar extracted"); assert.ok(rightSidebar, "Device Pod right sidebar must exist"); assert.match(html, /data-route="skills"/u, "Cloud Web must expose the skills activity route"); assert.match(html, /id="session-tabs"[^>]*role="tablist"/u, "Cloud Web workspace must expose session tabs"); assert.match(html, /class="session-sidebar"[^>]*id="session-sidebar"/u, "Cloud Web must place sessions in a second-level sidebar"); assert.match(html, /id="skills"[^>]*data-view="skills"/u, "Cloud Web must expose a skills view"); assert.match(html, /id="skill-upload-input"[^>]*webkitdirectory/u, "Skills view must support directory upload"); assertIncludes(app, "/v1/skills", "skills frontend must call /v1/skills"); assertIncludes(app, "/v1/skills/uploads", "skills frontend must call /v1/skills/uploads"); assert.match(app, /function\s+initSkillsPanel\s*\(/u, "missing initSkillsPanel"); assert.match(app, /function\s+loadSkillsSurface\s*\(/u, "missing loadSkillsSurface"); assert.match(app, /function\s+uploadSelectedSkillFiles\s*\(/u, "missing uploadSelectedSkillFiles"); assert.match(app, /function\s+renderSkillPreview\s*\(/u, "missing renderSkillPreview"); assertIncludes(app, "promptAssembly", "skills frontend must render AgentRun prompt assembly summary"); assertIncludes(app, "引导 prompt 装配", "skills frontend must label the prompt assembly panel"); assertIncludes(app, "ResourceBundleRef.promptRefs", "skills frontend must expose prompt assembly source metadata"); assertIncludes(styles, ".skill-prompt-assembly", "skills frontend must style prompt assembly rows"); assertIncludes(styles, ".skill-prompt-row", "skills frontend must style prompt assembly refs"); assertIncludes(app, "sessionTabsFromMessages", "workspace frontend must group messages by session"); assertIncludes(app, "activeSessionKey", "workspace frontend must track the active session tab"); assertIncludes(app, "renderSessionSidebar", "workspace frontend must render account-scoped sessions in the second-level sidebar"); assertIncludes(app, "selectSessionSidebarSession", "workspace frontend must switch sessions from the second-level sidebar"); assertIncludes(styles, ".session-tabs", "workspace frontend must style session tabs"); assertIncludes(styles, ".session-tab", "workspace frontend must style individual session tabs"); assertIncludes(app, "sessionTabsFromConversations", "workspace frontend must build session tabs from account conversations"); assertIncludes(app, "/v1/agent/conversations", "workspace frontend must load account-scoped sessions"); assertIncludes(app, "select-conversation", "workspace frontend must switch sessions through workbench select-conversation"); assertIncludes(styles, ".session-sidebar", "workspace frontend must style the second-level session sidebar"); assertIncludes(styles, "grid-template-columns: var(--rail-width) var(--session-sidebar-width)", "workspace shell must place session sidebar to the right of activity rail"); assert.match(cssRule(styles, ".activity-rail"), /overflow-x:\s*hidden;/u, "activity rail must not horizontally scroll"); assert.match(cssRule(styles, ".session-tabs"), /overflow-x:\s*hidden;/u, "session sidebar must not horizontally scroll"); assert.match(cssRule(styles, ".rail-button"), /white-space:\s*nowrap;/u, "activity rail labels must not auto-wrap"); assert.match(cssRule(styles, ".session-tab-label"), /white-space:\s*nowrap;/u, "session labels must not auto-wrap"); console.log("hwlab-cloud-web check: skills contracts verified"); for (const term of [ "Device Pod", "设备目标看板", "id=\"device-pod-select\"", "id=\"device-pod-summary\"", "data-device-detail=\"pod\"", "id=\"device-pod-interfaces\"", "id=\"device-event-scroll\"", "id=\"device-event-text\"", "id=\"device-detail-dialog\"" ]) { assertIncludes(rightSidebar, term, `right sidebar missing ${term}`); } assert.doesNotMatch(rightSidebar, /]*role="button"[^>]*tabindex="0"/u, "Pod summary is a click/keyboard summary tile"); assert.match(rightSidebar, /