import { describe, expect, it } from 'vitest'; import { TRACKED_SOURCE_FILES_ID, type FileExists, type GitRunner, resolveTrackedSourceFiles, trackedSourceFilesPlugin } from './trackedSourceFiles'; /** * The population this plugin supplies is what `web/src/api/pageSizeCallSites.guard.test.ts` asserts * completeness over (#819), so the plugin is verification code and carries its own proof rather * than inheriting the guard's — `testing.verification-code-needs-its-own-proof`. */ const RESOLVED_ID = `\0${TRACKED_SOURCE_FILES_ID}`; const allPresent: FileExists = () => true; /** `ls-files` answers the tracked query; `--others` answers the untracked one. */ function runnerReturning(tracked: string, others = ''): { run: GitRunner; calls: (readonly string[])[] } { const calls: (readonly string[])[] = []; return { calls, run: (args) => { calls.push(args); return args.includes('--others') ? others : tracked; } }; } /** Drives `load()` through the plugin's own `configResolved`, as Vite does. */ function loadedBy(plugin: ReturnType, root = '/repo/web'): string | undefined { const configResolved = plugin.configResolved as (config: { root: string }) => void; configResolved.call(plugin, { root }); return (plugin.load as (id: string) => string | undefined).call(plugin, RESOLVED_ID); } describe('resolveTrackedSourceFiles (#819)', () => { it("splits git's NUL-separated output into paths, dropping the trailing empty field", () => { const { run } = runnerReturning('src/a.ts\0src/b.tsx\0'); expect(resolveTrackedSourceFiles('/repo/web', run, allPresent).tracked).toEqual(['src/a.ts', 'src/b.tsx']); }); it('reports UNTRACKED paths from a SEPARATE query, so a narrowed index cannot hide behind them', () => { // #819: every other comparison in the guard is between two things derived from // `tracked`, so a filter applied to `tracked` cancels out of all of them. `others` comes from a // different git query and is what makes that filter visible — narrowing `tracked` adds nothing // here, so the walk finds a file in neither list. const { run } = runnerReturning('src/a.ts\0', 'src/scratch.ts\0src/notes.ts\0'); const result = resolveTrackedSourceFiles('/repo/web', run, allPresent); expect(result.tracked).toEqual(['src/a.ts']); expect(result.others).toEqual(['src/scratch.ts', 'src/notes.ts']); }); it('THROWS when the untracked query fails, rather than degrading to an empty set', () => { // An empty `others` would make a narrowed index look like a real hole AND, worse, is the // direction that could be used to silence the cross-check. const run: GitRunner = (args) => { if (args.includes('--others')) throw new Error('git exploded'); return 'src/a.ts\0'; }; expect(() => resolveTrackedSourceFiles('/repo/web', run, allPresent)).toThrow( /could not enumerate untracked files/ ); }); it('asks git for NUL-separated output, scoped to src/, in the given root', () => { // Pins `-z` specifically. Without it git applies `core.quotePath` to a non-ASCII name and // returns a C-quoted string that no longer equals the path Vite reports, which would drop that // file from the population silently — the guard would then under-enumerate while looking green. const { run, calls } = runnerReturning('src/a.ts\0'); resolveTrackedSourceFiles('/repo/web', run, allPresent); expect(calls).toEqual([ ['-C', '/repo/web', 'ls-files', '-z', '--', 'src'], ['-C', '/repo/web', 'ls-files', '-z', '--others', '--', 'src'] ]); }); it('keeps a path containing spaces intact rather than splitting on whitespace', () => { const { run } = runnerReturning('src/screens/My Screen.tsx\0src/b.ts\0'); expect(resolveTrackedSourceFiles('/repo/web', run, allPresent).tracked).toEqual([ 'src/screens/My Screen.tsx', 'src/b.ts' ]); }); it('REPORTS tracked paths with no file on disk rather than dropping them', () => { // The consumer needs a mid-edit deletion (transient, tolerated) told apart from a tracked file // the walk cannot see (a real hole). Collapsing them is how a population silently shrinks. const { run } = runnerReturning('src/present.ts\0src/deleted.ts\0'); const seen: string[] = []; const exists: FileExists = (path) => { seen.push(path); return !path.endsWith('deleted.ts'); }; const result = resolveTrackedSourceFiles('/repo/web', run, exists); expect(result.tracked).toEqual(['src/present.ts', 'src/deleted.ts']); expect(result.absentFromDisk).toEqual(['src/deleted.ts']); // Existence is checked against the ROOT-joined path, not the repo-relative one. expect(seen).toEqual(['/repo/web/src/present.ts', '/repo/web/src/deleted.ts']); }); it('deduplicates a path git emits once per merge-conflict stage', () => { // `git ls-files` lists an unmerged path once per stage. Nothing downstream needs the repetition. const { run } = runnerReturning('src/a.ts\0src/a.ts\0src/b.ts\0'); expect(resolveTrackedSourceFiles('/repo/web', run, allPresent).tracked).toEqual([ 'src/a.ts', 'src/b.ts' ]); }); it('THROWS when git fails, instead of degrading to an unfiltered walk', () => { // The failure this rejects is the fail-open one: answering "could not tell" with the very // population the index was brought in to replace is a permanent no-op that reports green. const boom: GitRunner = () => { throw new Error('not a git repository'); }; expect(() => resolveTrackedSourceFiles('/repo/web', boom, allPresent)).toThrow(/could not read the git index/); }); it('THROWS when git reports zero tracked files (anti-vacuity at the source)', () => { const { run } = runnerReturning(''); expect(() => resolveTrackedSourceFiles('/repo/web', run, allPresent)).toThrow(/ZERO tracked files/); }); }); describe('trackedSourceFilesPlugin (#819)', () => { it('resolves only its own virtual id', () => { const plugin = trackedSourceFilesPlugin(runnerReturning('src/a.ts\0').run, allPresent); const resolveId = plugin.resolveId as (id: string) => string | undefined; expect(resolveId.call(plugin, TRACKED_SOURCE_FILES_ID)).toBe(RESOLVED_ID); expect(resolveId.call(plugin, 'react')).toBeUndefined(); expect(resolveId.call(plugin, './pageSizeScan')).toBeUndefined(); }); it('emits the tracked list and the absent set as a default-exported module', () => { const plugin = trackedSourceFilesPlugin(runnerReturning('src/a.ts\0src/b.tsx\0').run, allPresent); expect(loadedBy(plugin)).toBe( 'export default {"tracked":["src/a.ts","src/b.tsx"],"absentFromDisk":[],"others":[]};\n' ); }); it('REFUSES rather than guessing when configResolved never ran', () => { // A wrong root is a silent wrong answer. Seeding it with `process.cwd()` would happen to work // in this repo's layout and quietly derive the population from the wrong tree elsewhere. const { run, calls } = runnerReturning('src/a.ts\0'); const plugin = trackedSourceFilesPlugin(run, allPresent); const load = plugin.load as (id: string) => string | undefined; expect(() => load.call(plugin, RESOLVED_ID)).toThrow(/configResolved` hook never ran/); expect(calls).toHaveLength(0); }); it('does NOT shell out to git unless the virtual module is actually loaded', () => { // Constructing the plugin, or loading anything else, must not run git — otherwise registering // it in vite.config.ts would break every build in a context without a repository. const { run, calls } = runnerReturning('src/a.ts\0'); const plugin = trackedSourceFilesPlugin(run, allPresent); const configResolved = plugin.configResolved as (config: { root: string }) => void; const load = plugin.load as (id: string) => string | undefined; configResolved.call(plugin, { root: '/repo/web' }); expect(calls).toHaveLength(0); expect(load.call(plugin, '\0some-other-virtual-module')).toBeUndefined(); expect(calls).toHaveLength(0); load.call(plugin, RESOLVED_ID); // Two: the tracked query and the separate untracked one. expect(calls).toHaveLength(2); }); });