Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/gate/test-presence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,27 @@ describe("evaluateTestPresence", () => {
expect(verdict?.reason).toContain("flagged conservatively");
expect(verdict?.nextStep).toContain("test");
});

// A repo whose data-layer test doesn't sit beside the source or share its stem
// (e.g. `tests/pg/postgres.test.ts` for `src/db/postgres.ts`) trips the diff
// heuristic — but the run captured the new queries, proving a test ran them.
test("passes when capture reports new queries the run executed", () => {
const files = [
changed("src/db/postgres.ts", "return db.select().from(projects).where(eq(projects.userId, id));"),
];
expect(evaluateTestPresence(files)).not.toBeNull();
const verdict = evaluateTestPresence(files, undefined, {
newQueryHashes: ["f13683ee48e6a487", "53350021ed44ae14"],
});
expect(verdict).toBeNull();
});

test("still flags when capture reports no new queries", () => {
const verdict = evaluateTestPresence(
[changed("src/db/postgres.ts", "return db.select().from(projects);")],
undefined,
{ newQueryHashes: [] },
);
expect(verdict?.dataAccessFiles).toEqual(["src/db/postgres.ts"]);
});
});
25 changes: 24 additions & 1 deletion src/gate/test-presence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,14 +260,26 @@ const TRIAGE_HINT =
"If this change intentionally needs no test, note why on the PR so the " +
"exception is auditable rather than silent.";

/**
* Capture evidence from the run, used to override the diff heuristic with what
* actually executed. `newQueryHashes` are the fingerprints this run captured
* that the baseline had not — query surface this PR introduced that a real-DB
* test ran against Postgres. Empty when there is no baseline to diff against.
*/
export interface TestPresenceCapture {
newQueryHashes: readonly string[];
}

/**
* Evaluate the gate. Returns a verdict listing the changed data-access files
* that have no related data-layer test, or `null` when the PR passes — no query
* change, or every query change has a related test alongside it.
* change, every query change has a related test alongside it, or capture proves
* the change ran against Postgres.
*/
export function evaluateTestPresence(
files: ChangedFile[],
config: TestPresenceConfig = DEFAULT_TEST_PRESENCE_CONFIG,
capture?: TestPresenceCapture,
): TestPresenceVerdict | null {
const { dataAccessChanged, dataLayerTestChanged } = classifyChangedFiles(
files,
Expand All @@ -277,6 +289,17 @@ export function evaluateTestPresence(
(path) => !dataLayerTestChanged.some((test) => isRelated(path, test)),
);
if (untested.length === 0) return null;

// Capture is ground truth for the blind spot this gate exists to catch: a
// query change that runs against Postgres in no test produces no captured
// query. If this run captured new query surface, a real-DB test *did* exercise
// the change — observed execution overrides the diff heuristic's "no related
// test" guess, so don't flag. Per-query→file attribution (which would let a
// partially-tested PR still flag its one uncovered file) is the next rung,
// #3503; until then this suppresses at the run level, on the safe under-fire
// side the gate already favours.
if (capture && capture.newQueryHashes.length > 0) return null;

return {
condition: "untested-data-access",
verdictClass: "uncertain-conservative-flag",
Expand Down
15 changes: 10 additions & 5 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,16 +211,21 @@ async function runInCI(
);
}

// Crude test-presence gate (#3496): flag data-access changes that ship with
// no data-layer test. A pure diff heuristic — independent of capture and of
// the baseline comparison, so it runs even on a brand-new PR with no
// baseline. Best-effort: a GitHub API hiccup must never sink the whole run.
// Test-presence gate (#3496): flag data-access changes that ship with no
// data-layer test. A diff heuristic, but capture overrides it (#3502): when
// the run captured new query surface, the change demonstrably ran against
// Postgres, so the "no related test" guess is dropped. Runs even on a
// brand-new PR with no baseline (then capture is empty and it's diff-only).
// Best-effort: a GitHub API hiccup must never sink the whole run.
if (env.GITHUB_TOKEN) {
try {
const changedFiles = await fetchPrChangedFiles(env.GITHUB_TOKEN);
if (changedFiles) {
const capture = reportContext.comparison
? { newQueryHashes: reportContext.comparison.newQueries.map((q) => q.hash) }
: undefined;
reportContext.testPresenceVerdict =
evaluateTestPresence(changedFiles) ?? undefined;
evaluateTestPresence(changedFiles, undefined, capture) ?? undefined;
}
} catch (err) {
log.warn(`Test-presence gate skipped: ${err}`, "main");
Expand Down
Loading