/** * 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(), 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, 'shell') 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 = async () => {}): void { ctx.provide('loader', { await: settle } as never) } interface BashContribution { name: string variables: Record resolve: () => Record } 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((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((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') } }) })