From e265c6038f5db7e2fff0c5c61ca7350047ff6b26 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Fri, 7 Aug 2026 16:39:44 +0800 Subject: [PATCH] test: cover the agent-preset wire routes and both restore edges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../tests/settings-store.spec.ts | 4 ++ .../apiproxy/tests/client-handler.spec.ts | 12 +++++ .../preset/agent-presets/tests/mount.spec.ts | 49 ++++++++++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/packages/client/ui-agent-preset/tests/settings-store.spec.ts b/packages/client/ui-agent-preset/tests/settings-store.spec.ts index 92f198db6b..4ea9a0b198 100644 --- a/packages/client/ui-agent-preset/tests/settings-store.spec.ts +++ b/packages/client/ui-agent-preset/tests/settings-store.spec.ts @@ -169,6 +169,7 @@ describe('the agent-preset settings controller', () => { describe: () => Promise.resolve({ rpcId: 'r', result: { ok: true as const, value: { writable: true, hasDocument: true, namespaces: [] } }, }), + // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- the non-Error branch is what this covers update: () => Promise.reject(value), }, } as unknown as IApiClient) @@ -181,6 +182,7 @@ describe('the agent-preset settings controller', () => { expect(controller.store.getSnapshot().error).toBe('socket closed') const failing = new AgentPresetSettingsController({ + // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- the non-Error branch is what this covers agentPresets: { list: () => Promise.reject('offline') }, settings: { describe: () => Promise.resolve({ @@ -368,7 +370,9 @@ describe('the composer seat controller', () => { it('reads a rejection that is not an Error', async () => { const api = { agentPresets: { + // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- the non-Error branch is what this covers list: () => Promise.reject('offline'), + // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- the non-Error branch is what this covers select: () => Promise.reject('socket closed'), }, } as unknown as IApiClient diff --git a/packages/host/apiproxy/tests/client-handler.spec.ts b/packages/host/apiproxy/tests/client-handler.spec.ts index bc3f6aa840..64f931224e 100644 --- a/packages/host/apiproxy/tests/client-handler.spec.ts +++ b/packages/host/apiproxy/tests/client-handler.spec.ts @@ -226,6 +226,18 @@ describe('unary round trip', () => { expect(appended.result.ok).toBe(true) }) + it('routes the agent-preset roster and switch through the wire', async () => { + const c = client(scriptedApi()) + + const listed = await c.agentPresets.list({}) + expect(listed.result).toEqual({ ok: true, value: { presets: [] } }) + + // The switch carries the session it is about: the host refuses one whose + // conversation has started, and it can only know which by id. + const selected = await c.agentPresets.select({ sessionId: sid('s1'), agentPreset: 'standard' }) + expect(selected.result).toEqual({ ok: true, value: { agentPreset: 'standard' } }) + }) + it('passes business errors through as 200 + err result, not a throw', async () => { const api = scriptedApi({ sessions: { diff --git a/packages/preset/agent-presets/tests/mount.spec.ts b/packages/preset/agent-presets/tests/mount.spec.ts index a24248a3ec..397f4d594d 100644 --- a/packages/preset/agent-presets/tests/mount.spec.ts +++ b/packages/preset/agent-presets/tests/mount.spec.ts @@ -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/)