mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-10-31 19:52:43 +00:00
* fix(cli): restore bounded OpenTUI dialogs and add migration gates
* fix(cli): preserve runtime boundaries during standalone updates
Preserve Node/Bun archive flavor and validate actual CLI loading, package
identity, version and prepared installation identity before activation.
Keep Node npm transport and restrict Bun registry lookup to trusted config.
Integrates chiga0's update stack through ce67edaba5,
with isolated fixtures and real Node/Bun runtime regression coverage.
Co-authored-by: gary.gq <gary.gq@alibaba-inc.com>
* fix(cli): preserve OpenTUI input and attachment ownership
* test(cli): bound cancellation test waits and cleanup
* feat(cli): ship private Bun for opt-in npm OpenTUI sessions
* feat(cli): align OpenTUI status lines with Ink
* fix(cli): align OpenTUI editor and cancellation transactions
* fix(tui): harden migration cleanup and evidence after review
* test(tui): fail acceptance on unhandled errors
* fix(cli): validate standalone launchers and registry settings
* docs(cli): clarify consolidated OpenTUI delivery status
* test(cli): isolate system settings in TUI parity fixtures
* test(cli): isolate compile cache in standalone runtime checks
* test(cli): reduce standalone runtime fixture compression cost
* fix(cli): settle OpenTUI cancellation when replay resets a session
* feat(cli): align OpenTUI Vim, history and session output
Reuse the shared editing and history contracts, preserve cancellation ownership, and connect accepted interactive turns to the existing output bridge.
* style(cli): format merged OpenTUI test callbacks
* test(core): control terminal conversion in PTY parking regression
* test(cli): preserve visible input across session rotation
* test(cli): cover shell recall provenance and streaming search selection
* test(cli): await cancellation before releasing held response
* fix(cli): preserve imported history ownership and active search
* docs(cli): synchronize consolidated OpenTUI validation status
* test(cli): await AppContainer configuration cleanup
* test(cli): mock Vim clipboard imports before module loading
* test(cli): check standalone asset roots for npm subpaths
* test(cli): share clipboard mocks across module export views
* test(cli): verify finalized MOD parts and shared JSON projection
* fix(cli): reuse Ink editing history for OpenTUI Unicode input
* test(cli): keep native Vim input callbacks void
* test(cli): preserve Escape boundaries in Unicode Vim fixtures
* test(cli): read the current composer across Vim remounts
* test(cli): align Unicode deletion cursor with Ink oracle
* fix(cli): preserve completion and escape precedence and validate status colors
* fix(cli): use boundary selection for the OpenTUI composer
* fix(cli): preserve attachment navigation with one completion
* test(cli): preserve Shift in the native redo key sequence
* fix(cli): display session titles in the OpenTUI composer border
* fix(cli): discard generated command work after cancellation
* test(cli): align Unicode unit cursor with Ink oracle
* test(cli): model native selection key delegation
* fix(cli): preserve Vim Escape ownership across listeners
* fix(cli): preserve shared Vim command messages in OpenTUI
* test(cli): observe rendered Vim mode during parity checks
* docs(cli): record integrated OpenTUI validation checkpoint
* fix(ci): account for dedicated TUI parity coverage
* fix(cli): preserve collapsed paste payloads across Vim undo
* docs(cli): refresh OpenTUI distribution acceptance status
---------
Co-authored-by: gary.gq <gary.gq@alibaba-inc.com>
1889 lines
62 KiB
JavaScript
1889 lines
62 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
utimesSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
truncateSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { pathToFileURL } from 'node:url';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import {
|
|
copyBundleAssets,
|
|
reviewSourceDigestForBuild,
|
|
} from '../copy_bundle_assets.js';
|
|
import { copyFiles } from '../copy_files.js';
|
|
import { preparePackage } from '../prepare-package.js';
|
|
import {
|
|
NPM_BUN_OPTIONAL_DEPENDENCIES,
|
|
NPM_OPENTUI_NATIVE_ASSETS,
|
|
OPENTUI_COMMON_ASSETS,
|
|
} from '../opentui-package-assets.js';
|
|
import { arePermissionBitsEnforced } from '../../packages/cli/src/test-utils/permission-bits.js';
|
|
|
|
const realReadFileSync = fs.readFileSync;
|
|
const realReaddirSync = fs.readdirSync;
|
|
const realStatSync = fs.statSync;
|
|
const realRmSync = fs.rmSync;
|
|
|
|
function reportsMissingArtifact(message, missingArtifact, pathFlavor = path) {
|
|
const normalized = String(message).split(pathFlavor.sep).join('/');
|
|
return (
|
|
normalized.includes('Required package artifact not found') &&
|
|
normalized.includes(missingArtifact)
|
|
);
|
|
}
|
|
|
|
describe('package asset scripts', () => {
|
|
const tempDirs = [];
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
for (const tempDir of tempDirs.splice(0)) {
|
|
rmSync(tempDir, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it('copies the manifest and service worker into the published shell', () => {
|
|
const rootDir = createFixtureRoot();
|
|
stubConsole();
|
|
writeFile(rootDir, 'packages/web-shell/dist/index.html', '<!doctype html>');
|
|
writeFile(
|
|
rootDir,
|
|
'packages/web-shell/dist/assets/index-abc.js',
|
|
'export {};',
|
|
);
|
|
writeFile(
|
|
rootDir,
|
|
'packages/web-shell/dist/manifest.webmanifest',
|
|
'{"name":"Qwen Code"}',
|
|
);
|
|
writeFile(
|
|
rootDir,
|
|
'packages/web-shell/dist/sw.js',
|
|
'self.addEventListener("fetch", () => {});',
|
|
);
|
|
copyBundleAssets({ root: rootDir });
|
|
for (const file of ['manifest.webmanifest', 'sw.js']) {
|
|
expect(
|
|
readFileSync(path.join(rootDir, 'dist/web-shell', file), 'utf8'),
|
|
).toBe(
|
|
readFileSync(
|
|
path.join(rootDir, 'packages/web-shell/dist', file),
|
|
'utf8',
|
|
),
|
|
);
|
|
}
|
|
});
|
|
|
|
it('warns and skips a missing service worker from a stale shell build', () => {
|
|
const rootDir = createFixtureRoot();
|
|
stubConsole();
|
|
writeFile(rootDir, 'packages/web-shell/dist/index.html', '<!doctype html>');
|
|
writeFile(
|
|
rootDir,
|
|
'packages/web-shell/dist/assets/index-abc.js',
|
|
'export {};',
|
|
);
|
|
writeFile(
|
|
rootDir,
|
|
'packages/web-shell/dist/manifest.webmanifest',
|
|
'{"name":"Qwen Code"}',
|
|
);
|
|
|
|
expect(() => copyBundleAssets({ root: rootDir })).not.toThrow();
|
|
|
|
expect(
|
|
readFileSync(
|
|
path.join(rootDir, 'dist/web-shell/manifest.webmanifest'),
|
|
'utf8',
|
|
),
|
|
).toBe('{"name":"Qwen Code"}');
|
|
expect(existsSync(path.join(rootDir, 'dist/web-shell/sw.js'))).toBe(false);
|
|
expect(
|
|
console.warn.mock.calls
|
|
.map(([message]) => String(message))
|
|
.some(
|
|
(message) =>
|
|
message.includes('PWA asset not found') &&
|
|
message.includes(
|
|
path.join(rootDir, 'packages/web-shell/dist/sw.js'),
|
|
),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('emits an executable dist/cli.js — shebang plus the exec bit, once', () => {
|
|
// shellContextEnv blanks a QWEN_CODE_CLI a POSIX shell cannot exec (no
|
|
// shebang, or no exec bit), and `"${QWEN_CODE_CLI:-qwen}"` then silently
|
|
// runs whatever `qwen` the PATH resolves — a different install for every
|
|
// review subcommand of a session launched off the bundle (measured on
|
|
// three live runs). Deleting the block, dropping the chmod, or dropping
|
|
// the already-has-a-shebang guard must not stay green.
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(rootDir, 'dist/cli.js', 'console.log("bundle");\n');
|
|
const cliEntry = path.join(rootDir, 'dist', 'cli.js');
|
|
// A build time in the past, so the rewrite's own clock cannot coincide
|
|
// with it: the digest stamp reads this mtime as "when the bundle was
|
|
// built", and a rewrite that bumps it certifies a bundle as newer than
|
|
// review sources edited before it — the staleness warning then never
|
|
// fires and a review silently measures the old behaviour.
|
|
const builtAt = new Date(Date.now() - 60_000);
|
|
utimesSync(cliEntry, builtAt, builtAt);
|
|
// Read back what the filesystem actually recorded. Comparing against the
|
|
// `Date` handed to `utimesSync` would re-pin libuv's double-seconds →
|
|
// `timespec` truncation (about half of all millisecond values land 1 ns
|
|
// low), which is not the invariant under test — only whether the rewrite
|
|
// moves the time the filesystem holds.
|
|
const builtMs = fs.statSync(cliEntry).mtimeMs;
|
|
stubConsole();
|
|
|
|
copyBundleAssets({ root: rootDir });
|
|
|
|
const once = readFileSync(cliEntry, 'utf8');
|
|
expect(once.startsWith('#!/usr/bin/env node\n')).toBe(true);
|
|
expect(once).toContain('console.log("bundle");');
|
|
expect(fs.statSync(cliEntry).mtimeMs).toBe(builtMs);
|
|
if (process.platform !== 'win32') {
|
|
// Windows has no POSIX exec bit for chmod to set; the shebang half
|
|
// still holds there.
|
|
expect(fs.statSync(cliEntry).mode & 0o777).toBe(0o755);
|
|
}
|
|
|
|
// Re-running the bundle step must not stack a second shebang — and must
|
|
// still set the exec bit on a file that already carries one (an entry
|
|
// arriving with a shebang but mode 0644 is exactly the shape that blanks
|
|
// QWEN_CODE_CLI; demoting the chmod inside the shebang guard must fail).
|
|
if (process.platform !== 'win32') {
|
|
fs.chmodSync(cliEntry, 0o644);
|
|
}
|
|
copyBundleAssets({ root: rootDir });
|
|
expect(readFileSync(cliEntry, 'utf8')).toBe(once);
|
|
if (process.platform !== 'win32') {
|
|
expect(fs.statSync(cliEntry).mode & 0o777).toBe(0o755);
|
|
}
|
|
});
|
|
|
|
it('stamps the review source digest into dist', () => {
|
|
// Nothing gated this call site: removing it left the whole scripts suite
|
|
// green while `npm run bundle` silently stopped writing the stamp — and a
|
|
// missing stamp is `unmeasured`, so the staleness warning would never fire
|
|
// again with nothing in CI to notice.
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(
|
|
rootDir,
|
|
'packages/cli/src/commands/review/drive.ts',
|
|
'export const drive = 1;\n',
|
|
);
|
|
writeFile(rootDir, 'packages/cli/src/commands/review.ts', 'registers\n');
|
|
// The stamp attests to a bundle, so there has to be one, and it has to be
|
|
// at least as new as the sources it claims to describe.
|
|
writeFile(rootDir, 'dist/cli.js', 'the bundle\n');
|
|
// Force the boundary itself: the refusal is the strict
|
|
// `newestSource > builtAt`, so EQUAL mtimes must still stamp. Without a
|
|
// forced equality, whether a `>=` mutant fails depends on the runner's
|
|
// clock granularity — the non-determinism this removes.
|
|
const builtAt = new Date();
|
|
for (const rel of [
|
|
'packages/cli/src/commands/review/drive.ts',
|
|
'packages/cli/src/commands/review.ts',
|
|
'dist/cli.js',
|
|
]) {
|
|
utimesSync(path.join(rootDir, rel), builtAt, builtAt);
|
|
}
|
|
stubConsole();
|
|
|
|
copyBundleAssets({ root: rootDir });
|
|
|
|
const stamped = readFileSync(
|
|
path.join(rootDir, 'dist', 'review-sources.sha256'),
|
|
'utf8',
|
|
);
|
|
expect(stamped).toBe(reviewSourceDigestForBuild(rootDir).digest);
|
|
expect(stamped).toMatch(/^[0-9a-f]{64}$/);
|
|
});
|
|
|
|
it('refuses to stamp a bundle older than the sources it would describe', () => {
|
|
// The copier runs after esbuild, so a source edited in between — or this
|
|
// script run on its own — would certify a `cli.js` built from something
|
|
// else. That is the one direction where a silent check is affirmatively
|
|
// wrong rather than uninformative.
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(rootDir, 'dist/cli.js', 'the bundle\n');
|
|
writeFile(
|
|
rootDir,
|
|
'packages/cli/src/commands/review/drive.ts',
|
|
'export const drive = 1;\n',
|
|
);
|
|
const later = new Date(Date.now() + 3_600_000);
|
|
utimesSync(
|
|
path.join(rootDir, 'packages/cli/src/commands/review/drive.ts'),
|
|
later,
|
|
later,
|
|
);
|
|
stubConsole();
|
|
|
|
copyBundleAssets({ root: rootDir });
|
|
|
|
expect(
|
|
existsSync(path.join(rootDir, 'dist', 'review-sources.sha256')),
|
|
).toBe(false);
|
|
// `refuse()` warns because the runtime notice sends its reader back to
|
|
// this build's output; deleting the warn kept this whole suite green.
|
|
expect(console.warn).toHaveBeenCalledWith(
|
|
expect.stringContaining('newer than'),
|
|
);
|
|
});
|
|
|
|
it('removes an older stamp when it refuses to write a new one', () => {
|
|
// Leaving a previous attestation beside a newer bundle is a weaker form of
|
|
// the certifying the refusal exists to avoid.
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(rootDir, 'dist/cli.js', 'the bundle\n');
|
|
writeFile(rootDir, 'dist/review-sources.sha256', 'a'.repeat(64));
|
|
writeFile(
|
|
rootDir,
|
|
'packages/cli/src/commands/review/drive.ts',
|
|
'export const drive = 1;\n',
|
|
);
|
|
const later = new Date(Date.now() + 3_600_000);
|
|
utimesSync(
|
|
path.join(rootDir, 'packages/cli/src/commands/review/drive.ts'),
|
|
later,
|
|
later,
|
|
);
|
|
stubConsole();
|
|
|
|
copyBundleAssets({ root: rootDir });
|
|
|
|
expect(
|
|
existsSync(path.join(rootDir, 'dist', 'review-sources.sha256')),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('keeps refusing by name when the old stamp cannot be removed', () => {
|
|
// `force` swallows only ENOENT; a permission or lock error on the old
|
|
// stamp (an antivirus holding a just-written file, a stamp owned by
|
|
// another user on a shared volume) must not turn the named refusal into
|
|
// an unhandled crash after every asset is already in place. Leaving the
|
|
// unremovable stamp is the lesser outcome: at worst a later "stale"
|
|
// notice whose rebuild advice is still correct.
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(rootDir, 'dist/cli.js', 'the bundle\n');
|
|
writeFile(rootDir, 'dist/review-sources.sha256', 'a'.repeat(64));
|
|
writeFile(
|
|
rootDir,
|
|
'packages/cli/src/commands/review/drive.ts',
|
|
'export const drive = 1;\n',
|
|
);
|
|
const later = new Date(Date.now() + 3_600_000);
|
|
utimesSync(
|
|
path.join(rootDir, 'packages/cli/src/commands/review/drive.ts'),
|
|
later,
|
|
later,
|
|
);
|
|
stubConsole();
|
|
const stampPath = path.join(rootDir, 'dist', 'review-sources.sha256');
|
|
vi.spyOn(fs, 'rmSync').mockImplementation((target, ...rest) => {
|
|
if (String(target) === stampPath) {
|
|
throw Object.assign(new Error('EACCES: permission denied'), {
|
|
code: 'EACCES',
|
|
});
|
|
}
|
|
return realRmSync(target, ...rest);
|
|
});
|
|
|
|
expect(() => copyBundleAssets({ root: rootDir })).not.toThrow();
|
|
expect(existsSync(stampPath)).toBe(true);
|
|
expect(console.warn).toHaveBeenCalledWith(
|
|
expect.stringContaining('Could not remove the stale source digest'),
|
|
);
|
|
expect(console.warn).toHaveBeenCalledWith(
|
|
expect.stringContaining('newer than'),
|
|
);
|
|
});
|
|
|
|
it('does not stamp a tree with no review sources', () => {
|
|
// The zero-files branch of the digest: with nothing to hash there is no
|
|
// digest to attest. The stamping cases pin the other side of the guard —
|
|
// that a tree WITH sources is stamped — and this pins the side that
|
|
// fires, so a guard that never fired would certify the empty-set digest
|
|
// of a bundle nothing digested was built from.
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(rootDir, 'dist/cli.js', 'the bundle\n');
|
|
// Every refusal removes an existing stamp first; pre-seed one to pin that.
|
|
writeFile(rootDir, 'dist/review-sources.sha256', 'a'.repeat(64));
|
|
stubConsole();
|
|
|
|
copyBundleAssets({ root: rootDir });
|
|
|
|
expect(reviewSourceDigestForBuild(rootDir).digest).toBeUndefined();
|
|
expect(
|
|
existsSync(path.join(rootDir, 'dist', 'review-sources.sha256')),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('refuses by the newest source, not the last one walked', () => {
|
|
// `newest` must track the largest mtimeMs. The newest file sits in the
|
|
// MIDDLE of the sort order, where both a keep-first and a keep-last
|
|
// comparison miss it; either would see only an older file and certify a
|
|
// bundle the newest source may not describe.
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(rootDir, 'dist/cli.js', 'the bundle\n');
|
|
for (const name of ['a-older.ts', 'm-newest.ts', 'z-older.ts']) {
|
|
writeFile(
|
|
rootDir,
|
|
`packages/cli/src/commands/review/${name}`,
|
|
`export const ${name[0]} = 1;\n`,
|
|
);
|
|
}
|
|
const earlier = new Date(Date.now() - 3_600_000);
|
|
for (const name of ['a-older.ts', 'z-older.ts']) {
|
|
utimesSync(
|
|
path.join(rootDir, 'packages/cli/src/commands/review', name),
|
|
earlier,
|
|
earlier,
|
|
);
|
|
}
|
|
const later = new Date(Date.now() + 3_600_000);
|
|
utimesSync(
|
|
path.join(rootDir, 'packages/cli/src/commands/review/m-newest.ts'),
|
|
later,
|
|
later,
|
|
);
|
|
// The field directly, not only its consequence: keep-first and keep-last
|
|
// mutants stamp by an older file here — the no-stamp assertion below is
|
|
// what catches them. The complementary class tracks the right mtime but
|
|
// attributes it to the wrong path: it still refuses, by the wrong file,
|
|
// and only this field assertion catches it.
|
|
expect(reviewSourceDigestForBuild(rootDir).newest?.file).toBe(
|
|
path.join(rootDir, 'packages/cli/src/commands/review/m-newest.ts'),
|
|
);
|
|
stubConsole();
|
|
|
|
copyBundleAssets({ root: rootDir });
|
|
|
|
expect(
|
|
existsSync(path.join(rootDir, 'dist', 'review-sources.sha256')),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('does not fail the bundle when the review sources cannot be read', () => {
|
|
// The stamp is the copier's last step, so throwing here would fail a build
|
|
// whose every asset is already in place. A missing stamp is `unmeasured`,
|
|
// which the runtime check already accepts.
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(
|
|
rootDir,
|
|
'packages/cli/src/commands/review/drive.ts',
|
|
'export const drive = 1;\n',
|
|
);
|
|
// The bundle LAST, so the sources are strictly older: the stamp's absence
|
|
// is then attributable only to the error refusal, where a bundle written
|
|
// first lets the mtime gate mask a skip-and-continue mutant.
|
|
writeFile(rootDir, 'dist/cli.js', 'the bundle\n');
|
|
// Every refusal removes an existing stamp first; pre-seed one to pin that.
|
|
writeFile(rootDir, 'dist/review-sources.sha256', 'a'.repeat(64));
|
|
stubConsole();
|
|
// The file vanishes between the walk that listed it and the read that
|
|
// hashes it — a concurrent checkout, mid-build.
|
|
vi.spyOn(fs, 'readFileSync').mockImplementation((target, ...rest) => {
|
|
if (String(target).endsWith('drive.ts')) {
|
|
throw Object.assign(new Error('ENOENT: vanished mid-build'), {
|
|
code: 'ENOENT',
|
|
});
|
|
}
|
|
return realReadFileSync(target, ...rest);
|
|
});
|
|
|
|
expect(() => copyBundleAssets({ root: rootDir })).not.toThrow();
|
|
expect(
|
|
existsSync(path.join(rootDir, 'dist', 'review-sources.sha256')),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('does not fail the bundle when a source directory cannot be listed', () => {
|
|
// The directory-level twin of the case above: the walk throws instead of
|
|
// silently hashing the survivors, and the refusal must stay a refusal —
|
|
// never a failed bundle with every asset already in place.
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(
|
|
rootDir,
|
|
'packages/cli/src/commands/review/drive.ts',
|
|
'export const drive = 1;\n',
|
|
);
|
|
writeFile(
|
|
rootDir,
|
|
'packages/cli/src/commands/review/lib/ledger.ts',
|
|
'export const ledger = 1;\n',
|
|
);
|
|
// The bundle LAST, so the stamp's absence is attributable only to the
|
|
// error refusal, not the mtime gate (see the unreadable-file twin).
|
|
writeFile(rootDir, 'dist/cli.js', 'the bundle\n');
|
|
// Every refusal removes an existing stamp first; pre-seed one to pin that.
|
|
writeFile(rootDir, 'dist/review-sources.sha256', 'a'.repeat(64));
|
|
stubConsole();
|
|
const libDir = path.join(rootDir, 'packages/cli/src/commands/review/lib');
|
|
vi.spyOn(fs, 'readdirSync').mockImplementation((target, ...rest) => {
|
|
if (String(target) === libDir) {
|
|
throw Object.assign(new Error('EACCES: permission denied'), {
|
|
code: 'EACCES',
|
|
});
|
|
}
|
|
return realReaddirSync(target, ...rest);
|
|
});
|
|
|
|
expect(() => copyBundleAssets({ root: rootDir })).not.toThrow();
|
|
expect(
|
|
existsSync(path.join(rootDir, 'dist', 'review-sources.sha256')),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('does not fail the bundle when a directory vanishes mid-walk', () => {
|
|
// The parent walk listed this as a directory a moment ago; now both the
|
|
// listing and the stat fail. Skipping it would stamp a digest over the
|
|
// survivors, and every review after the tree settles would report stale
|
|
// against a byte-correct bundle — so the walk refuses instead, and the
|
|
// refusal must stay never-fatal.
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(
|
|
rootDir,
|
|
'packages/cli/src/commands/review/drive.ts',
|
|
'export const drive = 1;\n',
|
|
);
|
|
writeFile(
|
|
rootDir,
|
|
'packages/cli/src/commands/review/lib/ledger.ts',
|
|
'export const ledger = 1;\n',
|
|
);
|
|
// The bundle LAST so the mtime gate cannot mask a skip-and-continue
|
|
// mutant digesting the survivors.
|
|
writeFile(rootDir, 'dist/cli.js', 'the bundle\n');
|
|
// Every refusal removes an existing stamp first; pre-seed one to pin that.
|
|
writeFile(rootDir, 'dist/review-sources.sha256', 'a'.repeat(64));
|
|
stubConsole();
|
|
const libDir = path.join(rootDir, 'packages/cli/src/commands/review/lib');
|
|
const vanished = Object.assign(new Error('ENOENT: vanished mid-build'), {
|
|
code: 'ENOENT',
|
|
});
|
|
vi.spyOn(fs, 'readdirSync').mockImplementation((target, ...rest) => {
|
|
if (String(target) === libDir) throw vanished;
|
|
return realReaddirSync(target, ...rest);
|
|
});
|
|
vi.spyOn(fs, 'statSync').mockImplementation((target, ...rest) => {
|
|
if (String(target) === libDir) throw vanished;
|
|
return realStatSync(target, ...rest);
|
|
});
|
|
|
|
expect(() => copyBundleAssets({ root: rootDir })).not.toThrow();
|
|
expect(
|
|
existsSync(path.join(rootDir, 'dist', 'review-sources.sha256')),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('does not fail the bundle when a digest root cannot be measured', () => {
|
|
// Root-level twin of the unlistable-directory case, for the failure the
|
|
// `listed` flag cannot cover: both the listing and the stat fail with a
|
|
// non-ENOENT error (an ancestor transiently without search permission).
|
|
// Skipping the root would stamp a digest over the surviving roots — the
|
|
// runtime twin (`sourceFilesUnder`) marks the same case incomplete, so a
|
|
// skip here is a parity gap that would accuse a byte-for-byte correct
|
|
// bundle once the tree becomes readable again. `review.ts` is the
|
|
// survivor OUTSIDE the failing root: without it a skip mutant finds one
|
|
// file fewer and the partial digest is harder to tell from a refusal.
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(
|
|
rootDir,
|
|
'packages/cli/src/commands/review/drive.ts',
|
|
'export const drive = 1;\n',
|
|
);
|
|
writeFile(rootDir, 'packages/cli/src/commands/review.ts', 'registers\n');
|
|
writeFile(rootDir, 'dist/cli.js', 'the bundle\n');
|
|
// Every refusal removes an existing stamp first; pre-seed one to pin that.
|
|
writeFile(rootDir, 'dist/review-sources.sha256', 'a'.repeat(64));
|
|
stubConsole();
|
|
const reviewDir = path.join(rootDir, 'packages/cli/src/commands/review');
|
|
const denied = Object.assign(new Error('EACCES: permission denied'), {
|
|
code: 'EACCES',
|
|
});
|
|
vi.spyOn(fs, 'readdirSync').mockImplementation((target, ...rest) => {
|
|
if (String(target) === reviewDir) throw denied;
|
|
return realReaddirSync(target, ...rest);
|
|
});
|
|
vi.spyOn(fs, 'statSync').mockImplementation((target, ...rest) => {
|
|
if (String(target) === reviewDir) throw denied;
|
|
return realStatSync(target, ...rest);
|
|
});
|
|
|
|
expect(() => copyBundleAssets({ root: rootDir })).not.toThrow();
|
|
expect(
|
|
existsSync(path.join(rootDir, 'dist', 'review-sources.sha256')),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('does not stamp when there is no bundle to attest', () => {
|
|
// The stamp attests to a bundle; without `dist/cli.js` there is nothing
|
|
// to attest. An orphan digest beside a later-built bundle would report
|
|
// stale against sources that bundle may describe perfectly, instead of
|
|
// the honest 'could not check'.
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(
|
|
rootDir,
|
|
'packages/cli/src/commands/review/drive.ts',
|
|
'export const drive = 1;\n',
|
|
);
|
|
// Every refusal removes an existing stamp first; pre-seed one to pin that.
|
|
writeFile(rootDir, 'dist/review-sources.sha256', 'a'.repeat(64));
|
|
stubConsole();
|
|
|
|
expect(() => copyBundleAssets({ root: rootDir })).not.toThrow();
|
|
expect(
|
|
existsSync(path.join(rootDir, 'dist', 'review-sources.sha256')),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('does not fail the bundle when the stamp cannot be written', () => {
|
|
// The final write sits under the same never-fatal contract as the digest
|
|
// walk: a stray directory occupying the stamp path makes writeFileSync
|
|
// throw EISDIR after every asset is already in place.
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(
|
|
rootDir,
|
|
'packages/cli/src/commands/review/drive.ts',
|
|
'export const drive = 1;\n',
|
|
);
|
|
writeFile(rootDir, 'dist/cli.js', 'the bundle\n');
|
|
mkdirSync(path.join(rootDir, 'dist', 'review-sources.sha256'), {
|
|
recursive: true,
|
|
});
|
|
stubConsole();
|
|
|
|
expect(() => copyBundleAssets({ root: rootDir })).not.toThrow();
|
|
expect(
|
|
existsSync(path.join(rootDir, 'dist', 'review-sources.sha256')),
|
|
).toBe(false);
|
|
});
|
|
|
|
// chmod is the only lever this case has: on Windows it is a no-op, and
|
|
// root with its DAC capabilities reads through it, so the branch under
|
|
// test is unreachable there. The case skips rather than running into the
|
|
// other branch — a readable tree, stamped as usual — and failing red
|
|
// against the no-stamp assertion.
|
|
it.skipIf(!arePermissionBitsEnforced())(
|
|
'refuses when a digest root exists but cannot be listed',
|
|
() => {
|
|
// The root-level arm of the unlistable-directory refusal: with the
|
|
// ROOT itself unreadable, skipping it would stamp a partial digest as
|
|
// the truth, and every review after the tree becomes readable would
|
|
// report stale against a bundle the full sources do not describe.
|
|
// `review.ts` is the survivor OUTSIDE the unreadable root: without it
|
|
// a skip-and-continue mutant finds zero files, refuses for the
|
|
// no-sources reason, and passes this test anyway.
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(
|
|
rootDir,
|
|
'packages/cli/src/commands/review/drive.ts',
|
|
'export const drive = 1;\n',
|
|
);
|
|
writeFile(rootDir, 'packages/cli/src/commands/review.ts', 'registers\n');
|
|
writeFile(rootDir, 'dist/cli.js', 'the bundle\n');
|
|
// Every refusal removes an existing stamp first; pre-seed one to pin that.
|
|
writeFile(rootDir, 'dist/review-sources.sha256', 'a'.repeat(64));
|
|
const reviewDir = path.join(rootDir, 'packages/cli/src/commands/review');
|
|
stubConsole();
|
|
fs.chmodSync(reviewDir, 0o000);
|
|
try {
|
|
expect(() => copyBundleAssets({ root: rootDir })).not.toThrow();
|
|
expect(
|
|
existsSync(path.join(rootDir, 'dist', 'review-sources.sha256')),
|
|
).toBe(false);
|
|
} finally {
|
|
fs.chmodSync(reviewDir, 0o755);
|
|
}
|
|
},
|
|
);
|
|
|
|
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 the HTML export renderer into the published package', () => {
|
|
const rootDir = createFixtureRoot();
|
|
createBundleArtifacts(rootDir);
|
|
writeFile(
|
|
rootDir,
|
|
'packages/web-templates/src/export-html/dist/export-transcript-document.js',
|
|
'window.QwenExportRenderer = true;',
|
|
);
|
|
writeFile(
|
|
rootDir,
|
|
'packages/web-templates/src/export-html/dist/export-transcript-document.css',
|
|
'body{color:red}',
|
|
);
|
|
stubConsole();
|
|
|
|
copyBundleAssets({ root: rootDir });
|
|
createOpenTuiArtifacts(rootDir);
|
|
preparePackage({ rootDir, requireNativeAudioCapture: false });
|
|
|
|
expect(
|
|
readFileSync(
|
|
path.join(rootDir, 'dist', 'export-transcript-document.js'),
|
|
'utf8',
|
|
),
|
|
).toBe('window.QwenExportRenderer = true;');
|
|
expect(
|
|
readFileSync(
|
|
path.join(rootDir, 'dist', 'export-transcript-document.css'),
|
|
'utf8',
|
|
),
|
|
).toBe('body{color:red}');
|
|
const distPackageJson = JSON.parse(
|
|
readFileSync(path.join(rootDir, 'dist', 'package.json'), 'utf8'),
|
|
);
|
|
expect(distPackageJson.files).toContain('export-transcript-document.js');
|
|
expect(distPackageJson.files).toContain('export-transcript-document.css');
|
|
expect(distPackageJson.files).toContain('execution-worker.js');
|
|
expect(distPackageJson.files).toContain('mem0');
|
|
expect(distPackageJson.files).toContain('generated-ui');
|
|
});
|
|
|
|
it('names the missing stylesheet when only the renderer JS was built', () => {
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(
|
|
rootDir,
|
|
'packages/web-templates/src/export-html/dist/export-transcript-document.js',
|
|
'window.QwenExportRenderer = true;',
|
|
);
|
|
stubConsole();
|
|
|
|
copyBundleAssets({ root: rootDir });
|
|
|
|
const warning = console.warn.mock.calls
|
|
.map(([message]) => String(message))
|
|
.find((message) => message.includes('HTML export renderer assets'));
|
|
// The reachable way into that branch with the JS present is a stale
|
|
// web-templates build, so the warning has to name the CSS that is actually
|
|
// missing rather than send the operator looking for a JS file that exists.
|
|
expect(warning).toContain(
|
|
path.join(
|
|
rootDir,
|
|
'packages',
|
|
'web-templates',
|
|
'src',
|
|
'export-html',
|
|
'dist',
|
|
'export-transcript-document.css',
|
|
),
|
|
);
|
|
expect(warning).not.toContain('export-transcript-document.js');
|
|
// All-or-nothing stays: prepare-package.js requires both artifacts, so the
|
|
// renderer JS that *was* found must not be half-published into dist/.
|
|
expect(
|
|
existsSync(path.join(rootDir, 'dist', 'export-transcript-document.js')),
|
|
).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
'execution-worker.js',
|
|
'mem0/main.js',
|
|
'mem0/write-confirmation.js',
|
|
'generated-ui/main.js',
|
|
'generated-ui/app.html',
|
|
'generated-ui/skills/generated-ui/SKILL.md',
|
|
'generated-ui/THIRD_PARTY_NOTICES.txt',
|
|
'sandboxBwrapRelay.js',
|
|
'sandboxLandlockRelay.js',
|
|
'sandboxFileWorker.js',
|
|
'export-transcript-document.css',
|
|
])('fails packaging when the published %s is missing', (missingArtifact) => {
|
|
const rootDir = createFixtureRoot();
|
|
createBundleArtifacts(rootDir);
|
|
rmSync(path.join(rootDir, 'dist', missingArtifact));
|
|
stubConsole();
|
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
// verifyBundleArtifacts reports with console.error + process.exit(1), not a
|
|
// throw, so the exit has to become one to keep the rest of the suite alive.
|
|
const exit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
throw new Error('process.exit(1)');
|
|
});
|
|
|
|
expect(() =>
|
|
preparePackage({ rootDir, requireNativeAudioCapture: false }),
|
|
).toThrow('process.exit(1)');
|
|
expect(exit).toHaveBeenCalledWith(1);
|
|
// verifyBundleArtifacts builds `requiredPath` with path.join, so on Windows
|
|
// the reported message carries `\`. The two multi-segment entries above
|
|
// (`mem0/main.js`, `mem0/write-confirmation.js`) are spelled with `/`, so
|
|
// normalise the separator before matching; otherwise they pass on POSIX and
|
|
// go red in the test_windows lane (which runs test:scripts).
|
|
expect(
|
|
console.error.mock.calls.some(([message]) =>
|
|
reportsMissingArtifact(message, missingArtifact),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it.each(['mem0/main.js', 'mem0/write-confirmation.js'])(
|
|
'recognizes a Windows missing-artifact error for %s',
|
|
(missingArtifact) => {
|
|
const message =
|
|
'Error: Required package artifact not found: ' +
|
|
path.win32.join('D:\\publish\\dist', ...missingArtifact.split('/'));
|
|
expect(reportsMissingArtifact(message, missingArtifact, path.win32)).toBe(
|
|
true,
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each(['manifest.webmanifest', 'sw.js'])(
|
|
'rejects a published shell missing %s',
|
|
(file) => {
|
|
const rootDir = createFixtureRoot();
|
|
createBundleArtifacts(rootDir);
|
|
rmSync(path.join(rootDir, 'dist', 'web-shell', file));
|
|
stubConsole();
|
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
// verifyBundleArtifacts reports with console.error + process.exit(1), not a
|
|
// throw, so the exit has to become one to keep the rest of the suite alive.
|
|
const exit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
throw new Error('process.exit(1)');
|
|
});
|
|
|
|
expect(() =>
|
|
preparePackage({ rootDir, requireNativeAudioCapture: false }),
|
|
).toThrow('process.exit(1)');
|
|
expect(exit).toHaveBeenCalledWith(1);
|
|
expect(
|
|
console.error.mock.calls
|
|
.map(([message]) => String(message))
|
|
.some(
|
|
(message) =>
|
|
message.includes('Required package artifact not found') &&
|
|
message.includes(file),
|
|
),
|
|
).toBe(true);
|
|
},
|
|
);
|
|
|
|
it('copies bundled skill references to both CLI and core distributions', () => {
|
|
const rootDir = createFixtureRoot();
|
|
const coreDir = path.join(rootDir, 'packages', 'core');
|
|
const resources = [
|
|
'SKILL.md',
|
|
'references/macos.md',
|
|
'references/windows-linux.md',
|
|
];
|
|
for (const resource of resources) {
|
|
writeFile(
|
|
rootDir,
|
|
`packages/core/src/skills/bundled/fixture-skill/${resource}`,
|
|
resource,
|
|
);
|
|
}
|
|
stubConsole();
|
|
copyBundleAssets({ root: rootDir });
|
|
copyFiles({ root: coreDir });
|
|
for (const resource of resources) {
|
|
expect(
|
|
readFileSync(
|
|
path.join(rootDir, 'dist/bundled/fixture-skill', resource),
|
|
'utf8',
|
|
),
|
|
).toBe(resource);
|
|
expect(
|
|
readFileSync(
|
|
path.join(coreDir, 'dist/src/skills/bundled/fixture-skill', resource),
|
|
'utf8',
|
|
),
|
|
).toBe(resource);
|
|
}
|
|
});
|
|
|
|
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,
|
|
'packages/core/src/skills/bundled/dataviz/DESIGN.md',
|
|
'# Design notes\n',
|
|
);
|
|
writeFile(rootDir, 'dist/bundled/dataviz/scripts/stale.test.js', 'stale\n');
|
|
writeFile(
|
|
rootDir,
|
|
'packages/core/src/skills/bundled/dataviz/.DS_Store',
|
|
'finder droppings\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);
|
|
expect(
|
|
existsSync(path.join(rootDir, 'dist', 'bundled', 'dataviz', 'DESIGN.md')),
|
|
).toBe(false);
|
|
// The copier's `.DS_Store` skip, pinned by the copier's own output: the
|
|
// boundary test in review-source-digest models the skip, so only this
|
|
// execution catches a copier that lost it.
|
|
expect(
|
|
existsSync(path.join(rootDir, 'dist', 'bundled', 'dataviz', '.DS_Store')),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('keeps bundled-skill DESIGN.md out of the per-package dist/src copy', () => {
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(
|
|
rootDir,
|
|
'packages/core/src/skills/bundled/review/SKILL.md',
|
|
'---\nname: review\ndescription: Review changes\n---\nBody\n',
|
|
);
|
|
writeFile(
|
|
rootDir,
|
|
'packages/core/src/skills/bundled/review/DESIGN.md',
|
|
'# Design notes\n',
|
|
);
|
|
writeFile(rootDir, 'packages/core/src/notes/DESIGN.md', '# Keep\n');
|
|
stubConsole();
|
|
|
|
copyFiles({ root: path.join(rootDir, 'packages', 'core') });
|
|
|
|
const distSrc = path.join(rootDir, 'packages', 'core', 'dist', 'src');
|
|
expect(
|
|
existsSync(path.join(distSrc, 'skills', 'bundled', 'review', 'SKILL.md')),
|
|
).toBe(true);
|
|
expect(
|
|
existsSync(
|
|
path.join(distSrc, 'skills', 'bundled', 'review', 'DESIGN.md'),
|
|
),
|
|
).toBe(false);
|
|
expect(existsSync(path.join(distSrc, 'notes', 'DESIGN.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.private).not.toBe(true);
|
|
expect(distPackageJson.bundledDependencies).toBeUndefined();
|
|
expect(distPackageJson.optionalDependencies).toMatchObject({
|
|
'@qwen-code/audio-capture': rootPackageJson.version,
|
|
...NPM_BUN_OPTIONAL_DEPENDENCIES,
|
|
});
|
|
|
|
expect(distPackageJson.optionalDependencies.sharp).toBe('0.35.4');
|
|
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('falls back to the hoisted sharp when core has no nested copy', () => {
|
|
const rootDir = createFixtureRoot();
|
|
rmSync(path.join(rootDir, 'packages/core/node_modules'), {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
writeFile(
|
|
rootDir,
|
|
'packages/core/package.json',
|
|
JSON.stringify(
|
|
{
|
|
name: '@qwen-code/qwen-code-core',
|
|
version: '0.17.0',
|
|
dependencies: {
|
|
sharp: '^0.35.0',
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
createBundleArtifacts(rootDir);
|
|
stubConsole();
|
|
|
|
preparePackage({ rootDir, requireNativeAudioCapture: false });
|
|
|
|
const distPackageJson = JSON.parse(
|
|
readFileSync(path.join(rootDir, 'dist', 'package.json'), 'utf8'),
|
|
);
|
|
expect(distPackageJson.optionalDependencies.sharp).toBe('0.35.3');
|
|
});
|
|
|
|
it('derives every published node-pty pin from the core manifest', () => {
|
|
const rootDir = createFixtureRoot();
|
|
const corePath = path.join(rootDir, 'packages/core/package.json');
|
|
const core = JSON.parse(readFileSync(corePath, 'utf8'));
|
|
const pins = Object.fromEntries(
|
|
Object.entries(
|
|
JSON.parse(
|
|
readFileSync(
|
|
new URL('../../packages/core/package.json', import.meta.url),
|
|
'utf8',
|
|
),
|
|
).optionalDependencies,
|
|
)
|
|
.filter(([name]) => name.startsWith('@lydell/node-pty'))
|
|
.map(([name]) => [name, '1.2.0-test-pin']),
|
|
);
|
|
// The pin *count* is owned by conpty-host.test.ts as a deliberate human
|
|
// re-check tripwire; here a non-empty guard keeps `toEqual(pins)` honest
|
|
// without duplicating a number that fails before the code under test runs.
|
|
expect(Object.keys(pins).length).toBeGreaterThan(0);
|
|
core.optionalDependencies = pins;
|
|
writeFileSync(corePath, JSON.stringify(core));
|
|
createBundleArtifacts(rootDir);
|
|
stubConsole();
|
|
|
|
preparePackage({ rootDir, requireNativeAudioCapture: false });
|
|
|
|
const published = JSON.parse(
|
|
readFileSync(path.join(rootDir, 'dist/package.json'), 'utf8'),
|
|
);
|
|
expect(
|
|
Object.fromEntries(
|
|
Object.entries(published.optionalDependencies).filter(([name]) =>
|
|
name.startsWith('@lydell/node-pty'),
|
|
),
|
|
),
|
|
).toEqual(pins);
|
|
});
|
|
|
|
it('rejects a locked sharp version outside the core declaration', () => {
|
|
const rootDir = createFixtureRoot();
|
|
writeFile(
|
|
rootDir,
|
|
'packages/core/package.json',
|
|
JSON.stringify({ dependencies: { sharp: '^0.34.0' } }),
|
|
);
|
|
createBundleArtifacts(rootDir);
|
|
stubConsole();
|
|
|
|
expect(() =>
|
|
preparePackage({ rootDir, requireNativeAudioCapture: false }),
|
|
).toThrow(/installed 0\.35\.4, packages\/core declares \^0\.34\.0/);
|
|
});
|
|
|
|
it('publishes a complete POSIX asset tree and leaves host-only dist assets intact', () => {
|
|
const rootDir = createFixtureRoot();
|
|
createBundleArtifacts(rootDir);
|
|
stubConsole();
|
|
const windowsAsset =
|
|
'dist/opentui-assets/@opentui/core-win32-x64/opentui.dll';
|
|
writeFile(rootDir, windowsAsset, 'windows-host-library');
|
|
preparePackage({ rootDir, requireNativeAudioCapture: false });
|
|
const published = JSON.parse(
|
|
readFileSync(path.join(rootDir, 'dist/package.json'), 'utf8'),
|
|
);
|
|
expect(published.files).toEqual(
|
|
expect.arrayContaining([
|
|
'opentui-assets/@opentui/core',
|
|
'opentui-assets/web-tree-sitter',
|
|
...Object.keys(NPM_OPENTUI_NATIVE_ASSETS).map(
|
|
(name) => `opentui-assets/${name}`,
|
|
),
|
|
]),
|
|
);
|
|
expect(published.files).not.toContain('opentui-assets');
|
|
expect(published.files).not.toContain(
|
|
'opentui-assets/@opentui/core-win32-x64',
|
|
);
|
|
expect(readFileSync(path.join(rootDir, windowsAsset), 'utf8')).toBe(
|
|
'windows-host-library',
|
|
);
|
|
expect(published.scripts).toBeUndefined();
|
|
});
|
|
|
|
it.each([
|
|
...OPENTUI_COMMON_ASSETS,
|
|
...Object.entries(NPM_OPENTUI_NATIVE_ASSETS).map(
|
|
([name, library]) => `${name}/${library}`,
|
|
),
|
|
])('rejects npm packaging without %s', (key) => {
|
|
const rootDir = createFixtureRoot();
|
|
createBundleArtifacts(rootDir);
|
|
stubConsole();
|
|
rmSync(path.join(rootDir, 'dist/opentui-assets', key));
|
|
expect(() =>
|
|
preparePackage({ rootDir, requireNativeAudioCapture: false }),
|
|
).toThrow(`Required npm OpenTUI asset not found: ${key}`);
|
|
});
|
|
|
|
it('prepares desktop metadata without universal assets or Bun dependencies', () => {
|
|
const rootDir = createFixtureRoot();
|
|
createBundleArtifacts(rootDir);
|
|
stubConsole();
|
|
rmSync(path.join(rootDir, 'dist/opentui-assets'), { recursive: true });
|
|
writeFile(
|
|
rootDir,
|
|
'dist/opentui-assets/@opentui/core-win32-x64/opentui.dll',
|
|
'host-native',
|
|
);
|
|
preparePackage({
|
|
rootDir,
|
|
requireNativeAudioCapture: false,
|
|
npmDistribution: false,
|
|
});
|
|
const published = JSON.parse(
|
|
readFileSync(path.join(rootDir, 'dist/package.json'), 'utf8'),
|
|
);
|
|
expect(
|
|
Object.keys(published.optionalDependencies).some((name) =>
|
|
name.startsWith('@oven/'),
|
|
),
|
|
).toBe(false);
|
|
expect(
|
|
readFileSync(
|
|
path.join(
|
|
rootDir,
|
|
'dist/opentui-assets/@opentui/core-win32-x64/opentui.dll',
|
|
),
|
|
'utf8',
|
|
),
|
|
).toBe('host-native');
|
|
expect(
|
|
existsSync(
|
|
path.join(rootDir, 'dist/opentui-assets/@opentui/core-linux-x64'),
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
['darwin-x64', false],
|
|
['win32-x64', false],
|
|
['', true],
|
|
['', false],
|
|
['unknown-x64', false],
|
|
])(
|
|
'keeps VSIX metadata and copied assets scoped to target=%s universal=%s',
|
|
(target, universal) => {
|
|
const rootDir = createFixtureRoot();
|
|
createBundleArtifacts(rootDir);
|
|
const extension = 'packages/vscode-ide-companion';
|
|
writeFile(rootDir, `${extension}/package.json`, '{"type":"module"}');
|
|
for (const script of ['prepackage.js', 'copy-bundled-cli.js']) {
|
|
writeFile(
|
|
rootDir,
|
|
`${extension}/scripts/${script}`,
|
|
readFileSync(
|
|
new URL(`../../${extension}/scripts/${script}`, import.meta.url),
|
|
'utf8',
|
|
),
|
|
);
|
|
}
|
|
writeFile(
|
|
rootDir,
|
|
'dist/opentui-assets/@opentui/core-win32-x64/opentui.dll',
|
|
'windows-native',
|
|
);
|
|
writeFile(
|
|
rootDir,
|
|
`${extension}/dist/qwen-cli/node_modules/existing-runtime/index.js`,
|
|
'keep-existing-dependency',
|
|
);
|
|
const preload = path.join(rootDir, 'prepackage-fixture.mjs');
|
|
const callsFile = path.join(rootDir, 'calls.jsonl');
|
|
const bundled = path.join(rootDir, extension, 'dist', 'qwen-cli');
|
|
// Execute the real orchestration, metadata builder, and copy script.
|
|
// Only npm build/install processes are replaced; no downloads occur.
|
|
writeFileSync(
|
|
preload,
|
|
`
|
|
import childProcess from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import { syncBuiltinESMExports } from 'node:module';
|
|
import { preparePackage } from ${JSON.stringify(new URL('../prepare-package.js', import.meta.url).href)};
|
|
const realSpawn = childProcess.spawnSync;
|
|
childProcess.spawnSync = (command, args, options) => {
|
|
if (command === 'node') {
|
|
return realSpawn(process.execPath, args, { ...options, shell: false });
|
|
}
|
|
if (args.includes('prepare:package')) {
|
|
preparePackage({
|
|
rootDir: ${JSON.stringify(rootDir)},
|
|
requireNativeAudioCapture: false,
|
|
npmDistribution: !args.includes('--host-runtime'),
|
|
});
|
|
}
|
|
const manifest = args.includes('install')
|
|
? JSON.parse(fs.readFileSync(${JSON.stringify(path.join(bundled, 'package.json'))}, 'utf8'))
|
|
: undefined;
|
|
fs.appendFileSync(${JSON.stringify(callsFile)}, JSON.stringify({ args, manifest }) + '\\n');
|
|
return { status: 0 };
|
|
};
|
|
syncBuiltinESMExports();
|
|
`,
|
|
);
|
|
const home = path.join(rootDir, 'home');
|
|
mkdirSync(home);
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[
|
|
'--import',
|
|
pathToFileURL(preload).href,
|
|
path.join(rootDir, extension, 'scripts', 'prepackage.js'),
|
|
],
|
|
{
|
|
cwd: rootDir,
|
|
encoding: 'utf8',
|
|
timeout: 30_000,
|
|
env: {
|
|
...process.env,
|
|
HOME: home,
|
|
USERPROFILE: home,
|
|
QWEN_HOME: path.join(home, '.qwen'),
|
|
npm_config_cache: path.join(rootDir, 'npm-cache'),
|
|
npm_config_prefix: path.join(rootDir, 'npm-prefix'),
|
|
NODE_OPTIONS: '',
|
|
UNIVERSAL_BUILD: String(universal),
|
|
VSCODE_TARGET: target,
|
|
},
|
|
},
|
|
);
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.status, result.stderr || result.stdout).toBe(0);
|
|
const calls = readFileSync(callsFile, 'utf8')
|
|
.trim()
|
|
.split('\n')
|
|
.map((line) => JSON.parse(line));
|
|
const install = calls.find(({ args }) => args.includes('install'));
|
|
expect(
|
|
Object.keys(install.manifest.optionalDependencies).some((name) =>
|
|
name.startsWith('@oven/'),
|
|
),
|
|
).toBe(false);
|
|
expect(install.manifest.optionalDependencies).toMatchObject({
|
|
'@qwen-code/audio-capture': '0.17.0',
|
|
'@teddyzhu/clipboard': '0.0.5',
|
|
sharp: '0.35.4',
|
|
});
|
|
expect(install.args.includes('--omit=optional')).toBe(universal);
|
|
expect(install.args).toContain('--omit=dev');
|
|
const scope = 'opentui-assets/@opentui';
|
|
const sourceEntries = readdirSync(
|
|
path.join(rootDir, 'dist', scope),
|
|
).sort();
|
|
const expectedEntries =
|
|
target === 'darwin-x64' || target === 'win32-x64'
|
|
? ['core', `core-${target}`]
|
|
: sourceEntries;
|
|
expect(readdirSync(path.join(bundled, scope)).sort()).toEqual(
|
|
expectedEntries,
|
|
);
|
|
expect(sourceEntries).toHaveLength(6);
|
|
for (const key of OPENTUI_COMMON_ASSETS) {
|
|
expect(
|
|
readFileSync(path.join(bundled, 'opentui-assets', key), 'utf8'),
|
|
).toBe(key);
|
|
}
|
|
expect(
|
|
readFileSync(
|
|
path.join(bundled, 'node_modules/existing-runtime/index.js'),
|
|
'utf8',
|
|
),
|
|
).toBe('keep-existing-dependency');
|
|
},
|
|
);
|
|
|
|
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', '<svg>chrome-devtools-mcp</svg>\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 160 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, 159 * 1024 * 1024);
|
|
stubConsole();
|
|
|
|
expect(() =>
|
|
preparePackage({
|
|
rootDir,
|
|
requireNativeAudioCapture: false,
|
|
}),
|
|
).not.toThrow();
|
|
|
|
truncateSync(largeFile, 160 * 1024 * 1024);
|
|
|
|
expect(() =>
|
|
preparePackage({
|
|
rootDir,
|
|
requireNativeAudioCapture: false,
|
|
}),
|
|
).toThrow(
|
|
/Prepared package unpacked size \d+ bytes exceeds 167772160 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',
|
|
private: true,
|
|
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,
|
|
'node_modules/sharp/package.json',
|
|
JSON.stringify({ name: 'sharp', version: '0.35.3' }),
|
|
);
|
|
writeFile(
|
|
rootDir,
|
|
'packages/core/node_modules/sharp/package.json',
|
|
JSON.stringify({ name: 'sharp', version: '0.35.4' }),
|
|
);
|
|
|
|
writeFile(
|
|
rootDir,
|
|
'packages/cli/src/i18n/locales/en.json',
|
|
'{"hello":"world"}\n',
|
|
);
|
|
writeFile(
|
|
rootDir,
|
|
'packages/core/package.json',
|
|
JSON.stringify(
|
|
{
|
|
name: '@qwen-code/qwen-code-core',
|
|
version: '0.17.0',
|
|
dependencies: {
|
|
sharp: '^0.35.0',
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
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 createOpenTuiArtifacts(rootDir) {
|
|
for (const key of [
|
|
...OPENTUI_COMMON_ASSETS,
|
|
...Object.entries(NPM_OPENTUI_NATIVE_ASSETS).map(
|
|
([name, library]) => `${name}/${library}`,
|
|
),
|
|
]) {
|
|
writeFile(rootDir, `dist/opentui-assets/${key}`, key);
|
|
}
|
|
}
|
|
|
|
function createBundleArtifacts(rootDir) {
|
|
createOpenTuiArtifacts(rootDir);
|
|
writeFile(rootDir, 'dist/mem0/main.js', '');
|
|
writeFile(rootDir, 'dist/mem0/write-confirmation.js', '');
|
|
for (const file of [
|
|
'main.js',
|
|
'app.html',
|
|
'skills/generated-ui/SKILL.md',
|
|
'THIRD_PARTY_NOTICES.txt',
|
|
]) {
|
|
writeFile(rootDir, `dist/generated-ui/${file}`, 'generated-ui asset\n');
|
|
}
|
|
writeFile(rootDir, 'dist/cli.js', '');
|
|
writeFile(rootDir, 'dist/execution-worker.js', '');
|
|
writeFile(rootDir, 'dist/sandboxBwrapRelay.js', '');
|
|
writeFile(rootDir, 'dist/sandboxLandlockRelay.js', '');
|
|
writeFile(rootDir, 'dist/sandboxFileWorker.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', '<!doctype html>');
|
|
writeFile(rootDir, 'dist/web-shell/manifest.webmanifest', '{}');
|
|
writeFile(
|
|
rootDir,
|
|
'dist/web-shell/sw.js',
|
|
'self.addEventListener("fetch", () => {});',
|
|
);
|
|
mkdirSync(path.join(rootDir, 'dist', 'web-shell', 'assets'), {
|
|
recursive: true,
|
|
});
|
|
writeFile(
|
|
rootDir,
|
|
'dist/export-transcript-document.js',
|
|
'window.QwenExportRenderer = true;\n',
|
|
);
|
|
writeFile(
|
|
rootDir,
|
|
'dist/export-transcript-document.css',
|
|
'body{color:red}\n',
|
|
);
|
|
}
|
|
|
|
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(() => {});
|
|
}
|
|
});
|