From d54f6382c8ccda21f71fc199c152ea0ce86d0ad2 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:45:25 +0800 Subject: [PATCH] fix(ci): preserve Windows coverage failures --- ...8-18-in-job-partitioned-coverage.i18n.yaml | 4 +- .../2026-08-18-in-job-partitioned-coverage.md | 2 +- ...26-08-18-in-job-partitioned-coverage.zh.md | 2 +- .../subagent-codex/tests/real-product.spec.ts | 50 +++++++++++-------- scripts/coverage-partitions.spec.ts | 5 +- scripts/coverage-partitions.ts | 29 +++++++++-- 6 files changed, 62 insertions(+), 30 deletions(-) diff --git a/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.i18n.yaml b/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.i18n.yaml index 62baafd8ed..4b33d023ed 100644 --- a/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.md -2026-08-18-in-job-partitioned-coverage.md: d6b8f98095ebb77c6caf88ce67999e683c71f74c -2026-08-18-in-job-partitioned-coverage.zh.md: 36a49b91b2544c611bf77350bda66b06a2708ba6 +2026-08-18-in-job-partitioned-coverage.md: b7335c490c4a4921e5c786809e0db492613ef5c8 +2026-08-18-in-job-partitioned-coverage.zh.md: 5e2a6a60d59751a22c7d664d4bc50d93390c6995 diff --git a/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.md b/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.md index d6b8f98095..b7335c490c 100644 --- a/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.md +++ b/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.md @@ -22,7 +22,7 @@ The coordinator waits for every child, validates that the blob directory contain ## Failure and output semantics -Partition children inherit the coordinator's stdout and stderr. The coverage gate opts into `run-gates` streaming, so test progress and failures reach CI logs as they occur without buffering the complete log in the scheduler or printing it a second time at completion. When a child settles unsuccessfully, the coordinator immediately prints its spawn error, exit code, or signal before validating the complete blob set. +Partition children stream stdout and stderr through the coordinator. The coverage gate opts into `run-gates` streaming, so test progress and failures reach CI logs as they occur without buffering the complete log in the scheduler. The coordinator also retains a bounded 64 KiB combined tail per child; when a child settles unsuccessfully, it prints the spawn error, exit code, or signal and repeats that tail before validating the complete blob set, keeping the specific Vitest failure beside the final partition diagnostic. A normal failed test still emits a blob through `--coverage.reportOnFailure`, allowing the merge to report the complete coverage state before the coordinator returns failure. Spawn failure, signal termination, non-zero exit, a missing or extra blob, or a failed merge all make the gate fail. The coordinator removes only its owned coverage tree and unlinks a link-shaped path instead of recursively following it. diff --git a/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.zh.md b/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.zh.md index 36a49b91b2..5e2a6a60d5 100644 --- a/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.zh.md +++ b/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.zh.md @@ -22,7 +22,7 @@ Status: implemented ## 失败与输出语义 -分区子进程继承协调器的 stdout 与 stderr。覆盖率门禁选择 `run-gates` 流式输出,因此测试进度与失败会在发生时进入 CI 日志;调度器不会缓冲完整日志,也不会在结束时重复打印。子进程以失败状态结算时,协调器会立即打印其 spawn 错误、退出码或信号,再校验完整的 blob 集合。 +分区子进程通过协调器流式传递 stdout 与 stderr。覆盖率门禁选择 `run-gates` 流式输出,因此测试进度与失败会在发生时进入 CI 日志,调度器不会缓冲完整日志。协调器还会为每个子进程保留一份有界的 64 KiB 混合输出尾部;子进程以失败状态结算时,它会打印 spawn 错误、退出码或信号,并在校验完整 blob 集合前重印这份尾部,使具体 Vitest 失败与最终分区诊断相邻。 普通测试失败仍通过 `--coverage.reportOnFailure` 产出 blob,使合并步骤可以先报告完整覆盖率状态,再由协调器返回失败。spawn 失败、信号终止、非零退出、blob 缺失或多余,以及合并失败都会让门禁失败。协调器只删除自己拥有的覆盖率目录树;若该路径是链接,则只 unlink,不递归跟随。 diff --git a/packages/subagent/subagent-codex/tests/real-product.spec.ts b/packages/subagent/subagent-codex/tests/real-product.spec.ts index abf3c5147a..3b95de4f79 100644 --- a/packages/subagent/subagent-codex/tests/real-product.spec.ts +++ b/packages/subagent/subagent-codex/tests/real-product.spec.ts @@ -65,17 +65,19 @@ interface RealInstanceFixture { readonly workspace: string } +type ResponsesScript = readonly ResponsesBehavior[] | ((workspace: string) => readonly ResponsesBehavior[]) + async function realInstanceFixture( - script: readonly ResponsesBehavior[], + script: ResponsesScript, ): Promise { const root = mkdtempSync(join(tmpdir(), 'dsh-codex-real-')) roots.push(root) const workspace = join(root, 'workspace') const codexHome = join(root, 'codex-home') - const fixture = await startResponsesFixture(script) - fixtures.push(fixture) mkdirSync(workspace) mkdirSync(codexHome) + const fixture = await startResponsesFixture(typeof script === 'function' ? script(workspace) : script) + fixtures.push(fixture) writeFileSync(join(codexHome, 'config.toml'), [ 'model = "fixture-model"', 'model_provider = "fixture"', @@ -133,7 +135,7 @@ async function realRuntime(): Promise { } async function realHarness( - script: readonly ResponsesBehavior[], + script: ResponsesScript, permissionMode?: CodexPermissionMode, ): Promise<{ readonly harness: RealHarness @@ -385,25 +387,30 @@ describe('real @openai/codex 0.147.0 product', () => { it('executes an explicitly selected dangerous bypass write in the isolated workspace', async () => { const sideEffect = 'bypass-side-effect' - const command = `echo bypass>${sideEffect}` - const commandCalls = [ - { - name: 'exec_command', - arguments: { - cmd: command, + const { harness, fixture } = await realHarness((workspace): readonly ResponsesBehavior[] => { + const target = join(workspace, sideEffect) + const command = process.platform === 'win32' + ? `powershell.exe -NoLogo -NoProfile -NonInteractive -Command "Set-Content -LiteralPath '${target.replaceAll("'", "''")}' -Value 'bypass' -NoNewline"` + : `printf bypass > ${JSON.stringify(target)}` + const commandCalls = [ + { + name: 'exec_command', + arguments: { + cmd: command, + }, }, - }, - { - name: 'shell_command', - arguments: { - command, + { + name: 'shell_command', + arguments: { + command, + }, }, - }, - ] as const - const { harness } = await realHarness([ - { kind: 'advertisedFunctionCall', choices: commandCalls }, - { kind: 'complete', text: 'bypass complete' }, - ], 'dangerously-bypass-approvals-and-sandbox') + ] as const + return [ + { kind: 'advertisedFunctionCall', choices: commandCalls }, + { kind: 'complete', text: 'bypass complete' }, + ] + }, 'dangerously-bypass-approvals-and-sandbox') const target = join(harness.workspace, sideEffect) const run = await harness.ctx.subagents.start('codex', { prompt: [{ type: 'text', text: 'Create the fixture side effect.' }], @@ -414,6 +421,7 @@ describe('real @openai/codex 0.147.0 product', () => { output: [{ type: 'text', text: 'bypass complete' }], stopReason: 'completed', }) + expect(existsSync(target), JSON.stringify(fixture.requests.at(-1)?.body.input)).toBe(true) expect(readFileSync(target, 'utf8').trim()).toBe('bypass') await run.dispose() await expectQuiescent(harness.handles) diff --git a/scripts/coverage-partitions.spec.ts b/scripts/coverage-partitions.spec.ts index 81040f650e..749a63d6a7 100644 --- a/scripts/coverage-partitions.spec.ts +++ b/scripts/coverage-partitions.spec.ts @@ -129,7 +129,7 @@ describe('coverage partition coordinator', () => { const runCommand = vi.fn(async (command: CoverageCommand) => { await writeBlob(command) return command.label === 'partition 2/2' - ? { exitCode: 1, signalCode: null } + ? { exitCode: 1, signalCode: null, outputTail: 'specific Vitest failure' } : passed }) const coordinator = new CoveragePartitionCoordinator({ @@ -141,6 +141,9 @@ describe('coverage partition coordinator', () => { await expect(coordinator.run()).resolves.toBe(1) expect(reported).toHaveBeenCalledWith('coverage-partitions: FAIL partition 2/2 (exit 1)') + expect(reported).toHaveBeenCalledWith( + 'coverage-partitions: output tail for partition 2/2:\nspecific Vitest failure', + ) expect(runCommand).toHaveBeenCalledTimes(3) }) diff --git a/scripts/coverage-partitions.ts b/scripts/coverage-partitions.ts index 9302eea612..d9abc06101 100644 --- a/scripts/coverage-partitions.ts +++ b/scripts/coverage-partitions.ts @@ -34,6 +34,8 @@ export interface CoverageCommandResult { signalCode: NodeJS.Signals | null /** Spawn failure recorded independently from process completion. */ error?: string + /** Bounded combined stdout/stderr tail repeated when the command fails. */ + outputTail?: string } /** Execute one coordinator command with inherited output. */ @@ -120,6 +122,9 @@ export class CoveragePartitionCoordinator { const result = await this.runCommand(command) if (commandFailed(result)) { console.error(`coverage-partitions: FAIL ${command.label} (${commandFailureReason(result)})`) + if (result.outputTail !== undefined && result.outputTail !== '') { + console.error(`coverage-partitions: output tail for ${command.label}:\n${result.outputTail}`) + } } return result })) @@ -202,6 +207,7 @@ export class CoveragePartitionCoordinator { /** Spawn one pnpm-backed command without a platform shell. */ function runCoverageCommand(command: CoverageCommand): Promise { return new Promise((resolveCommand) => { + let outputTail = '' const env = { ...process.env } for (const [name, value] of Object.entries(command.env)) { if (value === undefined) Reflect.deleteProperty(env, name) @@ -210,17 +216,32 @@ function runCoverageCommand(command: CoverageCommand): Promise { + process.stdout.write(chunk) + outputTail = appendOutputTail(outputTail, chunk) + }) + child.stderr.on('data', (chunk: string) => { + process.stderr.write(chunk) + outputTail = appendOutputTail(outputTail, chunk) }) child.once('error', (error: Error) => { - resolveCommand({ exitCode: null, signalCode: null, error: error.message }) + resolveCommand({ exitCode: null, signalCode: null, error: error.message, outputTail }) }) - child.once('exit', (exitCode, signalCode) => { - resolveCommand({ exitCode, signalCode }) + child.once('close', (exitCode, signalCode) => { + resolveCommand({ exitCode, signalCode, outputTail }) }) }) } +function appendOutputTail(previous: string, chunk: string): string { + const combined = previous + chunk + return combined.length <= 65_536 ? combined : combined.slice(-65_536) +} + function commandFailed(result: CoverageCommandResult): boolean { return result.exitCode !== 0 || result.signalCode !== null || result.error !== undefined }