Files
deepseek-harness/packages/preset/agent-preset-registry/tests/registry.spec.ts
T

371 lines
19 KiB
TypeScript

import { afterEach, describe, expect, it } from 'vitest'
import { createScope } from '@deepseek-ai/dsh-scope'
import { assembleContextFor } from '@deepseek-ai/dsh-agent'
import { entryListProblem } from '../src/index.ts'
import { currentKey, harness, declare, contribution, agentOn, liveRegistries, plugin } from './harness.ts'
import { omitsGeneratedPage } from '../../../settings/settings/tests/live-config.ts'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import AgentPresets from '../src/index.ts'
import type { Context } from '@deepseek-ai/cordis'
const contexts: Context[] = []
afterEach(async () => { for (const ctx of contexts.splice(0)) await ctx.fiber.dispose() })
async function setup() { const ctx = await harness(); contexts.push(ctx); return ctx }
describe('declarative preset revisions', () => {
it('eagerly scopes tool and prompt registrations and shares one revision across Agents', async () => {
const ctx = await setup()
await declare(ctx, contribution('standard'))
await declare(ctx, contribution('minimal'))
expect(ctx.agentPresets.inspectCompositions()).toHaveLength(2)
const first = await agentOn(ctx, 'first')
const second = await agentOn(ctx, 'second', 'minimal')
const third = await agentOn(ctx, 'third')
expect(ctx.agentPresets.inspectCompositions(ctx)).toEqual([])
expect(ctx.agentPresets.inspectCompositions(first.ctx).map(row => row.id)).toEqual(['standard'])
expect(ctx.agentPresets.inspectCompositions(second.ctx).map(row => row.id)).toEqual(['minimal'])
expect(ctx.tools.schemas(first).map(row => row.name)).toEqual(['standard'])
expect(ctx.tools.schemas(second).map(row => row.name)).toEqual(['minimal'])
expect(ctx.tools.schemas(third).map(row => row.name)).toEqual(['standard'])
expect(ctx.tools.schemas()).toEqual([])
const prompt = await ctx.systemPrompt.assemble(assembleContextFor(first))
expect(prompt.sections.map(row => row.name)).toContain('preset:standard')
expect(prompt.sections.map(row => row.name)).not.toContain('preset:minimal')
expect(ctx.agentPresets.inspectCompositions()).toHaveLength(2)
})
it('retains a replaced revision for existing scopes and their children, then releases it', async () => {
const ctx = await setup()
const old = await declare(ctx, contribution('standard'))
const agent = createScope(ctx, {})
await ctx.agentPresets.mount(agent.ctx)
const oldKey = await currentKey(ctx)
await old.dispose()
await declare(ctx, { ...contribution('replacement'), id: 'standard' })
expect(await currentKey(ctx)).not.toBe(oldKey)
expect(ctx.agentPresets.inspectCompositions()).toHaveLength(2)
const child = createScope(ctx, {})
expect(ctx.agentPresets.composeFrom(child.ctx, agent.ctx)).toBe('standard')
await agent.dispose()
expect(ctx.agentPresets.inspectCompositions()).toHaveLength(2)
await child.dispose()
expect(ctx.agentPresets.inspectCompositions()).toHaveLength(1)
const fresh = await agentOn(ctx, 'fresh')
expect(ctx.tools.schemas(fresh).map(row => row.name)).toEqual(['replacement'])
})
it('removes definitions without revoking live bindings', async () => {
const ctx = await setup()
const definition = await declare(ctx, contribution('standard'))
const agent = createScope(ctx, {})
await ctx.agentPresets.mount(agent.ctx)
await definition.dispose()
expect(await ctx.agentPresets.list()).toEqual([])
await expect(ctx.agentPresets.resolve()).rejects.toThrow('Unknown agent preset')
expect(ctx.agentPresets.composedPreset(agent.ctx)).toBe('standard')
await agent.dispose()
expect(ctx.agentPresets.inspectCompositions()).toEqual([])
})
it('reports a failed definition while allowing healthy definitions and the host to start', async () => {
const ctx = await setup()
await declare(ctx, { id: 'broken', plugins: [{ name: 'missing-preset-plugin-for-test' }] })
await declare(ctx, contribution('standard'))
expect(await ctx.agentPresets.resolve('broken')).toMatchObject({ id: 'broken', broken: expect.any(String) as string })
const scope = createScope(ctx, {})
await expect(ctx.agentPresets.mount(scope.ctx, 'broken')).rejects.toThrow()
expect(await ctx.agentPresets.mount(scope.ctx)).toEqual({ id: 'standard' })
const roster = await ctx.agentPresets.remoteExportList()
expect(roster.presets.find(row => row.id === 'standard')?.isDefault).toBe(true)
})
it('rejects duplicate IDs without disposing the first definition', async () => {
const ctx = await setup()
await declare(ctx, contribution('standard'))
await expect(declare(ctx, contribution('standard'))).rejects.toThrow('Duplicate agent preset')
expect(await ctx.agentPresets.resolve()).toEqual({ id: 'standard' })
})
it('rebinds a blank scope and preserves a child inheriting its former revision', async () => {
const ctx = await setup()
await declare(ctx, contribution('standard'))
await declare(ctx, contribution('minimal'))
const scope = createScope(ctx, {})
await ctx.agentPresets.mount(scope.ctx)
await ctx.agentPresets.mount(scope.ctx)
const child = createScope(ctx, {})
ctx.agentPresets.composeFrom(child.ctx, scope.ctx)
await ctx.agentPresets.recompose(scope.ctx, 'minimal')
expect(ctx.agentPresets.composedPreset(scope.ctx)).toBe('minimal')
expect(ctx.agentPresets.composedPreset(child.ctx)).toBe('standard')
expect(ctx.agentPresets.composeFrom(createScope(ctx, {}).ctx, ctx)).toBeUndefined()
})
it('logs selection and rejects preset changes after the first turn', async () => {
const ctx = await setup()
await declare(ctx, contribution('standard'))
await declare(ctx, contribution('minimal'))
const agent = await agentOn(ctx, 'selection')
expect(await ctx.agentPresets.select(agent, 'minimal')).toBe('minimal')
expect(ctx.sessionProjections.checkpoint(agent.session).agentPreset?.val).toBe('minimal')
agent.session.append('turn/start', { turn: 1 })
await expect(ctx.agentPresets.select(agent, 'standard')).rejects.toThrow('already started')
})
it('inventories active and disabled child entries and declared display metadata', async () => {
const ctx = await setup()
await declare(ctx, { ...contribution('standard'), name: 'Standard', description: 'General', order: 2 })
await declare(ctx, { id: 'empty', order: 1, plugins: [{ name: 'missing', disabled: true }] })
expect((await ctx.agentPresets.list()).map(row => row.id)).toEqual(['empty', 'standard'])
expect(await ctx.agentPresets.compositionInventory()).toEqual(expect.arrayContaining([
expect.objectContaining({ id: 'standard', name: 'Standard', isDefault: true, rows: expect.any(Array) as unknown[] }),
expect.objectContaining({ id: 'empty', rows: [expect.objectContaining({ moduleName: 'missing', enabled: false })] }),
]))
})
it('renders a declaration as entry-list YAML for reading, conditions included', async () => {
const ctx = await setup()
await declare(ctx, {
...contribution('standard'), name: 'Standard', description: 'General',
plugins: [
{ id: 'contribute', name: plugin('contribute'), config: { tool: 'standard' } },
{ id: 'windows-only', name: 'missing', disabled: { __jsExpr: "process.platform !== 'win32'" } },
],
})
const document = await ctx.agentPresets.readDocument('standard')
expect(document).toMatchObject({ agentPreset: 'standard', name: 'Standard', description: 'General' })
expect(document.content.startsWith('- id: contribute\n name: ')).toBe(true)
expect(document.content).toContain("\n config:\n tool: standard\n- id: windows-only\n name: missing\n disabled: !!js process.platform !== 'win32'\n")
expect(document.content).not.toContain('__jsExpr')
await declare(ctx, contribution('minimal'))
expect(await ctx.agentPresets.readDocument('minimal')).toEqual({
agentPreset: 'minimal', content: expect.any(String) as string,
})
await expect(ctx.agentPresets.readDocument('absent')).rejects.toThrow('Unknown agent preset: absent')
})
})
it('retains a retired revision for an in-flight cold read', async () => {
const ctx = await setup()
const definition = await declare(ctx, contribution('standard'))
const lease = await ctx.agentPresets.acquireScope()
await definition.dispose()
expect(ctx.agentPresets.inspectCompositions()).toHaveLength(1)
await lease[Symbol.asyncDispose]()
await lease[Symbol.asyncDispose]()
expect(ctx.agentPresets.inspectCompositions()).toEqual([])
})
it.each([['throws', 'refused'], ['global-service', 'require isolate realms']])('contains a %s plugin activation failure', async (name, reason) => {
const ctx = await setup()
const { plugin } = await import('./harness.ts')
await declare(ctx, { id: 'invalid', plugins: [{ name: plugin(name), config: { message: 'refused', service: 'fixtureService' } }] })
expect((await ctx.agentPresets.resolve('invalid')).broken).toContain(reason)
expect(ctx.get('fixtureService')).toBeUndefined()
expect(ctx.agentPresets.inspectCompositions()).toEqual([])
})
it('keeps a row waiting for an absent service mounted, reports it, and refuses bindings', async () => {
const ctx = await setup()
const { plugin } = await import('./harness.ts')
await declare(ctx, { id: 'invalid', plugins: [{ name: plugin('needs-missing') }] })
expect((await ctx.agentPresets.resolve('invalid')).broken).toContain('waiting for serviceThatDoesNotExist')
expect(ctx.agentPresets.inspectCompositions()).toHaveLength(1)
await expect(ctx.agentPresets.mount(createScope(ctx, {}).ctx, 'invalid')).rejects.toThrow('waiting for serviceThatDoesNotExist')
expect((await ctx.agentPresets.list())[0]!.broken).toContain('waiting for serviceThatDoesNotExist')
})
it('waits for a Host provider still activating instead of recording the preset as broken', async () => {
const ctx = await setup()
const { plugin } = await import('./harness.ts')
let release!: () => void
const paused = new Promise<void>((resolve) => { release = resolve })
let entered!: () => void
const started = new Promise<void>((resolve) => { entered = resolve })
ctx.loader.builtins.slowProvider = { async apply(host: Context) {
entered()
await paused
host.provide('serviceThatDoesNotExist', { label: 'late host' })
} }
const hostUpdate = ctx.loader.root.update([{ id: 'slow', name: 'cordis:slowProvider' }])
await started
await declare(ctx, { id: 'late', plugins: [{ name: plugin('needs-missing') }] })
expect(ctx.agentPresets.inspectCompositions()).toHaveLength(1)
const acquiring = ctx.agentPresets.acquireScope('late')
const outcome = await Promise.race([
acquiring.then(() => 'settled', () => 'settled'),
new Promise<string>((resolve) => { setImmediate(() => { resolve('waiting') }) }),
])
expect(outcome).toBe('waiting')
release()
await hostUpdate
const lease = await acquiring
expect(await ctx.agentPresets.resolve('late')).toEqual({ id: 'late' })
expect((await ctx.agentPresets.compositionInventory())[0]).toMatchObject({ id: 'late',
rows: [expect.objectContaining({ moduleName: plugin('needs-missing'), enabled: true })] })
await lease[Symbol.asyncDispose]()
})
it('allows an isolated service and resolves it through the Agent composition', async () => {
const ctx = await setup()
const { plugin } = await import('./harness.ts')
await declare(ctx, { id: 'standard', plugins: [{ name: 'cordis:group', group: true,
isolate: { fixtureService: true }, config: [{ name: plugin('global-service'), config: { service: 'fixtureService', label: 'scoped' } }] }] })
const scope = createScope(ctx, {})
await ctx.agentPresets.mount(scope.ctx)
expect(ctx.agentPresets.serviceFor({ ctx: scope.ctx }, 'fixtureService' as string & keyof Context)).toEqual({ label: 'scoped' })
expect(ctx.agentPresets.serviceFor({ ctx }, 'fixtureService' as string & keyof Context)).toBeUndefined()
expect(ctx.agentPresets.serviceFor({ ctx: scope.ctx }, 'loader')).toBeUndefined()
})
it('resolves the saved default over the deployment default, and drops a removed override', async () => {
const ctx = await harness({ live: true })
contexts.push(ctx)
const live = liveRegistries.get(ctx)!
await live.update({ selectedDefault: 'minimal' })
expect(ctx.agentPresets.defaultId).toBe('minimal')
await live.replace({ default: 'standard' })
expect(ctx.agentPresets.defaultId).toBe('standard')
})
it('ignores a retired modeSelectionEnabled field in the user patch', async () => {
const ctx = await harness({ live: true })
contexts.push(ctx)
const live = liveRegistries.get(ctx)!
await declare(ctx, contribution('minimal'))
// A patch written before Developer tools became the only gate still carries
// this key; the Loader's declared fields simply do not include it.
await live.update({ selectedDefault: 'minimal', modeSelectionEnabled: false })
expect(ctx.agentPresets.defaultId).toBe('minimal')
expect((await ctx.agentPresets.remoteExportList()).presets.find(row => row.id === 'minimal')?.isDefault).toBe(true)
// Neither read nor rewritten: the raw entry keeps the key it was loaded with.
expect(live.entry.options.config).toMatchObject({ selectedDefault: 'minimal', modeSelectionEnabled: false })
})
it('keeps its own instance off the generated Settings pages', () => omitsGeneratedPage(async (ctx) => {
await ctx.plugin(Loader)
await ctx.plugin(SessionProjectionRegistry)
return ctx.plugin(AgentPresets, { default: 'standard' })
}))
it('refuses an unscoped binding and a second child join without leaking references', async () => {
const ctx = await setup()
const definition = await declare(ctx, contribution('standard'))
await expect(ctx.agentPresets.mount(ctx)).rejects.toThrow('scoped context')
await expect(ctx.agentPresets.register({ id: ' ', plugins: [] })).rejects.toThrow('empty')
const parent = createScope(ctx, {})
await ctx.agentPresets.mount(parent.ctx)
expect(() => ctx.agentPresets.composeFrom(ctx, parent.ctx)).toThrow('requires a scope')
const child = createScope(ctx, {})
ctx.agentPresets.composeFrom(child.ctx, parent.ctx)
expect(() => ctx.agentPresets.composeFrom(child.ctx, parent.ctx)).toThrow('already joined')
await definition.dispose()
await child.dispose()
await parent.dispose()
expect(ctx.agentPresets.inspectCompositions()).toHaveLength(0)
await expect(ctx.agentPresets.acquireScope()).rejects.toThrow('Unknown')
})
it('serializes competing blank-session selections and recovers after a failed selection', async () => {
const ctx = await setup()
await declare(ctx, contribution('standard'))
await declare(ctx, contribution('minimal'))
const agent = await agentOn(ctx, 'competing')
const results = await Promise.allSettled([
ctx.agentPresets.select(agent, 'absent'), ctx.agentPresets.select(agent, 'minimal'), ctx.agentPresets.select(agent, 'standard'),
])
expect(results.map(row => row.status)).toEqual(['rejected', 'fulfilled', 'fulfilled'])
expect(ctx.sessionProjections.checkpoint(agent.session).agentPreset?.val).toBe('standard')
})
it('retains a newly acquired generation when a previous activation is replaced', async () => {
const ctx = await setup()
let release!: () => void
const paused = new Promise<void>((resolve) => { release = resolve })
let entered!: () => void
const started = new Promise<void>((resolve) => { entered = resolve })
ctx.loader.builtins.delayed = { async apply() { entered(); await paused } }
const pending = ctx.agentPresets.register({ id: 'standard', plugins: [{ name: 'cordis:delayed' }] })
await started
const leasePromise = ctx.agentPresets.acquireScope()
release()
const dispose = await pending
const lease = await leasePromise
await dispose()
expect(ctx.agentPresets.inspectCompositions()).toHaveLength(1)
await lease[Symbol.asyncDispose]()
expect(ctx.agentPresets.inspectCompositions()).toHaveLength(0)
})
it('does not lose the durable selection when a tools observer rejects notification', async () => {
const ctx = await setup()
await declare(ctx, contribution('standard'))
await declare(ctx, contribution('minimal'))
const agent = await agentOn(ctx, 'observer')
ctx.on('tools/change', () => { throw new Error('observer refused') })
expect(await ctx.agentPresets.select(agent, 'minimal')).toBe('minimal')
expect(ctx.sessionProjections.checkpoint(agent.session).agentPreset?.val).toBe('minimal')
})
it('inventories failed conditional entries without executing their expressions again', async () => {
const ctx = await setup()
await declare(ctx, { id: 'broken', name: 'Broken', plugins: [
{ name: 'missing', disabled: { __jsExpr: 'false' } },
] })
const inventory = await ctx.agentPresets.compositionInventory()
expect(inventory[0]).toMatchObject({ id: 'broken', broken: expect.any(String) as string,
rows: [{ moduleName: 'missing', enabled: 'conditional', condition: 'false' }] })
})
it('does not bind a definition removed while acquisition yields, and disposal is idempotent', async () => {
const ctx = await setup()
const dispose = await ctx.agentPresets.register(contribution('standard'))
const acquiring = ctx.agentPresets.acquireScope()
const rejection = expect(acquiring).rejects.toThrow('Unknown agent preset')
await dispose()
await rejection
await declare(ctx, contribution('standard'))
await dispose()
expect(await ctx.agentPresets.resolve()).toEqual({ id: 'standard' })
})
it('refuses to inherit a revision owned by a different registry', async () => {
const ctx = await setup()
const other = await setup()
await declare(other, contribution('standard'))
const parent = createScope(other, {})
await other.agentPresets.mount(parent.ctx)
expect(() => ctx.agentPresets.composeFrom(createScope(ctx, {}).ctx, parent.ctx)).toThrow('revision is unavailable')
})
it('validates malformed child YAML at the declaring plugin and retains diagnostics', async () => {
const ctx = await setup()
const { default: Preset } = await import('@deepseek-ai/dsh-agent-preset')
ctx.loader.builtins.preset = Preset
await ctx.loader.root.update([{ id: 'invalid', name: 'cordis:preset', config: { id: 'invalid', plugins: [1] } }])
expect((await ctx.agentPresets.resolve('invalid')).broken).toContain('not a plugin row')
expect((await ctx.agentPresets.compositionInventory())[0]!.rows).toEqual([])
})
it.each([
['nope', 'the composition must be a top-level list of plugin rows'],
[[[]], 'row 1 is not a plugin row (expected a map with a "name")'],
[[{ name: '' }], 'row 1 names no plugin (a "name" string is required)'],
[[{ name: 'group', group: true, config: 'x' }], 'group row 1 must hold a list of plugin rows'],
[[{ name: 'group', group: true, config: [{}] }], 'row 1 row 1 names no plugin (a "name" string is required)'],
[[{ name: 'group', group: true, config: [{ name: 'child' }] }], undefined],
])('names the first invalid row of %j', (rows, problem) => {
expect(entryListProblem(rows)).toBe(problem)
})
it('reports every process-global service published by a preset', async () => {
const ctx = await setup()
ctx.loader.builtins.leaks = { apply(child: Context) {
child.provide('presetZulu', {})
child.provide('presetAlpha', {})
} }
await declare(ctx, { id: 'leaks', plugins: [{ name: 'cordis:leaks' }] })
expect((await ctx.agentPresets.resolve('leaks')).broken).toContain('presetAlpha, presetZulu')
})