Files
deepseek-harness/packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts
Yichen Jiang 5ed79887fb fix(web): correct the preset-layer contracts review found stale
None of these change behavior; each said something that was not true.

`SessionCwdConflict`'s doc block had been left stranded above the
`AgentPresetConflict` inserted under it, so one class carried a comment
about the other and the second carried none.

The roster comment named a `.system` directory that does not exist; the
shipped root is `config/agent-presets/`, and `system` is the trust its
entries carry.

The real-composition test attributed the disabled `api-gateway` row to
"side effects outside this process" alongside the port and the exporter.
It is disabled for a different reason — the api-proxy cannot mount in
this layer at all — and hiding that behind the same phrase would leave a
later layer unable to tell whether the line can come out.

One test claimed to refuse an adoption while asserting only that the
header records the preset; it now says what it checks.

`PERSONA_SECTION`/`PERSONA_ORDER` existed twice, once in the registry
that declares the slot and once restated in the row that replaces it —
a drift that would land a preset's persona beside the deployment's
instead of shadowing it. The registry exports them now.

The preset conflict message read "already runs agent preset undefined"
for a session that records none, which is the shape a deployment with no
roster produces; it names that case instead, with the regression that
reaches it through the gateway.

Finally, `PresetTree.write()` drops the `loader/config-update` the
inherited method emits — recorded on the override, since a future
edit-while-running flow needs its own persistence path.
2026-08-07 00:35:30 +08:00

238 lines
10 KiB
TypeScript

/**
* A session's agent preset is fixed at creation. The gateway records the
* resolved id on the header and refuses to adopt the identity under a different
* one, because the session's history was produced under that preset's tools:
* rebuilding it differently would replay tool calls the new agent cannot make.
*/
import { mkdtempSync, realpathSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Context } from 'cordis'
import AgentRegistry, { type AgentFactory } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import { RpcId, type RpcRequest } from '../src/api/rpc.ts'
import { UnknownPresetError } from '@deepseek-ai/dsh-agent-presets'
import { GoalId } from '@deepseek-ai/dsh-goal'
import { createApiProxy } from '../src/api-proxy.ts'
import { describe, expect, it } from 'vitest'
let nextRpc = 0
function request<P>(payload: P): RpcRequest<P> {
return { rpcId: RpcId(`preset-${String(nextRpc++)}`), payload }
}
/** Minimal live agent; the gateway only needs identity and its session. */
function stubAgent(session: Session): Agent {
return { id: session.id, session, status: 'idle' } as unknown as Agent
}
/**
* A roster whose `mount` is a no-op: this spec is about the gateway's identity
* rules, and the composition itself is covered by the real-composition test in
* `apps/cli`.
*/
function roster(ids: readonly string[]): unknown {
return {
defaultId: ids[0],
list: () => Promise.resolve(ids.map(id => ({ id, trust: 'system', path: `/presets/${id}.yml` }))),
resolve: (id?: string) => {
const wanted = id ?? ids[0] ?? ''
if (!ids.includes(wanted)) return Promise.reject(new UnknownPresetError(wanted, ids))
return Promise.resolve({ id: wanted, trust: 'system', path: `/presets/${wanted}.yml` })
},
mount: (_ctx: Context, id?: string) =>
Promise.resolve({ id: id ?? ids[0], trust: 'system', path: '/presets/x.yml' }),
// What a real mount leaves behind: a service instance only the agent that
// mounted it can be used to address. The doubles are per agent so a test
// can tell "this session's" from "some session's".
serviceFor: (agent: { id: unknown }, name: string) => {
const perAgent = services.get(String(agent.id))
return perAgent?.[name]
},
}
}
/** Per-agent service instances a mounted preset would own, keyed by session id. */
const services = new Map<string, Record<string, unknown>>()
async function harness(presets?: readonly string[]) {
const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-apiproxy-preset-')))
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
ctx.provide('sessionPersistence', { list: () => Promise.resolve([]) } as never)
if (presets !== undefined) ctx.provide('agentPresets', roster(presets) as never)
const factory: AgentFactory = {
async createAgent(_ownerCtx, options) {
const session = ctx.sessions.create(
options.sessionId,
options.meta === undefined ? {} : { meta: options.meta },
)
const agent = stubAgent(session)
// Setup runs before publication against a context that carries the
// agent, and the agent reaches back through `agent.ctx` — the pair the
// gateway's own `installTarget` relies on.
const agentCtx = ctx.extend({ agent })
;(agent as { ctx?: Context }).ctx = agentCtx
await options.setup?.(agentCtx)
const unregister = ctx.agents.register(agent)
return { agent, dispose: () => { unregister(); return Promise.resolve() } }
},
async resume() {
throw new Error('test harness has no persisted sessions')
},
}
ctx.agents.setFactory(factory)
const api = createApiProxy(ctx, { provider: 'test', model: 'test-model', cwd, workspaceRoot: cwd })
return { api, ctx, cwd }
}
describe('session.create with an agent preset', () => {
it('records the resolved preset on the session header', async () => {
const { api, ctx } = await harness(['standard', 'core-web'])
const created = await api.sessions.create(request({ sessionId: SessionId('s1'), agentPreset: 'core-web' }))
expect(created.result.ok).toBe(true)
expect(ctx.sessions.get(SessionId('s1'))?.header.agentPreset).toBe('core-web')
})
it('records the default when the caller names none', async () => {
const { api, ctx } = await harness(['standard', 'core-web'])
await api.sessions.create(request({ sessionId: SessionId('s2') }))
expect(ctx.sessions.get(SessionId('s2'))?.header.agentPreset).toBe('standard')
})
it('rejects an unknown preset and names the ones that exist', async () => {
const { api } = await harness(['standard'])
const response = await api.sessions.create(request({ sessionId: SessionId('s3'), agentPreset: 'nope' }))
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-not-found')
})
it('refuses to adopt a live session under a different preset', async () => {
const { api } = await harness(['standard', 'core-web'])
await api.sessions.create(request({ sessionId: SessionId('s4'), agentPreset: 'core-web' }))
const response = await api.sessions.create(request({ sessionId: SessionId('s4'), agentPreset: 'standard' }))
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-conflict')
expect(response.result.error.details).toEqual({
sessionId: 's4',
requestedPreset: 'standard',
existingPreset: 'core-web',
})
})
it('adopts a live session unchanged when the caller names no preset', async () => {
const { api } = await harness(['standard', 'core-web'])
await api.sessions.create(request({ sessionId: SessionId('s5'), agentPreset: 'core-web' }))
// Reconnecting and retrying a create must stay ordinary operations.
const response = await api.sessions.create(request({ sessionId: SessionId('s5') }))
expect(response.result.ok).toBe(true)
})
it('leaves the header preset-less when no roster is composed', async () => {
const { api, ctx } = await harness()
await api.sessions.create(request({ sessionId: SessionId('s6') }))
expect(ctx.sessions.get(SessionId('s6'))?.header.agentPreset).toBeUndefined()
})
it('says why a preset-less session cannot be adopted under one', async () => {
// Two callers reach this: a deployment that composes no roster, and a
// session created before one existed. Both record no preset, so naming
// any is a conflict rather than an adoption — the history was produced
// under a composition this roster cannot name. The message has to say
// that, because "already runs agent preset undefined" reads as a bug.
const { api } = await harness()
await api.sessions.create(request({ sessionId: SessionId('s7') }))
const response = await api.sessions.create(request({ sessionId: SessionId('s7'), agentPreset: 'standard' }))
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-conflict')
expect(response.result.error.message).toContain('records no agent preset')
expect(response.result.error.details).toEqual({
sessionId: 's7',
requestedPreset: 'standard',
existingPreset: undefined,
})
})
})
/**
* A capability a preset mounts is reachable from nowhere the host normally
* looks: an `isolate` realm is what makes it per session. The gateway serves
* requests that are ABOUT a session from OUTSIDE it, so it addresses the
* instance through the agent instead of reading a root-realm singleton.
*/
describe('a capability the session\'s preset mounts', () => {
it('serves the goal RPC from the session\'s own goal service', async () => {
const { api } = await harness(['standard'])
await api.sessions.create(request({ sessionId: SessionId('g1'), agentPreset: 'standard' }))
const ref = { id: GoalId('goal-1'), revision: 1 }
const paused: unknown[] = []
services.set('g1', {
goals: { pause: (agent: { id: unknown }, r: unknown) => { paused.push([String(agent.id), r]); return ref } },
})
const response = await api.goals.pause(request({ sessionId: SessionId('g1'), ref }))
expect(response.result).toMatchObject({ ok: true, value: { ref } })
// Reached the instance this session mounted, and was handed its own agent.
expect(paused).toEqual([['g1', ref]])
services.delete('g1')
})
it('serves the skill catalog from the session\'s own registry', async () => {
const { api } = await harness(['standard'])
await api.sessions.create(request({ sessionId: SessionId('k1'), agentPreset: 'standard' }))
services.set('k1', {
skills: {
list: () => Promise.resolve([{
name: 'preset-owned',
description: 'ships inside the preset directory',
invocation: { modelInvocable: true, userInvocable: true },
}]),
},
})
const response = await api.skills.list(request({ sessionId: SessionId('k1') }))
// A preset ships its own skill directory, so the catalog IS the
// session's; reading a host singleton would answer for the wrong one.
expect(response.result).toMatchObject({ ok: true, value: { skills: [{ name: 'preset-owned' }] } })
services.delete('k1')
})
it('says so when no composition mounts the capability at all', async () => {
const { api } = await harness(['standard'])
await api.sessions.create(request({ sessionId: SessionId('n1'), agentPreset: 'standard' }))
const response = await api.skills.list(request({ sessionId: SessionId('n1') }))
// Absent means absent — not "this session has none", which is what a
// root-realm read used to report for every presetd session.
expect(response.result.ok).toBe(false)
const failure = response.result as { ok: false; error: { message: string } }
expect(failure.error.message).toContain('neither this session')
})
})