Files
deepseek-harness/packages/preset/agent-presets/tests/mount.spec.ts
T
Yichen Jiang c58cc23d45 fix(web): address a session's own services from the host
A preset publishes its services behind `isolate` realms, which is what
makes them per session — and what makes them invisible to every host
context. The api-proxy kept reading the root realm, so requests that are
ABOUT a session but arrive from outside it answered for a singleton that
no longer exists: `goal.pause`/`clear` and `skill.list` returned "this
deployment does not mount @deepseek-ai/dsh-goal / dsh-skill" for sessions
whose composition mounts exactly that. Verified against a running host
before and after.

`agentPresets.serviceFor(agent, name)` addresses the instance instead,
reading the same subtree-ownership relation `leakedServices` already
uses, inverted. It is read addressing for a caller holding the agent: a
host row that `inject`s a service cannot use it, because injection
resolves before any session exists — which is why `tools` and
`subagents` stay host-plane and this is not a way around that.

Tool presenters had the same shape and the same cure: `viewFor` looked
definitions up without a scope while the global layer is empty by
design, so every card degraded to the generic renderer. It now takes the
owning agent.

Cold resume through `agentFor()` mounted no preset at all, so every
generic entry point — prompt, models, commands — rebuilt a restarted
session on host tools and the deployment persona. It composes the
recorded preset now, as the other resume path already did.
2026-08-07 00:35:30 +08:00

235 lines
9.4 KiB
TypeScript

import { dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import Include from '@cordisjs/plugin-include'
import LlmService from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { assembleContextFor, type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { beforeEach, describe, expect, it } from 'vitest'
import AgentPresets, { leakedServices, livePresetMounts } from '@deepseek-ai/dsh-agent-presets'
declare module 'cordis' {
interface Context {
/** Published by the `isolated` fixture preset behind an entry-local realm. */
fixtureIsolatedSvc: { label: string }
}
}
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures')
const ROOTS = [
{ path: join(FIXTURES, 'system'), trust: 'system' as const },
{ path: join(FIXTURES, 'user'), trust: 'user' as const },
]
/** A composition carrying the registries a preset contributes to, plus the preset roster. */
async function harness(): Promise<Context> {
const ctx = new Context()
ctx.baseUrl = pathToFileURL(FIXTURES).href + '/'
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
await ctx.plugin(LlmService)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona: '' })
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(AgentPresets, { default: 'standard', roots: ROOTS })
return ctx
}
/** Create one agent composed from `presetId`, exactly as a factory `setup` would. */
async function agentOn(ctx: Context, id: string, presetId?: string): Promise<Agent> {
const handle = await ctx.agents.create({
sessionId: SessionId(id),
setup: async (agentCtx: Context) => void await ctx.agentPresets.mount(agentCtx, presetId),
})
return handle.agent
}
const toolNames = (ctx: Context, agent?: Agent): string[] =>
ctx.tools.schemas(agent).map(schema => schema.name).sort()
/** Every service registration in the runtime, regardless of which realm holds it. */
function providedServiceNames(ctx: Context): string[] {
const store = ctx.reflect.store
return Object.getOwnPropertySymbols(store)
.map(key => store[key]?.name)
.filter((name): name is string => name !== undefined)
}
/** Whether the root realm maps `name` to a live registration. */
function rootResolves(ctx: Context, name: string): boolean {
const key = ctx.root[Context.isolate][name]
return key !== undefined && ctx.reflect.store[key] !== undefined
}
let ctx: Context
beforeEach(async () => {
ctx = await harness()
})
describe('composing an agent from a preset', () => {
it('gives each session only its own preset\'s tools', async () => {
const alpha = await agentOn(ctx, 'sess-alpha', 'standard')
const beta = await agentOn(ctx, 'sess-beta', 'minimal')
expect(toolNames(ctx, alpha)).toEqual(['alpha'])
expect(toolNames(ctx, beta)).toEqual(['beta'])
expect(toolNames(ctx)).toEqual([])
})
it('scopes prompt sections and assembled schemas to the same session', async () => {
const alpha = await agentOn(ctx, 'sess-alpha', 'standard')
const beta = await agentOn(ctx, 'sess-beta', 'minimal')
const alphaPrompt = await ctx.systemPrompt.assemble(assembleContextFor(alpha))
const betaPrompt = await ctx.systemPrompt.assemble(assembleContextFor(beta))
expect(alphaPrompt.sections.map(section => section.name)).toContain('preset:alpha')
expect(alphaPrompt.sections.map(section => section.name)).not.toContain('preset:beta')
expect(betaPrompt.sections.map(section => section.name)).toContain('preset:beta')
expect(alphaPrompt.tools.map(schema => schema.name)).toEqual(['alpha'])
})
it('mounts the default preset when the caller names none', async () => {
const agent = await agentOn(ctx, 'sess-default')
expect(toolNames(ctx, agent)).toEqual(['alpha'])
})
it('lets two sessions share one preset without colliding', async () => {
const first = await agentOn(ctx, 'sess-first', 'standard')
const second = await agentOn(ctx, 'sess-second', 'standard')
expect(toolNames(ctx, first)).toEqual(['alpha'])
expect(toolNames(ctx, second)).toEqual(['alpha'])
})
it('unwinds one session\'s composition without touching another\'s', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('sess-gone'),
setup: async (agentCtx: Context) => void await ctx.agentPresets.mount(agentCtx, 'standard'),
})
const survivor = await agentOn(ctx, 'sess-stays', 'minimal')
expect(toolNames(ctx, handle.agent)).toEqual(['alpha'])
await handle.dispose()
expect(ctx.agents.get(SessionId('sess-gone'))).toBeUndefined()
expect(toolNames(ctx, survivor)).toEqual(['beta'])
expect(toolNames(ctx)).toEqual([])
})
})
describe('rejecting a composition that cannot be used', () => {
it('refuses to mount into a context that carries no agent scope', async () => {
await expect(ctx.agentPresets.mount(ctx, 'standard'))
.rejects.toThrow(/unscoped context/)
})
it('rolls the whole agent back when a row fails to load', async () => {
await expect(agentOn(ctx, 'sess-broken', 'broken')).rejects.toThrow(/failed to mount/)
expect(ctx.agents.get(SessionId('sess-broken'))).toBeUndefined()
expect(toolNames(ctx)).toEqual([])
})
it('names the unresolved service when a row never activates', async () => {
await expect(agentOn(ctx, 'sess-pending', 'pending'))
.rejects.toThrow(/waiting for serviceThatDoesNotExist/)
})
it('rejects a row that publishes a process-global service', async () => {
await expect(agentOn(ctx, 'sess-leaky', 'leaky'))
.rejects.toThrow(/process-global service\(s\) \[aaaFixtureLeakedSvc, zzzFixtureLeakedSvc\]/)
// The rejected subtree is fully unwound, so its registrations are gone from
// the store rather than merely unreachable.
expect(providedServiceNames(ctx)).not.toContain('aaaFixtureLeakedSvc')
expect(providedServiceNames(ctx)).not.toContain('zzzFixtureLeakedSvc')
})
it('accepts the same provider behind an isolate realm', async () => {
const agent = await agentOn(ctx, 'sess-isolated', 'isolated')
expect(agent.id).toBe(SessionId('sess-isolated'))
// The provider ran, but under a realm-private symbol the root cannot reach.
expect(providedServiceNames(ctx)).toContain('fixtureIsolatedSvc')
expect(rootResolves(ctx, 'fixtureIsolatedSvc')).toBe(false)
})
it('addresses one agent\'s instance of a realm-private service', async () => {
const first = await agentOn(ctx, 'sess-reach-a', 'isolated')
const second = await agentOn(ctx, 'sess-reach-b', 'isolated')
// The realm keeps the service out of every host context — that is what
// makes it per session — so a caller holding the agent is the only way a
// request from OUTSIDE the session can read the instance it is about.
expect(rootResolves(ctx, 'fixtureIsolatedSvc')).toBe(false)
const mine = ctx.agentPresets.serviceFor(first, 'fixtureIsolatedSvc')
const theirs = ctx.agentPresets.serviceFor(second, 'fixtureIsolatedSvc')
expect(mine).toBeDefined()
expect(theirs).toBeDefined()
// Each agent gets ITS own: the addressing is per subtree, not a lookup
// that happens to find the first match.
expect(mine).not.toBe(theirs)
})
it('answers undefined for a service the agent\'s preset does not mount', async () => {
const agent = await agentOn(ctx, 'sess-reach-none', 'standard')
expect(ctx.agentPresets.serviceFor(agent, 'fixtureIsolatedSvc')).toBeUndefined()
})
it('reports the known ids when a preset is unknown', async () => {
await expect(ctx.agentPresets.resolve('nope'))
.rejects.toThrow(/preset "nope" not found \(available: .*standard/)
})
})
describe('the preset roster', () => {
it('lists every root\'s presets with the earlier root winning', async () => {
const listed = await ctx.agentPresets.list()
expect(listed.map(preset => preset.id).sort())
.toEqual(['broken', 'isolated', 'late', 'leaky', 'minimal', 'pending', 'standard'])
expect(listed.find(preset => preset.id === 'standard')?.trust).toBe('system')
})
it('exposes the configured default id', () => {
expect(ctx.agentPresets.defaultId).toBe('standard')
})
})
describe('a roster with nothing in it', () => {
it('says so instead of naming an empty list of candidates', async () => {
const bare = new Context()
await bare.plugin(Loader)
await bare.plugin(AgentPresets, { default: 'standard', roots: [] })
await expect(bare.agentPresets.resolve())
.rejects.toThrow(/preset "standard" not found \(available: none\)/)
})
})
describe('attributing a service to a subtree', () => {
it('attributes nothing to a subtree that is already torn down', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('sess-torn'),
setup: async (agentCtx: Context) => void await ctx.agentPresets.mount(agentCtx, 'standard'),
})
const [mount] = livePresetMounts().filter(entry => entry.presetId === 'standard')
expect(mount).toBeDefined()
await handle.dispose()
// A disposed subtree owns nothing, so it can never be blamed for a service
// some other subtree published under the same name afterwards.
expect(leakedServices(ctx, mount!.fiber)).toEqual([])
})
})