返回 DeepSeek-Reasonix
bundle-contract.test.ts
根目录 / desktop / frontend / src / __tests__ / bundle-contract.test.ts
1 // Run: tsx src/__tests__/bundle-contract.test.ts
2
3 import { readFileSync } from "node:fs";
4 import { dirname, resolve } from "node:path";
5 import { fileURLToPath } from "node:url";
6 import ts from "typescript";
7
8 let passed = 0;
9 let failed = 0;
10
11 function ok(cond: boolean, label: string) {
12 if (cond) {
13 process.stdout.write(` PASS ${label}\n`);
14 passed += 1;
15 } else {
16 process.stdout.write(` FAIL ${label}\n`);
17 failed += 1;
18 }
19 }
20
21 const here = dirname(fileURLToPath(import.meta.url));
22 function lazyRuntimeImport(owner: string, target: string): boolean {
23 const file = resolve(here, owner);
24 const tree = ts.createSourceFile(file, readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true);
25 let dynamic = false;
26 let eager = false;
27 function visit(node: ts.Node) {
28 if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)
29 && node.moduleSpecifier.text === target && !node.importClause?.isTypeOnly) eager = true;
30 if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword
31 && node.arguments[0] && ts.isStringLiteral(node.arguments[0]) && node.arguments[0].text === target) dynamic = true;
32 ts.forEachChild(node, visit);
33 }
34 visit(tree);
35 return dynamic && !eager;
36 }
37 const appSource = readFileSync(resolve(here, "../App.tsx"), "utf8");
38 const historyOwnerSource = readFileSync(resolve(here, "../app-runtime/useHistoryCommands.ts"), "utf8");
39 const paletteOwnerSource = readFileSync(resolve(here, "../app-runtime/usePaletteCommands.tsx"), "utf8");
40 const projectTreeSource = readFileSync(resolve(here, "../components/ProjectTree.tsx"), "utf8");
41 const settingsEntrySource = readFileSync(resolve(here, "../components/SettingsPanelEntry.tsx"), "utf8");
42 const settingsSource = readFileSync(resolve(here, "../components/SettingsPanel.tsx"), "utf8");
43 const remoteWizardEntrySource = readFileSync(resolve(here, "../components/RemoteConnectWizardEntry.tsx"), "utf8");
44 const remoteWizardSource = readFileSync(resolve(here, "../components/RemoteConnectWizard.tsx"), "utf8");
45 const projectCreationSource = readFileSync(resolve(here, "../components/useProjectCreation.tsx"), "utf8");
46 const markdownSource = readFileSync(resolve(here, "../components/Markdown.tsx"), "utf8");
47 const localPathLinksSource = readFileSync(resolve(here, "../lib/localPathLinks.ts"), "utf8");
48 const i18nSource = readFileSync(resolve(here, "../lib/i18n.tsx"), "utf8");
49 const mainSource = readFileSync(resolve(here, "../main.tsx"), "utf8");
50 const stylesSource = readFileSync(resolve(here, "../styles.css"), "utf8");
51
52 console.log("\nbundle contract");
53
54 ok(
55 !/import\s+\{[^}]*\}\s+from\s+["']\.\/lib\/sessionExport["']/.test(appSource),
56 "App keeps session export code out of the initial chunk",
57 );
58 ok(
59 lazyRuntimeImport("../app-runtime/useSessionExportCommands.ts", "../lib/sessionExportOperation") &&
60 lazyRuntimeImport("../lib/sessionExportOperation.ts", "./sessionExport"),
61 "App loads session export code on demand",
62 );
63 ok(
64 !/import\s+\{[^}]*\}\s+from\s+["']\.\/components\/SettingsPanel["']/.test(appSource) &&
65 !/import\s+\{[^}]*\}\s+from\s+["']\.\/components\/HistoryPanel["']/.test(appSource),
66 "App keeps secondary drawers out of the initial chunk",
67 );
68 ok(
69 lazyRuntimeImport("../app-shell/AppOverlayHost.tsx", "../components/SettingsPanelEntry") &&
70 lazyRuntimeImport("../app-shell/AppOverlayHost.tsx", "../components/HistoryPanel"),
71 "Overlay Host owns lazy secondary drawer imports without an eager runtime edge",
72 );
73 ok(
74 !/import\s+\{\s*ProjectTree\s*\}\s+from\s+["']\.\/components\/ProjectTree["']/.test(appSource),
75 "App keeps the project tree out of the first-paint bundle",
76 );
77 ok(
78 lazyRuntimeImport("../app-shell/SidebarRegion.tsx", "../components/ProjectTree"),
79 "Sidebar Region owns the lazy project tree import without an eager runtime edge",
80 );
81 ok(
82 lazyRuntimeImport("../app-shell/WorkspaceDockRegion.tsx", "../components/BrowserPanelEntry"),
83 "Workspace dock owns the lazy browser panel import without an eager runtime edge",
84 );
85 ok(
86 lazyRuntimeImport("../lib/controllerEventRecovery.ts", "./controllerEventRecoveryWorker"),
87 "Recovery worker stays behind the synchronous resync listener without an eager runtime edge",
88 );
89 ok(
90 lazyRuntimeImport("../lib/useController.ts", "./controllerSwitchNotices") &&
91 !readFileSync(resolve(here, "../lib/bridge.ts"), "utf8").includes('from "./forkWorktree"'),
92 "Fork actions load with message actions while the bridge imports only their mock implementation",
93 );
94 ok(
95 settingsEntrySource.includes('import "./CompactRatioSettings.css"') &&
96 settingsEntrySource.includes('from "./SettingsPanel"') &&
97 !settingsSource.includes('import "./CompactRatioSettings.css"'),
98 "Settings CSS stays in the lazy production entry without breaking direct module tests",
99 );
100 ok(
101 remoteWizardEntrySource.includes('import "./RemoteConnectWizard.css"') &&
102 remoteWizardEntrySource.includes('from "./RemoteConnectWizard"') &&
103 projectCreationSource.includes('import("./RemoteConnectWizardEntry")') &&
104 !remoteWizardSource.includes('import "./RemoteConnectWizard.css"'),
105 "Remote wizard CSS stays in its lazy production entry without breaking direct module tests",
106 );
107 ok(
108 !appSource.includes("openAllHistory") &&
109 !appSource.includes("cmd-history") &&
110 !appSource.includes("sidebar.allHistory") &&
111 !projectTreeSource.includes("onOpenProjectHistory") &&
112 !projectTreeSource.includes("project-history"),
113 "App has no dedicated history-page entry points",
114 );
115 ok(
116 paletteOwnerSource.includes('id: "cmd-trash"') &&
117 historyOwnerSource.includes("openTrash") &&
118 paletteOwnerSource.includes("paletteSessions.slice(0, 12)") &&
119 projectTreeSource.includes('t("projectTree.searchPlaceholder")'),
120 "Trash and existing session search remain available",
121 );
122 ok(
123 /\.sidebar--workbench\s+\.sidebar__utility-row\s*\{[^}]*grid-template-columns:\s*repeat\(4,\s*minmax\(0,\s*1fr\)\)/s.test(stylesSource),
124 "Workbench footer distributes its four utility actions evenly",
125 );
126 ok(
127 /\.app--creation\s+\.sidebar__nav,\s*:root\[data-theme-style\]\s+\.app--creation\s+\.sidebar__nav\s*\{[^}]*grid-template-columns:\s*repeat\(3,\s*minmax\(0,\s*1fr\)\)/s.test(stylesSource),
128 "Creation footer distributes search, trash, and settings evenly",
129 );
130 ok(
131 !/import\s+\{[^}]*\b(?:MCPServersSettingsPage|SkillsSettingsPage|PluginsSettingsPage)\b[^}]*\}\s+from\s+["']\.\/CapabilitiesPanel["']/.test(settingsSource) &&
132 !/import\s+\{[^}]*\bMemorySettingsPage\b[^}]*\}\s+from\s+["']\.\/MemoryPanel["']/.test(settingsSource),
133 "SettingsPanel keeps secondary settings pages out of the first settings chunk",
134 );
135 ok(
136 settingsSource.includes('import("./CapabilitiesPanel")') &&
137 settingsSource.includes('import("./MemoryPanel")'),
138 "SettingsPanel loads secondary settings pages on demand",
139 );
140 ok(
141 !/from\s+["']qrcode\.react["']/.test(settingsSource),
142 "SettingsPanel keeps QR rendering code out of the first settings chunk",
143 );
144 ok(
145 settingsSource.includes('import("qrcode.react")'),
146 "SettingsPanel loads QR rendering code on demand",
147 );
148 ok(
149 !/from\s+["']react-markdown["']/.test(markdownSource) &&
150 !/from\s+["']remark-gfm["']/.test(markdownSource) &&
151 !/from\s+["']remark-math["']/.test(markdownSource) &&
152 !/from\s+["']rehype-katex["']/.test(markdownSource) &&
153 !/katex\/dist\/katex\.min\.css/.test(markdownSource),
154 "Markdown wrapper keeps markdown/math vendor code out of the initial chunk",
155 );
156 ok(
157 markdownSource.includes('import("./MarkdownHistory")'),
158 "Markdown wrapper loads markdown renderer on demand",
159 );
160 ok(
161 !/String\.raw`[^`]*\(\?<!/s.test(localPathLinksSource),
162 "Markdown local-path support avoids RegExp lookbehind required by newer WebKit",
163 );
164 ok(
165 !/import\s+\{\s*zh\s*\}\s+from\s+["']\.\.\/locales\/zh["']/.test(i18nSource) &&
166 !/import\s+\{\s*zhTW\s*\}\s+from\s+["']\.\.\/locales\/zh-TW["']/.test(i18nSource),
167 "i18n keeps Chinese dictionaries out of the unconditional initial chunk",
168 );
169 ok(
170 i18nSource.includes('import("../locales/zh")') &&
171 i18nSource.includes('import("../locales/zh-TW")'),
172 "i18n loads Chinese dictionaries on demand",
173 );
174 ok(
175 mainSource.includes("await preloadDetectedLocale()"),
176 "main preloads the detected locale before mounting React",
177 );
178
179 console.log(`\n${passed} passed, ${failed} failed`);
180 if (failed > 0) process.exit(1);
181
181 lines TYPESCRIPT