import { expect, it } from "vitest"; import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; import { completeSemanticSteps } from "./javascriptSemanticTraversal.js"; import { buildJavaScriptRouteInventorySteps, javaScriptRouteInventorySchema, type JavaScriptRouteInventoryInput, } from "./javascriptRouteInventory.js"; const source = ( key: string, text: string, url: string | null = null, ): JavaScriptRouteInventoryInput => ({ key, path: url === null ? key : null, url, sha256: "a".repeat(64), container_sha256: null, status: "complete", reason: null, facts: analyzeJavaScriptStaticSource(text).routes!, ...(url === null && key.startsWith("app/") ? { next_app_context: { kind: "app-directory" as const, directory: "app", package: { path: "package.json", sha256: "b".repeat(64), container_sha256: "c".repeat(64), }, }, } : {}), }); const build = (inputs: JavaScriptRouteInventoryInput[], bytes?: number) => javaScriptRouteInventorySchema.parse( completeSemanticSteps(buildJavaScriptRouteInventorySteps(inputs, bytes)), ); it("does not promote page-shaped paths without source-router applicability", () => { const unknown = source("app/page.js", ""); delete (unknown as { next_app_context?: unknown }).next_app_context; const result = build([unknown]); expect(result.routes).toEqual([]); expect(result.unknowns).toMatchObject([ { kind: "interpretation", reason: expect.stringContaining("not measured") }, ]); }); it("binds source identity to the package evidence establishing router applicability", () => { const first = source("app/page.js", ""); const context = first.next_app_context; if (context?.kind !== "app-directory") throw new Error("Expected declared fixture context"); const second = { ...first, next_app_context: { ...context, package: { ...context.package, sha256: "d".repeat(64) }, }, }; expect(build([first]).sources[0]!.id).not.toBe( build([second]).sources[0]!.id, ); }); it("coalesces exact patterns while retaining independent occurrences and stable identities", () => { const inputs = [ source("one.js", `router.push('/reports');`), source( "two.js", `createBrowserRouter([{path:'/reports',children:[{path:'settings'}]}]);`, ), ]; const result = build(inputs), repeated = build([...inputs].reverse()); expect(result.routes.map(({ path }) => path)).toEqual([ "/reports", "/reports/settings", ]); expect(result.routes[0]).toMatchObject({ occurrence_count: 3, source_count: 2, }); expect(result.routes.map(({ id }) => id)).toEqual( repeated.routes.map(({ id }) => id), ); expect(result.source_manifest_sha256).toBe(repeated.source_manifest_sha256); expect(result.inventory_id).toBe(repeated.inventory_id); expect(result.coverage.application_routes).toBe("unknown"); }); it("chooses deterministic primary provenance when sources have equal confidence", () => { const inputs = [ source("one.js", `router.push('/reports');`), source("two.js", `router.push('/reports');`), ]; expect(build(inputs)).toEqual(build([...inputs].reverse())); }); it("binds ordinary constants to application segments across sources and excludes framework filesystem noise", () => { const result = build([ source("navigation.js", `router.push('/reports');`), source( "runtime.js", `globalThis.TURBOPACK=[];const routes=['/reports/:id','/ROOT/node_modules/next/process/','/_not-found'];`, ), ]); expect(result.routes.map(({ path }) => path)).toEqual([ "/reports", "/reports/:id", ]); expect(result.statistics.unanchored_literals_excluded).toBe(2); }); it("retains inferred Next script bases and the unprefixed path", () => { const result = build([ source( "script", `router.push('/planner');`, "https://example.test/app/_next/static/chunks/123.js", ), ]); expect(result.routes.map(({ path }) => path)).toEqual(["/app/planner"]); expect(result.occurrences[0]).toMatchObject({ unprefixed_path: "/planner", inferred_base_path: "/app", confidence: "low", }); expect(result.sources[0]!.url).toBe( "https://example.test/app/_next/static/chunks/123.js", ); expect(result.unknowns.some(({ kind }) => kind === "inferred-base")).toBe( true, ); }); it("reports missing source facts as partial and preserves filename-only discoveries without fake AST ranges", () => { const result = build([ { ...source("app/reports/[id]/page.js", ""), status: "unavailable", reason: "source was not selected", facts: null, }, ]); expect(result.coverage).toMatchObject({ source_parsing: "partial", candidate_enumeration: "partial", application_routes: "unknown", }); expect(result.routes[0]!.path).toBe("/reports/:id"); expect(result.occurrences[0]).toMatchObject({ origin: "source-path", source_range: null, }); }); it("does not interpret the URL authority or file URL prefix as an App Router directory or routing base", () => { const result = build([ source("script", `router.push('/reports');`, "https://app/reports/page.js"), ]); expect(result.routes.map(({ path }) => path)).toEqual(["/reports"]); const local = build([ source("file", `router.push('/reports');`, "file:///tmp/_next/123.js"), ]); expect(local.sources[0]!.base_path).toBeNull(); expect(local.routes[0]!.path).toBe("/reports"); }); it("rejects forged aggregate counts and detached provenance", () => { const result = build([source("one.js", `router.push('/reports');`)]); expect( javaScriptRouteInventorySchema.safeParse({ ...result, routes: result.routes.map((route) => ({ ...route, occurrence_count: 99, })), }).success, ).toBe(false); expect( javaScriptRouteInventorySchema.safeParse({ ...result, sources: [] }) .success, ).toBe(false); }); it("retains a bounded prefix with explicit capacity and counts only retained facts", () => { const inputs = [ source( "many.js", Array.from( { length: 100 }, (_, index) => `router.push('/reports/${index}');`, ).join("\n"), ), ]; const result = build(inputs, 8192); expect(result.routes.length).toBeGreaterThan(0); expect(result.routes.length).toBeLessThan(100); expect(result.coverage.candidate_enumeration).toBe("partial"); expect(result.capacity?.limit_bytes).toBe(8192); expect(Buffer.byteLength(JSON.stringify(result))).toBeLessThanOrEqual(8192); expect(() => build(inputs, 1)).toThrow(RangeError); }); it("anchors generic constants before applying an inferred asset base", () => { const result = build([ source( "prefixed.js", `globalThis.TURBOPACK=[];createBrowserRouter([{path:'/reports'}]);const paths=['/reports/:id','/noise'];`, "https://example.test/app/_next/static/chunks/one.js", ), ]); expect(result.routes.map((row) => row.path)).toEqual([ "/app/reports", "/app/reports/:id", ]); expect(result.statistics.unanchored_literals_excluded).toBe(1); }); it("classifies typed page configuration while generic server route calls remain unknown", () => { const result = build([ source( "typed.js", `defineRoutes({reports:{_$:'reports',settings:{_$:'settings'}}});app.route('/api/users');`, ), ]); expect( result.routes .filter((row) => row.classification === "page") .map((row) => row.path), ).toEqual(["/reports", "/reports/settings"]); expect(result.routes.find((row) => row.path === "/api/users")).toMatchObject({ classification: "unknown", classification_basis: "unknown", }); }); it("keeps TypeScript page filename provenance separate from syntax ranges", () => { const result = build([ source( "app/(account)/reports/[id]/page.tsx", "export default function Page() { return null; }", ), ]); expect(result.routes).toHaveLength(1); expect(result.routes[0]).toMatchObject({ path: "/reports/:id", classification: "page", classification_basis: "next-app-page", }); expect(result.occurrences[0]).toMatchObject({ origin: "source-path", source_range: null, }); }); it("coalesces inferred deployment roots without rewriting explicit trailing slashes", () => { const result = build([ source( "root-call", "router.push('/');", "https://example.test/app/_next/static/chunks/runtime.js", ), source( "root-page", "", "https://example.test/app/_next/static/chunks/app/page.tsx", ), source("explicit.js", "router.push('/app');router.push('/app/');"), ]); expect(result.routes.map(({ path }) => path)).toEqual(["/app", "/app/"]); const root = result.routes.find(({ path }) => path === "/app")!; expect(root.source_count).toBe(3); const page = result.occurrences.find(({ origin }) => origin === "source-url"); expect(page).toMatchObject({ route_id: root.id, unprefixed_path: "/", inferred_base_path: "/app", source_range: null, }); expect(result.unknowns.some(({ kind }) => kind === "inferred-base")).toBe( true, ); }); it("keeps enumeration partial when a recognized navigation target is unresolved", () => { const result = build([source("navigation.js", "router.navigate(target);")]); expect(result.routes).toEqual([]); expect(result.coverage.candidate_enumeration).toBe("partial"); expect(result.unknowns).toEqual([ expect.objectContaining({ kind: "interpretation", source_id: result.sources[0]!.id, }), ]); }); it.each(["/app/reports", "/reports"])( "matches based and unbased constants against application segments for %s", (anchor) => { const result = build([ source( "mixed.js", `globalThis.TURBOPACK=[];router.push('${anchor}');const paths=['/reports/settings','/app/reports/history','/noise','/app/noise','/ROOT/node_modules/framework'];`, "https://example.test/app/_next/static/chunks/one.js", ), ]); expect(result.routes.map(({ path }) => path)).toEqual([ "/app/reports", "/app/reports/history", "/app/reports/settings", ]); expect(result.occurrences).toEqual( expect.arrayContaining([ expect.objectContaining({ unprefixed_path: "/reports/settings", inferred_base_path: "/app", }), expect.objectContaining({ unprefixed_path: "/app/reports/history", inferred_base_path: null, }), ]), ); }, ); it("keeps filename and module-derived anchors application-relative when a deployment base matches an application segment", () => { const result = build([ source( "page", "", "https://example.test/app/_next/static/chunks/app/app/page-abc.js", ), source( "root-runtime", "globalThis.TURBOPACK=[];const paths=['/app/settings','/noise'];", "https://example.test/_next/static/chunks/root.js", ), source( "module-runtime", "globalThis.TURBOPACK=[];const modulePath='app/app/page.js';", "https://example.test/app/_next/static/chunks/runtime.js", ), ]); expect(result.routes.map(({ path }) => path)).toEqual([ "/app/app", "/app/settings", ]); const page = result.routes.find(({ path }) => path === "/app/app")!; expect(page.source_count).toBe(2); for (const origin of ["source-url", "source-syntax"]) expect(result.occurrences).toEqual( expect.arrayContaining([ expect.objectContaining({ route_id: page.id, origin, unprefixed_path: "/app", inferred_base_path: "/app", }), ]), ); expect(result.statistics.unanchored_literals_excluded).toBe(1); }); it("reports generated typed configurations as partial even without retained candidates", () => { const result = build([ source("generated.js", "defineRoutes({reports:buildRoutes()});"), ]); expect(result.routes).toEqual([]); expect(result.coverage.candidate_enumeration).toBe("partial"); expect(result.unknowns).toHaveLength(1); expect(result.unknowns[0]!.kind).toBe("interpretation"); }); it.each([ ["app/feed/(.)photo/page.tsx", null], [ "page.js", "https://example.test/_next/static/chunks/app/feed/(.)photo/page-123.js", ], ] as const)( "retains intercepted filename metadata as partial: %s", (path, url) => { const result = build([ source(path, "export default function Photo(){return null;}", url), ]); expect(result.routes).toEqual([]); expect(result.coverage.candidate_enumeration).toBe("partial"); expect(result.unknowns).toEqual( expect.arrayContaining([ expect.objectContaining({ kind: "interpretation", reason: expect.stringContaining("intercepted"), source_range: null, }), ]), ); }, );