qwen-code/scripts/tests/package-scripts.test.js
Shaojin Wen 6e6a69c71f
test: gate the remaining chmod-based root skips on the permission-bit probe (#14403)
Fifty cases across core, cli, browser-use, sdk-typescript,
test-failure-explorer and scripts/tests force a failure with chmod and
skip whenever uid is 0. ci.yml runs unit tests as root on the ECS pool,
but drops CAP_DAC_OVERRIDE and CAP_DAC_READ_SEARCH first, so permission
bits bind there and these cases could run; the uid check skips them all
on that lane anyway.

Gate them on whether a mode-000 directory refuses this process, as #14162,
#14271 and #14283 do for the rest:
- core (22 cases in 14 files) and cli (1) use their own helper.
- browser-use, sdk-typescript and test-failure-explorer import nothing
  from another package, so each gets a copy of the helper; browser-use,
  with six cases, also gets its tests, as core did in #14283.
- scripts/tests (19 cases in 7 files) imports the cli helper, as other
  scripts tests do.

Three core cases returned early as root instead of skipping, so they
reported a pass without asserting anything; they now skip there. The
workflow-extension case left its unreadable directory empty, so it passed
whether or not the directory was refused; it now holds a workflow that
would show up if it were read.

package-scripts pins every copy of the helper, and the cli, core and
browser-use copies of its tests, byte-identical, so a fix to one cannot
leave the others behind.

Left on the uid check: pathReader's case, whose permissions come from
mock-fs, which lets uid 0 through whatever the mode; the session writer
lease's chown case, which needs root; and the release workflow's wipe
test, which does not depend on permission bits.
2026-10-16 17:34:52 +00:00

1916 lines
69 KiB
JavaScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import {
chmodSync,
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { parse, stringify } from 'yaml';
import { hooks as pnpmHooks, workspacePackageNames } from '../../.pnpmfile.mjs';
import { getPinnedPnpmPackage } from '../pnpm-package.js';
import { INDEPENDENT_PACKAGES } from '../release-packages.mjs';
import { getWorkflowJob, getWorkflowStep } from './workflow-helpers.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, '../..');
const huskyTestEnv = {
HUSKY: '1',
GIT_CONFIG_COUNT: '1',
GIT_CONFIG_KEY_0: 'core.hooksPath',
GIT_CONFIG_VALUE_0: '.husky/_',
};
function readPackageJson() {
return JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
}
function readWorkflow(relativePath) {
return readFileSync(path.join(root, relativePath), 'utf8');
}
const releaseStepScript = readWorkflow('.github/scripts/run-release-step.sh');
describe('package scripts', () => {
it('keeps documented CI variable defaults in sync with workflows', () => {
const guide = readWorkflow('docs/developers/development/ci-variables.md');
const docRows = new Map(
[
...guide.matchAll(
/^\|\s*`(QWEN_\w+)`\s*\|\s*`([^`]+)`\s*\|\s*([^|]+)\s*\|/gm,
),
].map(([, name, value, usedIn]) => [
name,
{
defaultVal: value,
files: [...usedIn.matchAll(/`([^`]+\.yml)`/g)].map(([, f]) => f),
},
]),
);
// These variables are documented below the test-execution table.
const excludedNonTestVariables = new Set([
'QWEN_RELEASE_STATIC_TIMEOUT_MINUTES',
'QWEN_RELEASE_BUILD_TIMEOUT_MINUTES',
]);
const relatedRows = new Map(
[...guide.matchAll(/`(QWEN_RELEASE_\w+)` \(default `([^`]+)`/g)].map(
([, name, value]) => [name, value],
),
);
const workflowFiles = ['ci.yml', 'release.yml'];
const workflowVars = new Map();
for (const file of workflowFiles) {
const workflow = readWorkflow(`.github/workflows/${file}`);
for (const [, name] of workflow.matchAll(/vars\.(QWEN_\w+)/g)) {
if (!workflowVars.has(name)) {
workflowVars.set(name, { fallbacks: new Set(), files: new Set() });
}
const entry = workflowVars.get(name);
entry.files.add(file);
}
for (const [, name, fallback] of workflow.matchAll(
/vars\.(QWEN_\w+)\s*\|\|\s*'([^']+)'/g,
)) {
workflowVars.get(name).fallbacks.add(fallback);
}
}
// Every workflow variable must have a known fallback and a matching doc.
for (const [name, info] of workflowVars.entries()) {
expect(info.fallbacks.size, `No fallback found for ${name}`).toBe(1);
const [fallback] = info.fallbacks;
if (excludedNonTestVariables.has(name)) {
expect(info.files, `Unexpected workflow for ${name}`).toEqual(
new Set(['release.yml']),
);
expect(relatedRows.get(name), `Related variable ${name} drifted`).toBe(
fallback,
);
expect(docRows.has(name), `${name} is outside the test table`).toBe(
false,
);
continue;
}
const docEntry = docRows.get(name);
expect(
docEntry,
`Workflow variable ${name} missing from docs`,
).toBeDefined();
expect(docEntry.defaultVal, `Default mismatch for ${name}`).toBe(
fallback,
);
expect(new Set(docEntry.files), `Used-in mismatch for ${name}`).toEqual(
info.files,
);
}
// A deleted workflow variable must not linger in the documentation.
for (const [name, docEntry] of docRows.entries()) {
expect(
workflowVars.has(name),
`Documented variable ${name} not found in test workflows`,
).toBe(true);
expect(
docEntry.files.length,
`No workflow listed for ${name}`,
).toBeGreaterThan(0);
}
for (const name of excludedNonTestVariables) {
expect(workflowVars.has(name), `Missing related variable ${name}`).toBe(
true,
);
}
});
it('accepts only an exact pnpm package-manager version', () => {
expect(getPinnedPnpmPackage({ packageManager: 'pnpm@11.24.0' })).toBe(
'pnpm@11.24.0',
);
// `corepack use pnpm@x.y.z` appends the tarball's integrity hash; the
// bootstrap must accept the string corepack itself writes.
const hashed =
'pnpm@11.24.0+sha512.bd27e345e976dcb0be0b7a1228217b049a817e21b1f355c90dbe7dc46671895a8bc1e6d06c24554505ea93ea0b45f489a27ec1bfbc8de6a9659fca0f16fa0000';
expect(getPinnedPnpmPackage({ packageManager: hashed })).toBe(hashed);
expect(() =>
getPinnedPnpmPackage({ packageManager: 'pnpm@latest' }),
).toThrow('packageManager must pin an exact pnpm version');
expect(() =>
getPinnedPnpmPackage({
packageManager: 'pnpm@11.24.0+sha512.not-hex',
}),
).toThrow('packageManager must pin an exact pnpm version');
});
it('pins pnpm with the corepack integrity hash', () => {
expect(readPackageJson().packageManager).toMatch(
/^pnpm@\d+\.\d+\.\d+\+sha512\.[0-9a-f]{128}$/,
);
});
it('keeps internal pnpm workspaces independent of manifest versions', () => {
const packageJson = {
dependencies: {
'@qwen-code/qwen-code-core': 'file:../core',
'@qwen-code/channel-base': '0.22.4',
fixture: 'file:../fixture',
},
devDependencies: {
'@qwen-code/acp-bridge': 'file:../acp-bridge',
},
optionalDependencies: {
'@qwen-code/sdk': '0.22.4',
},
};
expect(pnpmHooks.readPackage(packageJson)).toEqual({
dependencies: {
'@qwen-code/qwen-code-core': 'workspace:*',
'@qwen-code/channel-base': 'workspace:*',
fixture: 'file:../fixture',
},
devDependencies: {
'@qwen-code/acp-bridge': 'workspace:*',
},
optionalDependencies: {
'@qwen-code/sdk': 'workspace:*',
},
});
});
it('keeps the pnpm rewrite set in sync with the workspace manifests', () => {
const { workspaces } = readPackageJson();
const negated = workspaces
.filter((entry) => entry.startsWith('!'))
.map((entry) => entry.slice(1));
const directories = [];
for (const pattern of workspaces) {
if (pattern.startsWith('!')) continue;
if (pattern.endsWith('/*')) {
const parent = pattern.slice(0, -2);
for (const entry of readdirSync(path.join(root, parent))) {
directories.push(`${parent}/${entry}`);
}
} else {
directories.push(pattern);
}
}
const names = directories
.filter((directory) => !negated.includes(directory))
.map((directory) => {
const manifestPath = path.join(root, directory, 'package.json');
if (!existsSync(manifestPath)) return undefined;
return JSON.parse(readFileSync(manifestPath, 'utf8')).name;
})
.filter((name) => name !== undefined);
// A member missing from the set keeps its release version or file:
// specifier under pnpm, which is exactly the lockfile staleness the
// rewrite exists to prevent.
expect([...workspacePackageNames].sort()).toEqual(
[...new Set(names)].sort(),
);
});
it('mirrors the npm workspace boundaries in pnpm-workspace.yaml', () => {
const workspace = parse(readWorkflow('pnpm-workspace.yaml'));
expect([...workspace.packages].sort()).toEqual(
[...readPackageJson().workspaces].sort(),
);
});
it('mirrors npm overrides in pnpm-workspace.yaml', () => {
const workspace = parse(readWorkflow('pnpm-workspace.yaml'));
const { cliui, ...npmOverrides } = readPackageJson().overrides;
expect(workspace.overrides).toMatchObject({
...npmOverrides,
'cliui>wrap-ansi': cliui['wrap-ansi'],
});
});
it('preserves the registry retry policy under pnpm', () => {
const workspace = parse(readWorkflow('pnpm-workspace.yaml'));
expect(workspace).toMatchObject({
fetchRetries: 5,
fetchRetryMintimeout: 20000,
fetchRetryMaxtimeout: 120000,
fetchTimeout: 300000,
});
});
it('checks the pnpm lockfile for integrity and Playwright parity', () => {
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/check-lockfile.js')],
{ cwd: root, encoding: 'utf8' },
);
expect(result.status).toBe(0);
expect(result.stdout).toContain('pnpm lockfile check passed.');
// Pins the Playwright parity block's existence and happy path: deleting it,
// or returning before it, goes red here. Its drift arms live in
// check-lockfile.test.js, which runs the script against perturbed fixtures.
expect(result.stdout).toContain('Playwright parity check passed.');
});
describe('check-lockfile failure branches', () => {
// Each case runs the real script against a temp root holding copies of
// the committed lockfiles with one mutation, so the detectors — not
// today's lockfile data — are what the assertions pin.
function runCheckLockfile(mutate) {
const fixtureRoot = mkdtempSync(path.join(tmpdir(), 'check-lockfile-'));
try {
// The Playwright parity section reads the pinned manifests as
// well as the lockfiles, so the fixture carries them too: without
// them the run dies on a missing package.json before reaching the
// branch under test.
for (const file of [
'pnpm-lock.yaml',
'pnpm-workspace.yaml',
'package.json',
'packages/web-shell/package.json',
]) {
const to = path.join(fixtureRoot, file);
mkdirSync(path.dirname(to), { recursive: true });
copyFileSync(path.join(root, file), to);
}
mutate(fixtureRoot);
return spawnSync(
process.execPath,
[path.join(root, 'scripts/check-lockfile.js')],
{
encoding: 'utf8',
env: { ...process.env, CHECK_LOCKFILE_ROOT: fixtureRoot },
},
);
} finally {
rmSync(fixtureRoot, { recursive: true, force: true });
}
}
function mutatePnpmLock(fixtureRoot, mutate) {
const file = path.join(fixtureRoot, 'pnpm-lock.yaml');
const lock = parse(readFileSync(file, 'utf8'));
mutate(lock);
writeFileSync(file, stringify(lock));
}
it('fails closed when pnpm-lock.yaml has no packages section', () => {
const result = runCheckLockfile((fixtureRoot) => {
writeFileSync(
path.join(fixtureRoot, 'pnpm-lock.yaml'),
"lockfileVersion: '9.0'\n",
);
});
expect(result.status).toBe(1);
expect(result.stderr).toContain('has no packages section');
});
it('accepts a git dependency pnpm keys by source instead of version', () => {
const resolved = 'git+https://github.com/example/git-dep.git#7ae66ab2';
const result = runCheckLockfile((fixtureRoot) =>
mutatePnpmLock(fixtureRoot, (lock) => {
lock.packages[`git-dep@${resolved}`] = {
resolution: { type: 'git' },
};
}),
);
expect(result.status).toBe(0);
expect(result.stdout).toContain('pnpm lockfile check passed.');
});
it('fails when a registry package loses its integrity hash', () => {
const result = runCheckLockfile((fixtureRoot) =>
mutatePnpmLock(fixtureRoot, (lock) => {
lock.packages['left-pad@1.3.0'] = { resolution: {} };
}),
);
expect(result.status).toBe(1);
expect(result.stderr).toContain('missing "resolution.integrity"');
expect(result.stderr).toContain('- left-pad@1.3.0');
});
});
it('checks undeclared imports in web-shell shipped sources', () => {
// web-shell is published and keeps its shipped sources in client/, so the
// src/ globs reach none of it. Pinning the glob inside this block keeps a
// published package from silently dropping out of the check.
const config = readFileSync(path.join(root, 'eslint.config.js'), 'utf8');
const start = config.indexOf(
'A package must declare what its own sources import',
);
const end = config.indexOf('export-html and insight carry', start);
expect(start).toBeGreaterThan(-1);
expect(end).toBeGreaterThan(start);
expect(config.slice(start, end)).toContain(
"'packages/web-shell/client/**/*.{ts,tsx}'",
);
});
it('keeps the internal release-age exception independent of the version', () => {
const workspace = parse(
readFileSync(path.join(root, 'pnpm-workspace.yaml'), 'utf8'),
);
expect(workspace.minimumReleaseAgeExclude).toEqual([
'@qwen-code/channel-base',
]);
});
it('imports packages without hard links so patch-package cannot corrupt the store', () => {
const workspace = parse(
readFileSync(path.join(root, 'pnpm-workspace.yaml'), 'utf8'),
);
// The root postinstall rewrites node_modules files in place. Under
// 'auto' or 'hardlink' those files are hard links into the
// content-addressable store, so the rewrite corrupts the store entry and
// every later worktree misses its --offline stage.
expect(readPackageJson().scripts.postinstall).toBe('patch-package');
expect(['clone-or-copy', 'copy', 'clone']).toContain(
workspace.packageImportMethod,
);
});
it('keeps the pnpm lockfile out of prettier so formatting cannot fight pnpm', () => {
const ignored = readFileSync(path.join(root, '.prettierignore'), 'utf8')
.split('\n')
.map((line) => line.trim())
.filter((line) => line !== '' && !line.startsWith('#'));
expect(ignored).toContain('pnpm-lock.yaml');
});
it('keeps channel workspace lock entries independent of release versions', () => {
const lockfile = parse(
readFileSync(path.join(root, 'pnpm-lock.yaml'), 'utf8'),
);
const specifiers = Object.values(lockfile.importers).flatMap((importer) =>
['dependencies', 'devDependencies', 'optionalDependencies'].flatMap(
(field) => {
const entry = importer[field]?.['@qwen-code/channel-base'];
return entry ? [entry.specifier] : [];
},
),
);
expect(specifiers.length).toBeGreaterThan(0);
expect(new Set(specifiers)).toEqual(new Set(['workspace:*']));
});
it('bootstraps worktrees and preserves explicit hook settings', () => {
const binDir = mkdtempSync(path.join(tmpdir(), 'qwen-worktree-setup-'));
const commandDir = path.join(binDir, 'runner bin');
const logFile = path.join(binDir, 'corepack.log');
mkdirSync(commandDir);
// The stub husky has to leave the wrapper the bootstrap verifies. It is
// confined to `.husky/_` and removed again unless a real Husky install
// already put one there.
const stubHooksDir = path.join(root, '.husky', '_');
const hadStubHooksDir = existsSync(stubHooksDir);
const runSetup = (envOverride = {}) => {
writeFileSync(logFile, '');
return spawnSync(
process.execPath,
[path.join(root, 'scripts/setup-worktree.js')],
{
cwd: root,
encoding: 'utf8',
env: {
...process.env,
...huskyTestEnv,
...envOverride,
PATH: `${commandDir}${path.delimiter}${process.env.PATH ?? ''}`,
WORKTREE_SETUP_LOG: logFile,
},
},
);
};
try {
if (process.platform === 'win32') {
writeFileSync(
path.join(commandDir, 'corepack.cmd'),
'@echo %QWEN_SKIP_PREPARE% %QWEN_SKIP_NOTICE_GENERATION% %*>>"%WORKTREE_SETUP_LOG%"\r\n@if not "%2"=="exec" exit /b 0\r\n@if exist ".husky\\_\\pre-commit" exit /b 0\r\n@if not exist ".husky\\_" mkdir ".husky\\_"\r\n@type nul > ".husky\\_\\pre-commit"\r\n',
);
} else {
writeFileSync(
path.join(commandDir, 'corepack'),
'#!/bin/sh\necho "$QWEN_SKIP_PREPARE $QWEN_SKIP_NOTICE_GENERATION $*" >> "$WORKTREE_SETUP_LOG"\n[ "$2" = "exec" ] || exit 0\n[ -e .husky/_/pre-commit ] && exit 0\nmkdir -p .husky/_\n: > .husky/_/pre-commit\n',
);
chmodSync(path.join(commandDir, 'corepack'), 0o755);
}
const result = runSetup();
expect(result.status).toBe(0);
expect(readFileSync(logFile, 'utf8').trim().split(/\r?\n/)).toEqual([
'1 1 pnpm install --frozen-lockfile --offline',
'1 1 pnpm exec husky',
]);
expect(runSetup({ HUSKY: '0' }).status).toBe(0);
expect(readFileSync(logFile, 'utf8').trim()).toBe(
'1 1 pnpm install --frozen-lockfile --offline',
);
expect(runSetup({ GIT_CONFIG_VALUE_0: '/custom/hooks' }).status).toBe(0);
expect(readFileSync(logFile, 'utf8').trim()).toBe(
'1 1 pnpm install --frozen-lockfile --offline',
);
// The pin above proves an explicit HUSKY value is honoured, but the
// state every real shell and CI job is in is unset. Deleting the key
// must still reach husky, so a gate that treats unset as disabled
// (e.g. `!== '1'`) goes red on the missing exec line.
const unsetEnv = {
...process.env,
...huskyTestEnv,
PATH: `${commandDir}${path.delimiter}${process.env.PATH ?? ''}`,
WORKTREE_SETUP_LOG: logFile,
};
for (const key of Object.keys(unsetEnv)) {
if (key.toUpperCase() === 'HUSKY') delete unsetEnv[key];
}
writeFileSync(logFile, '');
const unsetResult = spawnSync(
process.execPath,
[path.join(root, 'scripts/setup-worktree.js')],
{ cwd: root, encoding: 'utf8', env: unsetEnv },
);
expect(unsetResult.status).toBe(0);
expect(readFileSync(logFile, 'utf8').trim().split(/\r?\n/)).toEqual([
'1 1 pnpm install --frozen-lockfile --offline',
'1 1 pnpm exec husky',
]);
} finally {
if (!hadStubHooksDir) {
rmSync(stubHooksDir, { recursive: true, force: true });
}
rmSync(binDir, { recursive: true, force: true });
}
});
/**
* The hook logic reads `core.hooksPath` from git and asks git which root owns
* the config husky would write. Neither is reachable from the injected
* `GIT_CONFIG_*` constant above: it pins the value for the child's whole
* lifetime, so the unset state — the only one where husky is asked to write —
* cannot come from it, and ownership comes from `git rev-parse`, which
* answers only for a real layout. These cases build throwaway trees with real
* git commands instead: a primary checkout (`.git` directory), a linked
* worktree and a `--separate-git-dir` clone (both `.git` files, only the
* clone owning its config), and a directory with no repository at all. The
* stub husky lets each case choose what husky writes and what it exits with.
*/
it('installs hooks only where the checkout owns the repository config', () => {
const sandbox = mkdtempSync(path.join(tmpdir(), 'qwen-worktree-hooks-'));
const commandDir = path.join(sandbox, 'bin');
const primary = path.join(sandbox, 'primary');
const linked = path.join(sandbox, 'linked');
const separate = path.join(sandbox, 'separate');
const separateGitDir = path.join(sandbox, 'separate-git');
const noRepo = path.join(sandbox, 'no-repo');
const logFile = path.join(sandbox, 'corepack.log');
const emptyConfig = path.join(sandbox, 'empty-config');
const gitEnv = {
...process.env,
GIT_CONFIG_COUNT: '0',
GIT_CONFIG_GLOBAL: emptyConfig,
GIT_CONFIG_SYSTEM: emptyConfig,
};
const installLine = '1 1 pnpm install --frozen-lockfile --offline';
const huskyLine = '1 1 pnpm exec husky';
try {
mkdirSync(commandDir, { recursive: true });
writeFileSync(emptyConfig, '');
// Stands in for husky: logs the invocation like the neighbouring stubs,
// then writes what husky writes and exits as the case asks.
const stub =
process.platform === 'win32'
? '@echo %QWEN_SKIP_PREPARE% %QWEN_SKIP_NOTICE_GENERATION% %*>>"%WORKTREE_SETUP_LOG%"\r\n@if not "%2"=="exec" exit /b 0\r\n@if not "%STUB_HUSKY_EXIT%"=="" exit /b %STUB_HUSKY_EXIT%\r\n@if "%STUB_HUSKY_SETS_HOOKS_PATH%"=="1" git config core.hooksPath .husky/_\r\n@if not "%STUB_HUSKY_WRITES_HOOKS%"=="1" exit /b 0\r\n@if not exist ".husky\\_" mkdir ".husky\\_"\r\n@type nul > ".husky\\_\\pre-commit"\r\n'
: '#!/bin/sh\necho "$QWEN_SKIP_PREPARE $QWEN_SKIP_NOTICE_GENERATION $*" >> "$WORKTREE_SETUP_LOG"\n[ "$2" = "exec" ] || exit 0\n[ -z "$STUB_HUSKY_EXIT" ] || exit "$STUB_HUSKY_EXIT"\n[ "$STUB_HUSKY_SETS_HOOKS_PATH" = "1" ] && git config core.hooksPath .husky/_\n[ "$STUB_HUSKY_WRITES_HOOKS" = "1" ] && mkdir -p .husky/_ && : > .husky/_/pre-commit\nexit 0\n';
writeFileSync(
path.join(
commandDir,
process.platform === 'win32' ? 'corepack.cmd' : 'corepack',
),
stub,
);
if (process.platform !== 'win32') {
chmodSync(path.join(commandDir, 'corepack'), 0o755);
}
const seedCheckout = (checkout) => {
mkdirSync(path.join(checkout, 'scripts'), { recursive: true });
writeFileSync(
path.join(checkout, 'package.json'),
`${JSON.stringify({ packageManager: 'pnpm@11.24.0' }, null, 2)}\n`,
);
for (const script of ['setup-worktree.js', 'pnpm-package.js']) {
writeFileSync(
path.join(checkout, 'scripts', script),
readFileSync(path.join(root, 'scripts', script), 'utf8'),
);
}
};
const git = (args, cwd) =>
spawnSync('git', args, { cwd, encoding: 'utf8', env: gitEnv });
const identity = [
'-c',
'user.name=fixture',
'-c',
'user.email=fixture@example.com',
];
seedCheckout(primary);
expect(git(['init', '--quiet', primary], sandbox).status).toBe(0);
expect(git(['add', '--all'], primary).status).toBe(0);
expect(
git([...identity, 'commit', '--quiet', '-m', 'fixture'], primary)
.status,
).toBe(0);
expect(
git(['worktree', 'add', '--quiet', '--detach', linked], primary).status,
).toBe(0);
expect(
git(
[
'clone',
'--quiet',
'--separate-git-dir',
separateGitDir,
primary,
separate,
],
sandbox,
).status,
).toBe(0);
seedCheckout(noRepo);
const runSetup = (checkout, huskyStub = {}) => {
writeFileSync(logFile, '');
return spawnSync(
process.execPath,
[path.join(checkout, 'scripts', 'setup-worktree.js')],
{
cwd: checkout,
encoding: 'utf8',
env: {
...process.env,
HUSKY: '1',
STUB_HUSKY_SETS_HOOKS_PATH: huskyStub.setsHooksPath ? '1' : '0',
STUB_HUSKY_WRITES_HOOKS: huskyStub.writesHooks ? '1' : '0',
STUB_HUSKY_EXIT: huskyStub.exit ?? '',
PATH: `${commandDir}${path.delimiter}${process.env.PATH ?? ''}`,
WORKTREE_SETUP_LOG: logFile,
// Read and write the throwaway trees' own config rather than the
// host checkout's, which already carries `core.hooksPath`.
GIT_CONFIG_COUNT: '0',
GIT_CONFIG_GLOBAL: emptyConfig,
GIT_CONFIG_SYSTEM: emptyConfig,
},
},
);
};
const hooksPathIsSet = (checkout) =>
git(['config', '--get', 'core.hooksPath'], checkout).status === 0;
const resetPrimaryHooks = () => {
git(['config', '--unset', 'core.hooksPath'], primary);
rmSync(path.join(primary, '.husky'), { recursive: true, force: true });
};
// A linked worktree with the key unset must not let husky add it to the
// config every worktree of the repository shares.
const linkedRun = runSetup(linked, {
setsHooksPath: true,
writesHooks: true,
});
expect(linkedRun.status).toBe(0);
expect(linkedRun.stdout).toContain('skipping Husky');
// The skip leaves the worktree hook-less until it is re-run after the
// primary installs, so the notice must carry that recovery path.
expect(linkedRun.stdout).toContain(
'Re-run this script here once hooks are installed in the primary checkout.',
);
expect(readFileSync(logFile, 'utf8').trim()).toBe(installLine);
expect(hooksPathIsSet(primary)).toBe(false);
expect(existsSync(path.join(linked, '.husky'))).toBe(false);
// The primary checkout owns the config husky writes, so the same unset
// key installs hooks there.
const primaryRun = runSetup(primary, {
setsHooksPath: true,
writesHooks: true,
});
expect(primaryRun.status).toBe(0);
expect(readFileSync(logFile, 'utf8').trim().split(/\r?\n/)).toEqual([
installLine,
huskyLine,
]);
expect(hooksPathIsSet(primary)).toBe(true);
expect(existsSync(path.join(primary, '.husky', '_', 'pre-commit'))).toBe(
true,
);
// A `.git` file does not by itself mean another root owns the config: a
// `--separate-git-dir` clone owns its own, so hooks install there.
const separateRun = runSetup(separate, {
setsHooksPath: true,
writesHooks: true,
});
expect(separateRun.status).toBe(0);
expect(readFileSync(logFile, 'utf8').trim().split(/\r?\n/)).toEqual([
installLine,
huskyLine,
]);
expect(hooksPathIsSet(separate)).toBe(true);
// With no repository at all there is no config to write, so husky is
// skipped rather than run into its `.git can't be found` soft failure.
const noRepoRun = runSetup(noRepo, {
setsHooksPath: true,
writesHooks: true,
});
expect(noRepoRun.status).toBe(0);
expect(noRepoRun.stdout).toContain('skipping Husky');
expect(readFileSync(logFile, 'utf8').trim()).toBe(installLine);
expect(existsSync(path.join(noRepo, '.husky'))).toBe(false);
// A husky that configures the hooks path but writes no wrappers still
// ships a hook-less checkout, so the on-disk half fires on its own.
resetPrimaryHooks();
const configOnly = runSetup(primary, { setsHooksPath: true });
expect(configOnly.status).toBe(1);
expect(configOnly.stderr).toContain('Husky did not install hooks');
// A husky that exits 0 having written nothing fails closed too.
resetPrimaryHooks();
const hookless = runSetup(primary);
expect(hookless.status).toBe(1);
expect(hookless.stderr).toContain('Husky did not install hooks');
// A husky that fails outright decides the exit code, not the install
// result that preceded it.
const failing = runSetup(primary, { exit: '7' });
expect(failing.status).toBe(7);
expect(readFileSync(logFile, 'utf8').trim().split(/\r?\n/)).toEqual([
installLine,
huskyLine,
]);
// A git that answers but refuses to read the config (a shared pool's
// dubious-ownership exit 128, a config error) is not "key unset": the
// read failure must surface rather than land on a skip branch with a
// green exit. A script stub cannot shadow git.exe on Windows.
if (process.platform !== 'win32') {
writeFileSync(path.join(commandDir, 'git'), '#!/bin/sh\nexit 128\n');
chmodSync(path.join(commandDir, 'git'), 0o755);
const unreadable = runSetup(primary, {
setsHooksPath: true,
writesHooks: true,
});
expect(unreadable.status).toBe(1);
expect(unreadable.stdout).not.toContain('skipping Husky');
expect(unreadable.stderr).toContain('could not read core.hooksPath');
}
} finally {
rmSync(sandbox, { recursive: true, force: true });
}
});
it.skipIf(process.platform !== 'win32')(
'resolves the path variable under its native Windows casing',
() => {
const binDir = mkdtempSync(path.join(tmpdir(), 'qwen-worktree-path-'));
const commandDir = path.join(binDir, 'runner bin');
const logFile = path.join(binDir, 'corepack.log');
mkdirSync(commandDir);
try {
writeFileSync(
path.join(commandDir, 'corepack.cmd'),
'@echo %QWEN_SKIP_PREPARE% %QWEN_SKIP_NOTICE_GENERATION% %*>>"%WORKTREE_SETUP_LOG%"\r\n',
);
// Native shells expose the path variable as `Path`; a case-sensitive
// `env.PATH` read on the spread object would miss it and report
// Corepack unavailable.
const env = {
...process.env,
...huskyTestEnv,
WORKTREE_SETUP_LOG: logFile,
};
delete env.PATH;
delete env.Path;
delete env.HUSKY;
env.Path = `${commandDir}${path.delimiter}${process.env.Path ?? process.env.PATH ?? ''}`;
env.Husky = '0';
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/setup-worktree.js')],
{
cwd: root,
encoding: 'utf8',
env,
},
);
expect(result.status).toBe(0);
expect(readFileSync(logFile, 'utf8').trim()).toBe(
'1 1 pnpm install --frozen-lockfile --offline',
);
} finally {
rmSync(binDir, { recursive: true, force: true });
}
},
);
it('fails closed when Corepack is unavailable', () => {
const binDir = mkdtempSync(
path.join(tmpdir(), 'qwen-worktree-no-corepack-'),
);
const env = { ...process.env, PATH: binDir };
for (const name of Object.keys(env)) {
if (name !== 'PATH' && name.toUpperCase() === 'PATH') delete env[name];
}
try {
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/setup-worktree.js')],
{ cwd: root, encoding: 'utf8', env },
);
expect(result.status).toBe(1);
expect(result.stderr).toContain('Corepack is required');
} finally {
rmSync(binDir, { recursive: true, force: true });
}
});
it.skipIf(process.platform === 'win32')(
'ignores a PATH entry that cannot be executed',
() => {
// A `corepack` left without its exec bit by a half-removed toolchain
// used to be chosen anyway, and the spawn then died with EACCES instead
// of the actionable message below.
const binDir = mkdtempSync(path.join(tmpdir(), 'qwen-worktree-noexec-'));
const env = { ...process.env, PATH: binDir };
try {
writeFileSync(path.join(binDir, 'corepack'), '#!/bin/sh\nexit 0\n');
chmodSync(path.join(binDir, 'corepack'), 0o644);
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/setup-worktree.js')],
{ cwd: root, encoding: 'utf8', env },
);
expect(result.status).toBe(1);
expect(result.stderr).toContain('Corepack is required');
} finally {
rmSync(binDir, { recursive: true, force: true });
}
},
);
it('falls back to registry access when the pnpm store is incomplete', () => {
const binDir = mkdtempSync(path.join(tmpdir(), 'qwen-worktree-fallback-'));
const logFile = path.join(binDir, 'corepack.log');
try {
if (process.platform === 'win32') {
writeFileSync(
path.join(binDir, 'corepack.cmd'),
'@echo %QWEN_SKIP_PREPARE% %QWEN_SKIP_NOTICE_GENERATION% %*>>"%WORKTREE_SETUP_LOG%"\r\n@if "%4"=="--offline" exit /b 1\r\n',
);
} else {
writeFileSync(
path.join(binDir, 'corepack'),
'#!/bin/sh\necho "$QWEN_SKIP_PREPARE $QWEN_SKIP_NOTICE_GENERATION $*" >> "$WORKTREE_SETUP_LOG"\n[ "$4" != "--offline" ]\n',
);
chmodSync(path.join(binDir, 'corepack'), 0o755);
}
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/setup-worktree.js')],
{
cwd: root,
encoding: 'utf8',
// `PATH` holds only the stub directory, so the fallback stays pinned
// as needing no ambient tooling: with no git to resolve a repository
// the hook step reports itself skipped rather than failing an install
// that succeeded.
env: {
...process.env,
HUSKY: '1',
PATH: binDir,
WORKTREE_SETUP_LOG: logFile,
},
},
);
expect(result.status).toBe(0);
expect(result.stdout).toContain('skipping Husky');
expect(readFileSync(logFile, 'utf8').trim().split(/\r?\n/)).toEqual([
'1 1 pnpm install --frozen-lockfile --offline',
'1 1 pnpm install --frozen-lockfile --prefer-offline',
]);
} finally {
rmSync(binDir, { recursive: true, force: true });
}
});
it('preserves a worktree bootstrap interrupt status', () => {
const binDir = mkdtempSync(path.join(tmpdir(), 'qwen-worktree-signal-'));
const logFile = path.join(binDir, 'corepack.log');
try {
if (process.platform === 'win32') {
writeFileSync(
path.join(binDir, 'corepack.cmd'),
'@echo called>>"%WORKTREE_SETUP_LOG%"\r\n@exit /b 130\r\n',
);
} else {
writeFileSync(
path.join(binDir, 'corepack'),
'#!/bin/sh\necho called >> "$WORKTREE_SETUP_LOG"\nkill -INT $$\n',
);
chmodSync(path.join(binDir, 'corepack'), 0o755);
}
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/setup-worktree.js')],
{
cwd: root,
encoding: 'utf8',
env: {
...process.env,
PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ''}`,
WORKTREE_SETUP_LOG: logFile,
},
},
);
expect(result.status).toBe(130);
expect(readFileSync(logFile, 'utf8').trim()).toBe('called');
} finally {
rmSync(binDir, { recursive: true, force: true });
}
});
it('does not couple Node REPL to Qwen release versions', () => {
const versionScript = readFileSync(
path.join(root, 'scripts/version.js'),
'utf8',
);
expect(versionScript).toContain('INDEPENDENT_PACKAGES.map((name)');
expect(INDEPENDENT_PACKAGES).toContain('@qwen-code/node-repl-mcp');
});
it('smoke-tests the real worktree bootstrap on every supported host', () => {
const workflow = parse(
readWorkflow('.github/workflows/pnpm-worktree-smoke.yml'),
);
const job = workflow.jobs.install;
expect(job.strategy.matrix.os).toEqual([
'ubuntu-latest',
'macos-latest',
'windows-latest',
]);
expect(job.strategy['fail-fast']).toBe(false);
expect(
job.steps.find(
(step) => step.name === 'Install frozen pnpm worktree dependencies',
)?.run,
).toBe('node scripts/setup-worktree.js');
// The pnpm linker only materializes declared dependencies, so a
// workspace import that npm's hoisting hides breaks typecheck and tests
// in the bootstrapped tree; resolve one through the worst offender.
const resolveCheck = job.steps.find(
(step) => step.name === 'Verify workspace links resolve',
);
expect(resolveCheck?.run).toContain(
'@qwen-code/qwen-code-core/package.json',
);
expect(resolveCheck?.run).toContain('packages/vscode-ide-companion');
// `git diff --exit-code` misses untracked files; the install must leave
// the porcelain output empty on every host, including Windows (hence
// the explicit POSIX shell).
const cleanCheck = job.steps.find(
(step) => step.name === 'Ensure bootstrap keeps the worktree clean',
);
expect(cleanCheck?.shell).toBe('bash');
expect(cleanCheck?.run).toContain('git status --porcelain');
// The checks only mean anything after the install; pin the order.
const stepNames = job.steps.map((step) => step.name);
expect(
stepNames.indexOf('Install frozen pnpm worktree dependencies'),
).toBeLessThan(stepNames.indexOf('Verify workspace links resolve'));
expect(stepNames.indexOf('Verify workspace links resolve')).toBeLessThan(
stepNames.indexOf('Ensure bootstrap keeps the worktree clean'),
);
// A post-merge run is the only witness of a regression on main, so
// consecutive merges must not cancel each other.
expect(workflow.concurrency['cancel-in-progress']).toBe(
"${{ github.event_name == 'pull_request' }}",
);
// Substring check on the raw job text: an exact `step.run` match is
// bypassed by any other spelling of a build step (block scalar,
// compound command).
const installJob = getWorkflowJob(
readWorkflow('.github/workflows/pnpm-worktree-smoke.yml'),
'install',
);
expect(installJob).not.toContain('npm run build');
expect(installJob).not.toContain('node scripts/build.js');
});
it('runs the pnpm smoke workflow when a dependency input changes', () => {
const workflow = parse(
readWorkflow('.github/workflows/pnpm-worktree-smoke.yml'),
);
// The install validates pnpm-lock.yaml against the workspace manifests,
// so these are the inputs that can change its result.
const expectedPaths = [
'.github/workflows/pnpm-worktree-smoke.yml',
'.npmrc',
'.pnpmfile.mjs',
'package.json',
'packages/*/package.json',
'!packages/desktop/package.json',
'!packages/live-host/package.json',
'!packages/mobile-shell/package.json',
'packages/channels/*/package.json',
'integrations/*/package.json',
'patches/**',
'packages/audio-capture/install.js',
'packages/core/scripts/postinstall.js',
'packages/vscode-ide-companion/scripts/generate-notices.js',
'pnpm-lock.yaml',
'pnpm-workspace.yaml',
'scripts/generate-git-commit-info.js',
'scripts/prepare.js',
'scripts/pnpm-package.js',
'scripts/setup-worktree.js',
];
expect(workflow.on.pull_request.paths).toEqual(expectedPaths);
expect(workflow.on.push.paths).toEqual(expectedPaths);
});
it('includes the standalone qwen-live daemon in recursive root builds', () => {
const buildScript = readFileSync(
path.join(root, 'scripts/build.js'),
'utf8',
);
expect(buildScript).toContain('corepack pnpm -r');
expect(buildScript).not.toContain('!@qwen-code/qwen-live');
expect(readPackageJson().workspaces).toContain('packages/*');
});
it('detects an unapplied ink patch by a file the patch creates', () => {
const buildScript = readFileSync(
path.join(root, 'scripts/build.js'),
'utf8',
);
const sentinel = buildScript.match(
/'node_modules\/ink\/build\/([^']+)'/,
)?.[1];
expect(sentinel).toBeDefined();
expect(buildScript).toContain('npm run postinstall');
const inkPatch = readdirSync(path.join(root, 'patches')).find((name) =>
name.startsWith('ink+'),
);
expect(inkPatch).toBeDefined();
const created = readFileSync(path.join(root, 'patches', inkPatch), 'utf8')
.split(/^diff --git /m)
.find((file) =>
file.startsWith(`a/node_modules/ink/build/${sentinel} b/node_modules/`),
);
// The sentinel only proves the patch is missing if no published ink ships
// it, i.e. the patch must create the file rather than edit an existing one.
expect(created).toContain('new file mode');
});
it('selects the CLI dependency closure without selecting the same-named root', () => {
const buildScript = readFileSync(
path.join(root, 'scripts/build.js'),
'utf8',
);
const selector = buildScript.match(/\? '--filter "([^"]+)"/)?.[1];
expect(selector).toBeDefined();
const result = spawnSync(
'corepack',
['pnpm', '--filter', selector, 'list', '--depth', '-1', '--json'],
{ cwd: root, encoding: 'utf8', shell: process.platform === 'win32' },
);
expect(result.status, result.stderr).toBe(0);
const paths = JSON.parse(result.stdout).map((pkg) =>
path.relative(root, pkg.path).replaceAll('\\', '/'),
);
expect(paths).toEqual(
expect.arrayContaining([
'packages/cli',
'packages/core',
'packages/browser-use',
'packages/sdk-typescript',
'packages/web-shell',
'packages/web-templates',
]),
);
expect(paths).not.toContain('');
expect(paths).not.toContain('packages/mobile-mcp');
expect(paths).not.toContain('packages/vscode-ide-companion');
});
it('keeps the Mem0 Extension manifest aligned with release versions', () => {
const versionScript = readFileSync(
path.join(root, 'scripts/version.js'),
'utf8',
);
expect(versionScript).toContain(
"'integrations/external-context-mem0/qwen-extension.json'",
);
expect(versionScript).toContain(
'const mem0Manifest = readJson(mem0ManifestPath);',
);
expect(versionScript).toContain('mem0Manifest.version = newVersion');
expect(versionScript).toContain(
'writeJson(mem0ManifestPath, mem0Manifest);',
);
expect(versionScript).toContain(
"'npx prettier --experimental-cli --write integrations/external-context-mem0/qwen-extension.json'",
);
});
it('lets the CI unit lane retry a contended attempt', () => {
// Identical work measures 6.7min or 36min on this fleet depending only on
// which host the job lands on, and the same commit run three times failed
// three disjoint test sets (#10490). A retry lets that pass; a real break
// still fails every attempt. The release lane already runs this way.
const packageJson = readPackageJson();
// `test:ci` ends in `&&`, so args appended to it would reach only
// `test:scripts`. The workspaces half is its own script, ending in `--`,
// so a flag appended to it reaches every workspace's vitest.
expect(packageJson.scripts['test:ci:workspaces']).toMatch(/--$/);
expect(packageJson.scripts['test:ci:workspaces']).toContain(
'npm run test:ci --workspaces --if-present',
);
expect(packageJson.scripts['test:ci']).toBe(
'npm run test:ci:workspaces && npm run test:scripts',
);
const step = getWorkflowStep(
getWorkflowJob(readWorkflow('.github/workflows/ci.yml'), 'test'),
'Run tests and generate reports',
);
expect(step).toContain(
'VITEST_RETRY: "${{ vars.QWEN_CI_VITEST_RETRY || \'2\' }}"',
);
// 'off' must omit the flag rather than pass --retry=0, which would
// outrank a workspace's own config-level retry (sdk-typescript).
expect(step).toContain('[ "${VITEST_RETRY}" != \'off\' ]');
expect(step).toContain('retry_arg=(--retry="${VITEST_RETRY}")');
expect(step).toContain('npm run test:ci:workspaces -- "${retry_arg[@]}"');
expect(step).toContain('npm run test:scripts -- "${retry_arg[@]}"');
});
it('drops DAC capabilities for root unit tests on both CI commands', () => {
// The permission-bit probes in the workspaces and scripts/tests ask the
// kernel, so on a root lane that keeps these capabilities every case
// they gate skips, without failing.
const step = getWorkflowStep(
getWorkflowJob(readWorkflow('.github/workflows/ci.yml'), 'test'),
'Run tests and generate reports',
);
// The step's code, one command per entry: indentation, blank lines and
// comments do not change what runs, a commented-out line does.
const code = step
.split('\n')
.map((line) => line.trim())
.filter((line) => line && !line.startsWith('#'));
const setpriv =
"test_runner=(setpriv '--bounding-set=-dac_override,-dac_read_search' '--inh-caps=-dac_override,-dac_read_search')";
const branch = code.indexOf('if [ "$(id -u)" -eq 0 ]; then');
expect(branch).toBeGreaterThan(0);
expect(code.slice(branch - 1, branch + 5)).toEqual([
'test_runner=()',
'if [ "$(id -u)" -eq 0 ]; then',
'chown -R 0 "$GITHUB_WORKSPACE" "$HOME"',
"echo 'Dropping DAC override capabilities for root unit tests'",
setpriv,
'fi',
]);
// Nothing else touches it (no unset, read or reset), and both test
// commands run under it, with no unwrapped copy beside them.
expect(code.filter((line) => /\btest_runner\b/.test(line))).toEqual([
'test_runner=()',
setpriv,
'"${test_runner[@]}" npm run test:ci:workspaces -- "${retry_arg[@]}"',
'"${test_runner[@]}" npm run test:scripts -- "${retry_arg[@]}"',
]);
expect(
code.filter((line) =>
/\bnpm run test:(?:ci:workspaces|scripts)\b/.test(line),
),
).toHaveLength(2);
});
it('keeps every copy of the permission-bit probe identical', () => {
// The probe is copied, not shared: each package that uses it keeps its
// own copy, and cli, core and browser-use also keep its tests (wecom
// inlines a smaller probe for its one case). A fix to one copy has to
// reach the others, or the packages measure differently.
const copies = {
'permission-bits.ts': [
'packages/cli/src/test-utils',
'packages/core/src/test-utils',
'packages/browser-use/src/test-utils',
'packages/sdk-typescript/test/test-utils',
'integrations/test-failure-explorer/src/test-utils',
],
'permission-bits.test.ts': [
'packages/cli/src/test-utils',
'packages/core/src/test-utils',
'packages/browser-use/src/test-utils',
],
'permission-bits.mocked-fs.test.ts': [
'packages/cli/src/test-utils',
'packages/core/src/test-utils',
'packages/browser-use/src/test-utils',
],
};
for (const [file, dirs] of Object.entries(copies)) {
const [reference, ...others] = dirs.map((dir) =>
readFileSync(path.join(root, dir, file), 'utf8'),
);
others.forEach((text, i) => {
expect(text, `${file}: ${dirs[i + 1]} vs ${dirs[0]}`).toBe(reference);
});
}
});
it('bounds the CI unit step so a hang fails instead of stalling', () => {
// #10490's run 3 was cancelled at ~60 minutes, leaving behind
// orphaned test processes; a stall reads as a timeout, not a failure.
const step = getWorkflowStep(
getWorkflowJob(readWorkflow('.github/workflows/ci.yml'), 'test'),
'Run tests and generate reports',
);
expect(step).toContain('timeout-minutes: 110');
});
it('keeps the serve fast-path bundle check outside unit test scripts', () => {
const packageJson = readPackageJson();
expect(packageJson.scripts['test:ci']).not.toContain(
'npm run check:serve-fast-path-bundle',
);
expect(packageJson.scripts.preflight).toContain(
'npm run check:serve-fast-path-bundle',
);
});
it('limits SDK integration tests through the forks pool', () => {
const packageJson = readPackageJson();
expect(packageJson.scripts['test:integration:sdk:sandbox:none']).toContain(
'--poolOptions.forks.maxForks 2',
);
expect(
packageJson.scripts['test:integration:sdk:sandbox:docker'],
).toContain('--poolOptions.forks.maxForks 2');
});
it('cleans package build artifacts before checking the serve fast path bundle', () => {
const packageJson = readPackageJson();
expect(packageJson.scripts['check:serve-fast-path-bundle']).toBe(
[
'node scripts/clean-package-build-artifacts.js',
'&& npm run build -- --cli-only',
'&& cross-env DEV=true npm run bundle',
'&& node scripts/check-serve-fast-path-bundle.js',
].join(' '),
);
expect(packageJson.scripts['check:serve-fast-path-bundle']).not.toContain(
'npm run clean',
);
});
it('defines a release test script that disables workspace coverage', () => {
const packageJson = readPackageJson();
expect(packageJson.scripts['test:release']).toBe(
'npm run test:release:workspaces && npm run test:scripts',
);
expect(packageJson.scripts['test:release:workspaces']).toBe(
[
'cross-env NODE_OPTIONS="--max-old-space-size=3072"',
'npm run test:ci --workspaces --if-present -- --coverage.enabled=false',
].join(' '),
);
// No workspace forces coverage from its test:ci script any more; the
// configs decide, and only a post-merge run flips their switch on. A
// reintroduced `--coverage` flag would override the switch on every
// pull-request run.
for (const workspace of [
'packages/vscode-ide-companion',
'packages/web-shell',
]) {
const workspacePackageJson = JSON.parse(
readFileSync(path.join(root, workspace, 'package.json'), 'utf8'),
);
expect(workspacePackageJson.scripts['test:ci']).not.toContain(
'--coverage',
);
const vitestConfig = readFileSync(
path.join(root, workspace, 'vitest.config.ts'),
'utf8',
);
expect(vitestConfig).toContain(
"enabled: process.env['QWEN_CI_COVERAGE'] === '1'",
);
}
});
it('skips build/bundle/husky but still generates git-commit info when CI builds explicitly', () => {
const packageJson = readPackageJson();
expect(packageJson.scripts.prepare).toBe('node scripts/prepare.js');
const binDir = mkdtempSync(path.join(tmpdir(), 'qwen-prepare-skip-'));
const logFile = path.join(binDir, 'commands.log');
writeFileSync(logFile, '');
try {
if (process.platform === 'win32') {
writeFileSync(
path.join(binDir, 'husky.cmd'),
'@echo(husky>>"%PREPARE_LOG_FILE%"\r\n',
);
writeFileSync(
path.join(binDir, 'npm.cmd'),
'@echo(npm %*>>"%PREPARE_LOG_FILE%"\r\n',
);
} else {
writeFileSync(
path.join(binDir, 'husky'),
'#!/bin/sh\necho husky >> "$PREPARE_LOG_FILE"\n',
);
writeFileSync(
path.join(binDir, 'npm'),
'#!/bin/sh\necho "npm $*" >> "$PREPARE_LOG_FILE"\n',
);
chmodSync(path.join(binDir, 'husky'), 0o755);
chmodSync(path.join(binDir, 'npm'), 0o755);
}
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/prepare.js')],
{
cwd: root,
encoding: 'utf8',
env: {
...process.env,
PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ''}`,
PREPARE_LOG_FILE: logFile,
QWEN_SKIP_PREPARE: '1',
},
},
);
expect(result.status).toBe(0);
expect(result.stdout).toContain('Skipping prepare');
// git-commit info is still generated so a later per-workspace build or
// typecheck (e.g. the review tooling's) doesn't fail on the missing
// module; the heavy build/bundle/husky are skipped.
expect(readFileSync(logFile, 'utf8').trim().split(/\r?\n/)).toEqual([
'npm run generate',
]);
} finally {
rmSync(binDir, { recursive: true, force: true });
}
});
it('runs prepare steps in order when CI does not skip prepare', () => {
const binDir = mkdtempSync(path.join(tmpdir(), 'qwen-prepare-bin-'));
const logFile = path.join(binDir, 'commands.log');
try {
if (process.platform === 'win32') {
writeFileSync(
path.join(binDir, 'husky.cmd'),
'@echo(husky>>"%PREPARE_LOG_FILE%"\r\n',
);
writeFileSync(
path.join(binDir, 'npm.cmd'),
'@echo(npm %*>>"%PREPARE_LOG_FILE%"\r\n',
);
} else {
writeFileSync(
path.join(binDir, 'husky'),
'#!/bin/sh\necho husky >> "$PREPARE_LOG_FILE"\n',
);
writeFileSync(
path.join(binDir, 'npm'),
'#!/bin/sh\necho "npm $*" >> "$PREPARE_LOG_FILE"\n',
);
chmodSync(path.join(binDir, 'husky'), 0o755);
chmodSync(path.join(binDir, 'npm'), 0o755);
}
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/prepare.js')],
{
cwd: root,
encoding: 'utf8',
env: {
...process.env,
PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ''}`,
PREPARE_LOG_FILE: logFile,
QWEN_SKIP_PREPARE: '',
},
},
);
expect(result.status).toBe(0);
expect(readFileSync(logFile, 'utf8').trim().split(/\r?\n/)).toEqual([
'husky',
'npm run build',
'npm run bundle',
]);
} finally {
rmSync(binDir, { recursive: true, force: true });
}
});
it('exits when a prepare step fails', () => {
const binDir = mkdtempSync(path.join(tmpdir(), 'qwen-prepare-fail-'));
const logFile = path.join(binDir, 'commands.log');
writeFileSync(logFile, '');
try {
if (process.platform === 'win32') {
writeFileSync(path.join(binDir, 'husky.cmd'), '@exit /b 7\r\n');
writeFileSync(
path.join(binDir, 'npm.cmd'),
'@echo npm %* >> "%PREPARE_LOG_FILE%"\r\n',
);
} else {
writeFileSync(path.join(binDir, 'husky'), '#!/bin/sh\nexit 7\n');
writeFileSync(
path.join(binDir, 'npm'),
'#!/bin/sh\necho "npm $*" >> "$PREPARE_LOG_FILE"\n',
);
chmodSync(path.join(binDir, 'husky'), 0o755);
chmodSync(path.join(binDir, 'npm'), 0o755);
}
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/prepare.js')],
{
cwd: root,
encoding: 'utf8',
env: {
...process.env,
PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ''}`,
PREPARE_LOG_FILE: logFile,
QWEN_SKIP_PREPARE: '',
},
},
);
expect(result.status).toBe(7);
expect(result.stderr).toContain('prepare: husky exited with status 7');
expect(readFileSync(logFile, 'utf8')).toBe('');
} finally {
rmSync(binDir, { recursive: true, force: true });
}
});
it('reports the failing prepare step after earlier steps succeed', () => {
const binDir = mkdtempSync(path.join(tmpdir(), 'qwen-prepare-late-fail-'));
const logFile = path.join(binDir, 'commands.log');
writeFileSync(logFile, '');
try {
if (process.platform === 'win32') {
writeFileSync(
path.join(binDir, 'husky.cmd'),
'@echo(husky>>"%PREPARE_LOG_FILE%"\r\n',
);
writeFileSync(
path.join(binDir, 'npm.cmd'),
[
'@echo(npm %*>>"%PREPARE_LOG_FILE%"',
'@if "%1 %2"=="run build" exit /b 7',
'@exit /b 0',
'',
].join('\r\n'),
);
} else {
writeFileSync(
path.join(binDir, 'husky'),
'#!/bin/sh\necho husky >> "$PREPARE_LOG_FILE"\n',
);
writeFileSync(
path.join(binDir, 'npm'),
[
'#!/bin/sh',
'echo "npm $*" >> "$PREPARE_LOG_FILE"',
'if [ "$1 $2" = "run build" ]; then exit 7; fi',
'',
].join('\n'),
);
chmodSync(path.join(binDir, 'husky'), 0o755);
chmodSync(path.join(binDir, 'npm'), 0o755);
}
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/prepare.js')],
{
cwd: root,
encoding: 'utf8',
env: {
...process.env,
PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ''}`,
PREPARE_LOG_FILE: logFile,
QWEN_SKIP_PREPARE: '',
},
},
);
expect(result.status).toBe(7);
expect(result.stderr).toContain(
'prepare: npm run build exited with status 7',
);
expect(readFileSync(logFile, 'utf8').trim().split(/\r?\n/)).toEqual([
'husky',
'npm run build',
]);
} finally {
rmSync(binDir, { recursive: true, force: true });
}
});
it.skipIf(process.platform === 'win32')(
'reports when a prepare command is killed by a signal',
() => {
const binDir = mkdtempSync(path.join(tmpdir(), 'qwen-prepare-signal-'));
try {
writeFileSync(path.join(binDir, 'husky'), '#!/bin/sh\nkill -TERM $$\n');
writeFileSync(path.join(binDir, 'npm'), '#!/bin/sh\nexit 0\n');
chmodSync(path.join(binDir, 'husky'), 0o755);
chmodSync(path.join(binDir, 'npm'), 0o755);
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/prepare.js')],
{
cwd: root,
encoding: 'utf8',
env: {
...process.env,
PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ''}`,
QWEN_SKIP_PREPARE: '',
},
},
);
expect(result.status).toBe(1);
expect(result.stderr).toContain(
'prepare: husky killed by signal SIGTERM',
);
} finally {
rmSync(binDir, { recursive: true, force: true });
}
},
);
it.skipIf(process.platform === 'win32')(
'reports when a prepare command cannot be spawned',
() => {
const missingBinDir = mkdtempSync(
path.join(tmpdir(), 'qwen-prepare-missing-bin-'),
);
try {
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/prepare.js')],
{
cwd: root,
encoding: 'utf8',
env: {
...process.env,
PATH: missingBinDir,
QWEN_SKIP_PREPARE: '',
},
},
);
expect(result.status).toBe(1);
expect(result.stderr).toContain('prepare: husky failed:');
} finally {
rmSync(missingBinDir, { recursive: true, force: true });
}
},
);
it('wires release quality checks to fast explicit validation steps', () => {
const workflow = readWorkflow('.github/workflows/release.yml');
const workspaceTestScript = readWorkflow(
'.github/scripts/run-release-workspace-tests.sh',
);
const buildJob = getWorkflowJob(workflow, 'quality_build');
const workspaceTestJob = getWorkflowJob(workflow, 'workspace_tests');
const buildStep = getWorkflowStep(buildJob, 'Build Project');
const serveFastPathStep = getWorkflowStep(
buildJob,
'Check Serve Fast Path Bundle',
);
const packStep = getWorkflowStep(buildJob, 'Pack Build Outputs');
const uploadStep = getWorkflowStep(buildJob, 'Upload Build Outputs');
const verifyPackageStep = getWorkflowStep(
buildJob,
'Verify Prepared Package',
);
const workspaceTestStep = getWorkflowStep(
workspaceTestJob,
'Run Workspace Tests',
);
const scriptsTestStep = getWorkflowStep(
getWorkflowJob(workflow, 'quality_scripts'),
'Run Script Tests',
);
expect(buildJob).toContain("name: 'Check Serve Fast Path Bundle'");
expect(buildJob).toContain('npm run check:serve-fast-path-bundle');
expect(buildJob.indexOf(serveFastPathStep)).toBeLessThan(
buildJob.indexOf(buildStep),
);
expect(buildJob.indexOf(uploadStep)).toBeGreaterThan(
buildJob.indexOf(packStep),
);
expect(buildJob.indexOf(verifyPackageStep)).toBeGreaterThan(
buildJob.indexOf(uploadStep),
);
expect(verifyPackageStep).toContain('run-release-step.sh verify-package');
expect(releaseStepScript).toContain('npm run bundle');
expect(releaseStepScript).toContain('dist/review-sources.sha256');
expect(releaseStepScript).toContain('npm run prepare:package');
expect(workspaceTestStep).toContain(
'.github/scripts/run-release-workspace-tests.sh',
);
expect(workspaceTestScript).toContain('npm run test:release:workspaces');
expect(workspaceTestScript).not.toContain('npm run test:ci');
expect(scriptsTestStep).toContain('npm run test:scripts');
for (const cappedStep of [workspaceTestStep, scriptsTestStep]) {
for (const name of ['VITEST_MAX_THREADS', 'VITEST_MAX_FORKS']) {
expect(cappedStep).toContain(
`${name}: "\${{ startsWith(runner.name, 'ecs-qwen-') && (vars.QWEN_CI_VITEST_MAX_WORKERS || '4') || '' }}"`,
);
}
for (const name of ['VITEST_MIN_THREADS', 'VITEST_MIN_FORKS']) {
expect(cappedStep).toContain(
`${name}: "\${{ startsWith(runner.name, 'ecs-qwen-') && '1' || '' }}"`,
);
}
}
});
it('skips release install-time prepare and builds before publish bundling', () => {
const workflow = readWorkflow('.github/workflows/release.yml');
expect(workflow.slice(0, workflow.indexOf('jobs:'))).not.toContain(
'CI_BOT_PAT',
);
const installSteps =
workflow.match(
/ {6}- (?:&[^\n]+\n {8})?name: 'Install Dependencies'[\s\S]*?(?=\n {6}- (?:&[^\n]+\n {8})?name: '|\n {4}[A-Za-z0-9_-]+:|$)/g,
) || [];
// The validation jobs reuse the anchored install step; only the anchor
// definition plus prepare and publish appear as full textual copies.
expect(installSteps.length).toBe(3);
for (const installStep of installSteps.slice(0, 2)) {
expect(installStep).toContain('npm run generate');
}
for (const installStep of installSteps) {
expect(installStep).toContain(
'corepack pnpm install --frozen-lockfile --ignore-scripts --prefer-offline --reporter=append-only',
);
expect(installStep).toContain('npm run postinstall');
expect(installStep).not.toContain('QWEN_SKIP_PREPARE');
expect(installStep).not.toContain('CI_BOT_PAT');
}
for (const jobName of ['integration_none', 'integration_docker']) {
const integrationJob = getWorkflowJob(workflow, jobName);
const buildStep = getWorkflowStep(integrationJob, 'Build Bundle');
expect(buildStep).toContain('npm run build && npm run bundle');
}
const publishJob = getWorkflowJob(workflow, 'publish');
expect(getWorkflowStep(publishJob, 'Install Dependencies')).not.toContain(
'npm run generate',
);
expect(publishJob.slice(0, publishJob.indexOf('steps:'))).not.toContain(
'CI_BOT_PAT',
);
const checkoutStep = getWorkflowStep(publishJob, 'Checkout');
const releaseBranchStep = getWorkflowStep(
publishJob,
'Prepare release branch',
);
const pushReleaseBranchStep = getWorkflowStep(
publishJob,
'Conditionally push release branch',
);
const buildStep = getWorkflowStep(
publishJob,
'Build Bundle and Prepare Package',
);
expect(checkoutStep).toContain('persist-credentials: false');
expect(releaseBranchStep).not.toContain('CI_BOT_PAT');
expect(pushReleaseBranchStep).toContain(
"CI_BOT_PAT: '${{ secrets.CI_BOT_PAT }}'",
);
expect(releaseStepScript).toContain('git config core.hooksPath .husky');
expect(releaseStepScript).toContain('export GH_TOKEN="${CI_BOT_PAT}"');
expect(releaseStepScript).toContain('gh auth setup-git');
const exportTokenIdx = releaseStepScript.indexOf(
'export GH_TOKEN="${CI_BOT_PAT}"',
);
const setupGitIdx = releaseStepScript.indexOf(
'gh auth setup-git',
exportTokenIdx,
);
expect(setupGitIdx).toBeGreaterThan(exportTokenIdx);
expect(setupGitIdx).toBeLessThan(
releaseStepScript.indexOf('git push --force --set-upstream'),
);
expect(buildStep).toContain('run-release-step.sh build-package');
});
it('skips npm packages whose release version is already published', () => {
const workflow = readWorkflow('.github/workflows/release.yml');
const publishJob = getWorkflowJob(workflow, 'publish');
const publishStep = getWorkflowStep(publishJob, 'Publish npm packages');
expect(publishStep).toContain(
"RELEASE_VERSION: '${{ needs.prepare.outputs.release_version }}'",
);
expect(releaseStepScript).toContain(
'package_name="$(node -p "require(\'./package.json\').name")"',
);
expect(releaseStepScript).toContain('publish_args+=(--dry-run)');
expect(releaseStepScript).toContain(
'npm view "${package_name}@${RELEASE_VERSION}" version',
);
expect(releaseStepScript).toContain('already published; skipping');
expect(releaseStepScript).toContain('exit 0');
expect(releaseStepScript).toContain(
'corepack pnpm publish --no-git-checks --provenance "${publish_args[@]}"',
);
expect(releaseStepScript).toContain(
'corepack pnpm -r publish "${publish_args[@]}"',
);
});
it('meets npm trusted publishing requirements', () => {
for (const [workflowPath, jobName, publishStepName] of [
['.github/workflows/release.yml', 'publish', 'Publish npm packages'],
[
'.github/workflows/release-sdk.yml',
'release-sdk',
'Publish @qwen-code/sdk',
],
[
'.github/workflows/cd-cua-driver.yml',
'publish-sdk',
'Publish immutable SDK tarball',
],
[
'.github/workflows/cd-cua-driver.yml',
'publish-node-repl',
'Publish immutable Node REPL tarball',
],
['.github/workflows/cd-mobile-mcp.yml', 'build-and-publish', 'Publish'],
]) {
const publishJob = getWorkflowJob(readWorkflow(workflowPath), jobName);
const installStep = getWorkflowStep(publishJob, 'Install npm 11');
const publishStep = getWorkflowStep(publishJob, publishStepName);
const publishImplementation =
workflowPath === '.github/workflows/release.yml'
? releaseStepScript
: publishStep;
expect(installStep).toContain('npm install --global npm@11.19.0');
expect(publishJob).toContain("id-token: 'write'");
expect(publishImplementation).toContain('--provenance');
expect(publishJob).toContain("name: 'production-release'");
expect(publishJob.indexOf(installStep)).toBeLessThan(
publishJob.indexOf(publishStep),
);
}
for (const packageDirectory of [
'integrations/external-context-mem0',
'packages/audio-capture',
'packages/cli',
'packages/channels/base',
'packages/channels/dingtalk',
'packages/channels/dws',
'packages/channels/feishu',
'packages/channels/github',
'packages/channels/qqbot',
'packages/channels/telegram',
'packages/channels/wecom',
'packages/channels/weixin',
'packages/cua-driver/typescript',
'packages/mobile-mcp',
'packages/node-repl',
'packages/sdk-typescript',
'packages/web-shell',
]) {
const packageJson = JSON.parse(
readFileSync(path.join(root, packageDirectory, 'package.json'), 'utf8'),
);
expect(packageJson.repository?.url?.replace(/^git\+/, '')).toBe(
'https://github.com/QwenLM/qwen-code.git',
);
}
});
it('fast-tracks trusted autofix issue triggers before LLM assessment', () => {
const workflow = readWorkflow('.github/workflows/qwen-autofix.yml');
const issueJob = getWorkflowJob(workflow, 'issue-autofix');
const scanStep = getWorkflowStep(issueJob, 'Find candidate issues');
const fastTrackStep = getWorkflowStep(issueJob, 'Fast-track decision');
const assessStep = getWorkflowStep(issueJob, 'Assess candidates');
expect(issueJob.indexOf(scanStep)).toBeLessThan(
issueJob.indexOf(fastTrackStep),
);
expect(issueJob.indexOf(fastTrackStep)).toBeLessThan(
issueJob.indexOf(assessStep),
);
expect(fastTrackStep).toContain("id: 'fasttrack'");
expect(fastTrackStep).toContain('FAST_TRACK=false');
expect(fastTrackStep).toContain('FAST_TRACK=true');
expect(fastTrackStep).toContain('fast_tracked=false');
expect(fastTrackStep).toContain('-n "${FORCED_ISSUE}"');
expect(fastTrackStep).toContain(
'Fast-tracked: trusted trigger bypasses LLM assessment.',
);
expect(assessStep).toContain(
"steps.fasttrack.outputs.fast_tracked != 'true'",
);
});
it('skips autofix install-time prepare without disabling dependency scripts', () => {
const workflow = readWorkflow('.github/workflows/qwen-autofix.yml');
// review-address restores the shared build-cli bundle instead of
// compiling, so its install step is npm ci only; the other two jobs
// still build from sources. Husky hooks are re-armed after the
// prepare-skip only where git commits happen (build-cli never commits).
for (const [jobName, stepName, armsHooks] of [
['issue-autofix', 'Install dependencies and build', true],
['build-cli', 'Install dependencies and build', false],
['review-address', 'Install dependencies', true],
]) {
const job = getWorkflowJob(workflow, jobName);
const installStep = getWorkflowStep(job, stepName);
expect(installStep).toContain("QWEN_SKIP_PREPARE: '1'");
expect(installStep).toContain(
'corepack pnpm install --frozen-lockfile --prefer-offline --reporter=append-only',
);
if (armsHooks) {
expect(installStep).toContain('git config core.hooksPath .husky');
}
expect(installStep).not.toContain('--ignore-scripts');
}
});
it('runs changed autofix tests instead of full touched-package suites', () => {
const workflow = readWorkflow('.github/workflows/qwen-autofix.yml');
const reviewVerificationRunner = readWorkflow(
'.github/scripts/run-autofix-review-verification.sh',
);
const reviewJob = getWorkflowJob(workflow, 'review-address');
for (const verificationBody of [
getWorkflowStep(
getWorkflowJob(workflow, 'issue-autofix'),
'Verification gate',
),
reviewVerificationRunner,
]) {
expect(verificationBody).toContain(
'npm run test --workspace "${p}" --if-present -- --changed origin/main --passWithNoTests',
);
expect(verificationBody).toContain(
'bash "${RUNNER_TEMP}/resolve-owning-packages.sh"',
);
expect(verificationBody).toContain('pkg.scripts?.test');
expect(verificationBody).toContain('!= *vitest*');
expect(verificationBody).not.toContain(
'npm run test --workspace "${p}" --if-present\n',
);
}
expect(getWorkflowStep(reviewJob, 'Verification gate')).toContain(
'bash --norc "${RUNNER_TEMP}/run-autofix-review-verification.sh"',
);
});
});