Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ jobs:
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
KEEP_OUTPUT: 'true'
VERBOSE: 'true'
# Mapped for integration-tests/vitest.config.ts, which exempts
# self-hosted runners from pressure-flake unhandled errors.
RUNNER_ENVIRONMENT: '${{ runner.environment }}'
run: |-
# The docker leg runs vitest directly instead of through
# test:integration:sandbox:docker: that script would rebuild the image
Expand Down
85 changes: 85 additions & 0 deletions integration-tests/globalSetup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

// The env keys globalSetup's setup() writes, saved so a case can restore the
// suite-wide values after re-importing the module and running its lifecycle.
const SETUP_ENV_KEYS = [
'INTEGRATION_TEST_FILE_DIR',
'QWEN_CODE_INTEGRATION_TEST',
'TELEMETRY_LOG_FILE',
'E2E_TEST_FILE_DIR',
'TEST_CLI_PATH',
'VERBOSE',
'KEEP_OUTPUT',
] as const;

describe('globalSetup memory-file save/restore', () => {
let qwenHome: string;
let savedEnv: Map<string, string | undefined>;

beforeEach(async () => {
qwenHome = await mkdtemp(join(tmpdir(), 'qwen-globalsetup-test-'));
savedEnv = new Map(
[...SETUP_ENV_KEYS, 'QWEN_HOME'].map((key) => [key, process.env[key]]),
);
process.env['QWEN_HOME'] = qwenHome;
// Let teardown remove the run directories this case creates.
process.env['KEEP_OUTPUT'] = 'false';
});

afterEach(async () => {
for (const [key, value] of savedEnv) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
vi.resetModules();
await rm(qwenHome, { recursive: true, force: true });
});

// memoryFilePath is captured at module import time, so point QWEN_HOME at
// the scratch dir BEFORE a fresh import of the module.
async function loadGlobalSetup() {
vi.resetModules();
return import('./globalSetup.js');
}

it('restores the saved memory file after the run', async () => {
await writeFile(join(qwenHome, 'QWEN.md'), 'original content', 'utf-8');
const { setup, teardown } = await loadGlobalSetup();
await setup();
await writeFile(join(qwenHome, 'QWEN.md'), 'mutated by tests', 'utf-8');

await expect(teardown()).resolves.toBeUndefined();

await expect(readFile(join(qwenHome, 'QWEN.md'), 'utf-8')).resolves.toBe(
'original content',
);
});

it('does not exit an all-green run red when the restore cannot write', async () => {
// The persistent pool runners can carry a readable-but-unwritable
// QWEN.md left behind by a privileged job; before #10325 the teardown
// restore threw on it and exited every all-green E2E run on that host
// red with no failing test. Swap the file for a directory after setup()
// read it — the write then fails regardless of privilege, since root
// bypasses permission bits.
await writeFile(join(qwenHome, 'QWEN.md'), 'original content', 'utf-8');
const { setup, teardown } = await loadGlobalSetup();
await setup();
await rm(join(qwenHome, 'QWEN.md'), { force: true });
await mkdir(join(qwenHome, 'QWEN.md'));

await expect(teardown()).resolves.toBeUndefined();
Comment on lines +81 to +83

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The failure-path witness test pins only that teardown() resolves — it never asserts the warning this diff's own comment deliberately keeps ("Keep the warning visible so the poisoned host is still diagnosable"). I mutation-tested this at the reviewed commit: deleting the console.error line in globalSetup.ts leaves Tests 2 passed (2) — the mutation survives. If a later cleanup removes or debug-levels that warning, every test stays green and a poisoned pool host becomes undiagnosable: the next #10325-flavoured incident has no warning in the log and a green suite, reproducing the exact "red run with no signal" diagnosis problem this PR set out to keep visible. Spy on the warning and pin it in the failure-path case:

Suggested change
await mkdir(join(qwenHome, 'QWEN.md'));
await expect(teardown()).resolves.toBeUndefined();
await mkdir(join(qwenHome, 'QWEN.md'));
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
await expect(teardown()).resolves.toBeUndefined();
expect(errSpy).toHaveBeenCalledWith(
expect.stringContaining('Warning: could not restore'),
expect.anything(),
);
errSpy.mockRestore();

Fix witness: with this assertion in place, deleting the console.error line from integration-tests/globalSetup.ts turns the failure-path case red — verified at the reviewed commit: mutant + assertion → 1 failed (AssertionError: expected "error" to be called with arguments [ StringContaining, Anything ]); un-mutated code + assertion → Tests 2 passed (2).

中文说明

失败路径的见证测试只钉住了 teardown() 正常结束,却没有断言本 diff 注释刻意保留的警告("Keep the warning visible so the poisoned host is still diagnosable")。已在评审提交上做了变异测试:删除 globalSetup.ts 中的 console.error 行后仍是 Tests 2 passed (2) —— 变异存活。若后续清理移除或降级该警告,所有测试依旧绿色,中毒的池宿主机将变得无法诊断:下一次 #10325 式事故在日志中没有任何警告且套件全绿,重新造成本 PR 着力保留可见性的"红色运行却无信号"诊断难题。建议在失败路径用例中监听并钉住该警告(见上方 suggestion 代码块)。

修复见证:加上该断言后,删除 integration-tests/globalSetup.ts 中的 console.error 行会使失败路径用例变红 —— 已在评审提交上验证:变异体 + 断言 → 1 failedAssertionError: expected "error" to be called with arguments [ StringContaining, Anything ]);未变异代码 + 断言 → Tests 2 passed (2)

— qwen3.8-max via Qwen Code /review (v0.22.2)

});
});
13 changes: 11 additions & 2 deletions integration-tests/globalSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,17 @@ export async function teardown() {
}

if (originalMemoryContent !== null) {
await mkdir(dirname(memoryFilePath), { recursive: true });
await writeFile(memoryFilePath, originalMemoryContent, 'utf-8');
try {
await mkdir(dirname(memoryFilePath), { recursive: true });
await writeFile(memoryFilePath, originalMemoryContent, 'utf-8');
} catch (e) {
Comment on lines +122 to +125

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This best-effort restore guard and its only witness test (integration-tests/globalSetup.test.ts) never execute before merge: both sit outside every npm workspace (this review's scoped build/test collected zero workspaces and ran nothing), PR CI only typechecks them, and e2e.yml — the only workflow whose vitest run --root ./integration-tests collects the test — has no pull_request trigger (it runs post-merge on main, nightly, and on workflow_dispatch), while ci.yml's integration legs filter to cli / an explicit no-AK file list that does not collect the root-level test. The PR can therefore merge with every pre-merge gate green; if the guard is wrong — or a later change mis-scopes the try/catch — the first execution is a post-merge E2E run on main on a pool host carrying a poisoned QWEN.md, where a broken teardown turns every all-green E2E run red with no failing test — the exact #10325 failure class this PR fixes — forcing a revert on main instead of a pre-merge fix. Give the new test one pre-merge run, e.g. in PR CI after build + bundle:

npx vitest run --root ./integration-tests globalSetup.test.ts

or have a maintainer dispatch e2e.yml on this branch before merging (it supports workflow_dispatch).

Fix witness: integration-tests/globalSetup.test.ts — "does not exit an all-green run red when the restore cannot write" goes red if this try/catch is removed.

中文说明

这个尽力而为的还原守卫及其唯一见证测试(integration-tests/globalSetup.test.ts)在合并前从不执行:两者都在所有 npm 工作区之外(本次评审按范围划定的构建/测试收集到零个工作区、没有运行任何套件),PR CI 只对其做类型检查;而 e2e.yml —— 唯一会以 vitest run --root ./integration-tests 收集该测试的工作流 —— 没有 pull_request 触发器(只在合并后的 main、夜间定时与 workflow_dispatch 时运行),ci.yml 的集成腿则用 cli / 显式 no-AK 文件列表过滤,不会收集根目录下的该测试。因此本 PR 合并时所有合并前门槛都是绿色的;若守卫有错 —— 或后续改动错误地限制了 try/catch 的范围 —— 首次执行将是合并后 main 上的 E2E 运行,且池宿主机上带着被投毒的 QWEN.md:损坏的 teardown 会让每一次全绿 E2E 运行变红且没有可定位的失败测试 —— 恰是本 PR 要修复的 #10325 失败类别 —— 只能被迫在 main 上回滚而不是在合并前修复。请给新测试一次合并前的执行,例如在 PR CI 中于 build + bundle 之后运行上方命令,或请维护者在合并前对本分支手动触发 e2e.yml(支持 workflow_dispatch)。

修复见证:integration-tests/globalSetup.test.ts —— 若移除该 try/catch,"does not exit an all-green run red when the restore cannot write" 会变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

// Best-effort restore: on the persistent pool runners a privileged job
// can leave a readable-but-unwritable QWEN.md behind, and the throw
// turned every all-green E2E run on that host red with no failing test
// ('Startup Error: EACCES'; #10325). Keep the warning visible so the
// poisoned host is still diagnosable.
console.error(`Warning: could not restore ${memoryFilePath}:`, e);
}
} else {
try {
await unlink(memoryFilePath);
Expand Down
20 changes: 12 additions & 8 deletions integration-tests/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,18 @@ export default defineConfig({
maxForks: 4,
},
},
// The worker->main `onTaskUpdate` RPC runs on a 60s budget; under the
// resource pressure of the macOS E2E lane a stall longer than that
// surfaces as an unhandled error and exits an all-green run red (the
// same failure class the core, cli, and scripts suites hit on these
// lanes). Test failures still fail the run; only unhandled errors stop
// being fatal, and only off Linux — the ubuntu shards and Linux local
// runs keep the unhandled-error signal.
dangerouslyIgnoreUnhandledErrors: process.platform !== 'linux',
// The worker->main `onTaskUpdate` RPC runs on a 60s budget; under
// resource pressure a stall longer than that surfaces as an unhandled
// error and exits an all-green run red (the same failure class the
// core, cli, and scripts suites hit on the macOS lane). Since #10085
// the Linux shards run on the shared self-hosted pool instead of
// ubuntu-hosted VMs and hit the same pressure class there (#10325), so
// self-hosted runners are exempted as well. Test failures still fail
// the run; only unhandled errors stop being fatal — github-hosted Linux
// (the nightly isolated legs) and local Linux runs keep the signal.
dangerouslyIgnoreUnhandledErrors:
process.platform !== 'linux' ||
process.env['RUNNER_ENVIRONMENT'] === 'self-hosted',
},
resolve: {
alias: {
Expand Down
53 changes: 46 additions & 7 deletions scripts/tests/integration-vitest-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import integrationConfig from '../../integration-tests/vitest.config.js';

describe('integration Vitest config', () => {
Expand All @@ -17,11 +17,50 @@ describe('integration Vitest config', () => {
expect(integrationConfig.test?.poolOptions?.threads).toBeUndefined();
});

it('keeps unhandled errors fatal only on Linux', () => {
// toBe, not toBeFalsy: a deleted flag is `undefined` and must fail
// this pin on every platform, including Linux where the value is false.
expect(integrationConfig.test?.dangerouslyIgnoreUnhandledErrors).toBe(
process.platform !== 'linux',
);
describe('unhandled-error exemption', () => {
const savedRunnerEnvironment = process.env['RUNNER_ENVIRONMENT'];

afterEach(() => {
if (savedRunnerEnvironment === undefined) {
delete process.env['RUNNER_ENVIRONMENT'];
} else {
process.env['RUNNER_ENVIRONMENT'] = savedRunnerEnvironment;
}
vi.resetModules();
});

// The flag reads RUNNER_ENVIRONMENT at config import time, so each case
// re-imports the config under a controlled value instead of trusting the
// ambient one.
async function configFor(runnerEnvironment: string | undefined) {
vi.resetModules();
if (runnerEnvironment === undefined) {
delete process.env['RUNNER_ENVIRONMENT'];
} else {
process.env['RUNNER_ENVIRONMENT'] = runnerEnvironment;
}
const { default: config } = await import(
'../../integration-tests/vitest.config.js'
);
return config;
}

it('exempts self-hosted pool runners on every platform', async () => {
// Dropping the self-hosted clause makes the shared pool's pressure
// flakes exit all-green E2E runs red again (#10325).
const config = await configFor('self-hosted');
expect(config.test?.dangerouslyIgnoreUnhandledErrors).toBe(true);
});

it('keeps unhandled errors fatal on github-hosted Linux and local runs', async () => {
// toBe, not toBeFalsy: a deleted flag is `undefined` and must fail
// this pin on every platform, including Linux where the value is false.
for (const environment of ['github-hosted', undefined]) {
const config = await configFor(environment);
expect(config.test?.dangerouslyIgnoreUnhandledErrors).toBe(
process.platform !== 'linux',
);
}
});
});
});
Loading