diff --git a/adapters/package.json b/adapters/package.json index 5ae9495b..debe4dc8 100644 --- a/adapters/package.json +++ b/adapters/package.json @@ -10,12 +10,12 @@ "dingtalk": "bun run dingtalk/index.ts", "whatsapp": "bun run whatsapp/index.ts", "test": "bun test", - "test:common": "bun test common/", - "test:telegram": "bun test telegram/", - "test:feishu": "bun test feishu/", - "test:wechat": "bun test wechat/", - "test:dingtalk": "bun test dingtalk/", - "test:whatsapp": "bun test whatsapp/" + "test:common": "bun test ./common/", + "test:telegram": "bun test ./telegram/", + "test:feishu": "bun test ./feishu/", + "test:wechat": "bun test ./wechat/", + "test:dingtalk": "bun test ./dingtalk/", + "test:whatsapp": "bun test ./whatsapp/" }, "dependencies": { "@larksuiteoapi/node-sdk": "^1.60.0", diff --git a/desktop/package.json b/desktop/package.json index a18ce21a..77da9de3 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -91,7 +91,7 @@ "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: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", "test": "vitest", "test:ui": "vitest --ui", diff --git a/package.json b/package.json index 10bec4d1..d1f46d4b 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "start": "bun run ./bin/claude-haha", "check:pr": "bun run scripts/pr/check-pr.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:provider-contract": "bun run scripts/pr/run-provider-contract-tests.ts", "check:chat-contract": "bun run scripts/pr/run-chat-contract-tests.ts", diff --git a/scripts/pr/bun-test-filter.test.ts b/scripts/pr/bun-test-filter.test.ts new file mode 100644 index 00000000..161053f6 --- /dev/null +++ b/scripts/pr/bun-test-filter.test.ts @@ -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', + ) + }) +}) diff --git a/scripts/pr/bun-test-filter.ts b/scripts/pr/bun-test-filter.ts new file mode 100644 index 00000000..f1ad5e45 --- /dev/null +++ b/scripts/pr/bun-test-filter.ts @@ -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}` +} diff --git a/scripts/pr/quality-contract.test.ts b/scripts/pr/quality-contract.test.ts index 78db48c7..11ccdff5 100644 --- a/scripts/pr/quality-contract.test.ts +++ b/scripts/pr/quality-contract.test.ts @@ -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: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:policy']).toContain('bun test ./scripts/') expect(packageJson.scripts?.['check:native']).toContain('electron:package:dir') 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') @@ -137,6 +138,7 @@ describe('feature quality contract', () => { expect(serverRunner).toContain('TEST_FILE_PATTERN') expect(serverRunner).toContain("'--no-env-file'") expect(serverRunner).toContain('createSandboxedTestEnvironment') + expect(serverRunner).toContain('rootBunTestFilter(file)') expect(serverRunner).toContain('evidenceComplete') expect(serverRunner).toContain('reportedFiles === 1') expect(serverRunner).toContain('passedTests + failedTests > 0') @@ -145,6 +147,7 @@ describe('feature quality contract', () => { expect(coverageRunner).toContain('TEST_FILE_PATTERN') expect(coverageRunner).toContain("'--no-env-file'") 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('rootCoverageAvailable') expect(coverageRunner).toContain('rootTestDiscoveryComplete') @@ -152,6 +155,7 @@ describe('feature quality contract', () => { for (const contractRunner of [providerRunner, chatRunner]) { expect(contractRunner).toContain("'--no-env-file'") expect(contractRunner).toContain('createSandboxedTestEnvironment') + expect(contractRunner).toContain('rootBunTestFilter') } }) diff --git a/scripts/pr/run-chat-contract-tests.ts b/scripts/pr/run-chat-contract-tests.ts index 33116977..63a201a9 100644 --- a/scripts/pr/run-chat-contract-tests.ts +++ b/scripts/pr/run-chat-contract-tests.ts @@ -3,6 +3,7 @@ import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { resolve } from 'node:path' +import { rootBunTestFilter } from './bun-test-filter' import { createSandboxedTestEnvironment } from './test-environment' interface ContractSuite { @@ -20,7 +21,7 @@ const suites: ContractSuite[] = [ 'bun', '--no-env-file', 'test', - 'src/server/__tests__/websocket-handler.test.ts', + rootBunTestFilter('src/server/__tests__/websocket-handler.test.ts'), ], }, { @@ -30,7 +31,7 @@ const suites: ContractSuite[] = [ 'bun', '--no-env-file', 'test', - 'src/server/__tests__/conversations.test.ts', + rootBunTestFilter('src/server/__tests__/conversations.test.ts'), ], }, { diff --git a/scripts/pr/run-provider-contract-tests.ts b/scripts/pr/run-provider-contract-tests.ts index 3b68816c..09952662 100644 --- a/scripts/pr/run-provider-contract-tests.ts +++ b/scripts/pr/run-provider-contract-tests.ts @@ -3,6 +3,7 @@ import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' +import { rootBunTestFilter } from './bun-test-filter' import { createSandboxedTestEnvironment } from './test-environment' const root = join(import.meta.dir, '..', '..') @@ -34,7 +35,12 @@ for (const testFile of testFiles) { const sandboxHome = mkdtempSync(join(tmpdir(), 'cc-haha-provider-contract-')) let exitCode = 1 try { - const proc = Bun.spawn(['bun', '--no-env-file', 'test', testFile], { + const proc = Bun.spawn([ + 'bun', + '--no-env-file', + 'test', + rootBunTestFilter(testFile), + ], { cwd: root, env: createSandboxedTestEnvironment(sandboxHome), stdout: 'inherit', diff --git a/scripts/pr/run-server-tests.ts b/scripts/pr/run-server-tests.ts index 717a549b..061d15d9 100644 --- a/scripts/pr/run-server-tests.ts +++ b/scripts/pr/run-server-tests.ts @@ -4,6 +4,7 @@ import { mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, relative, sep } from 'node:path' import { loadQuarantineManifest, quarantinedPathSet } from '../quality-gate/quarantine' +import { rootBunTestFilter } from './bun-test-filter' import { createSandboxedTestEnvironment } from './test-environment' const root = process.cwd() @@ -69,7 +70,14 @@ async function runTestFile(file: string): Promise { const sandboxHome = mkdtempSync(join(tmpdir(), 'cc-haha-server-test-')) try { 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, env: createSandboxedTestEnvironment(sandboxHome), diff --git a/scripts/quality-gate/coverage.ts b/scripts/quality-gate/coverage.ts index 6297fb94..57be76d9 100644 --- a/scripts/quality-gate/coverage.ts +++ b/scripts/quality-gate/coverage.ts @@ -3,6 +3,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join, relative, sep, win32 } from 'node:path' +import { rootBunTestFilter } from '../pr/bun-test-filter' import { createSandboxedTestEnvironment } from '../pr/test-environment' import { loadQuarantineManifest, quarantinedPathSet } from './quarantine' @@ -805,7 +806,7 @@ export async function runCoverageGate(options: { const coverageByFile = new Map() 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 rootResult = await runCommand(rootCommand, rootDir, rootLogPath) const rootLcovPath = join(outputDir, 'root-server', 'lcov.info') diff --git a/scripts/quality-gate/modes.ts b/scripts/quality-gate/modes.ts index bc66cb2c..e4d2bb6c 100644 --- a/scripts/quality-gate/modes.ts +++ b/scripts/quality-gate/modes.ts @@ -150,7 +150,7 @@ export function lanesForMode(mode: QualityGateMode, baselineTargets: BaselineTar title: 'Baseline case catalog validation', description: 'Validate real Coding Agent baseline case definitions and fixture metadata.', 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'], category: 'unit', }, diff --git a/scripts/quality-gate/persistence-upgrade.ts b/scripts/quality-gate/persistence-upgrade.ts index f91cafcf..56a7211f 100644 --- a/scripts/quality-gate/persistence-upgrade.ts +++ b/scripts/quality-gate/persistence-upgrade.ts @@ -10,7 +10,7 @@ const rootDir = process.cwd() const checks: Check[] = [ { 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',