fix: a removed test is named by any whole-word mention of its name without the extension (pregate lanes)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-28 09:30:32 +00:00
1 parent 39fce52f2d
commit f943239692
4 files changed
+26 -5

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@
- Only removed tests are waived. These still fail PREGATE_NO_LANE until a test or a `dependencies` entry covers them: any removed non-test path, a renamed source file's old name included; a removed test that remaining code imports; a deleted or renamed Go or Ruby test (it may define helpers its package still uses); and a deleted test whose merge-base text cannot be read (a symlink, invalid UTF-8, over 1 MiB). A remaining test that reads a deleted file or directory is still selected.
### Gate integrity
- New `test/pregate-removed-paths.test.ts` (42 cases) covers the waived tests and the fail-closed negative controls; no existing test, threshold or allowlist changed.
- New `test/pregate-removed-paths.test.ts` (45 cases) covers the waived tests and the fail-closed negative controls; no existing test, threshold or allowlist changed.
## [1.91.291.0] - 2026-10-28
+3 -2
View File
@@ -200,8 +200,9 @@ Two stages with separate clocks, both printed:
<why>` (the `lanes` detail adds `N removed path(s)`), when it is a JS, TS,
Python, Dart or bats test and no remaining file imports it (directly or
through a directory it reads; otherwise `still imported by …`). It runs
the tests of each remaining non-Markdown file that mentions its basename,
such as a curation list (`files naming it run: …`), and fails closed when
the tests of each remaining non-Markdown file that mentions its name
without the final extension as a whole word (`alpha.test`, `test_alpha`:
a path with or without the extension, or a dotted module), such as a curation list (`files naming it run: …`), and fails closed when
such a file has no test (`still named by …, which no test covers`). A test
renamed to another test path then defers to its new name (`old name of
<new>`). A deleted test, or one renamed to a non-test path, also runs its
+8 -2
View File
@@ -105,9 +105,15 @@ function readWorkingTree(repoRoot: string, rel: string): { text: string | null;
/** Tracked files matching a registry entry's output globs. */
function outputsOf(all: readonly string[], globs: readonly string[]): string[] { return all.filter(f => matchesAny(globs, f)); }
/** Tracked files outside Markdown that mention `file`'s basename, where the import graph cannot see: a curation list, a durations manifest, a script or a workflow. */
/**
* Tracked files outside Markdown that mention `file`'s name without its final extension as a whole word, where the import graph
* cannot see: a curation list, a durations manifest, a script, a workflow, an extensionless path (`node test/alpha.test`) or a
* dotted module (`tests.test_alpha`).
*/
function namedBy(ctx: PregateContext, file: string): string[] {
const r = spawnSync('git', ['grep', '--untracked', '-l', '-F', '-e', path.posix.basename(file), '--', '.', ':(exclude)*.md', ':(exclude)*.mdx'], { cwd: ctx.repoRoot, encoding: 'utf8', timeout: 60_000, maxBuffer: 16 * 1024 * 1024, env: ctx.env });
const base = path.posix.basename(file);
const stem = base.includes('.') ? base.slice(0, base.lastIndexOf('.')) : base;
const r = spawnSync('git', ['grep', '--untracked', '-l', '-w', '-F', '-e', stem, '--', '.', ':(exclude)*.md', ':(exclude)*.mdx'], { cwd: ctx.repoRoot, encoding: 'utf8', timeout: 60_000, maxBuffer: 16 * 1024 * 1024, env: ctx.env });
if (r.status === 1) return [];
return r.status === 0 ? (r.stdout ?? '').split('\n').filter(Boolean) : ['(git grep failed)'];
}
+14
View File
@@ -229,6 +229,20 @@ describe('removed paths in lanes (#3529)', () => {
expect(r.removed[0]).toContain('files naming it run: test/curation.test.js');
});
test.each([
['an extensionless path in package.json', 'test/util.test.js', { 'package.json': '{ "name": "fixture", "scripts": { "smoke": "node --test test/util.test" } }\n' }],
['a dotted module name in a Makefile', 'tests/test_alpha.py', { 'tests/__init__.py': '', 'tests/test_alpha.py': 'import unittest\nclass T(unittest.TestCase):\n def test_a(self):\n pass\n', 'Makefile': 'smoke:\n\tpython3 -m unittest tests.test_alpha\n' }],
['a dotted module name untested code imports at run time', 'app/test_handlers.py', { 'app/__init__.py': '', 'app/test_handlers.py': 'X = 1\n', 'app/registry.py': 'import importlib\nH = importlib.import_module("app.test_handlers")\n' }],
])('a removed test named only by %s that no test covers fails closed', (_how, removed, files) => {
const repo = fixture({ baseFiles: files });
commit(repo, { [removed]: null });
const r = lanes(repo);
expect(r.check.code).toBe('PREGATE_NO_LANE');
expect(r.removed).toEqual([]);
const explain = runBin(repo, 'gstack-pregate', ['--base', 'main', '--allow-repo-commands', '--stage', 'tests', '--explain']);
expect(explain.out + explain.err).toContain(`${removed}: removed, but still named by`);
});
test('a removed test a file no test covers still names fails closed', () => {
const repo = fixture({ baseFiles: { 'scripts/skip-list.txt': 'test/util.test.js\n' } });
commit(repo, { 'test/util.test.js': null });