Files
qwen-code/packages/desktop/scripts/test-release.js
T
易良andQwen-Coder 8aea64d5f7 fix(desktop): verify AppImage library closure (#16062)
Read every ELF's DT_NEEDED entries from the extracted AppImage and fail release when a dependency is neither bundled nor part of AppImage's host-provided baseline. Document the contract and cover the failing and repaired cases.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 19:43:23 +08:00

2543 lines
86 KiB
JavaScript

#!/usr/bin/env node
import assert from 'node:assert/strict';
import { execFileSync, spawnSync } from 'node:child_process';
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import vm from 'node:vm';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { resolveLogRoot, sliceNewLog } from './resolve-log-root.js';
import {
verifyBundledRipgrep,
verifyRuntimeIntegrity,
} from './runtime-smoke-checks.js';
import {
findLinuxInstallers,
findRuntimeRoot,
verifyAppImageClosure,
verifyExtractedRuntime,
} from './smoke-linux-installers.js';
const packageDir = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..',
);
const repoRoot = path.resolve(packageDir, '../..');
const manifestScript = path.join(
repoRoot,
'.github',
'scripts',
'create-desktop-update-manifest.mjs',
);
const electronBridgeScript = path.join(
repoRoot,
'.github',
'scripts',
'create-electron-bridge-manifest.mjs',
);
const versionScript = path.join(packageDir, 'scripts', 'version.js');
const tauriConfig = JSON.parse(
fs.readFileSync(
path.join(packageDir, 'src-tauri', 'tauri.conf.json'),
'utf8',
),
);
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'qwen-desktop-release-test-'),
);
try {
const canSpawnShebang = probeShebangExecution(path.join(root, 'exec-probe'));
testBootstrapBridgeConfiguration();
testZoomHotkeyScript();
await testBootstrapWorkspaceVisibility();
testLegacyApplicationIdentity();
testWindowsInstallerStopsBundledRuntime();
testElectronBridgeWorkflow();
testReleaseMatrixCoversUpdaterPlatforms();
testLinuxReleaseLegsShareGlibcFloor();
testDesktopReleaseSigningWorkflow();
testDesktopReleaseHardening();
testImmutableRuntimePatchelf(path.join(root, 'patchelf'), canSpawnShebang);
testLinuxInstallerSmokeDiscovery(path.join(root, 'linux-installers'));
testAppImageDependencyClosure(
path.join(root, 'appimage-closure'),
canSpawnShebang,
);
testRuntimeSmokeChecks(path.join(root, 'runtime-smoke'), canSpawnShebang);
testRuntimeTargetMappings();
testUpdaterMirrorConfiguration();
testResolveLogRoot();
testSliceNewLog();
testUpdateManifest(path.join(root, 'manifest'));
testElectronBridgeManifest(path.join(root, 'electron-bridge'));
testVersionSynchronization(path.join(root, 'version'));
testRuntimePreparation(path.join(root, 'runtime'));
console.log('Desktop release helper checks passed.');
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
function probeShebangExecution(file) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, '#!/usr/bin/env sh\nexit 0\n');
fs.chmodSync(file, 0o755);
const result = spawnSync(file, [], { stdio: 'ignore' });
return !result.error && result.status === 0;
}
async function testBootstrapWorkspaceVisibility() {
const bootstrapHtml = fs.readFileSync(
path.join(packageDir, 'bootstrap', 'index.html'),
'utf8',
);
assert.match(bootstrapHtml, /class="mark" src="qwen-code-logo\.svg"/);
assert.ok(
fs.existsSync(path.join(packageDir, 'bootstrap', 'qwen-code-logo.svg')),
'The bootstrap splash mark must ship with the frontendDist directory.',
);
assert.doesNotMatch(bootstrapHtml, /class="mark">Q</);
const reducedMotionBlock = bootstrapHtml.match(
/@media \(prefers-reduced-motion: reduce\) \{([\s\S]*?)(?:@media|<\/style>)/,
);
assert.ok(
reducedMotionBlock,
'The bootstrap splash must keep a reduced-motion media block.',
);
for (const centeringRule of [
/body\[data-state='starting'\] \.brand \{[^}]*justify-content: center;[^}]*\}/,
/body\[data-state='starting'\] \.status \{[^}]*text-align: center;[^}]*\}/,
]) {
assert.match(
reducedMotionBlock[1],
centeringRule,
'The reduced-motion startup view must keep the logo and status text on the same horizontal center.',
);
}
const runtimeSource = fs.readFileSync(
path.join(packageDir, 'src-tauri', 'src', 'runtime.rs'),
'utf8',
);
assert.match(
runtimeSource,
/let mut child = spawn_runtime_group\(&mut command\)/,
'DesktopRuntime::start must spawn the runtime through the hidden-console helper.',
);
const primary = await createBootstrapHarness();
const { body, commands, element, listeners, resolveBootstrapState } = primary;
listeners['runtime-starting']({
payload: '/Users/example/Projects/qwen-code',
});
assert.equal(body.dataset.state, 'starting');
assert.equal(element('#workspace').hidden, true);
listeners['runtime-failed']({ payload: 'runtime failed' });
assert.equal(body.dataset.state, 'error');
assert.equal(element('#workspace').hidden, false);
assert.equal(
element('#workspace').textContent,
'/Users/example/Projects/qwen-code',
);
await element('#logs').listeners.click();
assert.equal(element('#workspace').hidden, false);
element('#retry').listeners.click();
assert.equal(commands.at(-1), 'restart_runtime');
assert.equal(body.dataset.state, 'starting');
assert.equal(element('#workspace').hidden, true);
resolveBootstrapState({
desktopVersion: '0.2.0',
status: 'starting',
workspace: '/Users/example/Documents',
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(element('#title').textContent, 'Restarting Qwen Code');
assert.equal(
element('#workspace').hidden,
true,
'A stale bootstrap snapshot must not overwrite a newer recovery action.',
);
const failed = await createBootstrapHarness();
failed.listeners['runtime-failed']({ payload: 'runtime failed' });
failed.resolveBootstrapState({
desktopVersion: '0.2.0',
status: 'idle',
workspace: '/Users/example/Documents/Qwen',
error: 'runtime failed',
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(failed.element('#workspace').hidden, false);
assert.equal(
failed.element('#workspace').textContent,
'/Users/example/Documents/Qwen',
);
const cancelled = await createBootstrapHarness();
cancelled.listeners['runtime-starting']({
payload: '/Users/example/Documents/Qwen',
});
cancelled.listeners['runtime-failed']({ payload: 'runtime failed' });
await cancelled.element('#choose').listeners.click();
assert.equal(cancelled.body.dataset.state, 'idle');
assert.equal(cancelled.element('#workspace').hidden, false);
assert.equal(
cancelled.element('#workspace').textContent,
'/Users/example/Documents/Qwen',
);
}
async function createBootstrapHarness() {
const elements = {};
const element = (selector) => {
elements[selector] ??= {
addEventListener(event, listener) {
this.listeners ??= {};
this.listeners[event] = listener;
},
style: {},
};
return elements[selector];
};
const listeners = {};
const commands = [];
const body = { dataset: {} };
let resolveBootstrapState;
const tauri = {
core: {
invoke: async (command) => {
commands.push(command);
if (command === 'bootstrap_state') {
return new Promise((resolve) => {
resolveBootstrapState = resolve;
});
}
if (command === 'open_logs') throw new Error('no file handler');
if (command === 'choose_workspace') return null;
if (command === 'restart_runtime') return new Promise(() => {});
throw new Error(`Unexpected desktop command: ${command}`);
},
},
event: {
listen: async (event, listener) => {
listeners[event] = listener;
},
},
};
vm.runInNewContext(
fs.readFileSync(path.join(packageDir, 'bootstrap', 'bootstrap.js'), 'utf8'),
{
document: { body, querySelector: element },
window: { __TAURI__: tauri },
},
{ timeout: 5000 },
);
await new Promise((resolve) => setImmediate(resolve));
return {
body,
commands,
element,
listeners,
resolveBootstrapState: (state) => resolveBootstrapState(state),
};
}
function testLegacyApplicationIdentity() {
const config = JSON.parse(
fs.readFileSync(
path.join(packageDir, 'src-tauri', 'tauri.conf.json'),
'utf8',
),
);
assert.equal(config.productName, 'Qwen Code Desktop');
assert.equal(config.identifier, 'com.alibaba.qwen-code');
assert.equal(
config.bundle.windows.nsis.installerHooks,
'windows/electron-migration.nsh',
);
const migrationHook = fs.readFileSync(
path.join(packageDir, 'src-tauri', 'windows', 'electron-migration.nsh'),
'utf8',
);
assert.match(migrationHook, /Software\\821b18a9-7c63-5bb4-9e20-51ba63d5ecc3/);
assert.match(migrationHook, /!macro NSIS_HOOK_PREINSTALL/);
assert.match(
migrationHook,
/StrCpy \$R1 \$R1 17\s*\n\s*\$\{If\} \$R0 != ""\s*\n\s*\$\{AndIf\} \$R1 == "Qwen Code Desktop"/,
);
assert.match(
migrationHook,
/\$\{AndIf\} \$\{FileExists\} "\$R0\\Uninstall Qwen Code Desktop\.exe"/,
);
assert.match(
migrationHook,
/ExecWait '"\$R0\\Uninstall Qwen Code Desktop\.exe" \/currentuser \/S --updated _\?=\$R0'/,
);
assert.match(migrationHook, /\$\{If\} \$R2 != 0\s*\n\s*Abort/);
}
function testWindowsInstallerStopsBundledRuntime() {
const hook = fs.readFileSync(
path.join(packageDir, 'src-tauri', 'windows', 'electron-migration.nsh'),
'utf8',
);
const macroBody = (name) => {
const match = hook.match(
new RegExp(`!macro ${name}\\s([\\s\\S]*?)!macroend`),
);
assert.ok(match, `${name} must define the installer step`);
return match[1];
};
// An update driven by a build that predates the app-side runtime stop can
// only be unlocked by the installer, and the pre-install stage is the only
// hook that runs before the copy stage (#15933).
assert.match(
macroBody('NSIS_HOOK_PREINSTALL'),
/!insertmacro StopBundledRuntime/,
);
assert.ok(
hook.indexOf('!macro StopBundledRuntime') <
hook.indexOf('!insertmacro StopBundledRuntime'),
'StopBundledRuntime must be defined before the hook inserts it',
);
const stop = macroBody('StopBundledRuntime');
assert.match(
stop,
/StrCpy \$R0 "\$INSTDIR\\runtime\\qwen-code\\node\\node\.exe"/,
);
// Matching the image path, not the process name, is what keeps a node.exe
// the user runs elsewhere alive; a name-only kill has to redden the gate.
assert.match(stop, /Get-CimInstance Win32_Process/);
assert.match(
stop,
/\$\$_\.ExecutablePath\.StartsWith\(\$\$root,\[StringComparison\]::OrdinalIgnoreCase\)/,
);
assert.match(stop, /Stop-Process -Id \$\$_\.ProcessId -Force/);
assert.doesNotMatch(stop, /KillProcess|taskkill/i);
// Killing is not unlocking: the step has to wait for the image to become
// writable instead of running into the copy while a handle lingers.
assert.match(
stop,
/\[IO\.File\]::Open\(\$\$exe,\[IO\.FileMode\]::Open,\[IO\.FileAccess\]::Write,/,
);
assert.match(stop, /if\(\$\$locked\)\{exit 1\}/);
// Only an explicit zero continues. A lock that survives the wait, or
// nsExec failing to run PowerShell, must abort before the copy rather than
// falling through to the Ignore dialog. The message gives the user an
// actionable recovery path; a silent install must not show a dialog.
assert.match(
stop,
/\$\{If\} \$R1 != "0"\s*\n\s*\$\{If\} \$\{Silent\}\s*\n\s*Abort\s*\n\s*\$\{Else\}\s*\n\s*Abort "/,
);
}
function testElectronBridgeWorkflow() {
const workflow = fs.readFileSync(
path.join(repoRoot, '.github', 'workflows', 'desktop-release.yml'),
'utf8',
);
assert.match(workflow, /^ {6}electron_bridge:$/m);
assert.match(workflow, /create-electron-bridge-manifest\.mjs/);
assert.match(workflow, /macos:latest-mac\.yml/);
assert.match(workflow, /windows:latest\.yml/);
assert.match(workflow, /linux:latest-linux\.yml/);
assert.match(
workflow,
/windows_installers=\(release-assets\/\*-setup\.exe\)/,
);
assert.match(
workflow,
/linux_appimages=\(release-assets\/\*_amd64\.AppImage\)/,
);
assert.match(workflow, /^\s+release-assets\/latest\.yml$/m);
assert.match(workflow, /^\s+release-assets\/latest-linux\.yml$/m);
assert.match(workflow, /^\s+"\$\{windows_installers\[0\]\}"$/m);
assert.match(workflow, /^\s+"\$\{linux_appimages\[0\]\}"$/m);
assert.match(
workflow,
/if \[ "\$ELECTRON_BRIDGE" = 'true' \]; then\s+echo "::error::Electron bridge \$RELEASE_VERSION cannot replace newer stable feed \$current\."\s+exit 1/,
);
for (const artifact of [
'Qwen-Code-Desktop-arm64.zip',
'Qwen-Code-Desktop-x64.zip',
'Qwen-Code-Desktop-arm64.dmg',
'Qwen-Code-Desktop-x64.dmg',
]) {
assert.match(workflow, new RegExp(artifact.replaceAll('.', '\\.')));
}
}
function testReleaseMatrixCoversUpdaterPlatforms() {
const workflow = fs.readFileSync(
path.join(repoRoot, '.github', 'workflows', 'desktop-release.yml'),
'utf8',
);
const manifestSource = fs.readFileSync(manifestScript, 'utf8');
// The matrix speaks in rust targets and the updater feed in Tauri's
// `${os}-${arch}` keys, so this table is the only thing tying them together.
// A leg nobody taught the manifest about still ships its artifact while the
// feed omits it, and nothing downstream fails loudly: that is how an arm64
// Linux installer ends up pulling the x86_64 AppImage (#12806).
const updaterPlatformByRustTarget = {
'aarch64-apple-darwin': 'darwin-aarch64',
'x86_64-apple-darwin': 'darwin-x86_64',
'x86_64-pc-windows-msvc': 'windows-x86_64',
'x86_64-unknown-linux-gnu': 'linux-x86_64',
'aarch64-unknown-linux-gnu': 'linux-aarch64',
};
const built = new Set();
for (const [, target] of workflow.matchAll(
/^ +rust_target: '([^']+)'\s*$/gm,
)) {
const platform = updaterPlatformByRustTarget[target];
assert.ok(
platform,
`build matrix target ${target} has no updater platform mapping`,
);
built.add(platform);
}
assert.ok(built.size > 0, 'the build matrix must declare rust targets');
// Keyed on the `[platform, selectArtifact(` entry shape, so commenting out
// one of the multi-line entries stops it counting as published.
const published = new Set();
for (const [, platform] of manifestSource.matchAll(
/\[\s*'((?:darwin|linux|windows)-(?:x86_64|aarch64))'\s*,/g,
)) {
published.add(platform);
}
assert.deepEqual(
[...built].sort(),
[...published].sort(),
'every build matrix leg needs an updater feed entry, and every feed entry ' +
'needs a leg that produces it',
);
}
function testLinuxReleaseLegsShareGlibcFloor() {
const workflow = fs.readFileSync(
path.join(repoRoot, '.github', 'workflows', 'desktop-release.yml'),
'utf8',
);
// The AppImage and .deb link against the glibc of the runner image that
// builds them, and the updater feed publishes whatever the matrix built.
// Both Linux legs therefore have to stay on the same Ubuntu generation:
// 22.04 gives glibc 2.35, which Ubuntu 22.04 and Debian 12 users can run,
// while 24.04 gives 2.39, which they cannot. A runner bump on one leg
// (`ubuntu-24.04-arm`, or an `ubuntu-latest` sweep) keeps every other check
// here green because the rust target does not change, and the artifact
// only refuses to start on the user's machine (#12877). The image floor is
// pinned here so that raising it is a deliberate edit of both legs and of
// this line, never a side effect of a runner modernisation.
const linuxImageFloor = 'ubuntu-22.04';
// `os:` only names the image when the job runs on it directly. The build
// job binds `runs-on` to the matrix field and runs no `container:`, and
// both facts are what make the pin below a statement about the glibc the
// artifacts link against rather than about a label nothing reads.
assert.match(
workflow,
/^ {4}runs-on: '\$\{\{ matrix\.os \}\}'$/m,
'the build job must run directly on the matrix `os:` image',
);
assert.doesNotMatch(
workflow,
/^ +container:/m,
'desktop-release.yml must not build inside a container: the glibc floor ' +
'is pinned to the runner image, so a container needs its own witness',
);
const legs = [];
for (const [, name, body] of workflow.matchAll(
/^ {10}- name: '([^']+)'[ \t]*\n((?: {12}\w+: '[^']*'[ \t]*\n)+)/gm,
)) {
const field = (key) =>
body.match(new RegExp(`^ {12}${key}: '([^']*)'[ \\t]*$`, 'm'))?.[1];
legs.push({ name, os: field('os'), rustTarget: field('rust_target') });
}
// The parity check above keys on `rust_target:` lines alone; this walk keys
// on whole matrix entries, so a shape drift that hides an `os:` from the
// entry regex has to surface as a count mismatch rather than as silence.
const targetLines = workflow.match(/^ +rust_target: '[^']+'\s*$/gm) ?? [];
assert.equal(
legs.length,
targetLines.length,
'every build matrix entry must be readable as a `- name:` block with its ' +
'own `os:` and `rust_target:` fields',
);
for (const leg of legs) {
assert.ok(
leg.os && leg.rustTarget,
`build matrix entry ${leg.name} must carry both \`os:\` and ` +
'`rust_target:` as adjacent quoted fields',
);
}
const linuxLegs = legs.filter((leg) =>
leg.rustTarget?.endsWith('-unknown-linux-gnu'),
);
assert.ok(
linuxLegs.length >= 2,
'the build matrix must declare both Linux legs',
);
for (const leg of linuxLegs) {
assert.ok(
leg.os === linuxImageFloor || leg.os === `${linuxImageFloor}-arm`,
`${leg.name} (${leg.rustTarget}) builds on '${leg.os}', but the Linux ` +
`artifacts must link against the glibc of ${linuxImageFloor}; raise ` +
'the floor for both Linux legs together, or keep this leg on the ' +
'pinned image',
);
}
}
function testDesktopReleaseSigningWorkflow() {
const workflow = fs.readFileSync(
path.join(repoRoot, '.github', 'workflows', 'desktop-release.yml'),
'utf8',
);
const primaryIncomplete =
'$primaryIncomplete = ([bool]$env:WINDOWS_CERTIFICATE) -ne ' +
'([bool]$env:WINDOWS_CERTIFICATE_PASSWORD)';
const legacyIncomplete =
'$legacyIncomplete = ([bool]$env:LEGACY_WIN_CSC_LINK) -ne ' +
'([bool]$env:LEGACY_WIN_CSC_KEY_PASSWORD)';
assert.ok(
workflow.includes(primaryIncomplete),
'Windows signing must fail closed when the primary certificate pair is incomplete',
);
assert.ok(
workflow.includes(legacyIncomplete),
'Windows signing must fail closed when the legacy certificate pair is incomplete',
);
assert.ok(
workflow.includes(
'elif [ "$RUNNER_OS" = \'Windows\' ] && [ -n "$WINDOWS_CONFIG" ]; then',
),
'Windows builds must only pass a Tauri config when signing config exists',
);
assert.ok(
workflow.includes(
"$signature.Status -eq 'NotSigned' -and -not $env:WINDOWS_CONFIG",
),
'Unsigned Windows installers are only allowed when no signing config exists',
);
// The step used to name the binaries it signed. It now discovers them,
// because the runtime is a copy of the CLI's dist tree and gains native
// payload without this package changing — an unsigned renderer library
// reached the notary service that way. These assertions pin the discovery
// and the properties the old list guaranteed by construction.
assert.match(
workflow,
/if \[ "\$\(file -b --mime-type "\$file"\)" = 'application\/x-mach-binary' \]; then/,
'the vendor signing step must discover Mach-O binaries rather than list them',
);
assert.match(
workflow,
/done < <\(find "\$runtime_dir" -type f -print0\)/,
'Mach-O discovery must cover the whole staged runtime',
);
assert.ok(
workflow.includes(
'--entitlements src-tauri/NodeEntitlements.plist "$file"',
),
'Node.js must use its minimal helper entitlements',
);
const entitlementFlags = workflow
.slice(
workflow.indexOf("name: 'Sign bundled vendor binaries (macOS)'"),
workflow.indexOf(
"name: 'Refresh bundled runtime checksums after signing",
),
)
.match(/--entitlements/g);
assert.deepStrictEqual(
entitlementFlags,
['--entitlements'],
'only the Node.js branch may pass entitlements; everything else signs without them',
);
assert.match(
workflow,
/codesign --verify --strict "\$file"/,
'the signing step must verify what it signed instead of leaving it to the notary service',
);
const nodeEntitlements = fs.readFileSync(
path.join(packageDir, 'src-tauri', 'NodeEntitlements.plist'),
'utf8',
);
const appEntitlements = fs.readFileSync(
path.join(packageDir, 'src-tauri', 'Entitlements.plist'),
'utf8',
);
assert.match(
appEntitlements,
/<key>com\.apple\.security\.device\.audio-input<\/key>\s*<true\/>/,
'the app bundle must keep microphone access for voice dictation',
);
const infoPlist = fs.readFileSync(
path.join(packageDir, 'src-tauri', 'Info.plist'),
'utf8',
);
assert.match(
infoPlist,
/NSMicrophoneUsageDescription<\/key>\s*<string>.+<\/string>/,
'the app bundle must declare a non-empty microphone usage description',
);
assert.match(
nodeEntitlements,
/<key>com\.apple\.security\.cs\.allow-jit<\/key>\s*<true\/>/,
'the bundled Node.js runtime must keep its JIT entitlement',
);
assert.doesNotMatch(
nodeEntitlements,
/com\.apple\.security\.device\.audio-input/,
'Node.js must not receive microphone access',
);
assert.match(
workflow,
/No Mach-O binary found under \$runtime_dir/,
'a runtime with no native binary must fail rather than silently sign nothing',
);
assert.match(
workflow,
/Node\.js runtime binary not found at \$node_bin/,
'missing Node.js runtime binary must be visible in release logs',
);
assert.match(
workflow,
/Print :com\.apple\.security\.device\.audio-input/,
'the macOS signature check must keep verifying the audio-input entitlement',
);
assert.match(
workflow,
/Print :NSMicrophoneUsageDescription/,
'the packaged smoke must keep verifying the microphone usage description',
);
assert.ok(
workflow.indexOf("name: 'Prepare bundled runtime'") <
workflow.indexOf("name: 'Sign bundled vendor binaries (macOS)'"),
'vendor binaries must be signed after the runtime is prepared',
);
assert.ok(
workflow.indexOf("name: 'Sign bundled vendor binaries (macOS)'") <
workflow.indexOf("name: 'Build desktop installers'"),
'vendor binaries must be signed before Tauri builds installers',
);
}
function testDesktopReleaseHardening() {
const workflow = fs.readFileSync(
path.join(repoRoot, '.github', 'workflows', 'desktop-release.yml'),
'utf8',
);
assert.match(
workflow,
/IS_PRERELEASE" = 'true' \] && \[\[ ! "\$version" =~ \^\[0-9\]\+\\\.\[0-9\]\+\\\.\[0-9\]\+-/,
'prerelease builds must not reuse a stable Desktop version',
);
assert.match(
workflow,
/\*-setup\.exe\|\*-setup\.exe\.sig/,
'Windows release collection must allow only installer executables',
);
assert.doesNotMatch(
workflow.slice(
workflow.indexOf('elif [ "$RUNNER_OS" = \'Windows\' ]'),
workflow.indexOf('elif [ "$RUNNER_OS" = \'Linux\' ]'),
),
/\*\.exe\)/,
'Windows release collection must not include embedded executables',
);
assert.match(
workflow,
/\*\.AppImage\|\*\.AppImage\.sig\|\*\.deb\|\*\.deb\.sig/,
'Linux release collection must allow only installers and updater signatures',
);
const prepareRuntime = fs.readFileSync(
path.join(packageDir, 'scripts', 'prepare-runtime.js'),
'utf8',
);
assert.match(
prepareRuntime,
/QWEN_DESKTOP_NODE_CACHE_DIR/,
'runtime preparation must cache verified Node.js archives',
);
assert.match(
workflow,
/desktop-node-v2-\$\{\{ matrix\.rust_target \}\}-\$\{\{ env\.NODE_VERSION \}\}-\$\{\{ inputs\.dry_run \}\}/,
'release builds must persist the bundled Node.js archive cache',
);
assert.match(workflow, /actions\/cache\/restore@/);
assert.match(workflow, /actions\/cache\/save@/);
assert.equal(
(
workflow.match(
/path: '\$\{\{ steps\.node-cache-path\.outputs\.path \}\}'/g,
) ?? []
).length,
2,
'cache restore and save must share the configured cache path',
);
assert.ok(
prepareRuntime.indexOf('replaceRuntime();') >
prepareRuntime.indexOf('writeChecksums();'),
'runtime replacement must happen only after assembly and checksums finish',
);
assert.match(
workflow,
/^ {12}PATCHELF="\$GITHUB_WORKSPACE\/packages\/desktop\/scripts\/patchelf-immutable-runtime\.sh"$/m,
'Linux installer builds must protect the checksummed runtime from post-link rewrites',
);
assert.match(
workflow,
/^ {12}QWEN_DESKTOP_PATCHELF="\$\(command -v patchelf\)"$/m,
'the patchelf wrapper must be given the real patchelf to delegate to',
);
assert.match(
workflow,
/^ {12}export QWEN_DESKTOP_PATCHELF PATCHELF$/m,
'both patchelf variables must be exported to the Tauri build',
);
const smokeRuntime = fs.readFileSync(
path.join(packageDir, 'scripts', 'smoke-runtime.js'),
'utf8',
);
const integrityCheck = smokeRuntime.indexOf(
'const manifest = verifyRuntimeIntegrity(runtimeRoot);',
);
const ripgrepCheck = smokeRuntime.indexOf(
'verifyBundledRipgrep(runtimeRoot, manifest.target);',
);
assert.ok(
integrityCheck !== -1 && integrityCheck < ripgrepCheck,
'the pre-bundle gate must probe ripgrep after verifying the runtime',
);
const buildIndex = workflow.indexOf("name: 'Build desktop installers'");
const installerSmokeIndex = workflow.indexOf(
"name: 'Verify Linux installer runtimes'",
);
const collectIndex = workflow.indexOf("name: 'Collect artifacts'");
assert.ok(
buildIndex < installerSmokeIndex && installerSmokeIndex < collectIndex,
'final Linux installer runtimes must be verified before artifacts are collected',
);
}
function testImmutableRuntimePatchelf(directory, canSpawnShebang) {
if (!canSpawnShebang) {
console.log('SKIP patchelf wrapper: shebang execution unavailable');
return;
}
const wrapper = path.join(
packageDir,
'scripts',
'patchelf-immutable-runtime.sh',
);
const fakePatchelf = path.join(directory, 'patchelf');
const log = path.join(directory, 'patchelf.log');
const runtimeElf = path.join(
directory,
'AppDir',
'usr',
'lib',
'Qwen Code Desktop',
'runtime',
'qwen-code',
'lib',
'vendor',
'ripgrep',
'x64-linux',
'rg',
);
const appElf = path.join(directory, 'AppDir', 'usr', 'bin', 'desktop');
fs.mkdirSync(path.dirname(runtimeElf), { recursive: true });
fs.mkdirSync(path.dirname(appElf), { recursive: true });
fs.writeFileSync(runtimeElf, 'runtime');
fs.writeFileSync(appElf, 'app');
fs.writeFileSync(log, '');
fs.writeFileSync(
fakePatchelf,
[
'#!/usr/bin/env sh',
'{',
' printf \'argc=%s\' "$#"',
' for arg in "$@"; do printf \'[%s]\' "$arg"; done',
" printf '\\n'",
'} >> "$QWEN_TEST_PATCHELF_LOG"',
'printf \'%s\\n\' "$QWEN_TEST_PATCHELF_STDOUT"',
'exit "${QWEN_TEST_PATCHELF_STATUS:-0}"',
'',
].join('\n'),
);
fs.chmodSync(fakePatchelf, 0o755);
const env = {
...process.env,
QWEN_DESKTOP_PATCHELF: fakePatchelf,
QWEN_TEST_PATCHELF_LOG: log,
QWEN_TEST_PATCHELF_STDOUT: '$ORIGIN/../lib',
};
const skipped = spawnSync(wrapper, ['--set-rpath', '$ORIGIN', runtimeElf], {
encoding: 'utf8',
env,
});
assert.equal(skipped.status, 0, skipped.stderr);
assert.equal(
skipped.stderr,
`patchelf-immutable-runtime: skipped --set-rpath on ${runtimeElf}\n`,
);
assert.equal(fs.readFileSync(log, 'utf8'), '');
const readOnly = spawnSync(wrapper, ['--print-rpath', runtimeElf], {
encoding: 'utf8',
env,
});
assert.equal(readOnly.status, 0, readOnly.stderr);
assert.equal(readOnly.stdout, '$ORIGIN/../lib\n');
const delegated = spawnSync(wrapper, ['--set-rpath', '$ORIGIN', appElf], {
encoding: 'utf8',
env,
});
assert.equal(delegated.status, 0, delegated.stderr);
assert.deepEqual(fs.readFileSync(log, 'utf8').trim().split('\n'), [
`argc=2[--print-rpath][${runtimeElf}]`,
`argc=3[--set-rpath][$ORIGIN][${appElf}]`,
]);
const failed = spawnSync(wrapper, ['--print-rpath', appElf], {
encoding: 'utf8',
env: { ...env, QWEN_TEST_PATCHELF_STATUS: '3' },
});
assert.equal(failed.status, 3);
const missing = spawnSync(
wrapper,
['--set-rpath', '$ORIGIN', path.join(runtimeElf, 'missing')],
{ encoding: 'utf8', env },
);
assert.notEqual(missing.status, 0);
assert.match(missing.stderr, /Runtime ELF is missing/);
}
function testLinuxInstallerSmokeDiscovery(directory) {
const bundleRoot = path.join(directory, 'bundle');
const appImage = path.join(bundleRoot, 'appimage', 'Desktop.AppImage');
const deb = path.join(bundleRoot, 'deb', 'desktop.deb');
fs.mkdirSync(path.dirname(appImage), { recursive: true });
fs.mkdirSync(path.dirname(deb), { recursive: true });
fs.writeFileSync(appImage, 'appimage');
fs.writeFileSync(deb, 'deb');
assert.deepEqual(findLinuxInstallers(bundleRoot), { appImage, deb });
const extractedRoot = path.join(directory, 'extracted');
const runtimeRoot = path.join(
extractedRoot,
'usr',
'lib',
'Qwen Code Desktop',
'runtime',
'qwen-code',
);
fs.mkdirSync(runtimeRoot, { recursive: true });
fs.writeFileSync(path.join(runtimeRoot, 'manifest.json'), '{}');
assert.equal(findRuntimeRoot(extractedRoot), runtimeRoot);
fs.writeFileSync(path.join(bundleRoot, 'duplicate.AppImage'), 'duplicate');
assert.throws(
() => findLinuxInstallers(bundleRoot),
/Expected one AppImage, found 2/,
);
}
function testAppImageDependencyClosure(directory, canSpawnShebang) {
if (!canSpawnShebang) {
console.log('SKIP AppImage dependency closure: shebang unavailable');
return;
}
const extractedRoot = path.join(directory, 'extracted');
const binaryDirectory = path.join(directory, 'bin');
const fakeReadelf = path.join(binaryDirectory, 'readelf');
fs.mkdirSync(path.join(extractedRoot, 'usr', 'lib'), { recursive: true });
fs.mkdirSync(binaryDirectory, { recursive: true });
const writeElf = (relative) => {
const file = path.join(extractedRoot, relative);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.rmSync(file, { force: true });
fs.writeFileSync(file, Buffer.from([0x7f, 0x45, 0x4c, 0x46]));
return file;
};
const executable = writeElf('usr/bin/qwen-code-desktop');
const bundledLibrary = writeElf('usr/lib/libbundled.so');
fs.writeFileSync(path.join(extractedRoot, 'README'), 'not an ELF');
fs.writeFileSync(
fakeReadelf,
[
'#!/usr/bin/env sh',
'case "$2" in',
' *qwen-code-desktop)',
" printf ' (NEEDED) Shared library: [libbundled.so]\\\\n'",
" printf ' (NEEDED) Shared library: [libc.so.6]\\\\n'",
' ;;',
' *libbundled.so)',
" printf ' (NEEDED) Shared library: [libmystery.so.1]\\\\n'",
' ;;',
'esac',
'',
].join('\n'),
);
fs.chmodSync(fakeReadelf, 0o755);
const previousPath = process.env.PATH;
process.env.PATH = `${binaryDirectory}:${previousPath}`;
try {
assert.equal(fs.existsSync(executable), true);
assert.equal(fs.existsSync(bundledLibrary), true);
assert.throws(
() => verifyAppImageClosure(extractedRoot),
/AppImage dependency closure is incomplete: libmystery\.so\.1/,
);
fs.writeFileSync(
path.join(extractedRoot, 'usr', 'lib', 'libmystery.so.1'),
'',
);
verifyAppImageClosure(extractedRoot);
} finally {
process.env.PATH = previousPath;
}
}
function testRuntimeSmokeChecks(directory, canSpawnShebang) {
const runtimeRoot = path.join(directory, 'runtime', 'qwen-code');
const manifest = {
name: '@qwen-code/qwen-code',
desktopVersion: '0.0.0-test',
qwenCodeVersion: '0.0.0-test',
qwenCodeCommit: 'test-commit',
target: 'linux-x64',
node: process.version,
builtAt: '2026-10-08T00:00:00.000Z',
};
const files = new Map([
['manifest.json', `${JSON.stringify(manifest)}\n`],
['LICENSE', 'license\n'],
['NOTICE', 'notice\n'],
['node/LICENSE', 'node license\n'],
['node/bin/node', "#!/usr/bin/env sh\nprintf 'v22.0.0\\n'\n"],
['lib/cli-entry.js', ''],
['lib/web-shell/index.html', '<div id="root"></div>\n'],
['lib/generated-ui/main.js', '// bundled Generated UI\n'],
['lib/generated-ui/app.html', '<html></html>\n'],
['lib/generated-ui/skills/generated-ui/SKILL.md', '# Generated UI\n'],
['lib/generated-ui/THIRD_PARTY_NOTICES.txt', 'third-party licenses\n'],
[
'lib/vendor/ripgrep/x64-linux/rg',
"#!/usr/bin/env sh\nprintf 'ripgrep 15.0.0\\n'\n",
],
]);
for (const [relative, contents] of files) {
const file = path.join(runtimeRoot, relative);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, contents);
}
const ripgrep = path.join(
runtimeRoot,
'lib',
'vendor',
'ripgrep',
'x64-linux',
'rg',
);
const node = path.join(runtimeRoot, 'node', 'bin', 'node');
fs.chmodSync(node, 0o755);
fs.chmodSync(ripgrep, 0o755);
const checksums = Object.fromEntries(
[...files.keys()].map((relative) => [
relative,
crypto
.createHash('sha256')
.update(fs.readFileSync(path.join(runtimeRoot, relative)))
.digest('hex'),
]),
);
fs.writeFileSync(
path.join(runtimeRoot, 'checksums.json'),
`${JSON.stringify(checksums)}\n`,
);
assert.deepEqual(verifyRuntimeIntegrity(runtimeRoot), manifest);
const checksumsPath = path.join(runtimeRoot, 'checksums.json');
const webShell = path.join(runtimeRoot, 'lib', 'web-shell', 'index.html');
const hiddenWebShell = `${webShell}.missing`;
const checksumsWithoutWebShell = { ...checksums };
delete checksumsWithoutWebShell['lib/web-shell/index.html'];
fs.writeFileSync(
checksumsPath,
`${JSON.stringify(checksumsWithoutWebShell)}\n`,
);
fs.renameSync(webShell, hiddenWebShell);
assert.throws(
() => verifyRuntimeIntegrity(runtimeRoot),
/Bundled runtime file is missing: lib\/web-shell\/index\.html/,
);
fs.renameSync(hiddenWebShell, webShell);
fs.writeFileSync(checksumsPath, `${JSON.stringify(checksums)}\n`);
const generatedUiAssets = [
'lib/generated-ui/main.js',
'lib/generated-ui/app.html',
'lib/generated-ui/skills/generated-ui/SKILL.md',
'lib/generated-ui/THIRD_PARTY_NOTICES.txt',
];
for (const relative of generatedUiAssets) {
const asset = path.join(runtimeRoot, relative);
fs.renameSync(asset, `${asset}.missing`);
assert.throws(
() => verifyRuntimeIntegrity(runtimeRoot),
/Bundled runtime file is missing: lib\/generated-ui\//,
);
fs.renameSync(`${asset}.missing`, asset);
const incompleteChecksums = { ...checksums };
delete incompleteChecksums[relative];
fs.writeFileSync(checksumsPath, `${JSON.stringify(incompleteChecksums)}\n`);
assert.throws(
() => verifyRuntimeIntegrity(runtimeRoot),
/Bundled runtime checksum is missing: lib\/generated-ui\//,
);
fs.writeFileSync(checksumsPath, `${JSON.stringify(checksums)}\n`);
}
const hiddenNode = `${node}.missing`;
fs.renameSync(node, hiddenNode);
assert.throws(
() => verifyRuntimeIntegrity(runtimeRoot),
/Checksummed runtime file is missing: node\/bin\/node/,
);
fs.renameSync(hiddenNode, node);
const incompleteManifest = { ...manifest };
delete incompleteManifest.desktopVersion;
fs.writeFileSync(
path.join(runtimeRoot, 'manifest.json'),
`${JSON.stringify(incompleteManifest)}\n`,
);
assert.throws(
() => verifyRuntimeIntegrity(runtimeRoot),
/Runtime manifest is missing desktopVersion/,
);
fs.writeFileSync(
path.join(runtimeRoot, 'manifest.json'),
files.get('manifest.json'),
);
fs.writeFileSync(path.join(runtimeRoot, 'LICENSE'), 'mutated license\n');
fs.writeFileSync(path.join(runtimeRoot, 'NOTICE'), 'mutated notice\n');
assert.throws(
() => verifyRuntimeIntegrity(runtimeRoot),
/Bundled runtime checksum mismatch: LICENSE, NOTICE/,
);
fs.writeFileSync(path.join(runtimeRoot, 'LICENSE'), files.get('LICENSE'));
fs.writeFileSync(path.join(runtimeRoot, 'NOTICE'), files.get('NOTICE'));
const ripgrepHidden = `${ripgrep}.missing`;
fs.renameSync(ripgrep, ripgrepHidden);
assert.throws(
() => verifyBundledRipgrep(runtimeRoot, manifest.target),
/Bundled ripgrep is missing for linux-x64/,
);
fs.renameSync(ripgrepHidden, ripgrep);
assert.throws(
() => verifyBundledRipgrep(runtimeRoot, 'linux-x64-musl'),
/Unsupported desktop target/,
);
if (!canSpawnShebang) {
console.log('SKIP runtime executables: shebang execution unavailable');
return;
}
verifyExtractedRuntime(directory);
fs.chmodSync(node, 0o644);
assert.throws(
() => verifyExtractedRuntime(directory),
/Bundled runtime node is not runnable/,
);
fs.chmodSync(node, 0o755);
fs.writeFileSync(ripgrep, "#!/usr/bin/env sh\nprintf 'unexpected\\n'\n");
assert.throws(
() => verifyBundledRipgrep(runtimeRoot, manifest.target),
/Bundled ripgrep failed its version probe/,
);
}
function testRuntimeTargetMappings() {
const prepareRuntime = fs.readFileSync(
path.join(packageDir, 'scripts', 'prepare-runtime.js'),
'utf8',
);
const literal =
/const NODE_PTY_PREBUILD_PACKAGE = new Map\(\[([\s\S]*?)\]\);/.exec(
prepareRuntime,
);
assert.ok(
literal,
'runtime preparation must map desktop targets to node-pty prebuild packages',
);
const mapping = vm.runInNewContext(`new Map([${literal[1]}])`);
const allowList = /!\[\s*([\s\S]*?)\]\.includes\(resolved\)/.exec(
prepareRuntime,
);
assert.ok(allowList, 'desktopTarget() must keep validating its targets');
const supported = vm.runInNewContext(`[${allowList[1]}]`);
assert.deepEqual(
[...mapping.keys()].sort(),
[...supported].sort(),
'every target desktopTarget() accepts needs a node-pty prebuild package',
);
for (const target of supported) {
// The wrapper requires `@lydell/node-pty-${process.platform}-${process.arch}`
// at runtime, and the standalone packager keys Windows as 'win-x64': reusing
// that map here would stage '@lydell/node-pty-undefined' for 'win32-x64'.
assert.equal(
mapping.get(target),
`@lydell/node-pty-${target}`,
`${target} must stage the prebuild package its own wrapper requires`,
);
}
const assetFilter =
/const opentuiScope =[\s\S]*?(?=\n {2}stageNodePty\(target\);)/.exec(
prepareRuntime,
)?.[0];
assert.ok(
assetFilter,
'the final copied runtime must filter OpenTUI native targets',
);
for (const target of supported) {
const libDir = path.join(root, 'opentui-targets', target);
const scope = path.join(libDir, 'opentui-assets', '@opentui');
for (const candidate of supported) {
const directory = path.join(scope, `core-${candidate}`);
fs.mkdirSync(directory, { recursive: true });
const library = candidate.startsWith('darwin')
? 'libopentui.dylib'
: candidate.startsWith('win32')
? 'opentui.dll'
: 'libopentui.so';
fs.writeFileSync(path.join(directory, library), candidate);
}
fs.mkdirSync(path.join(scope, 'core'), { recursive: true });
fs.writeFileSync(path.join(scope, 'core', 'parser.worker.js'), 'common');
// Run the actual preparation block against disk for every release target;
// the full cross-build preparation fixture below verifies its call site.
vm.runInNewContext(assetFilter, { fs, path, libDir, target });
assert.deepEqual(fs.readdirSync(scope).sort(), ['core', `core-${target}`]);
assert.equal(
fs.readFileSync(path.join(scope, 'core', 'parser.worker.js'), 'utf8'),
'common',
);
assert.equal(fs.readdirSync(path.join(scope, `core-${target}`)).length, 1);
}
const runtimeSmokeChecks = fs.readFileSync(
path.join(packageDir, 'scripts', 'runtime-smoke-checks.js'),
'utf8',
);
const ripgrepLiteral =
/const ripgrepByTarget = new Map\(\[([\s\S]*?)\]\);/.exec(
runtimeSmokeChecks,
);
assert.ok(
ripgrepLiteral,
'runtime smoke checks must map desktop targets to bundled ripgrep binaries',
);
const ripgrepMapping = vm.runInNewContext(`new Map([${ripgrepLiteral[1]}])`);
assert.deepEqual(
[...ripgrepMapping.keys()].sort(),
[...supported].sort(),
'every target desktopTarget() accepts needs a bundled ripgrep binary',
);
for (const target of supported) {
const targetPath = ripgrepMapping.get(target);
assert.ok(
fs
.statSync(
path.join(
repoRoot,
'packages',
'core',
'vendor',
'ripgrep',
...targetPath,
),
{ throwIfNoEntry: false },
)
?.isFile(),
`${target} must point to a bundled ripgrep binary`,
);
}
// Cross-built prebuilds must stay exact so staging fetches the same version
// that the frozen root install verified (#11872).
const rootPackage = JSON.parse(
fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'),
);
assert.match(
rootPackage.optionalDependencies?.['@lydell/node-pty-darwin-x64'],
/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/,
'the darwin-x64 prebuild must stay pinned so a cross-building leg can fetch its locked version',
);
}
function testRuntimePreparation(directory) {
const testPackageDir = path.join(directory, 'packages', 'desktop');
const testScript = path.join(testPackageDir, 'scripts', 'prepare-runtime.js');
const sourceRoot = path.join(directory, 'source');
const runtimeDir = path.join(testPackageDir, 'runtime');
const cacheRoot = path.join(directory, 'cache');
const nodeVersion = process.versions.node;
// darwin-x64 on purpose: this test runs on a linux-x64 CI host, so the
// target's prebuild can never come from a host node_modules — the same shape
// as the release matrix's x86_64-apple-darwin leg, which cross-builds on an
// arm64 macos-15 runner (#11872).
const archiveName = `node-v${nodeVersion}-darwin-x64.tar.gz`;
const cacheDir = path.join(cacheRoot, `v${nodeVersion}`);
const cachedArchivePath = path.join(cacheDir, archiveName);
const archivePath = path.join(directory, archiveName);
const checksumsPath = path.join(directory, 'SHASUMS256.txt');
const fetchLog = path.join(directory, 'fetch.log');
const fetchMock = path.join(directory, 'mock-fetch.mjs');
const npmLog = path.join(directory, 'npm.log');
const npmStub = path.join(directory, 'mock-npm.mjs');
const extractedRoot = path.join(directory, `node-v${nodeVersion}-darwin-x64`);
fs.mkdirSync(path.join(sourceRoot, 'dist', 'web-shell', 'assets'), {
recursive: true,
});
// The exact pins staging resolves its package specs from.
const nodePtyPins = {
'@lydell/node-pty': '0.0.0-test',
'@lydell/node-pty-darwin-x64': '0.0.0-test',
};
fs.writeFileSync(
path.join(sourceRoot, 'package.json'),
JSON.stringify({
version: '0.0.0-test',
optionalDependencies: nodePtyPins,
}),
);
fs.mkdirSync(path.dirname(testScript), { recursive: true });
fs.copyFileSync(
path.join(packageDir, 'scripts', 'prepare-runtime.js'),
testScript,
);
fs.writeFileSync(
path.join(directory, '.nvmrc'),
`${process.versions.node.split('.')[0]}\n`,
);
fs.writeFileSync(
path.join(testPackageDir, 'package.json'),
JSON.stringify({ version: '0.0.0-test' }),
);
fs.writeFileSync(path.join(testPackageDir, 'NOTICE'), 'test notice');
fs.writeFileSync(path.join(sourceRoot, 'LICENSE'), 'test license');
for (const file of [
'cli.js',
'cli-entry.js',
path.join('web-shell', 'index.html'),
path.join('web-shell', 'assets', 'app.js'),
path.join('generated-ui', 'main.js'),
path.join('generated-ui', 'app.html'),
path.join('generated-ui', 'skills', 'generated-ui', 'SKILL.md'),
path.join('generated-ui', 'THIRD_PARTY_NOTICES.txt'),
]) {
fs.mkdirSync(path.dirname(path.join(sourceRoot, 'dist', file)), {
recursive: true,
});
fs.writeFileSync(path.join(sourceRoot, 'dist', file), 'test');
}
const sourceRipgrep = path.join(
sourceRoot,
'dist',
'vendor',
'ripgrep',
'x64-darwin',
'rg',
);
fs.mkdirSync(path.dirname(sourceRipgrep), { recursive: true });
fs.writeFileSync(sourceRipgrep, 'test ripgrep');
const sourceOpenTui = path.join(
sourceRoot,
'dist',
'opentui-assets',
'@opentui',
);
for (const target of [
'darwin-arm64',
'darwin-x64',
'linux-arm64',
'linux-x64',
'win32-x64',
]) {
const directory = path.join(sourceOpenTui, `core-${target}`);
fs.mkdirSync(directory, { recursive: true });
fs.writeFileSync(path.join(directory, 'native-library'), target);
}
fs.mkdirSync(path.join(sourceOpenTui, 'core'), { recursive: true });
fs.writeFileSync(
path.join(sourceOpenTui, 'core', 'parser.worker.js'),
'shared-worker',
);
// Staging installs the target's pinned packages into a throwaway prefix
// instead of reading a host node_modules that cannot hold a cross-built
// target's addon (#11872), so npm is stubbed: it records the argv it is
// given and materializes those packages under --prefix. Nothing here creates
// sourceRoot/node_modules, and the assertions below check it stays absent.
fs.writeFileSync(
npmStub,
`import fs from 'node:fs';
import path from 'node:path';
const args = process.argv.slice(2);
fs.appendFileSync(process.env.QWEN_TEST_NPM_LOG, JSON.stringify(args) + '\\n');
const modules = path.join(args[args.indexOf('--prefix') + 1], 'node_modules');
for (const spec of args.filter((arg) => arg.startsWith('@lydell/'))) {
const at = spec.lastIndexOf('@');
const name = spec.slice(0, at);
const dir = path.join(modules, name);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name }));
fs.writeFileSync(path.join(dir, 'index.js'), 'module.exports = {};\\n');
const prebuild = name.slice('@lydell/node-pty-'.length);
if (!prebuild) continue;
const addonDir = path.join(dir, 'prebuilds', prebuild);
fs.mkdirSync(addonDir, { recursive: true });
fs.writeFileSync(path.join(addonDir, 'pty.node'), 'test addon');
fs.writeFileSync(path.join(addonDir, 'pty.pdb'), 'test symbols');
}
`,
);
fs.mkdirSync(path.join(extractedRoot, 'bin'), { recursive: true });
fs.writeFileSync(path.join(extractedRoot, 'bin', 'node'), 'node');
fs.writeFileSync(path.join(extractedRoot, 'LICENSE'), 'node license');
execFileSync('tar', [
'-czf',
archivePath,
'-C',
directory,
path.basename(extractedRoot),
]);
const archiveHash = crypto
.createHash('sha256')
.update(fs.readFileSync(archivePath))
.digest('hex');
fs.writeFileSync(checksumsPath, `${archiveHash} ${archiveName}\n`);
fs.writeFileSync(
fetchMock,
`import fs from 'node:fs';
globalThis.fetch = async (url) => {
const value = String(url);
const source = value.endsWith('/SHASUMS256.txt')
? process.env.QWEN_TEST_NODE_CHECKSUMS
: process.env.QWEN_TEST_NODE_ARCHIVE;
fs.appendFileSync(process.env.QWEN_TEST_FETCH_LOG, value + '\\n');
return new Response(fs.readFileSync(source), { status: 200 });
};
`,
);
const env = {
...process.env,
QWEN_CODE_COMMIT: 'test-commit',
QWEN_CODE_ROOT: sourceRoot,
QWEN_DESKTOP_NODE_CACHE_DIR: cacheRoot,
QWEN_DESKTOP_SKIP_BUILD: '1',
QWEN_DESKTOP_TARGET: 'x86_64-apple-darwin',
QWEN_TEST_FETCH_LOG: fetchLog,
QWEN_TEST_NPM_LOG: npmLog,
QWEN_TEST_NODE_ARCHIVE: archivePath,
QWEN_TEST_NODE_CHECKSUMS: checksumsPath,
NODE_OPTIONS: [
process.env.NODE_OPTIONS,
`--import=${pathToFileURL(fetchMock).href}`,
]
.filter(Boolean)
.join(' '),
npm_execpath: npmStub,
};
const first = spawnSync(process.execPath, [testScript], {
encoding: 'utf8',
env,
});
assert.equal(first.status, 0, first.stderr);
assert.doesNotMatch(first.stdout, /Using cached Node\.js runtime/);
assert.ok(fs.existsSync(cachedArchivePath));
assert.ok(
fs.existsSync(path.join(runtimeDir, 'qwen-code', 'checksums.json')),
);
const stagedOpenTui = path.join(
runtimeDir,
'qwen-code',
'lib',
'opentui-assets',
'@opentui',
);
assert.deepEqual(fs.readdirSync(stagedOpenTui).sort(), [
'core',
'core-darwin-x64',
]);
assert.equal(
fs.readFileSync(
path.join(stagedOpenTui, 'core-darwin-x64', 'native-library'),
'utf8',
),
'darwin-x64',
);
assert.equal(
fs.readFileSync(
path.join(stagedOpenTui, 'core', 'parser.worker.js'),
'utf8',
),
'shared-worker',
);
assert.equal(
fs.readdirSync(sourceOpenTui).length,
6,
'filter only the owned copied runtime, not the reusable dist',
);
// The Web Terminal resolves its PTY backend from lib/, so the runtime has to
// carry the wrapper and this target's prebuild under lib/node_modules
// (#11872).
const stagedLydellDir = path.join(
runtimeDir,
'qwen-code',
'lib',
'node_modules',
'@lydell',
);
assert.equal(
fs.readFileSync(path.join(stagedLydellDir, 'node-pty', 'index.js'), 'utf8'),
'module.exports = {};\n',
);
const stagedAddon = path.join(
stagedLydellDir,
'node-pty-darwin-x64',
'prebuilds',
'darwin-x64',
'pty.node',
);
assert.ok(fs.existsSync(stagedAddon));
assert.equal(
fs.existsSync(path.join(path.dirname(stagedAddon), 'pty.pdb')),
false,
'debug symbols must not be bundled into the runtime',
);
const stagedChecksums = JSON.parse(
fs.readFileSync(
path.join(runtimeDir, 'qwen-code', 'checksums.json'),
'utf8',
),
);
for (const relative of [
'lib/generated-ui/main.js',
'lib/generated-ui/app.html',
'lib/generated-ui/skills/generated-ui/SKILL.md',
'lib/generated-ui/THIRD_PARTY_NOTICES.txt',
]) {
assert.equal(
fs.readFileSync(path.join(runtimeDir, 'qwen-code', relative), 'utf8'),
'test',
);
assert.equal(
stagedChecksums[relative],
crypto.createHash('sha256').update('test').digest('hex'),
);
}
assert.equal(
stagedChecksums[
'lib/node_modules/@lydell/node-pty-darwin-x64/prebuilds/darwin-x64/pty.node'
],
crypto.createHash('sha256').update('test addon').digest('hex'),
'the staged prebuild must be checksummed so signing refresh and smoke verification cover it',
);
const stagedRipgrep = path.join(
runtimeDir,
'qwen-code',
'lib',
'vendor',
'ripgrep',
'x64-darwin',
'rg',
);
assert.equal(fs.readFileSync(stagedRipgrep, 'utf8'), 'test ripgrep');
assert.equal(
stagedChecksums['lib/vendor/ripgrep/x64-darwin/rg'],
crypto.createHash('sha256').update('test ripgrep').digest('hex'),
'the target ripgrep binary must be staged and checksummed',
);
// ...and it has to get there by fetching the TARGET's pinned package, at the
// version package.json pins, not out of a host node_modules that cannot
// contain a darwin-x64 addon.
const installs = fs
.readFileSync(npmLog, 'utf8')
.trim()
.split('\n')
.map((line) => JSON.parse(line));
assert.equal(installs.length, 1);
assert.deepEqual(
installs[0].filter((arg) => arg.startsWith('@lydell/')),
['@lydell/[email protected]', '@lydell/[email protected]'],
);
assert.ok(
installs[0].includes('--force'),
'npm must be allowed to install a prebuild whose os/cpu do not match this host',
);
assert.equal(
fs.existsSync(path.join(sourceRoot, 'node_modules')),
false,
'staging must not read or need a host node_modules',
);
const second = spawnSync(process.execPath, [testScript], {
encoding: 'utf8',
env,
});
assert.equal(second.status, 0, second.stderr);
assert.match(second.stdout, /Using cached Node\.js runtime/);
fs.appendFileSync(cachedArchivePath, 'tampered');
const poisonedHash = crypto
.createHash('sha256')
.update(fs.readFileSync(cachedArchivePath))
.digest('hex');
fs.writeFileSync(
path.join(cacheDir, 'SHASUMS256.txt'),
`${poisonedHash} ${archiveName}\n`,
);
const recoveredCache = spawnSync(process.execPath, [testScript], {
encoding: 'utf8',
env,
});
assert.equal(recoveredCache.status, 0, recoveredCache.stderr);
assert.doesNotMatch(recoveredCache.stdout, /Using cached Node\.js runtime/);
const fetches = fs.readFileSync(fetchLog, 'utf8').trim().split('\n');
assert.equal(fetches.filter((url) => url.endsWith(archiveName)).length, 2);
assert.equal(
fetches.filter((url) => url.endsWith('SHASUMS256.txt')).length,
3,
);
assert.equal(fs.existsSync(path.join(cacheDir, 'SHASUMS256.txt')), false);
// A target the repo pins nothing for must still produce a runtime: the
// degrade arm fires on a dropped pin, not on an unknown target, and failing
// there would trade a missing Web Terminal for no app at all (#11872).
// Emptying optionalDependencies reproduces that for this fixture's target,
// which desktopTarget() accepts.
fs.writeFileSync(
path.join(sourceRoot, 'package.json'),
JSON.stringify({ version: '0.0.0-test' }),
);
const degraded = spawnSync(process.execPath, [testScript], {
encoding: 'utf8',
env,
});
assert.equal(degraded.status, 0, degraded.stderr);
assert.match(degraded.stderr, /@lydell\/node-pty is not pinned/);
assert.equal(
fs.existsSync(path.join(runtimeDir, 'qwen-code', 'lib', 'node_modules')),
false,
);
// A missing target-specific prebuild pin should identify that package
// instead of blaming the wrapper package, which remains pinned here.
fs.writeFileSync(
path.join(sourceRoot, 'package.json'),
JSON.stringify({
version: '0.0.0-test',
optionalDependencies: { '@lydell/node-pty': '1.2.0-beta.10' },
}),
);
const missingPrebuild = spawnSync(process.execPath, [testScript], {
encoding: 'utf8',
env,
});
assert.equal(missingPrebuild.status, 0, missingPrebuild.stderr);
assert.match(
missingPrebuild.stderr,
/@lydell\/node-pty-darwin-x64 is not pinned/,
);
const marker = path.join(runtimeDir, 'qwen-code', 'complete-marker');
fs.writeFileSync(marker, 'preserve me');
const strandedRoot = path.join(runtimeDir, '.prepare-stranded');
fs.mkdirSync(strandedRoot);
fs.renameSync(
path.join(runtimeDir, 'qwen-code'),
path.join(strandedRoot, 'previous'),
);
fs.rmSync(path.join(sourceRoot, 'LICENSE'));
const failed = spawnSync(process.execPath, [testScript], {
encoding: 'utf8',
env,
});
assert.notEqual(failed.status, 0);
assert.equal(fs.readFileSync(marker, 'utf8'), 'preserve me');
assert.deepEqual(
fs.readdirSync(runtimeDir).filter((entry) => entry.startsWith('.prepare-')),
[],
);
}
function testUpdaterMirrorConfiguration() {
assert.deepEqual(tauriConfig.plugins?.updater?.endpoints, [
'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/desktop/latest/desktop-latest.json',
'https://github.com/QwenLM/qwen-code/releases/download/desktop-latest/desktop-latest.json',
]);
const main = fs.readFileSync(
path.join(packageDir, 'src-tauri', 'src', 'main.rs'),
'utf8',
);
assert.match(
main,
/const UPDATE_CHECK_TIMEOUT: Duration = Duration::from_secs\(3\);/,
);
assert.match(
main,
/app\.updater_builder\(\)\s*\.timeout\(UPDATE_CHECK_TIMEOUT\)/,
);
assert.match(main, /"QWEN_DESKTOP_DISABLE_UPDATES"/);
// The adapter must actually read the env var: without this pin, rewiring
// updates_disabled() to any other source keeps every other gate green.
assert.match(
main,
/fn updates_disabled\(\) -> bool \{\s*updates_disabled_value\(\s*std::env::var_os\(DISABLE_UPDATES_ENV\)\s*\.as_deref\(\)\s*,?\s*\)\s*\}/,
'updates_disabled() must read DISABLE_UPDATES_ENV so the opt-out env var reaches the parser.',
);
// The gate must run before the task is spawned: moving it inside the task,
// after check_for_update, would still phone home to the update feed.
assert.match(
main,
/fn check_updates_silently\(app: AppHandle\) \{\s*if cfg!\(debug_assertions\) \|\| updates_disabled\(\) \{\s*return;\s*\}\s*tauri::async_runtime::spawn\(/,
);
assert.equal((main.match(/check_for_update\(&app\)/g) ?? []).length, 2);
}
function testBootstrapBridgeConfiguration() {
assert.equal(
tauriConfig.app?.withGlobalTauri,
true,
'The Bootstrap UI requires window.__TAURI__ for desktop commands.',
);
assert.deepEqual(
tauriConfig.app?.security?.capabilities,
['bootstrap', 'web-shell-external-url'],
'The local bootstrap and remote Web Shell capabilities must be enabled.',
);
const capability = JSON.parse(
fs.readFileSync(
path.join(packageDir, 'src-tauri', 'capabilities', 'bootstrap.json'),
'utf8',
),
);
assert.deepEqual(capability.windows, ['main']);
assert.equal(
capability.remote,
undefined,
'The bootstrap capability must not grant remote IPC access.',
);
assert.deepEqual(capability.permissions, [
'core:event:allow-listen',
'core:event:allow-unlisten',
'core:window:allow-start-dragging',
'allow-bootstrap-state',
'allow-change-zoom',
'allow-choose-workspace',
'allow-install-update',
'allow-open-logs',
'allow-restart-runtime',
]);
// Declaring an app ACL manifest makes every app command opt-in, so one that
// is registered but missing from build.rs is denied at runtime with no build
// error to point at it.
const build = fs.readFileSync(
path.join(packageDir, 'src-tauri', 'build.rs'),
'utf8',
);
const main = fs.readFileSync(
path.join(packageDir, 'src-tauri', 'src', 'main.rs'),
'utf8',
);
// One class shared by both parsers, and wide enough for any Rust identifier.
// If the two drift, or the class is narrower than the names it parses, a
// command is dropped from `listed` and `registered` identically: deepEqual
// still passes and the grant loop below never demands its capability, so a
// command that is registered but ungranted ships green and is only denied at
// runtime. Both parses stay bounded to the declaration slices, so the wide
// class cannot pick up unrelated identifiers elsewhere in the file.
const COMMAND_NAME = '[A-Za-z0-9_]+';
const commandsStart = build.indexOf('const COMMANDS');
const listed = [
...build
.slice(commandsStart, build.indexOf('];', commandsStart))
.matchAll(new RegExp(`"(${COMMAND_NAME})"`, 'g')),
].map(([, command]) => command);
const handlerStart = main.indexOf('generate_handler![');
const registered = [
...main
.slice(handlerStart, main.indexOf('])', handlerStart))
.matchAll(new RegExp(`\\b(${COMMAND_NAME}),`, 'g')),
].map(([, command]) => command);
assert.deepEqual(
[...listed].sort(),
[...registered].sort(),
'build.rs must list exactly the commands main.rs registers.',
);
for (const command of listed) {
assert.ok(
capability.permissions.includes(`allow-${command.replaceAll('_', '-')}`),
`The bootstrap capability must grant ${command}.`,
);
}
assert.ok(
registered.includes('change_zoom'),
'The zoom shortcut script invokes change_zoom; renaming the command means renaming it there too.',
);
const webShellCapability = JSON.parse(
fs.readFileSync(
path.join(
packageDir,
'src-tauri',
'capabilities',
'web-shell-external-url.json',
),
'utf8',
),
);
assert.equal(webShellCapability.local, false);
assert.deepEqual(webShellCapability.remote, {
urls: ['http://127.0.0.1:*'],
});
assert.deepEqual(webShellCapability.windows, ['main']);
// The daemon-served page reaches exactly two things: the external-URL opener
// and the zoom shortcut. The workspace, updater, and log commands stay
// bootstrap-only.
assert.deepEqual(webShellCapability.permissions, [
'core:window:allow-start-dragging',
{
identifier: 'opener:allow-open-url',
allow: [{ url: 'http://*' }, { url: 'https://*' }, { url: 'mailto:*' }],
},
'allow-change-zoom',
]);
assert.match(main, /title_bar_style\(tauri::TitleBarStyle::Overlay\)/);
assert.match(main, /hidden_title\(true\)/);
assert.match(main, /initialization_script\(MACOS_TITLEBAR_INIT_SCRIPT\)/);
}
function testZoomHotkeyScript() {
const main = fs.readFileSync(
path.join(packageDir, 'src-tauri', 'src', 'main.rs'),
'utf8',
);
const script = /const ZOOM_HOTKEY_SCRIPT: &str = r#"([\s\S]*?)"#;/.exec(
main,
)?.[1];
assert.ok(script, 'main.rs must keep the zoom shortcut script.');
// Simulating the constant proves nothing if the webview never receives it, so
// pin the injection the same way the titlebar script is pinned above.
assert.match(main, /initialization_script\(ZOOM_HOTKEY_SCRIPT\)/);
// Pinning only the injection would leave the persisted factor free to go
// unapplied: both application sites discard their Result via `let _ =`, so
// dropping either - or re-applying on Started, before the document exists -
// ships green with no runtime error, and the README's "the chosen factor is
// restored on the next launch" quietly stops being true.
assert.match(main, /\.on_page_load\(/);
assert.match(
main,
/PageLoadEvent::Finished/,
'Zoom must be re-applied after the document finishes loading.',
);
assert.match(
main,
/webview\.set_zoom\(state\.settings\.zoom\(\)\.unwrap_or\(DEFAULT_ZOOM\)\)/,
'on_page_load must apply the persisted factor to the webview.',
);
assert.match(
main,
/let _ = window\.set_zoom\(zoom\);/,
'Startup must apply the persisted factor to the first document.',
);
const listeners = [];
const invoked = [];
vm.runInNewContext(
script,
{
window: {
__TAURI__: {
core: {
invoke: async (command, args) => {
invoked.push({ command, args });
},
},
},
addEventListener: (event, listener, options) => {
listeners.push({ event, listener, options });
},
},
},
{ timeout: 5000 },
);
let stopped = false;
const dispatch = (event, properties) => {
const registered = listeners.find((entry) => entry.event === event);
assert.ok(registered, `The script must listen for ${event}.`);
let prevented = false;
stopped = false;
registered.listener({
preventDefault: () => {
prevented = true;
},
// Spied rather than left unstubbed: without it a stopPropagation() on any
// path throws a TypeError before send() and the failure names the stub
// instead of the gesture the script claims.
stopPropagation: () => {
stopped = true;
},
...properties,
});
return prevented;
};
assert.equal(
listeners.find((entry) => entry.event === 'keydown').options,
true,
'The keydown listener must capture, so an editor that stops propagation cannot swallow the shortcut.',
);
// Read field by field: the options object comes from the vm realm, so it has
// a different Object.prototype than a literal here.
const wheel = listeners.find((entry) => entry.event === 'wheel').options;
assert.equal(wheel.capture, true);
assert.equal(
wheel.passive,
false,
'The wheel listener must stay non-passive to suppress the pinch gesture.',
);
for (const [properties, action] of [
[{ metaKey: true, key: '=' }, 'in'],
[{ ctrlKey: true, key: '+' }, 'in'],
[{ ctrlKey: true, key: '-' }, 'out'],
[{ metaKey: true, key: '0' }, 'reset'],
]) {
assert.equal(dispatch('keydown', properties), true);
assert.equal(invoked.at(-1).command, 'change_zoom');
assert.equal(invoked.at(-1).args.action, action);
}
// Shortcuts the shell does not own must reach the page untouched.
const owned = invoked.length;
for (const properties of [
{ ctrlKey: true, key: 'a' },
{ key: '=' },
{ altKey: true, metaKey: true, key: '=' },
]) {
assert.equal(dispatch('keydown', properties), false);
}
assert.equal(invoked.length, owned);
// A trackpad pinch reaches the page as a ctrlKey wheel event, in both
// directions: one discrete notch stays exactly one zoom step.
assert.equal(dispatch('wheel', { ctrlKey: true, deltaY: -120 }), true);
assert.equal(invoked.at(-1).command, 'change_zoom');
assert.equal(invoked.at(-1).args.action, 'in');
// Cancelling the default is not enough: the notch keeps propagating, and the
// page's own wheel consumers then read a scroll that provably will not
// happen. TranscriptViewport drops the selection and scroll anchor and pages
// history in; MessageList marks user scroll intent.
assert.equal(
stopped,
true,
'A committed zoom step must stop propagating to the page wheel consumers.',
);
assert.equal(dispatch('wheel', { ctrlKey: true, deltaY: 120 }), true);
assert.equal(invoked.at(-1).args.action, 'out');
assert.equal(stopped, true, 'Both pinch directions must stop propagating.');
// ctrl+shift+wheel is the horizontal-scroll gesture, not a pinch.
assert.equal(
dispatch('wheel', { ctrlKey: true, shiftKey: true, deltaY: -120 }),
false,
'ctrl+shift+wheel must reach the page instead of zooming.',
);
assert.equal(
stopped,
false,
'ctrl+shift+wheel must keep propagating: the shell does not own it.',
);
assert.equal(dispatch('wheel', { deltaY: -120 }), false);
assert.equal(
stopped,
false,
'Plain scrolling must keep propagating, or the desktop shell loses transcript scroll, edge history loading and wheel intent.',
);
assert.equal(invoked.length, owned + 2);
// A real pinch arrives as a burst of small deltas. Steps must follow the
// accumulated movement, not the event count, or a gentle pinch sweeps the
// factor to the clamp and the flusher persists it.
const burst = invoked.length;
for (let event = 0; event < 30; event += 1) {
dispatch('wheel', { ctrlKey: true, deltaY: -2 });
}
// Exact, not merely bounded from above. An upper bound alone passes both when
// the threshold is lowered - a third of a mouse notch then buys a full step,
// which is the clamp-sweeping outcome this comment says the accumulator
// prevents - and when it is raised past the burst total, where the pinch path
// is simply dead and emits nothing.
assert.equal(
invoked.length - burst,
1,
`A 30-event pinch burst (-60) must emit exactly one zoom step, got ${
invoked.length - burst
}.`,
);
assert.equal(invoked.at(-1).args.action, 'in');
// No vertical movement means nothing to zoom: the event, and the horizontal
// scroll it belongs to, must pass through unowned.
const afterBurst = invoked.length;
assert.equal(dispatch('wheel', { ctrlKey: true, deltaY: 0 }), false);
assert.equal(
dispatch('wheel', { ctrlKey: true, deltaX: 50, deltaY: 0 }),
false,
);
assert.equal(
stopped,
false,
'A horizontal swipe carries no zoom: it and its scroll must pass through.',
);
assert.equal(invoked.length, afterBurst);
// The other side of the threshold, which is what pins it from below: a burst
// that stops one event short must buy nothing at all. It is already owned
// though - preventDefault has cancelled its scroll on every event - so it
// must stop propagating too, or the page's wheel consumers read a scroll that
// cannot happen once per sub-threshold event while no zoom is emitted.
const subThreshold = invoked.length;
for (let event = 0; event < 29; event += 1) {
assert.equal(
dispatch('wheel', { ctrlKey: true, deltaY: -2 }),
true,
'An owned sub-threshold pinch event must still cancel the native scroll.',
);
}
assert.equal(
invoked.length,
subThreshold,
`A sub-threshold burst (-58) must not zoom, got ${
invoked.length - subThreshold
}.`,
);
assert.equal(
stopped,
true,
'An owned sub-threshold pinch must stop propagating to the page wheel consumers.',
);
}
function testResolveLogRoot() {
const paths = {
isolatedHome: path.join('/', 'home'),
isolatedState: path.join('/', 'state'),
appId: tauriConfig.identifier,
};
assert.equal(
resolveLogRoot('darwin', {}, paths),
path.join('/', 'home', 'Library', 'Logs', tauriConfig.identifier),
);
assert.equal(
resolveLogRoot('linux', {}, paths),
path.join('/', 'state', tauriConfig.identifier, 'logs'),
);
assert.equal(
resolveLogRoot('win32', { LOCALAPPDATA: path.join('C:', 'x') }, paths),
path.join('C:', 'x', tauriConfig.identifier, 'logs'),
);
assert.throws(
() => resolveLogRoot('win32', {}, paths),
/LOCALAPPDATA is required/,
);
// Structural invariants that cannot be tested through the exported helper:
// the smoke must not override LOCALAPPDATA in the child env, and the
// pre-spawn snapshot must precede the spawn call.
const smoke = fs.readFileSync(
path.join(packageDir, 'scripts', 'smoke-packaged.js'),
'utf8',
);
assert.doesNotMatch(smoke, /^\s*LOCALAPPDATA:/m);
assert.match(
smoke,
/QWEN_DESKTOP_DISABLE_SETTINGS_PERSISTENCE: '1'/,
'Windows packaged smoke must not persist its temporary desktop state',
);
assert.match(
smoke,
/const logRoot = resolveLogRoot\(process\.platform, process\.env, \{/,
'smoke must resolve log root via resolveLogRoot',
);
assert.match(
smoke,
/const appId = JSON\.parse\(\s*fs\.readFileSync\(\s*path\.join\(packageDir, 'src-tauri', 'tauri\.conf\.json'\),\s*'utf8',\s*\),\s*\)\.identifier;/,
'smoke appId must be derived from the tauri.conf.json identifier',
);
const previousLogIndex = smoke.indexOf(
'let previousLog = fs.readFileSync(logPath',
);
const spawnIndex = smoke.indexOf('const child = spawn(executable');
assert.notEqual(previousLogIndex, -1, 'smoke must capture previousLog');
assert.notEqual(spawnIndex, -1, 'smoke must spawn the child');
assert.ok(
previousLogIndex < spawnIndex,
'previousLog must be captured before the child is spawned',
);
const readNewLogCalls = smoke.match(/const contents = readNewLog\(\)/g);
assert.ok(
readNewLogCalls && readNewLogCalls.length === 1,
'the polling loop must be the only incremental readNewLog() call site',
);
assert.match(
smoke,
/console\.warn\([\s\S]*?previousLog = contents;/,
'a rewritten log must warn and rebase the slice baseline',
);
assert.match(
smoke,
/const contents = fs\.readFileSync\(logPath, \{\s*encoding: 'utf8',\s*flag: 'a\+',\s*\}\);\s*throw smokeError\('Timed out waiting for packaged desktop runtime\.', contents\);/,
'the timeout error must embed the full log, not the incremental delta',
);
assert.match(
smoke,
/sliceNewLog\(/,
'smoke must slice the log via the tested sliceNewLog helper',
);
assert.match(
smoke,
/function smokeError[\s\S]*?Log: \$\{logPath\}/,
'smokeError must embed the log path like the timeout error does',
);
}
function testSliceNewLog() {
assert.deepEqual(sliceNewLog('hello', ''), {
text: 'hello',
baseline: '',
});
assert.deepEqual(sliceNewLog('hello world', 'hello'), {
text: ' world',
baseline: 'hello',
});
assert.deepEqual(sliceNewLog('new', 'old'), {
text: 'new',
baseline: '',
});
}
function testUpdateManifest(directory) {
const assets = path.join(directory, 'assets');
fs.mkdirSync(assets, { recursive: true });
const artifacts = [
'Qwen-Code-aarch64-apple-darwin.app.tar.gz',
'Qwen-Code-x86_64-apple-darwin.app.tar.gz',
'Qwen-Code_0.1.0_x64-setup.exe',
'Qwen-Code_0.1.0_amd64.AppImage',
'Qwen-Code_0.1.0_aarch64.AppImage',
];
for (const artifact of artifacts) {
assert.ok(
!artifact.includes(' '),
`Artifact name must not contain spaces: ${artifact}`,
);
}
for (const artifact of artifacts) {
fs.writeFileSync(path.join(assets, artifact), artifact);
fs.writeFileSync(
path.join(assets, `${artifact}.sig`),
`signature:${artifact}\n`,
);
}
const output = path.join(directory, 'desktop-latest.json');
execFileSync(process.execPath, [
manifestScript,
'--assets',
assets,
'--repository',
'QwenLM/qwen-code',
'--tag',
'desktop-v0.1.0',
'--version',
'0.1.0',
'--output',
output,
]);
const manifest = JSON.parse(fs.readFileSync(output, 'utf8'));
assert.equal(manifest.version, '0.1.0');
assert.deepEqual(Object.keys(manifest.platforms).sort(), [
'darwin-aarch64',
'darwin-x86_64',
'linux-aarch64',
'linux-x86_64',
'windows-x86_64',
]);
for (const [platform, artifact] of [
['darwin-aarch64', artifacts[0]],
['darwin-x86_64', artifacts[1]],
['windows-x86_64', artifacts[2]],
['linux-x86_64', artifacts[3]],
['linux-aarch64', artifacts[4]],
]) {
assert.equal(
manifest.platforms[platform].signature,
`signature:${artifact}`,
);
assert.equal(
manifest.platforms[platform].url,
`https://github.com/QwenLM/qwen-code/releases/download/desktop-v0.1.0/${encodeURIComponent(artifact)}`,
);
}
execFileSync(process.execPath, [
manifestScript,
'--assets',
assets,
'--repository',
'QwenLM/qwen-code',
'--tag',
'desktop-v0.1.0',
'--version',
'0.1.0',
'--base-url',
'https://mirror.example/desktop/v0.1.0/',
'--output',
output,
]);
const mirrorManifest = JSON.parse(fs.readFileSync(output, 'utf8'));
for (const [platform, artifact] of [
['darwin-aarch64', artifacts[0]],
['darwin-x86_64', artifacts[1]],
['windows-x86_64', artifacts[2]],
['linux-x86_64', artifacts[3]],
['linux-aarch64', artifacts[4]],
]) {
assert.equal(
mirrorManifest.platforms[platform].url,
`https://mirror.example/desktop/v0.1.0/${encodeURIComponent(artifact)}`,
);
}
// Re-mirroring an already-published release has to be able to reproduce the
// feed that release shipped, so a platform it predates can be named as
// optional -- by name, and only when it is genuinely absent (#12806).
// `spawnOptions`/`outputValue` exist for the relative --output spelling both
// production callers use (`cd`, then `--output desktop-latest.json`); every
// other run here passes absolute paths and inherits this test's cwd.
const spawnManifest = (spawnOptions, outputValue, ...extra) =>
spawnSync(
process.execPath,
[
manifestScript,
'--assets',
assets,
'--repository',
'QwenLM/qwen-code',
'--tag',
'desktop-v0.1.0',
'--version',
'0.1.0',
'--output',
outputValue,
...extra,
],
{ encoding: 'utf8', ...spawnOptions },
);
const runManifest = (...extra) => spawnManifest({}, output, ...extra);
// Tolerated-and-present, which is the normal case on the only production
// caller: sync-desktop-to-oss.yml passes --allow-missing-platform
// linux-aarch64 on every SOURCE=release re-mirror, and from this release
// onward that artifact is in the downloaded assets. Without this run the
// `matches.length === 0 &&` conjunct is unpinned -- dropping it turns the
// escape hatch into a blanket opt-out that omits the arm64 leg from the
// mirror feed (the first updater endpoint) while the asset sits in the same
// bucket, exits 0, and prints a warning that is false. That is #12806 again.
const toleratedButPresent = runManifest(
'--allow-missing-platform',
'linux-aarch64',
);
assert.equal(toleratedButPresent.status, 0, toleratedButPresent.stderr);
assert.deepEqual(
Object.keys(JSON.parse(fs.readFileSync(output, 'utf8')).platforms).sort(),
[
'darwin-aarch64',
'darwin-x86_64',
'linux-aarch64',
'linux-x86_64',
'windows-x86_64',
],
'a tolerated platform whose artifact did upload must still be published',
);
assert.doesNotMatch(
toleratedButPresent.stdout,
/::warning::/,
'a tolerated platform that did upload must not be reported missing',
);
// Accumulation belongs to --allow-missing-platform alone. A duplicated
// single-valued flag has to override: comma-joining it would put
// `"version": "0.1.0,0.1.0"` in the signed feed, which the updater client
// cannot parse as semver, and for --output it would write a file named
// `<f>,<f>` so no feed exists at all -- both with the step exiting 0.
const duplicatedVersion = runManifest('--version', '0.1.0');
assert.equal(duplicatedVersion.status, 0, duplicatedVersion.stderr);
assert.equal(
JSON.parse(fs.readFileSync(output, 'utf8')).version,
'0.1.0',
'a repeated --version must override, not comma-join into the feed',
);
// Read the feed back the way duplicatedVersion does. On the absolute
// fixture path a comma-joined --output dies with ENOENT for a parent that
// does not exist, so `status === 0` only caught it by accident of the
// fixture; deleting the feed first makes the assertion about where the run
// wrote rather than about whether the write happened to fail.
fs.rmSync(output);
const duplicatedOutput = runManifest('--output', output);
assert.equal(
duplicatedOutput.status,
0,
`a repeated --output must override, not write a comma-joined filename: ${duplicatedOutput.stderr}`,
);
assert.equal(
JSON.parse(fs.readFileSync(output, 'utf8')).version,
'0.1.0',
'a repeated --output must write the feed to the path that was asked for',
);
// Both production callers spell --output relatively after a `cd`
// (sync-desktop-to-oss.yml:135, desktop-release.yml:721), and there the
// comma-joined value is a legal filename in the cwd: the run exits 0, writes
// `desktop-latest.json,desktop-latest.json`, and the upload step checksums a
// path that holds no feed.
const relativeDir = path.join(directory, 'relative-output');
fs.mkdirSync(relativeDir, { recursive: true });
const duplicatedRelativeOutput = spawnManifest(
{ cwd: relativeDir },
'desktop-latest.json',
'--output',
'desktop-latest.json',
);
assert.equal(
duplicatedRelativeOutput.status,
0,
duplicatedRelativeOutput.stderr,
);
assert.equal(
JSON.parse(
fs.readFileSync(path.join(relativeDir, 'desktop-latest.json'), 'utf8'),
).version,
'0.1.0',
'a repeated relative --output must override, not comma-join in the cwd',
);
assert.ok(
!fs.existsSync(
path.join(relativeDir, 'desktop-latest.json,desktop-latest.json'),
),
'a repeated relative --output must not leave a comma-joined feed behind',
);
fs.rmSync(path.join(assets, artifacts[4]));
fs.rmSync(path.join(assets, `${artifacts[4]}.sig`));
const missingLeg = runManifest();
assert.notEqual(missingLeg.status, 0);
assert.match(
missingLeg.stderr,
/Expected one updater artifact for linux-aarch64, found 0/,
);
assert.doesNotMatch(
missingLeg.stdout,
/::warning::/,
'a run that refused to publish must not also report a tolerated platform',
);
assert.match(
runManifest('--allow-missing-platform', 'darwin-x86_64').stderr,
/linux-aarch64, found 0/,
'the escape hatch is keyed per platform, not a blanket opt-out',
);
const tolerant = runManifest('--allow-missing-platform', 'linux-aarch64');
assert.equal(tolerant.status, 0, tolerant.stderr);
assert.deepEqual(
Object.keys(JSON.parse(fs.readFileSync(output, 'utf8')).platforms).sort(),
['darwin-aarch64', 'darwin-x86_64', 'linux-x86_64', 'windows-x86_64'],
);
// A dropped key is otherwise invisible: the run exits 0 and its log is
// byte-identical to a complete one, so the omission is only discoverable by
// diffing the published feed against the previous mirror.
assert.match(
tolerant.stdout,
/::warning::no updater artifact for linux-aarch64/,
);
// Both spellings of a multi-valued option must mean the same thing: a
// bash-array caller writes repeated flags, and plain assignment in
// parseArguments would silently keep only the last one.
fs.rmSync(path.join(assets, artifacts[0]));
fs.rmSync(path.join(assets, `${artifacts[0]}.sig`));
for (const spelling of [
[
'--allow-missing-platform',
'linux-aarch64',
'--allow-missing-platform',
'darwin-aarch64',
],
['--allow-missing-platform', 'linux-aarch64,darwin-aarch64'],
]) {
const both = runManifest(...spelling);
assert.equal(both.status, 0, both.stderr);
assert.deepEqual(
Object.keys(JSON.parse(fs.readFileSync(output, 'utf8')).platforms).sort(),
['darwin-x86_64', 'linux-x86_64', 'windows-x86_64'],
);
}
// darwin-aarch64 is tolerated but linux-aarch64 is not, so this run throws
// and writes no feed. It must not also publish an annotation claiming an
// incomplete feed went out: GitHub parses workflow commands from stdout, so
// a warning emitted at selection time turns a red run into a claim that
// sends oncall looking for a truncated desktop-latest.json that never
// existed, while the real cause is the other leg named in stderr.
const refusedAfterTolerating = runManifest(
'--allow-missing-platform',
'darwin-aarch64',
);
assert.notEqual(refusedAfterTolerating.status, 0);
assert.match(
refusedAfterTolerating.stderr,
/Expected one updater artifact for linux-aarch64, found 0/,
);
assert.doesNotMatch(
refusedAfterTolerating.stdout,
/::warning::/,
'a run that refused to publish must not also report a tolerated platform',
);
for (const artifact of [artifacts[0], artifacts[4]]) {
fs.writeFileSync(path.join(assets, artifact), artifact);
fs.writeFileSync(
path.join(assets, `${artifact}.sig`),
`signature:${artifact}\n`,
);
}
fs.rmSync(path.join(assets, `${artifacts[3]}.sig`));
const failure = runManifest();
assert.notEqual(failure.status, 0);
assert.match(failure.stderr, /Missing updater signature/);
}
function testElectronBridgeManifest(directory) {
const assets = path.join(directory, 'assets');
fs.mkdirSync(assets, { recursive: true });
const artifacts = [
'Qwen-Code-Desktop-arm64.zip',
'Qwen-Code-Desktop-x64.zip',
'Qwen-Code-Desktop-arm64.dmg',
'Qwen-Code-Desktop-x64.dmg',
];
for (const artifact of artifacts) {
fs.writeFileSync(path.join(assets, artifact), `contents:${artifact}`);
}
artifacts.push(
'Qwen-Code-Desktop_0.1.0_x64-setup.exe',
'Qwen-Code-Desktop_0.1.0_amd64.AppImage',
// Not selected by any platform: the release matrix builds a second Linux
// AppImage, and the linux manifest must keep picking the x64 one.
'Qwen-Code-Desktop_0.1.0_aarch64.AppImage',
);
for (const artifact of artifacts.slice(4)) {
fs.writeFileSync(path.join(assets, artifact), `contents:${artifact}`);
}
const macOutput = path.join(directory, 'latest-mac.yml');
for (const [platform, filename, selected] of [
['macos', 'latest-mac.yml', artifacts.slice(0, 4)],
['windows', 'latest.yml', artifacts.slice(4, 5)],
['linux', 'latest-linux.yml', artifacts.slice(5, 6)],
]) {
const output = path.join(directory, filename);
execFileSync(process.execPath, [
electronBridgeScript,
'--assets',
assets,
'--platform',
platform,
'--version',
'0.1.0',
'--output',
output,
]);
const manifest = fs.readFileSync(output, 'utf8');
assert.match(manifest, /^version: 0\.1\.0$/m);
assert.match(
manifest,
/^releaseDate: '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'$/m,
);
for (const artifact of selected) {
const contents = fs.readFileSync(path.join(assets, artifact));
const sha512 = crypto
.createHash('sha512')
.update(contents)
.digest('base64');
assert.match(
manifest,
new RegExp(
`^ - url: ${artifact.replaceAll('.', '\\.')}\\n sha512: ${sha512.replaceAll('+', '\\+')}\\n size: ${contents.length}$`,
'm',
),
);
if (artifact === selected[0]) {
assert.match(
manifest,
new RegExp(`^path: ${artifact.replaceAll('.', '\\.')}$`, 'm'),
);
assert.match(
manifest,
new RegExp(`^sha512: ${sha512.replaceAll('+', '\\+')}$`, 'm'),
);
}
}
assert.equal(
(manifest.match(/^ {2}- url:/gm) ?? []).length,
selected.length,
);
}
const duplicateWindowsArtifact = path.join(
assets,
'Qwen-Code-Desktop_0.1.0_arm64-setup.exe',
);
fs.writeFileSync(duplicateWindowsArtifact, 'duplicate');
const ambiguousWindows = spawnSync(
process.execPath,
[
electronBridgeScript,
'--assets',
assets,
'--platform',
'windows',
'--version',
'0.1.0',
'--output',
path.join(directory, 'ambiguous-windows.yml'),
],
{ encoding: 'utf8' },
);
assert.notEqual(ambiguousWindows.status, 0);
assert.match(ambiguousWindows.stderr, /found 2/);
fs.rmSync(duplicateWindowsArtifact);
fs.rmSync(path.join(assets, artifacts[1]));
const failure = spawnSync(
process.execPath,
[
electronBridgeScript,
'--assets',
assets,
'--platform',
'macos',
'--version',
'0.1.0',
'--output',
macOutput,
],
{ encoding: 'utf8' },
);
assert.notEqual(failure.status, 0);
assert.match(failure.stderr, /Expected one Electron bridge artifact/);
const invalidVersion = spawnSync(
process.execPath,
[
electronBridgeScript,
'--assets',
assets,
'--platform',
'macos',
'--version',
'0.1',
'--output',
macOutput,
],
{ encoding: 'utf8' },
);
assert.notEqual(invalidVersion.status, 0);
assert.match(invalidVersion.stderr, /Invalid --version/);
const missingOutput = spawnSync(
process.execPath,
[
electronBridgeScript,
'--assets',
assets,
'--platform',
'macos',
'--version',
'0.1.0',
],
{ encoding: 'utf8' },
);
assert.notEqual(missingOutput.status, 0);
assert.match(missingOutput.stderr, /Missing --output/);
}
function testVersionSynchronization(directory) {
fs.mkdirSync(path.join(directory, 'src-tauri'), { recursive: true });
fs.copyFileSync(
path.join(packageDir, 'package.json'),
path.join(directory, 'package.json'),
);
fs.copyFileSync(
path.join(packageDir, 'src-tauri', 'Cargo.toml'),
path.join(directory, 'src-tauri', 'Cargo.toml'),
);
fs.copyFileSync(
path.join(packageDir, 'src-tauri', 'tauri.conf.json'),
path.join(directory, 'src-tauri', 'tauri.conf.json'),
);
execFileSync(process.execPath, [versionScript, '1.2.3'], {
cwd: directory,
env: { ...process.env, QWEN_DESKTOP_PACKAGE_DIR: directory },
});
assert.equal(
JSON.parse(fs.readFileSync(path.join(directory, 'package.json'), 'utf8'))
.version,
'1.2.3',
);
assert.equal(
JSON.parse(
fs.readFileSync(
path.join(directory, 'src-tauri', 'tauri.conf.json'),
'utf8',
),
).version,
'1.2.3',
);
assert.match(
fs.readFileSync(path.join(directory, 'src-tauri', 'Cargo.toml'), 'utf8'),
/^version = "1\.2\.3"$/m,
);
}