From 56e7bea7f53557e5bbb42291aba2cf632dfead8a Mon Sep 17 00:00:00 2001 From: Qiuner Date: Tue, 18 Aug 2026 21:51:09 +0800 Subject: [PATCH] test(desktop): cover market pnpm uninstall wiring --- .../tests/market-pnpm-integration.spec.ts | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 dsh-plugin-desktop/tests/market-pnpm-integration.spec.ts diff --git a/dsh-plugin-desktop/tests/market-pnpm-integration.spec.ts b/dsh-plugin-desktop/tests/market-pnpm-integration.spec.ts new file mode 100644 index 0000000000..f6bd668a9e --- /dev/null +++ b/dsh-plugin-desktop/tests/market-pnpm-integration.spec.ts @@ -0,0 +1,246 @@ +import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { createServer, type IncomingMessage, type ServerResponse } from 'node:http' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { Readable } from 'node:stream' +import { Context } from '@deepseek-ai/cordis' +import { FileSettingsProvider } from '@deepseek-ai/dsh-settings-file' +import type { SubprocessHandle, SubprocessRuntime, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess' +import { describe, expect, it, vi } from 'vitest' +import { parse as parseYaml, stringify as stringifyYaml } from 'yaml' +import { + apply as applyDesktopPnpm, + inject as desktopPnpmInject, + name as desktopPnpmName, + type DesktopPnpmBootstrap, +} from '../src/pnpm.js' + +const PACKAGE_NAME = 'dsh-plugin-market-integration' +const PACKAGE_VERSION = '1.2.3' +const INTEGRITY = `sha512-${Buffer.alloc(64).toString('base64')}` + +interface CommunityMarketModule { + readonly name: string + readonly inject: string[] + readonly apply: (ctx: Context) => void + readonly marketRoutes: { + readonly operationPreview: string + readonly operationExecute: string + } +} + +function bootstrap(root: string, profileDir: string): DesktopPnpmBootstrap { + return { + activeProfileName: 'web', + activeProfileDir: profileDir, + homeDir: join(root, 'home'), + appExecutable: join(root, 'DSH Desktop'), + pnpmBinPath: join(root, 'runtime', 'pnpm.mjs'), + electronVersion: '43.4.0', + nodeBinDir: join(root, 'runtime', 'node-bin'), + nodeShimPath: join(root, 'runtime', 'node-bin', 'node'), + clearEnvironmentPath: join(root, 'runtime', 'clear-env.mjs'), + dshBootstrapPath: join(root, 'app.asar', 'lib', 'desktop-cli.js'), + installRecoveryStatePath: join(root, 'plugin-install-recovery', 'state.json'), + generationId: 'market-integration-generation-0001', + } +} + +async function writeInstalledProfile(profileDir: string): Promise { + const pluginDir = join(profileDir, 'node_modules', PACKAGE_NAME) + await mkdir(pluginDir, { recursive: true }) + await writeFile(join(pluginDir, 'cordis.patch.yml'), '[]\n') + await writeFile(join(pluginDir, 'package.json'), JSON.stringify({ + name: PACKAGE_NAME, + version: PACKAGE_VERSION, + dsh: { bundle: { patch: './cordis.patch.yml' } }, + })) + await writeFile(join(profileDir, 'package.json'), JSON.stringify({ + name: 'market-integration-profile', + dependencies: { [PACKAGE_NAME]: PACKAGE_VERSION }, + dsh: { profile: { bundles: [PACKAGE_NAME] } }, + })) + await writeFile(join(profileDir, 'pnpm-lock.yaml'), stringifyYaml({ + lockfileVersion: '9.0', + importers: { + '.': { + dependencies: { + [PACKAGE_NAME]: { specifier: PACKAGE_VERSION, version: PACKAGE_VERSION }, + }, + }, + }, + packages: { + [`${PACKAGE_NAME}@${PACKAGE_VERSION}`]: { resolution: { integrity: INTEGRITY } }, + }, + snapshots: { [`${PACKAGE_NAME}@${PACKAGE_VERSION}`]: {} }, + })) +} + +async function removeInstalledProfilePlugin(profileDir: string): Promise { + await rm(join(profileDir, 'node_modules', PACKAGE_NAME), { recursive: true, force: true }) + await writeFile(join(profileDir, 'package.json'), JSON.stringify({ + name: 'market-integration-profile', + dependencies: {}, + dsh: { profile: { bundles: [] } }, + })) + await writeFile(join(profileDir, 'pnpm-lock.yaml'), stringifyYaml({ + lockfileVersion: '9.0', + importers: { '.': {} }, + packages: {}, + snapshots: {}, + })) +} + +async function createWebServer() { + type Handler = (request: IncomingMessage, response: ServerResponse) => void | Promise + const handlers = new Map() + const server = createServer((request, response) => { + const path = new URL(request.url ?? '/', 'http://localhost').pathname + const handler = handlers.get(path) + if (handler === undefined) { + response.statusCode = 404 + response.end() + return + } + void Promise.resolve(handler(request, response)).catch(() => { + if (!response.writableEnded) { + response.statusCode = 500 + response.end() + } + }) + }) + await new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', resolve) + }) + const address = server.address() + if (address === null || typeof address === 'string') throw new Error('fixture web server did not bind TCP') + const origin = `http://127.0.0.1:${address.port}` + return { + service: { + port: address.port, + register(route: { path: string; handler: Handler }) { + handlers.set(route.path, route.handler) + return () => { handlers.delete(route.path) } + }, + }, + async post(path: string, body: unknown) { + const response = await fetch(`${origin}${path}`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + origin, + 'sec-fetch-site': 'same-origin', + }, + body: JSON.stringify(body), + }) + return { status: response.status, body: await response.json() as Record } + }, + close: async () => await new Promise((resolve, reject) => { + server.close(error => { if (error === undefined) resolve(); else reject(error) }) + }), + } +} + +describe('desktop pnpm and community market integration', () => { + it('executes a market uninstall route through the managed desktop pnpm boundary', async () => { + const marketModuleUrl = new URL('../../dsh-community-market/src/index.js', import.meta.url).href + const market = await import(marketModuleUrl) as CommunityMarketModule + const root = await mkdtemp(join(tmpdir(), 'dsh-market-desktop-pnpm-')) + const profileDir = join(root, 'profiles', 'web') + const settingsPath = join(root, 'settings.yaml') + const receipt = { + receiptId: 'receipt:desktop-market-integration-0001', + profileName: 'web', + packageName: PACKAGE_NAME, + version: PACKAGE_VERSION, + integrity: INTEGRITY, + bundlePatch: './cordis.patch.yml', + sourceRecordId: 'source-integration-1', + providerId: 'provider-integration-1', + itemId: 'example/dsh-plugin-market-integration', + displayName: 'Market Integration Plugin', + installedAt: '2026-08-18T00:00:00.000Z', + } + const ctx = new Context() + const webServer = await createWebServer() + try { + await writeInstalledProfile(profileDir) + await writeFile(settingsPath, stringifyYaml({ + 'dsh-community-market': { sources: [], installReceipts: [receipt] }, + })) + + const selectedBootstrap = bootstrap(root, profileDir) + const spawn = vi.fn<(spec: SubprocessSpawnSpec) => SubprocessHandle>((_spec) => ({ + pid: 43_120, + stdin: undefined, + stdout: Readable.from([]), + stderr: Readable.from([]), + collected: {}, + done: (async () => { + await removeInstalledProfilePlugin(profileDir) + return { exitCode: 0, signal: null } + })(), + terminate: vi.fn(), + waitForExit: vi.fn(async () => true), + })) + ctx.provide('webServer', webServer.service as never) + ctx.provide('desktopProfiles', { current: { name: 'web', dir: profileDir } }) + ctx.provide('desktopPnpmBootstrap', selectedBootstrap) + ctx.provide('subprocess', { spawn } as unknown as SubprocessRuntime) + await ctx.plugin(FileSettingsProvider, { path: settingsPath, watch: false }) + await ctx.plugin({ name: desktopPnpmName, inject: desktopPnpmInject, apply: applyDesktopPnpm }) + await ctx.plugin({ name: market.name, inject: market.inject, apply: market.apply }) + + const preview = await webServer.post(market.marketRoutes.operationPreview, { + action: 'uninstall', + receiptId: receipt.receiptId, + }) + expect(preview).toMatchObject({ + status: 200, + body: { action: 'uninstall', packageName: PACKAGE_NAME }, + }) + expect(preview.body.previewId).toEqual(expect.any(String)) + + const executed = await webServer.post(market.marketRoutes.operationExecute, { + previewId: preview.body.previewId, + }) + expect(executed).toMatchObject({ + status: 200, + body: { + action: 'uninstall', + receiptId: receipt.receiptId, + packageName: PACKAGE_NAME, + }, + }) + expect(spawn).toHaveBeenCalledOnce() + expect(spawn.mock.calls[0]?.[0]).toMatchObject({ + argv: [ + selectedBootstrap.appExecutable, + '--expose-internals', + selectedBootstrap.dshBootstrapPath, + 'plugin', + '--profile', + 'web', + 'remove', + PACKAGE_NAME, + ], + cwd: profileDir, + env: { ELECTRON_RUN_AS_NODE: '1', DSH_HOME: selectedBootstrap.homeDir }, + }) + + const persisted = parseYaml(await readFile(settingsPath, 'utf8')) as { + 'dsh-community-market': { installReceipts: unknown[] } + } + expect(persisted['dsh-community-market'].installReceipts).toEqual([]) + const manifest = JSON.parse(await readFile(join(profileDir, 'package.json'), 'utf8')) as { + dependencies: Record + } + expect(manifest.dependencies).not.toHaveProperty(PACKAGE_NAME) + } finally { + await ctx.fiber.dispose() + await webServer.close() + await rm(root, { recursive: true, force: true }) + } + }) +})