mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-11-06 20:58:14 -04:00
fix(test): verify isolated child and release suite completion (#2425)
This commit is contained in:
@@ -105,7 +105,7 @@ jobs:
|
||||
run: bun run build
|
||||
|
||||
- name: Run unit tests
|
||||
run: bun test --feature=UNATTENDED_RETRY --max-concurrency=1
|
||||
run: bun run test
|
||||
|
||||
- name: Smoke test
|
||||
run: bun run smoke
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { testReportStatus } from '../src/test/testReport.js'
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
||||
import { closeSync, fstatSync, openSync, readSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
@@ -125,13 +126,13 @@ try {
|
||||
}
|
||||
if (result !== 0 && exitCode === 0) exitCode = result
|
||||
const report = await readFile(reportPath, 'utf8').catch(() => '')
|
||||
const summary = report.match(/<testsuites\b[^>]*>/)?.[0]
|
||||
if (!summary || !report.trimEnd().endsWith('</testsuites>')) {
|
||||
const status = testReportStatus(report)
|
||||
if (status === 'incomplete') {
|
||||
console.error('Test runner exited without a complete JUnit report.')
|
||||
if (exitCode === 0) exitCode = 1
|
||||
} else {
|
||||
completed++
|
||||
if (!/\bfailures="0"/.test(summary) || /<failure\b|<error\b/.test(report)) {
|
||||
if (status === 'failed') {
|
||||
if (result === 0) console.error('Test runner reported failures despite exiting successfully.')
|
||||
if (exitCode === 0) exitCode = 1
|
||||
}
|
||||
|
||||
+6
-1
@@ -10,7 +10,12 @@ import {
|
||||
const scenario = process.env.ISOLATION_FIXTURE_SCENARIO
|
||||
const inChild = process.env[REAL_PROVIDER_TEST_CHILD_ENV] === '1'
|
||||
|
||||
if (scenario === 'assertion-failure') {
|
||||
if (scenario === 'early-exit') {
|
||||
test('premature successful exit', () => process.exit(0))
|
||||
test('unreached assertion', () => expect(true).toBe(false))
|
||||
} else if (scenario === 'complete') {
|
||||
test('completed child sentinel', () => expect(inChild).toBe(true))
|
||||
} else if (scenario === 'assertion-failure') {
|
||||
test('child assertion failure sentinel', () => {
|
||||
expect('actual child value').toBe('expected child value')
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from 'bun:test'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'fs'
|
||||
import { mkdtempSync, readdirSync, rmSync, writeFileSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import {
|
||||
@@ -75,33 +75,47 @@ for (const scenario of [
|
||||
}, { timeout: testTimeout })
|
||||
}
|
||||
|
||||
test('propagates child assertion failures through the launcher', async () => {
|
||||
await withFixture('assertion-failure', async (entry, env) => {
|
||||
// Run the real launcher in a disposable process so coordinator env and mocks
|
||||
// remain untouched even when other suites share this process.
|
||||
const runner = join(entry, '..', 'launcher.ts')
|
||||
writeFileSync(runner, `
|
||||
import { runTestFileWithRealProviders } from ${JSON.stringify(
|
||||
join(import.meta.dir, 'providerModuleIsolation.ts'),
|
||||
)}
|
||||
await runTestFileWithRealProviders(${JSON.stringify(entry)})
|
||||
`)
|
||||
const child = Bun.spawn({
|
||||
cmd: [process.execPath, runner],
|
||||
env,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
timeout: fixtureTimeout,
|
||||
killSignal: process.platform === 'win32' ? undefined : 'SIGKILL',
|
||||
for (const scenario of ['assertion-failure', 'early-exit', 'complete']) {
|
||||
test(`verifies isolated child completion: ${scenario}`, async () => {
|
||||
await withFixture(scenario, async (entry, env) => {
|
||||
// Run the real launcher in a disposable process so coordinator env and mocks
|
||||
// remain untouched even when other suites share this process.
|
||||
const runner = join(entry, '..', 'launcher.test.ts')
|
||||
writeFileSync(runner, `
|
||||
import { test } from 'bun:test'
|
||||
import { runTestFileWithRealProviders } from ${JSON.stringify(
|
||||
join(import.meta.dir, 'providerModuleIsolation.ts'),
|
||||
)}
|
||||
test('nested launcher', () => runTestFileWithRealProviders(${JSON.stringify(entry)}), ${REAL_PROVIDER_TEST_TIMEOUT_MS + 5_000})
|
||||
`)
|
||||
const child = Bun.spawn({
|
||||
cmd: [process.execPath, join(import.meta.dir, '../../scripts/run-tests.ts'), runner],
|
||||
env: { ...env, TMPDIR: join(entry, '..'), TMP: join(entry, '..'), TEMP: join(entry, '..') },
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
timeout: fixtureTimeout,
|
||||
killSignal: process.platform === 'win32' ? undefined : 'SIGKILL',
|
||||
})
|
||||
const [exitCode, stdout, stderr] = await Promise.all([
|
||||
child.exited,
|
||||
new Response(child.stdout).text(),
|
||||
new Response(child.stderr).text(),
|
||||
])
|
||||
const output = `${stdout}\n${stderr}`
|
||||
expect(exitCode, output).toBe(scenario === 'complete' ? 0 : 1)
|
||||
if (scenario !== 'complete') {
|
||||
expect(output).toContain('provider-isolated test process failed')
|
||||
expect(output).toMatch(/^\s*1 fail$/m)
|
||||
expect(output).toContain(scenario === 'early-exit'
|
||||
? 'without a complete JUnit report'
|
||||
: 'expected child value')
|
||||
if (scenario === 'assertion-failure') expect(output).toContain('child assertion failure sentinel')
|
||||
} else {
|
||||
expect(output).toMatch(/^\s*1 pass$/m)
|
||||
}
|
||||
expect(readdirSync(join(entry, '..')).filter(name =>
|
||||
name.startsWith('openclaude-test-report-'),
|
||||
)).toEqual([])
|
||||
})
|
||||
const [exitCode, stdout, stderr] = await Promise.all([
|
||||
child.exited,
|
||||
new Response(child.stdout).text(),
|
||||
new Response(child.stderr).text(),
|
||||
])
|
||||
expect(exitCode).not.toBe(0)
|
||||
expect(`${stdout}\n${stderr}`).toContain('provider-isolated test process failed')
|
||||
expect(`${stdout}\n${stderr}`).toContain('child assertion failure sentinel')
|
||||
expect(`${stdout}\n${stderr}`).toContain('expected child value')
|
||||
})
|
||||
}, { timeout: testTimeout })
|
||||
}, { timeout: testTimeout })
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { testReportStatus } from './testReport.js'
|
||||
|
||||
export const REAL_PROVIDER_TEST_CHILD_ENV =
|
||||
'OPENCLAUDE_REAL_PROVIDER_TEST_CHILD'
|
||||
export const REAL_PROVIDER_TEST_TIMEOUT_MS = 30_000
|
||||
@@ -91,26 +96,37 @@ export async function attributionModulesNeedIsolation(): Promise<boolean> {
|
||||
export async function runTestFileWithRealProviders(
|
||||
testFile: string,
|
||||
): Promise<void> {
|
||||
const child = Bun.spawn({
|
||||
cmd: [process.execPath, 'test', testFile],
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
[REAL_PROVIDER_TEST_CHILD_ENV]: '1',
|
||||
},
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
timeout: REAL_PROVIDER_TEST_TIMEOUT_MS,
|
||||
killSignal: process.platform === 'win32' ? undefined : 'SIGKILL',
|
||||
})
|
||||
const [exitCode, stdout, stderr] = await Promise.all([
|
||||
child.exited,
|
||||
new Response(child.stdout).text(),
|
||||
new Response(child.stderr).text(),
|
||||
])
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(
|
||||
`provider-isolated test process failed for ${testFile} (${exitCode})\n${stdout}\n${stderr}`,
|
||||
)
|
||||
const reportDir = await mkdtemp(join(tmpdir(), 'openclaude-test-report-'))
|
||||
const reportPath = join(reportDir, 'results.xml')
|
||||
try {
|
||||
const child = Bun.spawn({
|
||||
cmd: [process.execPath, 'test', testFile, '--reporter=junit', `--reporter-outfile=${reportPath}`],
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
[REAL_PROVIDER_TEST_CHILD_ENV]: '1',
|
||||
},
|
||||
stdin: 'ignore',
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
timeout: REAL_PROVIDER_TEST_TIMEOUT_MS,
|
||||
killSignal: process.platform === 'win32' ? undefined : 'SIGKILL',
|
||||
})
|
||||
const [exitCode, stdout, stderr] = await Promise.all([
|
||||
child.exited,
|
||||
new Response(child.stdout).text(),
|
||||
new Response(child.stderr).text(),
|
||||
])
|
||||
const status = testReportStatus(await readFile(reportPath, 'utf8').catch(() => ''))
|
||||
if (exitCode !== 0 || status !== 'passed') {
|
||||
const reason = status === 'incomplete'
|
||||
? 'Test runner exited without a complete JUnit report.'
|
||||
: status === 'failed' ? 'Test runner reported failures.' : ''
|
||||
throw new Error(
|
||||
`provider-isolated test process failed for ${testFile} (${exitCode})\n${reason}\n${stdout}\n${stderr}`,
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
await rm(reportDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/** Verify the completion and failure markers in Bun's final JUnit report. */
|
||||
export function testReportStatus(report: string): 'incomplete' | 'failed' | 'passed' {
|
||||
const summary = report.match(/<testsuites\b[^>]*>/)?.[0]
|
||||
if (!summary || !report.trimEnd().endsWith('</testsuites>')) return 'incomplete'
|
||||
return !/\bfailures="0"/.test(summary) || /<failure\b|<error\b/.test(report)
|
||||
? 'failed'
|
||||
: 'passed'
|
||||
}
|
||||
Reference in New Issue
Block a user