| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- import { createHash } from 'node:crypto'
- import { existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
- import { tmpdir } from 'node:os'
- import { dirname, join, relative, sep } from 'node:path'
- import { pathToFileURL } from 'node:url'
- import { afterEach, describe, expect, it } from 'vitest'
- import { resolveDesktopPaths } from '../src/paths.ts'
- import {
- createSeedMetadata,
- DesktopProjectManager,
- packageNameFromSpec,
- verifySeedIntegrity,
- type DesktopProjectHooks,
- } from '../src/project-manager.ts'
- import { DESKTOP_HOST_PROTOCOL_VERSION } from '../src/host-protocol.ts'
- import { DESKTOP_PACKAGES_DIR, DESKTOP_PACKAGE_SET_FILE } from '../src/core-package-set.ts'
- import type { DesktopRelease } from '../src/release.ts'
- import { archivePnpmStore } from '../src/seed-store.ts'
- const roots: string[] = []
- const releaseWorkers: Array<() => Promise<void>> = []
- function temporaryRoot(): string {
- const root = mkdtempSync(join(tmpdir(), 'dsh-desktop-test-'))
- roots.push(root)
- return root
- }
- function writeIntegrity(seed: string): void {
- const paths: string[] = []
- const visit = (directory: string): void => {
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
- const path = join(directory, entry.name)
- if (entry.isDirectory()) visit(path)
- else if (entry.name !== 'integrity.json') paths.push(path)
- }
- }
- visit(seed)
- const files = paths.sort().map((path) => {
- const body = readFileSync(path)
- return {
- path: relative(seed, path).split(sep).join('/'),
- bytes: statSync(path).size,
- sha256: createHash('sha256').update(body).digest('hex'),
- }
- })
- writeFileSync(join(seed, 'integrity.json'), `${JSON.stringify({ schemaVersion: 2, files })}\n`)
- }
- function archiveStore(seed: string): void {
- const store = join(seed, 'store')
- mkdirSync(store, { recursive: true })
- if (readdirSync(store).length === 0) writeFileSync(join(store, 'test-entry'), 'content')
- archivePnpmStore(seed, store)
- }
- function writeCorePackageSet(seed: string, version: string): void {
- const packages = [
- { name: '@deepseek-ai/dsh', file: `deepseek-ai-dsh-${version}.tgz`, body: Buffer.from(`dsh-${version}`) },
- {
- name: '@deepseek-ai/dsh-desktop-host',
- file: `deepseek-ai-dsh-desktop-host-${version}.tgz`,
- body: Buffer.from(`desktop-host-${version}`),
- },
- ]
- mkdirSync(join(seed, DESKTOP_PACKAGES_DIR), { recursive: true })
- for (const entry of packages) writeFileSync(join(seed, DESKTOP_PACKAGES_DIR, entry.file), entry.body)
- writeFileSync(join(seed, DESKTOP_PACKAGE_SET_FILE), `${JSON.stringify({
- schemaVersion: 1,
- packages: packages.map(({ name, file, body }) => ({
- name,
- version,
- file,
- bytes: body.byteLength,
- integrity: `sha512-${createHash('sha512').update(body).digest('base64')}`,
- })),
- })}\n`)
- }
- function createTestSeedMetadata(seed: string, desktopRelease: DesktopRelease): void {
- writeCorePackageSet(seed, desktopRelease.version)
- createSeedMetadata(seed, desktopRelease)
- }
- function writeFakePnpm(root: string): string {
- const path = join(root, 'pnpm.mjs')
- writeFileSync(path, String.raw`
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
- import { dirname, join } from 'node:path'
- const args = process.argv.slice(2)
- const project = process.cwd()
- const command = args.find(value => value === 'install' || value === 'add' || value === 'remove')
- const manifestPath = join(project, 'package.json')
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
- const packageName = spec => spec.startsWith('@')
- ? spec.slice(0, spec.indexOf('@', spec.indexOf('/') + 1) === -1 ? undefined : spec.indexOf('@', spec.indexOf('/') + 1))
- : spec.split('@')[0]
- const packageVersion = spec => {
- const index = spec.startsWith('@') ? spec.indexOf('@', spec.indexOf('/') + 1) : spec.indexOf('@')
- return index === -1 ? '1.0.0' : spec.slice(index + 1)
- }
- if (command === 'add') {
- const spec = args[args.indexOf('add') + 1]
- manifest.dependencies[packageName(spec)] = packageVersion(spec)
- }
- if (command === 'remove') delete manifest.dependencies[args[args.indexOf('remove') + 1]]
- writeFileSync(manifestPath, JSON.stringify(manifest))
- rmSync(join(project, 'node_modules'), { recursive: true, force: true })
- for (const [name, version] of Object.entries(manifest.dependencies)) {
- const packageRoot = join(project, 'node_modules', ...name.split('/'))
- mkdirSync(packageRoot, { recursive: true })
- const core = name === '@deepseek-ai/dsh' || name === '@deepseek-ai/dsh-desktop-host'
- const plugin = !core
- const installedVersion = plugin
- ? version
- : JSON.parse(readFileSync(join(project, 'desktop-release.json'), 'utf8')).version
- writeFileSync(join(packageRoot, 'package.json'), JSON.stringify({
- name, version: installedVersion,
- ...(plugin ? { dsh: { bundle: { patch: './bundle.yml' } } } : {}),
- }))
- if (plugin) writeFileSync(join(packageRoot, 'bundle.yml'), '[]\n')
- else if (name === '@deepseek-ai/dsh-desktop-host') {
- mkdirSync(join(packageRoot, 'lib'), { recursive: true })
- writeFileSync(join(packageRoot, 'lib', 'index.js'), '')
- }
- }
- writeFileSync(join(project, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n')
- if (process.env.TEST_PNPM_LOG) writeFileSync(process.env.TEST_PNPM_LOG, JSON.stringify({ args, env: process.env }))
- `)
- return path
- }
- function writeBlockingFakePnpm(root: string, ready: string, release: string): string {
- const path = join(root, 'blocking-pnpm.mjs')
- const delegate = writeFakePnpm(root)
- writeFileSync(path, `
- import { existsSync, writeFileSync } from 'node:fs'
- import { setTimeout as sleep } from 'node:timers/promises'
- writeFileSync(${JSON.stringify(ready)}, String(process.pid))
- while (!existsSync(${JSON.stringify(release)})) await sleep(10)
- await import(${JSON.stringify(pathToFileURL(delegate).href)})
- `)
- return path
- }
- function hooks(overrides: Partial<DesktopProjectHooks> = {}): DesktopProjectHooks {
- return {
- healthCheck: async () => {},
- beforeActivate: async () => {},
- afterActivate: async () => {},
- ...overrides,
- }
- }
- function release(version = '1.0.0'): DesktopRelease {
- return {
- schemaVersion: 1,
- version,
- hostProtocolVersion: DESKTOP_HOST_PROTOCOL_VERSION,
- nodeVersion: '24.17.0',
- pnpmVersion: '11.7.0',
- }
- }
- afterEach(async () => {
- const cleanups = releaseWorkers.splice(0)
- const directories = roots.splice(0)
- const results = await Promise.allSettled(cleanups.map(cleanup => cleanup()))
- for (const root of directories) rmSync(root, { recursive: true, force: true })
- const failures: unknown[] = results.flatMap((result): unknown[] => result.status === 'rejected' ? [result.reason] : [])
- if (failures.length > 0) throw new AggregateError(failures, 'desktop worker cleanup failed')
- })
- describe('desktop package policy', () => {
- it('accepts registry package specs but rejects alternate sources and flags', () => {
- expect(packageNameFromSpec('@scope/plugin@1.2.3')).toBe('@scope/plugin')
- expect(packageNameFromSpec('plugin@next')).toBe('plugin')
- expect(() => packageNameFromSpec('file:../plugin')).toThrow(/unsupported npm package spec/u)
- expect(() => packageNameFromSpec('--registry=evil')).toThrow(/unsupported npm package spec/u)
- expect(() => packageNameFromSpec('https://example.test/plugin.tgz')).toThrow(/unsupported npm package spec/u)
- })
- it('rejects any seed content changed after release inventory generation', () => {
- const seed = join(temporaryRoot(), 'seed')
- createTestSeedMetadata(seed, release())
- writeFileSync(join(seed, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n')
- writeIntegrity(seed)
- expect(() => { verifySeedIntegrity(seed) }).not.toThrow()
- writeFileSync(join(seed, 'package.json'), '{}\n')
- expect(() => { verifySeedIntegrity(seed) }).toThrow(/integrity verification failed/u)
- })
- })
- describe('desktop project transactions', () => {
- it('installs the offline seed and reconciles a mismatched private Host', async () => {
- const root = temporaryRoot()
- const seed = join(root, 'seed')
- const log = join(root, 'pnpm-log.json')
- createTestSeedMetadata(seed, release())
- writeFileSync(join(seed, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n')
- mkdirSync(join(seed, 'store'), { recursive: true })
- writeFileSync(join(seed, 'store', 'seed-entry'), 'content')
- archiveStore(seed)
- writeIntegrity(seed)
- const paths = resolveDesktopPaths(join(root, '.dsh'))
- const manager = new DesktopProjectManager(paths, { node: process.execPath, pnpm: writeFakePnpm(root) })
- const previousLog = process.env.TEST_PNPM_LOG
- const previousRegistry = process.env.npm_config_registry
- process.env.TEST_PNPM_LOG = log
- process.env.npm_config_registry = 'https://user-registry.invalid'
- try {
- await expect(manager.applyRelease(seed, '2.0.0', hooks())).rejects.toThrow(/does not match Electron/u)
- await manager.applyRelease(seed, '1.0.0', hooks())
- writeFileSync(
- join(paths.profile, 'node_modules', '@deepseek-ai', 'dsh-desktop-host', 'package.json'),
- '{"name":"@deepseek-ai/dsh-desktop-host","version":"0.9.0"}\n',
- )
- await expect(manager.applyRelease(seed, '1.0.0', hooks())).resolves.toBe(true)
- } finally {
- if (previousLog === undefined) delete process.env.TEST_PNPM_LOG
- else process.env.TEST_PNPM_LOG = previousLog
- if (previousRegistry === undefined) delete process.env.npm_config_registry
- else process.env.npm_config_registry = previousRegistry
- }
- expect(manager.dshVersion()).toBe('1.0.0')
- expect(manager.releaseVersion()).toBe('1.0.0')
- expect(paths.profile).toBe(join(root, '.dsh', 'profiles', 'desktop'))
- expect(existsSync(join(paths.profile, 'node_modules', '@deepseek-ai', 'dsh'))).toBe(true)
- const installedHost = JSON.parse(readFileSync(
- join(paths.profile, 'node_modules', '@deepseek-ai', 'dsh-desktop-host', 'package.json'),
- 'utf8',
- )) as { version: string }
- expect(installedHost.version).toBe('1.0.0')
- expect(existsSync(join(paths.profile, 'desktop-plugins.json'))).toBe(false)
- expect(readFileSync(join(paths.pnpm.store, 'seed-entry'), 'utf8')).toBe('content')
- const invocation = JSON.parse(readFileSync(log, 'utf8')) as { args: string[]; env: Record<string, string> }
- expect(invocation.args).toContain('--offline')
- expect(invocation.args).toContain('--trust-lockfile')
- expect(invocation.args).toContain(`--config.store-dir=${paths.pnpm.store}`)
- expect(invocation.args).toContain('--config.enable-global-virtual-store=false')
- expect(invocation.args).toContain('--config.registry=https://registry.npmjs.org/')
- expect(invocation.env.NPM_CONFIG_REGISTRY).toBe('https://registry.npmjs.org/')
- expect(invocation.env.NPM_CONFIG_STORE_DIR).toBe(paths.pnpm.store)
- expect(invocation.env.NPM_CONFIG_USERCONFIG).toBe(join(paths.pnpm.config, 'npmrc'))
- expect(invocation.env.npm_config_registry).toBeUndefined()
- })
- it('restores the active project when the replacement backend cannot start', async () => {
- const root = temporaryRoot()
- const seed = join(root, 'seed')
- createTestSeedMetadata(seed, release())
- writeFileSync(join(seed, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n')
- archiveStore(seed)
- writeIntegrity(seed)
- const paths = resolveDesktopPaths(join(root, '.dsh'))
- const manager = new DesktopProjectManager(paths, { node: process.execPath, pnpm: writeFakePnpm(root) })
- await manager.applyRelease(seed, '1.0.0', hooks())
- let starts = 0
- await expect(manager.mutate({ type: 'plugin-add', spec: '@scope/plugin@2.0.0' }, hooks({
- afterActivate: async () => {
- starts += 1
- if (starts === 1) throw new Error('backend rejected staged graph')
- },
- }))).rejects.toThrow(/backend rejected staged graph/u)
- expect(manager.listPlugins()).toEqual([])
- expect(manager.dshVersion()).toBe('1.0.0')
- expect(starts).toBe(2)
- })
- it('restores rollback when the active move completed before its journal update', async () => {
- const root = temporaryRoot()
- const seed = join(root, 'seed')
- createTestSeedMetadata(seed, release())
- writeFileSync(join(seed, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n')
- archiveStore(seed)
- writeIntegrity(seed)
- const paths = resolveDesktopPaths(join(root, '.dsh'))
- const manager = new DesktopProjectManager(paths, { node: process.execPath, pnpm: writeFakePnpm(root) })
- await manager.applyRelease(seed, '1.0.0', hooks())
- await manager.mutate({ type: 'plugin-add', spec: '@scope/plugin@2.0.0' }, hooks())
- const stagingProfile = join(paths.staging, 'interrupted', 'profile')
- mkdirSync(stagingProfile, { recursive: true })
- writeFileSync(join(stagingProfile, 'marker'), 'staging')
- rmSync(paths.rollback, { recursive: true, force: true })
- mkdirSync(dirname(paths.rollback), { recursive: true })
- renameSync(paths.profile, paths.rollback)
- writeFileSync(paths.pending, `${JSON.stringify({
- schemaVersion: 1,
- id: 'interrupted',
- stagingProfile,
- step: 'prepared',
- })}\n`)
- manager.recover()
- expect(manager.listPlugins()).toEqual([{ name: '@scope/plugin', version: '2.0.0' }])
- expect(existsSync(stagingProfile)).toBe(false)
- expect(existsSync(paths.pending)).toBe(false)
- })
- it('records the live pnpm worker as transaction owner until it exits', async ({ task, signal }) => {
- const root = temporaryRoot()
- const seed = join(root, 'seed')
- const ready = join(root, 'pnpm-ready')
- const releaseWorker = join(root, 'pnpm-release')
- createTestSeedMetadata(seed, release())
- writeFileSync(join(seed, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n')
- archiveStore(seed)
- writeIntegrity(seed)
- const paths = resolveDesktopPaths(join(root, '.dsh'))
- const runtime = { node: process.execPath, pnpm: writeBlockingFakePnpm(root, ready, releaseWorker) }
- const manager = new DesktopProjectManager(paths, runtime)
- const installing = manager.applyRelease(seed, '1.0.0', hooks())
- // Teardown observes failures even if the runner has abandoned the test body.
- const completed = installing.then(value => ({ value }), (error: unknown) => ({ error }))
- releaseWorkers.push(async () => {
- writeFileSync(releaseWorker, 'continue')
- const outcome = await completed
- if ('error' in outcome) throw outcome.error
- })
- // Child startup shares the test budget; an aborted poll must not resume ownership assertions.
- await expect.poll(() => {
- signal.throwIfAborted()
- return existsSync(ready)
- }, { timeout: task.timeout }).toBe(true)
- signal.throwIfAborted()
- const workerPid = Number.parseInt(readFileSync(ready, 'utf8'), 10)
- expect(readFileSync(paths.lock, 'utf8')).toBe(`${String(workerPid)}\n`)
- const competing = new DesktopProjectManager(paths, runtime)
- await expect(competing.applyRelease(seed, '1.0.0', hooks())).rejects.toThrow(/another package transaction is active/u)
- writeFileSync(releaseWorker, 'continue')
- await expect(installing).resolves.toBe(true)
- expect(existsSync(paths.lock)).toBe(false)
- })
- it('keeps core packages local while installing plugins from the desktop registry', async () => {
- const root = temporaryRoot()
- const seed = join(root, 'seed')
- const log = join(root, 'pnpm-log.json')
- createTestSeedMetadata(seed, release())
- writeFileSync(join(seed, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n')
- archiveStore(seed)
- writeIntegrity(seed)
- const paths = resolveDesktopPaths(join(root, '.dsh'))
- const manager = new DesktopProjectManager(paths, { node: process.execPath, pnpm: writeFakePnpm(root) })
- await manager.applyRelease(seed, '1.0.0', hooks())
- const previousLog = process.env.TEST_PNPM_LOG
- process.env.TEST_PNPM_LOG = log
- try {
- await manager.mutate({ type: 'plugin-add', spec: '@scope/plugin@2.0.0' }, hooks())
- } finally {
- if (previousLog === undefined) delete process.env.TEST_PNPM_LOG
- else process.env.TEST_PNPM_LOG = previousLog
- }
- const manifest = JSON.parse(readFileSync(join(paths.profile, 'package.json'), 'utf8')) as {
- dependencies: Record<string, string>
- }
- const coreSpec = manifest.dependencies['@deepseek-ai/dsh']
- expect(coreSpec).toMatch(/^file:\.\/desktop-packages\//u)
- expect(readFileSync(join(paths.profile, 'pnpm-workspace.yaml'), 'utf8'))
- .toContain(`${JSON.stringify('@deepseek-ai/dsh')}: ${JSON.stringify(coreSpec)}`)
- expect(manifest.dependencies['@scope/plugin']).toBe('2.0.0')
- const invocation = JSON.parse(readFileSync(log, 'utf8')) as { args: string[]; env: Record<string, string> }
- expect(invocation.args).toContain('add')
- expect(invocation.args).toContain('@scope/plugin@2.0.0')
- expect(invocation.args).toContain('--config.registry=https://registry.npmjs.org/')
- expect(invocation.env.NPM_CONFIG_REGISTRY).toBe('https://registry.npmjs.org/')
- })
- it('reconciles dsh to the packaged release without removing desktop plugins', async () => {
- const root = temporaryRoot()
- const paths = resolveDesktopPaths(join(root, '.dsh'))
- const manager = new DesktopProjectManager(paths, { node: process.execPath, pnpm: writeFakePnpm(root) })
- const firstSeed = join(root, 'seed-1')
- createTestSeedMetadata(firstSeed, release('1.0.0'))
- writeFileSync(join(firstSeed, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n')
- mkdirSync(join(firstSeed, 'store'), { recursive: true })
- writeFileSync(join(firstSeed, 'store', 'release-1'), 'one')
- archiveStore(firstSeed)
- writeIntegrity(firstSeed)
- await manager.applyRelease(firstSeed, '1.0.0', hooks())
- await manager.mutate({ type: 'plugin-add', spec: '@scope/plugin@2.0.0' }, hooks())
- const nextSeed = join(root, 'seed-2')
- createTestSeedMetadata(nextSeed, release('1.1.0'))
- writeFileSync(join(nextSeed, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n')
- mkdirSync(join(nextSeed, 'store'), { recursive: true })
- writeFileSync(join(nextSeed, 'store', 'release-2'), 'two')
- archiveStore(nextSeed)
- writeIntegrity(nextSeed)
- await expect(manager.applyRelease(nextSeed, '1.1.0', hooks())).resolves.toBe(true)
- expect(manager.releaseVersion()).toBe('1.1.0')
- expect(manager.dshVersion()).toBe('1.1.0')
- expect(manager.listPlugins()).toEqual([{ name: '@scope/plugin', version: '2.0.0' }])
- const profile = JSON.parse(readFileSync(join(paths.profile, 'package.json'), 'utf8')) as {
- dsh: { profile: { bundles: string[] } }
- }
- expect(profile.dsh.profile.bundles).toEqual([
- '@deepseek-ai/dsh-base',
- '@deepseek-ai/dsh-web-app',
- '@scope/plugin',
- ])
- expect(readFileSync(join(paths.pnpm.store, 'release-1'), 'utf8')).toBe('one')
- expect(readFileSync(join(paths.pnpm.store, 'release-2'), 'utf8')).toBe('two')
- await expect(manager.applyRelease(nextSeed, '1.1.0', hooks())).resolves.toBe(false)
- })
- })
|