Files
deepseek-harness/packages/host/runtime/tests/web-plugins.spec.ts
T
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

112 lines
4.6 KiB
TypeScript

/**
* mountWebPlugins unit coverage (keyless). The Loader-facing behavior —
* baseUrl anchoring, entry creation with idempotent reuse, the fiber-less
* fail-loud sweep, and the resolver seam — is exercised against a stubbed
* loader service so it runs without built lib/ artifacts. The roster is
* caller-supplied now (composition moved to apps/cli), so these tests pass
* their own lists.
*/
import { Context } from 'cordis'
import { afterEach, describe, expect, it } from 'vitest'
import { mountWebPlugins } from '../src/web-plugins.ts'
const ROSTER = [
'@deepseek-ai/dsh-plugin-a',
'@deepseek-ai/dsh-plugin-b',
'@deepseek-ai/dsh-plugin-c',
] as const
interface FakeEntry {
options: { name: string }
fiber?: unknown
disabled: boolean
}
/** Loader stub provided under the real service name (mountWebPlugins skips ctx.plugin(Loader) when present). */
class FakeLoader {
readonly created: string[] = []
awaited = 0
constructor(private readonly entriesList: FakeEntry[], private readonly onCreate?: (name: string) => void) {}
entries(): Iterable<FakeEntry> {
return this.entriesList
}
async create(options: { name: string }): Promise<void> {
this.created.push(options.name)
this.onCreate?.(options.name)
}
async await(): Promise<void> {
this.awaited += 1
}
}
let root: Context | undefined
afterEach(async () => {
await root?.fiber.dispose()
root = undefined
})
function withLoader(entriesList: FakeEntry[], onCreate?: (name: string) => void): { ctx: Context; loader: FakeLoader } {
root = new Context()
const loader = new FakeLoader(entriesList, onCreate)
root.reflect.provide('loader', loader)
return { ctx: root, loader }
}
describe('mountWebPlugins (stubbed loader)', () => {
it('creates one entry per roster package, awaits the tree, and returns the loader view + resolver', async () => {
const entriesList: FakeEntry[] = []
const { ctx, loader } = withLoader(entriesList, (name) => {
entriesList.push({ options: { name }, fiber: {}, disabled: false })
})
const mounted = await mountWebPlugins(ctx, ROSTER, import.meta.url)
expect(loader.created).toEqual([...ROSTER])
expect(loader.awaited).toBe(1)
expect([...mounted.loader.entries()].map(e => e.options.name)).toEqual([...ROSTER])
// The resolver resolves a real package manifest through real module resolution, anchored at this test file.
expect(mounted.resolvePkgJson('@deepseek-ai/dsh-host-runtime')).toMatch(/package\.json$/)
expect(ctx.baseUrl).toBeDefined()
})
it('reuses existing entries (idempotent mount creates no duplicates)', async () => {
const preexisting: FakeEntry[] = ROSTER.map(name => ({ options: { name }, fiber: {}, disabled: false }))
const { ctx, loader } = withLoader(preexisting)
await mountWebPlugins(ctx, ROSTER, import.meta.url)
expect(loader.created).toEqual([])
})
it('throws listing every fiber-less entry (silent import failure must not drop a client plugin)', async () => {
const entriesList: FakeEntry[] = []
const { ctx } = withLoader(entriesList, (name) => {
// First one loads; the rest stay fiber-less (import failed silently).
entriesList.push({ options: { name }, fiber: entriesList.length < 1 ? {} : undefined, disabled: false })
})
await expect(mountWebPlugins(ctx, ROSTER, import.meta.url))
.rejects.toThrow(/client plugin\(s\) failed to load: .*dsh-plugin-c/)
})
it('skips disabled entries in the fail-loud sweep (disabled is the one valid fiber-less state)', async () => {
const entriesList: FakeEntry[] = ROSTER.map(name => ({ options: { name }, fiber: undefined, disabled: true }))
const { ctx } = withLoader(entriesList)
await expect(mountWebPlugins(ctx, ROSTER, import.meta.url)).resolves.toBeDefined()
})
it('mounts the real Loader when none is present (the ctx.plugin(Loader) branch)', async () => {
root = new Context()
// An empty roster keeps this keyless and artifact-free: the branch under
// test is only the Loader auto-mount.
await mountWebPlugins(root, [], import.meta.url)
expect(root.get('loader') !== undefined).toBe(true)
}, 30_000) // cold-cache import of the real vendored Loader crosses the network-disk 5s default
it('keeps a caller-set baseUrl (anchors only when absent)', async () => {
const entriesList: FakeEntry[] = []
const { ctx } = withLoader(entriesList, (name) => {
entriesList.push({ options: { name }, fiber: {}, disabled: false })
})
ctx.baseUrl = 'file:///caller/anchor/'
await mountWebPlugins(ctx, ROSTER, import.meta.url)
expect(ctx.baseUrl).toBe('file:///caller/anchor/')
})
})