stack-tests: only bun test files count as new tests, passed as paths

TEST_FILE matched every .ts under test/, so a PR whose only added file
was a helper or fixture ran 'bun test test/helpers/x.ts', which exits 1
(the filter matches no test file) and was reported QUEUE_TRAIN_STACK_RED.
A bare positional is also a substring filter that picks up unrelated
files. The rule is now bun's own (.test, _test_, .spec, _spec_ in the
name) and each file is passed as ./<path>, which bun treats as the exact
file. The fixture PR 3 now adds a helper and is still 'no new test
files'.

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-26 16:16:40 +00:00
1 parent f25b22b0d4
commit 84f924f7df
2 files changed
+6 -5

No files matched your search

+3 -2
View File
@@ -20,7 +20,8 @@ import { git } from './ship-queue-gate';
import type { TrainPrefix, TrainSlot } from './ship-queue-train-record';
export const DEFAULT_STACK_TEST_COMMAND = ['bun', ...bunIsolationArgs(), 'test'];
const TEST_FILE = /(?:^|\/)(?:tests?|__tests__|spec)\/.*\.[cm]?[jt]sx?$|\.(?:test|spec)\.[cm]?[jt]sx?$/;
/** Bun's own test-file rule (`.test`, `_test_`, `.spec` or `_spec_` in the name); a helper or fixture under test/ is not a test file and would make `bun test <file>` exit 1 for matching nothing. */
const TEST_FILE = /(?:\.(?:test|spec)|_(?:test|spec)_)[^/]*\.[cm]?[jt]sx?$/;
export interface StackTestResult { pr: string; prefix: number; files: string[]; ok: boolean; detail: string }
@@ -44,7 +45,7 @@ export function stackTests(repoRoot: string, baseSha: string, slots: TrainSlot[]
const modules = path.join(repoRoot, 'node_modules');
if (fs.existsSync(modules) && !fs.existsSync(path.join(dir, 'node_modules'))) fs.symlinkSync(modules, path.join(dir, 'node_modules'), 'dir');
const [bin, ...args] = command;
const run = spawnSync(bin!, [...args, ...files], { cwd: dir, encoding: 'utf8', timeout: timeoutMs, env: { ...process.env, CI: process.env.CI ?? '1' }, maxBuffer: 64 * 1024 * 1024 });
const run = spawnSync(bin!, [...args, ...files.map(f => `./${f}`)], { cwd: dir, encoding: 'utf8', timeout: timeoutMs, env: { ...process.env, CI: process.env.CI ?? '1' }, maxBuffer: 64 * 1024 * 1024 });
const tail = `${run.stdout ?? ''}\n${run.stderr ?? ''}`.split('\n').filter(l => /^\(fail\)|^\s*\d+ (pass|fail)$|^error:/.test(l)).slice(-6).join('; ');
results.push({ pr: slot.pr, prefix: prefix.index, files, ok: run.status === 0, detail: run.status === 0 ? 'pass' : run.error ? `${run.error.message}` : tail || `exit ${run.status}` });
} finally {
+3 -3
View File
@@ -442,12 +442,12 @@ describe('ready --dispatch-ci', () => {
describe('train build --dry-run --stack-tests', () => {
// Value: protects=a PR whose new tests pass on its own branch but fail once stacked behind another PR's change is caught before the train spends a gate (#3460 on #3451, 2026-10-26);
// fails_when=the new test files run against the PR head instead of the integrated prefix, a failing prefix still dry-runs green, or a PR without new tests is misreported; why_new=no pre-dispatch stack check existed; seam=bun test in a scratch worktree; no_claim=the repo's full suite on the prefix
test('runs each PR\'s new test files on its integrated prefix T<k>: a pass and a no-tests PR are reported, a failure refuses the dry run and names the prefix, and the ordinary dry run skips it', () => {
// fails_when=the new test files run against the PR head instead of the integrated prefix, a failing prefix still dry-runs green, or a PR without new tests (a helper under test/ included: bun exits 1 when a positional filter matches no test file) is misreported; why_new=no pre-dispatch stack check existed; seam=bun test in a scratch worktree; no_claim=the repo's full suite on the prefix
test('runs each PR\'s new test files on its integrated prefix T<k>: a pass and a helper-only PR are reported, a failure refuses the dry run and names the prefix, and the ordinary dry run skips it', () => {
const fx = make();
const h1 = openPr(fx, '1', { file: 'test/pr-1.test.ts', text: "import { expect, test } from 'bun:test';\ntest('pr-1', () => { expect(1).toBe(1); });\n" });
const h2 = openPr(fx, '2', { file: 'test/pr-2.test.ts', text: "import { existsSync } from 'node:fs';\nimport { expect, test } from 'bun:test';\ntest('pr-2 wants no pr-1 test beside it', () => { expect(existsSync('test/pr-1.test.ts')).toBe(false); });\n" });
const h3 = openPr(fx, '3');
const h3 = openPr(fx, '3', { file: 'test/helpers/pr-3-helper.ts', text: 'export const three = 3;\n' });
readyReceipt(fx, '1', h1);
readyReceipt(fx, '2', h2);
readyReceipt(fx, '3', h3);