@skyramp/mcp 0.1.8 → 0.2.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/build/index.js +4 -2
  2. package/build/playwright/registerPlaywrightTools.js +12 -0
  3. package/build/playwright/traceRecordingPrompt.js +15 -0
  4. package/build/prompts/code-reuse.js +106 -7
  5. package/build/prompts/pom-aware-code-reuse.js +106 -7
  6. package/build/prompts/startTraceCollectionPrompts.js +37 -15
  7. package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -31
  8. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +40 -1
  9. package/build/prompts/test-maintenance/driftAnalysisSections.js +90 -86
  10. package/build/prompts/test-recommendation/analysisOutputPrompt.js +286 -163
  11. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -45
  12. package/build/prompts/test-recommendation/diffExecutionPlan.js +246 -117
  13. package/build/prompts/test-recommendation/promptPlan.js +290 -0
  14. package/build/prompts/test-recommendation/promptPlan.test.js +336 -0
  15. package/build/prompts/test-recommendation/recommendationSections.js +4 -3
  16. package/build/prompts/test-recommendation/recommendationShared.js +23 -1
  17. package/build/prompts/test-recommendation/scopeAssessment.js +65 -14
  18. package/build/prompts/test-recommendation/scopeAssessment.test.js +93 -2
  19. package/build/prompts/test-recommendation/test-recommendation-prompt.js +36 -12
  20. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +316 -1
  21. package/build/prompts/testbot/testbot-prompts.js +73 -13
  22. package/build/prompts/testbot/testbot-prompts.test.js +114 -1
  23. package/build/resources/testbotResource.js +1 -1
  24. package/build/services/ScenarioGenerationService.integration.test.js +158 -0
  25. package/build/services/ScenarioGenerationService.js +47 -4
  26. package/build/services/ScenarioGenerationService.test.js +158 -22
  27. package/build/services/TestExecutionService.js +73 -15
  28. package/build/services/TestExecutionService.test.js +105 -0
  29. package/build/services/TestGenerationService.js +11 -1
  30. package/build/tools/executeSkyrampTestTool.js +1 -10
  31. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
  32. package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
  33. package/build/tools/generate-tests/generateUIRestTool.js +2 -0
  34. package/build/tools/test-management/actionsTool.js +152 -63
  35. package/build/tools/test-management/analyzeChangesTool.js +178 -64
  36. package/build/tools/test-management/analyzeChangesTool.test.js +103 -16
  37. package/build/tools/test-management/analyzeTestHealthTool.js +30 -81
  38. package/build/tools/test-management/index.js +1 -0
  39. package/build/tools/test-management/uiAnalyzeChangesTool.js +149 -0
  40. package/build/tools/test-management/uiAnalyzeChangesTool.test.js +100 -0
  41. package/build/tools/trace/resolveSaveStoragePath.js +16 -0
  42. package/build/tools/trace/resolveSaveStoragePath.test.js +17 -0
  43. package/build/tools/trace/resolveSessionPaths.js +39 -0
  44. package/build/tools/trace/resolveSessionPaths.test.js +103 -0
  45. package/build/tools/trace/sessionState.js +14 -0
  46. package/build/tools/trace/sessionState.test.js +17 -0
  47. package/build/tools/trace/startTraceCollectionTool.js +84 -14
  48. package/build/tools/trace/stopTraceCollectionTool.js +9 -2
  49. package/build/types/TestAnalysis.js +50 -0
  50. package/build/types/TestRecommendation.js +6 -58
  51. package/build/types/TestTypes.js +1 -1
  52. package/build/utils/AnalysisStateManager.js +22 -11
  53. package/build/utils/branchDiff.js +11 -2
  54. package/build/utils/docker.test.js +1 -1
  55. package/build/utils/gitStaging.js +52 -3
  56. package/build/utils/gitStaging.test.js +19 -1
  57. package/build/utils/repoScanner.js +18 -10
  58. package/build/utils/repoScanner.test.js +92 -0
  59. package/build/utils/routeParsers.js +180 -25
  60. package/build/utils/routeParsers.test.js +180 -1
  61. package/build/utils/scenarioDrafting.js +220 -17
  62. package/build/utils/scenarioDrafting.test.js +182 -9
  63. package/build/utils/sourceRouteExtractor.js +806 -0
  64. package/build/utils/sourceRouteExtractor.test.js +565 -0
  65. package/build/utils/uiPageEnumerator.js +319 -0
  66. package/build/utils/uiPageEnumerator.test.js +422 -0
  67. package/build/utils/utils.js +27 -0
  68. package/build/utils/versions.js +1 -1
  69. package/build/utils/workspaceAuth.js +33 -4
  70. package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
  71. package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
  72. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1210 -0
  73. package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
  74. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
  75. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
  76. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +254 -0
  77. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +304 -0
  78. package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
  79. package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
  80. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
  81. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
  82. package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
  83. package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
  84. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
  85. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
  86. package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
  87. package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
  88. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
  89. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
  90. package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.js +150 -0
  91. package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.test.js +470 -0
  92. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
  93. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
  94. package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
  95. package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
  96. package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
  97. package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
  98. package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
  99. package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
  100. package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
  101. package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
  102. package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
  103. package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
  104. package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
  105. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +146 -0
  106. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +140 -0
  107. package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
  108. package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
  109. package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
  110. package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
  111. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
  112. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
  113. package/node_modules/playwright/package.json +1 -1
  114. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
  115. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.4.tgz +0 -0
  116. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.5.tgz +0 -0
  117. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz +0 -0
  118. package/package.json +3 -3
  119. package/build/services/TestHealthService.js +0 -694
  120. package/build/services/TestHealthService.test.js +0 -241
  121. package/build/types/TestDriftAnalysis.js +0 -1
  122. package/build/types/TestHealth.js +0 -4
@@ -0,0 +1,565 @@
1
+ // No external mocks needed — the extractor is pure fs + TS Compiler API.
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+ import { extractSourceRoutes } from "./sourceRouteExtractor.js";
6
+ function makeTmpRepo(files) {
7
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-route-test-"));
8
+ for (const f of files) {
9
+ const fullPath = path.join(dir, f.path);
10
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
11
+ fs.writeFileSync(fullPath, f.content);
12
+ }
13
+ return dir;
14
+ }
15
+ function cleanup(dir) {
16
+ fs.rmSync(dir, { recursive: true, force: true });
17
+ }
18
+ // ── Tests ──────────────────────────────────────────────────────────────────
19
+ describe("extractSourceRoutes", () => {
20
+ it("returns empty when no router files found", () => {
21
+ const repo = makeTmpRepo([
22
+ {
23
+ path: "src/components/Button.tsx",
24
+ content: "export default function Button() { return <button/>; }",
25
+ },
26
+ ]);
27
+ expect(extractSourceRoutes(repo)).toEqual([]);
28
+ cleanup(repo);
29
+ });
30
+ it("extracts a single React Router Route with element prop", () => {
31
+ const repo = makeTmpRepo([
32
+ {
33
+ path: "src/App.tsx",
34
+ content: `
35
+ import { Routes, Route } from 'react-router-dom';
36
+ import Cart from './pages/Cart';
37
+
38
+ export default function App() {
39
+ return (
40
+ <Routes>
41
+ <Route path="/cart" element={<Cart />} />
42
+ </Routes>
43
+ );
44
+ }
45
+ `,
46
+ },
47
+ {
48
+ path: "src/pages/Cart.tsx",
49
+ content: "export default function Cart() { return <div/>; }",
50
+ },
51
+ ]);
52
+ const routes = extractSourceRoutes(repo);
53
+ expect(routes).toHaveLength(1);
54
+ expect(routes[0]).toMatchObject({
55
+ path: "/cart",
56
+ componentName: "Cart",
57
+ });
58
+ expect(routes[0].componentFile).toMatch(/src\/pages\/Cart\.tsx$/);
59
+ cleanup(repo);
60
+ });
61
+ it("extracts multiple Routes from one file", () => {
62
+ const repo = makeTmpRepo([
63
+ {
64
+ path: "src/App.tsx",
65
+ content: `
66
+ import { Routes, Route } from 'react-router-dom';
67
+ import Cart from './pages/Cart';
68
+ import Orders from './pages/Orders';
69
+ import ProductDetail from './pages/ProductDetail';
70
+
71
+ export default function App() {
72
+ return (
73
+ <Routes>
74
+ <Route path="/cart" element={<Cart />} />
75
+ <Route path="/orders" element={<Orders />} />
76
+ <Route path="/products/:id" element={<ProductDetail />} />
77
+ </Routes>
78
+ );
79
+ }
80
+ `,
81
+ },
82
+ ]);
83
+ const routes = extractSourceRoutes(repo);
84
+ expect(routes).toHaveLength(3);
85
+ expect(routes.map((r) => r.path).sort()).toEqual([
86
+ "/cart",
87
+ "/orders",
88
+ "/products/:id",
89
+ ]);
90
+ cleanup(repo);
91
+ });
92
+ it("extracts Route with component prop (alternate syntax)", () => {
93
+ const repo = makeTmpRepo([
94
+ {
95
+ path: "src/App.tsx",
96
+ content: `
97
+ import { Route } from 'react-router-dom';
98
+ import Cart from './pages/Cart';
99
+
100
+ export default function App() {
101
+ return <Route path="/cart" component={Cart} />;
102
+ }
103
+ `,
104
+ },
105
+ ]);
106
+ const routes = extractSourceRoutes(repo);
107
+ expect(routes).toHaveLength(1);
108
+ expect(routes[0].path).toBe("/cart");
109
+ expect(routes[0].componentName).toBe("Cart");
110
+ });
111
+ it("recognizes SentryRoute and other configured wrapper components", () => {
112
+ const repo = makeTmpRepo([
113
+ {
114
+ path: "src/AppRouter.tsx",
115
+ content: `
116
+ import { Switch } from 'react-router-dom';
117
+ import { SentryRoute } from './components/SentryRoute';
118
+ import Workspaces from './pages/Workspaces';
119
+
120
+ export default function AppRouter() {
121
+ return (
122
+ <Switch>
123
+ <SentryRoute path="/workspaces" component={Workspaces} />
124
+ </Switch>
125
+ );
126
+ }
127
+ `,
128
+ },
129
+ ]);
130
+ const routes = extractSourceRoutes(repo);
131
+ expect(routes).toHaveLength(1);
132
+ expect(routes[0].path).toBe("/workspaces");
133
+ expect(routes[0].componentName).toBe("Workspaces");
134
+ });
135
+ it("resolves identifier-valued path attribute via local const (basic case)", () => {
136
+ const repo = makeTmpRepo([
137
+ {
138
+ path: "src/App.tsx",
139
+ content: `
140
+ import { Route } from 'react-router-dom';
141
+ import Workspaces from './pages/Workspaces';
142
+
143
+ const WORKSPACE_URL = "/workspace";
144
+
145
+ export default function App() {
146
+ return <Route path={WORKSPACE_URL} element={<Workspaces />} />;
147
+ }
148
+ `,
149
+ },
150
+ ]);
151
+ const routes = extractSourceRoutes(repo);
152
+ expect(routes).toHaveLength(1);
153
+ expect(routes[0].path).toBe("/workspace");
154
+ });
155
+ it("skips Route with template literal path whose parts are not module-scoped", () => {
156
+ // Const declarations inside a function aren't visible to the resolver
157
+ // (we only walk module-scope consts). The route is correctly skipped.
158
+ const repo = makeTmpRepo([
159
+ {
160
+ path: "src/App.tsx",
161
+ content: `
162
+ import { Route } from 'react-router-dom';
163
+ import Foo from './pages/Foo';
164
+
165
+ export default function App() {
166
+ const prefix = '/admin';
167
+ return <Route path={\`\${prefix}/foo\`} element={<Foo />} />;
168
+ }
169
+ `,
170
+ },
171
+ ]);
172
+ const routes = extractSourceRoutes(repo);
173
+ expect(routes).toEqual([]);
174
+ });
175
+ it("skips Route without a path attribute", () => {
176
+ const repo = makeTmpRepo([
177
+ {
178
+ path: "src/App.tsx",
179
+ content: `
180
+ import { Route } from 'react-router-dom';
181
+ import Foo from './pages/Foo';
182
+
183
+ export default function App() {
184
+ return <Route element={<Foo />} />;
185
+ }
186
+ `,
187
+ },
188
+ ]);
189
+ const routes = extractSourceRoutes(repo);
190
+ expect(routes).toEqual([]);
191
+ });
192
+ it("resolves component file path via relative import", () => {
193
+ const repo = makeTmpRepo([
194
+ {
195
+ path: "src/App.tsx",
196
+ content: `
197
+ import { Route } from 'react-router-dom';
198
+ import Cart from './pages/Cart';
199
+
200
+ export default function App() {
201
+ return <Route path="/cart" element={<Cart />} />;
202
+ }
203
+ `,
204
+ },
205
+ {
206
+ path: "src/pages/Cart.tsx",
207
+ content: "export default function Cart() { return <div/>; }",
208
+ },
209
+ ]);
210
+ const routes = extractSourceRoutes(repo);
211
+ expect(routes[0].componentFile).toMatch(/src\/pages\/Cart\.tsx$/);
212
+ cleanup(repo);
213
+ });
214
+ it("leaves componentFile undefined when import not found", () => {
215
+ const repo = makeTmpRepo([
216
+ {
217
+ path: "src/App.tsx",
218
+ content: `
219
+ import { Route } from 'react-router-dom';
220
+ // Cart referenced but not imported in this file
221
+ export default function App() {
222
+ return <Route path="/cart" element={<Cart />} />;
223
+ }
224
+ `,
225
+ },
226
+ ]);
227
+ const routes = extractSourceRoutes(repo);
228
+ expect(routes).toHaveLength(1);
229
+ expect(routes[0].componentName).toBe("Cart");
230
+ expect(routes[0].componentFile).toBeUndefined();
231
+ cleanup(repo);
232
+ });
233
+ it("ignores non-Route JSX elements", () => {
234
+ const repo = makeTmpRepo([
235
+ {
236
+ path: "src/App.tsx",
237
+ content: `
238
+ import { Routes, Route, Link } from 'react-router-dom';
239
+ import Cart from './pages/Cart';
240
+
241
+ export default function App() {
242
+ return (
243
+ <Routes>
244
+ <Link to="/somewhere">Click</Link>
245
+ <Route path="/cart" element={<Cart />} />
246
+ </Routes>
247
+ );
248
+ }
249
+ `,
250
+ },
251
+ ]);
252
+ const routes = extractSourceRoutes(repo);
253
+ expect(routes).toHaveLength(1);
254
+ expect(routes[0].path).toBe("/cart");
255
+ cleanup(repo);
256
+ });
257
+ it("discovers routes across multiple candidate router files", () => {
258
+ const repo = makeTmpRepo([
259
+ {
260
+ path: "src/App.tsx",
261
+ content: `
262
+ import { Route } from 'react-router-dom';
263
+ import Foo from './pages/Foo';
264
+ export default function App() {
265
+ return <Route path="/foo" element={<Foo />} />;
266
+ }
267
+ `,
268
+ },
269
+ {
270
+ path: "src/router.tsx",
271
+ content: `
272
+ import { Route } from 'react-router-dom';
273
+ import Bar from './pages/Bar';
274
+ export const Bar2 = <Route path="/bar" element={<Bar />} />;
275
+ `,
276
+ },
277
+ ]);
278
+ const routes = extractSourceRoutes(repo);
279
+ expect(routes.map((r) => r.path).sort()).toEqual(["/bar", "/foo"]);
280
+ cleanup(repo);
281
+ });
282
+ it("resolves import with explicit .tsx extension", () => {
283
+ const repo = makeTmpRepo([
284
+ {
285
+ path: "src/App.tsx",
286
+ content: `
287
+ import { Route } from 'react-router-dom';
288
+ import Cart from './pages/Cart.tsx';
289
+ export default function App() {
290
+ return <Route path="/cart" element={<Cart />} />;
291
+ }
292
+ `,
293
+ },
294
+ {
295
+ path: "src/pages/Cart.tsx",
296
+ content: "export default function Cart() { return <div/>; }",
297
+ },
298
+ ]);
299
+ const routes = extractSourceRoutes(repo);
300
+ expect(routes[0].componentFile).toMatch(/src\/pages\/Cart\.tsx$/);
301
+ cleanup(repo);
302
+ });
303
+ // ── Slice 3b: constant resolution ─────────────────────────────────────────
304
+ it("resolves identifier-valued path via cross-file import (named)", () => {
305
+ const repo = makeTmpRepo([
306
+ {
307
+ path: "src/constants/routes.ts",
308
+ content: `
309
+ export const APPLICATIONS_URL = "/applications";
310
+ export const WORKSPACE_URL = "/workspace";
311
+ `,
312
+ },
313
+ {
314
+ path: "src/AppRouter.tsx",
315
+ content: `
316
+ import { Route } from 'react-router-dom';
317
+ import { APPLICATIONS_URL } from './constants/routes';
318
+ import Apps from './pages/Apps';
319
+
320
+ export default function AppRouter() {
321
+ return <Route path={APPLICATIONS_URL} element={<Apps />} />;
322
+ }
323
+ `,
324
+ },
325
+ ]);
326
+ const routes = extractSourceRoutes(repo);
327
+ expect(routes).toHaveLength(1);
328
+ expect(routes[0].path).toBe("/applications");
329
+ cleanup(repo);
330
+ });
331
+ it("resolves identifier-valued path via DEFAULT import (export default)", () => {
332
+ // Regression: default imports were recorded with exportedName=null and
333
+ // resolved with `name` (the local identifier) — but the resolver only
334
+ // looks up named exports, so default-exported path constants never resolved.
335
+ // Fix: mark default imports as exportedName="default" and handle
336
+ // `export default <expr>` in the resolver.
337
+ const repo = makeTmpRepo([
338
+ {
339
+ path: "src/constants/orders.ts",
340
+ content: `export default "/orders";`,
341
+ },
342
+ {
343
+ path: "src/AppRouter.tsx",
344
+ content: `
345
+ import { Route } from 'react-router-dom';
346
+ import ORDERS_URL from './constants/orders';
347
+ import Orders from './pages/Orders';
348
+
349
+ export default function AppRouter() {
350
+ return <Route path={ORDERS_URL} element={<Orders />} />;
351
+ }
352
+ `,
353
+ },
354
+ ]);
355
+ const routes = extractSourceRoutes(repo);
356
+ expect(routes).toHaveLength(1);
357
+ expect(routes[0].path).toBe("/orders");
358
+ cleanup(repo);
359
+ });
360
+ it("resolves identifier-valued path via DEFAULT import (export { X as default })", () => {
361
+ const repo = makeTmpRepo([
362
+ {
363
+ path: "src/constants/cart.ts",
364
+ content: `
365
+ const CART_URL = "/cart";
366
+ export { CART_URL as default };
367
+ `,
368
+ },
369
+ {
370
+ path: "src/AppRouter.tsx",
371
+ content: `
372
+ import { Route } from 'react-router-dom';
373
+ import CART_URL from './constants/cart';
374
+ import Cart from './pages/Cart';
375
+
376
+ export default function AppRouter() {
377
+ return <Route path={CART_URL} element={<Cart />} />;
378
+ }
379
+ `,
380
+ },
381
+ ]);
382
+ const routes = extractSourceRoutes(repo);
383
+ expect(routes).toHaveLength(1);
384
+ expect(routes[0].path).toBe("/cart");
385
+ cleanup(repo);
386
+ });
387
+ it("rejects absolute import specs (path-traversal hardening)", () => {
388
+ // A hostile repo could write `import X from "/etc/passwd"` and trigger
389
+ // arbitrary host-path file reads during route extraction. The collector
390
+ // skips absolute import specs entirely. Verify by writing an import that
391
+ // *would* otherwise resolve to outside repositoryPath if absolute paths
392
+ // were honored.
393
+ const repo = makeTmpRepo([
394
+ {
395
+ path: "src/constants/bad.ts",
396
+ content: `export const BAD = "/should-not-resolve";`,
397
+ },
398
+ {
399
+ path: "src/AppRouter.tsx",
400
+ content: `
401
+ import { Route } from 'react-router-dom';
402
+ import { BAD } from "/etc/passwd";
403
+ import Page from './pages/Page';
404
+
405
+ export default function AppRouter() {
406
+ return <Route path={BAD} element={<Page />} />;
407
+ }
408
+ `,
409
+ },
410
+ ]);
411
+ const routes = extractSourceRoutes(repo);
412
+ // The path constant doesn't resolve (absolute import rejected), so the
413
+ // identifier remains unresolved and the route is skipped. Zero routes.
414
+ expect(routes).toHaveLength(0);
415
+ cleanup(repo);
416
+ });
417
+ it("resolves template literal that references another resolvable constant", () => {
418
+ const repo = makeTmpRepo([
419
+ {
420
+ path: "src/constants/routes.ts",
421
+ content: `
422
+ export const USER_AUTH_URL = "/user";
423
+ export const AUTH_LOGIN_URL = \`\${USER_AUTH_URL}/login\`;
424
+ `,
425
+ },
426
+ {
427
+ path: "src/AppRouter.tsx",
428
+ content: `
429
+ import { Route } from 'react-router-dom';
430
+ import { AUTH_LOGIN_URL } from './constants/routes';
431
+ import Login from './pages/Login';
432
+
433
+ export default function AppRouter() {
434
+ return <Route path={AUTH_LOGIN_URL} element={<Login />} />;
435
+ }
436
+ `,
437
+ },
438
+ ]);
439
+ const routes = extractSourceRoutes(repo);
440
+ expect(routes).toHaveLength(1);
441
+ expect(routes[0].path).toBe("/user/login");
442
+ cleanup(repo);
443
+ });
444
+ it("skips identifier path when constant cannot be resolved", () => {
445
+ const repo = makeTmpRepo([
446
+ {
447
+ path: "src/AppRouter.tsx",
448
+ content: `
449
+ import { Route } from 'react-router-dom';
450
+ import { UNKNOWN_URL } from 'some-package';
451
+ import Foo from './pages/Foo';
452
+
453
+ export default function AppRouter() {
454
+ return <Route path={UNKNOWN_URL} element={<Foo />} />;
455
+ }
456
+ `,
457
+ },
458
+ ]);
459
+ const routes = extractSourceRoutes(repo);
460
+ expect(routes).toEqual([]);
461
+ cleanup(repo);
462
+ });
463
+ // ── Slice 3b: Vue Router object literals ──────────────────────────────────
464
+ it("extracts Vue Router routes from object-literal array", () => {
465
+ const repo = makeTmpRepo([
466
+ {
467
+ path: "src/router.ts",
468
+ content: `
469
+ import { createRouter } from 'vue-router';
470
+ import Login from '@/routes/login.vue';
471
+ import Dashboard from '@/routes/dashboard.vue';
472
+
473
+ export const defaultRoutes = [
474
+ { name: 'login', path: '/login', component: Login },
475
+ { name: 'dashboard', path: '/dashboard', component: Dashboard },
476
+ ];
477
+ `,
478
+ },
479
+ ]);
480
+ const routes = extractSourceRoutes(repo);
481
+ expect(routes.map((r) => r.path).sort()).toEqual(["/dashboard", "/login"]);
482
+ cleanup(repo);
483
+ });
484
+ it("ignores object literals that aren't route declarations", () => {
485
+ const repo = makeTmpRepo([
486
+ {
487
+ path: "src/router.ts",
488
+ content: `
489
+ const config = { path: '/somewhere', enabled: true };
490
+ const settings = { timeout: 30 };
491
+ `,
492
+ },
493
+ ]);
494
+ const routes = extractSourceRoutes(repo);
495
+ expect(routes).toEqual([]);
496
+ cleanup(repo);
497
+ });
498
+ // ── Slice 3b: defineModule aggregation ────────────────────────────────────
499
+ it("aggregates defineModule routes with module id as URL prefix", () => {
500
+ const repo = makeTmpRepo([
501
+ {
502
+ path: "src/modules/notifications/index.ts",
503
+ content: `
504
+ import { defineModule } from '@directus/extensions';
505
+ import NotificationsCollection from './routes/collection.vue';
506
+
507
+ export default defineModule({
508
+ id: 'notifications',
509
+ name: 'Notifications',
510
+ routes: [
511
+ { name: 'notifications-collection', path: '', component: NotificationsCollection },
512
+ ],
513
+ });
514
+ `,
515
+ },
516
+ ]);
517
+ const routes = extractSourceRoutes(repo);
518
+ expect(routes).toHaveLength(1);
519
+ expect(routes[0].path).toBe("/notifications");
520
+ cleanup(repo);
521
+ });
522
+ it("aggregates defineModule routes with nested path", () => {
523
+ const repo = makeTmpRepo([
524
+ {
525
+ path: "src/modules/users/index.ts",
526
+ content: `
527
+ import { defineModule } from '@directus/extensions';
528
+ import UserDetail from './routes/detail.vue';
529
+
530
+ export default defineModule({
531
+ id: 'users',
532
+ routes: [
533
+ { name: 'user-detail', path: ':id', component: UserDetail },
534
+ ],
535
+ });
536
+ `,
537
+ },
538
+ ]);
539
+ const routes = extractSourceRoutes(repo);
540
+ expect(routes).toHaveLength(1);
541
+ expect(routes[0].path).toBe("/users/:id");
542
+ cleanup(repo);
543
+ });
544
+ it("resolves import that is a directory with index.tsx", () => {
545
+ const repo = makeTmpRepo([
546
+ {
547
+ path: "src/App.tsx",
548
+ content: `
549
+ import { Route } from 'react-router-dom';
550
+ import Cart from './pages/Cart';
551
+ export default function App() {
552
+ return <Route path="/cart" element={<Cart />} />;
553
+ }
554
+ `,
555
+ },
556
+ {
557
+ path: "src/pages/Cart/index.tsx",
558
+ content: "export default function Cart() { return <div/>; }",
559
+ },
560
+ ]);
561
+ const routes = extractSourceRoutes(repo);
562
+ expect(routes[0].componentFile).toMatch(/src\/pages\/Cart\/index\.tsx$/);
563
+ cleanup(repo);
564
+ });
565
+ });