Files
deepseek-harness/packages/host/webserver/tests/invariant.spec.ts
imccyu fb47f61a83 refactor(gui): host graph from dshClient discovery; webserver self-watches bundles for HMR
The registry scans mounted Loader entries' dshClient declarations and
composes __DSH_BOOT__ {rev, entries} — inject edges and the immediately
mark come from manifests, never hand-copied; malformed fields fail loud
at load. The composing app owns one flat roster plus the --dev switch
(hmr row and bundle watching are dev-graph decisions).

The rebuild signal is the webserver's own observation: in dev mode the
registry stat-polls each scanned bundle (fs.watchFile; polling because
network mounts deliver no inotify), re-hashes on change, and broadcasts
a rebuilt frame on the /plugins/events SSE channel only when the rev
actually changed. Watch membership follows the table across rescans;
dispose drops all watches; a torn read self-heals on the next tick.
The POST /plugins/rebuilt endpoint is gone — builders and the host
share zero protocol. dsh web --dev logs the watched bundle list and
each rebuilt id with its rev transition.
2026-07-24 02:09:42 +08:00

51 lines
2.0 KiB
TypeScript

/**
* Webserver invariant companion: the boot-graph consistency audit — every
* fetch-arrival graph row must resolve a clientPath, checked on fiber
* lifecycle events against the assembly-published 'webPlugins' context key.
*/
import { Context } from 'cordis'
import { describe, expect, it } from 'vitest'
import InvariantService from '@deepseek-ai/dsh-invariants'
import * as WebserverInvariant from '../src/invariant.ts'
interface RegistryStub {
graph(): { entries: { id: string; url: string }[] }
clientPath(id: string): string | undefined
}
async function setup(registry?: RegistryStub): Promise<Context> {
const ctx = new Context()
await ctx.plugin(InvariantService, { enabled: true })
await ctx.plugin(WebserverInvariant).await()
if (registry !== undefined) ctx.reflect.provide('webPlugins', registry)
return ctx
}
/** Fire the audit trigger directly (same technique as the scope invariant
* spec): a synchronous emit propagates the fail() throw to the caller. */
function trigger(ctx: Context): void {
;(ctx.emit as (event: string, ...args: unknown[]) => void)('internal/plugin', ctx.fiber)
}
describe('webserver manifest invariant', () => {
it('stays silent without a registry (carrier-only deployment) and with a consistent table', async () => {
const bare = await setup()
expect(() => { trigger(bare) }).not.toThrow() // no 'webPlugins' key published
const consistent = await setup({
graph: () => ({ entries: [{ id: 'p1', url: '/plugins/p1/client.js?rev=abc' }] }),
clientPath: id => id === 'p1' ? '/tmp/p1/lib/client.js' : undefined,
})
expect(() => { trigger(consistent) }).not.toThrow()
})
it('throws on a graph row whose bundle path no longer resolves', async () => {
const ctx = await setup({
graph: () => ({ entries: [{ id: 'ghost', url: '/plugins/ghost/client.js?rev=abc' }] }),
clientPath: () => undefined,
})
expect(() => { trigger(ctx) })
.toThrow(/graph row "ghost".*resolves no client bundle path/)
})
})