Merge remote-tracking branch 'origin/master' into codex/pr-555-ci-fix

# Conflicts:
#	docs/config-catalog.i18n.yaml
#	docs/config-catalog.md
#	docs/config-catalog.zh.md
#	docs/module-graph.i18n.yaml
#	docs/module-graph.md
#	docs/module-graph.zh.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/client/connection/src/client/fixture.ts
#	packages/host/apiproxy/src/api-proxy.ts
This commit is contained in:
creatixchu
2026-08-10 12:33:36 +08:00
331 changed files with 16610 additions and 557 deletions

View File

@@ -0,0 +1,652 @@
/**
* 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 {
InvalidPresetIdError, PresetExistsError, resolveSessionPreset, 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`. Ids listed in `userIds` present as locally authored; the rest
* ship with the deployment.
*/
function roster(ids: readonly string[], userIds: readonly string[] = []): unknown {
const trustOf = (id: string): 'system' | 'user' => (userIds.includes(id) ? 'user' : 'system')
const presetOf = (id: string): object =>
({ id, trust: trustOf(id), path: `/presets/${id}/agent.cordis.yml` })
return {
defaultId: ids[0],
list: () => Promise.resolve(ids.map(presetOf)),
resolve: (id?: string) => {
const wanted = id ?? ids[0] ?? ''
if (!ids.includes(wanted)) return Promise.reject(new UnknownPresetError(wanted, ids))
return Promise.resolve(presetOf(wanted))
},
mount: (_ctx: Context, id?: string) => Promise.resolve(presetOf(id ?? ids[0] ?? '')),
// 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]
},
authorable: true,
read: (id: string) => Promise.resolve(`# ${id}\n- id: x\n name: y\n`),
copy: (from: string, id: string) => {
if (!ids.includes(from)) return Promise.reject(new UnknownPresetError(from, ids))
if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) return Promise.reject(new InvalidPresetIdError(id))
if (ids.includes(id)) return Promise.reject(new PresetExistsError(id))
return Promise.resolve()
},
remove: (id: string) => {
if (!ids.includes(id)) return Promise.reject(new UnknownPresetError(id, ids))
return Promise.resolve()
},
recompose: (_ctx: Context, id: string) => {
if (!ids.includes(id)) return Promise.reject(new UnknownPresetError(id, ids))
return Promise.resolve({ id, trust: 'system', path: `/presets/${id}.yml` })
},
// The standing scope key a cold transcript read resolves presenters in.
standingKeyFor: (id?: string) => {
const wanted = id ?? ids[0] ?? ''
standingKeyRequests.push(wanted)
if (!ids.includes(wanted) || failingStandingKeys.has(wanted)) {
return Promise.reject(new UnknownPresetError(wanted, ids))
}
let key = standingKeys.get(wanted)
if (key === undefined) {
key = { agentPreset: wanted }
standingKeys.set(wanted, key)
}
return Promise.resolve(key)
},
}
}
/** Standing keys the roster double minted, and the ids readers asked for. */
const standingKeys = new Map<string, object>()
const standingKeyRequests: string[] = []
/** Preset ids whose standing mount the double reports as unusable. */
const failingStandingKeys = new Set<string>()
/** 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[],
persistence?: unknown,
options: { userIds?: readonly string[]; defaults?: Record<string, unknown> } = {},
) {
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', (persistence ?? { list: () => Promise.resolve([]) }) as never)
if (presets !== undefined) ctx.provide('agentPresets', roster(presets, options.userIds) 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, {
defaultModelSelection: () => ({ provider: 'test', model: 'test-model' }),
cwd,
workspaceRoot: cwd,
...options.defaults,
})
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', 'minimal'])
const created = await api.sessions.create(request({ sessionId: SessionId('s1'), agentPreset: 'minimal' }))
expect(created.result.ok).toBe(true)
expect(ctx.sessions.get(SessionId('s1'))?.header.agentPreset).toBe('minimal')
})
it('records the default when the caller names none', async () => {
const { api, ctx } = await harness(['standard', 'minimal'])
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', 'minimal'])
await api.sessions.create(request({ sessionId: SessionId('s4'), agentPreset: 'minimal' }))
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: 'minimal',
})
})
it('adopts a live session unchanged when the caller names no preset', async () => {
const { api } = await harness(['standard', 'minimal'])
await api.sessions.create(request({ sessionId: SessionId('s5'), agentPreset: 'minimal' }))
// 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')
})
})
describe('agentPreset.list', () => {
it('marks the default and carries each preset\'s trust', async () => {
const { api } = await harness(['standard', 'minimal'])
const response = await api.agentPresets.list(request({}))
expect(response.result.ok).toBe(true)
if (!response.result.ok) throw new Error('unreachable')
expect(response.result.value.presets).toEqual([
{ id: 'standard', trust: 'system', isDefault: true },
{ id: 'minimal', trust: 'system', isDefault: false },
])
expect(response.result.value.authorable).toBe(true)
})
it('answers with an empty roster when the deployment composes no presets', async () => {
const { api } = await harness()
const response = await api.agentPresets.list(request({}))
// Composing no presets is a valid deployment, not an error: every session
// then shares the host composition and the browser offers no choice.
expect(response.result.ok).toBe(true)
if (!response.result.ok) throw new Error('unreachable')
expect(response.result.value.presets).toEqual([])
// Nothing to write to either, so a surface offering "new preset" knows to
// stay hidden rather than offering a button whose save always fails.
expect(response.result.value.authorable).toBe(false)
})
})
describe('agentPreset.select', () => {
it('recomposes a blank session', async () => {
const { api } = await harness(['standard', 'minimal'])
await api.sessions.create(request({ sessionId: SessionId('sel-1'), agentPreset: 'standard' }))
const response = await api.agentPresets.select(
request({ sessionId: SessionId('sel-1'), agentPreset: 'minimal' }))
expect(response.result.ok).toBe(true)
if (!response.result.ok) throw new Error('unreachable')
expect(response.result.value.agentPreset).toBe('minimal')
})
it('records the switch in the log, and the list reads it back', async () => {
const { api, ctx } = await harness(['standard', 'core-web'])
await api.sessions.create(request({ sessionId: SessionId('sel-log'), agentPreset: 'standard' }))
await api.agentPresets.select(
request({ sessionId: SessionId('sel-log'), agentPreset: 'core-web' }))
// The header is written once at creation, so the switch lives in the log —
// this is what a restart replays and what every projection resolves from.
// Asserting only the RPC's echo would miss a switch that never persisted.
const session = ctx.sessions.get(SessionId('sel-log'))
if (session === undefined) throw new Error('unreachable')
expect(session.header.agentPreset).toBe('standard')
expect(resolveSessionPreset(session)).toBe('core-web')
const listed = await api.sessions.list(request({}))
if (!listed.result.ok) throw new Error('unreachable')
expect(listed.result.value.items.find(item => item.sessionId === 'sel-log')?.agentPreset)
.toBe('core-web')
})
it('serializes two concurrent selects on one session', async () => {
const { api, ctx } = await harness(['standard', 'core-web'])
await api.sessions.create(request({ sessionId: SessionId('sel-race'), agentPreset: 'standard' }))
// Both pass the blank check; unserialized, the second unmount finds no
// record because the first already removed it, and two compositions end up
// in one agent layer. The client's busy flag is not enforcement.
const [first, second] = await Promise.all([
api.agentPresets.select(request({ sessionId: SessionId('sel-race'), agentPreset: 'core-web' })),
api.agentPresets.select(request({ sessionId: SessionId('sel-race'), agentPreset: 'standard' })),
])
expect(first.result.ok).toBe(true)
expect(second.result.ok).toBe(true)
const session = ctx.sessions.get(SessionId('sel-race'))
if (session === undefined) throw new Error('unreachable')
// One winner, and the log agrees with it: the last committed switch.
expect(resolveSessionPreset(session)).toBe('standard')
})
it('refuses once the conversation has started', async () => {
const { api, ctx } = await harness(['standard', 'minimal'])
await api.sessions.create(request({ sessionId: SessionId('sel-2'), agentPreset: 'standard' }))
// One turn is enough: the history from here on was produced under
// `standard`'s tools, and a swap would strand those tool calls.
ctx.sessions.get(SessionId('sel-2'))?.append('turn/start', { turn: 0 })
const response = await api.agentPresets.select(
request({ sessionId: SessionId('sel-2'), agentPreset: 'minimal' }))
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-locked')
})
it('reports an unknown preset without disturbing the session', async () => {
const { api } = await harness(['standard'])
await api.sessions.create(request({ sessionId: SessionId('sel-3') }))
const response = await api.agentPresets.select(
request({ sessionId: SessionId('sel-3'), 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('reports a deployment that composes no presets', async () => {
const { api } = await harness()
await api.sessions.create(request({ sessionId: SessionId('sel-4') }))
const response = await api.agentPresets.select(
request({ sessionId: SessionId('sel-4'), agentPreset: 'anything' }))
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-not-found')
})
})
describe('authoring over the wire', () => {
it('reads a composition with its trust', async () => {
const { api } = await harness(['standard'])
const response = await api.agentPresets.read(request({ agentPreset: 'standard' }))
expect(response.result.ok).toBe(true)
if (!response.result.ok) throw new Error('unreachable')
// The shipped set is readable: it is the known-good composition a copy
// starts from, and trust is what tells a surface to say so.
expect(response.result.value.trust).toBe('system')
expect(response.result.value.content).toContain('- id: x')
})
it('copies a preset under a new id', async () => {
const { api } = await harness(['standard'])
const response = await api.agentPresets.copy(
request({ from: 'standard', agentPreset: 'mine', name: '我的模式' }))
expect(response.result.ok).toBe(true)
if (!response.result.ok) throw new Error('unreachable')
expect(response.result.value.agentPreset).toBe('mine')
})
it('rejects a copy target that could escape the preset root', async () => {
const { api } = await harness(['standard'])
const response = await api.agentPresets.copy(request({ from: 'standard', agentPreset: '../escape' }))
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-invalid')
})
it('rejects a copy target the roster already supplies', async () => {
const { api } = await harness(['standard', 'minimal'])
const response = await api.agentPresets.copy(request({ from: 'standard', agentPreset: 'minimal' }))
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-invalid')
expect(response.result.error.message).toMatch(/already exists/)
})
it('rejects a copy whose source is unknown', async () => {
const { api } = await harness(['standard'])
const response = await api.agentPresets.copy(request({ from: 'never-existed', agentPreset: 'mine' }))
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('reports a deployment that composes no presets', async () => {
const { api } = await harness()
const response = await api.agentPresets.read(request({ agentPreset: 'anything' }))
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('reports an unknown id on delete rather than succeeding silently', async () => {
const { api } = await harness(['standard'])
const response = await api.agentPresets.remove(request({ agentPreset: 'never-existed' }))
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-not-found')
})
})
describe('opening a preset directory', () => {
it('hands the resolved directory to the native opener', async () => {
const opened: string[] = []
const { api } = await harness(['standard', 'my-preset'], undefined, {
userIds: ['my-preset'],
defaults: { openPath: (path: string) => { opened.push(path); return Promise.resolve() } },
})
const response = await api.agentPresets.openDocument(
request({ agentPreset: 'my-preset' }), new AbortController().signal)
expect(response.result.ok).toBe(true)
if (!response.result.ok) throw new Error('unreachable')
expect(response.result.value).toEqual({ opened: true })
// The id selected the directory; the browser supplied no path.
expect(opened).toEqual(['/presets/my-preset'])
})
it('answers the path as text where the deployment has no opener', async () => {
const { api } = await harness(['standard', 'my-preset'], undefined, {
userIds: ['my-preset'],
defaults: { canOpenPath: () => false },
})
const response = await api.agentPresets.openDocument(
request({ agentPreset: 'my-preset' }), new AbortController().signal)
expect(response.result.ok).toBe(true)
if (!response.result.ok) throw new Error('unreachable')
expect(response.result.value).toEqual({ opened: false, path: '/presets/my-preset' })
})
it('refuses a preset that ships with the deployment', async () => {
const opened: string[] = []
const { api } = await harness(['standard'], undefined, {
defaults: { openPath: (path: string) => { opened.push(path); return Promise.resolve() } },
})
const response = await api.agentPresets.openDocument(
request({ agentPreset: 'standard' }), new AbortController().signal)
// Pointing an editor into the install invites edits an upgrade will
// silently overwrite; the refusal mirrors copy/remove.
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-read-only')
expect(opened).toEqual([])
})
it('reports the roster capability on list', async () => {
const openable = await harness(['standard'], undefined, {
defaults: { canOpenPath: () => true },
})
const headless = await harness(['standard'], undefined, {
defaults: { canOpenPath: () => false },
})
const yes = await openable.api.agentPresets.list(request({}))
const no = await headless.api.agentPresets.list(request({}))
expect(yes.result.ok && yes.result.value.hasDocument).toBe(true)
expect(no.result.ok && no.result.value.hasDocument).toBe(false)
})
it('counts an injected opener as openable', async () => {
const { api } = await harness(['standard'], undefined, {
defaults: { openPath: () => Promise.resolve() },
})
const response = await api.agentPresets.list(request({}))
expect(response.result.ok && response.result.value.hasDocument).toBe(true)
})
})
describe('skills over the layered host registry', () => {
it('passes the live agent as the view scope to the host registry', async () => {
const { api, ctx } = await harness(['standard'])
const seen: unknown[] = []
ctx.provide('skills', {
list: (options: { scope?: unknown }) => {
seen.push(options.scope)
return Promise.resolve([])
},
} as never)
await api.sessions.create(request({ sessionId: SessionId('h1'), agentPreset: 'standard' }))
const response = await api.skills.list(request({ sessionId: SessionId('h1') }))
expect(response.result).toMatchObject({ ok: true, value: { skills: [] } })
expect(seen).toEqual([ctx.agents.get(SessionId('h1'))])
})
it('resolves a cold session to its recorded preset standing key', async () => {
const { api, ctx } = await harness(['standard', 'core-web'])
const seen: unknown[] = []
ctx.provide('skills', {
list: (options: { scope?: unknown }) => {
seen.push(options.scope)
return Promise.resolve([])
},
} as never)
ctx.sessions.create(SessionId('h2'), { meta: { cwd: '/workspace/cold', agentPreset: 'core-web' } })
const response = await api.skills.list(request({ sessionId: SessionId('h2') }))
expect(response.result).toMatchObject({ ok: true, value: { skills: [] } })
expect(seen).toEqual([standingKeys.get('core-web')])
})
it('serves the global view when the roster no longer supplies the recorded preset', async () => {
const { api, ctx } = await harness(['standard'])
const seen: unknown[] = []
ctx.provide('skills', {
list: (options: { scope?: unknown }) => {
seen.push(options.scope)
return Promise.resolve([])
},
} as never)
ctx.sessions.create(SessionId('h3'), { meta: { cwd: '/workspace/cold', agentPreset: 'gone' } })
const response = await api.skills.list(request({ sessionId: SessionId('h3') }))
expect(response.result).toMatchObject({ ok: true, value: { skills: [] } })
expect(seen).toEqual([undefined])
})
})
describe('session.history presenter scope', () => {
it('asks the roster for the RECORDED preset\'s standing key on a cold read', async () => {
const { api } = await harness(['standard', 'core-web'])
await api.sessions.create(request({ sessionId: SessionId('p1'), agentPreset: 'core-web' }))
// Cold: creation registered a live agent in this harness, so simulate the
// cold path by asking for a session only persistence knows... the harness
// has no persistence, so read the live one and assert no roster query.
standingKeyRequests.length = 0
const live = await api.sessions.history(request({ sessionId: SessionId('p1') }))
expect(live.result.ok).toBe(true)
// A live agent IS the presenter scope; the roster is not consulted.
expect(standingKeyRequests).toEqual([])
})
it('serves a COLD transcript whose standing mount is no longer usable', async () => {
// A genuinely cold session: persistence knows it, no live agent exists.
const meta = { id: SessionId('p3'), createdAt: 1, cwd: '/tmp/p3', agentPreset: 'standard' }
const { api } = await harness(['standard'], {
list: () => Promise.resolve([meta]),
inspect: () => Promise.resolve({ meta, events: [] }),
})
// The preset broke after the session ran: the roster rejects the mount.
failingStandingKeys.add('standard')
try {
standingKeyRequests.length = 0
const response = await api.sessions.history(request({ sessionId: SessionId('p3') }))
// Degraded, never failed: the roster WAS asked, and the transcript
// still serves — with the generic cards a viewless entry renders.
expect(standingKeyRequests).toEqual(['standard'])
expect(response.result.ok).toBe(true)
} finally {
failingStandingKeys.delete('standard')
}
})
})

View File

@@ -356,6 +356,21 @@ describe('settings domain', () => {
expect(frames).toEqual([{ type: 'host/settings-changed', ns: 'ui-onboarding' }])
})
it('serves the agent-preset namespace, so a browser preset picker can persist its choice', async () => {
const ctx = await harness()
ctx.settings.register(settingsNamespace('agent-presets'), z.object({ default: z.string() }))
const api = createApiProxy(ctx, DEFAULTS)
expectOk(await api.settings.update(request({ ns: 'agent-presets', patch: { default: 'minimal' } })))
// Both browser surfaces that offer the choice — the General row and the
// management section — write the default through `settings.update`, so a
// namespace outside this boundary makes the picker move and then silently
// forget, which is worse than refusing the control.
expect(ctx.settings.describe().find(view => String(view.ns) === 'agent-presets')?.value)
.toEqual({ default: 'minimal' })
})
it('refuses even a model-provider namespace once its directory entry is gone', async () => {
const ctx = await harness({ configurableProviders: false })
ctx.settings.register(NS, AdapterConfig)

View File

@@ -23,6 +23,7 @@ function scriptedApi(overrides: {
host?: Partial<ApiProxy['host']>
commands?: Partial<ApiProxy['commands']>
skills?: Partial<ApiProxy['skills']>
agentPresets?: Partial<ApiProxy['agentPresets']>
events?: Partial<ApiProxy['events']>
goals?: Partial<ApiProxy['goals']>
settings?: Partial<ApiProxy['settings']>
@@ -92,6 +93,15 @@ function scriptedApi(overrides: {
...overrides.commands,
},
skills: { list: r => ok(r, { skills: [] }), ...overrides.skills },
agentPresets: {
list: r => ok(r, { presets: [], authorable: false, hasDocument: false }),
select: r => ok(r, { agentPreset: r.payload.agentPreset }),
read: r => ok(r, { agentPreset: r.payload.agentPreset, trust: 'user' as const, content: '' }),
copy: r => ok(r, { agentPreset: r.payload.agentPreset }),
openDocument: r => ok(r, { opened: true as const }),
remove: r => ok(r, {}),
...overrides.agentPresets,
},
goals: {
create: err,
edit: err,
@@ -226,6 +236,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: [], authorable: false, hasDocument: false } })
// 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: {

View File

@@ -203,6 +203,32 @@ function fakeApi(overrides: Partial<{ muxFrames: MuxFrame[]; hostFrames: HostFra
return { rpcId: request.rpcId, result: { ok: true, value: { matched: false } } }
},
},
agentPresets: {
list(request: RpcRequest<{}>) {
return Promise.resolve({
rpcId: request.rpcId,
result: { ok: true as const, value: { presets: [], authorable: false, hasDocument: false } },
})
},
select(request: RpcRequest<{ agentPreset: string }>) {
const value = { agentPreset: request.payload.agentPreset }
return Promise.resolve({ rpcId: request.rpcId, result: { ok: true as const, value } })
},
read(request: RpcRequest<{ agentPreset: string }>) {
const value = { agentPreset: request.payload.agentPreset, trust: 'user' as const, content: '' }
return Promise.resolve({ rpcId: request.rpcId, result: { ok: true as const, value } })
},
copy(request: RpcRequest<{ from: string; agentPreset: string }>) {
const value = { agentPreset: request.payload.agentPreset }
return Promise.resolve({ rpcId: request.rpcId, result: { ok: true as const, value } })
},
openDocument(request: RpcRequest<{ agentPreset: string }>) {
return Promise.resolve({ rpcId: request.rpcId, result: { ok: true as const, value: { opened: true as const } } })
},
remove(request: RpcRequest<{ agentPreset: string }>) {
return Promise.resolve({ rpcId: request.rpcId, result: { ok: true as const, value: {} } })
},
},
skills: {
async list(request) {
return { rpcId: request.rpcId, result: { ok: true, value: { skills: [{ name: 'commit-helper', description: 'Git commits', modelInvocable: true }] } } }
@@ -347,6 +373,28 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
expect((await c.host.describe({})).result.ok).toBe(true)
})
it('round-trips every agent-preset method, authoring included', async () => {
const c = client()
// The whole domain crosses the carrier: the roster a picker reads, the
// per-session switch, and the authoring calls the settings page makes.
// Each has its own request schema, so a registration missing from either
// half fails here rather than in the browser.
expect((await c.agentPresets.list({})).result).toEqual({
ok: true, value: { presets: [], authorable: false, hasDocument: false },
})
expect((await c.agentPresets.select({ sessionId: 's' as never, agentPreset: 'minimal' })).result)
.toEqual({ ok: true, value: { agentPreset: 'minimal' } })
expect((await c.agentPresets.read({ agentPreset: 'mine' })).result).toEqual({
ok: true, value: { agentPreset: 'mine', trust: 'user', content: '' },
})
expect((await c.agentPresets.copy({ from: 'standard', agentPreset: 'mine' })).result)
.toEqual({ ok: true, value: { agentPreset: 'mine' } })
expect((await c.agentPresets.openDocument({ agentPreset: 'mine' })).result)
.toEqual({ ok: true, value: { opened: true } })
expect((await c.agentPresets.remove({ agentPreset: 'mine' })).result).toEqual({ ok: true, value: {} })
})
it('round-trips the native picker without the default unary timeout', async () => {
const api = fakeApi()
api.host.pickDirectory = async (request) => {

View File

@@ -16,7 +16,7 @@ vi.mock('node:child_process', () => ({ execFile: execFileMock }))
import { release as osRelease } from 'node:os'
import { describe, expect, it, vi } from 'vitest'
import { openNativePath, openNativeTextFile, type PathOpenerRunner } from '../src/native-path-opener.ts'
import { canOpenNativePath, openNativePath, openNativeTextFile, type PathOpenerRunner } from '../src/native-path-opener.ts'
const signal = () => new AbortController().signal
@@ -287,3 +287,35 @@ describe('browser-renderable documents', () => {
])
})
})
describe('canOpenNativePath', () => {
it('always answers yes where the desktop is part of the platform', () => {
expect(canOpenNativePath({ platform: 'darwin', env: {} })).toBe(true)
expect(canOpenNativePath({ platform: 'win32', env: {} })).toBe(true)
})
it('requires a display server or WSL interop on linux', () => {
const linux = { platform: 'linux' as const, osRelease: '6.8.0-generic' }
// Headless is the case the capability exists for: `xdg-open` would spawn
// into nothing, so a surface should show the path as text instead.
expect(canOpenNativePath({ ...linux, env: {} })).toBe(false)
expect(canOpenNativePath({ ...linux, env: { DISPLAY: ':0' } })).toBe(true)
expect(canOpenNativePath({ ...linux, env: { WAYLAND_DISPLAY: 'wayland-0' } })).toBe(true)
expect(canOpenNativePath({
platform: 'linux', osRelease: '5.15.153.1-microsoft-standard-WSL2', env: {},
})).toBe(true)
})
it('answers no on a platform the opener does not support', () => {
expect(canOpenNativePath({ platform: 'freebsd', env: {} })).toBe(false)
})
it('samples the ambient environment when no override is supplied', () => {
const env = process.env
const marked = (value: string | undefined): boolean => value !== undefined && value !== ''
const expected = marked(env.WSL_DISTRO_NAME) || marked(env.WSL_INTEROP)
|| marked(env.DISPLAY) || marked(env.WAYLAND_DISPLAY)
expect(canOpenNativePath({ platform: 'linux', osRelease: '6.8.0-generic' })).toBe(expected)
})
})

View File

@@ -32,6 +32,9 @@ import {
commandListRequestSchema, commandListValueSchema,
} from '../src/api/commands.schema.ts'
import { skillEntrySchema, skillListRequestSchema, skillListValueSchema } from '../src/api/skills.schema.ts'
import {
agentPresetEntrySchema, agentPresetListValueSchema, agentPresetOpenDocumentValueSchema,
} from '../src/api/agent-presets.schema.ts'
import { hostFrameSchema, muxFrameSchema, askUserQuestionItemSchema } from '../src/api/events.schema.ts'
import { approvalRequestIdSchema, approvalResponsePayloadSchema } from '../src/api/approvals.schema.ts'
import { askUserQuestionAnswerSchema, questionResponsePayloadSchema } from '../src/api/questions.schema.ts'
@@ -508,3 +511,27 @@ describe('respond payload schemas', () => {
expect(payload.sessionId).toBe('s')
})
})
describe('agent-preset schemas', () => {
it('accepts a roster row and rejects an unknown trust', () => {
expect(agentPresetEntrySchema.parse({ id: 'standard', trust: 'system', isDefault: true }))
.toEqual({ id: 'standard', trust: 'system', isDefault: true })
expect(() => agentPresetEntrySchema.parse({ id: 'x', trust: 'root', isDefault: false })).toThrow()
expect(() => agentPresetEntrySchema.parse({ id: '', trust: 'user', isDefault: false })).toThrow()
})
it('accepts an empty roster', () => {
// A deployment composing no presets still reports its authoring and
// native-open capabilities, so a surface knows what to offer.
expect(agentPresetListValueSchema.parse({ presets: [], authorable: false, hasDocument: false }))
.toEqual({ presets: [], authorable: false, hasDocument: false })
})
it('answers the open-document union by its discriminant', () => {
expect(agentPresetOpenDocumentValueSchema.parse({ opened: true })).toEqual({ opened: true })
expect(agentPresetOpenDocumentValueSchema.parse({ opened: false, path: '/presets/mine' }))
.toEqual({ opened: false, path: '/presets/mine' })
// A closed reply must carry the path the surface shows instead.
expect(() => agentPresetOpenDocumentValueSchema.parse({ opened: false })).toThrow()
})
})