Files
pikasTech-HWLAB/web/hwlab-cloud-web/scripts/r6-architecture-parity.test.ts
T

373 lines
17 KiB
TypeScript
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 fs from "node:fs";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { messageFromError } from "../src/composables/useForm.ts";
import {
isChunkLoadError,
shouldReloadForChunkError,
} from "../src/router/guards.ts";
import { resolveDocumentTitle } from "../src/router/title.ts";
const rootDir = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"..",
);
test("R6 route meta title and chunk recovery match Sub2API navigation pattern", () => {
assert.equal(resolveDocumentTitle("Gate"), "Gate - HWLAB Cloud Web");
assert.equal(resolveDocumentTitle(" "), "平台控制台 - HWLAB Cloud Web");
assert.equal(
isChunkLoadError(new Error("Failed to fetch dynamically imported module")),
true,
);
assert.equal(
isChunkLoadError(new Error("Loading CSS chunk 42 failed")),
true,
);
assert.equal(
isChunkLoadError(
new Error("Unable to preload CSS for /assets/MdtodoView.css"),
),
true,
);
assert.equal(
isChunkLoadError(new Error("ordinary route guard failure")),
false,
);
const memory = new Map<string, string>();
const storage = {
getItem: (key: string) => memory.get(key) ?? null,
setItem: (key: string, value: string) => {
memory.set(key, value);
},
};
assert.equal(shouldReloadForChunkError(100_000, storage), true);
assert.equal(shouldReloadForChunkError(105_000, storage), false);
assert.equal(shouldReloadForChunkError(111_000, storage), true);
});
test("R6 useForm normalizes nested API errors for toast/error surfaces", () => {
assert.equal(messageFromError(new Error("plain failure")), "plain failure");
assert.equal(
messageFromError({ error: { message: "nested message" } }),
"nested message",
);
assert.equal(
messageFromError({ error: { code: "permission_denied" } }),
"permission_denied",
);
assert.equal(
messageFromError({ reason: "blocked_by_gate" }),
"blocked_by_gate",
);
});
test("R6 common module map keeps HWLAB views on shared console primitives", () => {
assert.equal(
fs.existsSync(path.join(rootDir, "src/components/common/DataTable.vue")),
false,
"legacy DataTable should stay removed after DataGrid migration",
);
const expectations: Array<[string, string[]]> = [
[
"src/views/admin/ProviderProfilesView.vue",
[
"AsyncBoundary",
"DataGrid",
"ResourceCollection",
"BaseDialog",
"ConfirmDialog",
"TablePageLayout",
"useTableLoader",
"useForm",
],
],
[
"src/components/access/AccessMatrixPanel.vue",
[
"AsyncBoundary",
"OperationsWorkspace",
"AccessUserIndex",
"AccessPermissionWorkspace",
"AccessUserInspector",
],
],
[
"src/views/admin/SecretsView.vue",
[
"AsyncBoundary",
"OperationsWorkspace",
"SecretInventoryIndex",
"SecretInventoryWorkspace",
],
],
[
"src/views/admin/UsersView.vue",
[
"AsyncBoundary",
"OperationsWorkspace",
"UserDirectoryIndex",
"UserDirectoryCollection",
"UserAccountInspector",
"ViewModeSwitch",
],
],
[
"src/views/admin/BillingView.vue",
[
"AsyncBoundary",
"OperationsWorkspace",
"BillingCodeIndex",
"BillingCodeInspector",
"ViewModeSwitch",
],
],
[
"src/views/GateView.vue",
["AsyncBoundary", "GateWorkspace", "useTableLoader"],
],
[
"src/views/PerformanceView.vue",
["AsyncBoundary", "PerformanceOverview", "PerformanceDetailGrid", "ViewModeSwitch", "useTableLoader"],
],
[
"src/views/SkillsView.vue",
[
"AsyncBoundary",
"DataGrid",
"ResourceCollection",
"VirtualList",
"BaseDialog",
"TablePageLayout",
"useTableLoader",
"useForm",
],
],
];
for (const [relativePath, terms] of expectations) {
const source = read(relativePath);
for (const term of terms)
assert.match(
source,
new RegExp(`\\b${term}\\b`, "u"),
`${relativePath} should use ${term}`,
);
assert.doesNotMatch(
source,
/<table\s/u,
`${relativePath} should not regress to local raw table markup`,
);
if (
/admin\/(?:Secrets|Users|Billing)View|access\/AccessMatrixPanel/u.test(
relativePath,
)
)
assert.doesNotMatch(
source,
/view-toggle|admin-user-glyph|billing-glyph|BoundedWorkspace|TablePageLayout/u,
`${relativePath} should not restore private legacy layout or glyph implementations`,
);
}
});
test("R6 console semantics keep real icons, native entity lists, and dynamic access columns", () => {
assert.equal(fs.existsSync(path.join(rootDir, "src/components/console/ConsoleIcon.vue")), false);
assert.match(read("package.json"), /"lucide-vue-next"/u);
const viewSwitchSource = read("src/components/console/ViewModeSwitch.vue");
assert.match(viewSwitchSource, /type \{ Component \}/u);
assert.match(viewSwitchSource, /<component :is="option\.icon"/u);
const entityIndexSource = read("src/components/console/EntityIndex.vue");
assert.match(entityIndexSource, /<ul\b/u);
assert.match(entityIndexSource, /<li\b/u);
assert.doesNotMatch(entityIndexSource, /role="listbox"|role="option"/u);
const accessSource = read("src/components/access/AccessMatrixPanel.vue");
assert.match(accessSource, /--permission-tool-count/u);
assert.doesNotMatch(accessSource, /repeat\(6,/u);
assert.match(accessSource, /if \(!value\) access\.clearSelection\(\)/u);
assert.match(
accessSource,
/function closeInspector\(\): void \{\s*access\.clearSelection\(\);/u,
);
const accessStoreSource = read("src/stores/access.ts");
assert.match(accessStoreSource, /function clearSelection\(\): void/u);
assert.match(accessStoreSource, /selectedUserId\.value = null/u);
assert.match(accessStoreSource, /detail\.value = null/u);
const accessWorkspaceSource = read("src/components/access/AccessPermissionWorkspace.vue");
assert.doesNotMatch(accessWorkspaceSource, /<button[^>]+role="row"/u);
assert.match(accessWorkspaceSource, /role="grid"/u);
assert.match(accessWorkspaceSource, /role="row"/u);
assert.match(accessWorkspaceSource, /role="gridcell"/u);
const accessViewSource = read("src/stores/admin-access-view.ts");
assert.match(accessViewSource, /interface AccessOpenFgaView/u);
assert.match(accessViewSource, /interface AccessTupleView/u);
assert.doesNotMatch(read("src/components/access/AccessUserInspector.vue"), /Record<string, unknown>/u);
assert.doesNotMatch(read("src/components/access/AccessPermissionWorkspace.vue"), /Record<string, unknown>/u);
});
test("R6 MDTODO task rows expose opaque task references", () => {
const taskTreeSource = read("src/components/mdtodo/MdtodoTaskTree.vue");
assert.match(taskTreeSource, /:data-task-ref="task\.taskRef"/u);
});
test("R2 console rewrite owns the final visual layer and removes private letter glyphs", () => {
const mainSource = read("src/main.ts");
assert.match(mainSource, /console-shell\.css/u);
assert.match(mainSource, /console-r2\.css/u);
assert.doesNotMatch(mainSource, /console-legacy\.css/u);
const migratedViews: Array<[string, string]> = [
["src/components/common/EmptyState.vue", "lucide-vue-next"],
["src/views/SkillsView.vue", "EntityCard"],
["src/views/projects/ProjectsView.vue", "ProjectResourceCard"],
["src/views/admin/ProviderProfilesView.vue", "EntityCard"],
["src/views/user/ApiKeysView.vue", "ApiKeyResourceCard"],
];
for (const [relativePath, sharedComponent] of migratedViews) {
const source = read(relativePath);
assert.match(source, new RegExp(sharedComponent, "u"), `${relativePath} should use ${sharedComponent}`);
assert.doesNotMatch(source, />\s*(?:SK|PJ|AI|KEY|HW)\s*</u, `${relativePath} should not render private letter glyphs`);
}
const r2Styles = read("src/styles/console-r2.css");
for (const selector of [".platform-sidebar", ".page-command-bar", ".console-status-strip", ".console-data-grid", ".pcb-card", ".mdtodo-workspace"])
assert.match(r2Styles, new RegExp(selector.replaceAll(".", "\\."), "u"), `R2 visual layer should own ${selector}`);
assert.match(r2Styles, /@media \(max-width: 1100px\)/u);
assert.match(r2Styles, /@media \(max-width: 760px\)/u);
assert.match(r2Styles, /@media \(max-width: 430px\)/u);
assert.match(r2Styles, /prefers-reduced-motion/u);
const collectionSource = read("src/components/common/ResourceCollection.vue");
assert.match(collectionSource, /VirtualList/u);
assert.match(collectionSource, /ViewModeSwitch/u);
assert.match(collectionSource, /virtualizeAfter/u);
assert.match(collectionSource, /itemIcon/u);
assert.doesNotMatch(collectionSource, /segmented-control/u);
const cardSource = read("src/components/common/EntityCard.vue");
assert.match(cardSource, /type \{ Component \}/u);
assert.match(cardSource, /<component :is="icon"/u);
assert.match(cardSource, /entity-card-meta/u);
});
test("R2 page rewrites move business markup into owned modules instead of CSS wrappers", () => {
const moduleExpectations: Array<[string, string[]]> = [
["src/components/access/AccessUserIndex.vue", ["EntityIndex", "AccessUserRow"]],
["src/components/access/AccessPermissionWorkspace.vue", ["permission-matrix", "FactPath", "ViewModeSwitch"]],
["src/components/access/AccessUserInspector.vue", ["InspectorPanel", "Tool grants", "Tuples"]],
["src/components/secrets/SecretInventoryIndex.vue", ["EntityIndex", "StatusBadge"]],
["src/components/secrets/SecretInventoryWorkspace.vue", ["FactPath", "SectionFrame", "安全边界"]],
["src/components/users/UserDirectoryIndex.vue", ["EntityIndex", "Pagination"]],
["src/components/users/UserDirectoryCollection.vue", ["lucide-vue-next", "DataGrid", "用户实体网格"]],
["src/components/users/UserAccountInspector.vue", ["InspectorPanel", "entitlements", "reservations", "ledger"]],
["src/components/billing/BillingCodeIndex.vue", ["EntityIndex", "RedeemCodeSummary"]],
["src/components/billing/BillingCodeInspector.vue", ["InspectorPanel", "Credits", "Usage"]],
["src/components/hwpod/HwpodDevicesWorkspace.vue", ["HwpodDeviceCard", "DataGrid", "ResourceCollection"]],
["src/components/hwpod/HwpodNodesWorkspace.vue", ["挂载 HWPOD", "HwpodReadinessRail", "DataGrid"]],
["src/components/hwpod/HwpodOnboardingWorkspace.vue", ["Python single file", "SHA-256", "Staged readiness"]],
["src/components/common/OverlaySurface.vue", ["useOverlaySurface", "lucide-vue-next", "Teleport"]],
["src/components/mdtodo/MdtodoTaskPanel.vue", ["ContentViewer", "editingBody", "Markdown"]],
["src/components/mdtodo/MdtodoReportPanel.vue", ["ContentViewer", "openFullscreen", "closeFullscreen"]],
["src/components/dashboard/DashboardWorkspace.vue", ["StatusStrip", "ResourceCollection", "SectionFrame"]],
["src/components/usage/UsageWorkspace.vue", ["StatusStrip", "DataGrid", "SectionFrame", "ViewModeSwitch"]],
["src/components/billing/UserBillingWorkspace.vue", ["StatusStrip", "DataGrid", "SectionFrame", "BaseDialog"]],
["src/components/settings/SettingsWorkspace.vue", ["StatusStrip", "SectionFrame", "InspectorPanel"]],
["src/components/help/HelpWorkspace.vue", ["ContentViewer", "SectionFrame", "InspectorPanel"]],
["src/components/secrets/ExternalSecretWorkspace.vue", ["StatusStrip", "DataGrid", "ResourceCollection", "ViewModeSwitch"]],
["src/components/gate/GateWorkspace.vue", ["StatusStrip", "DataGrid", "InspectorPanel", "Pagination"]],
["src/components/performance/PerformanceOverview.vue", ["StatusStrip", "SectionFrame", "ResourceCollection", "performance-line-chart"]],
["src/components/performance/PerformanceDetailGrid.vue", ["DataGrid", "SectionFrame"]],
];
for (const [relativePath, terms] of moduleExpectations) {
const source = read(relativePath);
for (const term of terms) assert.match(source, new RegExp(term, "u"), `${relativePath} should contain ${term}`);
}
const cardMigrations: Array<[string, string]> = [
["src/views/SkillsView.vue", "EntityCard"],
["src/views/admin/ProviderProfilesView.vue", "EntityCard"],
["src/views/projects/ProjectsView.vue", "ProjectResourceCard"],
["src/views/user/ApiKeysView.vue", "ApiKeyResourceCard"],
];
for (const [relativePath, component] of cardMigrations) assert.match(read(relativePath), new RegExp(component, "u"));
assert.doesNotMatch(read("src/views/SkillsView.vue"), /\.skill-card\b|\.skill-glyph\b/u);
assert.doesNotMatch(read("src/views/admin/ProviderProfilesView.vue"), /\.provider-card\b|\.provider-glyph\b/u);
assert.doesNotMatch(read("src/views/projects/ProjectsView.vue"), /\.project-card\b|\.project-glyph\b/u);
assert.doesNotMatch(read("src/views/user/ApiKeysView.vue"), /\.api-key-card\b|\.api-key-glyph\b/u);
const viewModules: Array<[string, string]> = [
["src/views/user/DashboardView.vue", "DashboardWorkspace"],
["src/views/user/UsageView.vue", "UsageWorkspace"],
["src/views/user/BillingView.vue", "UserBillingWorkspace"],
["src/views/SettingsView.vue", "SettingsWorkspace"],
["src/views/HelpView.vue", "HelpWorkspace"],
["src/views/admin/ExternalSecretDetailView.vue", "ExternalSecretWorkspace"],
["src/views/GateView.vue", "GateWorkspace"],
["src/views/PerformanceView.vue", "PerformanceOverview"],
];
for (const [relativePath, component] of viewModules) {
const source = read(relativePath);
assert.match(source, new RegExp(component, "u"));
assert.doesNotMatch(source, /<DataGrid\b|<ResourceCollection\b|<section class="data-panel/u, `${relativePath} should delegate business markup to owned modules`);
}
});
test("R2 visual constraints keep industrial controls bounded and component-owned", () => {
const changedStyleSources = [
"src/styles/console-r2.css",
"src/styles/console-foundation.css",
"src/components/hwpod/HwpodDeviceCard.vue",
"src/views/admin/HwpodGroupsView.vue",
"src/views/projects/ProjectsView.vue",
"src/views/user/ApiKeysView.vue",
"src/views/SkillsView.vue",
"src/views/admin/ProviderProfilesView.vue",
"src/views/admin/SecretsView.vue",
"src/components/access/AccessMatrixPanel.vue",
"src/components/mdtodo/MdtodoToolbar.vue",
"src/components/mdtodo/MdtodoCreateTaskDialog.vue",
"src/components/mdtodo/MdtodoInfoDialog.vue",
"src/components/mdtodo/MdtodoSourceDialog.vue",
"src/views/LoginView.vue",
"src/views/RegisterView.vue",
"src/views/NotFoundView.vue",
"src/views/opencode/OpenCodeFrameView.vue",
].map(read).join("\n");
for (const declaration of changedStyleSources.matchAll(/letter-spacing:\s*([^;]+);/gu))
assert.equal(declaration[1]?.trim(), "0", `letter-spacing must be 0, got ${declaration[1]}`);
assert.doesNotMatch(changedStyleSources, /font-size:\s*clamp\([^)]*(?:vw|vh)/u);
for (const relativePath of ["src/components/hwpod/HwpodDeviceCard.vue", "src/views/admin/HwpodGroupsView.vue", "src/views/projects/ProjectsView.vue", "src/views/user/ApiKeysView.vue"])
assert.doesNotMatch(read(relativePath), /border-radius:\s*(?:9|10|11|12)px/u, `${relativePath} should keep new card and command radii at 8px or below`);
const r2Styles = read("src/styles/console-r2.css");
assert.doesNotMatch(r2Styles, /\.(?:project-card|skill-card|provider-card|api-key-card|settings-grid|usage-panels|help-body)\b/u);
assert.doesNotMatch(read("src/components/mdtodo/MdtodoReportPanel.vue"), /<svg\b|>\s*×\s*</u);
assert.doesNotMatch(read("src/components/mdtodo/MdtodoToolbar.vue"), /<svg\b|>\s*×\s*</u);
for (const relativePath of ["src/views/SkillsView.vue", "src/views/admin/ProviderProfilesView.vue", "src/views/projects/ProjectsView.vue", "src/views/user/ApiKeysView.vue", "src/views/admin/HwpodGroupsView.vue"])
assert.match(read(relativePath), /ViewModeSwitch/u, `${relativePath} should use the shared view switch`);
});
test("R6 Sub2API source comparison is recorded in parent/child issue scope", () => {
const issueBody = readIssueBodyFixture();
assert.match(issueBody, /Wei-Shaw\/sub2api@v0\.1\.136=a2f76e4/u);
assert.match(issueBody, /router\/index\.ts/u);
assert.match(issueBody, /components\/common/u);
assert.match(issueBody, /useTableLoader\.ts/u);
assert.match(issueBody, /useForm\.ts/u);
});
function read(relativePath: string): string {
return fs.readFileSync(path.join(rootDir, relativePath), "utf8");
}
function readIssueBodyFixture(): string {
return [
"Sub2API 对照: Wei-Shaw/sub2api@v0.1.136=a2f76e4",
"router/index.ts api/client.ts components/common/* composables/useTableLoader.ts useForm.ts admin/user CRUD views",
].join("\n");
}