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]>
413 lines
15 KiB
JavaScript
413 lines
15 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { spawnSync } from 'node:child_process';
|
|
import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const root = path.resolve(__dirname, '..', '..');
|
|
const verifier = path.join(
|
|
root,
|
|
'packages',
|
|
'web-shell',
|
|
'scripts',
|
|
'verify-publish-artifacts.mjs',
|
|
);
|
|
|
|
// A published version cannot be replaced, so the guard is driven here the way
|
|
// `prepublishOnly` drives it: the real script copied into a throwaway package
|
|
// root that carries only the shape under test. Nothing imports the workspace,
|
|
// and `npm pack` answers for the fixture's own `files` globs.
|
|
function runVerifier(build, getEnv = () => ({})) {
|
|
const fixture = mkdtempSync(path.join(tmpdir(), 'web-shell-publish-'));
|
|
try {
|
|
build(fixture);
|
|
mkdirSync(path.join(fixture, 'scripts'), { recursive: true });
|
|
cpSync(
|
|
verifier,
|
|
path.join(fixture, 'scripts', 'verify-publish-artifacts.mjs'),
|
|
);
|
|
return spawnSync(
|
|
process.execPath,
|
|
['scripts/verify-publish-artifacts.mjs'],
|
|
{
|
|
cwd: fixture,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, ...getEnv(fixture) },
|
|
timeout: 120_000,
|
|
},
|
|
);
|
|
} finally {
|
|
rmSync(fixture, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function write(fixture, name, contents) {
|
|
const target = path.join(fixture, name);
|
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
writeFileSync(
|
|
target,
|
|
name.endsWith('package.json')
|
|
? `${JSON.stringify(contents, null, 2)}\n`
|
|
: contents,
|
|
);
|
|
}
|
|
|
|
// The real manifest's `files`: `dist/*.js` does not cross a `/`, so anything
|
|
// the build emits below `dist/` is on disk yet absent from the tarball.
|
|
const files = ['dist/*.js', 'dist/types'];
|
|
|
|
function declarePackage(fixture, exports, packageFiles = files) {
|
|
write(fixture, 'NOTICES.txt', 'Bundled dependency licenses');
|
|
write(fixture, 'package.json', {
|
|
name: 'fixture-web-shell',
|
|
version: '0.0.0',
|
|
type: 'module',
|
|
files: ['NOTICES.txt', ...packageFiles],
|
|
exports,
|
|
});
|
|
}
|
|
|
|
describe('web-shell publish artifact verifier', () => {
|
|
it('refuses bundled dependency notices excluded from the npm package', () => {
|
|
const result = runVerifier((fixture) => {
|
|
write(fixture, 'NOTICES.txt', 'Bundled dependency licenses');
|
|
write(fixture, 'dist/index.js', 'export {};');
|
|
write(fixture, 'package.json', {
|
|
name: 'fixture-web-shell',
|
|
version: '0.0.0',
|
|
type: 'module',
|
|
files,
|
|
exports: { '.': './dist/index.js' },
|
|
});
|
|
});
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(
|
|
'NOTICES.txt is missing from the npm package',
|
|
);
|
|
});
|
|
it('refuses an export target that exists on disk but is left out of the tarball', () => {
|
|
const result = runVerifier((fixture) => {
|
|
declarePackage(fixture, {
|
|
'.': {
|
|
types: './dist/types/index.d.ts',
|
|
import: './dist/nested/index.js',
|
|
},
|
|
});
|
|
write(fixture, 'dist/nested/index.js', 'export default 1;\n');
|
|
write(
|
|
fixture,
|
|
'dist/types/index.d.ts',
|
|
'export declare const a: number;\n',
|
|
);
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain('./dist/nested/index.js');
|
|
expect(result.stderr).toContain('not included in the npm package');
|
|
});
|
|
|
|
it('refuses a relative chunk that lands in a subdirectory `files` does not reach', () => {
|
|
const result = runVerifier((fixture) => {
|
|
declarePackage(fixture, { '.': { import: './dist/index.js' } });
|
|
write(
|
|
fixture,
|
|
'dist/index.js',
|
|
'import { chunk } from "./nested/chunk.js";\nexport default chunk;\n',
|
|
);
|
|
write(fixture, 'dist/nested/chunk.js', 'export const chunk = 1;\n');
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain('./nested/chunk.js');
|
|
expect(result.stderr).toContain('not included in the npm package');
|
|
});
|
|
|
|
it('still fails closed on an export target that was never built', () => {
|
|
const result = runVerifier((fixture) => {
|
|
declarePackage(fixture, {
|
|
'.': {
|
|
types: './dist/types/index.d.ts',
|
|
import: './dist/absent.js',
|
|
},
|
|
});
|
|
write(
|
|
fixture,
|
|
'dist/types/index.d.ts',
|
|
'export declare const a: number;\n',
|
|
);
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain('missing ./dist/absent.js');
|
|
});
|
|
|
|
it('accepts a wildcard export pattern whose targets are packed', () => {
|
|
const result = runVerifier((fixture) => {
|
|
declarePackage(fixture, {
|
|
'.': { import: './dist/index.js' },
|
|
'./*': './dist/*',
|
|
});
|
|
write(fixture, 'dist/index.js', 'export default 1;\n');
|
|
write(fixture, 'dist/daemon-react-sdk.js', 'export default 2;\n');
|
|
});
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stderr).toBe('');
|
|
});
|
|
|
|
it('refuses a wildcard target under a literal exports key', () => {
|
|
const result = runVerifier((fixture) => {
|
|
declarePackage(fixture, {
|
|
'.': { import: './dist/index.js' },
|
|
'./foo': './dist/*.js',
|
|
});
|
|
write(fixture, 'dist/index.js', 'export default 1;\n');
|
|
write(fixture, 'dist/bar.js', 'export default 2;\n');
|
|
});
|
|
|
|
// Node gives `*` pattern meaning only when the exports KEY carries it: a
|
|
// wildcard target under a literal key resolves to a literal path, so the
|
|
// verifier must keep the literal checks for it instead of pattern-matching
|
|
// it against whatever the package happens to contain.
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain('missing ./dist/*.js');
|
|
});
|
|
|
|
it('refuses an exports key that carries more than one star', () => {
|
|
const result = runVerifier((fixture) => {
|
|
declarePackage(fixture, {
|
|
'.': { import: './dist/index.js' },
|
|
'./*/*': './dist/*',
|
|
});
|
|
write(fixture, 'dist/index.js', 'export default 1;\n');
|
|
});
|
|
|
|
// Node honours a pattern key only when it carries exactly one `*`; a
|
|
// two-star key matches nothing at all, so the pair must fall through to
|
|
// the literal checks and be refused like the base verifier refused it.
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain('missing ./dist/*');
|
|
});
|
|
|
|
it('refuses a star-bearing exports key that is not a subpath key', () => {
|
|
const result = runVerifier((fixture) => {
|
|
declarePackage(fixture, {
|
|
'.': { import: './dist/index.js' },
|
|
'dist/*': './dist/*',
|
|
});
|
|
write(fixture, 'dist/index.js', 'export default 1;\n');
|
|
write(fixture, 'dist/other.js', 'export default 2;\n');
|
|
});
|
|
|
|
// `"dist/*"` carries exactly one star and its target is packed, so a
|
|
// star-count gate alone would pattern-match it and certify the publish.
|
|
// Node never gives a key pattern meaning unless it is a `./`-prefixed
|
|
// subpath key: mixed with `"."` it rejects the whole manifest with
|
|
// ERR_INVALID_PACKAGE_CONFIG for every specifier, the root import
|
|
// included. It must fall through to the literal checks and be refused the
|
|
// way the base verifier refused it.
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain('missing ./dist/*');
|
|
});
|
|
|
|
it('refuses a multi-star target whose capture cannot match every star', () => {
|
|
const result = runVerifier((fixture) => {
|
|
// `files: ['dist']` packs dist/a/b.js, so the refusal cannot be
|
|
// explained by an unpacked file: Node substitutes the one captured
|
|
// substring into BOTH stars (`./a` -> `dist/a/a.js`), and only a
|
|
// back-referencing regex rejects the packed `dist/a/b.js` here.
|
|
declarePackage(fixture, { './*': './dist/*/*.js' }, ['dist']);
|
|
write(fixture, 'dist/a/b.js', 'export default 1;\n');
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain('matches no file in the npm package');
|
|
});
|
|
|
|
it('accepts a multi-star target when one capture satisfies every star', () => {
|
|
const result = runVerifier((fixture) => {
|
|
declarePackage(fixture, { './*': './dist/*/*.js' }, ['dist']);
|
|
write(fixture, 'dist/a/a.js', 'export default 1;\n');
|
|
});
|
|
|
|
// Positive control: the same-capture shape Node actually resolves, so the
|
|
// gate must not be a blanket refusal of multi-star targets.
|
|
expect(result.status).toBe(0);
|
|
expect(result.stderr).toBe('');
|
|
});
|
|
|
|
it('accepts a multi-star target whose star is followed by a digit', () => {
|
|
const result = runVerifier((fixture) => {
|
|
declarePackage(fixture, { './*': './dist/*/*1.js' }, ['dist']);
|
|
write(fixture, 'dist/a/a1.js', 'export default 42;\n');
|
|
});
|
|
|
|
// Node substitutes one capture into every star, so `./a` resolves to
|
|
// `dist/a/a1.js` and the import succeeds. A positional `\1` here would
|
|
// compile as `\11` — the Annex B octal escape U+0009, since the pattern
|
|
// has only one group — which no packed path can satisfy, refusing a
|
|
// publishable manifest. The back-reference has to be named.
|
|
expect(result.status).toBe(0);
|
|
expect(result.stderr).toBe('');
|
|
});
|
|
|
|
it('accepts a wildcard pattern whose only packed match is nested', () => {
|
|
const result = runVerifier((fixture) => {
|
|
// `files: ['dist']`: the suite default `dist/*.js` never packs a nested
|
|
// file, so here the family's only packed member lives in a subdirectory
|
|
// and the `*` capture has to cross a `/` to reach it — the property
|
|
// Node's resolver has and this branch depends on.
|
|
declarePackage(fixture, { './*': './dist/*' }, ['dist']);
|
|
write(fixture, 'dist/nested/sdk.js', 'export default 1;\n');
|
|
});
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stderr).toBe('');
|
|
});
|
|
|
|
it('refuses an exports target carrying a dot segment', () => {
|
|
const result = runVerifier((fixture) => {
|
|
declarePackage(fixture, { './*': './dist/../dist/*.js' }, ['dist']);
|
|
write(fixture, 'dist/a.js', 'export default 1;\n');
|
|
});
|
|
|
|
// `join` normalizes `./dist/../dist/*.js` down to `dist/*.js`, which the
|
|
// packed `dist/a.js` satisfies — but Node throws
|
|
// ERR_INVALID_PACKAGE_TARGET for every subpath through that key, so the
|
|
// DECLARED string is what has to be validated, not the normalized path.
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain('not a valid "exports" target');
|
|
});
|
|
|
|
it('refuses an exports target that is not ./-relative', () => {
|
|
const result = runVerifier((fixture) => {
|
|
declarePackage(fixture, { './*': 'dist/*' }, ['dist']);
|
|
write(fixture, 'dist/a.js', 'export default 1;\n');
|
|
});
|
|
|
|
// `dist/*` normalizes through `packPath` to exactly what the well-formed
|
|
// `./dist/*` does, so the packed-list check alone cannot tell them apart,
|
|
// yet Node rejects a non-relative target for every specifier through it.
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain('not a valid "exports" target');
|
|
});
|
|
|
|
it('checks a target advertised by both a pattern key and a literal key', () => {
|
|
const result = runVerifier((fixture) => {
|
|
declarePackage(fixture, { './*': './dist/*', './foo': './dist/*' });
|
|
write(fixture, 'dist/index.js', 'export default 1;\n');
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain('missing ./dist/*');
|
|
});
|
|
|
|
it('keeps the literal checks for a pattern key with a literal target', () => {
|
|
const result = runVerifier((fixture) => {
|
|
declarePackage(fixture, { './*': './dist/entry.js' });
|
|
write(
|
|
fixture,
|
|
'dist/entry.js',
|
|
'import { chunk } from "./nested/chunk.js";\nexport default chunk;\n',
|
|
);
|
|
write(fixture, 'dist/nested/chunk.js', 'export const chunk = 1;\n');
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain('not included in the npm package');
|
|
});
|
|
|
|
it('still fails closed on a wildcard pattern that matches nothing packed', () => {
|
|
const result = runVerifier((fixture) => {
|
|
declarePackage(fixture, {
|
|
'.': { import: './dist/index.js' },
|
|
'./*': './dist/absent/*',
|
|
});
|
|
write(fixture, 'dist/index.js', 'export default 1;\n');
|
|
// On disk but not packed: `dist/*.js` does not cross a `/`, so this
|
|
// keeps the fixture discriminating between the packed-list oracle and a
|
|
// filesystem-existence oracle.
|
|
write(fixture, 'dist/absent/sdk.js', 'export const sdk = 1;\n');
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain('./dist/absent/*');
|
|
expect(result.stderr).toContain('matches no file in the npm package');
|
|
});
|
|
|
|
it('accepts the shape the published package actually ships', () => {
|
|
const result = runVerifier((fixture) => {
|
|
declarePackage(fixture, {
|
|
'.': {
|
|
types: './dist/types/index.d.ts',
|
|
import: './dist/index.js',
|
|
},
|
|
'./daemon-react-sdk': {
|
|
types: './dist/types/daemon-react-sdk.d.ts',
|
|
import: './dist/daemon-react-sdk.js',
|
|
},
|
|
'./transcript': {
|
|
types: './dist/types/transcript.d.ts',
|
|
import: './dist/transcript.js',
|
|
},
|
|
});
|
|
write(
|
|
fixture,
|
|
'dist/index.js',
|
|
'import { chunk } from "./chunk.js";\nexport default chunk;\n',
|
|
);
|
|
write(fixture, 'dist/chunk.js', 'export const chunk = 1;\n');
|
|
write(fixture, 'dist/daemon-react-sdk.js', 'export default 2;\n');
|
|
write(fixture, 'dist/transcript.js', 'export default 3;\n');
|
|
write(
|
|
fixture,
|
|
'dist/types/index.d.ts',
|
|
'export declare const a: number;\n',
|
|
);
|
|
write(
|
|
fixture,
|
|
'dist/types/daemon-react-sdk.d.ts',
|
|
'export declare const b: number;\n',
|
|
);
|
|
write(
|
|
fixture,
|
|
'dist/types/transcript.d.ts',
|
|
'export declare const c: number;\n',
|
|
);
|
|
});
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stderr).toBe('');
|
|
});
|
|
|
|
it('keeps npm warnings out of the verifier output', () => {
|
|
const result = runVerifier(
|
|
(fixture) => {
|
|
declarePackage(fixture, {
|
|
'.': { import: './dist/index.js' },
|
|
});
|
|
write(fixture, 'dist/index.js', 'export default 1;\n');
|
|
write(
|
|
fixture,
|
|
'unknown.npmrc',
|
|
'ELECTRON_MIRROR=https://mirror.invalid\n',
|
|
);
|
|
},
|
|
(fixture) => ({
|
|
NPM_CONFIG_USERCONFIG: path.join(fixture, 'unknown.npmrc'),
|
|
}),
|
|
);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stderr).toBe('');
|
|
});
|
|
});
|