Skip to content

Commit 3616282

Browse files
committed
test: cover the exclusion matcher from both import identities so shard attribution cannot miss it
1 parent 29ed46a commit 3616282

2 files changed

Lines changed: 55 additions & 0 deletions

File tree

test/unit/screenshot-table-gate-engine.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
requiredScreenshotMatrixPairs,
1818
type ScreenshotMatrixPair,
1919
} from "../../packages/loopover-engine/src/review/screenshot-table-gate";
20+
import { matchesAnyWithExclusions } from "../../packages/loopover-engine/src/signals/change-guardrail";
2021
import type { ScreenshotTableGateConfig } from "../../packages/loopover-engine/src/types/manifest-deps-types";
2122

2223
function config(overrides: Partial<ScreenshotTableGateConfig> = {}): ScreenshotTableGateConfig {
@@ -795,3 +796,27 @@ describe("extractTableRowImageUrls (#4366)", () => {
795796
]);
796797
});
797798
});
799+
800+
// #9434: the exclusion matcher exercised through the ENGINE source path (change-guardrail.test.ts reaches the
801+
// same function through the `src/signals/change-guardrail` re-export shim, a distinct coverage identity).
802+
// Both paths are asserted so the lines stay attributed under either identity regardless of how CI shards the
803+
// suites — the misattribution class vitest.config.ts's coverage-include comment documents.
804+
describe("matchesAnyWithExclusions via the engine path (#9434)", () => {
805+
it("excludes a generated file from a directory glob, and leaves the rest matching", () => {
806+
const globs = ["apps/ui/public/**", "!apps/ui/public/openapi.json"];
807+
expect(matchesAnyWithExclusions("apps/ui/public/openapi.json", globs)).toBe(false);
808+
expect(matchesAnyWithExclusions("apps/ui/public/hero.png", globs)).toBe(true);
809+
});
810+
811+
it("requires an include: an all-exclude list never matches, and a non-matching path stays false", () => {
812+
expect(matchesAnyWithExclusions("apps/ui/public/x.png", ["!apps/ui/public/**"])).toBe(false);
813+
expect(matchesAnyWithExclusions("docs/readme.md", ["apps/ui/**", "!apps/ui/public/**"])).toBe(false);
814+
});
815+
816+
it("SECURITY: an over-complex exclude glob excludes NOTHING — it can only widen gate scope, never shrink it", () => {
817+
// Opposite fail direction from an include: an unsafe exclude resolving to 'matches everything' would
818+
// silently shrink a safety gate's coverage.
819+
const result = matchesAnyWithExclusions("apps/ui/public/openapi.json", ["apps/ui/public/**", "!apps/*-*-*-x.json"]);
820+
expect(result).toBe(true);
821+
});
822+
});

test/unit/screenshot-table-gate.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,3 +792,33 @@ describe("extractTableRowImageUrls (#4366)", () => {
792792
]);
793793
});
794794
});
795+
796+
// #9434: the same exclusion behaviour asserted in screenshot-table-gate-engine.test.ts, but reached through
797+
// the `src/review/screenshot-table-gate` re-export shim rather than the engine source path.
798+
//
799+
// Not redundant: the two import paths are DISTINCT coverage identities (the shim is its own real file), and
800+
// CI sharding means a given shard may run only one of the two suites. Exercising the exclusion from both
801+
// sides keeps these lines attributed under either identity in whichever shard picks them up — the
802+
// misattribution class vitest.config.ts's own coverage-include comment documents from the 2026-07-24
803+
// codecov/patch incident.
804+
describe("whenPaths exclusions via the src re-export (#9434)", () => {
805+
const scoped = ["apps/ui/public/**", "!apps/ui/public/openapi.json"];
806+
807+
it("carves a generated file out of an otherwise-in-scope directory", () => {
808+
expect(isScreenshotTableGateInScope(config({ whenPaths: scoped }), [], ["apps/ui/public/openapi.json"])).toBe(false);
809+
expect(isScreenshotTableGateInScope(config({ whenPaths: scoped }), [], ["apps/ui/public/hero.png"])).toBe(true);
810+
});
811+
812+
it("applies the SAME exclusion to the committed-image check, so the two cannot disagree on what is scoped", () => {
813+
// A path excluded from gate scope must not still count as a stray committed image — that disagreement is
814+
// exactly what sharing one matcher prevents.
815+
expect(hasCommittedImageFile(["apps/ui/public/openapi.json"], scoped)).toBe(false);
816+
expect(hasCommittedImageFile(["apps/ui/public/hero.png"], scoped)).toBe(true);
817+
});
818+
819+
it("INVARIANT: a plain whenPaths list with no exclusions behaves exactly as before", () => {
820+
expect(isScreenshotTableGateInScope(config({ whenPaths: ["apps/ui/**"] }), [], ["apps/ui/src/App.tsx"])).toBe(true);
821+
expect(hasCommittedImageFile(["apps/ui/src/logo.png"], ["apps/ui/**"])).toBe(true);
822+
expect(hasCommittedImageFile(["docs/logo.png"], ["apps/ui/**"])).toBe(false);
823+
});
824+
});

0 commit comments

Comments
 (0)