Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 35s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 57s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 37s
PR Gates / Docs update reminder (pull_request) Successful in 1m0s
PR Gates / decisions lifecycle (pull_request) Successful in 20s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 17s
review-verdict/h10 Review-verdict: MERGEABLE @ a7d91bf (base: main)
Review verdict / Set review-verdict status (pull_request_target) Successful in 45s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m25s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m17s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 19m27s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m4s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
`docs.no-session-narrative` reaches every durable artifact, but its detector scanned only `docs/**/*.md` and root markdown, and nothing had ever swept the rest. The issue named four sites from one grep and called them a floor. Deriving the population instead — a whitespace-joined sweep over every tracked file outside the detector, for the detector's own phrasings plus the attribution and review-round class #812 found — gave 453 sites in 108 files at `fb5592971`, and a second pass for phrasings the first list missed (hyphenated `round-N`, "an earlier version", "the reviewer proved") added residuals in the same files. Every site was classified with #812's three dispositions (CUT / SEVER / KEEP with its sub-kind) under the who-benefits test; the per-site manifests are on the PR. The rejected designs, tested-and-rejected fixtures, measurements and traps stay; the attribution of who found them and the round in which they were found go. The detector's population grows to `.claude/`, `.gitea/`, `.husky/` and `scripts/` regardless of extension, minus the detector and its own test (whose fixtures ARE the phrasings) and minus `scripts/tests/fixtures/` (test data, including decision-record copies — the same reasoning as the records' own exemption, and what keeps the record's depth measurement true), and `--all` lists tracked REGULAR files only — a symlink's content is its target and a gitlink has none. The #812 argument for leaving `docs/superpowers/**` in the population runs the other way here: `--diff` sees only ADDED lines, and 287 of the 453 sites were under 30 days old — this corpus is where narrative is being added, so the advisory nudge has reach. Density agrees: 56 line-mode hits over the 113 regular files the predicate admits, against 9 over 66 docs files before #812. `web/` and C# stay out on the same measurement (3 of 74 PATTERNS-matching sites, ~4,600 files). The predicate did not grow: PATTERNS matched 74 of 453 sites, and widening the word list to the attribution class is the treadmill the withdrawn parity test ran on. The population oracle is restated over segments with the new arms, the synthetic cross product gains the process heads and non-markdown extensions, a fixture witnesses that a tracked symlink is neither scanned nor counted, a `.py.bak` axis separates a by-name exemption from a `startswith` over the same tuple, and eight mutants (drop the process arm, drop the by-name exemption, exempt by `startswith`, drop or add a prefix, drop the fixtures exemption, list only markdown, drop the symlink filter, test the mode per row instead of per path) each redden it. A pre-existing silent drop in `--diff` goes with it: git tab-terminates a `+++` filename that contains a space, and the kept tab made `is_scanned_path` refuse the file with no notice — fixed, with a positive control and its own mutant. Code is unchanged by construction, measured per file type against `origin/main`: Python modules are AST-equal with docstrings stripped, except `#` lines inside the embedded fixture programs (string literals) of three test modules; workflows differ only in `#` lines inside `run:` block scalars; shell, C#, TypeScript and jq are equal with comment lines stripped. The stated exceptions: the detector and its test, 26 vitest titles that carried review-round or severity labels or a reviewer attribution (call sites whose title changed — every changed title line walked back to its `it(` / `it.each(...)(` anchor, so a `' + '` concatenation counts once), two registry note strings and the mutation manifest's prose fields. scripts/tests: 1565 passed. Web: lint, typecheck, 1319 tests green. Closes #876. Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PEcBoFw7ctrf3Nb7R7x7wk
177 lines
8.1 KiB
TypeScript
177 lines
8.1 KiB
TypeScript
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<typeof trackedSourceFilesPlugin>, 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);
|
|
});
|
|
});
|