| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231 |
- /**
- * 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 URL-line printing with the
- * runtime's bind-dependent LAN snapshot.
- */
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
- import { tmpdir } from 'node:os'
- import { join } from 'node:path'
- import { afterEach, describe, expect, it, vi } from 'vitest'
- import { Context } from '@deepseek-ai/cordis'
- import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
- import type { HttpServerService } from '@deepseek-ai/dsh-host-webserver'
- import { apply, Config, internals } from '../src/index.ts'
- 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
- afterEach(() => {
- vi.restoreAllMocks()
- internals.resolveDistIndex = originalResolve
- if (dist !== undefined) rmSync(dist, { recursive: true, force: true })
- dist = undefined
- })
- const originalResolve = internals.resolveDistIndex
- /** 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 httpServer capturing the fallback seat and index taps. */
- function fakeHttpServer(host: '127.0.0.1' | '0.0.0.0' = '127.0.0.1'): { server: HttpServerService; seat: () => unknown } {
- let fallback: unknown
- const server = {
- host,
- port: 4567,
- registerFallback: (handler: unknown) => {
- fallback = handler
- return () => { fallback = undefined }
- },
- applyIndexTaps: (html: string) => html,
- } as unknown as HttpServerService
- return { server, seat: () => fallback }
- }
- /** 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 prints the URL with the LAN snapshot', async () => {
- stageDist()
- const ctx = new Context()
- const { server, seat } = fakeHttpServer('0.0.0.0')
- ctx.provide('httpServer', server)
- const contributions: BashContribution[] = []
- ctx.provide('bashEnv', {
- register: (contribution: BashContribution) => {
- contributions.push(contribution)
- return () => {}
- },
- } as never)
- provideLoader(ctx)
- const log = vi.spyOn(console, 'log').mockImplementation(() => {})
- apply(ctx, new Config({ printUrl: true, surfaceContext: true, trustedHosts: ['lab.internal'] }))
- await ctx.plugin(SystemPrompt, { persona: '' })
- // 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 (LAN: http://192.168.1.5:4567)')
- 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('stays quiet with printUrl off', async () => {
- stageDist()
- const ctx = new Context()
- ctx.provide('httpServer', fakeHttpServer().server)
- const log = vi.spyOn(console, 'log').mockImplementation(() => {})
- apply(ctx, new Config({ printUrl: false, surfaceContext: true, trustedHosts: [] }))
- await ctx.plugin(SystemPrompt, { persona: '' })
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(log).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('httpServer', fakeHttpServer().server)
- const contributions: BashContribution[] = []
- ctx.provide('bashEnv', {
- register: (contribution: BashContribution) => {
- contributions.push(contribution)
- return () => {}
- },
- } as never)
- apply(ctx, new Config({ printUrl: false, surfaceContext: false, trustedHosts: [] }))
- await ctx.plugin(SystemPrompt, { persona: '' })
- 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('httpServer', fakeHttpServer().server)
- const log = vi.spyOn(console, 'log').mockImplementation(() => {})
- apply(ctx, new Config({ printUrl: true, surfaceContext: true, trustedHosts: [] }))
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(log).toHaveBeenCalledWith('dsh web: http://127.0.0.1:4567')
- await ctx.fiber.dispose()
- })
- it('defers the URL line until Loader settlement and drops it on failure or teardown', async () => {
- stageDist()
- // Settlement path: the line waits for loader.await() so supervisors can
- // RPC immediately after observing it.
- const settled = new Context()
- settled.provide('httpServer', fakeHttpServer().server)
- 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({ printUrl: true, surfaceContext: true, trustedHosts: [] }))
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(log).not.toHaveBeenCalled()
- release!()
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(log).toHaveBeenCalledWith('dsh web: http://127.0.0.1:4567')
- 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()
- const failed = new Context()
- failed.provide('httpServer', fakeHttpServer().server)
- provideLoader(failed, async () => { throw new Error('boot failed') })
- apply(failed, new Config({ printUrl: true, surfaceContext: true, trustedHosts: [] }))
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(log).not.toHaveBeenCalled()
- await failed.fiber.dispose()
- // Torn-down path: settlement resolves after the webserver is gone — no
- // line, no crash.
- log.mockClear()
- const torn = new Context()
- const child = torn.plugin((childCtx: Context) => {
- childCtx.provide('httpServer', fakeHttpServer().server)
- })
- await child
- let releaseTorn: () => void
- const tornSettlement = new Promise<void>((resolve) => { releaseTorn = resolve })
- provideLoader(torn, () => tornSettlement)
- apply(torn, new Config({ printUrl: true, surfaceContext: true, trustedHosts: [] }))
- await child.dispose() // the httpServer service goes away
- releaseTorn!()
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(log).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('httpServer', server)
- apply(ctx, new Config({ printUrl: false, surfaceContext: true, trustedHosts: [] }))
- await ctx.plugin(SystemPrompt, { persona: '' })
- await new Promise(resolve => setTimeout(resolve, 0))
- await expect(ctx.systemPrompt.assemble()).rejects.toThrow('httpServer service missing')
- await ctx.fiber.dispose()
- })
- it('resolves the real built frontend dist through the package exports, failing loud unbuilt', () => {
- // The production resolver (not the test hook). A built checkout resolves
- // the frontend package's index.html; a dist-less one (the CI coverage
- // lane runs before any build) must fail with the build hint, never a
- // silent fallback.
- try {
- expect(originalResolve()).toMatch(/dist[/\\]index\.html$/)
- } catch (error) {
- expect((error as Error).message).toContain('frontend dist not built')
- }
- })
- })
|