|
5 | 5 | * sleep injection is wired through `TestDeps.sleep` to avoid real delays. |
6 | 6 | */ |
7 | 7 |
|
8 | | -import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; |
| 8 | +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; |
9 | 9 | import { tmpdir } from 'node:os'; |
10 | 10 | import { join } from 'node:path'; |
11 | 11 | import type { Command } from 'commander'; |
@@ -2725,6 +2725,7 @@ describe('runTestRunAll — batch fresh run', () => { |
2725 | 2725 | fetchImpl, |
2726 | 2726 | stdout: line => stdoutLines.push(line), |
2727 | 2727 | stderr: () => undefined, |
| 2728 | + env: {} as NodeJS.ProcessEnv, |
2728 | 2729 | sleep: instantSleep, |
2729 | 2730 | }, |
2730 | 2731 | ); |
@@ -3458,6 +3459,7 @@ describe('[B-E2E-01] runTestRunAll --wait: non-passed runs must exit 1 (regressi |
3458 | 3459 | fetchImpl, |
3459 | 3460 | stdout: line => stdoutLines.push(line), |
3460 | 3461 | stderr: () => undefined, |
| 3462 | + env: {} as NodeJS.ProcessEnv, |
3461 | 3463 | sleep: instantSleep, |
3462 | 3464 | }, |
3463 | 3465 | ); |
@@ -3866,6 +3868,7 @@ describe('[finding-5] runTestRunAll --wait: RequestTimeoutError during fan-out p |
3866 | 3868 | fetchImpl, |
3867 | 3869 | stdout: line => stdoutLines.push(line), |
3868 | 3870 | stderr: () => undefined, |
| 3871 | + env: {} as NodeJS.ProcessEnv, |
3869 | 3872 | sleep: instantSleep, |
3870 | 3873 | }, |
3871 | 3874 | ).catch(e => e); |
@@ -3957,3 +3960,125 @@ describe('runTestRun --wait — InterruptError graceful detach (DEV-331)', () => |
3957 | 3960 | expect(stderrBlock).toContain('testsprite test wait run_abc'); |
3958 | 3961 | }); |
3959 | 3962 | }); |
| 3963 | + |
| 3964 | +describe('gh-output integration on run --all --wait (issue #99 reshape)', () => { |
| 3965 | + function makeTerminalRun(runId: string, testId: string, status: string): RunResponse { |
| 3966 | + return { |
| 3967 | + runId, |
| 3968 | + testId, |
| 3969 | + projectId: 'project_be', |
| 3970 | + userId: 'user_1', |
| 3971 | + status: status as RunResponse['status'], |
| 3972 | + source: 'cli', |
| 3973 | + createdAt: '2026-06-09T11:00:00.000Z', |
| 3974 | + startedAt: '2026-06-09T11:00:01.000Z', |
| 3975 | + finishedAt: '2026-06-09T11:00:30.000Z', |
| 3976 | + codeVersion: 'v1', |
| 3977 | + targetUrl: 'https://api.example.com', |
| 3978 | + createdFrom: 'cli', |
| 3979 | + failedStepIndex: null, |
| 3980 | + failureKind: null, |
| 3981 | + error: null, |
| 3982 | + videoUrl: null, |
| 3983 | + stepSummary: { |
| 3984 | + total: 3, |
| 3985 | + completed: 3, |
| 3986 | + passedCount: status === 'passed' ? 3 : 0, |
| 3987 | + failedCount: 0, |
| 3988 | + }, |
| 3989 | + }; |
| 3990 | + } |
| 3991 | + |
| 3992 | + function mixedHarness() { |
| 3993 | + const { credentialsPath } = makeCreds(); |
| 3994 | + const mixedBatch: BatchRunFreshResponse = { |
| 3995 | + accepted: [ |
| 3996 | + { testId: 'test_p', runId: 'run_p', enqueuedAt: '2026-06-09T11:00:00.000Z' }, |
| 3997 | + { testId: 'test_f', runId: 'run_f', enqueuedAt: '2026-06-09T11:00:02.000Z' }, |
| 3998 | + ], |
| 3999 | + conflicts: [], |
| 4000 | + deferred: [], |
| 4001 | + skippedFrontend: [], |
| 4002 | + skippedIntegration: [], |
| 4003 | + }; |
| 4004 | + const fetchImpl = makeFetch((url, init) => { |
| 4005 | + if ((init.method ?? 'GET') === 'POST') return { body: mixedBatch }; |
| 4006 | + const runId = url.split('/runs/')[1]?.split('?')[0] ?? ''; |
| 4007 | + if (runId === 'run_p') return { body: makeTerminalRun('run_p', 'test_p', 'passed') }; |
| 4008 | + if (runId === 'run_f') return { body: makeTerminalRun('run_f', 'test_f', 'failed') }; |
| 4009 | + return errorBody('NOT_FOUND'); |
| 4010 | + }); |
| 4011 | + return { credentialsPath, fetchImpl }; |
| 4012 | + } |
| 4013 | + |
| 4014 | + it('under Actions with --output json: stdout stays parseable JSON, ::error:: goes to stderr', async () => { |
| 4015 | + const { credentialsPath, fetchImpl } = mixedHarness(); |
| 4016 | + const stdoutLines: string[] = []; |
| 4017 | + const stderrLines: string[] = []; |
| 4018 | + const err = await runTestRunAll( |
| 4019 | + { |
| 4020 | + profile: 'default', |
| 4021 | + output: 'json', |
| 4022 | + debug: false, |
| 4023 | + projectId: 'project_be', |
| 4024 | + wait: true, |
| 4025 | + timeoutSeconds: 60, |
| 4026 | + maxConcurrency: 5, |
| 4027 | + }, |
| 4028 | + { |
| 4029 | + credentialsPath, |
| 4030 | + fetchImpl, |
| 4031 | + stdout: line => stdoutLines.push(line), |
| 4032 | + stderr: line => stderrLines.push(line), |
| 4033 | + env: { GITHUB_ACTIONS: 'true' } as NodeJS.ProcessEnv, |
| 4034 | + sleep: instantSleep, |
| 4035 | + }, |
| 4036 | + ).catch(e => e); |
| 4037 | + expect(err).toMatchObject({ exitCode: 1 }); |
| 4038 | + // The documented machine envelope must remain parseable as-is. |
| 4039 | + const payload = JSON.parse(stdoutLines.join('\n')) as { accepted?: unknown[] }; |
| 4040 | + expect(Array.isArray(payload.accepted)).toBe(true); |
| 4041 | + expect(stdoutLines.some(line => line.startsWith('::error'))).toBe(false); |
| 4042 | + const annotations = stderrLines.filter(line => line.startsWith('::error')); |
| 4043 | + expect(annotations).toHaveLength(1); |
| 4044 | + expect(annotations[0]).toContain('test_f'); |
| 4045 | + }); |
| 4046 | + |
| 4047 | + it('--gh-output --summary-file writes the reduced artifact even though the gate exits 1', async () => { |
| 4048 | + const { credentialsPath, fetchImpl } = mixedHarness(); |
| 4049 | + const dir = mkdtempSync(join(tmpdir(), 'cli-gh-output-')); |
| 4050 | + const summaryFile = join(dir, 'summary.json'); |
| 4051 | + const stdoutLines: string[] = []; |
| 4052 | + const err = await runTestRunAll( |
| 4053 | + { |
| 4054 | + profile: 'default', |
| 4055 | + output: 'text', |
| 4056 | + debug: false, |
| 4057 | + projectId: 'project_be', |
| 4058 | + wait: true, |
| 4059 | + timeoutSeconds: 60, |
| 4060 | + maxConcurrency: 5, |
| 4061 | + ghOutput: true, |
| 4062 | + summaryFile, |
| 4063 | + }, |
| 4064 | + { |
| 4065 | + credentialsPath, |
| 4066 | + fetchImpl, |
| 4067 | + stdout: line => stdoutLines.push(line), |
| 4068 | + stderr: () => undefined, |
| 4069 | + env: {} as NodeJS.ProcessEnv, |
| 4070 | + sleep: instantSleep, |
| 4071 | + }, |
| 4072 | + ).catch(e => e); |
| 4073 | + expect(err).toMatchObject({ exitCode: 1 }); |
| 4074 | + const artifact = JSON.parse(readFileSync(summaryFile, 'utf8')) as { |
| 4075 | + total: number; |
| 4076 | + passed: number; |
| 4077 | + failed: number; |
| 4078 | + runs: unknown[]; |
| 4079 | + }; |
| 4080 | + expect(artifact).toMatchObject({ total: 2, passed: 1, failed: 1 }); |
| 4081 | + // Forced annotations (off-Actions) land on the text stdout, not the file. |
| 4082 | + expect(stdoutLines.some(line => line.startsWith('::error'))).toBe(true); |
| 4083 | + }); |
| 4084 | +}); |
0 commit comments