/** * @license * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, truncateSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { copyBundleAssets } from '../copy_bundle_assets.js'; import { preparePackage } from '../prepare-package.js'; describe('package asset scripts', () => { const tempDirs = []; afterEach(() => { vi.restoreAllMocks(); for (const tempDir of tempDirs.splice(0)) { rmSync(tempDir, { force: true, recursive: true }); } }); it('copies extension examples into the bundled runtime dist', () => { const rootDir = createFixtureRoot(); writeFile( rootDir, 'packages/cli/src/commands/extensions/examples/mcp-server/keep.test.js', 'console.log("example test fixture");\n', ); stubConsole(); copyBundleAssets({ root: rootDir }); expect(readdirSync(path.join(rootDir, 'dist', 'examples')).sort()).toEqual([ 'agent', 'commands', 'context', 'mcp-server', 'skills', ]); expect( existsSync( path.join(rootDir, 'dist', 'examples', 'mcp-server', 'package.json'), ), ).toBe(true); expect( existsSync( path.join(rootDir, 'dist', 'examples', 'mcp-server', 'keep.test.js'), ), ).toBe(true); }); it('copies bundled skill scripts and references into the runtime dist', () => { const rootDir = createFixtureRoot(); writeFile( rootDir, 'packages/core/src/skills/bundled/dataviz/SKILL.md', '---\nname: dataviz\ndescription: Chart guidance\n---\nBody\n', ); writeFile( rootDir, 'packages/core/src/skills/bundled/dataviz/scripts/validate_palette.js', 'console.log("ok");\n', ); writeFile( rootDir, 'packages/core/src/skills/bundled/dataviz/scripts/validate_palette.test.js', 'import { it } from "vitest";\n', ); writeFile( rootDir, 'packages/core/src/skills/bundled/dataviz/scripts/validate_palette.test.d.ts', 'export {};\n', ); writeFile( rootDir, 'packages/core/src/skills/bundled/dataviz/scripts/validate_palette.test.js.map', '{}\n', ); writeFile( rootDir, 'packages/core/src/skills/bundled/dataviz/scripts/chart.spec.tsx', 'export {};\n', ); writeFile( rootDir, 'packages/core/src/skills/bundled/dataviz/scripts/font-test-regular.woff2', 'font\n', ); writeFile( rootDir, 'packages/core/src/skills/bundled/dataviz/references/palette.md', '# Palette\n', ); writeFile(rootDir, 'dist/bundled/dataviz/scripts/stale.test.js', 'stale\n'); stubConsole(); copyBundleAssets({ root: rootDir }); expect( existsSync( path.join( rootDir, 'dist', 'bundled', 'dataviz', 'scripts', 'validate_palette.js', ), ), ).toBe(true); expect( existsSync( path.join( rootDir, 'dist', 'bundled', 'dataviz', 'scripts', 'validate_palette.test.js', ), ), ).toBe(false); expect( existsSync( path.join( rootDir, 'dist', 'bundled', 'dataviz', 'scripts', 'validate_palette.test.d.ts', ), ), ).toBe(false); expect( existsSync( path.join( rootDir, 'dist', 'bundled', 'dataviz', 'scripts', 'validate_palette.test.js.map', ), ), ).toBe(false); expect( existsSync( path.join( rootDir, 'dist', 'bundled', 'dataviz', 'scripts', 'chart.spec.tsx', ), ), ).toBe(false); expect( existsSync( path.join( rootDir, 'dist', 'bundled', 'dataviz', 'scripts', 'stale.test.js', ), ), ).toBe(false); expect( existsSync( path.join( rootDir, 'dist', 'bundled', 'dataviz', 'scripts', 'font-test-regular.woff2', ), ), ).toBe(true); expect( existsSync( path.join( rootDir, 'dist', 'bundled', 'dataviz', 'references', 'palette.md', ), ), ).toBe(true); }); it('includes extension examples in the prepared dist package', () => { const rootDir = createFixtureRoot(); createBundleArtifacts(rootDir); stubConsole(); preparePackage({ rootDir, requireNativeAudioCapture: false }); const distPackageJson = JSON.parse( readFileSync(path.join(rootDir, 'dist', 'package.json'), 'utf8'), ); const rootPackageJson = JSON.parse( readFileSync(path.join(rootDir, 'package.json'), 'utf8'), ); expect(distPackageJson.files).toContain('examples'); expect(distPackageJson.bundledDependencies).toBeUndefined(); expect(distPackageJson.optionalDependencies).toMatchObject({ '@qwen-code/audio-capture': rootPackageJson.version, }); expect( existsSync( path.join( rootDir, 'dist', 'node_modules', '@qwen-code', 'audio-capture', ), ), ).toBe(false); expect( existsSync( path.join(rootDir, 'dist', 'examples', 'mcp-server', 'package.json'), ), ).toBe(true); }); it('omits browser MCP install hooks and deps from the prepared dist package', () => { const rootDir = createFixtureRoot(); createBundleArtifacts(rootDir); stubConsole(); const browserMcpPackageName = ['chrome', 'devtools', 'mcp'].join('-'); const browserAutomationPackageName = ['puppeteer', 'core'].join('-'); const installScriptFile = ['postinstall', 'js'].join('.'); const browserMcpPatchFile = `${browserMcpPackageName}+1.4.0.patch`; preparePackage({ rootDir, requireNativeAudioCapture: false }); const distDir = path.join(rootDir, 'dist'); const distPackageJson = JSON.parse( readFileSync(path.join(distDir, 'package.json'), 'utf8'), ); expect(distPackageJson.files).not.toEqual( expect.arrayContaining(['patches', installScriptFile]), ); expect(distPackageJson.scripts).toBeUndefined(); expect(distPackageJson.dependencies).toEqual({}); expect(distPackageJson.optionalDependencies).not.toHaveProperty( browserMcpPackageName, ); expect(distPackageJson.optionalDependencies).not.toHaveProperty( browserAutomationPackageName, ); expect(existsSync(path.join(distDir, installScriptFile))).toBe(false); expect(existsSync(path.join(distDir, 'patches'))).toBe(false); expect(existsSync(path.join(distDir, 'patches', browserMcpPatchFile))).toBe( false, ); }); it.each([ ['dist/web-shell/assets/icon.svg', 'chrome-devtools-mcp\n'], ['dist/chunks/server.js.map', '{"sources":["Chrome-Devtools-MCP"]}\n'], ])( 'fails packaging when prepared dist contains scanner-sensitive literals in %s', (packagePath, contents) => { const rootDir = createFixtureRoot(); createBundleArtifacts(rootDir); const browserMcpPackageName = ['chrome', 'devtools', 'mcp'].join('-'); writeFile(rootDir, packagePath, contents); stubConsole(); const expectedPath = path.relative( path.join(rootDir, 'dist'), path.join(rootDir, packagePath), ); expect(() => preparePackage({ rootDir, requireNativeAudioCapture: false }), ).toThrow( `Prepared package contains forbidden string "${browserMcpPackageName}" in ${expectedPath}`, ); }, ); it('fails packaging when prepared dist exceeds the unpacked size budget', () => { const rootDir = createFixtureRoot(); createBundleArtifacts(rootDir); stubConsole(); preparePackage({ rootDir, requireNativeAudioCapture: false, maxPackageUnpackedBytes: 50_000, }); const oversizedRootDir = createFixtureRoot(); createBundleArtifacts(oversizedRootDir); writeFile(oversizedRootDir, 'dist/chunks/large.js', 'x'.repeat(64 * 1024)); expect(() => preparePackage({ rootDir: oversizedRootDir, requireNativeAudioCapture: false, maxPackageUnpackedBytes: 50_000, }), ).toThrow(/Prepared package unpacked size \d+ bytes exceeds 50000 bytes/); }); it('enforces a 96 MiB default unpacked size budget', () => { const rootDir = createFixtureRoot(); createBundleArtifacts(rootDir); writeFile(rootDir, 'dist/chunks/large.bin', ''); const largeFile = path.join(rootDir, 'dist', 'chunks', 'large.bin'); truncateSync(largeFile, 95 * 1024 * 1024); stubConsole(); expect(() => preparePackage({ rootDir, requireNativeAudioCapture: false, }), ).not.toThrow(); truncateSync(largeFile, 96 * 1024 * 1024); expect(() => preparePackage({ rootDir, requireNativeAudioCapture: false, }), ).toThrow( /Prepared package unpacked size \d+ bytes exceeds 100663296 bytes/, ); }); it('omits bundledDependencies when audio-capture artifacts are missing', () => { const rootDir = createFixtureRoot(); rmSync(path.join(rootDir, 'packages', 'audio-capture', 'prebuilds'), { recursive: true, force: true, }); createBundleArtifacts(rootDir); stubConsole(); preparePackage({ rootDir, requireNativeAudioCapture: false }); const distPackageJson = JSON.parse( readFileSync(path.join(rootDir, 'dist', 'package.json'), 'utf8'), ); expect(distPackageJson.bundledDependencies).toBeUndefined(); expect( existsSync( path.join( rootDir, 'dist', 'node_modules', '@qwen-code', 'audio-capture', ), ), ).toBe(false); }); it('removes stale bundled audio-capture files when artifacts are missing', () => { const rootDir = createFixtureRoot(); createBundleArtifacts(rootDir); writeFile( rootDir, 'dist/node_modules/@qwen-code/audio-capture/prebuilds/darwin-arm64/@qwen-code+audio-capture.node', 'stale native addon\n', ); stubConsole(); rmSync(path.join(rootDir, 'packages', 'audio-capture', 'prebuilds'), { recursive: true, force: true, }); preparePackage({ rootDir, requireNativeAudioCapture: false }); const distPackageJson = JSON.parse( readFileSync(path.join(rootDir, 'dist', 'package.json'), 'utf8'), ); expect(distPackageJson.bundledDependencies).toBeUndefined(); expect( existsSync( path.join( rootDir, 'dist', 'node_modules', '@qwen-code', 'audio-capture', ), ), ).toBe(false); }); it('fails packaging when required audio-capture artifacts are missing', () => { const rootDir = createFixtureRoot(); rmSync(path.join(rootDir, 'packages', 'audio-capture', 'prebuilds'), { recursive: true, force: true, }); createBundleArtifacts(rootDir); stubConsole(); expect(() => preparePackage({ rootDir, requireNativeAudioCapture: true }), ).toThrow(/Required audio capture package artifact not found at/); }); it('fails packaging when required audio-capture runtime output is empty', () => { const rootDir = createFixtureRoot(); rmSync(path.join(rootDir, 'packages', 'audio-capture', 'dist', 'index.js')); createBundleArtifacts(rootDir); stubConsole(); expect(() => preparePackage({ rootDir, requireNativeAudioCapture: true }), ).toThrow(/Required audio capture package artifact has no runtime JS/); }); it('fails packaging when required audio-capture prebuilds are empty', () => { const rootDir = createFixtureRoot(); rmSync( path.join( rootDir, 'packages', 'audio-capture', 'prebuilds', 'darwin-arm64', '@qwen-code+audio-capture.node', ), ); createBundleArtifacts(rootDir); stubConsole(); expect(() => preparePackage({ rootDir, requireNativeAudioCapture: true }), ).toThrow(/Required audio capture package artifact has no native prebuild/); }); it('omits bundledDependencies when audio-capture dependencies are missing and not required', () => { const rootDir = createFixtureRoot(); const audioPackagePath = path.join( rootDir, 'packages', 'audio-capture', 'package.json', ); const audioPackageJson = JSON.parse(readFileSync(audioPackagePath, 'utf8')); audioPackageJson.dependencies['missing-audio-runtime'] = '1.0.0'; writeFileSync(audioPackagePath, JSON.stringify(audioPackageJson, null, 2)); createBundleArtifacts(rootDir); stubConsole(); preparePackage({ rootDir, requireNativeAudioCapture: false }); const distPackageJson = JSON.parse( readFileSync(path.join(rootDir, 'dist', 'package.json'), 'utf8'), ); expect(distPackageJson.bundledDependencies).toBeUndefined(); expect( existsSync( path.join( rootDir, 'dist', 'node_modules', '@qwen-code', 'audio-capture', ), ), ).toBe(false); }); it('fails packaging when required audio-capture dependencies are missing', () => { const rootDir = createFixtureRoot(); const audioPackagePath = path.join( rootDir, 'packages', 'audio-capture', 'package.json', ); const audioPackageJson = JSON.parse(readFileSync(audioPackagePath, 'utf8')); audioPackageJson.dependencies['missing-audio-runtime'] = '1.0.0'; writeFileSync(audioPackagePath, JSON.stringify(audioPackageJson, null, 2)); createBundleArtifacts(rootDir); stubConsole(); expect(() => preparePackage({ rootDir, requireNativeAudioCapture: true }), ).toThrow(/Required audio capture dependency not resolvable/); }); it('omits bundledDependencies when audio-capture package JSON is invalid and not required', () => { const rootDir = createFixtureRoot(); writeFile(rootDir, 'packages/audio-capture/package.json', '{ invalid json'); createBundleArtifacts(rootDir); stubConsole(); preparePackage({ rootDir, requireNativeAudioCapture: false }); const distPackageJson = JSON.parse( readFileSync(path.join(rootDir, 'dist', 'package.json'), 'utf8'), ); expect(distPackageJson.bundledDependencies).toBeUndefined(); expect( existsSync( path.join( rootDir, 'dist', 'node_modules', '@qwen-code', 'audio-capture', ), ), ).toBe(false); }); it('fails packaging when required audio-capture package JSON is invalid', () => { const rootDir = createFixtureRoot(); writeFile(rootDir, 'packages/audio-capture/package.json', '{ invalid json'); createBundleArtifacts(rootDir); stubConsole(); expect(() => preparePackage({ rootDir, requireNativeAudioCapture: true }), ).toThrow(/Required audio capture package\.json is not valid JSON/); }); function createFixtureRoot() { const rootDir = mkdtempSync(path.join(tmpdir(), 'qwen-package-assets-')); tempDirs.push(rootDir); writeFile(rootDir, 'README.md', '# Qwen Code\n'); writeFile(rootDir, 'LICENSE', 'Apache-2.0\n'); writeFile( rootDir, 'package.json', JSON.stringify( { name: '@qwen-code/qwen-code', version: '0.17.0', description: 'Qwen Code', repository: { type: 'git', url: 'https://github.com/QwenLM/qwen-code.git', }, config: {}, engines: { node: '>=22.0.0', }, devDependencies: { 'patch-package': '^8.0.1', }, }, null, 2, ), ); writeFile( rootDir, 'packages/cli/src/i18n/locales/en.json', '{"hello":"world"}\n', ); writeFile( rootDir, 'packages/audio-capture/package.json', JSON.stringify( { name: '@qwen-code/audio-capture', version: '0.17.0', type: 'module', main: 'dist/index.js', dependencies: { 'node-gyp-build': '^4.8.4', }, scripts: { install: 'node install.js', }, devDependencies: { typescript: '^5.3.3', }, }, null, 2, ), ); writeFile(rootDir, 'packages/audio-capture/dist/index.js', ''); writeFile( rootDir, 'packages/audio-capture/dist/index.test.js', 'throw new Error("should not copy tests");\n', ); writeFile( rootDir, 'packages/audio-capture/dist/index.spec.js', 'throw new Error("should not copy specs");\n', ); writeFile( rootDir, 'packages/audio-capture/prebuilds/darwin-arm64/@qwen-code+audio-capture.node', 'fake native addon\n', ); writeFile( rootDir, 'packages/audio-capture/prebuilds/darwin-arm64/debug.log', 'should not ship\n', ); writeFile( rootDir, 'packages/audio-capture/node_modules/node-gyp-build/package.json', '{"name":"node-gyp-build","version":"4.8.4"}\n', ); writeFile( rootDir, 'packages/audio-capture/node_modules/node-gyp-build/index.js', '', ); for (const template of [ 'agent', 'commands', 'context', 'mcp-server', 'skills', ]) { writeFile( rootDir, `packages/cli/src/commands/extensions/examples/${template}/package.json`, '{}\n', ); } return rootDir; } function createBundleArtifacts(rootDir) { writeFile(rootDir, 'dist/cli.js', ''); mkdirSync(path.join(rootDir, 'dist', 'vendor'), { recursive: true }); mkdirSync(path.join(rootDir, 'dist', 'bundled', 'qc-helper', 'docs'), { recursive: true, }); // Web Shell release gate (prepare-package.js verifyBundleArtifacts): the // published package must ship the UI, so the fixture provides it too. writeFile(rootDir, 'dist/web-shell/index.html', ''); mkdirSync(path.join(rootDir, 'dist', 'web-shell', 'assets'), { recursive: true, }); } function writeFile(rootDir, relativePath, content) { const filePath = path.join(rootDir, relativePath); mkdirSync(path.dirname(filePath), { recursive: true }); writeFileSync(filePath, content); } function stubConsole() { vi.spyOn(console, 'log').mockImplementation(() => {}); vi.spyOn(console, 'warn').mockImplementation(() => {}); } });