| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
- import { tmpdir } from 'node:os'
- import { join } from 'node:path'
- import { afterEach, describe, expect, it, vi } from 'vitest'
- import { DesktopHostProcess } from '../src/host-process.ts'
- const roots: string[] = []
- const hosts: DesktopHostProcess[] = []
- const HTTP_HOST = `
- import { createServer } from 'node:http'
- import { writeFileSync } from 'node:fs'
- import { join } from 'node:path'
- const server = createServer((request, response) => {
- if (request.url === '/fatal') {
- process.send({ type: 'fatal', message: 'plugin unavailable' })
- response.end('reported')
- return
- }
- if (request.url === '/crash') {
- response.end('exiting', () => {
- process.stderr.write('plugin crashed', () => process.exit(7))
- })
- return
- }
- response.setHeader('content-type', 'application/json')
- response.end(JSON.stringify({runtime: process.argv[2], profile: process.argv[3], cwd: process.cwd(), nodePath: process.env.NODE_PATH, registry: process.env.NPM_CONFIG_REGISTRY, nodeOptions: process.env.NODE_OPTIONS, runAsNode: process.env.ELECTRON_RUN_AS_NODE, internals: process.execArgv.includes('--expose-internals')}))
- })
- server.listen(0, '127.0.0.1', () => {
- process.send({ type: 'ready', url: 'http://127.0.0.1:' + server.address().port + '/?token=fixture' })
- })
- process.on('message', message => {
- if (message.type !== 'shutdown') return
- server.close(() => {
- writeFileSync(join(process.argv[3], 'stopped'), '')
- process.disconnect()
- })
- server.closeAllConnections()
- })
- `
- function projectWithHost(source = HTTP_HOST): string {
- const project = mkdtempSync(join(tmpdir(), 'dsh-desktop-host-test-'))
- roots.push(project)
- const packageRoot = join(project, 'node_modules', '@deepseek-ai', 'dsh-desktop-host')
- mkdirSync(join(packageRoot, 'lib'), { recursive: true })
- writeFileSync(join(packageRoot, 'package.json'), '{"name":"@deepseek-ai/dsh-desktop-host","type":"module"}\n')
- writeFileSync(join(packageRoot, 'lib', 'index.js'), source)
- return project
- }
- function hostProcess(
- runtime: string, profile = runtime, onFailure?: (error: Error) => void, environment = process.env,
- ): DesktopHostProcess {
- const host = new DesktopHostProcess(process.execPath, runtime, profile, undefined, environment, onFailure)
- hosts.push(host)
- return host
- }
- afterEach(async () => {
- await Promise.all(hosts.splice(0).map(host => host.stop()))
- for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
- })
- describe('desktop host process', () => {
- it('returns the Web authentication URL and waits for graceful shutdown', async () => {
- const runtime = projectWithHost()
- const failure = vi.fn()
- const host = hostProcess(runtime, runtime, failure)
- const ready = await host.start()
- expect(new URL(ready.url).searchParams.get('token')).toBe('fixture')
- expect(await host.start()).toEqual(ready)
- expect((await fetch(ready.url)).status).toBe(200)
- await host.stop()
- expect(existsSync(join(runtime, 'stopped'))).toBe(true)
- await expect(fetch(ready.url)).rejects.toThrow()
- expect(failure).not.toHaveBeenCalled()
- })
- it('passes external dependencies and runtime profile resolution to the Host', async () => {
- const runtime = projectWithHost(HTTP_HOST.replace('runtime: process.argv[2]',
- 'primaryRuntime: process.argv[4], profileResolution: process.argv[5], runtime: process.argv[2]'))
- const primaryRuntime = join(runtime, 'external-primary-runtime')
- const host = new DesktopHostProcess(process.execPath, runtime, runtime, undefined, process.env,
- undefined, primaryRuntime, 'runtime')
- hosts.push(host)
- const { url } = await host.start()
- expect(await (await fetch(url)).json()).toMatchObject({ primaryRuntime, profileResolution: 'runtime' })
- })
- it('reports a fatal event after readiness once', async () => {
- const runtime = projectWithHost()
- const failure = vi.fn()
- const host = hostProcess(runtime, runtime, failure)
- const { url } = await host.start()
- await fetch(new URL('/fatal', url))
- await expect.poll(() => failure.mock.calls.length).toBe(1)
- await host.stop()
- expect(failure).toHaveBeenCalledTimes(1)
- expect(failure).toHaveBeenCalledWith(new Error('plugin unavailable'))
- })
- it('reports a child crash after readiness with its stderr diagnostic', async () => {
- const runtime = projectWithHost()
- const failure = vi.fn()
- const host = hostProcess(runtime, runtime, failure)
- const { url } = await host.start()
- await fetch(new URL('/crash', url))
- await expect.poll(() => failure.mock.calls.length).toBe(1)
- expect(failure).toHaveBeenCalledWith(new Error('dsh desktop host exited with 7: plugin crashed'))
- })
- it('retains only recent diagnostics from a noisy child', async () => {
- const runtime = projectWithHost('process.stderr.write(\'discarded-prefix\' + \'x\'.repeat(70_000) + \'recent-failure\', () => { process.exitCode = 7; process.disconnect() })')
- const failure = await hostProcess(runtime).start().catch((error: unknown) => error)
- expect(failure).toBeInstanceOf(Error)
- const message = (failure as Error).message
- expect(message).not.toContain('discarded-prefix')
- expect(message.endsWith('recent-failure')).toBe(true)
- expect(message.length).toBeLessThan(66_000)
- })
- it('settles teardown when the executable cannot be spawned', async () => {
- const runtime = projectWithHost()
- const host = new DesktopHostProcess(join(runtime, 'missing-node'), runtime, runtime)
- hosts.push(host)
- await expect(host.start()).rejects.toThrow()
- await host.stop()
- })
- it('loads the resource entry with a separate profile and inherits runtime and package-manager configuration', async () => {
- const runtime = projectWithHost()
- const profile = mkdtempSync(join(tmpdir(), 'desktop-external-profile-'))
- roots.push(profile)
- const host = hostProcess(runtime, profile, undefined, {
- ...process.env, NODE_OPTIONS: '--no-warnings', NODE_PATH: '/custom', NPM_CONFIG_REGISTRY: 'https://registry.example.test/',
- })
- const { url } = await host.start()
- const response = await fetch(url)
- expect(await response.json()).toEqual({ runtime, profile, cwd: realpathSync(profile), nodePath: '/custom', registry: 'https://registry.example.test/', nodeOptions: '--no-warnings', runAsNode: '1', internals: true })
- })
- it.each([
- ["process.send({ type: 'fatal', message: 'startup failed' }); process.disconnect()", 'startup failed'],
- ["process.send({ type: 'ready', url: 4 })", 'invalid IPC event'],
- ['process.exit(0)', 'host stopped'],
- ])('rejects startup when the child fails before readiness: %s', async (source, message) => {
- const host = hostProcess(projectWithHost(source))
- await expect(host.start()).rejects.toThrow(message)
- })
- })
|