99 lines
4.8 KiB
TypeScript
99 lines
4.8 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { assertCloudWebDistFresh, buildCloudWebDist, readCloudWebAppSource } from "./dist-contract.ts";
|
|
|
|
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
const requiredFiles = Object.freeze([
|
|
"index.html",
|
|
"src/main.ts",
|
|
"src/App.vue",
|
|
"src/router/index.ts",
|
|
"src/router/guards.ts",
|
|
"src/api/client.ts",
|
|
"src/api/auth.ts",
|
|
"src/api/workbench.ts",
|
|
"src/api/agent.ts",
|
|
"src/stores/index.ts",
|
|
"src/stores/app.ts",
|
|
"src/stores/auth.ts",
|
|
"src/stores/workbench.ts",
|
|
"src/composables/useTraceSubscription.ts",
|
|
"src/composables/useWorkspaceBootstrap.ts",
|
|
"src/composables/useAutoRefresh.ts",
|
|
"src/composables/useClipboard.ts",
|
|
"src/components/layout/AppShell.vue",
|
|
"src/components/common/EmptyState.vue",
|
|
"src/views/workbench/CodeWorkbenchView.vue",
|
|
"src/views/admin/AccessView.vue",
|
|
"src/views/admin/ProviderProfilesView.vue",
|
|
"src/views/admin/HwpodGroupsView.vue",
|
|
"src/views/user/DashboardView.vue",
|
|
"src/views/user/ApiKeysView.vue",
|
|
"src/styles/workbench.css",
|
|
"src/style.css",
|
|
"tailwind.config.js",
|
|
"postcss.config.js",
|
|
"favicon.svg",
|
|
"favicon.ico",
|
|
"help.md"
|
|
]);
|
|
|
|
for (const file of requiredFiles) {
|
|
assert.ok(fs.existsSync(path.resolve(rootDir, file)), `missing Vue web asset: ${file}`);
|
|
}
|
|
|
|
const html = readWeb("index.html");
|
|
const pkg = JSON.parse(readWeb("package.json")) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
|
|
const appSource = readCloudWebAppSource(rootDir);
|
|
|
|
assert.match(html, /<div id="app"><\/div>/u, "index.html must expose Vue mount root");
|
|
assert.match(html, /src="\/src\/main\.ts"/u, "index.html must load Vue TS entry");
|
|
assert.match(html, /HWLAB_CLOUD_WEB_EARLY_WORKSPACE_BOOTSTRAP/u, "workspace bootstrap must still start during HTML parse");
|
|
assert.doesNotMatch(html, /src="\/src\/main\.tsx"|id="root"/u, "React entry must be gone");
|
|
|
|
for (const dep of ["vue", "pinia", "vue-router", "axios", "@vueuse/core", "vue-i18n"]) {
|
|
assert.ok(pkg.dependencies?.[dep], `missing Vue/Sub2API dependency ${dep}`);
|
|
}
|
|
for (const dep of ["@vitejs/plugin-vue", "tailwindcss", "postcss", "autoprefixer", "vue-tsc", "vitest"]) {
|
|
assert.ok(pkg.devDependencies?.[dep], `missing Vue/Tailwind devDependency ${dep}`);
|
|
}
|
|
for (const removed of ["react", "react-dom", "react-markdown", "remark-gfm"]) {
|
|
assert.equal(pkg.dependencies?.[removed], undefined, `React dependency must be removed: ${removed}`);
|
|
}
|
|
|
|
for (const directory of ["api", "stores", "composables", "components/common", "components/layout", "components/workbench", "components/agent", "components/hwpod", "components/access", "views/admin", "views/user", "views/workbench", "router", "i18n", "styles", "types", "utils"]) {
|
|
assert.ok(fs.existsSync(path.join(rootDir, "src", directory)), `missing Sub2API-style module boundary src/${directory}`);
|
|
}
|
|
|
|
assertIncludes(appSource, "createApp", "Vue app must use createApp");
|
|
assertIncludes(appSource, "createPinia", "Pinia must be installed");
|
|
assertIncludes(appSource, "createRouter", "Vue Router must be installed");
|
|
assertIncludes(appSource, "defineStore", "Pinia stores must exist");
|
|
assertIncludes(appSource, "hwlab_session", "auth comments/code must preserve Web session cookie boundary");
|
|
assertIncludes(appSource, "activityRef", "Code Agent inactivity-timeout activityRef must be preserved");
|
|
assertIncludes(appSource, "activityRef: () => activityRef.value", "Workbench must pass activityRef into trace subscription");
|
|
assertIncludes(appSource, "getAgentChatResult(resultUrl, inactivityTimeoutMs, activityRef)", "Trace result polling must use activityRef-backed inactivity timeout");
|
|
assertIncludes(appSource, "for (;;) ", "trace subscription must keep unbounded polling without total timeout");
|
|
assertIncludes(appSource, "/v1/agent/chat/trace/", "trace replay must use Cloud Web same-origin trace API");
|
|
assertIncludes(appSource, "session_required", "explicit session policy must be represented");
|
|
assertIncludes(appSource, "DEV-LIVE", "evidence labels must remain visible");
|
|
assertIncludes(appSource, "CodeWorkbenchView", "Code Workbench must be a route view, not the app shell");
|
|
assert.doesNotMatch(appSource, /from ["']react["']|React\.StrictMode|createRoot|\.tsx/u, "React runtime references must not remain");
|
|
|
|
await buildCloudWebDist(rootDir);
|
|
console.log("hwlab-cloud-web check: dist bundle built by Vite/Vue");
|
|
await assertCloudWebDistFresh(rootDir);
|
|
console.log("hwlab-cloud-web check ok: Vue + Pinia + Router + Tailwind architecture is present");
|
|
|
|
function readWeb(relativePath: string): string {
|
|
return fs.readFileSync(path.resolve(rootDir, relativePath), "utf8");
|
|
}
|
|
|
|
function assertIncludes(source: string, term: string, message: string): void {
|
|
assert.ok(source.includes(term), message);
|
|
}
|