mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-01 20:22:33 +00:00
* test(integration): let workflow-completion reply waits inherit the helper default (#13552) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(integration): retarget the #13552 fix at the ready-prompt startup wait The recorded #13552 failure (run 37547210501) was InteractiveSession.start's ready-prompt wait dying at its 30s cap on all three retries while the sandbox:docker leg's container was still booting — 'Timeout (30000ms) waiting for text: "Type your message"' at interactive-session.ts, before any test body ran. The reply waits the previous round widened sit after start() and never fired in that log, so the widening could not fix the issue while 'Fixes #13552' would have closed its dedup tracker. - Revert workflow-completion.test.ts to main: the reply-wait widening was premised on a misread signature, and the inherited 120s default would have pushed this file's systematic-failure cost past the no-AK gate's 20-minute step budget (18 attempts x ~132s). - Give start's ready-prompt wait a 90s budget on self-hosted runners (the /about loop's documented stall magnitude for the shared ECS pool), keeping the tight 30s signal everywhere else. - Pin both branches with a stub CLI whose first prompt lands at 35s: self-hosted resolves, other runners reject with the exact CI string. * test(integration): harden InteractiveSession.start failure paths (#13552) Address the round-2 review findings on the #13552 ready-prompt budget: - Extract the RUNNER_ENVIRONMENT budget decision into a pure readyPromptBudgetMs(env) helper so the suite-wide decision has one home; pin every runner state instantly from scripts/tests/ (collected by PR CI via test:scripts — no pull_request job executes integration-tests/**). The helper comment records that only InteractiveSession.start was observed to stall; the TestRig readiness waits keep their own budgets and are tracked separately. - Race the ready-prompt wait against the child's onExit: a CLI that dies before printing its prompt now fails in milliseconds with its exit code instead of holding start() for the full budget (probe: a process.exit(3) child held start() for 30045ms before this change). - Close the session when the startup wait rejects so a refused start() cannot orphan the child, pty, and terminal (probe: the 30s rejection left the stub child alive before this change); cleanup sits in a nested try so a throwing dispose() cannot mask the startup error, and the original error is rethrown so the #13552 timeout signature stays byte-identical. - Keep one real-spawn wiring case (30s rejection + pid-death witness) instead of two real-time cases; capture an unexpected resolve straight into the afterEach-visible binding so the leaked-session cleanup can actually run. * test(integration): keep dead-child boot logs and stop abandoned start polls (#13552) * test(integration): word the dead-child exit line from captured output (#13552) onExit settles ahead of waitFor's 200ms scan, so a child that printed the ready prompt and died inside that gap was reported as having exited before the prompt its own tail contained. Word the failure from the captured output instead, pin the closed-flag statements the loop-text pin cannot see, and make the call-site pins tolerate the trailing-comma multi-line form prettier emits past 80 columns. * test(integration): neutral dead-child exit wording, teardown latch, tighter pins (#13552) The post-prompt-death case's outcome rode waitFor's 200ms scan phase (~27% bad band, measured), and the sawPrompt wording could claim the CLI exited before the prompt its own tail contains once a >4KB burst outruns the parent's final read. Word the line neutrally ('during startup'), swap the phase-dependent case for a deterministic >4KB-burst case, latch the onExit handler off after startup is decided so healthy teardowns skip the strip passes, and narrow the waitFor guard pin so displacing the guard into the loop body reddens it. --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
84 lines
3.8 KiB
TypeScript
84 lines
3.8 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
import { join, resolve } from 'node:path';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
// No PR-triggered job collects the behavioural cases in
|
|
// integration-tests/interactive/interactive-session.test.ts: ci.yml's
|
|
// PR-gated integration_no_ak leg runs an explicit allow-list under
|
|
// --root ./integration-tests that does not name it, and the whole-root legs
|
|
// run post-merge and at release. These byte-shape pins give the #13552
|
|
// start() contract a PR-gated witness at ~0ms through test:scripts — each
|
|
// assertion names the revert it exists to redden, and each was probe-checked
|
|
// against that revert. The behaviour behind them is covered by the
|
|
// real-spawn cases beside the module. Patterns that pin a call site must
|
|
// tolerate the trailing-comma multi-line form prettier emits past 80
|
|
// columns, or a pure reflow reddens a PR-gated lane with no behavioural
|
|
// cause.
|
|
const source = readFileSync(
|
|
join(
|
|
resolve(import.meta.dirname, '../..'),
|
|
'integration-tests/interactive/interactive-session.ts',
|
|
),
|
|
'utf8',
|
|
);
|
|
|
|
describe('InteractiveSession.start source pins (#13552)', () => {
|
|
it('threads the resolved ready-prompt budget into the waitFor', () => {
|
|
// A full revert to the literal 30_000 drops the readyPromptBudgetMs call
|
|
// and its import together, and otherwise passes every lane. The call
|
|
// sits one token short of printWidth, so the pattern must also match the
|
|
// trailing-comma multi-line form a reflow emits.
|
|
expect(source).toMatch(
|
|
/session\.waitFor\(\s*'Type your message',\s*readyPromptBudgetMs\(process\.env\),?\s*\)/,
|
|
);
|
|
});
|
|
|
|
it('races the ready-prompt wait against child exit, keeping the tail', () => {
|
|
// Reverting the race returns a dead-child boot to a full-budget wait;
|
|
// dropping the tail discards the boot log that made #13552 diagnosable
|
|
// from the job log alone. The exit line stays neutral: whether the
|
|
// prompt reached rawOutput is undecidable once a child out-writes the
|
|
// parent's final read, so a before/after claim can contradict the very
|
|
// tail attached to it.
|
|
expect(source).toContain('Promise.race([');
|
|
expect(source).toContain('ptyProcess.onExit(');
|
|
// Without the latch, close()'s kill() re-runs the handler's strip passes
|
|
// and builds a rejection no settled race observes on every healthy
|
|
// session's teardown.
|
|
expect(source).toContain('if (!startupUndecided) return;');
|
|
expect(source).toContain(
|
|
'CLI exited during startup (code ${exitCode}, signal ${signal})',
|
|
);
|
|
expect(source).toContain(
|
|
'Last 500 chars: ${stripAnsi(session.rawOutput).slice(-500)}',
|
|
);
|
|
});
|
|
|
|
it('closes the session a failed start refuses to hand out', () => {
|
|
// Dropping the catch-close orphans the child, pty, and terminal.
|
|
expect(source).toMatch(/catch \(err\) \{[\s\S]*?await session\.close\(\)/);
|
|
});
|
|
|
|
it('stops the waitFor poll when the session closes', () => {
|
|
// Without the flag, the race's abandoned loser keeps re-scanning a dead
|
|
// pty's output every 200ms until its own budget expires. The guard is
|
|
// pinned leading waitFor's loop condition — displaced into the body it
|
|
// no longer stops the poll — while prettier's operator-leading reflow of
|
|
// the condition still matches.
|
|
expect(source).toMatch(/while\s*\(\s*!this\.closed\s*&&/);
|
|
});
|
|
|
|
it('fails an abandoned poll with the closed-session error', () => {
|
|
// Deleting either statement leaves the loop text intact, so the guard
|
|
// pin alone cannot see it: close() must set the flag, and waitFor must
|
|
// throw on it.
|
|
expect(source).toContain('this.closed = true;');
|
|
expect(source).toContain('Session closed while waiting for text:');
|
|
});
|
|
});
|