Files
deepseek-harness/packages/bundle/web-app/tests/web-app.spec.ts
T
Turtle c598989d08 refactor(cmdline): trim the command-line seams to existing interfaces
The web runtime creates its dev-mode client-hmr row in the root tree
after Loader settlement with plain loader.create, deleting the vendored
Entry.enableRuntime state machine and dsh-cmdline's enableRow export.
Include declares the existing EntryGroup.key tree-carrier marker instead
of the EntryConfigResolver protocol (its own path stays literal; nothing
used a dynamic path). The launcher recognizes no app row: SIGTERM exits
0 on every surface, every boot watches its user patch layers, and the
headless runner exits through ctx.appExit, deleting ctx.headlessIo. Also
restores the vendor README rescope entry to the position the
rescope-vendor exact-edit anchor requires, fixing the master hygiene
regression.
2026-08-11 18:00:15 +08:00

324 lines
14 KiB
TypeScript

/**
* 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 capturing the dev-mode row creation the runtime performs after settlement. */
function provideHmrRow(ctx: Context, settle: () => Promise<void> = async () => {}): string[] {
const created: string[] = []
const entries: { options: { name: string } }[] = []
ctx.provide('loader', {
entries: () => entries[Symbol.iterator](),
create: (options: { name: string }) => {
created.push(options.name)
entries.push({ options })
return Promise.resolve(options.name)
},
await: settle,
} as never)
return created
}
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)
const enabledRows = provideHmrRow(ctx)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
apply(ctx, new Config({ mode: 'development', 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(enabledRows).toEqual(['@deepseek-ai/dsh-client-hmr'])
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')
expect(section?.text).toContain('--dev')
const webRuntime = contributions.find(contribution => contribution.name === 'web-runtime')
expect(webRuntime?.resolve()).toEqual({ DSH_WEB_URL: 'http://127.0.0.1:4567', DSH_WEB_MODE: 'development' })
await ctx.fiber.dispose()
})
it('stays quiet in production mode with printUrl off and reports the production update contract', async () => {
stageDist()
const ctx = new Context()
ctx.provide('httpServer', fakeHttpServer().server)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
apply(ctx, new Config({ mode: 'production', 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('without `--dev`')
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({ mode: 'production', 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({ mode: 'production', 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('creates the client-hmr row exactly once across runtime reloads', async () => {
stageDist()
const ctx = new Context()
ctx.provide('httpServer', fakeHttpServer().server)
const created = provideHmrRow(ctx)
const mount = async (): Promise<() => Promise<void>> => {
const fiber = ctx.plugin((child: Context) => {
apply(child, new Config({ mode: 'development', printUrl: false, surfaceContext: false, trustedHosts: [] }))
})
await fiber
await new Promise(resolve => setTimeout(resolve, 0))
return () => fiber.dispose()
}
const disposeFirst = await mount()
expect(created).toEqual(['@deepseek-ai/dsh-client-hmr'])
await disposeFirst()
// A reload generation must not duplicate the row the previous one created.
const disposeSecond = await mount()
expect(created).toEqual(['@deepseek-ai/dsh-client-hmr'])
await disposeSecond()
await ctx.fiber.dispose()
})
it('skips the dev row when the tree is disposed during settlement and logs a creation failure', async () => {
stageDist()
const raced = new Context()
raced.provide('httpServer', fakeHttpServer().server)
let release!: () => void
const settlement = new Promise<void>((resolve) => { release = resolve })
const created: string[] = []
const disposeLoader = raced.provide('loader', {
entries: () => [][Symbol.iterator](),
create: (options: { name: string }) => {
created.push(options.name)
return Promise.resolve(options.name)
},
await: () => settlement,
} as never)
apply(raced, new Config({ mode: 'development', printUrl: false, surfaceContext: false, trustedHosts: [] }))
disposeLoader()
release()
await new Promise(resolve => setTimeout(resolve, 0))
expect(created).toEqual([])
await raced.fiber.dispose()
const failing = new Context()
failing.provide('httpServer', fakeHttpServer().server)
const failure = new Error('row creation failed')
failing.provide('loader', {
entries: () => [][Symbol.iterator](),
create: () => Promise.reject(failure),
await: () => Promise.resolve(),
} as never)
const errors: unknown[] = []
failing.logger.error = ((error: unknown) => { errors.push(error) }) as typeof failing.logger.error
apply(failing, new Config({ mode: 'development', printUrl: false, surfaceContext: false, trustedHosts: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(errors).toEqual([failure])
await failing.fiber.dispose()
})
it('mounts no dev row in production and only warns without a Loader in development', async () => {
stageDist()
const prod = new Context()
prod.provide('httpServer', fakeHttpServer().server)
const created = provideHmrRow(prod)
apply(prod, new Config({ mode: 'production', printUrl: false, surfaceContext: false, trustedHosts: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(created).toEqual([])
await prod.fiber.dispose()
const bare = new Context()
bare.provide('httpServer', fakeHttpServer().server)
const warnings: string[] = []
bare.logger.warn = ((message: unknown) => { warnings.push(String(message)) }) as typeof bare.logger.warn
apply(bare, new Config({ mode: 'development', printUrl: false, surfaceContext: false, trustedHosts: [] }))
expect(warnings).toEqual(['web-app: development mode without a Loader tree mounts no client-hmr row'])
// Let the vitest invariant host settle before tearing the root down.
await new Promise(resolve => setTimeout(resolve, 0))
await bare.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 })
provideHmrRow(settled, () => settlement)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
apply(settled, new Config({ mode: 'production', 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)
provideHmrRow(failed, async () => { throw new Error('boot failed') })
apply(failed, new Config({ mode: 'production', 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 })
provideHmrRow(torn, () => tornSettlement)
apply(torn, new Config({ mode: 'production', 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({ mode: 'production', 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')
}
})
})