import { execFileSync } from 'node:child_process'; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterAll, describe, expect, it } from 'vitest'; import { type GitRunner, resolveTrackedSourceFiles } from './trackedSourceFiles'; /** * SEPARATE FILE because these are the only tests in the SPA suite that need the `git` BINARY but NO * CHECKOUT — they build their own repository (ersatztv#819). That is not the same as saying the * guard needs only a checkout: it needs a checkout AND, through it, the binary. Measured — with * `.git` present and git off `PATH`, `pageSizeCallSites.guard.test.ts` still fails with the * plugin's refusal. Reading the two exclusions as checkout-versus-binary is what broke the Docker * stage once already; see `docker/Dockerfile`, which retracts that framing. * * Keeping these apart from `trackedSourceFiles.test.ts` confines the git prerequisite to one file: * its thirteen injected-runner siblings run with no git and no repository. The suite as a whole is * therefore run only where git is present. `docker/Dockerfile`'s web-build stage is not such a * place — `node:22-bookworm-slim` ships no git at all — and since ersatztv#887 it does not run the * suite there rather than naming the files that cannot run, which was a list nothing derived. * * Do NOT make these skip when git is missing. A check that answers "could not tell" by passing is a * permanent no-op, which is the whole failure mode this plugin exists to avoid. */ describe('resolveTrackedSourceFiles against a REAL repository (#819)', () => { // Every test in the sibling `trackedSourceFiles.test.ts` injects a fake `GitRunner`, which proves // the derivation's SHAPE and never that // it works against git. `scripts/tests/test_guard_populations_derive_from_git.py` makes exactly // this point about its own primitive — "It proves the mechanism by EXECUTING it rather than by // recognising its shape — no monkeypatching, no stand-in for git" — and the population this // plugin feeds is asserted for completeness, so it earns the same treatment. const repos: string[] = []; afterAll(() => { for (const dir of repos) { rmSync(dir, { recursive: true, force: true }); } }); function repoWithOneTrackedAndOneUntracked(): string { const dir = mkdtempSync(join(tmpdir(), 'etv-819-')); repos.push(dir); const git = (...args: string[]) => execFileSync('git', ['-C', dir, ...args], { encoding: 'utf8' }); git('init', '-q'); mkdirSync(join(dir, 'src'), { recursive: true }); writeFileSync(join(dir, 'src', 'tracked.ts'), 'export const a = 1;\n'); writeFileSync(join(dir, 'src', 'untracked.ts'), 'export const b = 2;\n'); // STAGED, not committed. `ls-files` reads the INDEX, so a commit adds nothing — and committing // makes the fixture depend on the developer's global git config: under a `commit.gpgsign = true` // that many people set, `git commit` fails and these tests go red on their machine while CI // stays green. That is the very shape #819 exists to remove, so it must not be reintroduced by // #819's own tests. `git add` is unaffected by signing. git('add', 'src/tracked.ts'); return dir; } it('really separates a tracked file from an identical untracked sibling', () => { const result = resolveTrackedSourceFiles(repoWithOneTrackedAndOneUntracked()); expect(result.tracked).toEqual(['src/tracked.ts']); expect(result.others).toEqual(['src/untracked.ts']); expect(result.absentFromDisk).toEqual([]); }); it('really reports a tracked file deleted from the working tree as absent', () => { const dir = repoWithOneTrackedAndOneUntracked(); rmSync(join(dir, 'src', 'tracked.ts')); const result = resolveTrackedSourceFiles(dir); expect(result.tracked).toEqual(['src/tracked.ts']); expect(result.absentFromDisk).toEqual(['src/tracked.ts']); }); // Residual: this asserts the message the MISSING-BINARY path also emits, so in an environment with // no git it passes for the wrong reason. Not silent overall — its two siblings fail ENOENT there, // loudly. Since ersatztv#887 no such environment runs this suite: `docker/Dockerfile` stopped // running it rather than excluding the files that cannot run, so the residual is now reachable // only on a developer machine with no git installed. it('really throws outside a repository, rather than returning an empty population', () => { const notARepo = mkdtempSync(join(tmpdir(), 'etv-819-bare-')); repos.push(notARepo); mkdirSync(join(notARepo, 'src'), { recursive: true }); // `GIT_CEILING_DIRECTORIES` stops git walking up into an enclosing repository — without it this // test depends on where TMPDIR happens to point, and reports the wrong throw when TMPDIR sits // inside a checkout. const ceiling: GitRunner = (args) => execFileSync('git', [...args], { encoding: 'utf8', // Same stderr suppression `runGitDefault` sets, for the same reason: without it git's own // `fatal: not a git repository` prints on every run of this file. stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, GIT_CEILING_DIRECTORIES: tmpdir() } }); expect(() => resolveTrackedSourceFiles(notARepo, ceiling)).toThrow(/could not read the git index/); }); });