Files
deepseek-harness/packages/subagent/subagent-inprocess/tests/preset-inheritance.spec.ts
imccyu ec601ca13d build(vendor): rescope the vendored Cordis packages into @deepseek-ai
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.

Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.

The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.

Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
2026-08-10 22:04:13 +08:00

136 lines
5.4 KiB
TypeScript

/**
* Composition inheritance: a child runs on the preset its parent runs on.
*
* With every model-facing row on the agent plane, the tool registry's global
* layer is empty, so a child that joins no preset reaches the model with no
* tools at all. These assert the model-visible result — the schemas in the
* child's own request — rather than the join that produces it.
*/
import { afterEach, describe, expect, it } from 'vitest'
import { dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import AgentPresets from '@deepseek-ai/dsh-agent-presets'
import { SessionId } from '@deepseek-ai/dsh-session'
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { startInProcessRun } from '../src/index.ts'
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures')
const ROOTS = [{ path: join(FIXTURES, 'presets'), trust: 'system' as const }]
const contexts: Context[] = []
afterEach(async () => {
for (const ctx of contexts.splice(0).reverse()) await ctx.fiber.dispose()
})
/** A host composition carrying no model-facing rows, plus the preset roster. */
async function setupPresetHost(): Promise<{ ctx: Context; adapter: MockAdapter; parent: Agent }> {
const ctx = new Context()
contexts.push(ctx)
ctx.baseUrl = pathToFileURL(FIXTURES).href + '/'
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
await mountAgentLoopTestDependencies(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(AgentPresets, { default: 'coding', roots: ROOTS })
const adapter = new MockAdapter([textResponse('parent idle'), textResponse('child done')])
ctx.llm.registerAdapter(['mock'], adapter)
const handle = await ctx.agents.create({
sessionId: SessionId('parent'),
agentOptions: { provider: 'mock', model: 'mock' },
setup: async (agentCtx: Context) => void await ctx.agentPresets.mount(agentCtx, 'coding'),
})
return { ctx, adapter, parent: handle.agent }
}
/** The one-shot spawn request shape both in-process providers build. */
function spawnRequest(parent: Agent) {
return {
label: 'child task',
prompt: [{ type: 'text' as const, text: 'child task' }],
parent,
signal: new AbortController().signal,
descriptor: snapshotSubagentDescriptor({
mode: 'one-shot' as const,
provider: 'spawn',
label: 'child task',
}),
}
}
describe('a child agent composed in-process', () => {
it('reaches the model with its parent\'s preset tools', async () => {
const { ctx, adapter, parent } = await setupPresetHost()
const run = await startInProcessRun(spawnRequest(parent), {})
await run.result
const childRequest = adapter.requests.at(-1)
expect(childRequest?.tools?.map(tool => tool.name)).toEqual(['preset_only'])
expect(ctx.tools.schemas(run.localAgent).map(schema => schema.name)).toEqual(['preset_only'])
await run.dispose()
})
it('carries its parent\'s prompt sections', async () => {
const { parent } = await setupPresetHost()
const run = await startInProcessRun(spawnRequest(parent), {})
await run.result
expect(run.localAgent?.session.events.some(event =>
event.type === 'request/header'
&& JSON.stringify(event.data).includes('section for preset_only'))).toBe(true)
await run.dispose()
})
it('records the composition it ran under on the child header', async () => {
const { parent } = await setupPresetHost()
const run = await startInProcessRun(spawnRequest(parent), {})
await run.result
// Without this the child's own history reads back under the deployment
// default, which is a different tool set than the one it actually used.
expect(run.localAgent?.session.header.agentPreset).toBe('coding')
await run.dispose()
})
it('honours a tool filter over the preset tools it inherited', async () => {
const { ctx, parent } = await setupPresetHost()
const run = await startInProcessRun(
{ ...spawnRequest(parent), toolFilter: { deny: ['preset_only'] } },
{},
)
await run.result
// The capability filter is the only thing bounding a delegated child, and
// every tool it can name now arrives from the preset rather than the host.
expect(ctx.tools.schemas(run.localAgent).map(schema => schema.name)).toEqual([])
await run.dispose()
})
it('follows a parent that switched preset while blank', async () => {
const { ctx, parent } = await setupPresetHost()
// A DIFFERENT preset, so the assertion below distinguishes reading the
// parent's live scope chain from reading its creation header — re-linking
// to the same id would pass either way.
await ctx.agentPresets.recompose(parent.ctx, 'reviewing')
const run = await startInProcessRun(spawnRequest(parent), {})
await run.result
expect(ctx.tools.schemas(run.localAgent).map(schema => schema.name)).toEqual(['reviewing_only'])
expect(run.localAgent?.session.header.agentPreset).toBe('reviewing')
await run.dispose()
})
})