fix(ci): root explicit Bun test filters

Avoid Bun filter-mode repository scans that exhaust macOS file descriptors and corrupt subprocess test evidence. Apply rooted filters across server, contract, coverage, persistence, policy, desktop native, and adapter test entrypoints.

Confidence: high

Scope-risk: narrow

Tested: bun run check:policy; bun run check:server; bun run check:chat-contract
This commit is contained in:
程序员阿江(Relakkes) 2026-07-10 23:49:46 +08:00
parent 46dca4e961
commit 08dd64596a
12 changed files with 81 additions and 15 deletions

View File

@ -10,12 +10,12 @@
"dingtalk": "bun run dingtalk/index.ts", "dingtalk": "bun run dingtalk/index.ts",
"whatsapp": "bun run whatsapp/index.ts", "whatsapp": "bun run whatsapp/index.ts",
"test": "bun test", "test": "bun test",
"test:common": "bun test common/", "test:common": "bun test ./common/",
"test:telegram": "bun test telegram/", "test:telegram": "bun test ./telegram/",
"test:feishu": "bun test feishu/", "test:feishu": "bun test ./feishu/",
"test:wechat": "bun test wechat/", "test:wechat": "bun test ./wechat/",
"test:dingtalk": "bun test dingtalk/", "test:dingtalk": "bun test ./dingtalk/",
"test:whatsapp": "bun test whatsapp/" "test:whatsapp": "bun test ./whatsapp/"
}, },
"dependencies": { "dependencies": {
"@larksuiteoapi/node-sdk": "^1.60.0", "@larksuiteoapi/node-sdk": "^1.60.0",

View File

@ -91,7 +91,7 @@
"electron:build": "bun run build:sidecars && bun run build && bun run build:electron", "electron:build": "bun run build:sidecars && bun run build && bun run build:electron",
"electron:package": "bun run clean:electron-output && bun run electron:build && node ./node_modules/electron-builder/out/cli/cli.js --publish never", "electron:package": "bun run clean:electron-output && bun run electron:build && node ./node_modules/electron-builder/out/cli/cli.js --publish never",
"electron:package:dir": "bun run clean:electron-output && bun run electron:build && node ./node_modules/electron-builder/out/cli/cli.js --dir --publish never", "electron:package:dir": "bun run clean:electron-output && bun run electron:build && node ./node_modules/electron-builder/out/cli/cli.js --dir --publish never",
"check:electron": "node ./node_modules/typescript/bin/tsc -p electron/tsconfig.json && bun test electron && bun run build:electron", "check:electron": "node ./node_modules/typescript/bin/tsc -p electron/tsconfig.json && bun test ./electron && bun run build:electron",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest", "test": "vitest",
"test:ui": "vitest --ui", "test:ui": "vitest --ui",

View File

@ -12,7 +12,7 @@
"start": "bun run ./bin/claude-haha", "start": "bun run ./bin/claude-haha",
"check:pr": "bun run scripts/pr/check-pr.ts", "check:pr": "bun run scripts/pr/check-pr.ts",
"check:impact": "bun run scripts/pr/impact-report.ts", "check:impact": "bun run scripts/pr/impact-report.ts",
"check:policy": "bun test scripts/pr/change-policy.test.ts scripts/pr/changed-files.test.ts scripts/pr/pr-triage-workflow.test.ts scripts/pr/pr-quality-workflow.test.ts scripts/pr/release-workflow.test.ts scripts/pr/quality-contract.test.ts scripts/pr/test-environment.test.ts scripts/release-update-metadata.test.ts scripts/git-hooks/install.test.ts scripts/quality-gate/quarantine.test.ts scripts/quality-gate/coverage.test.ts scripts/quality-gate/provider-smoke/execute.test.ts scripts/quality-gate/desktop-smoke/execute.test.ts scripts/quality-gate/providerTargets.test.ts scripts/quality-gate/runner.test.ts", "check:policy": "bun test ./scripts/pr/bun-test-filter.test.ts ./scripts/pr/change-policy.test.ts ./scripts/pr/changed-files.test.ts ./scripts/pr/pr-triage-workflow.test.ts ./scripts/pr/pr-quality-workflow.test.ts ./scripts/pr/release-workflow.test.ts ./scripts/pr/quality-contract.test.ts ./scripts/pr/test-environment.test.ts ./scripts/release-update-metadata.test.ts ./scripts/git-hooks/install.test.ts ./scripts/quality-gate/quarantine.test.ts ./scripts/quality-gate/coverage.test.ts ./scripts/quality-gate/provider-smoke/execute.test.ts ./scripts/quality-gate/desktop-smoke/execute.test.ts ./scripts/quality-gate/providerTargets.test.ts ./scripts/quality-gate/runner.test.ts",
"check:server": "bun run scripts/pr/run-server-tests.ts", "check:server": "bun run scripts/pr/run-server-tests.ts",
"check:provider-contract": "bun run scripts/pr/run-provider-contract-tests.ts", "check:provider-contract": "bun run scripts/pr/run-provider-contract-tests.ts",
"check:chat-contract": "bun run scripts/pr/run-chat-contract-tests.ts", "check:chat-contract": "bun run scripts/pr/run-chat-contract-tests.ts",

View File

@ -0,0 +1,26 @@
import { describe, expect, test } from 'bun:test'
import { rootBunTestFilter } from './bun-test-filter'
describe('rootBunTestFilter', () => {
test('roots repository-relative files and directories', () => {
expect(rootBunTestFilter('src/server/example.test.ts')).toBe(
'./src/server/example.test.ts',
)
expect(rootBunTestFilter('electron')).toBe('./electron')
})
test('preserves already rooted and absolute filters', () => {
expect(rootBunTestFilter('./src/example.test.ts')).toBe(
'./src/example.test.ts',
)
expect(rootBunTestFilter('../shared/example.test.ts')).toBe(
'../shared/example.test.ts',
)
expect(rootBunTestFilter('/tmp/example.test.ts')).toBe(
'/tmp/example.test.ts',
)
expect(rootBunTestFilter('C:\\repo\\example.test.ts')).toBe(
'C:\\repo\\example.test.ts',
)
})
})

View File

@ -0,0 +1,20 @@
import { isAbsolute } from 'node:path'
const WINDOWS_ABSOLUTE_PATH = /^(?:[a-zA-Z]:[\\/]|\\\\)/
/**
* Root explicit Bun test filters so Bun does not scan the whole repository.
* Unrooted filters can retain enough directory descriptors on macOS to break
* subprocess output capture: https://github.com/oven-sh/bun/issues/32067
*/
export function rootBunTestFilter(filter: string): string {
if (
filter.startsWith('./') ||
filter.startsWith('../') ||
isAbsolute(filter) ||
WINDOWS_ABSOLUTE_PATH.test(filter)
) {
return filter
}
return `./${filter}`
}

View File

@ -90,6 +90,7 @@ describe('feature quality contract', () => {
expect(packageJson.scripts?.['check:persistence-upgrade']).toBe('bun run scripts/quality-gate/persistence-upgrade.ts') expect(packageJson.scripts?.['check:persistence-upgrade']).toBe('bun run scripts/quality-gate/persistence-upgrade.ts')
expect(packageJson.scripts?.['check:provider-contract']).toBe('bun run scripts/pr/run-provider-contract-tests.ts') expect(packageJson.scripts?.['check:provider-contract']).toBe('bun run scripts/pr/run-provider-contract-tests.ts')
expect(packageJson.scripts?.['check:chat-contract']).toBe('bun run scripts/pr/run-chat-contract-tests.ts') expect(packageJson.scripts?.['check:chat-contract']).toBe('bun run scripts/pr/run-chat-contract-tests.ts')
expect(packageJson.scripts?.['check:policy']).toContain('bun test ./scripts/')
expect(packageJson.scripts?.['check:native']).toContain('electron:package:dir') expect(packageJson.scripts?.['check:native']).toContain('electron:package:dir')
expect(packageJson.scripts?.['check:native']).toContain('test:package-smoke:current') expect(packageJson.scripts?.['check:native']).toContain('test:package-smoke:current')
expect(packageJson.scripts?.['test:package-smoke:current']).toBe('bun run scripts/quality-gate/package-smoke/current.ts') expect(packageJson.scripts?.['test:package-smoke:current']).toBe('bun run scripts/quality-gate/package-smoke/current.ts')
@ -137,6 +138,7 @@ describe('feature quality contract', () => {
expect(serverRunner).toContain('TEST_FILE_PATTERN') expect(serverRunner).toContain('TEST_FILE_PATTERN')
expect(serverRunner).toContain("'--no-env-file'") expect(serverRunner).toContain("'--no-env-file'")
expect(serverRunner).toContain('createSandboxedTestEnvironment') expect(serverRunner).toContain('createSandboxedTestEnvironment')
expect(serverRunner).toContain('rootBunTestFilter(file)')
expect(serverRunner).toContain('evidenceComplete') expect(serverRunner).toContain('evidenceComplete')
expect(serverRunner).toContain('reportedFiles === 1') expect(serverRunner).toContain('reportedFiles === 1')
expect(serverRunner).toContain('passedTests + failedTests > 0') expect(serverRunner).toContain('passedTests + failedTests > 0')
@ -145,6 +147,7 @@ describe('feature quality contract', () => {
expect(coverageRunner).toContain('TEST_FILE_PATTERN') expect(coverageRunner).toContain('TEST_FILE_PATTERN')
expect(coverageRunner).toContain("'--no-env-file'") expect(coverageRunner).toContain("'--no-env-file'")
expect(coverageRunner).toContain('createSandboxedTestEnvironment') expect(coverageRunner).toContain('createSandboxedTestEnvironment')
expect(coverageRunner).toContain('serverFiles.map(rootBunTestFilter)')
expect(coverageRunner).toContain("correctness is enforced by check:server's per-file sandboxed test processes") expect(coverageRunner).toContain("correctness is enforced by check:server's per-file sandboxed test processes")
expect(coverageRunner).toContain('rootCoverageAvailable') expect(coverageRunner).toContain('rootCoverageAvailable')
expect(coverageRunner).toContain('rootTestDiscoveryComplete') expect(coverageRunner).toContain('rootTestDiscoveryComplete')
@ -152,6 +155,7 @@ describe('feature quality contract', () => {
for (const contractRunner of [providerRunner, chatRunner]) { for (const contractRunner of [providerRunner, chatRunner]) {
expect(contractRunner).toContain("'--no-env-file'") expect(contractRunner).toContain("'--no-env-file'")
expect(contractRunner).toContain('createSandboxedTestEnvironment') expect(contractRunner).toContain('createSandboxedTestEnvironment')
expect(contractRunner).toContain('rootBunTestFilter')
} }
}) })

View File

@ -3,6 +3,7 @@
import { mkdtempSync, rmSync } from 'node:fs' import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { resolve } from 'node:path' import { resolve } from 'node:path'
import { rootBunTestFilter } from './bun-test-filter'
import { createSandboxedTestEnvironment } from './test-environment' import { createSandboxedTestEnvironment } from './test-environment'
interface ContractSuite { interface ContractSuite {
@ -20,7 +21,7 @@ const suites: ContractSuite[] = [
'bun', 'bun',
'--no-env-file', '--no-env-file',
'test', 'test',
'src/server/__tests__/websocket-handler.test.ts', rootBunTestFilter('src/server/__tests__/websocket-handler.test.ts'),
], ],
}, },
{ {
@ -30,7 +31,7 @@ const suites: ContractSuite[] = [
'bun', 'bun',
'--no-env-file', '--no-env-file',
'test', 'test',
'src/server/__tests__/conversations.test.ts', rootBunTestFilter('src/server/__tests__/conversations.test.ts'),
], ],
}, },
{ {

View File

@ -3,6 +3,7 @@
import { mkdtempSync, rmSync } from 'node:fs' import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { join } from 'node:path' import { join } from 'node:path'
import { rootBunTestFilter } from './bun-test-filter'
import { createSandboxedTestEnvironment } from './test-environment' import { createSandboxedTestEnvironment } from './test-environment'
const root = join(import.meta.dir, '..', '..') const root = join(import.meta.dir, '..', '..')
@ -34,7 +35,12 @@ for (const testFile of testFiles) {
const sandboxHome = mkdtempSync(join(tmpdir(), 'cc-haha-provider-contract-')) const sandboxHome = mkdtempSync(join(tmpdir(), 'cc-haha-provider-contract-'))
let exitCode = 1 let exitCode = 1
try { try {
const proc = Bun.spawn(['bun', '--no-env-file', 'test', testFile], { const proc = Bun.spawn([
'bun',
'--no-env-file',
'test',
rootBunTestFilter(testFile),
], {
cwd: root, cwd: root,
env: createSandboxedTestEnvironment(sandboxHome), env: createSandboxedTestEnvironment(sandboxHome),
stdout: 'inherit', stdout: 'inherit',

View File

@ -4,6 +4,7 @@ import { mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { join, relative, sep } from 'node:path' import { join, relative, sep } from 'node:path'
import { loadQuarantineManifest, quarantinedPathSet } from '../quality-gate/quarantine' import { loadQuarantineManifest, quarantinedPathSet } from '../quality-gate/quarantine'
import { rootBunTestFilter } from './bun-test-filter'
import { createSandboxedTestEnvironment } from './test-environment' import { createSandboxedTestEnvironment } from './test-environment'
const root = process.cwd() const root = process.cwd()
@ -69,7 +70,14 @@ async function runTestFile(file: string): Promise<TestFileResult> {
const sandboxHome = mkdtempSync(join(tmpdir(), 'cc-haha-server-test-')) const sandboxHome = mkdtempSync(join(tmpdir(), 'cc-haha-server-test-'))
try { try {
const proc = Bun.spawn( const proc = Bun.spawn(
['bun', '--no-env-file', 'test', '--max-concurrency=1', '--timeout=20000', file], [
'bun',
'--no-env-file',
'test',
'--max-concurrency=1',
'--timeout=20000',
rootBunTestFilter(file),
],
{ {
cwd: root, cwd: root,
env: createSandboxedTestEnvironment(sandboxHome), env: createSandboxedTestEnvironment(sandboxHome),

View File

@ -3,6 +3,7 @@
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs' import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { dirname, join, relative, sep, win32 } from 'node:path' import { dirname, join, relative, sep, win32 } from 'node:path'
import { rootBunTestFilter } from '../pr/bun-test-filter'
import { createSandboxedTestEnvironment } from '../pr/test-environment' import { createSandboxedTestEnvironment } from '../pr/test-environment'
import { loadQuarantineManifest, quarantinedPathSet } from './quarantine' import { loadQuarantineManifest, quarantinedPathSet } from './quarantine'
@ -805,7 +806,7 @@ export async function runCoverageGate(options: {
const coverageByFile = new Map<string, FileLineCoverage>() const coverageByFile = new Map<string, FileLineCoverage>()
mkdirSync(join(outputDir, 'root-server'), { recursive: true }) mkdirSync(join(outputDir, 'root-server'), { recursive: true })
const rootCommand = ['bun', '--no-env-file', 'test', '--timeout=20000', '--coverage', '--coverage-reporter=lcov', '--coverage-reporter=text', '--coverage-dir', join(outputDir, 'root-server'), ...serverFiles] const rootCommand = ['bun', '--no-env-file', 'test', '--timeout=20000', '--coverage', '--coverage-reporter=lcov', '--coverage-reporter=text', '--coverage-dir', join(outputDir, 'root-server'), ...serverFiles.map(rootBunTestFilter)]
const rootLogPath = join(outputDir, 'root-server', 'coverage.log') const rootLogPath = join(outputDir, 'root-server', 'coverage.log')
const rootResult = await runCommand(rootCommand, rootDir, rootLogPath) const rootResult = await runCommand(rootCommand, rootDir, rootLogPath)
const rootLcovPath = join(outputDir, 'root-server', 'lcov.info') const rootLcovPath = join(outputDir, 'root-server', 'lcov.info')

View File

@ -150,7 +150,7 @@ export function lanesForMode(mode: QualityGateMode, baselineTargets: BaselineTar
title: 'Baseline case catalog validation', title: 'Baseline case catalog validation',
description: 'Validate real Coding Agent baseline case definitions and fixture metadata.', description: 'Validate real Coding Agent baseline case definitions and fixture metadata.',
kind: 'command', kind: 'command',
command: ['bun', 'test', 'scripts/quality-gate/baseline/cases.test.ts'], command: ['bun', 'test', './scripts/quality-gate/baseline/cases.test.ts'],
requiredForModes: ['baseline', 'release'], requiredForModes: ['baseline', 'release'],
category: 'unit', category: 'unit',
}, },

View File

@ -10,7 +10,7 @@ const rootDir = process.cwd()
const checks: Check[] = [ const checks: Check[] = [
{ {
title: 'Server persistent JSON migrations', title: 'Server persistent JSON migrations',
command: ['bun', 'test', 'src/server/__tests__/persistence-upgrade.test.ts'], command: ['bun', 'test', './src/server/__tests__/persistence-upgrade.test.ts'],
}, },
{ {
title: 'Desktop localStorage migrations', title: 'Desktop localStorage migrations',