qwen-code/scripts/tests/e2e-workflow.test.js
易良 11b40836d3
fix(ci): exempt non-TUI packages from the parity impact gate (#15937)
* fix(ci): exempt non-TUI packages from the parity impact gate

* fix(ci): exempt the manifest-less SDK trees from the parity gate

The exclusion list was audited from workspace manifests, so
packages/sdk-java, packages/sdk-python and packages/zed-extension — none of
which carries a package.json — stayed outside it and still selected the
component lane for a PR touching only them.

Add the three trees to the exclusion, pin each with a single-file case in the
classifier contract suite, and record why they are hand-listed in the design
doc pair.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmvvf8ed23g

* fix(ci): name the policy exclusion and cover the CUA driver tree

The exclusion branch skipped every matched path before recording a
reason, so a PR excluded on policy reported the same "No TUI impact"
as a PR whose paths the classifier never mapped. Record the reason once
inside that branch (R1-2).

packages/cua-driver is outside the --cli-only closure: node-repl
declares @qwen-code/cua-sdk, but the build filter pulls node-repl
without its dependencies and no TUI suite imports the SDK. Exempt it
with a single-file row and a rename row (R2-1).

The workspace-excluded trees were a third hand-maintained copy of the
set pnpm-workspace.yaml states, with nothing reconciling them. A new
case reads pnpm-workspace.yaml and asserts every excluded tree still
classifies as no-impact (R2-2).

The design docs claimed the excluded packages are never built by these
gates. npm run bundle compiles integrations/external-context into
dist/mem0 through hard-coded esbuild entries, so say what actually
happens and name esbuild entries and copy_bundle_assets copies as
inputs the manifest audit cannot see (R2-3).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmvvnt1g33t

* test(ci): reconcile workspace negations by value, not literal spelling

* test(ci): pin the policy reason for every excluded workspace tree

R4-1: the reconciliation test only asserted outputs.component, the value the
classifier starts with, so narrowing reasons.add(...) for any tree except
cua-driver silently reverted the human-facing diagnostic to "No TUI impact"
while all 163 tests stayed green.

- assert outputs.reason inside the loop, not only for the CUA driver tree
- extract excludedTrees(yamlText) and feed it a fixture with a trailing-comment
  negation and an !integrations/... negation, so the by-value derivation is
  exercised for the spellings its comment claims

Witness (both mutants, unmutated suite 163/163 green):
- reasons.add narrowed to packages/cua-driver/: reason assertion red
- derivation reverted to the old literal regex: fixture assertion red

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmvvs3czk3z

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-31 15:49:57 +08:00

2059 lines
80 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import {
existsSync,
readFileSync,
readdirSync,
mkdtempSync,
mkdirSync,
rmSync,
} from 'node:fs';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, expect, it, vi } from 'vitest';
import { parse } from 'yaml';
describe('TUI product evidence', () => {
const workflow = parse(
readFileSync('.github/workflows/tui-parity.yml', 'utf8'),
);
const steps = workflow.jobs.parity.steps;
const classify = new (Object.getPrototypeOf(async () => {}).constructor)(
'github',
'context',
'core',
workflow.jobs.changes.steps[0].with.script,
);
const serverFile = 'packages/managed-agent-server/src/main/java/Service.java';
const companionFile =
'packages/vscode-ide-companion/src/services/settingsWriter.ts';
async function impact(
files,
count = files.length,
eventName = 'pull_request',
apiError,
) {
const github = {
paginate: vi.fn().mockResolvedValue(files),
rest: { pulls: { listFiles: vi.fn() } },
};
if (apiError) github.paginate.mockRejectedValue(apiError);
const outputs = {};
const summary = {
addHeading: vi.fn().mockReturnThis(),
addTable: vi.fn().mockReturnThis(),
addRaw: vi.fn().mockReturnThis(),
write: vi.fn().mockResolvedValue(),
};
const core = {
setOutput: (key, value) => {
outputs[key] = value;
},
setFailed: vi.fn(),
summary,
};
// A real dispatch payload carries no `pull_request` key. Handing the
// dispatch case a populated one would keep this suite green if the
// `context.payload.pull_request` read were ever hoisted above the
// eventName guard, while every real dispatch then throws.
const payload =
eventName === 'pull_request'
? { pull_request: { number: 15261, changed_files: count } }
: {};
await classify(
github,
{
eventName,
repo: { owner: 'QwenLM', repo: 'qwen-code' },
payload,
},
core,
);
if (eventName === 'pull_request') {
expect(github.paginate).toHaveBeenCalledWith(
github.rest.pulls.listFiles,
{
owner: 'QwenLM',
repo: 'qwen-code',
pull_number: 15261,
per_page: 100,
},
);
} else {
expect(github.paginate).not.toHaveBeenCalled();
}
expect(summary.write).toHaveBeenCalledOnce();
expect(summary.addRaw).toHaveBeenCalledWith(outputs.reason);
return { outputs, core };
}
const required = async (...args) =>
(await impact(...args)).outputs.component === 'true';
const condition = (
expression,
outputs,
result = 'success',
cancelled = false,
event = 'pull_request',
) => {
if (!expression) return true;
return new Function(
'needs',
'cancelled',
'github',
`return (${expression.slice(3, -2)});`,
)({ changes: { result, outputs } }, () => cancelled, { event_name: event });
};
const named = (job, prefix) =>
workflow.jobs[job].steps.find((step) => step.name?.startsWith(prefix));
const lanes = [
'component',
'render',
'input',
'recovery',
'integration',
'full',
];
const flags = (selected) =>
Object.fromEntries(
lanes.map((lane) => [lane, String(selected.includes(lane))]),
);
it.each([
['packages/cli/src/ui/components/LoadingIndicator.tsx', ['component']],
['packages/core/src/utils/debugLogger.ts', ['component']],
['packages/new-dependency/src/unknown.ts', ['component']],
[
'packages/cli/src/ui/opentui/status-line-text.ts',
['component', 'render'],
],
['packages/core/src/telemetry/uiTelemetry.ts', ['component', 'render']],
['packages/cli/src/ui/opentui/clipboard.ts', ['component', 'input']],
[
'packages/cli/src/ui/components/shared/text-buffer.ts',
['component', 'input'],
],
[
'packages/core/src/services/fileHistoryService.ts',
['component', 'recovery'],
],
[
'packages/core/src/services/visionBridge/vision-bridge-service.ts',
['component', 'input', 'recovery'],
],
[
'packages/cli/src/ui/contexts/SessionContext.tsx',
['component', 'render', 'recovery'],
],
['packages/core/src/config/config.ts', lanes],
['packages/cli/src/llm.tsx', lanes],
['packages/cli/src/utils/tui-runtime.ts', lanes],
['packages/cli/src/utils/npm-runtime.ts', lanes],
['packages/cli/src/ui/opentui/opentui-assets.ts', lanes],
['scripts/tests/unrelated.test.js', ['component']],
[
'packages/core/src/tools/write-file.ts',
['component', 'recovery', 'integration'],
],
[
'packages/core/src/services/shellExecutionService.ts',
['component', 'integration'],
],
[
'packages/cli/src/config/extension-runtime-reload.ts',
['component', 'integration'],
],
['packages/cli/src/dualOutput/index.ts', ['component', 'integration']],
['packages/core/src/core/coreToolScheduler.ts', lanes.slice(0, -1)],
['packages/core/src/core/client.ts', lanes.slice(0, -1)],
['packages/cli/src/ui/opentui/live-turn.ts', lanes.slice(0, -1)],
['packages/cli/src/ui/opentui/opentui-runtime.ts', lanes],
['pnpm-lock.yaml', lanes],
['packages/cli/package.json', lanes],
['scripts/tui-parity/component-parity.sh', lanes],
['integration-tests/test-helper.ts', lanes],
['.github/workflows/tui-parity.yml', lanes],
['docs/design/unrelated.md', []],
['packages/web-shell/client/App.tsx', []],
['packages/chrome-extension/package.json', []],
['packages/mobile-mcp/package.json', []],
['packages/qwen-live/build.mjs', []],
['integrations/external-context/package.json', []],
['integrations/external-context-mem0/package.json', []],
['integrations/test-failure-explorer/package.json', []],
['packages/desktop/package.json', []],
['packages/live-host/electron-builder.yml', []],
['packages/mobile-shell/build.gradle.kts', []],
['packages/sdk-java/client/pom.xml', []],
['packages/sdk-python/pyproject.toml', []],
['packages/zed-extension/extension.toml', []],
['packages/cua-driver/typescript/src/native-assets.ts', []],
['scripts/unrelated-maintenance.js', ['component']],
])(
'selects mechanisms rather than blanket full acceptance for %s',
async (filename, selected) => {
if (!/unknown|unrelated|new-dependency/.test(filename)) {
expect(existsSync(filename), filename).toBe(true);
}
const { outputs, core } = await impact([{ filename }]);
expect(core.setFailed).not.toHaveBeenCalled();
expect(outputs).toMatchObject(flags(selected));
expect(condition(workflow.jobs.parity.if, outputs)).toBe(
selected.includes('component'),
);
expect(condition(workflow.jobs.noflicker.if, outputs)).toBe(
selected.some((lane) => ['render', 'recovery', 'full'].includes(lane)),
);
expect(condition(named('parity', 'Component parity').if, outputs)).toBe(
selected.includes('component'),
);
expect(condition(named('parity', 'Bundle the product').if, outputs)).toBe(
selected.some((lane) => lane !== 'component'),
);
expect(condition(named('parity', 'Three-leg product').if, outputs)).toBe(
selected.includes('full'),
);
expect(condition(named('parity', 'Targeted product').if, outputs)).toBe(
!selected.includes('full') &&
selected.some((lane) => lane !== 'component'),
);
expect(
condition(named('noflicker', 'OpenTUI no-flicker').if, outputs),
).toBe(selected.includes('render'));
for (const prefix of ['Bundle the product', 'Cold checkpoint recovery']) {
expect(condition(named('noflicker', prefix).if, outputs)).toBe(
selected.includes('recovery'),
);
}
if (selected.length === 1)
expect(outputs.reason).toContain(
'maintainers should consider manual full',
);
},
);
it('unions both rename paths and reports only fixed reasons, never untrusted path text', async () => {
const filename = 'packages/unknown/$(touch injected)\n<script>.ts';
const files = [
{
filename: 'packages/cli/src/ui/opentui/clipboard.ts',
previous_filename: 'packages/core/src/services/sessionService.ts',
},
{ filename },
];
const { outputs, core } = await impact(files);
expect((await impact([...files].reverse())).outputs).toMatchObject(
flags(['component', 'input', 'recovery']),
);
expect(outputs).toMatchObject(flags(['component', 'input', 'recovery']));
expect(JSON.stringify(core.summary.addTable.mock.calls)).not.toContain(
filename,
);
expect(outputs.reason).not.toContain(filename);
});
it('fails the classifier on API errors but routes all downstream full steps even without outputs', async () => {
const { outputs, core } = await impact(
[],
0,
'pull_request',
new Error('API unavailable'),
);
expect(core.setFailed).toHaveBeenCalledOnce();
expect(outputs).toMatchObject(flags(lanes));
for (const values of [outputs, {}]) {
for (const [job, prefixes] of [
[
'parity',
['Component parity', 'Bundle the product', 'Three-leg product'],
],
[
'noflicker',
[
'OpenTUI no-flicker',
'Bundle the product',
'Cold checkpoint recovery',
],
],
]) {
expect(condition(workflow.jobs[job].if, values, 'failure')).toBe(true);
for (const prefix of prefixes)
expect(condition(named(job, prefix).if, values, 'failure')).toBe(
true,
);
}
expect(
condition(named('parity', 'Targeted product').if, values, 'failure'),
).toBe(false);
}
});
it.each(['schedule', 'workflow_dispatch'])(
'runs all original source and installed acceptance on %s',
async (event) => {
const { outputs } = await impact([], 0, event);
expect(outputs).toMatchObject(flags(lanes));
expect(workflow.jobs.changes.if).toBeUndefined();
for (const job of ['parity', 'noflicker', 'installed-npm']) {
expect(
condition(workflow.jobs[job].if, outputs, 'success', false, event),
).toBe(true);
}
expect(workflow.jobs['installed-distribution'].needs).toBe(
'installed-npm',
);
for (const [job, prefix] of [
['parity', 'Three-leg product'],
['noflicker', 'Cold checkpoint recovery'],
['noflicker', 'OpenTUI no-flicker'],
]) {
expect(
condition(named(job, prefix).if, outputs, 'success', false, event),
).toBe(true);
}
expect(
condition(
named('parity', 'Targeted product').if,
outputs,
'success',
false,
event,
),
).toBe(false);
},
);
const runAcceptance = (step, outputs, failLeg = '') => {
const root = mkdtempSync(join(tmpdir(), 'tui-workflow-test-'));
mkdirSync(join(root, 'integration-tests'));
try {
const env = { ...process.env, RUNNER_TEMP: root, FAIL_LEG: failLeg };
delete env.QWEN_E2E_RUNTIME;
delete env.QWEN_E2E_RENDERER;
for (const [key, expression] of Object.entries(step.env ?? {})) {
const lane = expression.match(
/^\$\{\{ needs\.changes\.outputs\.(\w+) \}\}$/,
)?.[1];
if (lane) env[key] = outputs[lane];
}
const result = spawnSync(
'bash',
[
'-c',
`npx() { printf '%s\\t' "\${QWEN_E2E_RUNTIME:-unset}:\${QWEN_E2E_RENDERER:-unset}" "$@"; printf '\\n'; [ "\${QWEN_E2E_RUNTIME:-unset}:\${QWEN_E2E_RENDERER:-unset}" != "$FAIL_LEG" ]; }\n${step.run}`,
],
{ cwd: root, env, encoding: 'utf8' },
);
expect(result.error).toBeUndefined();
return {
status: result.status,
calls: result.stdout
.trim()
.split('\n')
.filter(Boolean)
.map((line) => line.trim().split('\t')),
};
} finally {
rmSync(root, { recursive: true, force: true });
}
};
const groupFiles = {
render: ['tui-migration', 'statusline-parity'],
input: ['tui-input-ownership', 'tui-editor-parity', 'tui-clipboard-source'],
recovery: ['tui-session-recovery'],
integration: [
'tui-foreground-shell',
'tui-extension-acquisition',
'tui-dual-output',
],
};
it.each(
[
['render'],
['input'],
['recovery'],
['integration'],
Object.keys(groupFiles),
].map((groups) => [groups]),
)(
'executes exactly the selected whole files for %j in all three serial legs',
async (groups) => {
const paths = {
render: 'statusline-parity',
input: 'tui-input-ownership',
recovery: 'tui-session-recovery',
integration: 'tui-dual-output',
};
const { outputs } = await impact(
groups.map((group) => ({
filename: `integration-tests/interactive/${paths[group]}.test.ts`,
})),
);
const step = named('parity', 'Targeted product');
expect(condition(step.if, outputs)).toBe(true);
const { status, calls } = runAcceptance(step, outputs);
expect(status).toBe(0);
const expected = groups.flatMap((group) =>
groupFiles[group].map((file) => `interactive/${file}.test.ts`),
);
for (const file of expected)
expect(existsSync(`integration-tests/${file}`)).toBe(true);
if (groups.includes('render')) {
expect(calls.shift()).toEqual([
'unset:unset',
'vitest',
'run',
'renderer-matrix.test.ts',
'interactive/interactive-session-viewport.test.ts',
'--retry=0',
'--maxWorkers=1',
'--minWorkers=1',
'--dangerouslyIgnoreUnhandledErrors=false',
]);
}
expect(calls).toEqual(
['node:ink', 'bun:ink', 'bun:opentui'].map((leg) => [
leg,
'vitest',
'run',
...expected,
'--retry=0',
'--maxWorkers=1',
'--minWorkers=1',
'--dangerouslyIgnoreUnhandledErrors=false',
]),
);
expect(step.env.QWEN_TUI_ACCEPTANCE_OUT).toBe(
named('parity', 'Three-leg product').env.QWEN_TUI_ACCEPTANCE_OUT,
);
expect(step['continue-on-error']).toBeUndefined();
},
);
it('keeps checkpoint separate and preserves failure after running the remaining serial legs', () => {
const outputs = flags(['component', 'recovery']);
const targeted = runAcceptance(
named('parity', 'Targeted product'),
outputs,
'node:ink',
);
expect(targeted.status).toBe(1);
expect(targeted.calls).toHaveLength(3);
const checkpoint = runAcceptance(
named('noflicker', 'Cold checkpoint recovery'),
outputs,
'bun:ink',
);
expect(checkpoint.status).toBe(1);
expect(checkpoint.calls).toEqual(
['node:ink', 'bun:ink', 'bun:opentui'].map((leg) => [
leg,
'vitest',
'run',
'interactive/tui-checkpoint-restore.test.ts',
'--retry=0',
'--maxWorkers=1',
'--minWorkers=1',
'--dangerouslyIgnoreUnhandledErrors=false',
]),
);
});
it('preserves the complete full matrix rather than replacing it with selected subsets', () => {
const { status, calls } = runAcceptance(
named('parity', 'Three-leg product'),
flags(lanes),
);
expect(status).toBe(0);
expect(calls).toHaveLength(4);
expect(calls[0].slice(0, 5)).toEqual([
'unset:unset',
'vitest',
'run',
'renderer-matrix.test.ts',
'interactive/interactive-session-viewport.test.ts',
]);
const expectedFiles = Object.values(groupFiles)
.flat()
.map((file) => `interactive/${file}.test.ts`)
.sort();
expect(calls.slice(1).map((call) => call[0])).toEqual([
'node:ink',
'bun:ink',
'bun:opentui',
]);
for (const call of calls.slice(1)) {
expect(call.filter((arg) => arg.endsWith('.test.ts')).sort()).toEqual(
expectedFiles,
);
expect(call.slice(-4)).toEqual([
'--retry=0',
'--maxWorkers=1',
'--minWorkers=1',
'--dangerouslyIgnoreUnhandledErrors=false',
]);
}
});
it('preserves runner trust boundaries and the no-checkout classifier', () => {
for (const job of ['changes', 'parity', 'noflicker']) {
const routing = workflow.jobs[job]['runs-on'];
expect(routing).toBe(workflow.jobs.parity['runs-on']);
const evaluate = new Function(
'github',
'vars',
'contains',
'fromJSON',
`return (${routing.slice(3, -2)});`,
);
for (const [association, sameRepo, disabled, expected] of [
['CONTRIBUTOR', false, 'false', ['ubuntu-latest']],
['MEMBER', false, 'false', ['self-hosted', 'linux', 'x64', 'ecs-qwen']],
[
'CONTRIBUTOR',
true,
'false',
['self-hosted', 'linux', 'x64', 'ecs-qwen'],
],
['OWNER', true, 'true', ['ubuntu-latest']],
]) {
expect(
evaluate(
{
repository: 'QwenLM/qwen-code',
event_name: 'pull_request',
event: {
pull_request: {
author_association: association,
head: {
repo: {
full_name: sameRepo
? 'QwenLM/qwen-code'
: 'fork/qwen-code',
},
},
},
},
},
{ MAINTAINER_ECS_RUNNER_DISABLED: disabled },
(values, value) => values.includes(value),
JSON.parse,
),
).toEqual(expected);
}
}
expect(workflow.jobs.changes.steps[0].uses).toMatch(
/^actions\/github-script@/,
);
expect(workflow.jobs.changes.steps[0].run).toBeUndefined();
expect(workflow.jobs.changes['timeout-minutes']).toBe(5);
expect(workflow.jobs.noflicker['timeout-minutes']).toBe(30);
expect(workflow.jobs.noflicker.env.QWEN_API_KEY).toBe(
'${{ secrets.QWEN_API_KEY }}',
);
expect(Object.keys(workflow.jobs)).toEqual([
'changes',
'parity',
'noflicker',
'installed-npm',
'installed-distribution',
]);
});
it('runs the foreground shell wholefile in all three product legs without retries', () => {
const step = steps.find(
(entry) =>
entry.name ===
'Three-leg product acceptance (Node+Ink, Bun+Ink, Bun+OpenTUI)',
);
expect(step.run).toContain('node:ink bun:ink bun:opentui');
expect(step.run).toContain('interactive/tui-foreground-shell.test.ts');
expect(step.run).toContain('--retry=0');
expect(step.run).toContain('--dangerouslyIgnoreUnhandledErrors=false');
expect(step.run).not.toContain(' -t ');
});
it('cancels superseded PRs while keeping dispatches independent', () => {
expect(workflow.concurrency).toEqual({
group:
'${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}',
'cancel-in-progress': "${{ github.event_name == 'pull_request' }}",
});
});
it('skips Java service changes with accompanying design documentation', async () => {
expect(
await required([
{ filename: serverFile },
{ filename: 'docs/design/runtime.md' },
]),
).toBe(false);
});
it('skips Companion-only source changes and their tests', async () => {
expect(
await required([
{ filename: companionFile },
{
filename:
'packages/vscode-ide-companion/src/services/settingsWriter.test.ts',
},
]),
).toBe(false);
});
it('skips Web Shell-only changes but runs when CLI source rides along', async () => {
expect(
await required([
{ filename: 'packages/web-shell/client/App.tsx' },
{
filename: 'packages/web-shell/client/completions/slashCompletion.ts',
},
]),
).toBe(false);
expect(
await required([
{ filename: 'packages/web-shell/client/App.tsx' },
{ filename: 'packages/cli/src/ui/App.tsx' },
]),
).toBe(true);
});
it('reports a policy exclusion instead of the no-impact default', async () => {
const { outputs } = await impact([
{ filename: 'packages/cua-driver/typescript/src/native-assets.ts' },
]);
expect(outputs.component).toBe('false');
expect(outputs.reason).toBe('Outside the TUI build/test closure');
});
// Parse by value, not by literal spelling: a trailing comment or an
// `!integrations/...` negation is lint-legal and must still be reconciled.
const excludedTrees = (yamlText) =>
parse(yamlText)
.packages.filter((entry) => entry.startsWith('!'))
.map((entry) => entry.slice(1));
it('excludes every tree the pnpm workspace itself leaves out', async () => {
const excluded = excludedTrees(readFileSync('pnpm-workspace.yaml', 'utf8'));
expect(excluded.length).toBeGreaterThan(0);
for (const tree of excluded) {
const { outputs } = await impact([
{ filename: `${tree}/placeholder.ts` },
]);
expect(outputs.component, tree).toBe('false');
expect(outputs.reason, tree).toBe('Outside the TUI build/test closure');
}
});
it('reconciles negations spelled with a trailing comment or another scope', () => {
expect(
excludedTrees(
[
'packages:',
" - 'packages/*'",
" - '!packages/desktop' # promoted to its own release later",
" - '!integrations/generated-ui'",
'',
].join('\n'),
),
).toEqual(['packages/desktop', 'integrations/generated-ui']);
});
it.each([
'packages/cli/src/ui/App.tsx',
'packages/vscode-ide-companion/package.json',
'packages/vscode-ide-companion/esbuild.js',
'packages/vscode-ide-companion/tsconfig.json',
'packages/vscode-ide-companion/src-extra/index.ts',
'packages/vscode-ide-companion/scripts/copy-bundled-cli.js',
'packages/core/src/core/client.ts',
'packages/channels/base/index.ts',
'packages/web-templates/package.json',
'packages/new-cli-dependency/index.ts',
'packages/managed-agent-server-extra/index.ts',
'integration-tests/interactive/tui-migration.test.ts',
'scripts/copy_bundle_assets.js',
'integrations/generated-ui/package.json',
'patches/ink+7.0.3.patch',
'pnpm-lock.yaml',
'pnpm-workspace.yaml',
'tsconfig.json',
'.nvmrc',
'.github/workflows/tui-parity.yml',
])('runs mixed PRs touching %s', async (filename) => {
expect(
await required([
{ filename: serverFile },
{ filename: companionFile },
{ filename },
]),
).toBe(true);
});
it.each([serverFile, companionFile])(
'checks the source of a rename into %s',
async (filename) => {
expect(
await required([
{
filename,
previous_filename: 'packages/cli/src/index.ts',
},
]),
).toBe(true);
},
);
it.each([
[
'both ends in the Java service',
serverFile,
'packages/managed-agent-server/src/main/java/Old.java',
],
[
'both ends in a Companion source subtree',
companionFile,
'packages/vscode-ide-companion/src/services/oldWriter.ts',
],
[
// Reachable only inside a mixed PR: `on.pull_request.paths` has no
// `docs/**` entry, so a docs-only PR never starts this workflow. The row
// still earns its place — the arm must keep skipping a docs rename that
// rides alongside package changes.
'both ends under docs/',
'docs/design/runtime.md',
'docs/design/old-runtime.md',
],
[
'both ends in the Web Shell',
'packages/web-shell/client/App.tsx',
'packages/web-shell/client/OldApp.tsx',
],
[
'both ends in the Chrome extension',
'packages/chrome-extension/package.json',
'packages/chrome-extension/old-package.json',
],
[
'both ends in mobile-mcp',
'packages/mobile-mcp/package.json',
'packages/mobile-mcp/old-package.json',
],
[
'both ends in qwen-live',
'packages/qwen-live/build.mjs',
'packages/qwen-live/old-build.mjs',
],
[
'both ends in external-context',
'integrations/external-context/package.json',
'integrations/external-context/old-package.json',
],
[
'both ends in external-context-mem0',
'integrations/external-context-mem0/package.json',
'integrations/external-context-mem0/old-package.json',
],
[
'both ends in test-failure-explorer',
'integrations/test-failure-explorer/package.json',
'integrations/test-failure-explorer/old-package.json',
],
[
'both ends in the workspace-excluded desktop tree',
'packages/desktop/package.json',
'packages/desktop/old-package.json',
],
[
'both ends in the workspace-excluded live-host tree',
'packages/live-host/electron-builder.yml',
'packages/live-host/old-electron-builder.yml',
],
[
'both ends in the workspace-excluded mobile-shell tree',
'packages/mobile-shell/build.gradle.kts',
'packages/mobile-shell/old-build.gradle.kts',
],
[
'both ends in the CUA driver tree',
'packages/cua-driver/typescript/src/native-assets.ts',
'packages/cua-driver/typescript/src/old-native-assets.ts',
],
])(
'still skips a rename with %s',
async (_label, filename, previousFilename) => {
// Pins the negative arm: `previous_filename` present, but itself
// unrelated. Without it, weakening that arm to a presence check keeps
// the suite green while re-running both gates on any unrelated
// restructure. Every excluded subtree needs its own row: a mutation
// confined to the arm (narrowing it to one prefix) leaves the shared
// `unrelated()` predicate and every single-file case green.
expect(
await required([{ filename, previous_filename: previousFilename }]),
).toBe(false);
},
);
it('uses files beyond the first API page and fails visibly on incomplete diffs', async () => {
const files = Array.from({ length: 101 }, () => ({ filename: serverFile }));
expect(
await required([...files, { filename: 'packages/cli/src/index.ts' }]),
).toBe(true);
for (const [entries, count] of [
[files, 102],
[files, null],
[Array.from({ length: 3000 }, () => ({ filename: serverFile })), 3000],
]) {
const result = await impact(entries, count);
expect(result.core.setFailed).toHaveBeenCalledOnce();
expect(result.outputs.full).toBe('true');
}
});
it('always runs manual dispatches without querying PR files', async () => {
expect(await required([], 0, 'workflow_dispatch')).toBe(true);
});
it('retains named checks and runs them when impact detection fails', () => {
expect(workflow.jobs.changes.permissions).toEqual({
'pull-requests': 'read',
});
for (const key of [
'component',
'render',
'input',
'recovery',
'integration',
'full',
'reason',
]) {
expect(workflow.jobs.changes.outputs[key]).toBe(
'${{ steps.changes.outputs.' + key + ' }}',
);
}
expect(workflow.jobs.changes['continue-on-error']).toBeUndefined();
expect(workflow.jobs.changes.steps[0]['continue-on-error']).toBeUndefined();
expect(workflow.jobs.changes.steps).toHaveLength(1);
for (const job of [workflow.jobs.parity, workflow.jobs.noflicker]) {
expect(job.needs).toBe('changes');
for (const result of ['failure', 'skipped', 'cancelled']) {
expect(condition(job.if, {}, result)).toBe(true);
}
expect(condition(job.if, {}, 'success', true)).toBe(false);
expect(job.steps.find((step) => step.name === 'Build cli').run).toBe(
'npm run build -- --cli-only',
);
}
expect(workflow.jobs.parity.name).toBe(
'TUI parity snapshots (ink vs opentui)',
);
expect(workflow.jobs.noflicker.name).toBe('OpenTUI no-flicker gate');
});
it('watches shared build inputs and retains all package dependencies', () => {
// Exact set, not arrayContaining: a floor still passes when a later PR
// drops a trigger, and the three entries it hides are load-bearing —
// esbuild.config.js drives the bundle step, package.json drives the
// --cli-only filter, tui-parity.yml is this workflow's own self-trigger.
expect([...workflow.on.pull_request.paths].sort()).toEqual(
[
'.github/actions/**',
'.github/scripts/**',
'.github/workflows/tui-parity.yml',
'.npmrc',
'.nvmrc',
'esbuild.config.js',
'integration-tests/**',
'integrations/**',
'package.json',
'packages/**',
'patches/**',
'pnpm-lock.yaml',
'pnpm-workspace.yaml',
'scripts/**',
'tsconfig.json',
].sort(),
);
expect(
workflow.on.pull_request.paths.some((path) => path.startsWith('!')),
).toBe(false);
});
it('budgets cold setup and all three serial product acceptance legs', () => {
expect(workflow.jobs.parity['timeout-minutes']).toBe(45);
});
it('treats unhandled errors as acceptance failures on every runner', () => {
const product = steps.find((step) =>
step.name?.startsWith('Three-leg product acceptance'),
);
const commands = product.run
.split('\n')
.filter((line) => line.includes('npx vitest run'));
expect(commands).toHaveLength(2);
for (const command of commands) {
expect(command).toContain('--dangerouslyIgnoreUnhandledErrors=false');
}
});
it('runs durable session recovery in every runtime leg', () => {
const product = steps.find((step) =>
step.name?.startsWith('Three-leg product acceptance'),
);
expect(product.run).toMatch(/for leg in node:ink bun:ink bun:opentui; do/);
const runtimeLoop = product.run.match(
/for leg[^\n]*; do([\s\S]*?)\n\s*done/,
);
expect(runtimeLoop).not.toBeNull();
const commands = runtimeLoop[1]
.split('\n')
.filter((line) => line.includes('npx vitest run'));
expect(commands).toHaveLength(1);
expect(commands[0].split(/\s+/)).toContain(
'interactive/tui-session-recovery.test.ts',
);
expect(
existsSync('integration-tests/interactive/tui-session-recovery.test.ts'),
).toBe(true);
expect(commands[0].split(/\s+/)).toContain(
'interactive/tui-extension-acquisition.test.ts',
);
expect(
existsSync(
'integration-tests/interactive/tui-extension-acquisition.test.ts',
),
).toBe(true);
});
it('keeps fresh evidence retrievable when the same head is rerun', () => {
const upload = steps.find(
(step) => step.name === 'Upload product acceptance evidence',
);
const product = steps.find((step) =>
step.name?.startsWith('Three-leg product acceptance'),
);
expect(upload.if).toContain('always()');
expect(upload.with.name).toContain('github.event.pull_request.head.sha');
expect(upload.with.name).toContain('${{ github.run_attempt }}');
const artifactName = (attempt) =>
upload.with.name.replace('${{ github.run_attempt }}', String(attempt));
expect(artifactName(1)).not.toBe(artifactName(2));
expect(upload.with.path.replace(/\/$/, '')).toBe(
product.env.QWEN_TUI_ACCEPTANCE_OUT,
);
});
});
describe('e2e workflow', () => {
const workflow = readFileSync('.github/workflows/e2e.yml', 'utf8');
const e2eRunScript = readFileSync('.github/scripts/run-e2e-tests.sh', 'utf8');
const buildSandboxScript = readFileSync('scripts/build_sandbox.js', 'utf8');
const yml = parse(workflow);
it('never cancels in-progress runs on main', () => {
// A full run takes ~40min while merges land every ~18min, so cancelling on
// every merge starved the suite — over 100 push runs, 67 were cancelled and
// only 25 ever reported. Runs on main must finish; dev branches still cancel
// superseded runs. A future simplification back to `event_name == 'push'`
// would silently reintroduce the starvation, so the guard is asserted.
const cancel = yml.concurrency['cancel-in-progress'];
expect(cancel).toContain(
"github.event_name == 'push' && github.ref_name != 'main'",
);
});
it('scopes main pushes by SHA and other events by ref', () => {
expect(yml.concurrency.group.trim()).toBe(
"${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.sha || github.head_ref || github.ref_name }}",
);
});
it('isolates child-process-heavy suites from the three parallel forks', () => {
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).toContain('.github/scripts/run-e2e-tests.sh');
expect(e2eRunScript).toContain(
'npx cross-env QWEN_E2E_RENDERER=ink QWEN_SANDBOX=docker vitest run --root ./integration-tests "$@"',
);
expect(e2eRunScript).toContain(
'QWEN_E2E_RENDERER=ink npm run test:integration:sandbox:none -- "$@"',
);
expect(
e2eRunScript.match(/--exclude '\*\*\/qwen-serve-routes\.test\.ts'/g),
).toHaveLength(1);
expect(
e2eRunScript.match(/--exclude '\*\*\/sdk-typescript\/\*\*'/g),
).toHaveLength(1);
expect(e2eRunScript).toContain(
'run_vitest sdk-typescript cli/qwen-serve-routes.test.ts --poolOptions.forks.maxForks=1',
);
expect(e2eRunScript).not.toContain('--poolOptions.forks.singleFork');
// The arrangement is load-bearing: the batch must run first and the
// isolated suites second, joined by `&&` with no other command between
// or after. Counts and substrings are order- and backgrounding-blind;
// the pattern tolerates formatting-only rewrites (continuations, comment
// lines, trailing spaces) so a benign edit cannot turn CI red.
expect(e2eRunScript).toMatch(
/run_vitest "\$\{bulk_args\[@\]\}"[ \t]*&&[ \t]*\\?\n?(?:[ \t]*#[^\n]*\n)*[ \t]*run_vitest sdk-typescript cli\/qwen-serve-routes\.test\.ts --poolOptions\.forks\.maxForks=1[ \t]*\n\}/,
);
// Text pins never touch the tree: renaming the file or directory these
// filters name silently returns the suites to the three-fork batch.
expect(existsSync('integration-tests/cli/qwen-serve-routes.test.ts')).toBe(
true,
);
expect(existsSync('integration-tests/sdk-typescript')).toBe(true);
expect(existsSync('integration-tests/channel-plugin.test.ts')).toBe(true);
expect(
existsSync('integration-tests/chat-transcript-document.test.ts'),
).toBe(true);
expect(
existsSync('integration-tests/interactive/cron-interactive.test.ts'),
).toBe(true);
// existsSync pins the directory's name, not its contents: suites moved
// out of it would rejoin the three-fork batch through the dead exclude,
// while vitest silently ignores a positional filter that matches nothing
// as long as a sibling filter still matches.
expect(
readdirSync('integration-tests/sdk-typescript').some((file) =>
file.endsWith('.test.ts'),
),
).toBe(true);
});
describe('sandbox image preparation', () => {
const steps = yml.jobs['e2e-test-linux'].steps;
const setupStep = steps.find((step) => step.name === 'Set up Docker');
const runStep = steps.find((step) => step.name === 'Run E2E tests');
it('does not create one Buildx builder per self-hosted shard', () => {
expect(setupStep.if).toContain("runner.environment == 'github-hosted'");
});
it('serializes image preparation on the shared Docker host', () => {
expect(e2eRunScript).toContain(
'docker-sandbox-build-e2e-${GITHUB_SHA}.lock',
);
expect(e2eRunScript).toContain('flock --wait 1800 8');
expect(e2eRunScript).toContain(
'exec 9>"${HOME}/.cache/qwen-code-ci/docker-sandbox-daemon.lock"',
);
expect(e2eRunScript).toContain('flock --shared --wait 1800 9');
expect(e2eRunScript).toContain(
'exec 7>"${HOME}/.cache/qwen-code-ci/docker-sandbox-build.lock"',
);
expect(e2eRunScript).toContain('flock --wait 1800 7');
expect(e2eRunScript).toContain(
'if [ "$RUNNER_ENVIRONMENT" = \'self-hosted\' ]',
);
});
it('limits image cleanup to less than the build-lock wait', () => {
const pruneMatch = e2eRunScript
.replace(/\\\r?\n/g, '')
.match(
/^[ \t]*timeout[ \t]+(?:(?:-[ks][ \t]*|--(?:kill-after|signal)(?:=|[ \t]+))\S+[ \t]+)*(\d+)([smhd]?)[ \t]+docker[ \t]+image[ \t]+prune\b/m,
);
expect(
pruneMatch,
'Expected a timeout on docker image prune',
).not.toBeNull();
const unitSeconds = { s: 1, m: 60, h: 3600, d: 86400 };
const pruneSeconds =
Number(pruneMatch[1]) * unitSeconds[pruneMatch[2] || 's'];
const lockWaitSeconds = Number(
e2eRunScript.match(/flock --wait (\d+) 7/)?.[1],
);
expect(pruneSeconds).toBeGreaterThan(0);
expect(pruneSeconds).toBeLessThan(lockWaitSeconds);
});
it('reuses a commit-qualified image', () => {
expect(runStep.env.BUILD_SANDBOX_FLAGS).toContain(
'org.qwen-code.ci.sandbox=true',
);
expect(e2eRunScript).toContain('sandboxImageUri")-e2e-${GITHUB_SHA}"');
expect(e2eRunScript).toContain('docker image inspect "$sandbox_image"');
});
it('pins each shard to the prepared image ID', () => {
expect(e2eRunScript).toContain("docker image inspect --format '{{.Id}}'");
expect(e2eRunScript).toContain(
'export QWEN_SANDBOX_IMAGE="$sandbox_image_id"',
);
});
it('keeps one bounded retry without pruning the shared daemon', () => {
expect(e2eRunScript.match(/build_image/g)).toHaveLength(3);
expect(e2eRunScript).toContain(
'npm run build:sandbox -- -s --no-prune -i "$sandbox_image"',
);
expect(buildSandboxScript).toContain(".option('prune'");
expect(buildSandboxScript).toContain('if (argv.prune)');
});
it('keeps the Docker build environment', () => {
expect(runStep.env.QWEN_SANDBOX).toContain("'docker'");
expect(runStep.env.VERBOSE).toBe('true');
});
it('reaps only the sandbox containers owned by its matrix job', () => {
const owner =
'${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.shard }}';
const cleanupStep = steps.find(
(step) => step.name === 'Remove job-owned E2E containers',
);
expect(yml.jobs['e2e-test-linux'].env.E2E_CONTAINER_OWNER).toBe(owner);
expect(runStep.env.SANDBOX_FLAGS).toContain(
'org.qwen-code.ci.owner=${E2E_CONTAINER_OWNER}',
);
expect(e2eRunScript).toContain('trap cleanup_e2e_job EXIT');
expect(e2eRunScript).toContain("trap 'exit 1' INT TERM");
expect(e2eRunScript).toContain(
'--filter "label=org.qwen-code.ci.owner=${E2E_CONTAINER_OWNER}"',
);
expect(cleanupStep.if).toContain('always()');
expect(cleanupStep.run).toContain(
'--filter "label=org.qwen-code.ci.owner=${E2E_CONTAINER_OWNER}"',
);
expect(cleanupStep.run).toContain('docker rm -f > /dev/null || true');
expect(cleanupStep.run.match(/docker ps -aq/g)).toHaveLength(2);
expect(cleanupStep.run).toContain('E2E containers remain');
});
it('never waits on a lock another run holds through its tests', () => {
// Run 33637097713 lost two Docker shards to the #10605 protocol on one
// host: shard 1/3 held the per-commit coordinator lock and polled 30
// minutes for an exclusive daemon lock that a shard of run 33638984513
// kept shared through its whole test phase, then shard 2/3 timed out
// behind the coordinator lock shard 1/3 was still holding. Image
// preparation may only ever wait on locks bounded by a build.
const sharedIndex = e2eRunScript.indexOf('flock --shared --wait 1800 9');
const buildLockIndex = e2eRunScript.indexOf('flock --wait 1800 7');
const releaseIndex = e2eRunScript.indexOf('flock --unlock 7');
const testIndex = e2eRunScript.indexOf('vitest run');
expect(sharedIndex).toBeGreaterThanOrEqual(0);
expect(buildLockIndex).toBeGreaterThan(sharedIndex);
expect(releaseIndex).toBeGreaterThan(buildLockIndex);
expect(testIndex).toBeGreaterThan(releaseIndex);
expect(e2eRunScript).not.toContain('acquire_daemon_write_lock');
expect(e2eRunScript).not.toContain('flock --unlock 9');
expect(e2eRunScript).not.toContain('flock --nonblock 9');
expect(
yml.jobs['e2e-test-linux'].strategy['max-parallel'],
).toBeUndefined();
});
it('closes the lock descriptors in every child process', () => {
// A flock lives on the open file description, so a descendant that
// inherits the descriptor and outlives its job keeps holding the lock
// on the host. This shell keeps its own copy of the descriptor, so
// closing it in children costs nothing.
expect(e2eRunScript).toContain('-i "$sandbox_image" 7>&- 8>&- 9>&-');
expect(e2eRunScript).toContain(
'vitest run --root ./integration-tests "$@" 9>&-',
);
expect(e2eRunScript).toContain('exec 7>&-');
expect(e2eRunScript).toContain('exec 8>&-');
const cleanupStep = steps.find(
(step) => step.name === 'Prune dangling docker images',
);
expect(cleanupStep.run).toContain("--filter 'until=24h' 9>&-");
});
});
describe('sandbox:none shard retry', () => {
// Runs 33293739505, 33302550436 and 33317457036 each failed the
// sandbox:none leg at the 'Run E2E tests' step with zero vitest FAIL
// lines — an all-green shard exiting red under shared-host pressure,
// with sibling shards of the same runs green and the shard green on
// re-run. The bounded retry absorbs one such transient death; a
// deterministic test failure fails both attempts and keeps the job red.
const steps = yml.jobs['e2e-test-linux'].steps;
const runStep = steps.find((step) => step.name === 'Run E2E tests');
const epochStep = steps.find(
(step) => step.name === 'Record job start epoch',
);
it('records the job start epoch before the expensive setup steps', () => {
// The retry gate budgets against the whole 60-minute job; an epoch
// recorded at the test step would hide ~30 minutes of setup spend.
expect(epochStep.run).toContain(
'echo "E2E_JOB_START_EPOCH=$(date +%s)" >> "${GITHUB_ENV}"',
);
expect(steps.indexOf(epochStep)).toBeLessThan(
steps.indexOf(
steps.find((step) => step.name === 'Install dependencies'),
),
);
// An `if:` here would skip the record on one leg, where the gate's
// ${E2E_JOB_START_EPOCH:-0} fallback then always takes the ::error::
// branch and the retry never fires on the leg it exists for.
expect(epochStep.if).toBeUndefined();
});
it('wraps the sandbox:none shard command in a retryable function', () => {
expect(e2eRunScript).toContain('run_shard() {');
});
it('retries the full shard command, shard and excludes included', () => {
expect(e2eRunScript).toContain('run_vitest "${bulk_args[@]}"');
expect(e2eRunScript).toContain('--poolOptions.forks.maxForks=3');
expect(e2eRunScript).toContain('--shard="$shard"');
});
it('retries the sandbox:none shard exactly once', () => {
expect(e2eRunScript).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.
const retryBranch = e2eRunScript.slice(
e2eRunScript.lastIndexOf('if [ "$sandbox" = \'sandbox:docker\' ]'),
);
expect(retryBranch.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(e2eRunScript).toMatch(/run_shard\s*\n\s*\}\s*\n\s*fi\s*$/);
});
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
// only — bash itself witnesses the execution semantics in
// e2e-shard-retry.test.js.
const group = e2eRunScript.slice(e2eRunScript.indexOf('run_shard || {'));
expect(group).toMatch(/elapsed[\s\S]*exit 1[\s\S]*run_shard\s*\n\s*\}/);
});
it('pins the job timeout the budget-gate arithmetic is built on', () => {
// The 2100s threshold is 3600s minus a 25-minute reserve; the 3600s
// comes from this timeout. Editing one without the other mis-budgets
// the retry in both directions with every other witness green.
expect(yml.jobs['e2e-test-linux']['timeout-minutes']).toBe(60);
});
it('keeps the run step red when the shard stays red', () => {
// continue-on-error sits above the script exit code that every other
// witness observes: with it, two failing attempts still report green.
// Pin both levels — a job-level key computes the job conclusion green
// whatever the run step exits. The sandbox-image build step's
// deliberate step-level key and isolated-nightly's deliberate job-level
// key stay untouched — this pins the run step and e2e-test-linux only.
expect(runStep['continue-on-error']).toBeUndefined();
expect(yml.jobs['e2e-test-linux']['continue-on-error']).toBeUndefined();
});
it('passes the matrix sandbox and shard to the runner script', () => {
expect(runStep.run).toBe(
"exec bash .github/scripts/run-e2e-tests.sh '${{ matrix.sandbox }}' '${{ matrix.shard }}'",
);
});
it('does not retry the docker leg', () => {
const dockerBranch = e2eRunScript
.slice(
e2eRunScript.lastIndexOf('if [ "$sandbox" = \'sandbox:docker\' ]'),
)
.split('\nelse\n', 1)[0];
expect(dockerBranch.match(/run_shard/g)).toHaveLength(1);
});
});
describe('macOS lane capacity', () => {
const macosJob = () => yml.jobs['e2e-test-macos'];
it('runs a single macOS shard on main pushes, two everywhere else', () => {
// macos-latest is the org-level bottleneck measured on #15052: two
// shards per main push serialized in the scarce regime, so the run
// ended with five green legs and a verdict lost to a slot-starved
// second shard. One unsharded leg keeps full coverage at the same
// runner-minute cost while halving slot acquisitions per main push.
const shard = macosJob().strategy.matrix.shard;
const matrixFor = (eventName, refName) => {
if (Array.isArray(shard)) return shard;
const expression = String(shard)
.replace(/^\$\{\{\s*/, '')
.replace(/\s*\}\}$/, '')
.replace(/github\.event_name/g, JSON.stringify(eventName))
.replace(/github\.ref_name/g, JSON.stringify(refName));
// Anything this guard does not model must throw, not evaluate as
// undefined — the failure is the contract, not a guess.
if (/github\.|\?[^:]/.test(expression)) {
throw new Error(
`macOS shard matrix carries an unmodeled term: ${expression}`,
);
}
return new Function('fromJSON', `return (${expression});`)(
JSON.parse.bind(JSON),
);
};
expect(matrixFor('push', 'main')).toEqual(['1/1']);
expect(matrixFor('push', 'feat/e2e/example')).toEqual(['1/2', '2/2']);
expect(matrixFor('schedule', 'main')).toEqual(['1/2', '2/2']);
expect(matrixFor('workflow_dispatch', 'main')).toEqual(['1/2', '2/2']);
});
it('keeps the macOS variant on macos-latest with the same shard plumbing', () => {
// Unsharding changes matrix cardinality only: the runner label, the
// fork gate, and the vitest --shard plumbing stay exactly as pinned
// elsewhere, so the suite the leg runs is identical.
expect(macosJob()['runs-on']).toBe('macos-latest');
const runStep = macosJob().steps.find((step) =>
String(step.run ?? '').includes('--shard='),
);
expect(runStep.run).toContain('--shard="${{ matrix.shard }}"');
});
});
describe('docker lock cache ownership heal', () => {
// Run 35054004633 (#11990) failed exactly the two steps that open
// ~/.cache/qwen-code-ci/docker-sandbox-daemon.lock — 'Run E2E tests'
// (via run-e2e-tests.sh) and 'Prune dangling docker images' — while
// the container-runtime preflight passed. Run 35039618620 (#11973)
// showed why: `exec 9>` on that lock fails with EACCES when a
// root-owned leftover sits in the runner's home, and both steps die
// on the same file. The workspace heal only chowns $GITHUB_WORKSPACE;
// the lock directory lives outside it, so the heal must reach it too.
const steps = yml.jobs['e2e-test-linux'].steps;
const heal = steps.find(
(step) => step.name === 'Restore workspace ownership',
);
it('chowns the host-side docker lock directory back to the runner', () => {
expect(heal).toBeDefined();
expect(heal.run).toContain('[ -d "${HOME}/.cache/qwen-code-ci" ]');
// The whole fallback chain as one contiguous pin: each half alone
// contains the short pin, so dropping `-R` from the unprivileged
// attempt or dropping the `sudo -n` fallback leaves a substring pin
// green while the heal dies on the pool.
expect(heal.run).toContain(
'chown -R "$RUNNER_UID:$RUNNER_GID" "${HOME}/.cache/qwen-code-ci" 2>/dev/null || sudo -n chown -R "$RUNNER_UID:$RUNNER_GID" "${HOME}/.cache/qwen-code-ci"',
);
});
it('heals before any step opens the daemon lock', () => {
const names = steps.map((step) => step.name);
expect(names.indexOf('Restore workspace ownership')).toBeLessThan(
names.indexOf('Run E2E tests'),
);
expect(names.indexOf('Restore workspace ownership')).toBeLessThan(
names.indexOf('Prune dangling docker images'),
);
});
});
describe('disk floor gate', () => {
// The E2E lane's slice of #10035 (#12764): run 36241138248 lost the
// sandbox:none leg when the saturated pool host's runner worker died on
// ENOSPC writing its own diag log — no failed step, no test result, one
// per-commit issue. ci.yml has gated its heavy jobs on
// check-disk-floor.sh since #10035 and the release lane since #11972;
// the pool-routed E2E leg went without. The gate fails the job fast
// with a DISKFLOOR sample naming the runner, and a re-run lands on an
// instance with headroom.
const steps = yml.jobs['e2e-test-linux'].steps;
const names = steps.map((step) => step.name);
const gateIndex = names.indexOf('Disk floor gate (self-hosted)');
it('gates the pool-routed Linux leg on a disk floor before its install', () => {
expect(gateIndex).toBeGreaterThanOrEqual(0);
const gate = steps[gateIndex];
expect(gate.if).toBe("${{ runner.environment == 'self-hosted' }}");
// The same direct call as ci.yml: this workflow only ever runs the
// pushed or current ref, so unlike the release lane the script always
// exists in the checkout and needs no presence guard.
expect(gate.run).toBe(
'bash .github/scripts/check-disk-floor.sh "${GITHUB_WORKSPACE}" "${RUNNER_TEMP:-/tmp}"',
);
const checkoutIndex = steps.findIndex((step) =>
String(step.uses ?? '').includes('actions/checkout'),
);
// The script rides the checkout, so the gate can only stand between
// it and the install that turns a near-full disk into a dead runner.
expect(gateIndex).toBeGreaterThan(checkoutIndex);
expect(gateIndex).toBeLessThan(names.indexOf('Install dependencies'));
});
it('keeps the self-hosted contract on every pool-routed job', () => {
// A new pool-routed leg without the gate reddens here instead of on a
// saturated host; the hosted legs stay ungated on fresh VMs. The same
// holds for the Node preflight (nodejs.org may be unreachable through
// the pool's egress proxy) and the ownership heal (docker shards leave
// root-owned files on the runners).
const poolJobs = Object.entries(yml.jobs).filter(([, job]) =>
String(job['runs-on'] ?? '').includes('ecs-qwen'),
);
expect(poolJobs.map(([id]) => id)).toEqual(['build', 'e2e-test-linux']);
// Keep the build aligned with the leg, and pin the workspace fallback
// chains below so editing both copies cannot silently remove them.
const pinnedHeal = yml.jobs['e2e-test-linux'].steps.find(
(step) => step.name === 'Restore workspace ownership',
);
for (const [id, job] of poolJobs) {
const steps = job.steps ?? [];
const names = steps.map((step) => step.name);
const checkoutIndex = steps.findIndex((step) =>
String(step.uses ?? '').includes('actions/checkout'),
);
const installIndex = names.indexOf('Install dependencies');
// Presence alone is not the contract: the heal must precede
// checkout, the gate must sit between checkout and install, and the
// preflight must be the pool's own action — a reordered or
// re-pointed step fails here instead of on a runner.
const heal = steps.find(
(step) => step.name === 'Restore workspace ownership',
);
expect(heal?.if, id).toBe("${{ runner.environment == 'self-hosted' }}");
expect(names.indexOf('Restore workspace ownership'), id).toBeLessThan(
checkoutIndex,
);
const gate = steps.find(
(step) => step.name === 'Disk floor gate (self-hosted)',
);
expect(gate, id).toBeDefined();
const gateIndex = names.indexOf('Disk floor gate (self-hosted)');
expect(gateIndex, id).toBeGreaterThan(checkoutIndex);
expect(gateIndex, id).toBeLessThan(installIndex);
const poolNode = steps.find(
(step) => step.name === 'Use pre-installed Node.js (self-hosted)',
);
expect(poolNode?.uses, id).toBe('./.github/actions/self-hosted-node');
const hostedNode = steps.find(
(step) =>
typeof step.name === 'string' &&
step.name.startsWith('Set up Node.js'),
);
expect(hostedNode?.if, id).toBe(
"${{ runner.environment == 'github-hosted' }}",
);
const npmCacheIndex = names.indexOf(
'Configure persistent npm cache (self-hosted)',
);
// Presence alone is not the contract here either: below the install it
// exports NPM_CONFIG_CACHE after npm has already picked a cache, so
// the pool re-downloads through the slow egress this step avoids, and
// without the `if:` the hosted lane points npm at a directory no cache
// step ever restores.
expect(npmCacheIndex, id).toBeGreaterThanOrEqual(0);
expect(npmCacheIndex, id).toBeLessThan(installIndex);
expect(steps[npmCacheIndex]?.if, id).toBe(
"${{ runner.environment == 'self-hosted' }}",
);
expect(steps[npmCacheIndex]?.run, id).toContain(
'cache_dir="${HOME}/.cache/qwen-code/npm"',
);
expect(steps[npmCacheIndex]?.run, id).toContain(
'echo "NPM_CONFIG_CACHE=${cache_dir}" >> "${GITHUB_ENV}"',
);
expect(heal?.run, id).toBe(pinnedHeal?.run);
expect(heal?.run, id).toContain(
'chown -R "$RUNNER_UID:$RUNNER_GID" "$GITHUB_WORKSPACE" 2>/dev/null || sudo -n chown -R "$RUNNER_UID:$RUNNER_GID" "$GITHUB_WORKSPACE"',
);
expect(heal?.run, id).toContain(
'chmod -R u+rwX "$GITHUB_WORKSPACE" 2>/dev/null || sudo -n chmod -R u+rwX "$GITHUB_WORKSPACE"',
);
}
});
});
describe('artifact download time-boxing', () => {
// #12132: each leg downloads the shared archive twice — once, and again
// from the retry that only fires when the first attempt failed. Only the
// first attempt was time-boxed, so a stalling retry held its runner with
// no ceiling at all, and two legs had no job-level ceiling either.
const legs = [
'e2e-test-linux',
'e2e-test-macos',
'e2e-interactive-opentui',
'isolated-nightly',
];
const downloads = (leg) =>
yml.jobs[leg].steps.filter((step) =>
String(step.name).startsWith('Download build artifact'),
);
it('caps every artifact-consuming leg at the job level', () => {
// 60 is the ceiling e2e-test-linux and e2e-interactive-opentui already
// carried; the other two legs consume the same archive on the same
// runners, so they get that budget instead of a new number.
for (const leg of legs) {
expect(yml.jobs[leg]['timeout-minutes'], leg).toBe(60);
}
});
it('time-boxes the retry as tightly as the first attempt', () => {
// 10 is the value every first-attempt download already pins. Without it
// the retry — the step that exists precisely because the first attempt
// misbehaved — was the only unbounded step in the download leg.
for (const leg of legs) {
const steps = downloads(leg);
expect(
steps.map((step) => step.name),
leg,
).toEqual([
'Download build artifact',
'Download build artifact (retry)',
]);
for (const step of steps) {
expect(step['timeout-minutes'], `${leg}/${step.name}`).toBe(10);
}
}
});
it('keeps the retry step red when the retry itself fails', () => {
// The first attempt's continue-on-error is what makes the retry
// reachable. Copying it onto the retry would leave a stalled download
// invisible: the leg would go on to unpack a missing archive and report
// a failure with the wrong cause.
for (const leg of legs) {
const retry = downloads(leg)[1];
expect(retry.name, leg).toBe('Download build artifact (retry)');
expect(retry['continue-on-error']).toBeUndefined();
}
});
it('leaves the already-landed ceilings where they are', () => {
// Regression guard. `build` produces the archive on a 30-minute budget
// while the legs consume it on 60, and 60 is also the number the
// shard-retry budget gate in run-e2e-tests.sh takes its 25-minute
// reserve out of. A later pass that "unifies" these values would move
// either the producer's ceiling or the retry arithmetic with every
// other witness green.
expect(yml.jobs.build['timeout-minutes']).toBe(30);
expect(yml.jobs['e2e-test-linux']['timeout-minutes']).toBe(60);
expect(yml.jobs['e2e-interactive-opentui']['timeout-minutes']).toBe(60);
for (const leg of legs) {
const first = downloads(leg)[0];
expect(first['timeout-minutes'], leg).toBe(10);
expect(first['continue-on-error'], leg).toBe(true);
}
});
});
describe('one build for every leg', () => {
// Each leg used to build and bundle on its own runner — 4–8 minutes on a
// hosted VM, 10–17 on a busy pool host, eleven times per run. The `build`
// job does it once and the legs unpack its archive; these pins keep a leg
// from quietly growing its own build back.
const build = yml.jobs.build;
const legs = [
'e2e-test-linux',
'e2e-test-macos',
'e2e-interactive-opentui',
'isolated-nightly',
];
it("builds once, sharing the legs' pool-first runner routing", () => {
expect(build.needs).toBeUndefined();
// Hosted ubuntu jobs queued for hours while the pool idled
// (2026-10-28), so the build rides the exact routing expression every
// other pool job uses instead of pinning a hosted-only label.
expect(build['runs-on']).toBe(yml.jobs['e2e-test-linux']['runs-on']);
expect(String(build['runs-on'])).toContain('ecs-qwen');
const names = build.steps.map((step) => step.name);
expect(names).toContain('Build project');
expect(names).toContain('Bundle CLI for E2E tests');
const pack = build.steps.find(
(step) => step.name === 'Pack build outputs',
);
expect(pack.run).toContain('.github/scripts/e2e-build-pack.sh');
// The same "the install must not build" premise as on the legs: without
// it the install runs prepare (a full build and bundle) and the explicit
// build steps below then do it a second time on the critical path.
const install = build.steps.find(
(step) => step.name === 'Install dependencies',
);
expect(install.env.QWEN_SKIP_PREPARE).toBe('1');
const upload = build.steps.find(
(step) => step.name === 'Upload build artifact',
);
expect(upload.uses).toMatch(/^actions\/upload-artifact@/);
expect(upload.with.name).toBe('e2e-build');
expect(upload.with['retention-days']).toBe(1);
});
it('gates the build like the legs, so a skipped build skips them', () => {
// The three fork-gated legs carry the build's exact gate; the nightly
// legs are narrower (schedule/dispatch only), which is a subset.
for (const job of [
'e2e-test-linux',
'e2e-test-macos',
'e2e-interactive-opentui',
]) {
expect(yml.jobs[job].if, job).toBe(build.if);
}
// The whole expression, not a substring: the workflow declares no
// pull_request trigger, so the `event_name != 'pull_request' ||`
// prefix is the only clause that is ever true. Dropping it would skip
// the build — and every leg behind it — on every real event, green.
expect(build.if).toBe(
"${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}",
);
});
it('keeps the web-shell regression job building during its install', () => {
// That job has no build step of its own: its tree comes solely from
// the prepare script that the install runs, so it must not carry the skip
// the artifact-fed legs carry.
const install = yml.jobs['web-shell-browser-regression'].steps.find(
(step) => step.name === 'Install dependencies',
);
expect(install.env?.QWEN_SKIP_PREPARE).toBeUndefined();
});
it.each(legs)('%s unpacks the shared build instead of building', (job) => {
const { needs, steps } = yml.jobs[job];
expect(needs).toEqual(['build']);
const names = steps.map((step) => step.name);
expect(names).not.toContain('Build project');
expect(names).not.toContain('Bundle CLI for E2E tests');
const download = steps.find(
(step) => step.name === 'Download build artifact',
);
expect(download.with.name).toBe('e2e-build');
const unpack = steps.find(
(step) => step.name === 'Unpack build artifact',
);
expect(unpack.run).toContain('.github/scripts/e2e-build-unpack.sh');
// Unpack before anything runs the CLI, after node_modules exist.
expect(names.indexOf('Install dependencies')).toBeLessThan(
names.indexOf('Unpack build artifact'),
);
expect(names.indexOf('Unpack build artifact')).toBeLessThan(
names.findIndex((name) => name.startsWith('Run ')),
);
const install = steps.find(
(step) => step.name === 'Install dependencies',
);
expect(install.env.QWEN_SKIP_PREPARE).toBe('1');
});
it('keeps the docker sandbox image build on the leg', () => {
// The image builds inside Docker from the checkout, so it is not part
// of the archive; the leg still prepares it under the host locks.
expect(e2eRunScript).toContain('npm run build:sandbox');
});
});
describe('install retry', () => {
// Run 34700339334 died at the build job's bare `npm ci` before any test
// ran — the same install reproduces clean at that commit, so the failure
// was a transient the tree could not explain — and every leg behind
// `needs: [build]` went down with it. repo-hygiene.yml and
// qwen-autofix.yml already wrap their installs in this exact bounded
// retry; a regression to a bare unretried install is silent until the next
// transient reddens a main run, so pin the shape on every install step.
const installSteps = Object.entries(yml.jobs).flatMap(([jobName, job]) =>
(job.steps ?? [])
.filter((step) => step.name === 'Install dependencies')
.map((step) => [jobName, step]),
);
it('wraps every Install dependencies step in the bounded retry', () => {
// Six jobs install: the build, the three artifact-fed legs, the
// nightly legs, and the web-shell browser gate. A new job adding a
// bare install must fail here, not in a main-branch run.
expect(installSteps.map(([jobName]) => jobName).sort()).toEqual([
'build',
'e2e-interactive-opentui',
'e2e-test-linux',
'e2e-test-macos',
'isolated-nightly',
'web-shell-browser-regression',
]);
// Fragment pins, not a byte-exact body: a formatting-only rewrite or
// a post-install line appended after `done` must stay green (the repo
// pins this same recipe in qwen-autofix.yml by fragment), while
// dropping the loop, the backoff, or the failure exit still reddens.
for (const [jobName, step] of installSteps) {
expect(step.run, jobName).toContain('for attempt in 1 2 3; do');
expect(step.run, jobName).toContain(
'if corepack pnpm install --frozen-lockfile --prefer-offline --reporter=append-only; then',
);
expect(step.run, jobName).toContain('exit 1');
expect(step.run, jobName).toContain('sleep $((attempt * 15))');
expect(step.run, jobName).toContain('break');
expect(step.run, jobName).toContain(
'if [[ "${attempt}" == "3" ]]; then',
);
expect(step.run, jobName).toContain(
'if [[ "${attempt}" != "1" ]]; then',
);
// The ::warning:: keeps an absorbed install transient countable even
// though the recovered job concludes green — the same rule the
// upload-artifact retry's announce step follows. Deleting the echo
// from any one copy must red this loop.
expect(step.run, jobName).toContain(
'echo "::warning::corepack pnpm install',
);
// The defect under test is a bare `corepack pnpm install` line outside the loop.
expect(step.run, jobName).not.toMatch(/^\s*corepack pnpm install/m);
}
});
// Fail closed on the mention, not on a recognised spelling: the ways
// shell can write one command cannot be enumerated against a regex. The
// previous pair exempted a whole body for carrying any retry loop — a
// trailing bare install rode the exemption — and saw only installs
// opening their line, so `cd … && pnpm install` and `time pnpm install` were
// invisible. The exemption is keyed on the line's shape, never the
// step's name: a name key pardons the six pinned bodies wholesale, so a
// bare install appended after `done` would ride the step's identity
// with its body never read. Require every executable line mentioning
// `pnpm install` to open with one of the two retry-loop lines pinned above,
// so an unrecognised shape reddens the suite for a human to judge
// instead of passing silently. Full-line `#` comments never execute, so
// a body quoting the recipe in prose is excluded rather than flagged.
const retriedInstallLines = [
'if corepack pnpm install --frozen-lockfile --prefer-offline --reporter=append-only; then',
'echo "::warning::corepack pnpm install',
];
const findUnretriedInstalls = (jobs) =>
Object.entries(jobs).flatMap(([jobName, job]) =>
(job.steps ?? [])
.filter(
(step) =>
typeof step.run === 'string' &&
step.run
.split('\n')
.filter((line) => !line.trimStart().startsWith('#'))
.some(
(line) =>
line.includes('pnpm install') &&
!retriedInstallLines.some((ok) =>
line.trimStart().startsWith(ok),
),
),
)
.map((step) => `${jobName}/${step.name ?? '(unnamed)'}`),
);
it('retries every pnpm install run body, whatever the step is named', () => {
// The name-keyed collection above misses an install hiding under any
// other step name — repo-hygiene.yml and qwen-autofix.yml call theirs
// 'Install dependencies and build' — so scan the command itself.
expect(findUnretriedInstalls(yml.jobs)).toEqual([]);
});
it('flags an unretried install however shell spells it', () => {
// Each synthetic body slipped the old regex pair — the loop exempting
// a trailing bare install was the filed escape; the rest never open
// their install line. The real bodies ride along under their own keys
// to pin that the allowlist recognises exactly the two retried lines,
// and `build` is overridden by a copy carrying a bare install appended
// after `done` — the shape a name-keyed exemption pardons unread. A
// comment-only mention stays unflagged because it never executes.
const jobs = {
...Object.fromEntries(
installSteps.map(([jobName, step]) => [jobName, { steps: [step] }]),
),
build: {
steps: [
{
name: 'Install dependencies',
run: [
'for attempt in 1 2 3; do',
' if corepack pnpm install --frozen-lockfile --prefer-offline --reporter=append-only; then',
' if [[ "${attempt}" != "1" ]]; then',
' echo "::warning::corepack pnpm install failed $((attempt - 1)) time(s)"',
' fi',
' break',
' fi',
' if [[ "${attempt}" == "3" ]]; then',
' exit 1',
' fi',
' sleep $((attempt * 15))',
'done',
'cd integration-tests && pnpm install',
].join('\n'),
},
],
},
synthetic: {
steps: [
{
name: 'Install dependencies and build',
run: [
'for attempt in 1 2 3; do',
' npx playwright install --with-deps chromium && break',
'done',
'corepack pnpm install --frozen-lockfile --prefer-offline --reporter=append-only',
].join('\n'),
},
{
name: 'Install integration dependencies',
run: 'cd integration-tests && pnpm install',
},
{ name: 'Time the install', run: 'time pnpm install' },
{
name: 'Retry the install once',
run: 'for attempt in 1; do pnpm install --prefer-offline; done',
},
{
name: 'Mention the recipe',
run: '# pnpm install is retried elsewhere\necho done',
},
],
},
};
expect(findUnretriedInstalls(jobs)).toEqual([
'build/Install dependencies',
'synthetic/Install dependencies and build',
'synthetic/Install integration dependencies',
'synthetic/Time the install',
'synthetic/Retry the install once',
]);
});
});
it('routes Linux E2E scratch files away from /tmp', () => {
expect(e2eRunScript).toContain('mktemp -d /var/tmp/qwen-ci-XXXXXX');
expect(e2eRunScript).toContain('rm -rf "$QWEN_CI_TMPDIR"');
});
});
describe('installed npm acceptance workflow', () => {
const workflow = parse(
readFileSync('.github/workflows/tui-parity.yml', 'utf8'),
);
const job = workflow.jobs['installed-npm'];
it('runs installed acceptance nightly and manually, never for PR updates', () => {
expect(Object.keys(workflow.on).sort()).toEqual([
'pull_request',
'schedule',
'workflow_dispatch',
]);
expect(workflow.on.schedule).toEqual([{ cron: '17 19 * * *' }]);
expect(job.if).toBe(
"${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}",
);
expect(job.needs).toBeUndefined();
expect(workflow.jobs.changes.if).toBeUndefined();
});
it('keeps installed setup independent of the original source parity budget', () => {
expect(workflow.jobs.parity['timeout-minutes']).toBe(45);
expect(job['timeout-minutes']).toBe(60);
expect(
job.steps.some((step) => step.uses?.startsWith('oven-sh/setup-bun')),
).toBe(false);
});
it('runs the full installed file without retries or ignored unhandled errors', () => {
const step = job.steps.find(
(entry) => entry.name === 'Nine installed npm acceptance cases',
);
expect(step.run).toContain(
'--config integration-tests/vitest.installed.config.ts',
);
expect(step.run).toContain('--retry=0');
expect(step.run).toContain('--dangerouslyIgnoreUnhandledErrors=false');
expect(step.run).not.toContain(' -t ');
});
it('always retains unique attempt evidence', () => {
const upload = job.steps.find(
(entry) => entry.name === 'Upload installed npm evidence',
);
expect(upload.if).toContain('always()');
expect(upload.with.name).toContain('github.run_id');
expect(upload.with.name).toContain('github.run_attempt');
expect(upload.with['if-no-files-found']).toBe('error');
});
});
describe('installed distribution matrix workflow', () => {
const workflow = parse(
readFileSync('.github/workflows/tui-parity.yml', 'utf8'),
);
const canonical = workflow.jobs['installed-npm'];
const job = workflow.jobs['installed-distribution'];
const names = job.steps.map((step) => step.name);
const named = (name) => job.steps.find((step) => step.name === name);
it('uploads one successful audited tarball and both canonical sidecars', () => {
const upload = canonical.steps.find(
(step) => step.name === 'Upload canonical installed npm artifact',
);
expect(upload.if).toBe('${{ success() }}');
expect(canonical.outputs['canonical-artifact-id']).toBe(
'${{ steps.canonical.outputs.artifact-id }}',
);
expect(upload.id).toBe('canonical');
expect(upload.with.name).toBe(
'installed-npm-canonical-${{ github.run_id }}-${{ github.run_attempt }}',
);
const root =
'${{ runner.temp }}/qwen-installed-${{ github.run_id }}-${{ github.run_attempt }}';
expect(upload.with.path.split('\n')).toEqual([
`${root}/AUDITED-ARTIFACT.json`,
`${root}/published-files.json`,
`${root}/tarballs/*.tgz`,
]);
expect(upload.with['if-no-files-found']).toBe('error');
const canonicalNames = canonical.steps.map((step) => step.name);
expect(canonicalNames.indexOf(upload.name)).toBeGreaterThan(
canonicalNames.indexOf('Nine installed npm acceptance cases'),
);
});
it('runs all four actual POSIX hosts after canonical acceptance succeeds', () => {
expect(job.needs).toBe('installed-npm');
expect(job.if).toBeUndefined();
expect(job['runs-on']).toBe('${{ matrix.os }}');
expect(job.strategy['fail-fast']).toBe(false);
expect(job.strategy.matrix.include).toEqual([
{
os: 'ubuntu-24.04',
target: 'linux-x64',
platform: 'linux',
arch: 'x64',
},
{
os: 'ubuntu-24.04-arm',
target: 'linux-arm64',
platform: 'linux',
arch: 'arm64',
},
{
os: 'macos-15',
target: 'darwin-arm64',
platform: 'darwin',
arch: 'arm64',
},
{
os: 'macos-15-intel',
target: 'darwin-x64',
platform: 'darwin',
arch: 'x64',
},
]);
const host = named('Assert actual distribution host');
expect(host.env).toEqual({
EXPECTED_PLATFORM: '${{ matrix.platform }}',
EXPECTED_ARCH: '${{ matrix.arch }}',
});
expect(host.run).toContain(
'assert.equal(process.platform,process.env.EXPECTED_PLATFORM)',
);
expect(host.run).toContain(
'assert.equal(process.arch,process.env.EXPECTED_ARCH)',
);
expect(host.run).toContain('getconf GNU_LIBC_VERSION');
expect(job['timeout-minutes']).toBe(120);
});
it('captures real pinned Node/npm pairs before returning to the Node24 controller', () => {
const setups = job.steps.filter((step) =>
step.uses?.startsWith('actions/setup-node@'),
);
expect(setups.map((step) => step.with['node-version'])).toEqual([
'22.23.2',
'26.4.0',
'24.19.0',
]);
for (const version of ['22', '26']) {
const capture = named(`Capture actual Node${version} npm toolchain`);
expect(capture.run).toContain(
`QWEN_DISTRIBUTION_NODE${version}=$(command -v node)`,
);
expect(capture.run).toContain(
`QWEN_DISTRIBUTION_NPM${version}=$(node -p`,
);
expect(capture.run).toContain('realpathSync(process.argv[1])');
}
expect(
job.steps.some((step) => step.uses?.startsWith('oven-sh/setup-bun')),
).toBe(false);
expect(named('Install dependencies').env.QWEN_SKIP_PREPARE).toBe('1');
expect(named('Build and bundle checkout').run).toBe(
'npm run build && npm run bundle',
);
});
it('binds the same downloaded canonical artifact once before either product leg', () => {
const download = named('Download canonical installed npm artifact');
expect(download.uses).toMatch(/^actions\/download-artifact@/);
expect(download.with['artifact-ids']).toBe(
'${{ needs.installed-npm.outputs.canonical-artifact-id }}',
);
expect(download.with.name).toBeUndefined();
expect(download.with.path).toBe(
'${{ env.QWEN_DISTRIBUTION_ARTIFACT_DIR }}',
);
expect(named('Bind canonical artifact to this checkout').run).toBe(
'node --import tsx integration-tests/installed/bind-distribution-artifact.ts "$QWEN_DISTRIBUTION_ARTIFACT_DIR" "$QWEN_DISTRIBUTION_ARTIFACT"',
);
expect(
names.indexOf('Download canonical installed npm artifact'),
).toBeLessThan(names.indexOf('Bind canonical artifact to this checkout'));
expect(
names.indexOf('Bind canonical artifact to this checkout'),
).toBeLessThan(
names.indexOf('Twelve Node22 global and npx acceptance cases'),
);
const paths = named('Prepare distribution attempt paths');
expect(paths.env.DISTRIBUTION_TARGET).toBe('${{ matrix.target }}');
expect(paths.run).toContain(
'QWEN_DISTRIBUTION_ARTIFACT=${RUNNER_TEMP}/qwen-distribution-bound.json',
);
expect(paths.run).toContain('>> "$GITHUB_ENV"');
expect(names.indexOf(paths.name)).toBeLessThan(
names.indexOf('Capture actual Node22 npm toolchain'),
);
expect(
job.steps.some((step) => /npm pack|prepare:package/.test(step.run ?? '')),
).toBe(false);
});
it('runs two serial whole-file legs without masking failure or retrying assertions', () => {
const node22 = named('Twelve Node22 global and npx acceptance cases');
const node26 = named('Twelve Node26 global and npx acceptance cases');
for (const [major, patch, step] of [
['22', '22.23.2', node22],
['26', '26.4.0', node26],
]) {
expect(step.env).toEqual({
QWEN_DISTRIBUTION_NODE:
'${{ env.QWEN_DISTRIBUTION_NODE' + major + ' }}',
QWEN_DISTRIBUTION_NPM: '${{ env.QWEN_DISTRIBUTION_NPM' + major + ' }}',
QWEN_DISTRIBUTION_NODE_VERSION: patch,
QWEN_DISTRIBUTION_OUT:
'${{ env.QWEN_DISTRIBUTION_ROOT_OUT }}/node' + major,
});
expect(step.run).toBe(
'npx vitest run --config integration-tests/vitest.distribution.config.ts --retry=0 --bail=1 --maxWorkers=1 --minWorkers=1 --dangerouslyIgnoreUnhandledErrors=false',
);
expect(step.if).toBeUndefined();
expect(step['continue-on-error']).toBeUndefined();
}
expect(job['continue-on-error']).toBeUndefined();
expect(names.indexOf(node22.name)).toBeLessThan(names.indexOf(node26.name));
});
it('always uploads uniquely named host/run/attempt evidence', () => {
const upload = named('Upload distribution attempt evidence');
expect(upload.if).toBe('${{ always() }}');
expect(upload.with.name).toBe(
'installed-distribution-${{ matrix.target }}-${{ github.run_id }}-${{ github.run_attempt }}',
);
expect(upload.with.path.split('\n')).toEqual([
'${{ env.QWEN_DISTRIBUTION_ROOT_OUT }}',
'${{ env.QWEN_DISTRIBUTION_ARTIFACT }}',
'!${{ env.QWEN_DISTRIBUTION_ROOT_OUT }}/**/home/.npm/**',
'!${{ env.QWEN_DISTRIBUTION_ROOT_OUT }}/**/verified-tar/**',
'!${{ env.QWEN_DISTRIBUTION_ROOT_OUT }}/**/global-ignore-scripts/prefix/**',
]);
expect(upload.with['if-no-files-found']).toBe('error');
});
});