import { mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { DshNpmExtensionInstaller, DshNpmMarketplaceCatalog, runPackageManager, type PackageManagerRunner } from './dsh-extension-marketplace' const temporaryDirectories: string[] = [] function response(value: unknown): Response { return new Response(JSON.stringify(value), { status: 200, headers: { 'content-type': 'application/json' } }) } afterEach(async () => { await Promise.all( temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }) ) ) }) describe('DSH npm marketplace', () => { it('loads every npm search page and keeps only DSH plugin packages', async () => { const fetcher = vi.fn(async (input) => { const from = Number(new URL(String(input)).searchParams.get('from')) return response({ total: 251, objects: from === 0 ? [ { package: { name: 'dsh-plugin-greet', version: '0.1.0', description: 'A greeting tool.', keywords: ['dsh-plugin'], license: 'MIT', links: { repository: 'git+https://github.com/example/greet.git' } } }, { package: { name: 'not-a-plugin', version: '1.0.0', keywords: ['unrelated'] } } ] : [ { package: { name: 'dsh-second-plugin', version: '2.0.0', keywords: ['dsh-plugin'] } } ] }) }) const catalog = new DshNpmMarketplaceCatalog({ fetcher, cacheTtlMs: 60_000 }) const entries = await catalog.list() expect(entries.map((entry) => entry.package.name)).toEqual([ 'dsh-plugin-greet', 'dsh-second-plugin' ]) expect(entries[0]).toMatchObject({ description: 'A greeting tool.', repository: 'https://github.com/example/greet.git' }) expect(fetcher).toHaveBeenCalledTimes(2) await catalog.list() expect(fetcher).toHaveBeenCalledTimes(2) }) it('coalesces concurrent catalog loads', async () => { let resolveFetch: ((response: Response) => void) | undefined const fetcher = vi.fn( () => new Promise((resolve) => { resolveFetch = resolve }) ) const catalog = new DshNpmMarketplaceCatalog({ fetcher }) const first = catalog.list() const second = catalog.list() expect(fetcher).toHaveBeenCalledOnce() resolveFetch?.( response({ total: 1, objects: [ { package: { name: 'dsh-plugin-greet', version: '0.1.0', keywords: ['dsh-plugin'] } } ] }) ) await expect(Promise.all([first, second])).resolves.toEqual([ [ expect.objectContaining({ package: expect.objectContaining({ name: 'dsh-plugin-greet' }) }) ], [ expect.objectContaining({ package: expect.objectContaining({ name: 'dsh-plugin-greet' }) }) ] ]) expect(fetcher).toHaveBeenCalledOnce() }) it('installs the exact package when an older manifest lacks DSH bundle metadata', async () => { const destinationDirectory = await mkdtemp( join(tmpdir(), 'goodbuddy-dsh-npm-installer-') ) temporaryDirectories.push(destinationDirectory) const integrity = `sha512-${Buffer.from('verified').toString( 'base64' )}` const packageName = 'dsh-plugin-greet' const version = '0.1.0' const manifest = { name: packageName, version, main: 'index.js', dist: { integrity }, dsh: { bundle: { patch: './cordis.patch.yml' } } } const npmCliPath = join(destinationDirectory, 'npm-cli.js') await writeFile(npmCliPath, '// bundled npm fixture\n', 'utf8') const fetcher = vi.fn(async () => response({ versions: { '0.0.1': { name: packageName, version: '0.0.1', dist: { integrity } }, [version]: manifest } }) ) const runner: PackageManagerRunner = vi.fn( async (_command, _args, options) => { const installedDirectory = join( options.cwd, 'node_modules', packageName ) await mkdir(installedDirectory, { recursive: true }) await Promise.all([ writeFile( join(installedDirectory, 'package.json'), JSON.stringify(manifest), 'utf8' ), writeFile( join(installedDirectory, 'index.js'), 'export function apply() {}\n', 'utf8' ), writeFile( join(options.cwd, 'package-lock.json'), JSON.stringify({ packages: { [`node_modules/${packageName}`]: { integrity } } }), 'utf8' ) ]) return { exitCode: 0, stdout: '', stderr: '' } } ) const installer = new DshNpmExtensionInstaller({ dshHome: destinationDirectory, npmCliPath, fetcher, runner, environment: { PATH: 'C:\\Node' } }) await expect( installer.install({ entry: { id: 'greet', package: { name: packageName, version }, displayName: packageName, description: 'A greeting tool.' }, destinationDirectory }) ).resolves.toEqual({ entrypoint: `node_modules/${packageName}/index.js`, integrity }) expect(runner).toHaveBeenCalledWith( process.execPath, [ npmCliPath, 'install', '--save-exact', '--no-audit', '--no-fund', '--dangerously-allow-all-scripts', '--loglevel=error', `${packageName}@${version}` ], expect.objectContaining({ cwd: destinationDirectory, env: expect.objectContaining({ ELECTRON_RUN_AS_NODE: '1', npm_execpath: npmCliPath, npm_node_execpath: process.execPath }) }) ) expect( ( await stat( join( destinationDirectory, 'package-manager-bin', process.platform === 'win32' ? 'node.cmd' : 'node' ) ) ).isFile() ).toBe(true) }) it('rejects packages that do not declare a DSH bundle', async () => { const directory = await mkdtemp( join(tmpdir(), 'goodbuddy-dsh-not-plugin-') ) temporaryDirectories.push(directory) const installer = new DshNpmExtensionInstaller({ dshHome: directory, fetcher: vi.fn(async () => response({ versions: { '1.0.0': { name: 'not-a-dsh-plugin', version: '1.0.0', main: 'index.js', dist: { integrity: `sha512-${Buffer.from('verified').toString( 'base64' )}` } } } }) ), runner: vi.fn() }) await expect( installer.install({ entry: { id: 'not-plugin', package: { name: 'not-a-dsh-plugin', version: '1.0.0' }, displayName: 'Not a plugin', description: 'Missing DSH bundle metadata.' }, destinationDirectory: directory }) ).rejects.toThrow() }) it('aborts an active package-manager process and settles the run', async () => { const directory = await mkdtemp( join(tmpdir(), 'goodbuddy-dsh-npm-abort-') ) temporaryDirectories.push(directory) const controller = new AbortController() const operation = runPackageManager( process.execPath, ['-e', 'setInterval(() => {}, 1_000)'], { cwd: directory, env: process.env, timeoutMs: 60_000, signal: controller.signal } ) await new Promise((resolve) => setTimeout(resolve, 50)) controller.abort(new Error('installer cancellation fixture')) await expect(operation).rejects.toThrow( 'installer cancellation fixture' ) }) it('disposes active installs, propagates cancellation, and rejects new work', async () => { const directory = await mkdtemp( join(tmpdir(), 'goodbuddy-dsh-installer-dispose-') ) temporaryDirectories.push(directory) const integrity = `sha512-${Buffer.from('verified').toString( 'base64' )}` const packageName = 'dsh-plugin-cancellable' const version = '1.0.0' const runner: PackageManagerRunner = vi.fn( (_command, _args, options) => new Promise<{ exitCode: number stdout: string stderr: string }>((_resolve, reject) => { const rejectCancellation = (): void => { reject(options.signal?.reason) } options.signal?.addEventListener( 'abort', rejectCancellation, { once: true } ) }) ) const installer = new DshNpmExtensionInstaller({ dshHome: directory, fetcher: vi.fn(async () => response({ versions: { [version]: { name: packageName, version, main: 'index.js', dist: { integrity }, dsh: { bundle: { patch: './cordis.patch.yml' } } } } }) ), runner }) const input = { entry: { id: 'cancellable', package: { name: packageName, version }, displayName: packageName, description: 'Cancellation fixture.' }, destinationDirectory: directory } const installation = installer.install(input) await vi.waitFor(() => expect(runner).toHaveBeenCalledOnce()) await installer.dispose() await expect(installation).rejects.toThrow( '应用退出,DSH 插件安装已取消' ) await expect(installer.install(input)).rejects.toThrow( 'DSH 插件安装器正在关闭' ) }) })