From 397267cf9627b5affcca445958826a3d7d124aa0 Mon Sep 17 00:00:00 2001 From: Qwen Autofix Date: Mon, 7 Sep 2026 07:31:12 +0000 Subject: [PATCH 1/3] ci(e2e): bound the sandbox:none first attempt so a slow death stays retryable (#11268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 34083672277's sandbox:none shard 1/3 degraded under shared-host pressure instead of dying fast: it burned 40 of the 60 job minutes beside 14-18 minute sibling shards, then exited with every test green and no vitest FAIL line — the known transient class, but at a job- elapsed of ~2591s the budget gate had no shard-time left, so the retry added in #10355 never fired and the run filed a per-commit issue. Bound the first attempt with timeout(1) at 1500s: above the worst measured healthy shard (~21min under pressure) and still inside the 2100s gate, so a degraded attempt is declared dead while its retry is still reachable. The retry itself stays unbounded; the gate has already reserved its shard-time. Co-authored-by: Qwen-Coder --- .github/workflows/e2e.yml | 17 +++++++- scripts/tests/e2e-shard-retry.test.js | 59 +++++++++++++++++++++++++-- scripts/tests/e2e-workflow.test.js | 23 +++++++++-- 3 files changed, 90 insertions(+), 9 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 618e4e1ffe7..a7e9930f337 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -351,6 +351,9 @@ jobs: run_shard() { QWEN_E2E_RENDERER=ink npm run test:integration:sandbox:none -- --exclude '**/interactive/cron-interactive.test.ts' --exclude '**/channel-plugin.test.ts' --exclude '**/chat-transcript-document.test.ts' --shard='${{ matrix.shard }}' } + # timeout(1) execs a file, not a shell function; the export is + # what lets the bounded child below resolve it. + export -f run_shard # One bounded retry: pool runners' sandbox:none shards die under # shared-host pressure with every test green and no vitest FAIL # line — runs 33293739505, 33302550436 and 33317457036 failed @@ -364,7 +367,19 @@ jobs: # be cancelled mid-flight by timeout-minutes. The docker leg # keeps no retry: two ~30min attempts would outrun the job's # timeout-minutes. - run_shard || { + # The first attempt is wall-clock bounded: the same pressure can + # degrade a shard slowly instead of killing it — run 34083672277's + # shard 1/3 burned 40 of the 60 job minutes beside 14-18 minute + # siblings and died with every test green and the retry budget + # already spent, which forfeits the retry this block exists to + # enable. 1500s clears the worst measured healthy shard (~21min + # under pressure, per the gate arithmetic below) and still lands + # a timeout-killed attempt inside the 2100s gate. Without + # --foreground, timeout signals the attempt's whole process + # group, and --kill-after promotes a wedged one to KILL. The + # retry stays unbounded: the gate below has already reserved its + # shard-time. + timeout --kill-after=30 1500 bash -c run_shard || { elapsed=$(( $(date +%s) - ${E2E_JOB_START_EPOCH:-0} )) # Budget gate: a retry needs a full shard-time inside the job's # timeout-minutes. 3600s minus a 25-minute reserve — the worst diff --git a/scripts/tests/e2e-shard-retry.test.js b/scripts/tests/e2e-shard-retry.test.js index d40c3f37c4c..0a9a9ad9524 100644 --- a/scripts/tests/e2e-shard-retry.test.js +++ b/scripts/tests/e2e-shard-retry.test.js @@ -34,7 +34,7 @@ describe('e2e workflow sandbox:none shard retry execution', () => { .replaceAll('${{ matrix.sandbox }}', 'sandbox:none') .replaceAll('${{ matrix.shard }}', '1/3'); - function runStepScript({ failCalls, elapsedSeconds }) { + function runStepScript({ failCalls, elapsedSeconds, timeoutExit }) { const dir = mkdtempSync(join(tmpdir(), 'qwen-e2e-retry-')); try { const callCountFile = join(dir, 'npm-call-count'); @@ -63,6 +63,27 @@ describe('e2e workflow sandbox:none shard retry execution', () => { ['#!/usr/bin/env bash', `printf '%s' '${now}'`].join('\n'), ); chmodSync(dateStub, 0o755); + // GNU timeout stand-in for the first-attempt bound: macOS runners + // carry no `timeout`, and a real one would race the pinned clock + // anyway. It records each duration it is asked to enforce, honors + // TIMEOUT_STUB_EXIT as the killed-attempt path (timeout's own 124), + // and otherwise execs the wrapped command so the retry wiring stays + // observable. + const timeoutDurationsFile = join(dir, 'timeout-durations'); + writeFileSync(timeoutDurationsFile, ''); + const timeoutStub = join(dir, 'timeout'); + writeFileSync( + timeoutStub, + [ + '#!/usr/bin/env bash', + 'while [[ "$1" == -* ]]; do shift; done', + 'printf "%s\\n" "$1" >> "$TIMEOUT_DURATIONS_FILE"', + 'shift', + 'if [[ -n "${TIMEOUT_STUB_EXIT:-}" ]]; then exit "$TIMEOUT_STUB_EXIT"; fi', + '"$@"', + ].join('\n'), + ); + chmodSync(timeoutStub, 0o755); const scriptFile = join(dir, 'run-e2e-tests.sh'); writeFileSync(scriptFile, script); let exitCode = 0; @@ -74,6 +95,10 @@ describe('e2e workflow sandbox:none shard retry execution', () => { PATH: `${dir}:${process.env.PATH}`, NPM_CALL_COUNT_FILE: callCountFile, NPM_FAIL_CALLS: failCalls, + TIMEOUT_DURATIONS_FILE: timeoutDurationsFile, + ...(timeoutExit === undefined + ? {} + : { TIMEOUT_STUB_EXIT: String(timeoutExit) }), E2E_JOB_START_EPOCH: String(now - elapsedSeconds), }, encoding: 'utf8', @@ -86,6 +111,9 @@ describe('e2e workflow sandbox:none shard retry execution', () => { exitCode, output, npmCalls: Number(readFileSync(callCountFile, 'utf8')), + timeoutDurations: readFileSync(timeoutDurationsFile, 'utf8') + .split('\n') + .filter(Boolean), }; } finally { rmSync(dir, { recursive: true, force: true }); @@ -96,11 +124,13 @@ describe('e2e workflow sandbox:none shard retry execution', () => { // The green first-attempt path needs its own witness: without one, an // unconditional pre-gate side effect (a spurious ::warning:: before // `run_shard || {`) ships with every other witness green. - const { exitCode, npmCalls, output } = runStepScript({ + const { exitCode, npmCalls, output, timeoutDurations } = runStepScript({ failCalls: '', elapsedSeconds: 1200, }); expect(npmCalls).toBe(1); + // The bound wraps the (only) attempt even when nothing fails. + expect(timeoutDurations).toEqual(['1500']); expect(output).not.toContain('::warning::'); expect(output).not.toContain('::error::'); expect(exitCode).toBe(0); @@ -109,11 +139,14 @@ describe('e2e workflow sandbox:none shard retry execution', () => { it('retries a shard that dies once and passes on the second attempt', () => { // The transient class the retry exists for: first attempt dead, re-run // green (runs 33293739505, 33302550436, 33317457036). - const { exitCode, npmCalls, output } = runStepScript({ + const { exitCode, npmCalls, output, timeoutDurations } = runStepScript({ failCalls: '1', elapsedSeconds: 1200, }); expect(npmCalls).toBe(2); + // One bounded attempt, not two: the retry stays unbounded because the + // budget gate has already reserved its shard-time. + expect(timeoutDurations).toEqual(['1500']); expect(output).toContain('::warning::'); expect(exitCode).toBe(0); }); @@ -132,11 +165,29 @@ describe('e2e workflow sandbox:none shard retry execution', () => { it('retries at exactly the 2100s budget-gate threshold', () => { // The gate admits a retry at elapsed <= 2100. Threshold mutations in // either direction must not ship silently between the 1200/3000 probes. - const { exitCode, npmCalls, output } = runStepScript({ + const { exitCode, npmCalls, output, timeoutDurations } = runStepScript({ failCalls: '1', elapsedSeconds: 2100, }); expect(npmCalls).toBe(2); + expect(timeoutDurations).toEqual(['1500']); + expect(output).toContain('::warning::'); + expect(exitCode).toBe(0); + }); + + it('retries a first attempt killed by its own wall-clock bound', () => { + // The run 34083672277 shape under the bound: timeout(1) kills the + // degraded first attempt (exit 124, the shard never reporting) and that + // exit must flow through the same budget gate as any other + // first-attempt death. + const { exitCode, npmCalls, output, timeoutDurations } = runStepScript({ + failCalls: '', + elapsedSeconds: 1200, + timeoutExit: 124, + }); + expect(timeoutDurations).toEqual(['1500']); + // Only the retried attempt ever reached the shard command. + expect(npmCalls).toBe(1); expect(output).toContain('::warning::'); expect(exitCode).toBe(0); }); diff --git a/scripts/tests/e2e-workflow.test.js b/scripts/tests/e2e-workflow.test.js index 6733a2fb61a..1aaf4af41fd 100644 --- a/scripts/tests/e2e-workflow.test.js +++ b/scripts/tests/e2e-workflow.test.js @@ -174,16 +174,31 @@ describe('e2e workflow', () => { it('retries the sandbox:none shard exactly once', () => { expect(runStep.run).toContain('run_shard || {'); - // Definition + first attempt + one retry: the second attempt's exit - // status is the step's, and a third attempt would burn pool time for - // nothing. - expect(runStep.run.match(/run_shard/g)).toHaveLength(3); + // Definition + export into the bounded first-attempt child + first + // attempt + one retry: the second attempt's exit status is the + // step's, and a third attempt would burn pool time for nothing. + expect(runStep.run.match(/run_shard/g)).toHaveLength(4); // End-anchored scope: the retry is the group's last command and the // group is the script's last statement. A retry moved outside the // `|| { ... }` would run unconditionally, re-running green shards too. expect(runStep.run).toMatch(/run_shard\s*\n\s*\}\s*\n\s*fi\s*$/); }); + it('bounds only the first attempt so a slow death stays retryable', () => { + // Run 34083672277's shard 1/3 degraded instead of dying: 40 of the 60 + // job minutes beside 14-18 minute siblings, then dead with every test + // green and the 2100s budget gate already out of reach — an unbounded + // first attempt forfeits the retry it exists to enable. The 1500s cap + // clears the worst measured healthy shard (~21min) and still lands + // the killed attempt inside the gate. + expect(runStep.run).toContain( + 'timeout --kill-after=30 1500 bash -c run_shard || {', + ); + // Exactly one bounded attempt: the retry stays unbounded because the + // gate has already reserved its shard-time. + expect(runStep.run.match(/bash -c run_shard/g)).toHaveLength(1); + }); + it('gates the retry on the remaining job budget', () => { // The retried run_shard is reachable only behind an elapsed-time check // that exits the step when the job cannot fit another shard. Shape From d020b46cd8a9a3f0010e26fd11a5f14540b8a714 Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Mon, 7 Sep 2026 21:58:03 +0800 Subject: [PATCH 2/3] Revert "ci(e2e): bound the sandbox:none first attempt so a slow death stays retryable (#11268)" This reverts commit 397267cf9627b5affcca445958826a3d7d124aa0. --- .github/workflows/e2e.yml | 17 +------- scripts/tests/e2e-shard-retry.test.js | 59 ++------------------------- scripts/tests/e2e-workflow.test.js | 23 ++--------- 3 files changed, 9 insertions(+), 90 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b10e519a6d4..ff63d7db458 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -367,9 +367,6 @@ jobs: run_shard() { QWEN_E2E_RENDERER=ink npm run test:integration:sandbox:none -- --exclude '**/interactive/cron-interactive.test.ts' --exclude '**/channel-plugin.test.ts' --exclude '**/chat-transcript-document.test.ts' --shard='${{ matrix.shard }}' } - # timeout(1) execs a file, not a shell function; the export is - # what lets the bounded child below resolve it. - export -f run_shard # One bounded retry: pool runners' sandbox:none shards die under # shared-host pressure with every test green and no vitest FAIL # line — runs 33293739505, 33302550436 and 33317457036 failed @@ -383,19 +380,7 @@ jobs: # be cancelled mid-flight by timeout-minutes. The docker leg # keeps no retry: two ~30min attempts would outrun the job's # timeout-minutes. - # The first attempt is wall-clock bounded: the same pressure can - # degrade a shard slowly instead of killing it — run 34083672277's - # shard 1/3 burned 40 of the 60 job minutes beside 14-18 minute - # siblings and died with every test green and the retry budget - # already spent, which forfeits the retry this block exists to - # enable. 1500s clears the worst measured healthy shard (~21min - # under pressure, per the gate arithmetic below) and still lands - # a timeout-killed attempt inside the 2100s gate. Without - # --foreground, timeout signals the attempt's whole process - # group, and --kill-after promotes a wedged one to KILL. The - # retry stays unbounded: the gate below has already reserved its - # shard-time. - timeout --kill-after=30 1500 bash -c run_shard || { + run_shard || { elapsed=$(( $(date +%s) - ${E2E_JOB_START_EPOCH:-0} )) # Budget gate: a retry needs a full shard-time inside the job's # timeout-minutes. 3600s minus a 25-minute reserve — the worst diff --git a/scripts/tests/e2e-shard-retry.test.js b/scripts/tests/e2e-shard-retry.test.js index 0a9a9ad9524..d40c3f37c4c 100644 --- a/scripts/tests/e2e-shard-retry.test.js +++ b/scripts/tests/e2e-shard-retry.test.js @@ -34,7 +34,7 @@ describe('e2e workflow sandbox:none shard retry execution', () => { .replaceAll('${{ matrix.sandbox }}', 'sandbox:none') .replaceAll('${{ matrix.shard }}', '1/3'); - function runStepScript({ failCalls, elapsedSeconds, timeoutExit }) { + function runStepScript({ failCalls, elapsedSeconds }) { const dir = mkdtempSync(join(tmpdir(), 'qwen-e2e-retry-')); try { const callCountFile = join(dir, 'npm-call-count'); @@ -63,27 +63,6 @@ describe('e2e workflow sandbox:none shard retry execution', () => { ['#!/usr/bin/env bash', `printf '%s' '${now}'`].join('\n'), ); chmodSync(dateStub, 0o755); - // GNU timeout stand-in for the first-attempt bound: macOS runners - // carry no `timeout`, and a real one would race the pinned clock - // anyway. It records each duration it is asked to enforce, honors - // TIMEOUT_STUB_EXIT as the killed-attempt path (timeout's own 124), - // and otherwise execs the wrapped command so the retry wiring stays - // observable. - const timeoutDurationsFile = join(dir, 'timeout-durations'); - writeFileSync(timeoutDurationsFile, ''); - const timeoutStub = join(dir, 'timeout'); - writeFileSync( - timeoutStub, - [ - '#!/usr/bin/env bash', - 'while [[ "$1" == -* ]]; do shift; done', - 'printf "%s\\n" "$1" >> "$TIMEOUT_DURATIONS_FILE"', - 'shift', - 'if [[ -n "${TIMEOUT_STUB_EXIT:-}" ]]; then exit "$TIMEOUT_STUB_EXIT"; fi', - '"$@"', - ].join('\n'), - ); - chmodSync(timeoutStub, 0o755); const scriptFile = join(dir, 'run-e2e-tests.sh'); writeFileSync(scriptFile, script); let exitCode = 0; @@ -95,10 +74,6 @@ describe('e2e workflow sandbox:none shard retry execution', () => { PATH: `${dir}:${process.env.PATH}`, NPM_CALL_COUNT_FILE: callCountFile, NPM_FAIL_CALLS: failCalls, - TIMEOUT_DURATIONS_FILE: timeoutDurationsFile, - ...(timeoutExit === undefined - ? {} - : { TIMEOUT_STUB_EXIT: String(timeoutExit) }), E2E_JOB_START_EPOCH: String(now - elapsedSeconds), }, encoding: 'utf8', @@ -111,9 +86,6 @@ describe('e2e workflow sandbox:none shard retry execution', () => { exitCode, output, npmCalls: Number(readFileSync(callCountFile, 'utf8')), - timeoutDurations: readFileSync(timeoutDurationsFile, 'utf8') - .split('\n') - .filter(Boolean), }; } finally { rmSync(dir, { recursive: true, force: true }); @@ -124,13 +96,11 @@ describe('e2e workflow sandbox:none shard retry execution', () => { // The green first-attempt path needs its own witness: without one, an // unconditional pre-gate side effect (a spurious ::warning:: before // `run_shard || {`) ships with every other witness green. - const { exitCode, npmCalls, output, timeoutDurations } = runStepScript({ + const { exitCode, npmCalls, output } = runStepScript({ failCalls: '', elapsedSeconds: 1200, }); expect(npmCalls).toBe(1); - // The bound wraps the (only) attempt even when nothing fails. - expect(timeoutDurations).toEqual(['1500']); expect(output).not.toContain('::warning::'); expect(output).not.toContain('::error::'); expect(exitCode).toBe(0); @@ -139,14 +109,11 @@ describe('e2e workflow sandbox:none shard retry execution', () => { it('retries a shard that dies once and passes on the second attempt', () => { // The transient class the retry exists for: first attempt dead, re-run // green (runs 33293739505, 33302550436, 33317457036). - const { exitCode, npmCalls, output, timeoutDurations } = runStepScript({ + const { exitCode, npmCalls, output } = runStepScript({ failCalls: '1', elapsedSeconds: 1200, }); expect(npmCalls).toBe(2); - // One bounded attempt, not two: the retry stays unbounded because the - // budget gate has already reserved its shard-time. - expect(timeoutDurations).toEqual(['1500']); expect(output).toContain('::warning::'); expect(exitCode).toBe(0); }); @@ -165,29 +132,11 @@ describe('e2e workflow sandbox:none shard retry execution', () => { it('retries at exactly the 2100s budget-gate threshold', () => { // The gate admits a retry at elapsed <= 2100. Threshold mutations in // either direction must not ship silently between the 1200/3000 probes. - const { exitCode, npmCalls, output, timeoutDurations } = runStepScript({ + const { exitCode, npmCalls, output } = runStepScript({ failCalls: '1', elapsedSeconds: 2100, }); expect(npmCalls).toBe(2); - expect(timeoutDurations).toEqual(['1500']); - expect(output).toContain('::warning::'); - expect(exitCode).toBe(0); - }); - - it('retries a first attempt killed by its own wall-clock bound', () => { - // The run 34083672277 shape under the bound: timeout(1) kills the - // degraded first attempt (exit 124, the shard never reporting) and that - // exit must flow through the same budget gate as any other - // first-attempt death. - const { exitCode, npmCalls, output, timeoutDurations } = runStepScript({ - failCalls: '', - elapsedSeconds: 1200, - timeoutExit: 124, - }); - expect(timeoutDurations).toEqual(['1500']); - // Only the retried attempt ever reached the shard command. - expect(npmCalls).toBe(1); expect(output).toContain('::warning::'); expect(exitCode).toBe(0); }); diff --git a/scripts/tests/e2e-workflow.test.js b/scripts/tests/e2e-workflow.test.js index 427f0167b66..12d1131d561 100644 --- a/scripts/tests/e2e-workflow.test.js +++ b/scripts/tests/e2e-workflow.test.js @@ -199,31 +199,16 @@ describe('e2e workflow', () => { it('retries the sandbox:none shard exactly once', () => { expect(runStep.run).toContain('run_shard || {'); - // Definition + export into the bounded first-attempt child + first - // attempt + one retry: the second attempt's exit status is the - // step's, and a third attempt would burn pool time for nothing. - expect(runStep.run.match(/run_shard/g)).toHaveLength(4); + // Definition + first attempt + one retry: the second attempt's exit + // status is the step's, and a third attempt would burn pool time for + // nothing. + expect(runStep.run.match(/run_shard/g)).toHaveLength(3); // End-anchored scope: the retry is the group's last command and the // group is the script's last statement. A retry moved outside the // `|| { ... }` would run unconditionally, re-running green shards too. expect(runStep.run).toMatch(/run_shard\s*\n\s*\}\s*\n\s*fi\s*$/); }); - it('bounds only the first attempt so a slow death stays retryable', () => { - // Run 34083672277's shard 1/3 degraded instead of dying: 40 of the 60 - // job minutes beside 14-18 minute siblings, then dead with every test - // green and the 2100s budget gate already out of reach — an unbounded - // first attempt forfeits the retry it exists to enable. The 1500s cap - // clears the worst measured healthy shard (~21min) and still lands - // the killed attempt inside the gate. - expect(runStep.run).toContain( - 'timeout --kill-after=30 1500 bash -c run_shard || {', - ); - // Exactly one bounded attempt: the retry stays unbounded because the - // gate has already reserved its shard-time. - expect(runStep.run.match(/bash -c run_shard/g)).toHaveLength(1); - }); - it('gates the retry on the remaining job budget', () => { // The retried run_shard is reachable only behind an elapsed-time check // that exits the step when the job cannot fit another shard. Shape From 20b6fc2c8af1c84d15f1deb92d2c930886b0347e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=93=E8=89=AF?= <1204183885@qq.com> Date: Mon, 7 Sep 2026 21:05:36 +0800 Subject: [PATCH 3/3] ci: run Linux E2E with Vitest forks (#11290) * ci: benchmark Vitest forks against E2E shards * ci: prepare E2E fork configuration for review --- .github/workflows/e2e.yml | 13 +++++-------- scripts/tests/e2e-workflow.test.js | 14 +++++++++++++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index ff63d7db458..59fb1e58b33 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -127,13 +127,10 @@ jobs: sandbox: - 'sandbox:none' - 'sandbox:docker' - # The suite is ~16min of wall clock on one runner, dominated by a long - # tail of sdk-typescript files. vitest assigns files to shards by path - # hash, so those spread out instead of clustering in one shard. + # Keep three-way test concurrency inside one runner so each sandbox + # shares setup work instead of occupying three pool runners. shard: - - '1/3' - - '2/3' - - '3/3' + - '1/1' node-version: - '22.x' steps: @@ -362,10 +359,10 @@ jobs: # test:integration:sandbox:docker: that script would rebuild the image # the step above just built. if [[ "${{ matrix.sandbox }}" == "sandbox:docker" ]]; then - npx cross-env QWEN_E2E_RENDERER=ink QWEN_SANDBOX=docker vitest run --root ./integration-tests --exclude '**/interactive/cron-interactive.test.ts' --exclude '**/channel-plugin.test.ts' --exclude '**/chat-transcript-document.test.ts' --shard='${{ matrix.shard }}' 9>&- + npx cross-env QWEN_E2E_RENDERER=ink QWEN_SANDBOX=docker vitest run --root ./integration-tests --exclude '**/interactive/cron-interactive.test.ts' --exclude '**/channel-plugin.test.ts' --exclude '**/chat-transcript-document.test.ts' --poolOptions.forks.maxForks=3 --shard='${{ matrix.shard }}' 9>&- else run_shard() { - QWEN_E2E_RENDERER=ink npm run test:integration:sandbox:none -- --exclude '**/interactive/cron-interactive.test.ts' --exclude '**/channel-plugin.test.ts' --exclude '**/chat-transcript-document.test.ts' --shard='${{ matrix.shard }}' + QWEN_E2E_RENDERER=ink npm run test:integration:sandbox:none -- --exclude '**/interactive/cron-interactive.test.ts' --exclude '**/channel-plugin.test.ts' --exclude '**/chat-transcript-document.test.ts' --poolOptions.forks.maxForks=3 --shard='${{ matrix.shard }}' } # One bounded retry: pool runners' sandbox:none shards die under # shared-host pressure with every test green and no vitest FAIL diff --git a/scripts/tests/e2e-workflow.test.js b/scripts/tests/e2e-workflow.test.js index 12d1131d561..d6a6fe19964 100644 --- a/scripts/tests/e2e-workflow.test.js +++ b/scripts/tests/e2e-workflow.test.js @@ -34,6 +34,18 @@ describe('e2e workflow', () => { expect(group).toContain('github.head_ref || github.ref_name'); }); + it('runs three Vitest forks on one Linux runner per sandbox', () => { + const linuxJob = yml.jobs['e2e-test-linux']; + const runStep = linuxJob.steps.find( + (step) => step.name === 'Run E2E tests', + ); + + expect(linuxJob.strategy.matrix.shard).toEqual(['1/1']); + expect(runStep.run.match(/--poolOptions\.forks\.maxForks=3/g)).toHaveLength( + 2, + ); + }); + describe('sandbox image preparation', () => { const steps = yml.jobs['e2e-test-linux'].steps; const setupStep = steps.find((step) => step.name === 'Set up Docker'); @@ -193,7 +205,7 @@ describe('e2e workflow', () => { // shard and exclude coverage lives only in this argument list. The // excludes are shared verbatim with the docker leg above. expect(runStep.run).toContain( - "npm run test:integration:sandbox:none -- --exclude '**/interactive/cron-interactive.test.ts' --exclude '**/channel-plugin.test.ts' --exclude '**/chat-transcript-document.test.ts' --shard='${{ matrix.shard }}'", + "npm run test:integration:sandbox:none -- --exclude '**/interactive/cron-interactive.test.ts' --exclude '**/channel-plugin.test.ts' --exclude '**/chat-transcript-document.test.ts' --poolOptions.forks.maxForks=3 --shard='${{ matrix.shard }}'", ); });