mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* 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]>
664 lines
22 KiB
JavaScript
664 lines
22 KiB
JavaScript
/**
|
||
* @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();
|
||
});
|
||
});
|