test(census): pin file DISCOVERY, which every existing test was blind to (#3259)

Follow-through on the recommendation I made reviewing #3256: **a gate
needs a test for its file discovery, not only its matcher.**

## The gap

This suite pinned the matcher and never the scan. Every case either
feeds the classifier a source string or drives the CLI against the real
tree — so **the file list could return empty and all 53 tests would
still pass.**

Not hypothetical. `git ls-files` lists tracked files only, so a new file
with a plain `task.column === "in-review"` scored 0 until staged
(#3254). The identical bug then turned up in the move-target ratchet
**behind its own 12 matcher tests** (#3256) — I wrote those 12
specifically to stop that gate regressing, and they could not see it,
because they import the matcher and never run a scan.

## Four cases, on a synthetic tree

Driven through `FUSION_CENSUS_FILE_ROOT` + `FUSION_CENSUS_FILE_LIST`, so
discovery is testable without creating files inside a checkout the
operator writes to concurrently:

- a guard in a scanned file **reaches the classifier** and is counted
- `--strict` fails **for the right reason** (message names the file; not
an ENOENT fail-closed)
- **every** listed file is counted, not just the first
- files are read from the **scan root**, so a listed path and a read
path cannot diverge

## The second case earns its wording

Its first version asserted only `code === 1` — and **passed while
discovery was broken.** With the injected list ignored, paths come from
the real repo while reads resolve against the fixture root, every read
misses, and the gate fails closed with exit 1. Right code, unrelated
cause.

A test that cannot tell *"found a guard"* from *"could not read
anything"* is not testing the ratchet. Asserting the message is what
separates them.

I found that only by checking which cases the control actually failed —
3 of 4, not 4 of 4. Had I stopped at "the control fails, ship it", I
would have added a test that passes for the wrong reason to a suite
whose whole purpose is catching tests that pass for the wrong reason.

## Measured

| check | result |
|---|---|
| suite | **57 passed** (53 + 4) |
| anti-vacuity: `injectedList` forced undefined | **all 4 fail** (3/4
before strengthening case 2) |
| restored | 57/57 |
| `census --strict` / `check-fnxc-future-dates` | 0 / 0 |

Tests only — no gate or product change. The same four assertions port
directly to the other lifecycle gates once each grows the fixture seam;
the move-target ratchet is the obvious next one, and its `.mjs` is
currently claimed by #3256.
This commit is contained in:
gsxdsm
2026-07-31 16:03:46 -07:00
committed by GitHub
parent f3d7b73741
commit c8a6af13a0

View File

@@ -1027,3 +1027,96 @@ describe("the backlog-state verdict the bare command prints", () => {
expect(describeBacklogState({ columnGuards: 0, unexaminedGuards: 3 }).join(" ")).toContain("BACKLOG ZERO");
});
});
/*
FNXC:LifecycleColumnCensus 2026-07-31-20:30 (u12 — the suite pinned the MATCHER, never the DISCOVERY):
Every test in this file feeds the classifier a source string, or drives the CLI against the real tree.
None asserts WHICH FILES REACH the classifier — so the scan could return an empty list and the whole
suite would still pass.
That is not hypothetical. `git ls-files` lists TRACKED files only, so a brand-new file with a plain
`task.column === "in-review"` scored 0 until it was staged (#3254), and the identical bug sat in the
move-target ratchet behind its own 12 matcher tests (#3256) — I wrote those tests specifically to stop
that gate regressing, and they could not see it, because they import the matcher and never run a scan.
`FUSION_CENSUS_FILE_ROOT` + `FUSION_CENSUS_FILE_LIST` drive a synthetic tree, so discovery is testable
without creating files inside a live checkout that the operator is writing to concurrently.
*/
describe("the census scans the files it claims to scan", () => {
const repoRootPath = new URL("../../../../", import.meta.url).pathname;
const cli = `${repoRootPath}scripts/lifecycle-column-census.mjs`;
async function runOnFixture(files: Record<string, string>, args: string[] = []) {
const { mkdtemp, writeFile, mkdir } = await import("node:fs/promises");
const { tmpdir } = await import("node:os");
const { join, dirname } = await import("node:path");
const { execFile } = await import("node:child_process");
const root = await mkdtemp(join(tmpdir(), "fusion-census-discovery-"));
for (const [rel, body] of Object.entries(files)) {
await mkdir(dirname(join(root, rel)), { recursive: true });
await writeFile(join(root, rel), body);
}
const baseline = join(root, "baseline.json");
await writeFile(baseline, JSON.stringify({ byFile: {} }));
return await new Promise<{ code: number; out: string }>((resolve) => {
execFile("node", [cli, ...args], {
cwd: repoRootPath,
env: {
...process.env,
FUSION_CENSUS_FILE_ROOT: root,
FUSION_CENSUS_FILE_LIST: Object.keys(files).join(","),
FUSION_CENSUS_BASELINE_PATH: baseline,
},
}, (err, stdout, stderr) => {
resolve({ code: (err as { code?: number } | null)?.code ?? 0, out: `${stdout}${stderr}` });
});
});
}
const GUARD = 'export const f = (t: { column: string }) => t.column === "in-review";\n';
it("REACHES the classifier — a guard in a scanned file is counted", async () => {
const { out } = await runOnFixture({ "pkg/src/a.ts": GUARD });
expect(out).toContain("scanned 1 source files");
// The half that matters: discovery delivered the file AND the classifier saw its guard.
expect(out).toMatch(/COLUMN guards \(the backlog\):\s+1/);
});
it("fails --strict FOR THE RIGHT REASON on a guard the baseline does not allow", async () => {
/*
Asserted on the MESSAGE, not just the exit code, and that is not belt-and-braces — the first
version checked `code === 1` and passed while discovery was broken. With the injected list
ignored, paths come from the real repo while reads resolve against the fixture root, every read
misses, and the gate fails CLOSED with exit 1. Right code, unrelated cause: a test that cannot
tell "found a guard" from "could not read anything" is not testing the ratchet.
*/
const { code, out } = await runOnFixture({ "pkg/src/a.ts": GUARD }, ["--strict"]);
expect(code).toBe(1);
expect(out).toContain("pkg/src/a.ts");
expect(out).not.toContain("ENOENT");
});
it("counts every listed file, not just the first", async () => {
const { out } = await runOnFixture({ "pkg/src/a.ts": GUARD, "pkg/src/b.ts": GUARD });
expect(out).toContain("scanned 2 source files");
expect(out).toMatch(/COLUMN guards \(the backlog\):\s+2/);
});
it("reads each file from the SCAN root, not the script's repo", async () => {
/*
The scan root and the read root were separate values, so an injected list was LISTED against the
fixture and READ against the repo — ENOENT on every file, which a `catch { continue }` turned into
a clean zero. Asserting a non-zero count here is what pins them to the same value.
*/
const { out, code } = await runOnFixture({ "pkg/src/only-here.ts": GUARD });
expect(out).not.toContain("ENOENT");
expect(code).toBe(0);
expect(out).toMatch(/COLUMN guards \(the backlog\):\s+1/);
});
});