/** * @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 { checkReleaseTarballExecBits } from '../check-release-tarball-exec-bits.mjs'; import { getPinnedPnpmPackage } from '../pnpm-package.js'; import { INDEPENDENT_PACKAGES } from '../release-packages.mjs'; import { arePermissionBitsEnforced } from '../../packages/cli/src/test-utils/permission-bits.js'; 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 heapCap = (value) => value?.match(/--max-old-space-size=(\d+)/)?.[1]; /** * Writes a throwaway package shaped like the generated CLI for the * vendored-binary packing cases: `vendor/ripgrep//rg` (or `rg.exe` * for a `-win32` platform) at the given modes, the manifest entry that makes * a packer pick the tree up, and optionally * `vendor/landlock-run//qwen-landlock-run` binaries. */ function writeVendoredBinaryFixture(directory, platforms, landlock = {}) { writeFileSync( path.join(directory, 'package.json'), JSON.stringify({ name: 'vendored-binary-fixture', version: '1.0.0', files: ['vendor'], }), ); const binaries = []; for (const [platform, mode] of Object.entries(platforms)) { binaries.push([ `vendor/ripgrep/${platform}/${platform.endsWith('-win32') ? 'rg.exe' : 'rg'}`, mode, ]); } for (const [platform, mode] of Object.entries(landlock)) { binaries.push([`vendor/landlock-run/${platform}/qwen-landlock-run`, mode]); } for (const [relative, mode] of binaries) { const binary = path.join(directory, relative); mkdirSync(path.dirname(binary), { recursive: true }); writeFileSync(binary, '#!/bin/sh\n'); chmodSync(binary, mode); } return directory; } 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('keeps the root build above the heap ceiling that broke the CLI compile', () => { const { build } = readPackageJson().scripts; const cap = heapCap(build); expect(cap).toBeDefined(); // The CLI's TypeScript compile crossed the previous 4 GiB ceiling on the // shared pool and failed the Java daemon E2E leg before any test ran // (#14835). Keep the build-time cap at or above the 6144 MB that fixed it. expect(Number(cap)).toBeGreaterThanOrEqual(6144); }); it('keeps the pnpm prepare build above the heap ceiling and the root build cap', () => { const { nodeOptions } = parse(readWorkflow('pnpm-workspace.yaml')); const pnpmCap = heapCap(nodeOptions); const buildCap = heapCap(readPackageJson().scripts.build); expect(pnpmCap).toBeDefined(); expect(buildCap).toBeDefined(); // The pnpm prepare path compiles the same CLI project that crossed the // 4 GiB ceiling in #14823; #14827 raised this cap, so pin it beside the // root build script instead of leaving one half of the fix unguarded. expect(Number(pnpmCap)).toBeGreaterThanOrEqual(6144); // pnpm replaces NODE_OPTIONS for every package script, so this cap — not // the root build script's — governs `tsc --build` on both the explicit // and install-time paths. A raise on the root entry point intended for // that compile must be mirrored here. expect(Number(pnpmCap)).toBeGreaterThanOrEqual(Number(buildCap)); }); 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( 'npm publish --provenance "${publish_args[@]}"', ); expect(releaseStepScript).toContain( 'corepack pnpm -r publish "${publish_args[@]}"', ); }); it('publishes the generated CLI with a packer that keeps the ripgrep exec bit', () => { // #12668: pnpm's packer writes 0644 for every entry outside `bin`, so a CLI // published with it ships a vendored ripgrep that nobody can spawn. The // generated CLI is the only package carrying vendored binaries and the only // one `publish_package` is called for, so it alone goes back to npm. expect(releaseStepScript).toContain("publish_package 'dist'"); expect(releaseStepScript).not.toContain('corepack pnpm publish'); expect(releaseStepScript).toContain( 'npm publish --provenance "${publish_args[@]}"', ); // The gate runs in the arm that produces `dist`, before the publish step // consumes it, so no release can publish a tarball it never checked. It // runs from the workflow-pinned scripts: a release ref that predates it // would have no `scripts/check-release-tarball-exec-bits.mjs` of its own. const buildPackageArm = releaseStepScript.slice( releaseStepScript.indexOf('build-package)'), releaseStepScript.indexOf('build-archives)'), ); expect(buildPackageArm).toContain( 'node .release-workflow/scripts/check-release-tarball-exec-bits.mjs dist', ); }); it.skipIf(!arePermissionBitsEnforced())( 'npm pack keeps the exec bit on every spawned vendored binary in the CLI tarball', () => { // The archive is where the bit is lost (#12668), so assert on its // recorded modes rather than on the staged tree. A `-win32` directory // ships a 0644 `rg.exe` by design and the gate excludes it structurally. const fixture = mkdtempSync(path.join(tmpdir(), 'cli-rg-exec-bit-')); try { writeVendoredBinaryFixture( fixture, { 'x64-linux': 0o755, 'arm64-darwin': 0o755, 'x64-win32': 0o644, }, { 'x64-linux': 0o755 }, ); const { entries } = checkReleaseTarballExecBits(fixture); expect(entries.map(([name]) => name)).toEqual([ 'package/vendor/landlock-run/x64-linux/qwen-landlock-run', 'package/vendor/ripgrep/arm64-darwin/rg', 'package/vendor/ripgrep/x64-linux/rg', ]); for (const [name, mode] of entries) { expect(mode & 0o111, name).toBe(0o111); } } finally { rmSync(fixture, { force: true, recursive: true }); } }, ); it.skipIf(!arePermissionBitsEnforced())( 'release tarball gate rejects a spawned vendored binary without an exec bit', () => { const fixture = mkdtempSync(path.join(tmpdir(), 'cli-rg-gate-')); try { writeVendoredBinaryFixture( fixture, { 'x64-linux': 0o755 }, { 'x64-linux': 0o644 }, ); expect(() => checkReleaseTarballExecBits(fixture)).toThrow( /package\/vendor\/landlock-run\/x64-linux\/qwen-landlock-run: 0644 lacks an exec bit/, ); // A platform directory whose binary was dropped during staging must // fail, not silently shrink the expected set. const shrunk = mkdtempSync(path.join(tmpdir(), 'cli-rg-missing-')); try { writeVendoredBinaryFixture( shrunk, { 'arm64-linux': 0o755 }, { 'x64-linux': 0o755 }, ); mkdirSync(path.join(shrunk, 'vendor', 'ripgrep', 'x64-linux'), { recursive: true, }); expect(() => checkReleaseTarballExecBits(shrunk)).toThrow( /package\/vendor\/ripgrep\/x64-linux\/rg: missing from the packed tarball/, ); } finally { rmSync(shrunk, { force: true, recursive: true }); } // A package whose vendor trees ship no spawned binary must not pass // vacuously: the gate refuses before it ever packs. const empty = mkdtempSync(path.join(tmpdir(), 'cli-rg-none-')); try { mkdirSync(path.join(empty, 'vendor', 'ripgrep'), { recursive: true, }); mkdirSync(path.join(empty, 'vendor', 'landlock-run'), { recursive: true, }); expect(() => checkReleaseTarballExecBits(empty)).toThrow( /no spawned vendored executables under/, ); } finally { rmSync(empty, { force: true, recursive: true }); } } finally { rmSync(fixture, { force: true, recursive: true }); } }, ); 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-browser', 'Publish immutable Browser SDK tarball', ], [ '.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"', ); }); });