| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- /**
- * Web runtime glue behavior: dist resolution through the bundle's own hook,
- * the frontend-static child claiming the fallback seat, the web-surface
- * prompt section and bash runtime variables, and readiness publication through
- * the URL line and default-browser handoff.
- */
- import { EventEmitter } from 'node:events'
- import { spawn, type ChildProcess } from 'node:child_process'
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
- import { tmpdir } from 'node:os'
- import { join } from 'node:path'
- import { PassThrough } from 'node:stream'
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
- import { Context } from '@deepseek-ai/cordis'
- import { createLaunchEnvironmentSnapshot, DSH_LAUNCH_ENVIRONMENT_KEY } from '@deepseek-ai/dsh-launch-environment'
- import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
- import type { WebServer } from '@deepseek-ai/dsh-host-webserver'
- import { apply, Config, internals } from '../src/index.ts'
- vi.mock('node:child_process', async importOriginal => ({
- ...await importOriginal<typeof import('node:child_process')>(),
- spawn: vi.fn(),
- }))
- vi.mock('node:os', async importOriginal => ({
- ...await importOriginal<typeof import('node:os')>(),
- networkInterfaces: () => ({
- lo0: [{ family: 'IPv4', internal: true, address: '127.0.0.1' }],
- en0: [{ family: 'IPv4', internal: false, address: '192.168.1.5' }],
- }),
- }))
- let dist: string | undefined
- beforeEach(() => {
- vi.stubEnv('SSH_CONNECTION', '')
- vi.stubEnv('SSH_TTY', '')
- })
- afterEach(() => {
- vi.restoreAllMocks()
- vi.mocked(spawn).mockReset()
- vi.unstubAllEnvs()
- internals.resolveDistIndex = originalResolve
- internals.openBrowser = originalOpenBrowser
- if (dist !== undefined) rmSync(dist, { recursive: true, force: true })
- dist = undefined
- })
- const originalResolve = internals.resolveDistIndex
- const originalOpenBrowser = internals.openBrowser
- type BrowserLauncher = ChildProcess & { stderr: PassThrough }
- /** Minimal browser-launcher process for the native handoff adapter. */
- function launcher(): BrowserLauncher {
- return Object.assign(new EventEmitter(), { stderr: new PassThrough() }) as unknown as BrowserLauncher
- }
- /** Stage a dist fixture and point the bundle's resolver at it. */
- function stageDist(): string {
- dist = mkdtempSync(join(tmpdir(), 'dsh-web-app-'))
- mkdirSync(join(dist, 'dist'))
- const index = join(dist, 'dist', 'index.html')
- writeFileSync(index, '<head></head><body>shell</body>')
- internals.resolveDistIndex = () => index
- return index
- }
- /** A fake webServer capturing the fallback seat and index taps. */
- function fakeHttpServer(host: '127.0.0.1' | '0.0.0.0' = '127.0.0.1'): { server: WebServer; seat: () => unknown } {
- let fallback: unknown
- const server = {
- host,
- port: 4567,
- registerFallback: (handler: unknown) => {
- fallback = handler
- return () => { fallback = undefined }
- },
- renderIndex: (html: string) => html,
- } as unknown as WebServer
- return { server, seat: () => fallback }
- }
- /** Deterministic Host Connection face for URL publication and frontend injection. */
- function provideConnection(ctx: Context): void {
- ctx.provide('connection', {
- authenticatedUrl(baseUrl: string) {
- const url = new URL(baseUrl)
- url.pathname = '/'
- url.searchParams.set('token', 'test-token')
- return url.href
- },
- authorizeIndex: () => true,
- requestRejection: () => undefined,
- rpc: {},
- } as never)
- }
- /** A fake Loader whose settlement the test controls (the URL line waits on it). */
- function provideLoader(ctx: Context, settle: () => Promise<void> = async () => {}): void {
- ctx.provide('loader', { await: settle } as never)
- }
- interface BashContribution {
- name: string
- variables: Record<string, { description: string }>
- resolve: () => Record<string, string>
- }
- describe('web-app runtime glue', () => {
- it('mounts dist serving, prompt section, bash variables, and publishes the URL with the LAN snapshot', async () => {
- stageDist()
- const ctx = new Context()
- // Editor markers and a project .env SSH value do not establish a remote launch.
- ctx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, createLaunchEnvironmentSnapshot([
- { source: 'process', values: { VSCODE_IPC_HOOK_CLI: '/tmp/local-vscode-ipc' } },
- { source: 'project-env', path: '/work/.env', values: { SSH_CONNECTION: 'stale-project-value' } },
- ]))
- const { server, seat } = fakeHttpServer('0.0.0.0')
- ctx.provide('webServer', server)
- provideConnection(ctx)
- const contributions: BashContribution[] = []
- ctx.provide('shellEnv', {
- register: (contribution: BashContribution) => {
- contributions.push(contribution)
- return () => {}
- },
- } as never)
- provideLoader(ctx)
- const lifecycle: string[] = []
- const log = vi.spyOn(console, 'log').mockImplementation((message) => { lifecycle.push(String(message)) })
- const openBrowser = vi.fn(async (url: string) => { lifecycle.push(`open:${url}`) })
- internals.openBrowser = openBrowser
- apply(ctx, new Config({ openBrowser: true, printUrl: true, surfaceContext: true, trustedHosts: ['lab.internal'] }))
- await ctx.plugin(SystemPrompt, { personaPrefix: '' })
- // Settle the injected registrations.
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(seat()).toBeDefined() // frontend-static claimed the fallback
- expect(ctx.get('webRuntime')).toEqual({
- lanAddresses: ['192.168.1.5'],
- trustedHosts: ['192.168.1.5', 'lab.internal'],
- })
- expect(log).toHaveBeenCalledWith('dsh web: http://127.0.0.1:4567/?token=test-token (LAN: http://192.168.1.5:4567/?token=test-token)')
- expect(log).toHaveBeenCalledWith('dsh web: opening the default browser; pass --no-open to disable')
- expect(openBrowser).toHaveBeenCalledWith('http://127.0.0.1:4567/?token=test-token')
- expect(lifecycle).toEqual([
- 'dsh web: http://127.0.0.1:4567/?token=test-token (LAN: http://192.168.1.5:4567/?token=test-token)',
- 'dsh web: opening the default browser; pass --no-open to disable',
- 'open:http://127.0.0.1:4567/?token=test-token',
- ])
- const assembly = await ctx.systemPrompt.assemble()
- expect(assembly.sections.find(entry => entry.name === 'harness:source')?.text).toContain('DeepSeek Harness implementation checkout')
- const section = assembly.sections.find(entry => entry.name === 'app:web-surface')
- expect(section?.text).toContain('http://127.0.0.1:4567')
- // The single update contract: the receiver is always on; no-refresh
- // reloads additionally need the rebuild watcher.
- expect(section?.text).toContain('pnpm run dev:web')
- const webRuntime = contributions.find(contribution => contribution.name === 'web-runtime')
- expect(webRuntime?.resolve()).toEqual({ DSH_WEB_URL: 'http://127.0.0.1:4567' })
- await ctx.fiber.dispose()
- })
- it('publishes no readiness side effect when printing and browser opening are disabled', async () => {
- stageDist()
- const ctx = new Context()
- ctx.provide('webServer', fakeHttpServer().server)
- provideConnection(ctx)
- const log = vi.spyOn(console, 'log').mockImplementation(() => {})
- const openBrowser = vi.fn(async () => {})
- internals.openBrowser = openBrowser
- apply(ctx, new Config({ openBrowser: false, printUrl: false, surfaceContext: true, trustedHosts: [] }))
- await ctx.plugin(SystemPrompt, { personaPrefix: '' })
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(log).not.toHaveBeenCalled()
- expect(openBrowser).not.toHaveBeenCalled()
- const assembly = await ctx.systemPrompt.assemble()
- expect(assembly.sections.find(entry => entry.name === 'app:web-surface')?.text)
- .toContain('rebuilding the affected Web artifacts')
- await ctx.fiber.dispose()
- })
- it('skips the surface context when disabled (the one-shot layer): no prompt section, no bash variables', async () => {
- stageDist()
- const ctx = new Context()
- ctx.provide('webServer', fakeHttpServer().server)
- provideConnection(ctx)
- const contributions: BashContribution[] = []
- ctx.provide('shellEnv', {
- register: (contribution: BashContribution) => {
- contributions.push(contribution)
- return () => {}
- },
- } as never)
- apply(ctx, new Config({ openBrowser: false, printUrl: false, surfaceContext: false, trustedHosts: [] }))
- await ctx.plugin(SystemPrompt, { personaPrefix: '' })
- await new Promise(resolve => setTimeout(resolve, 0))
- const assembly = await ctx.systemPrompt.assemble()
- expect(assembly.sections.some(entry => entry.name === 'app:web-surface')).toBe(false)
- expect(assembly.sections.some(entry => entry.name === 'harness:source')).toBe(false)
- expect(contributions).toEqual([])
- await ctx.fiber.dispose()
- })
- it('prints the loopback-only URL line when no LAN snapshot exists', async () => {
- stageDist()
- const ctx = new Context()
- ctx.provide('webServer', fakeHttpServer().server)
- provideConnection(ctx)
- const log = vi.spyOn(console, 'log').mockImplementation(() => {})
- apply(ctx, new Config({ openBrowser: false, printUrl: true, surfaceContext: true, trustedHosts: [] }))
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(log).toHaveBeenCalledWith('dsh web: http://127.0.0.1:4567/?token=test-token')
- await ctx.fiber.dispose()
- })
- it('does not publish readiness again when Connection reloads', async () => {
- stageDist()
- const ctx = new Context()
- ctx.provide('webServer', fakeHttpServer().server)
- const first = ctx.plugin((connectionCtx: Context) => { provideConnection(connectionCtx) })
- await first
- const log = vi.spyOn(console, 'log').mockImplementation(() => {})
- apply(ctx, new Config({ openBrowser: false, printUrl: true, surfaceContext: true, trustedHosts: [] }))
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(log).toHaveBeenCalledTimes(1)
- await first.dispose()
- await ctx.plugin((connectionCtx: Context) => { provideConnection(connectionCtx) })
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(log).toHaveBeenCalledTimes(1)
- await ctx.fiber.dispose()
- })
- it.each([
- ['SSH_CONNECTION', '10.0.0.2 55000 10.0.0.9 22'],
- ['SSH_TTY', '/dev/pts/3'],
- ] as const)('prints the host URL but skips browser handoff when %s marks an SSH launch', async (name, value) => {
- vi.stubEnv(name, value)
- stageDist()
- const ctx = new Context()
- ctx.provide('webServer', fakeHttpServer().server)
- provideConnection(ctx)
- const log = vi.spyOn(console, 'log').mockImplementation(() => {})
- const openBrowser = vi.fn(async () => {})
- internals.openBrowser = openBrowser
- apply(ctx, new Config({ openBrowser: true, printUrl: true, surfaceContext: false, trustedHosts: [] }))
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(log).toHaveBeenCalledWith('dsh web: http://127.0.0.1:4567/?token=test-token')
- expect(openBrowser).not.toHaveBeenCalled()
- await ctx.fiber.dispose()
- })
- it('defers readiness publication until Loader settlement and drops it on failure or teardown', async () => {
- stageDist()
- const openBrowser = vi.fn(async () => {})
- internals.openBrowser = openBrowser
- // Settlement path: both actions wait for loader.await() so their consumers
- // can request the complete app immediately.
- const settled = new Context()
- settled.provide('webServer', fakeHttpServer().server)
- provideConnection(settled)
- let release: () => void
- const settlement = new Promise<void>((resolve) => { release = resolve })
- provideLoader(settled, () => settlement)
- const log = vi.spyOn(console, 'log').mockImplementation(() => {})
- apply(settled, new Config({ openBrowser: true, printUrl: true, surfaceContext: true, trustedHosts: [] }))
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(log).not.toHaveBeenCalled()
- expect(openBrowser).not.toHaveBeenCalled()
- release!()
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(log).toHaveBeenCalledWith('dsh web: http://127.0.0.1:4567/?token=test-token')
- expect(openBrowser).toHaveBeenCalledWith('http://127.0.0.1:4567/?token=test-token')
- await settled.fiber.dispose()
- // Failed path: Loader reports the sibling failure; the app prints no URL
- // for a process that is about to exit.
- log.mockClear()
- openBrowser.mockClear()
- const failed = new Context()
- failed.provide('webServer', fakeHttpServer().server)
- provideConnection(failed)
- provideLoader(failed, async () => { throw new Error('boot failed') })
- apply(failed, new Config({ openBrowser: true, printUrl: true, surfaceContext: true, trustedHosts: [] }))
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(log).not.toHaveBeenCalled()
- expect(openBrowser).not.toHaveBeenCalled()
- await failed.fiber.dispose()
- // Torn-down path: settlement resolves after the webserver is gone — no
- // line, no crash.
- log.mockClear()
- openBrowser.mockClear()
- const torn = new Context()
- const child = torn.plugin((childCtx: Context) => {
- childCtx.provide('webServer', fakeHttpServer().server)
- provideConnection(childCtx)
- })
- await child
- let releaseTorn: () => void
- const tornSettlement = new Promise<void>((resolve) => { releaseTorn = resolve })
- provideLoader(torn, () => tornSettlement)
- apply(torn, new Config({ openBrowser: true, printUrl: true, surfaceContext: true, trustedHosts: [] }))
- await new Promise(resolve => setTimeout(resolve, 0))
- await child.dispose() // the webServer service goes away
- releaseTorn!()
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(log).not.toHaveBeenCalled()
- expect(openBrowser).not.toHaveBeenCalled()
- await torn.fiber.dispose()
- })
- it('fails loud when the prompt section resolves against a portless webserver', async () => {
- stageDist()
- const ctx = new Context()
- // A webserver whose bound port is gone (torn down mid-request): the
- // section must throw, never render a URL with an undefined port.
- const { server } = fakeHttpServer()
- Object.defineProperty(server, 'port', { get: () => undefined })
- ctx.provide('webServer', server)
- provideConnection(ctx)
- apply(ctx, new Config({ openBrowser: false, printUrl: false, surfaceContext: true, trustedHosts: [] }))
- await ctx.plugin(SystemPrompt, { personaPrefix: '' })
- await new Promise(resolve => setTimeout(resolve, 0))
- await expect(ctx.systemPrompt.assemble()).rejects.toThrow('webServer service missing')
- await ctx.fiber.dispose()
- })
- it('anchors the dist index on the frontend package manifest without requiring a built dist', () => {
- // The production resolver (not the test hook): the anchor resolves on any
- // checkout, built or not — dist existence is the fallback owner's
- // request-time concern, so a dist-less composition (the static worker
- // preview ships its own page) still boots.
- expect(originalResolve()).toMatch(/dist[/\\]index\.html$/)
- })
- it.each([
- ['Error', new Error('no desktop'), 'no desktop'],
- ['non-Error', 'desktop unavailable', 'desktop unavailable'],
- ] as const)('keeps the server running and reports the manual URL when a browser failure is %s', async (_kind, failure, reason) => {
- stageDist()
- const ctx = new Context()
- ctx.provide('webServer', fakeHttpServer().server)
- provideConnection(ctx)
- internals.openBrowser = vi.fn(async () => { throw failure })
- const log = vi.spyOn(console, 'log').mockImplementation(() => {})
- const diagnostic = vi.spyOn(console, 'error').mockImplementation(() => {})
- apply(ctx, new Config({ openBrowser: true, printUrl: false, surfaceContext: false, trustedHosts: [] }))
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(log).toHaveBeenCalledWith('dsh web: opening the default browser; pass --no-open to disable')
- expect(diagnostic).toHaveBeenCalledWith(
- `web-app: could not open the default browser because ${reason}; use the dsh web URL printed at startup`,
- )
- expect(ctx.get('webServer')).toBeDefined()
- await ctx.fiber.dispose()
- })
- it('scrubs the helper environment and reports helper spawn or exit failures', async () => {
- vi.stubEnv('DEEPSEEK_API_KEY', 'must-not-reach-browser')
- vi.stubEnv('DSH_HOME', '/must-not-reach-browser')
- const completed = launcher()
- vi.mocked(spawn).mockReturnValueOnce(completed)
- const completion = originalOpenBrowser('http://127.0.0.1:4567')
- const [command, args, options] = vi.mocked(spawn).mock.calls[0]!
- expect(command).toBe(process.execPath)
- expect(args).toEqual([
- '--input-type=module',
- '--eval', expect.stringContaining('await import('),
- '--', 'http://127.0.0.1:4567',
- ])
- expect(args?.[2]).toContain("if (process.platform === 'win32')")
- expect(args?.[2]).toContain('launcher.ref()')
- expect(options?.env).not.toHaveProperty('DEEPSEEK_API_KEY')
- expect(options?.env).not.toHaveProperty('DSH_HOME')
- expect(options?.env?.PATH).toBe(process.env.PATH)
- expect(options?.stdio).toEqual(['ignore', 'inherit', 'pipe'])
- completed.emit('close', 0)
- await expect(completion).resolves.toBeUndefined()
- expect(completed.listenerCount('error')).toBe(0)
- const completedWithStderr = launcher()
- vi.mocked(spawn).mockReturnValueOnce(completedWithStderr)
- const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
- const completionWithStderr = originalOpenBrowser('http://127.0.0.1:4567')
- completedWithStderr.stderr?.write('launcher note\n')
- completedWithStderr.emit('close', 0)
- await expect(completionWithStderr).resolves.toBeUndefined()
- expect(stderr).toHaveBeenCalledWith('launcher note\n')
- const failedWithReason = launcher()
- vi.mocked(spawn).mockReturnValueOnce(failedWithReason)
- const reasonFailure = originalOpenBrowser('http://127.0.0.1:4567')
- const reasonAssertion = expect(reasonFailure).rejects.toThrow('desktop unavailable')
- failedWithReason.stderr?.write('Error: desktop unavailable\n at fixture')
- failedWithReason.emit('close', 1)
- await reasonAssertion
- const failed = launcher()
- vi.mocked(spawn).mockReturnValueOnce(failed)
- const failure = originalOpenBrowser('http://127.0.0.1:4567')
- const failureAssertion = expect(failure).rejects.toThrow('exited with code 3')
- await Promise.resolve()
- failed.emit('close', 3)
- await failureAssertion
- const errored = launcher()
- vi.mocked(spawn).mockReturnValueOnce(errored)
- const error = originalOpenBrowser('http://127.0.0.1:4567')
- const errorAssertion = expect(error).rejects.toThrow('spawn failed')
- await Promise.resolve()
- errored.emit('error', new Error('spawn failed'))
- await errorAssertion
- expect(errored.listenerCount('close')).toBe(0)
- })
- })
|