From 84f924f7df65edebdc3784036bc36d8f140311c4 Mon Sep 17 00:00:00 2001 From: garrytan Date: Mon, 26 Oct 2026 16:16:11 +0000 Subject: [PATCH] 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 ./, 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> --- lib/ship-queue-stack-tests.ts | 5 +++-- test/ship-queue-train-ops.test.ts | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/ship-queue-stack-tests.ts b/lib/ship-queue-stack-tests.ts index cca639e1d..aa399f3b4 100644 --- a/lib/ship-queue-stack-tests.ts +++ b/lib/ship-queue-stack-tests.ts @@ -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 ` 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 { diff --git a/test/ship-queue-train-ops.test.ts b/test/ship-queue-train-ops.test.ts index 99a40a35c..603009eb7 100644 --- a/test/ship-queue-train-ops.test.ts +++ b/test/ship-queue-train-ops.test.ts @@ -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: 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: 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);