Files
deepseek-harness/packages/llm/llm/tests/topology.spec.ts
Yichen Jiang 4989494e75 feat(llm): topology event and configurable-provider directory
ctx.llm gains 'llm/adapters-updated' — a payload-free registry notification
emitted at every topology commit point (adapter routes registering or
disposing, directory entries appearing or withdrawing) with contained
observers and INVARIANT rethrow — plus registerConfigurableProviders/
listConfigurableProviders, the directory of routes an adapter plugin can
activate through configuration. llm-deepseek declares deepseek-official
(whole llm-deepseek section as profile); llm-pi-ai declares the full
installed catalog under providers.<provider> even while dormant, so the
web settings surface can offer every provider before any route exists.
The invariant companion asserts the registry stays readable at each
notification.
2026-07-29 16:45:06 +08:00

146 lines
5.8 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import LlmService, { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, LlmConfigurableProvider, StreamChunk } from '@deepseek-ai/dsh-llm'
class NoopAdapter extends LlmAdapter {
async * stream(_options: GenerateOptions): AsyncIterable<StreamChunk> {
throw new Error('not exercised')
}
}
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(LlmService)
return ctx
}
function entry(overrides: Partial<LlmConfigurableProvider> = {}): LlmConfigurableProvider {
return {
provider: 'openai',
displayName: 'OpenAI',
settingsNs: 'llm-pi-ai',
settingsPath: ['providers', 'openai'],
...overrides,
}
}
describe('llm/adapters-updated', () => {
it('fires at both adapter registration commit points with the registry already readable', async () => {
const ctx = await setup()
const observed: string[][] = []
ctx.on('llm/adapters-updated', () => {
observed.push(ctx.llm.listProviders().map(provider => provider.id))
})
const dispose = ctx.llm.registerAdapter(['a', 'b'], new NoopAdapter())
expect(observed).toEqual([['a', 'b']])
dispose()
expect(observed).toEqual([['a', 'b'], []])
})
it('contains a throwing listener without vetoing registration or starving later listeners', async () => {
const ctx = await setup()
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined)
const later = vi.fn()
ctx.on('llm/adapters-updated', () => {
throw new Error('broken observer')
})
ctx.on('llm/adapters-updated', later)
ctx.llm.registerAdapter(['a'], new NoopAdapter())
expect(ctx.llm.listProviders().map(provider => provider.id)).toEqual(['a'])
expect(later).toHaveBeenCalledTimes(1)
expect(warn).toHaveBeenCalledWith('llm: an llm/adapters-updated listener failed')
})
it('rethrows the first INVARIANT-coded listener failure after notifying the rest', async () => {
const ctx = await setup()
const later = vi.fn()
ctx.on('llm/adapters-updated', () => {
throw Object.assign(new Error('registry incoherent'), { code: 'INVARIANT' })
})
ctx.on('llm/adapters-updated', later)
expect(() => ctx.llm.registerAdapter(['a'], new NoopAdapter())).toThrow('registry incoherent')
expect(later).toHaveBeenCalledTimes(1)
})
})
describe('configurable-provider directory', () => {
it('registers entries, lists detached copies in order, and fires the topology event', async () => {
const ctx = await setup()
const events = vi.fn()
ctx.on('llm/adapters-updated', events)
ctx.llm.registerConfigurableProviders([
entry({ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [] }),
entry(),
])
expect(events).toHaveBeenCalledTimes(1)
const listed = ctx.llm.listConfigurableProviders()
expect(listed).toEqual([
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [] },
{ provider: 'openai', displayName: 'OpenAI', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'] },
])
listed[0]!.displayName = 'mutated'
;(listed[1]!.settingsPath as string[]).push('mutated')
expect(ctx.llm.listConfigurableProviders()[0]!.displayName).toBe('DeepSeek')
expect(ctx.llm.listConfigurableProviders()[1]!.settingsPath).toEqual(['providers', 'openai'])
})
it('detaches stored entries from caller-owned objects', async () => {
const ctx = await setup()
const source = entry()
ctx.llm.registerConfigurableProviders([source])
source.displayName = 'mutated'
expect(ctx.llm.listConfigurableProviders()[0]!.displayName).toBe('OpenAI')
})
it('withdraws every entry when the registration disposes', async () => {
const ctx = await setup()
const dispose = ctx.llm.registerConfigurableProviders([entry()])
const events = vi.fn()
ctx.on('llm/adapters-updated', events)
dispose()
expect(ctx.llm.listConfigurableProviders()).toEqual([])
expect(events).toHaveBeenCalledTimes(1)
})
it('withdraws entries when the contributing fiber disposes', async () => {
const ctx = await setup()
const fiber = await ctx.plugin({
inject: ['llm'],
apply: (child: Context) => {
child.llm.registerConfigurableProviders([entry()])
},
})
expect(ctx.llm.listConfigurableProviders()).toHaveLength(1)
await fiber.dispose()
expect(ctx.llm.listConfigurableProviders()).toEqual([])
})
it('rejects an empty registration', async () => {
const ctx = await setup()
expect(() => ctx.llm.registerConfigurableProviders([])).toThrow(LlmError)
expect(() => ctx.llm.registerConfigurableProviders([])).toThrow(/at least one provider/)
})
it.each([
[entry({ provider: '' }), /non-empty provider/],
[entry({ displayName: '' }), /non-empty provider/],
[entry({ settingsNs: '' }), /non-empty provider/],
[entry({ settingsPath: ['providers', ''] }), /empty settingsPath segment/],
])('rejects invalid entries all-or-nothing', async (invalid, message) => {
const ctx = await setup()
expect(() => ctx.llm.registerConfigurableProviders([entry({ provider: 'valid-first' }), invalid])).toThrow(message)
expect(ctx.llm.listConfigurableProviders()).toEqual([])
})
it('rejects duplicates within one registration and across registrations', async () => {
const ctx = await setup()
expect(() => ctx.llm.registerConfigurableProviders([entry(), entry()])).toThrow(/already declared/)
ctx.llm.registerConfigurableProviders([entry()])
expect(() => ctx.llm.registerConfigurableProviders([entry({ displayName: 'Other' }), entry({ provider: 'unseen' })]))
.toThrow(/already declared/)
expect(ctx.llm.listConfigurableProviders()).toHaveLength(1)
})
})