Files
deepseek-harness/apps/cli/tests/web-agent-presets.e2e.ts
T

1083 lines
54 KiB
TypeScript

import { executeToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-preset-registry'
import { subagentModelSelectionProjectionDefinition } from '@deepseek-ai/dsh-tool-subagent/src/model-selection-state.ts'
import { createRequire } from 'node:module'
import { randomUUID } from 'node:crypto'
import { mkdir, mkdtemp, readFile, realpath, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import {
initProfile,
createRuntimeResolution,
loadOverlayPatches,
loadProfile,
PluginPackages,
type Profile,
} from '@deepseek-ai/dsh-app-boot'
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import { afterAll, beforeAll, describe, expect, it, onTestFinished, vi } from 'vitest'
import { bundlePatchPaths, composeEntries } from '@deepseek-ai/dsh-app-boot'
import { createPluginProfile } from '../../desktop/src/project-manager.ts'
/** Profile entry ids whose volatile fields these scenarios edit through Settings. */
const SETTINGS_NAMESPACE = 'agent-preset-registry'
const SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE = 'subagent-model-selection-settings'
import { applyChildComposition, childSessionMeta } from '@deepseek-ai/dsh-subagent'
import { ToolCallId, LlmAdapter, createUserMessage } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-compaction-basic'
import type {} from '@deepseek-ai/dsh-skill'
import type {} from '@deepseek-ai/dsh-tools'
// Type-only: resolves `ctx.get('sessionProjections')` and `ctx.get('tokenMeter')`.
import type {} from '@deepseek-ai/dsh-session-projection'
import type {} from '@deepseek-ai/dsh-token-meter'
const REPO_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
const { boot } = createRequire(import.meta.url)(join(REPO_ROOT, 'packages/boot/app-boot/lib/index.js')) as typeof import('@deepseek-ai/dsh-app-boot')
/** The shipped Web surface: the dsh-base and dsh-web-app bundle patches over an empty profile. */
const BASE_PATCH = join(REPO_ROOT, 'packages/bundle/base/cordis.patch.yml')
const WEB_BUNDLE = join(REPO_ROOT, 'packages/bundle/web-app')
const WEB_PATCHES = bundlePatchPaths(WEB_BUNDLE, (JSON.parse(readFileSync(join(WEB_BUNDLE, 'package.json'), 'utf8')) as { dsh: { bundle: { patch: string[] } } }).dsh.bundle)
const webPatches = (label: string): PatchOptions[] => WEB_PATCHES.flatMap(file => loadOverlayPatches(label, file))
const CODEX_PACKAGE_DIR = join(REPO_ROOT, 'packages/subagent/subagent-codex')
const CLAUDE_CODE_PACKAGE_DIR = join(REPO_ROOT, 'packages/subagent/subagent-claude-code')
/** The installation anchor whose dependency surface the runtime resolution mirrors. */
const INSTALL_ANCHOR = join(REPO_ROOT, 'apps/cli/package.json')
const MINIMAL_PROMPT = 'You are a helpful software engineer assistant.'
/** Deterministic model responses for the shipped minimal compaction path. */
class MinimalCompactionAdapter extends LlmAdapter {
readonly requests: GenerateOptions[] = []
override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
return Promise.resolve({ provider, id: model, name: model, context: { contextWindow: 100_000 } })
}
override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
this.requests.push(options)
yield { type: 'block-start', index: 0, blockType: 'text' }
yield { type: 'block-end', index: 0, block: {
type: 'text', text: options.purpose === 'compaction' ? 'Preserve the requested project task.' : 'Ready.',
} }
yield { type: 'finish', reason: { kind: 'stop' } }
}
}
/** Emits one image read, then retains the request carrying its settled result. */
class MinimalImageAdapter extends LlmAdapter {
readonly requests: GenerateOptions[] = []
override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
return Promise.resolve({ provider, id: model, name: model, context: { contextWindow: 1_000_000 },
inputModalities: ['text', 'image'] })
}
override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
if (options.purpose !== undefined) {
yield { type: 'block-start', index: 0, blockType: 'text' }
yield { type: 'block-end', index: 0, block: { type: 'text', text: 'Image inspection' } }
yield { type: 'finish', reason: { kind: 'stop' } }
return
}
this.requests.push(options)
const first = this.requests.length === 1
yield { type: 'block-start', index: 0, blockType: first ? 'tool-call' : 'text' }
yield { type: 'block-end', index: 0, block: first
? { type: 'tool-call', id: ToolCallId('minimal-image-read'), name: 'read_image', arguments: '{"file_path":"red.png"}' }
: { type: 'text', text: 'Image inspected.' } }
yield { type: 'finish', reason: { kind: first ? 'tool-calls' : 'stop' } }
}
}
const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell
* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.
* State is persistent across command calls and discussions with the user.
* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
* Please avoid commands that may produce a very large amount of output.
* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.`
/**
* Boot the shipped Web composition, minus the rows that would bind a port,
* touch the network, or write outside the test. Everything that decides an
* agent's capabilities is the real thing, including both shipped presets.
*/
async function bootWeb(
profileHome: string,
extra: PatchOptions[] = [],
profilePackages: readonly string[] = [],
profileBundles?: readonly string[],
surface: 'web' | 'desktop' | 'spec' = 'spec',
): Promise<Context> {
const storageRoot = join(profileHome, 'storages')
const overrides: PatchOptions[] = [
// storage-json's root is anchored to the real $DSH_HOME. Unpinned, this
// file writes the developer's own `~/.dsh/storages/` — and then reads it
// back on the next run, so a stored document from any other build decides
// this test's boot.
{ id: 'storage-json', config: { root: storageRoot } },
// Fixed Session IDs must stay inside this boot's temporary profile root.
{ id: 'session-persistence-jsonl', config: { root: join(profileHome, 'sessions') } },
{ id: 'attachment-local', config: { dshHome: profileHome } },
// Host rows with side effects outside this process: a bound port, a served
// asset tree, a telemetry exporter. `api-gateway` and `directory-picker`
// stay ENABLED on purpose — the api-proxy is the host row that injects
// `subagents`, `workspace`, and the rest of the agent plane, so disabling
// it would hide exactly the breakage this file exists to catch: a service
// moved into the presets that a host row still waits for. The boot audit
// is that assertion.
{ id: 'webserver', disabled: true },
// This composition has no application readiness or file-watching lifecycle.
{ id: 'hmr', disabled: true },
// The web bundle's runtime row injects `webServer`, so it cannot
// activate without the bound port disabled above. It owns dist serving
// and the URL prompt line — surface glue, not anything that decides an
// agent's capabilities, which is all this file asserts.
{ id: 'web-runtime', disabled: true },
{ id: 'session-telemetry-otel', disabled: true },
{ id: 'desktop-product-telemetry', disabled: true },
{ id: 'product-analytics', disabled: true },
// A deployment-level skill on the host registry's GLOBAL layer — the same
// registration shape a repository plugin's skill root uses. The layered
// skills test below proves it reaches preset-composed agents.
{ insert: [{ id: 'skill-badge', name: import.meta.resolve('@deepseek-ai/dsh-skill-badge') }] },
{ id: 'modules', disabled: true },
// The physical Connection row owns the disabled HTTP server. bootWeb
// supplies only its in-process registries so Host services still prove
// their shipped dependency graph without binding a port.
{ id: 'connection', disabled: true },
// Export owns a Connection Fetch route, so this Host-only composition
// disables it with the transport service above.
{ id: 'session-log-download', disabled: true },
// The open-in-app host routes wait for the webserver and connection
// rows disabled above (connection's trust fence guards every route).
{ id: 'open-in-app', disabled: true },
// The always-on reload chain waits for the browser roster and bound port
// disabled above.
{ id: 'client-hmr', disabled: true },
// The shipped `-auto` chooser resolves its interaction from a running
// host and so waits for the webserver disabled above; the browse variant
// supplies `directoryPicker` without one.
{ id: 'directory-picker', disabled: true },
{ insert: [
{ id: 'directory-picker-browse', name: '@deepseek-ai/dsh-host-directory-picker-browse' },
{ id: 'ui-directory-picker-browse', name: '@deepseek-ai/dsh-client-ui-directory-picker-browse' },
] },
{ id: 'agent-preset-registry', config: { default: 'standard' } },
...extra,
]
const home = profileHome
const profileDir = join(home, 'profiles', surface)
await mkdir(profileDir, { recursive: true })
if (surface === 'desktop') createPluginProfile(profileDir)
else if (profileBundles === undefined) initProfile(profileDir, ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
// Product Bundles are installed into the Profile, not the dsh app. Model
// pnpm's package link for only the selected products; their own production
// dependencies resolve from the linked workspace packages, while shared
// peers still resolve through the installation fallback above.
for (const packageDir of profilePackages) {
const manifest = JSON.parse(await readFile(join(packageDir, 'package.json'), 'utf8')) as { name: string }
const link = join(profileDir, 'node_modules', manifest.name)
await mkdir(dirname(link), { recursive: true })
await symlink(packageDir, link, 'junction')
}
let profile: Profile = { skippedBundles: [],
name: surface,
dir: profileDir,
layers: [],
patchPath: join(profileDir, 'cordis.patch.yml'),
patches: [],
}
let bundlePatches: PatchOptions[] = [
...loadOverlayPatches('dsh-test', BASE_PATCH),
...webPatches('dsh-test'),
]
if (profileBundles !== undefined) {
await writeFile(join(profileDir, 'package.json'), JSON.stringify({
private: true,
dependencies: Object.fromEntries(profileBundles.map(name => [name, 'workspace:*'])),
dsh: { profile: { bundles: profileBundles } },
}, null, 2) + '\n')
profile = loadProfile('dsh-test', surface, INSTALL_ANCHOR, home, { userLayer: false })
bundlePatches = profile.layers.flatMap(layer => layer.patches)
}
// Deployment defaults live in a bundle beneath the profile patch, so Settings writes are not shadowed by overlays.
const fixtureName = 'dsh-web-presets-defaults'
const fixtureDir = join(profileDir, 'node_modules', fixtureName)
await mkdir(fixtureDir, { recursive: true })
await writeFile(join(fixtureDir, 'package.json'), JSON.stringify({ name: fixtureName, version: '1.0.0', dsh: { bundle: { patch: 'cordis.patch.yml' } } }))
await writeFile(join(fixtureDir, 'cordis.patch.yml'), JSON.stringify(overrides))
const manifest = JSON.parse(await readFile(join(profileDir, 'package.json'), 'utf8')) as { dsh: { profile: { bundles: string[] } } }
manifest.dsh.profile.bundles.push(fixtureName)
await writeFile(join(profileDir, 'package.json'), JSON.stringify(manifest))
const resolution = await createRuntimeResolution({ installAnchor: INSTALL_ANCHOR, home, profile })
const rootConfig = join(profileDir, 'cordis.yml')
await writeFile(rootConfig, '[]\n')
return await boot('dsh-test', rootConfig, [...bundlePatches, ...overrides], async (bootCtx) => {
bootCtx.provide('profileContext', { name: surface, dir: profileDir, patchPath: profile.patchPath,
installAnchor: INSTALL_ANCHOR, home, cwd: home,
startedBundles: profileBundles ?? ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'],
overlays: [], telemetryDisabledEnv: '1' })
await bootCtx.plugin(PluginPackages, { resolution })
bootCtx.provide('connection', {
fetch: { register: () => () => {} },
rpc: { intercept: () => () => {} },
} as never)
provideCmdline(bootCtx, { args: [], exit: () => {} })
})
}
const toolNames = (ctx: Context, agent?: Agent): string[] =>
ctx.tools.schemas(agent).map(schema => schema.name).sort()
function toolParameterNames(ctx: Context, agent: Agent, toolName: string): string[] {
const schema = ctx.tools.schemas(agent).find(tool => tool.name === toolName)
if (schema === undefined) throw new Error(`missing tool schema ${toolName}`)
const properties = schema.parameters.properties
if (typeof properties !== 'object' || properties === null || Array.isArray(properties)) {
throw new Error(`${toolName} has invalid parameter properties`)
}
return Object.keys(properties).sort()
}
let ctx: Context
beforeAll(async () => {
ctx = await bootWeb(await mkdtemp(join(tmpdir(), 'dsh-web-presets-')))
}, 120_000)
describe('the shipped Web composition', () => {
it('exposes only the directory tool in the global layer', () => {
// Directory selection is global; action tools belong to their Agent presets.
expect(toolNames(ctx)).toEqual(['working_directory'])
})
it('keeps the token meter and its context-meter projections on the host plane', async () => {
// Read before any preset in this file mounts, which is what makes this an
// ownership assertion rather than a mount-order coincidence: a preset-side
// meter sits behind an `isolate` realm and is invisible to `ctx.get`.
//
// The projection registry is process-wide rather than scope-layered, so a
// preset-side meter would also make the browser's context meter appear for
// a `minimal` session the moment some OTHER session mounted a preset that
// carries one, and vanish entirely in a process that only ever ran
// `minimal`. Host ownership is what makes the meter a per-session fact.
expect(ctx.get('tokenMeter')).toBeDefined()
const projections = ctx.get('sessionProjections')
if (projections === undefined) throw new Error('the Web composition must compose a projection registry')
const handle = await ctx.agents.create({
sessionId: SessionId('preset-minimal-meter'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
try {
// A subset assertion: `tasks`, `goal`, and the rest register into the
// same process-wide table, and this is about the meter's three units.
expect(Object.keys(projections.snapshot(handle.agent.session).values))
.toEqual(expect.arrayContaining(['contextBreakdown', 'contextPressure', 'tokenUsage']))
} finally {
await handle.dispose()
}
})
it('supplies both shipped presets, and only those, from the system root', async () => {
const listed = await ctx.agentPresets.list()
expect(listed.map(preset => preset.id).sort()).toEqual(['cordis', 'minimal', 'ptc', 'standard'])
expect(listed.every(preset => !('path' in preset))).toBe(true)
expect(ctx.agentPresets.defaultId).toBe('standard')
})
it('composes the full agent from `standard`', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('preset-standard'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
try {
// The EXACT catalog, not a spot-check: an omission is this design's
// quietest failure mode, because a row that registers into the wrong
// layer mounts cleanly and simply contributes nothing. `glob`/`grep` are
// excluded for the reason the TUI composition e2e excludes them — they
// depend on ripgrep being present on the machine.
expect(toolNames(ctx, handle.agent).filter(name => name !== 'glob' && name !== 'grep')).toEqual([
'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode',
'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'list_subagent_models', 'plugin_manager', 'present', 'read', 'read_image',
'schedule_create', 'schedule_delete', 'schedule_list', 'schedule_update',
'send_message', 'skill',
'subagent', 'subagent_fork', 'todo_write', 'update_goal', 'web_fetch', 'web_search',
'workflow', 'working_directory', 'write',
])
expect(ctx.commands.find(handle.agent, 'goal')).toBeDefined()
} finally {
await handle.dispose()
}
})
it('applies the default-off subagent model allowlist only to new sessions', async () => {
await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
enabled: false,
allowedModels: [],
})
const disabled = await ctx.agents.create({
sessionId: SessionId('preset-model-selection-disabled'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
enabled: true,
allowedModels: [{ provider: 'deepseek-official', model: 'deepseek-v4-flash' }],
})
const enabled = await ctx.agents.create({
sessionId: SessionId('preset-model-selection-enabled'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
const policy = await ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition)
try {
expect(toolNames(ctx, disabled.agent)).toContain('list_subagent_models')
expect(toolParameterNames(ctx, disabled.agent, 'subagent')).toEqual(expect.arrayContaining([
'model', 'provider', 'reasoning_effort',
]))
expect(policy.stateOf(disabled.agent.session)).toBeNull()
expect(policy.stateOf(enabled.agent.session)).toEqual([
{ provider: 'deepseek-official', model: 'deepseek-v4-flash' },
])
expect(toolNames(ctx, enabled.agent)).toContain('list_subagent_models')
expect(toolParameterNames(ctx, enabled.agent, 'subagent')).toEqual(expect.arrayContaining([
'model', 'provider', 'reasoning_effort',
]))
for (const agent of [disabled.agent, enabled.agent]) {
expect(toolParameterNames(ctx, agent, 'subagent_fork')).toEqual(['cwd', 'description', 'prompt'])
}
} finally {
await policy.dispose()
await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: false })
await enabled.dispose()
await disabled.dispose()
}
})
it('composes the exact RL prompt and persistent shell from `minimal`', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('preset-minimal'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
try {
const assembly = await ctx.systemPrompt.assemble({ scope: handle.agent })
expect(assembly.sections).toEqual([
{ name: 'deployment:persona-prefix', text: MINIMAL_PROMPT },
])
expect(assembly.tools.map(tool => tool.name)).toEqual(['bash', 'read_image', 'working_directory'])
expect(assembly.tools.find(tool => tool.name === 'bash')?.description).toBe(MINIMAL_BASH_DESCRIPTION)
expect(ctx.commands.find(handle.agent, 'goal')).toBeUndefined()
// serviceFor reports preset-owned providers; unisolated consumers inherit the host fs.
expect(ctx.agentPresets.serviceFor(handle.agent, 'fs')).toBeUndefined()
expect(ctx.get('fs')?.sandboxMode).toBeDefined()
expect(handle.agent.ctx.get('fs')?.sandboxMode).toBe(ctx.get('fs')?.sandboxMode)
expect(ctx.agentPresets.serviceFor(handle.agent, 'compaction')).toBeDefined()
expect(handle.agent.ctx.get('compaction')).toBeUndefined()
expect(ctx.commands.find(handle.agent, 'compact')).toBeDefined()
expect(await ctx.commands.execute(handle.agent, '/compact', [], new AbortController().signal))
.toMatchObject({ result: { kind: 'success', text: 'No compactable history yet.' } })
expect(handle.agent.session.snapshotEvents().some(event => event.type === 'compaction/start')).toBe(false)
} finally {
await handle.dispose()
}
})
it('reads an image in minimal mode and carries its durable reference into the next request', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'dsh-minimal-image-'))
onTestFinished(() => rm(cwd, { recursive: true, force: true }))
await writeFile(join(cwd, 'red.png'), Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC', 'base64'))
const adapter = new MinimalImageAdapter()
const unregister = ctx.llm.registerAdapter(['minimal-image-test'], adapter)
onTestFinished(unregister)
const handle = await ctx.agents.create({
sessionId: SessionId('preset-minimal-image'), meta: { cwd },
agentOptions: { provider: 'minimal-image-test', model: 'minimal-image-test' },
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
try {
handle.agent.followup(createUserMessage({ content: [{ type: 'text', text: 'Inspect red.png.' }], source: { kind: 'user' } }))
await handle.agent.whenIdle()
expect(adapter.requests).toHaveLength(2)
const result = handle.agent.session.snapshotEvents().find(event => event.type === 'tool/result')
expect(result?.data.message.isError, JSON.stringify(result?.data.message.content)).toBe(false)
const image = handle.agent.session.deriveMessages().flatMap(message => message.content)
.find(block => block.type === 'image')
expect(image?.type).toBe('image')
if (image?.type !== 'image') throw new Error('minimal image read produced no durable image')
expect((await ctx.attachments.readImage(image.attachment)).data.byteLength).toBe(image.attachment.bytes)
expect(JSON.stringify(adapter.requests[1]?.messages)).toContain(image.attachment.attachmentId)
expect(adapter.requests[0]?.tools?.map(tool => tool.name)).toEqual(['bash', 'read_image', 'working_directory'])
expect(result?.data.meta).toMatchObject({ path: await realpath(join(cwd, 'red.png')) })
} finally {
await handle.dispose()
}
})
it('compacts minimal history through /compact and continues from the checkpoint', async () => {
const adapter = new MinimalCompactionAdapter()
const unregister = ctx.llm.registerAdapter(['minimal-compaction-test'], adapter)
onTestFinished(unregister)
const handle = await ctx.agents.create({
sessionId: SessionId('preset-minimal-compaction'),
agentOptions: { provider: 'minimal-compaction-test', model: 'minimal-compaction-test' },
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
try {
handle.agent.followup(createUserMessage({
content: [{ type: 'text', text: 'Remember the project requirements. '.repeat(200) }], source: { kind: 'user' },
}))
await handle.agent.whenIdle()
const before = handle.agent.session.surface.replaceGeneration
const execution = await ctx.commands.execute(handle.agent, '/compact', [], new AbortController().signal)
expect(execution?.result.kind).toBe('success')
expect(handle.agent.session.surface.replaceGeneration).toBeGreaterThan(before)
const summary = handle.agent.session.snapshotEvents().find(event => event.type === 'compaction/summary')
expect(summary?.data.sourceCommandId).toBe(execution?.commandId)
expect(adapter.requests.filter(request => request.purpose === 'compaction')).toHaveLength(1)
handle.agent.followup(createUserMessage({
content: [{ type: 'text', text: 'Continue the project task.' }], source: { kind: 'user' },
}))
await handle.agent.whenIdle()
const last = adapter.requests.at(-1)
expect(last?.messages.flatMap(message => message.content)
.some(block => block.type === 'text' && block.text.includes('Preserve the requested project task.'))).toBe(true)
expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'read_image', 'working_directory'])
} finally {
await handle.dispose()
}
})
it('automatically compacts minimal history under context pressure', async () => {
const adapter = new MinimalCompactionAdapter()
const unregister = ctx.llm.registerAdapter(['minimal-pressure-test'], adapter)
onTestFinished(unregister)
const handle = await ctx.agents.create({
sessionId: SessionId('preset-minimal-pressure'),
agentOptions: { provider: 'minimal-pressure-test', model: 'minimal-pressure-test' },
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
try {
for (let turn = 0; turn < 3; turn++) {
handle.agent.followup(createUserMessage({
content: [{ type: 'text', text: 'Remember the project requirements. '.repeat(6000) }],
source: { kind: 'user' },
}))
await handle.agent.whenIdle()
}
const summaries = handle.agent.session.snapshotEvents().filter(event => event.type === 'compaction/summary')
expect(summaries.length).toBeGreaterThan(0)
expect(summaries.every(event => event.data.sourceCommandId === undefined)).toBe(true)
expect(handle.agent.session.snapshotEvents().some(event => event.type === 'compaction/start'
&& event.data.turn !== null)).toBe(true)
expect(adapter.requests.some(request => request.purpose === 'compaction')).toBe(true)
} finally {
await handle.dispose()
}
})
it('keeps two differently composed sessions independent', async () => {
const full = await ctx.agents.create({
sessionId: SessionId('preset-both-full'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
const minimal = await ctx.agents.create({
sessionId: SessionId('preset-both-minimal'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
try {
expect(toolNames(ctx, minimal.agent)).toEqual(['bash', 'read_image', 'working_directory'])
expect(toolNames(ctx, full.agent).length).toBeGreaterThan(10)
expect(ctx.agentPresets.serviceFor(minimal.agent, 'compaction'))
.not.toBe(ctx.agentPresets.serviceFor(full.agent, 'compaction'))
await minimal.dispose()
// Tearing the minimal session down leaves the full one whole.
expect(toolNames(ctx, full.agent).length).toBeGreaterThan(10)
expect(toolNames(ctx)).toEqual(['working_directory'])
} finally {
await full.dispose()
}
})
it('composes the cordis agent with its own toolset', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('preset-cordis'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'cordis').then(() => undefined),
})
try {
const tools = toolNames(ctx, handle.agent)
expect(tools).toEqual(expect.arrayContaining([
'cordis_inspect_list', 'cordis_inspect_query', 'plugin_manager',
]))
for (const removed of ['cordis_define', 'cordis_run', 'cordis_stop', 'cordis_undefine', 'cordis_inspect_self']) {
expect(tools).not.toContain(removed)
}
expect(tools).toEqual(expect.arrayContaining(['bash', 'read', 'edit', 'skill']))
expect(tools).not.toContain('str_replace_editor')
expect(ctx.commands.find(handle.agent, 'goal')).toBeDefined()
// The preset's own authoring skill registers into ITS layer of the host
// registry: the cordis agent's view carries it, the global view does not.
const scoped = (await ctx.skills.list({ scope: handle.agent })).map(skill => skill.name)
expect(scoped).toContain('editing-cordis-compositions')
expect((await ctx.skills.list()).map(skill => skill.name)).not.toContain('editing-cordis-compositions')
// The persona is `standard`'s, pinned verbatim so the two declarations
// cannot drift apart: tool descriptions and the skill catalog carry
// every creation-mode instruction.
const standard = await ctx.agents.create({
sessionId: SessionId('preset-cordis-standard-persona'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
try {
const persona = async (agent: Agent) => (await ctx.systemPrompt.assemble({ scope: agent })).sections
.filter(section => section.name.startsWith('deployment:persona-'))
const cordisPersona = await persona(handle.agent)
expect(cordisPersona.map(section => section.name)).toEqual(['deployment:persona-prefix', 'deployment:persona-suffix'])
expect(cordisPersona).toEqual(await persona(standard.agent))
} finally {
await standard.dispose()
}
} finally {
await handle.dispose()
}
})
it('presents `ptc` as PTC mode without disturbing a native session beside it', async () => {
const coded = await ctx.agents.create({
sessionId: SessionId('preset-ptc'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'ptc').then(() => undefined),
})
const native = await ctx.agents.create({
sessionId: SessionId('preset-ptc-native'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
try {
// One tool reaches the MODEL: the transport. The registry's catalog for
// this agent is unchanged — PTC mode collapses the presentation, not
// the capabilities — so the assembly is what carries the claim.
const assembly = await ctx.systemPrompt.assemble({ scope: coded.agent })
expect(assembly.tools.map(tool => tool.name)).toEqual(['run_code'])
expect(toolNames(ctx, coded.agent)).not.toContain('str_replace_editor')
expect(ctx.commands.find(coded.agent, 'goal')).toBeDefined()
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? ''
expect(sdk).not.toContain('str_replace_editor')
expect(sdk).toContain('web_search')
// The presentation is this agent's alone: the deployment default is
// native, and the session composed from `standard` still sees it.
const nativeAssembly = await ctx.systemPrompt.assemble({ scope: native.agent })
expect(nativeAssembly.tools.map(tool => tool.name)).toContain('bash')
expect(nativeAssembly.tools.map(tool => tool.name)).not.toContain('run_code')
expect(nativeAssembly.sections.some(section => section.name === 'tools:sdk')).toBe(false)
} finally {
await native.dispose()
await coded.dispose()
}
})
it('keeps the self-referential toolset out of every other preset', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('preset-no-cordis'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
try {
// Editing the live runtime is opt-in per session, not ambient.
expect(toolNames(ctx, handle.agent)).not.toContain('cordis_define')
} finally {
await handle.dispose()
}
})
it('ships the composition-authoring skill in the declaration package', async () => {
const skill = join(
REPO_ROOT, 'packages/preset/agent-preset', 'skills', 'editing-cordis-compositions', 'SKILL.md',
)
expect((await readFile(skill, 'utf8')).startsWith('---\nname: editing-cordis-compositions')).toBe(true)
})
it('merges the global skill layer into a preset agent\'s catalog, keeping local discovery preset-side', async () => {
const proj = await mkdtemp(join(tmpdir(), 'dsh-preset-skill-proj-'))
await mkdir(join(proj, '.dsh', 'skills', 'project-proof'), { recursive: true })
await writeFile(join(proj, '.dsh', 'skills', 'project-proof', 'SKILL.md'), [
'---',
'name: project-proof',
'description: Proves the preset layer discovers project skills beside global ones.',
'---',
'',
'Project proof body.',
'',
].join('\n'))
const handle = await ctx.agents.create({
// Unique per run: the composition persists into the ambient DSH home,
// and a fixed id would collide with a log an earlier run left there.
sessionId: SessionId(`preset-skills-standard-${randomUUID()}`),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
try {
// The host (global) view carries the deployment-level provider alone:
// local discovery moved behind the presets with `skill-filesystem`.
expect((await ctx.skills.list({ cwd: proj })).map(skill => skill.name)).toEqual(['dsh-badge'])
// The standard agent's view merges the global layer with its preset's
// own local discovery over the session cwd.
const scoped = (await ctx.skills.list({ cwd: proj, scope: handle.agent })).map(skill => skill.name)
expect(scoped).toContain('dsh-badge')
expect(scoped).toContain('project-proof')
// The preset's own loader tool resolves the global-layer skill.
const loaded = await executeToolFixture(ctx, {
callId: ToolCallId('preset-skills-load'),
name: 'skill',
arguments: { name: 'dsh-badge' },
signal: new AbortController().signal,
agent: handle.agent,
})
expect(loaded.isError).toBe(false)
expect(JSON.stringify(loaded.content)).toContain('powered by dsh')
} finally {
await handle.dispose()
}
})
it('shows a minimal agent the global layer but no loader tool', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId(`preset-skills-minimal-${randomUUID()}`),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
try {
// Layer visibility is the registry's; whether an agent can USE skills
// stays the preset's choice — minimal mounts no `tool-skill`, so its
// tool table has no loader even though the global layer is readable.
expect((await ctx.skills.list({ scope: handle.agent })).map(skill => skill.name)).toContain('dsh-badge')
expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'read_image', 'working_directory'])
} finally {
await handle.dispose()
}
})
it('never rewrites the shipped profile patch when an Agent is disposed', async () => {
// The Loader persists a tree whose plugin self-disposed, and tearing an
// agent down disposes its whole subtree. Inherited, that rewrote the
// shipped composition — truncating it to `[]` the first time a session
// ended — so `PresetTree` refuses to write at all.
const before = await Promise.all(WEB_PATCHES.map(file => readFile(file, 'utf8')))
const handle = await ctx.agents.create({
sessionId: SessionId('preset-readonly'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
await handle.dispose()
// Slack, not a race the number has to win. The write is driven by the
// Loader's fiber-unload listener, which fires as the subtree's fibers
// settle rather than when `dispose()` resolves, and the Loader exposes no
// flush to await. A regression writes synchronously inside that listener,
// so any wait past settlement fails; a longer one only slows the test.
await new Promise(resolve => setTimeout(resolve, 50))
expect(await Promise.all(WEB_PATCHES.map(file => readFile(file, 'utf8')))).toEqual(before)
})
})
describe.each(['web', 'desktop'] as const)('%s native subagent bundles', (surface) => {
const products = ['codex', 'claude-code'] as const
type Product = typeof products[number]
const packageDir = (product: Product): string => (product === 'codex' ? CODEX_PACKAGE_DIR : CLAUDE_CODE_PACKAGE_DIR)
const toolName = (product: Product): string => `subagent_${product.replaceAll('-', '_')}`
async function bootSelected(installed: readonly Product[], extra: PatchOptions[] = []): Promise<Context> {
const root = await mkdtemp(join(tmpdir(), `dsh-${surface}-native-bundles-`))
onTestFinished(async () => { await rm(root, { recursive: true, force: true }) })
return await bootWeb(root, extra, installed.map(packageDir), [
'@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', ...installed.map(product => `@deepseek-ai/dsh-subagent-${product}`),
], surface)
}
async function presetTools(productCtx: Context, preset: string): Promise<string[]> {
const handle = await productCtx.agents.create({
sessionId: SessionId(`native-${surface}-${preset}-${randomUUID()}`),
setup: owner => productCtx.agentPresets.mount(owner, preset).then(() => undefined),
})
try {
return toolNames(productCtx, handle.agent).filter(name => /(?:subagent|custom)_(?:codex|claude_code)$/.test(name))
} finally { await handle.dispose() }
}
it.each([[[]], [['codex']], [['claude-code']], [['codex', 'claude-code']]] as Array<[Product[]]>)(
'adds the selected %j delegation tools to every preset as global tools', async (installed) => {
const productCtx = await bootSelected(installed)
const spawn = vi.spyOn(productCtx.subprocess, 'spawn')
try {
expect(productCtx.subagents.list().filter(name => (products as readonly string[]).includes(name)).sort())
.toEqual([...installed].sort())
const expected = installed.map(toolName).sort()
for (const preset of ['standard', 'cordis', 'ptc', 'minimal']) {
expect(await presetTools(productCtx, preset)).toEqual(expected)
}
const handle = await productCtx.agents.create({
sessionId: SessionId(`native-${surface}-params-${randomUUID()}`),
setup: owner => productCtx.agentPresets.mount(owner, 'standard').then(() => undefined),
})
try {
for (const product of installed) {
expect(toolParameterNames(productCtx, handle.agent, toolName(product))).toEqual(['cwd', 'description', 'prompt'])
}
} finally { await handle.dispose() }
expect(spawn).not.toHaveBeenCalled()
} finally {
spawn.mockRestore()
await productCtx.fiber.dispose()
}
}, 120_000)
it('keeps provider identities and applies later Host row overrides', async () => {
const providerConfig = { model: 'adoption-model', env: { DSH_NATIVE_ADOPTION: 'preserved' } }
const productCtx = await bootSelected(products, [
...products.map(product => ({ id: `subagent-${product}`, config: providerConfig })),
{ id: 'tool-subagent-codex', disabled: true },
{ id: 'tool-subagent-claude-code', config: {
provider: 'claude-code', toolName: 'custom_claude_code', maxDepth: 'provider-managed',
} },
])
try {
for (const product of products) {
expect([...productCtx.loader.entries()].find(entry => entry.options.id === `subagent-${product}`)?.options.config)
.toMatchObject(providerConfig)
}
for (const preset of ['standard', 'ptc', 'minimal']) {
expect(await presetTools(productCtx, preset)).toEqual(['custom_claude_code'])
}
} finally { await productCtx.fiber.dispose() }
}, 120_000)
})
describe('a user preset declared from the shipped cordis rows', () => {
/** The JSON text a `cordis_inspect_*` result renders. */
function resultText(result: { content: ReadonlyArray<{ type: string; text?: string }> }): string {
return result.content.filter(part => part.type === 'text').map(part => part.text ?? '').join('')
}
it('mounts beside the shipped `cordis` preset and reads the shared Host inspect providers', async () => {
// The Host inspect providers are one process-global set registered by the
// host composition (`@deepseek-ai/dsh-tool-cordis/host`); each preset's
// `tool-cordis` row only registers the tools. Before that split the copy
// failed to mount: its row re-registered provider "Service".
const root = await mkdtemp(join(tmpdir(), 'dsh-copied-preset-'))
const cordis = composeEntries([webPatches('test')]).find(row => row.id === 'preset-cordis')!.config as import('@deepseek-ai/dsh-agent-preset-registry').PresetDefinition
const copyCtx = await bootWeb(root, [{ insert: [{
id: 'preset-cordis-copy', name: '@deepseek-ai/dsh-agent-preset',
config: { ...cordis, id: 'cordis-copy', name: 'Cordis copy' },
}] }])
try {
const shipped = await copyCtx.agents.create({
sessionId: SessionId(`preset-cordis-shipped-${randomUUID()}`),
setup: agentCtx => copyCtx.agentPresets.mount(agentCtx, 'cordis').then(() => undefined),
})
const copied = await copyCtx.agents.create({
sessionId: SessionId(`preset-cordis-copy-${randomUUID()}`),
setup: agentCtx => copyCtx.agentPresets.mount(agentCtx, 'cordis-copy').then(() => undefined),
})
try {
for (const handle of [shipped, copied]) {
expect(toolNames(copyCtx, handle.agent)).toEqual(expect.arrayContaining([
'cordis_inspect_list', 'cordis_inspect_query', 'plugin_manager',
]))
}
const signal = new AbortController().signal
const listed = await executeToolFixture(copyCtx, {
callId: ToolCallId('copied-preset-inspect-list'),
name: 'cordis_inspect_list',
arguments: {},
signal,
agent: copied.agent,
})
expect(listed.isError).toBe(false)
const providers = (JSON.parse(resultText(listed)) as { providers: Array<{ id: string; platform: string }> }).providers
expect(providers.filter(provider => provider.platform === 'host').map(provider => provider.id))
.toEqual(['Service', 'Event', 'Config', 'Tool'])
// The `Tool` provider is the one built over the host context: it
// answers with the shared registry's view of the REQUESTING agent,
// so the copy sees its own preset's tools, not the host's empty set.
const queried = await executeToolFixture(copyCtx, {
callId: ToolCallId('copied-preset-inspect-tools'),
name: 'cordis_inspect_query',
arguments: { platform: 'host', provider: 'Tool', method: 'listTools' },
signal,
agent: copied.agent,
})
expect(queried.isError).toBe(false)
// The whole-table answer can pass the inline token budget once the
// preset's tool table grows, and the spill policy then replaces its
// middle with a gap plus a spill footer. What this case proves is the
// provider's answer, so assert the names it must carry.
const queriedText = resultText(queried)
for (const toolName of ['bash', 'cordis_inspect_list', 'cordis_inspect_query', 'plugin_manager']) {
expect(queriedText).toContain(`"name": "${toolName}"`)
}
// The `Config` provider reads the booted profile tree: the Agent loop declares a Config,
// and the bootstrap include row is a carrier. The name filter keeps each page small.
type ConfigRow = { id: string; patchId: string; name: string; status: string }
type ConfigPage = { data: { entries: ConfigRow[]; total: number; nextOffset: number | null } }
const listConfigs = async (input: Record<string, string | number>, callId: string): Promise<ConfigPage['data']> => {
const page = await executeToolFixture(copyCtx, {
callId: ToolCallId(callId),
name: 'cordis_inspect_query',
arguments: { platform: 'host', provider: 'Config', method: 'listConfigs', input },
signal,
agent: copied.agent,
})
expect(page.isError, resultText(page)).toBe(false)
return (JSON.parse(resultText(page)) as ConfigPage).data
}
const toolsRows = await listConfigs({ name: '@deepseek-ai/dsh-agent-loop' }, 'copied-preset-inspect-configs')
const toolsRow = toolsRows.entries[0]
expect(toolsRow).toMatchObject({ patchId: 'agent-loop', status: 'schema' })
expect(toolsRows).toMatchObject({ total: 1, nextOffset: null })
const includes = await listConfigs({ name: 'cordis:include' }, 'copied-preset-inspect-includes')
expect(includes.entries.find(entry => entry.patchId === 'include')).toMatchObject({ status: 'tree' })
const firstPage = await listConfigs({ limit: 5 }, 'copied-preset-inspect-page')
expect(firstPage.entries).toHaveLength(5)
expect(firstPage.nextOffset).toBe(5)
const projected = await executeToolFixture(copyCtx, {
callId: ToolCallId('copied-preset-inspect-config'),
name: 'cordis_inspect_query',
arguments: { platform: 'host', provider: 'Config', method: 'listConfigs', input: { entry: toolsRow!.id } },
signal,
agent: copied.agent,
})
expect(projected.isError).toBe(false)
type Projected = { status: string; acceptsMissing: unknown; schema: { $defs: Record<string, unknown> } }
const { data } = JSON.parse(resultText(projected)) as { data: Projected }
expect(data.status).toBe('schema')
expect(data.schema.$defs).toHaveProperty('loaderExpression')
expect(JSON.stringify(data.schema)).toContain('"maxParallelToolCalls"')
} finally {
await copied.dispose()
await shipped.dispose()
}
} finally {
await copyCtx.fiber.dispose()
await rm(root, { recursive: true, force: true })
}
}, 120_000)
})
describe('a switch survives the session', () => {
it('records the choice so the log states what the agent runs', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('preset-switch-logged'),
meta: { agentPreset: 'standard' },
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
try {
// The api-proxy's select does exactly this pair while the session is blank.
expect(ctx.commands.find(handle.agent, 'goal')).toBeDefined()
await ctx.agentPresets.recompose(handle.agent.ctx, 'minimal')
handle.agent.session.append('agent-preset/selected', { agentPreset: 'minimal' })
expect(ctx.commands.find(handle.agent, 'goal')).toBeUndefined()
// The header keeps the creation fact; the log carries what it runs.
expect(handle.agent.session.header.agentPreset).toBe('standard')
expect(ctx.sessionProjections.checkpoint(handle.agent.session).agentPreset?.val).toBe('minimal')
} finally {
await handle.dispose()
}
})
})
describe('a forked session', () => {
it('inherits the composition its seeded history was produced under', async () => {
const parent = await ctx.agents.create({
sessionId: SessionId('preset-fork-parent'),
meta: { agentPreset: 'minimal' },
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
const projection = await ctx.sessionProjections.register(agentPresetProjectionDefinition)
const inherited = projection.stateOf(parent.agent.session) ?? undefined
const child = await ctx.agents.create({
sessionId: SessionId('preset-fork-child'),
seed: [],
inheritedEventCount: SessionLogOffset(0),
meta: {
parentSession: SessionId('preset-fork-parent'),
isSeeded: true,
...inherited === undefined ? {} : { agentPreset: inherited },
},
setup: agentCtx => ctx.agentPresets.mount(agentCtx, inherited).then(() => undefined),
})
try {
// Composing nothing would leave the child empty: this layer moved every
// model-facing row out of the host plane, so there is nothing to inherit
// for free any more.
expect(toolNames(ctx, child.agent)).toEqual(toolNames(ctx, parent.agent))
expect(toolNames(ctx, child.agent).length).toBeGreaterThan(0)
} finally {
await child.dispose()
await parent.dispose()
}
})
})
describe('a delegated child', () => {
it.each([false, true])('inherits preset tools with seeded=%s and refuses child reminders', async (seeded) => {
const parent = await ctx.agents.create({
sessionId: SessionId(`preset-child-parent-${seeded}`),
meta: { agentPreset: 'standard' },
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
// Exactly what an in-process subagent driver's creation window does.
const child = await parent.agent.ctx.agents.create({
sessionId: SessionId(`preset-child-${seeded}`),
meta: childSessionMeta(parent.agent, 1, seeded),
...(seeded ? { seed: [], inheritedEventCount: SessionLogOffset(0) } : {}),
setup: (agentCtx) => {
return applyChildComposition(agentCtx, parent.agent, {})
},
})
try {
expect(toolNames(ctx, child.agent)).toEqual(toolNames(ctx, parent.agent))
// The shipped `standard` preset is the whole coding agent; an empty
// child here is the defect, and equality alone would not catch it.
expect(toolNames(ctx, child.agent)).toContain('bash')
expect(ctx.tools.schemas(child.agent)).toEqual(ctx.tools.schemas(parent.agent))
const calls = [
['schedule_create', { prompt: 'Check', after_seconds: 60, title: 'Check' }],
['schedule_list', {}],
['schedule_delete', { id: 'task-1' }],
['schedule_update', { id: 'task-1', title: 'Check' }],
] as const
for (const [name, args] of calls) {
expect(toolNames(ctx, child.agent)).toContain(name)
const result = await executeToolFixture(ctx, {
callId: ToolCallId(`child-${name}`), signal: new AbortController().signal,
name, arguments: args, agent: child.agent,
})
expect(result.value).toEqual({ code: 'subagent_session', message: 'A delegated subagent cannot use reminders.' })
}
expect(child.agent.session.header.agentPreset).toBe('standard')
} finally {
await child.dispose()
await parent.dispose()
}
})
it('follows a parent that switched preset while blank', async () => {
const parent = await ctx.agents.create({
sessionId: SessionId('preset-child-switch-parent'),
meta: { agentPreset: 'standard' },
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
await ctx.agentPresets.recompose(parent.agent.ctx, 'minimal')
const child = await parent.agent.ctx.agents.create({
sessionId: SessionId('preset-child-switch'),
meta: childSessionMeta(parent.agent, 1, false),
setup: (agentCtx) => {
return applyChildComposition(agentCtx, parent.agent, {})
},
})
try {
// The live scope chain is the authority, not the parent's creation
// header — which still names `standard`.
expect(toolNames(ctx, child.agent)).toEqual(toolNames(ctx, parent.agent))
expect(child.agent.session.header.agentPreset).toBe('minimal')
} finally {
await child.dispose()
await parent.dispose()
}
})
})
describe('the default preset as a user setting', () => {
it('composes an unnamed session from the stored default, not the composed one', async () => {
expect(ctx.agentPresets.defaultId).toBe('standard')
await ctx.settings.update(SETTINGS_NAMESPACE, { selectedDefault: 'minimal' })
try {
expect(ctx.agentPresets.defaultId).toBe('minimal')
const handle = await ctx.agents.create({
sessionId: SessionId('preset-user-default'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined),
})
try {
// An omitted preset id resolves the stored default instead of the composed default.
expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'read_image', 'working_directory'])
} finally {
await handle.dispose()
}
} finally {
// The context is shared with the rest of the file. `replace({})` drops
// the user section wholesale so the field re-inherits the composition
// base; `update` merges, and would leave the override standing.
await ctx.settings.replace(SETTINGS_NAMESPACE, {})
}
expect(ctx.agentPresets.defaultId).toBe('standard')
})
})
describe('a profile patch stored before Developer tools owned preset selection', () => {
let legacy: Context
let legacyHome: string
beforeAll(async () => {
legacyHome = await mkdtemp(join(tmpdir(), 'dsh-web-presets-legacy-'))
// A stored configuration from before the switch moved to Developer tools:
// it carries the retired key beside the default the user had saved. The
// Loader resolves the declared fields and leaves the extra one alone.
legacy = await bootWeb(legacyHome, [{
id: SETTINGS_NAMESPACE,
config: { default: 'standard', selectedDefault: 'minimal', modeSelectionEnabled: false },
}])
}, 120_000)
afterAll(async () => {
await legacy?.fiber.dispose()
await rm(legacyHome, { recursive: true, force: true })
})
it('starts, keeps the retired key inert and composes new sessions from the saved default', async () => {
expect(legacy.agentPresets.defaultId).toBe('minimal')
expect((await legacy.agentPresets.remoteExportList()).presets.find(row => row.id === 'minimal')?.isDefault).toBe(true)
// Settings projects the declared fields, so the retired key is neither
// shown nor rewritten; the user's saved default is.
expect(legacy.settings.describe().find(row => row.ns === SETTINGS_NAMESPACE)?.value)
.toEqual({ selectedDefault: 'minimal' })
const handle = await legacy.agents.create({
sessionId: SessionId('preset-legacy-patch'),
setup: agentCtx => legacy.agentPresets.mount(agentCtx).then(() => undefined),
})
try {
expect(toolNames(legacy, handle.agent)).toEqual(['bash', 'read_image', 'working_directory'])
} finally {
await handle.dispose()
}
})
})
describe('a session keeps the preset it was created with', () => {
it('refuses to adopt a live session under a different preset', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('preset-locked'),
meta: { agentPreset: 'minimal' },
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
try {
// The api-proxy guard reads exactly this: the header records what the
// session runs, so naming anything else is a caller error rather than a
// switch.
expect(handle.agent.session.header.agentPreset).toBe('minimal')
} finally {
await handle.dispose()
}
})
})
afterAll(async () => { await ctx?.fiber.dispose() })