test: cover the agent-preset wire routes and both restore edges

master moved the RPC surface onto the fetch transport, so the two agentPreset
methods arrived there with nothing driving them. Adds the round trip, plus the
two `recompose` edges that had none: an agent that never composed a preset, so
there is nothing to restore, and a restore that fails because the composition
it reaches for is gone — the roster is a live directory.

Marks the deliberate non-Error rejections in the store specs, which is the
branch they exist to cover.
This commit is contained in:
Yichen Jiang
2026-08-07 16:39:44 +08:00
parent d13cb724ee
commit e265c6038f
3 changed files with 64 additions and 1 deletions

View File

@@ -1,4 +1,4 @@
import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
@@ -331,6 +331,53 @@ describe('replacing a composition', () => {
expect(toolNames(ctx, handle.agent)).toEqual(['alpha'])
})
it('leaves an agent that never composed one with nothing to restore', async () => {
const handle = await ctx.agents.create({ sessionId: SessionId('sess-bare') })
await expect(ctx.agentPresets.recompose(handle.agent.ctx, 'broken'))
.rejects.toThrow(/failed to mount/)
// There was no previous composition to put back, so the failure is the
// whole outcome — not a restore that silently invents one.
expect(toolNames(ctx, handle.agent)).toEqual([])
})
it('reports the switch failure when the restore fails too', async () => {
// A preset root this test owns, so removing the composition mid-flight
// cannot disturb the shipped fixtures.
const root = await mkdtemp(join(tmpdir(), 'dsh-preset-restore-'))
const seeded: [string, string][] = [['first', `- id: only\n name: ${join(FIXTURES, 'plugins', 'contribute.js')}\n config:\n tool: only\n`], ['broken', '- id: nope\n name: ./does-not-exist.js\n']]
for (const [id, body] of seeded) {
await mkdir(join(root, id))
await writeFile(join(root, id, COMPOSITION_FILE), body)
}
const scoped = new Context()
scoped.baseUrl = pathToFileURL(FIXTURES).href + '/'
await scoped.plugin(Loader)
scoped.loader.builtins.include = Include
await scoped.plugin(LlmService)
await scoped.plugin(SessionStore)
await scoped.plugin(SystemPrompt, { persona: '' })
await scoped.plugin(ToolRegistry)
await scoped.plugin(AgentRegistry)
await scoped.plugin(AgentLoop, { agents: [] })
await scoped.plugin(AgentPresets, { default: 'first', roots: [{ path: root, trust: 'user' as const }] })
const handle = await scoped.agents.create({
sessionId: SessionId('sess-restore-gone'),
setup: async (agentCtx: Context) => void await scoped.agentPresets.mount(agentCtx, 'first'),
})
// The roster is a live directory: the composition the agent came from can
// be gone by the time the restore reaches for it.
await rm(join(root, 'first'), { recursive: true })
await expect(scoped.agentPresets.recompose(handle.agent.ctx, 'broken'))
.rejects.toThrow(/failed to mount/)
// The switch failure is the actionable one; the restore's is swallowed.
expect(toolNames(scoped, handle.agent)).toEqual([])
})
it('refuses an unscoped context', async () => {
await expect(ctx.agentPresets.recompose(ctx, 'minimal'))
.rejects.toThrow(/unscoped context/)