Files
qwen-code/packages/vscode-ide-companion/scripts/generate-notices.test.js
T
ytahdnand钉萁 df5470e430 feat(web-shell): Add workspace file browser and previews (#13878)
* feat(web-shell): Add workspace file browser and previews

* fix(web-shell): Restore source preview CI fixtures and notices

* fix(web-shell): Address file pane review blockers

* fix(web-shell): Resolve review regressions and strengthen file checks

---------

Co-authored-by: 钉萁 <[email protected]>
2026-10-13 09:38:47 +00:00

664 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import fs from 'node:fs/promises';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import os from 'node:os';
import path from 'node:path';
import { createRequire } from 'node:module';
import { createHash } from 'node:crypto';
import {
collectDependencies,
findLicenseFile,
findNoticeFile,
findSupplementaryLicenseFiles,
getFallbackLicenseText,
getExceljsBrowserNotices,
normalizeRepositoryUrl,
runNoticeGeneration,
} from './generate-notices.js';
describe('ExcelJS browser bundle notices', () => {
const require = createRequire(
new URL('../../web-shell/package.json', import.meta.url),
);
const exceljsDir = path.dirname(require.resolve('exceljs/package.json'));
it('ships original licenses for each embedded source path and version', async () => {
const write = vi.spyOn(fs, 'writeFile').mockImplementation(async () => {});
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
try {
await runNoticeGeneration({});
expect(write).toHaveBeenCalledTimes(1);
const notices = String(write.mock.calls[0][1]);
const sourceMap = JSON.parse(
await fs.readFile(
path.join(exceljsDir, 'dist/exceljs.min.js.map'),
'utf-8',
),
);
const sourcesByPackage = new Map();
for (const [index, source] of sourceMap.sources.entries()) {
if (!source.includes('node_modules/')) continue;
const tail = source.split('node_modules/').at(-1);
const name = tail
.split('/')
.slice(0, tail.startsWith('@') ? 2 : 1)
.join('/');
const prefix = source.slice(0, source.length - tail.length) + name;
const files = sourcesByPackage.get(prefix) ?? [];
files.push([source, sourceMap.sourcesContent[index]]);
sourcesByPackage.set(prefix, files);
}
const inventory = JSON.parse(
await fs.readFile(
new URL('./licenses/exceljs-browser.json', import.meta.url),
'utf-8',
),
);
const mapBytes = await fs.readFile(
path.join(exceljsDir, 'dist/exceljs.min.js.map'),
);
expect(createHash('sha256').update(mapBytes).digest('hex')).toBe(
inventory.sourceMapSha256,
);
expect(inventory.packages).toHaveLength(72);
expect(
new Set(inventory.packages.map(({ sourcePrefix }) => sourcePrefix)),
).toEqual(new Set(sourcesByPackage.keys()));
for (const dep of inventory.packages) {
const hash = createHash('sha256');
for (const [source, content] of sourcesByPackage
.get(dep.sourcePrefix)
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
hash.update(source).update('\0').update(content).update('\0');
}
expect(hash.digest('hex')).toBe(dep.sourceSha256);
expect(() => new URL(dep.repository)).not.toThrow();
expect(notices).toContain(
`${dep.name}@${dep.version}\n(${dep.repository})\n\n${dep.license}`,
);
expect(dep.license).toMatch(/Copyright|copyright/);
expect(dep.license).not.toContain('License text not found.');
}
const pakoNotice = notices
.split('\[email protected]\n')[1]
.split(
'============================================================\n',
)[0];
expect(pakoNotice).toContain(
(
await fs.readFile(
new URL('./licenses/pako-1.0.11-zlib.txt', import.meta.url),
'utf-8',
)
).trim(),
);
expect(pakoNotice).toContain("This software is provided 'as-is'");
expect(pakoNotice).toContain('1995-2013 Jean-loup Gailly and Mark Adler');
expect(pakoNotice).toContain(
'2014-2017 Vitaly Puzrin and Andrey Tupitsin',
);
expect(pakoNotice).toContain(
'3. This notice may not be removed or altered from any source distribution.',
);
expect(notices).not.toContain('License text not found.');
const noticeCount = [
...notices.matchAll(/^={60}\n(?=[^\n]+@[^\n]+\n\()/gm),
].length;
expect(log).toHaveBeenCalledWith(`Total dependencies: ${noticeCount}`);
for (const key of [
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
]) {
expect(notices).toContain(`\n${key}\n`);
}
expect(
inventory.packages.find(({ name }) => name === 'browserify-rsa')
.repository,
).toBe('https://github.com/crypto-browserify/browserify-rsa');
expect(
inventory.packages
.filter(({ name }) => name === 'bn.js')
.map(({ version }) => version),
).toEqual(['4.12.0', '5.2.1']);
} finally {
write.mockRestore();
log.mockRestore();
}
});
it('rejects an unreviewed bundle or ExcelJS version', async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'exceljs-notices-'));
try {
await fs.mkdir(path.join(dir, 'dist'));
const manifest = JSON.parse(
await fs.readFile(path.join(exceljsDir, 'package.json'), 'utf-8'),
);
await fs.writeFile(
path.join(dir, 'package.json'),
JSON.stringify(manifest),
);
const bundle = await fs.readFile(
path.join(exceljsDir, 'dist/exceljs.min.js'),
);
await fs.writeFile(path.join(dir, 'dist/exceljs.min.js'), bundle);
expect(await getExceljsBrowserNotices(dir)).toHaveLength(72);
await fs.appendFile(
path.join(dir, 'dist/exceljs.min.js'),
'\n// changed',
);
await expect(getExceljsBrowserNotices(dir)).rejects.toThrow(
'refresh its bundled license inventory',
);
await fs.writeFile(path.join(dir, 'dist/exceljs.min.js'), bundle);
await fs.writeFile(
path.join(dir, 'package.json'),
JSON.stringify({ ...manifest, browser: './dist/exceljs.js' }),
);
await expect(getExceljsBrowserNotices(dir)).rejects.toThrow(
'refresh its bundled license inventory',
);
await fs.writeFile(
path.join(dir, 'package.json'),
JSON.stringify({ ...manifest, version: '4.5.0' }),
);
await expect(getExceljsBrowserNotices(dir)).rejects.toThrow(
'refresh its bundled license inventory',
);
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
});
describe('runNoticeGeneration', () => {
it('skips generation during dependency-only worktree setup', async () => {
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
// Stubbed no-op so a regression that drops the early return rewrites the
// tracked NOTICES.txt instead of merely failing this assertion.
const write = vi.spyOn(fs, 'writeFile').mockImplementation(async () => {});
try {
await runNoticeGeneration({
npm_lifecycle_event: 'generate:notices',
QWEN_SKIP_NOTICE_GENERATION: '1',
});
expect(log).toHaveBeenCalledWith(
'Skipping VS Code notice generation during worktree bootstrap.',
);
expect(write).not.toHaveBeenCalled();
} finally {
log.mockRestore();
write.mockRestore();
}
});
it('still generates notices when the skip flag is absent', async () => {
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
const error = vi.spyOn(console, 'error').mockImplementation(() => {});
// Empty dependency graph: main() reaches the write without touching the
// real node_modules, and a failing main() cannot process.exit the worker.
const read = vi.spyOn(fs, 'readFile').mockImplementation(async (file) => {
const name = String(file);
if (name.endsWith('package.json')) {
return JSON.stringify({ dependencies: {} });
}
throw Object.assign(new Error(`ENOENT: ${name}`), { code: 'ENOENT' });
});
const write = vi.spyOn(fs, 'writeFile').mockImplementation(async () => {});
try {
await runNoticeGeneration({ npm_lifecycle_event: 'generate:notices' });
expect(write).toHaveBeenCalledTimes(1);
expect(String(write.mock.calls[0]?.[1])).toContain(
'third-party software notices and license terms',
);
} finally {
log.mockRestore();
error.mockRestore();
read.mockRestore();
write.mockRestore();
}
});
});
describe('findLicenseFile', () => {
let packageDir;
beforeEach(async () => {
packageDir = await fs.mkdtemp(path.join(os.tmpdir(), 'notices-test-'));
});
afterEach(async () => {
await fs.rm(packageDir, { recursive: true, force: true });
});
// Regression guard: the Linux CI drift check runs generation and comparison
// on the same case-sensitive filesystem, so a revert to case-sensitive
// matching would produce consistent-but-wrong output and pass the check.
// This asserts the lookup resolves a mixed-case file regardless of platform.
it('resolves a mixed-case license file', async () => {
await fs.writeFile(path.join(packageDir, 'License'), 'MIT');
const resolved = await findLicenseFile(packageDir);
expect(resolved).toBe(path.join(packageDir, 'License'));
});
it('finds JSZip’s LICENSE.markdown attribution', async () => {
await fs.writeFile(
path.join(packageDir, 'LICENSE.markdown'),
'JSZip license',
);
expect(await findLicenseFile(packageDir)).toBe(
path.join(packageDir, 'LICENSE.markdown'),
);
});
it('prefers LICENSE over other variants', async () => {
await fs.writeFile(path.join(packageDir, 'LICENSE'), 'Apache-2.0');
await fs.writeFile(path.join(packageDir, 'LICENSE.md'), 'MIT');
const resolved = await findLicenseFile(packageDir);
expect(resolved).toBe(path.join(packageDir, 'LICENSE'));
});
it('honors the package.json licenseFile hint', async () => {
await fs.writeFile(path.join(packageDir, 'COPYING'), 'GPL');
const resolved = await findLicenseFile(packageDir, 'COPYING');
expect(resolved).toBe(path.join(packageDir, 'COPYING'));
});
it('returns undefined when no license file exists', async () => {
const resolved = await findLicenseFile(packageDir);
expect(resolved).toBeUndefined();
});
});
describe('collectDependencies', () => {
let root;
beforeEach(async () => {
// realpath: on macOS the temp dir sits behind /var -> /private/var, and
// the traversal reports real directories.
root = await fs.realpath(
await fs.mkdtemp(path.join(os.tmpdir(), 'notices-tree-')),
);
});
afterEach(async () => {
await fs.rm(root, { recursive: true, force: true });
});
async function writePackage(dir, manifest) {
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(
path.join(dir, 'package.json'),
JSON.stringify(manifest),
);
}
it('follows a workspace link to its dependencies without listing it', async () => {
const companion = path.join(root, 'packages', 'companion');
const core = path.join(root, 'packages', 'core');
await writePackage(companion, { name: 'companion' });
await writePackage(core, {
name: '@qwen-code/core',
version: '0.1.0',
dependencies: { nested: '^1.0.0' },
});
await writePackage(path.join(core, 'node_modules', 'nested'), {
name: 'nested',
version: '1.0.0',
});
await fs.mkdir(path.join(root, 'node_modules', '@qwen-code'), {
recursive: true,
});
await fs.symlink(
core,
path.join(root, 'node_modules', '@qwen-code', 'core'),
'junction',
);
const dependencies = new Map();
await collectDependencies(
'@qwen-code/core',
companion,
dependencies,
new Set(),
);
expect([...dependencies.values()]).toEqual([
{
name: 'nested',
version: '1.0.0',
dir: path.join(core, 'node_modules', 'nested'),
},
]);
});
it('excludes only ExcelJS Node-only branches while retaining other consumers', async () => {
await writePackage(path.join(root, 'node_modules', 'exceljs'), {
name: 'exceljs',
version: '4.4.0',
dependencies: { archiver: '*', unzipper: '*', tmp: '*', jszip: '*' },
});
for (const name of ['archiver', 'unzipper', 'tmp', 'jszip']) {
await writePackage(path.join(root, 'node_modules', name), {
name,
version: '1.0.0',
});
}
const dependencies = new Map();
await collectDependencies('exceljs', root, dependencies, new Set());
expect([...dependencies.keys()]).toEqual(['[email protected]', '[email protected]']);
await collectDependencies('archiver', root, dependencies, new Set());
expect(dependencies.has('[email protected]')).toBe(true);
});
it('resolves each dependency from the nearest node_modules above it', async () => {
const companion = path.join(root, 'packages', 'companion');
await writePackage(companion, { name: 'companion' });
await writePackage(path.join(root, 'node_modules', 'hoisted'), {
name: 'hoisted',
version: '2.0.0',
dependencies: { leaf: '^3.0.0' },
});
// The nested copy wins over the hoisted one for the package that nests it.
await writePackage(
path.join(root, 'node_modules', 'hoisted', 'node_modules', 'leaf'),
{ name: 'leaf', version: '3.1.0' },
);
await writePackage(path.join(root, 'node_modules', 'leaf'), {
name: 'leaf',
version: '4.0.0',
});
const dependencies = new Map();
await collectDependencies('hoisted', companion, dependencies, new Set());
expect([...dependencies.keys()]).toEqual(['[email protected]', '[email protected]']);
});
it('rejects missing bundled seeds without overwriting committed attribution', async () => {
await writePackage(root, { bundledDevDependencies: ['missing-package'] });
const output = path.join(root, 'NOTICES.txt');
await fs.writeFile(output, 'EXISTING ATTRIBUTION');
await expect(
runNoticeGeneration({}, { packageDir: root, bundledOnly: true }),
).rejects.toThrow('missing-package');
expect(await fs.readFile(output, 'utf8')).toBe('EXISTING ATTRIBUTION');
});
it('includes declared bundled dev dependencies and their notices, excluding other dev tools', async () => {
const workspace = path.join(root, 'packages', 'web-shell');
await writePackage(workspace, {
name: '@qwen-code/web-shell',
version: '1.0.0',
bundledDevDependencies: ['bundled'],
dependencies: { ordinary: '1.0.0' },
devDependencies: { bundled: '1.0.0', unused: '1.0.0' },
});
await fs.mkdir(path.join(root, 'node_modules', '@qwen-code'), {
recursive: true,
});
await fs.symlink(
workspace,
path.join(root, 'node_modules', '@qwen-code', 'web-shell'),
'junction',
);
for (const name of ['ordinary', 'unused', 'extra'])
await writePackage(path.join(root, 'node_modules', name), {
name,
version: '1.0.0',
license: 'MIT',
});
const bundled = path.join(root, 'node_modules', 'bundled');
await writePackage(bundled, {
name: 'bundled',
version: '1.0.0',
dependencies: { leaf: '1.0.0' },
bundledDevDependencies: ['extra'],
});
await fs.writeFile(path.join(bundled, 'LICENSE'), 'BUNDLED LICENSE');
await fs.writeFile(path.join(bundled, 'NOTICE.md'), 'DERIVED CODE CREDIT');
await writePackage(path.join(root, 'node_modules', 'leaf'), {
name: 'leaf',
version: '1.0.0',
license: 'MIT',
});
const dependencies = new Map();
await collectDependencies(
'@qwen-code/web-shell',
root,
dependencies,
new Set(),
);
expect([...dependencies.keys()]).toEqual([
'[email protected]',
'[email protected]',
'[email protected]',
]);
await runNoticeGeneration({}, { packageDir: workspace, bundledOnly: true });
const notice = await fs.readFile(
path.join(workspace, 'NOTICES.txt'),
'utf8',
);
expect(notice).toContain('[email protected]');
expect(notice).toContain('BUNDLED LICENSE');
expect(notice).toContain('DERIVED CODE CREDIT');
expect(notice).toContain('[email protected]');
expect(notice).not.toContain('unused');
expect(notice).not.toContain('ordinary');
await fs.unlink(path.join(workspace, 'NOTICES.txt'));
const result = spawnSync(
process.execPath,
[
fileURLToPath(new URL('./generate-notices.js', import.meta.url)),
'--bundled',
workspace,
],
{
encoding: 'utf8',
env: { ...process.env, QWEN_SKIP_NOTICE_GENERATION: '' },
},
);
expect(result.status, result.stderr).toBe(0);
expect(await fs.readFile(path.join(workspace, 'NOTICES.txt'), 'utf8')).toBe(
notice,
);
});
});
describe('normalizeRepositoryUrl', () => {
it('returns object-form repository urls unchanged', () => {
expect(
normalizeRepositoryUrl({
type: 'git',
url: 'git+https://github.com/nodejs/undici.git',
}),
).toBe('git+https://github.com/nodejs/undici.git');
});
it('accepts string-form repository urls', () => {
expect(
normalizeRepositoryUrl(
'https://github.com/theKashey/react-remove-scroll-bar',
),
).toBe('https://github.com/theKashey/react-remove-scroll-bar');
});
it('expands bare GitHub shorthand strings', () => {
expect(normalizeRepositoryUrl('yargs/cliui')).toBe(
'https://github.com/yargs/cliui',
);
});
it('expands github:-prefixed shorthand strings', () => {
expect(
normalizeRepositoryUrl('github:anthropics/anthropic-sdk-typescript'),
).toBe('https://github.com/anthropics/anthropic-sdk-typescript');
});
it('normalizes git://, git+https:// and scp-style string urls', () => {
expect(
normalizeRepositoryUrl('git://github.com/komagata/eastasianwidth.git'),
).toBe('https://github.com/komagata/eastasianwidth.git');
expect(
normalizeRepositoryUrl('git+https://github.com/jsdom/tr46.git'),
).toBe('https://github.com/jsdom/tr46.git');
expect(
normalizeRepositoryUrl('[email protected]:kwsites/file-exists.git'),
).toBe('https://github.com/kwsites/file-exists.git');
});
it('returns undefined for absent or empty repository values', () => {
expect(normalizeRepositoryUrl(undefined)).toBeUndefined();
expect(normalizeRepositoryUrl({})).toBeUndefined();
expect(normalizeRepositoryUrl(' ')).toBeUndefined();
});
});
describe('findNoticeFile', () => {
let packageDir;
beforeEach(async () => {
packageDir = await fs.mkdtemp(path.join(os.tmpdir(), 'notices-test-'));
});
afterEach(async () => {
await fs.rm(packageDir, { recursive: true, force: true });
});
it('resolves a mixed-case NOTICE file', async () => {
await fs.writeFile(path.join(packageDir, 'Notice'), 'Apache ECharts');
const resolved = await findNoticeFile(packageDir);
expect(resolved).toBe(path.join(packageDir, 'Notice'));
});
it('prefers NOTICE over NOTICE.txt', async () => {
await fs.writeFile(path.join(packageDir, 'NOTICE'), 'notice');
await fs.writeFile(path.join(packageDir, 'NOTICE.txt'), 'notice.txt');
const resolved = await findNoticeFile(packageDir);
expect(resolved).toBe(path.join(packageDir, 'NOTICE'));
});
it('returns undefined when no NOTICE file exists', async () => {
const resolved = await findNoticeFile(packageDir);
expect(resolved).toBeUndefined();
});
});
describe('findSupplementaryLicenseFiles', () => {
let packageDir;
beforeEach(async () => {
packageDir = await fs.mkdtemp(path.join(os.tmpdir(), 'notices-test-'));
});
afterEach(async () => {
await fs.rm(packageDir, { recursive: true, force: true });
});
it('lists files in the licenses directory sorted', async () => {
await fs.mkdir(path.join(packageDir, 'licenses'));
await fs.writeFile(path.join(packageDir, 'licenses', 'LICENSE-d3'), 'BSD');
await fs.writeFile(path.join(packageDir, 'licenses', 'LICENSE-abc'), 'MIT');
const resolved = await findSupplementaryLicenseFiles(packageDir);
expect(resolved).toEqual([
path.join(packageDir, 'licenses', 'LICENSE-abc'),
path.join(packageDir, 'licenses', 'LICENSE-d3'),
]);
});
it('ignores the uppercase REUSE-style LICENSES directory', async () => {
await fs.mkdir(path.join(packageDir, 'LICENSES'));
await fs.writeFile(
path.join(packageDir, 'LICENSES', 'Apache-2.0.txt'),
'Apache-2.0',
);
const resolved = await findSupplementaryLicenseFiles(packageDir);
expect(resolved).toEqual([]);
});
it('returns an empty list when the package has no licenses directory', async () => {
const resolved = await findSupplementaryLicenseFiles(packageDir);
expect(resolved).toEqual([]);
});
});
describe('getFallbackLicenseText', () => {
it('returns standard MIT text with the copyright holder from a string author', () => {
const text = getFallbackLicenseText('MIT', 'Wilson Page');
expect(text).toContain('Standard MIT license text');
expect(text).toContain('Copyright (c) Wilson Page');
expect(text).toContain('Permission is hereby granted, free of charge');
});
// MIT requires the permission notice to be included verbatim; the
// fallback body must carry the canonical disclaimer wording so every
// regenerated NOTICES.txt entry matches the other MIT entries.
it('emits the canonical MIT disclaimer wording verbatim', () => {
const text = getFallbackLicenseText('MIT', undefined);
expect(text).toContain(
'OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE',
);
expect(text).not.toContain('THE USE OF OTHER DEALINGS');
});
it('reads the author name from object-form authors', () => {
const text = getFallbackLicenseText('MIT', { name: 'Junyoung Choi' });
expect(text).toContain('Copyright (c) Junyoung Choi');
});
it('strips the trailing homepage from npm author strings', () => {
const text = getFallbackLicenseText(
'MIT',
'Junyoung Choi <[email protected]> (https://rokt33r.github.io)',
);
expect(text).toContain('Copyright (c) Junyoung Choi <[email protected]>');
expect(text).not.toContain('rokt33r.github.io');
});
it('omits the copyright line when no author is declared', () => {
const text = getFallbackLicenseText('MIT', undefined);
expect(text).toContain('Permission is hereby granted, free of charge');
expect(text).not.toContain('Copyright (c)');
});
it('returns undefined for non-MIT declarations', () => {
expect(getFallbackLicenseText('Apache-2.0', 'Some Author')).toBeUndefined();
expect(getFallbackLicenseText(undefined, 'Some Author')).toBeUndefined();
});
});