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 } from "./dist-contract.ts"; const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const requiredSourceFiles = Object.freeze([ "src/main.tsx", "src/App.tsx", "src/components/auth/LoginView.tsx", "src/components/layout/Workbench.tsx", "src/components/layout/AppShell.tsx", "src/components/layout/ActivityRail.tsx", "src/components/layout/SessionSidebar.tsx", "src/components/layout/CenterWorkspace.tsx", "src/components/layout/WorkspaceView.tsx", "src/components/layout/RightSidebar.tsx", "src/components/conversation/ConversationView.tsx", "src/components/conversation/MessageList.tsx", "src/components/conversation/MessageItem.tsx", "src/components/command-bar/CommandBar.tsx", "src/components/device-pod/DevicePodPanel.tsx", "src/components/device-pod/DevicePodSummary.tsx", "src/components/device-pod/DevicePodInterfaces.tsx", "src/components/device-pod/DeviceEventStream.tsx", "src/components/device-pod/DeviceDetailDialog.tsx", "src/components/skills/SkillsView.tsx", "src/components/settings/SettingsView.tsx", "src/components/help/HelpView.tsx", "src/hooks/useAuth.ts", "src/hooks/useWorkspace.ts", "src/hooks/useDevicePod.ts", "src/hooks/useSkills.ts", "src/hooks/useTracePolling.ts", "src/hooks/useCodeAgentSubmit.ts", "src/services/api/client.ts", "src/services/api/auth.ts", "src/services/api/sessions.ts", "src/services/api/conversations.ts", "src/services/api/codeAgent.ts", "src/services/api/devicePods.ts", "src/services/api/skills.ts", "src/services/api/workspace.ts", "src/services/auth.ts", "src/state/workbenchState.tsx", "src/logic/composer-policy.ts", "src/logic/code-agent-facts.ts", "src/logic/code-agent-status.ts", "src/logic/live-status.ts", "src/logic/message-markdown.ts", "src/logic/app-trace.ts", "src/logic/app-session-tabs.ts", "src/logic/runtime.ts", "vite.config.ts", "styles.css", "favicon.svg", "favicon.ico", "help.md", "third_party/marked/marked.esm.js", "third_party/marked/LICENSE", ]); for (const file of requiredSourceFiles) { const filePath = path.resolve(rootDir, file); if (!fs.existsSync(filePath)) throw new Error(`missing web asset: ${file}`); } console.log("hwlab-cloud-web check: react/typescript source tree present"); const html = readWeb("index.html"); const styles = readWeb("styles.css"); const traceSource = readWeb("src/logic/app-trace.ts"); const messageMarkdown = readWeb("src/logic/message-markdown.ts"); const sessionTabs = readWeb("src/logic/app-session-tabs.ts"); const composerPolicy = readWeb("src/logic/composer-policy.ts"); const liveStatus = readWeb("src/logic/live-status.ts"); const codeAgentFacts = readWeb("src/logic/code-agent-facts.ts"); const codeAgentStatus = readWeb("src/logic/code-agent-status.ts"); const appEntry = readWeb("src/main.tsx"); const appComponent = readWeb("src/App.tsx"); const workbench = readWeb("src/components/layout/Workbench.tsx"); const skillsView = readWeb("src/components/skills/SkillsView.tsx"); const settingsView = readWeb("src/components/settings/SettingsView.tsx"); const helpView = readWeb("src/components/help/HelpView.tsx"); const commandBar = readWeb("src/components/command-bar/CommandBar.tsx"); const devicePodPanel = readWeb("src/components/device-pod/DevicePodPanel.tsx"); const messageItem = readWeb("src/components/conversation/MessageItem.tsx"); const composerClient = readWeb("src/services/api/client.ts"); const authService = readWeb("src/services/auth.ts"); const authApi = readWeb("src/services/api/auth.ts"); const stateContext = readWeb("src/state/workbenchState.tsx"); const distContractScript = readWeb("scripts/dist-contract.ts"); assertReactHtmlShell(html); assertReactEntry(appEntry); assertReactAppComposition(appComponent); assertReactComponentSplit(workbench, skillsView, settingsView, helpView); assertReactComposerContract(composerPolicy, workbench, commandBar); assertReactCodeAgentRoutes(authService, composerClient); assertReactSkillsContract(skillsView, styles); assertReactDevicePodContract(devicePodPanel, styles); assertReactLiveStatusContract(liveStatus); assertReactCodeAgentFacts(codeAgentFacts, codeAgentStatus); assertReactMarkdownContract(messageItem, messageMarkdown); assertReactSessionTabsContract(sessionTabs, workbench); assertReactStateShape(stateContext); assertReactTestCoverage(traceSource, messageMarkdown, sessionTabs, composerPolicy, liveStatus, codeAgentFacts, codeAgentStatus); assertTraceHelpersBound(traceSource); assertDevicePodEvidenceSummaryDoesNotCrash(); console.log("hwlab-cloud-web check: react/typescript contracts verified"); await buildCloudWebDist(rootDir); console.log("hwlab-cloud-web check: vite dist bundle built"); await assertCloudWebDistFresh(rootDir); console.log("hwlab-cloud-web check: dist freshness verified"); for (const oldAsset of ["code-agent-m3-evidence.mjs", "wiring-status.mjs", "workbench-hardware-panel.mjs", "app.ts", "app-conversation.ts", "app-device-pod.ts", "app-skills.ts", "app-helpers.ts", "auth.ts"]) { if (fs.existsSync(path.resolve(rootDir, oldAsset))) { throw new Error(`legacy web runtime asset still present: ${oldAsset}`); } } console.log("hwlab-cloud-web check: legacy asset cleanup verified"); console.log("hwlab-cloud-web check ok: React + TypeScript SPA, Vite dist, module boundary contracts and tests are present"); function readWeb(relativePath) { return fs.readFileSync(path.resolve(rootDir, relativePath), "utf8"); } function assertReactHtmlShell(source) { assert.match(source, /
]*data-cloud-web-root/u, "index.html must mount React into #root"); assert.doesNotMatch(source, /class="login-shell"/u, "index.html must not inline the legacy login shell"); assert.doesNotMatch(source, /class="workbench-shell"/u, "index.html must not inline the legacy workbench shell"); assert.doesNotMatch(source, /id="login-form"/u, "index.html must not inline the legacy login form"); assert.doesNotMatch(source, /id="command-form"/u, "index.html must not inline the legacy command form"); assert.doesNotMatch(source, /id="device-pod-sidebar"/u, "index.html must not inline the legacy device pod sidebar"); assert.match(source, /