refactor(cli): discover app startup rows from injection

This commit is contained in:
Turtle
2026-08-07 15:43:26 +08:00
parent 1f0a0440f3
commit b692f38506
28 changed files with 302 additions and 283 deletions

View File

@@ -1,5 +1,5 @@
/**
* The web app's entrypoint row over a REAL Loader tree: every flag lands in the
* The web app's startup row over a REAL Loader tree: every flag lands in the
* `webStartup` service the web rows read, the bind it reports comes from the
* flag or from what the composition falls back to, `--help` resolves nothing,
* and a rejected argument exits without resolving anything.
@@ -39,7 +39,7 @@ afterEach(async () => {
})
/**
* Mount the real entrypoint row over a stand-in for the `webserver` row whose
* Mount the real startup row over a stand-in for the `webserver` row whose
* composed bind it reads, the way a profile mounts phase one.
* @param args - the invocation's inner arguments.
* @param webserverConfig - the composed `webserver` row config, or `null` to omit the row.
@@ -55,7 +55,7 @@ async function bootStartup(
// The Loader imports a row through Node's own resolver, which cannot resolve
// this workspace's sources; the row delegates to the real plugin the test
// imported through the source-plane path mapping.
writeFileSync(join(dir, 'entrypoint.mjs'), `
writeFileSync(join(dir, 'startup.mjs'), `
export const name = 'web-startup'
export const inject = ['cmdlineArgs']
export const apply = ctx => globalThis.__webStartupApply(ctx)
@@ -82,7 +82,8 @@ export const apply = ctx => globalThis.__webStartupApply(ctx)
` inject: [${WEB_STARTUP_SERVICE}]`,
' disabled: true',
'- id: web-startup',
` name: ${pathToFileURL(join(dir, 'entrypoint.mjs')).href}`,
` name: ${pathToFileURL(join(dir, 'startup.mjs')).href}`,
' inject: [cmdlineArgs]',
'',
].join('\n'))
const observing = { write: (chunk: string) => { observed.out += chunk; return true } }
@@ -155,7 +156,7 @@ describe('web startup', () => {
})
it('fails the boot when the composition lost the row whose bind it reads', async () => {
// The bundle patch and this entrypoint must agree on the row set; a
// The bundle patch and this startup row must agree on the row set; a
// missing row would otherwise silently drop the flag that targets it.
await expect(bootStartup([], null))
.rejects.toThrow('the web composition has no waiting "webserver" row to configure')

View File

@@ -2,7 +2,7 @@
* 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
* launcher's LAN snapshot.
* app startup row's LAN snapshot.
*/
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
@@ -68,15 +68,25 @@ describe('web-app runtime glue', () => {
return () => {}
},
} as never)
const hmrUpdates: unknown[] = []
ctx.provide('loader', {
entries: () => [{
options: { id: 'client-hmr' },
update: async (options: unknown) => { hmrUpdates.push(options) },
}],
await: async () => {},
} as never)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
apply(ctx, new Config({ mode: 'development', printUrl: true, surfaceContext: true, lanAddresses: ['192.168.1.5'] }))
await apply(ctx, new Config({ mode: 'development', printUrl: true, surfaceContext: true, lanAddresses: ['192.168.1.5'] }))
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(hmrUpdates).toEqual([{ disabled: false }])
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')
@@ -90,7 +100,7 @@ describe('web-app runtime glue', () => {
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, lanAddresses: [] }))
await apply(ctx, new Config({ mode: 'production', printUrl: false, surfaceContext: true, lanAddresses: [] }))
await ctx.plugin(SystemPrompt, { persona: '' })
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).not.toHaveBeenCalled()
@@ -111,11 +121,12 @@ describe('web-app runtime glue', () => {
return () => {}
},
} as never)
apply(ctx, new Config({ mode: 'production', printUrl: false, surfaceContext: false, lanAddresses: [] }))
await apply(ctx, new Config({ mode: 'production', printUrl: false, surfaceContext: false, lanAddresses: [] }))
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()
})
@@ -125,23 +136,23 @@ describe('web-app runtime glue', () => {
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, lanAddresses: [] }))
await apply(ctx, new Config({ mode: 'production', printUrl: true, surfaceContext: true, lanAddresses: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).toHaveBeenCalledWith('dsh web: http://127.0.0.1:4567')
await ctx.fiber.dispose()
})
it('waits for the launcher readiness the phased boot provides, and stays quiet when that boot failed', async () => {
it('waits for launcher readiness and stays quiet when the whole boot failed', async () => {
stageDist()
// The launcher-provided readiness wins over Loader settlement: a phased
// boot settles the Loader between phases, long before the app is up.
// Launcher readiness covers siblings that may still be mounting after
// this row itself has activated.
const ready = new Context()
ready.provide('httpServer', fakeHttpServer().server)
ready.provide('loader', { await: () => Promise.resolve() } as never)
let announce: () => void
ready.provide('appReady', new Promise<void>((resolve) => { announce = resolve }))
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
apply(ready, new Config({ mode: 'production', printUrl: true, surfaceContext: true, lanAddresses: [] }))
await apply(ready, new Config({ mode: 'production', printUrl: true, surfaceContext: true, lanAddresses: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).not.toHaveBeenCalled()
announce!()
@@ -157,7 +168,7 @@ describe('web-app runtime glue', () => {
const rejection = Promise.reject(new Error('boot failed'))
rejection.catch(() => {})
failed.provide('appReady', rejection)
apply(failed, new Config({ mode: 'production', printUrl: true, surfaceContext: true, lanAddresses: [] }))
await apply(failed, new Config({ mode: 'production', printUrl: true, surfaceContext: true, lanAddresses: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).not.toHaveBeenCalled()
await failed.fiber.dispose()
@@ -173,7 +184,7 @@ describe('web-app runtime glue', () => {
const settlement = new Promise<void>((resolve) => { release = resolve })
settled.provide('loader', { await: () => settlement } as never)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
apply(settled, new Config({ mode: 'production', printUrl: true, surfaceContext: true, lanAddresses: [] }))
await apply(settled, new Config({ mode: 'production', printUrl: true, surfaceContext: true, lanAddresses: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).not.toHaveBeenCalled()
release!()
@@ -192,7 +203,7 @@ describe('web-app runtime glue', () => {
let releaseTorn: () => void
const tornSettlement = new Promise<void>((resolve) => { releaseTorn = resolve })
torn.provide('loader', { await: () => tornSettlement } as never)
apply(torn, new Config({ mode: 'production', printUrl: true, surfaceContext: true, lanAddresses: [] }))
await apply(torn, new Config({ mode: 'production', printUrl: true, surfaceContext: true, lanAddresses: [] }))
await child.dispose() // the httpServer service goes away
releaseTorn!()
await new Promise(resolve => setTimeout(resolve, 0))
@@ -208,7 +219,7 @@ describe('web-app runtime glue', () => {
const { server } = fakeHttpServer()
Object.defineProperty(server, 'port', { get: () => undefined })
ctx.provide('httpServer', server)
apply(ctx, new Config({ mode: 'production', printUrl: false, surfaceContext: true, lanAddresses: [] }))
await apply(ctx, new Config({ mode: 'production', printUrl: false, surfaceContext: true, lanAddresses: [] }))
await ctx.plugin(SystemPrompt, { persona: '' })
await new Promise(resolve => setTimeout(resolve, 0))
await expect(ctx.systemPrompt.assemble()).rejects.toThrow('httpServer service missing')