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
58 changes: 58 additions & 0 deletions src/gate/test-presence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,46 @@ describe("patchAddsQueryCode", () => {
expect(patchAddsQueryCode(addPatch("const total = items.length + 1;"))).toBe(false);
});

test("ignores an import of a component named Select", () => {
// The Site#3650 false positive: `Select } from "…"` completes the raw
// `select … from` shape, so importing a UI component read as a query and
// failed the build on a frontend-only PR.
expect(
patchAddsQueryCode(addPatch('import { Button, Select } from "@query-doctor/ui";')),
).toBe(false);
});

test("ignores a re-export of a component named Select", () => {
// Same shape one keyword over. A barrel file re-exporting the component
// (packages/ui/src/index.tsx in the Site repo) hits it without an import.
expect(
patchAddsQueryCode(
addPatch('export { default as Select } from "./components/select";'),
),
).toBe(false);
});

test("still detects a query in an exported declaration", () => {
// The recall boundary of the re-export rule: `export` starts this line too,
// but the statement declares a query rather than re-exporting a binding.
expect(
patchAddsQueryCode(addPatch('export const q = sql`SELECT id FROM "users"`;')),
).toBe(true);
});

test("ignores prose spanning a JSX block comment", () => {
// The Site#3615 false positive: a comment explaining a sort control. Line
// stripping missed it twice — the opening line trims to `{/*`, not `/*`,
// and the continuation lines are bare prose — leaving "select stay … away
// from" to complete the raw select shape.
const patch =
"@@ -1,0 +1,3 @@\n" +
"+ {/* Icon, label and select stay one unwrappable unit: once the label is\n" +
"+ hidden the icon is the only thing saying \"sort\", so it must not\n" +
"+ wrap away from the control it labels. */}";
expect(patchAddsQueryCode(patch)).toBe(false);
});

test("ignores DDL keywords in a prose string literal", () => {
// The Site#3539 false positive: an MCP tool description mentioning the
// fix it returns. Prose, not a statement — no ON clause, no column list.
Expand Down Expand Up @@ -283,6 +323,24 @@ describe("evaluateTestPresence", () => {
expect(verdict).toBeNull();
});

test("credits a spec that runs a .sql script from another directory", () => {
// The Site#3650 false positive: an operational script under `scripts/` whose
// real-DB spec lives under `src/db/` and reads the file back. Different
// directory, so only the stem rule can link them, and it compared
// `backfill-personal-teams.sql` against `backfill-personal-teams`.
const verdict = evaluateTestPresence([
changed(
"apps/api/scripts/backfill-personal-teams.sql",
"INSERT INTO teams (id, name) SELECT gen_random_uuid(), u.name FROM users u;",
),
changed(
"apps/api/src/db/backfill-personal-teams.spec.ts",
'await db.execute(readFileSync("scripts/backfill-personal-teams.sql", "utf8"));',
),
]);
expect(verdict).toBeNull();
});

test("does not fire on the route-tree regeneration from Site#3614", () => {
// The reported false positive: a routeTree.gen.ts regeneration (import
// re-ordering) whose `select-plan` route path read as `SELECT ... FROM`.
Expand Down
42 changes: 40 additions & 2 deletions src/gate/test-presence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,18 @@ function addedLines(patch: string): string {
.join("\n");
}

/**
* Drop `/* … *\/` spans before the line rules run. A block comment is only
* recognisable line by line when every line is decorated (`*` prefix, JSDoc
* style); a JSX `{/* … *\/}` opens with `{` and continues in bare prose, so its
* middle lines survived line stripping and "select stay … away from" read as a
* query (Site#3615). An unterminated span — a hunk that opens a comment it
* doesn't close — is dropped to the end, the gate's usual under-fire side.
*/
function stripBlockComments(text: string): string {
return text.replace(/\/\*[\s\S]*?(?:\*\/|$)/g, " ");
}

/** Drop lines that are plainly comments, so prose mentioning SQL keywords doesn't match. */
function stripCommentLines(text: string): string {
return text
Expand All @@ -155,13 +167,36 @@ function stripCommentLines(text: string): string {
.join("\n");
}

/** `import … from "…"`. */
const IMPORT_LINE = /^\s*import\s/;
/**
* `export { … } from "…"` / `export * from "…"` — an import that re-exports.
* Matched by its own shape rather than a bare `export` prefix so a declaration
* like ``export const q = sql`SELECT id FROM "users"` `` is still inspected.
*/
const REEXPORT_LINE = /^\s*export\s+(\*|type\s+\{|\{)[^;]*\bfrom\b/;

/**
* Drop module-import statements. An import is never a query, but a component
* named `Select` puts `Select } from "…"` in the text, which completes the raw
* `select … from` shape and reddens a frontend-only PR (Site#3650).
*/
function stripImportLines(text: string): string {
return text
.split("\n")
.filter((line) => !IMPORT_LINE.test(line) && !REEXPORT_LINE.test(line))
.join("\n");
}

/** True when the diff's *added* lines contain query code. */
export function patchAddsQueryCode(
patch: string | undefined,
config: TestPresenceConfig = DEFAULT_TEST_PRESENCE_CONFIG,
): boolean {
if (!patch) return false;
const added = stripCommentLines(addedLines(patch));
const added = stripImportLines(
stripCommentLines(stripBlockComments(addedLines(patch))),
);
return matchesAny(added, config.queryCodePatterns);
}

Expand Down Expand Up @@ -203,7 +238,10 @@ function baseStem(path: string): string {
return name
.replace(/\.(test|spec)\./i, ".")
.replace(/\.[cm]?[jt]sx?$/i, "")
.replace(/\.(py|go|rb)$/i, "");
// `.sql` belongs here for a script whose spec reads and runs the file:
// `scripts/backfill.sql` ↔ `src/db/backfill.spec.ts`. Leaving it on made the
// stem `backfill.sql`, which no test stem can contain (Site#3650).
.replace(/\.(py|go|rb|sql)$/i, "");
}

/**
Expand Down