test(agent-loop): follow the managed subagent activation API
Master removed `backgroundMode` and the `run_in_background` argument, so every delegation starts a managed activation. Mount the working-directory owner SubagentRuntime injects and the JSONL persistence root the spawn provider requires, and synchronize on the child's cancellation, which the parent teardown or the manager drain raises instead of a parent-only cause.
This commit is contained in:
1 parent
e2cc73552f
commit
a5f0419217
1 file changed
+17
-9
@@ -1,9 +1,13 @@
|
||||
import { setImmediate } from 'node:timers/promises'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import { ToolCallId } from '@deepseek-ai/dsh-llm'
|
||||
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
|
||||
import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
|
||||
@@ -65,31 +69,35 @@ it.each([0, 1, 2])('retains Agent resources until all work exits with %i failed
|
||||
expect(ctx.sessions.get(agent.session.id)).toBeUndefined()
|
||||
})
|
||||
|
||||
it.each(['loop', 'root'] as const)('unloads %s while a background one-shot child is stopping', async (owner) => {
|
||||
it.each(['loop', 'root'] as const)('unloads %s while a background child is stopping', async (owner) => {
|
||||
const ctx = new Context()
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-resource-lifecycle-'))
|
||||
onTestFinished(async () => {
|
||||
release.resolve(undefined)
|
||||
await ctx.fiber.dispose()
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
await mountAgentLoopTestDependencies(ctx)
|
||||
await mountAgentLoopTestDependencies(ctx, { workingDirectory: true })
|
||||
await ctx.plugin(JsonlSessionPersistence, { root })
|
||||
const loop = await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
await ctx.plugin(LocalJobRegistry)
|
||||
await ctx.plugin(SubagentSpawn)
|
||||
await ctx.plugin(ToolSubagent, { provider: 'spawn', backgroundMode: 'one-shot' })
|
||||
await ctx.plugin(ToolSubagent, { provider: 'spawn' })
|
||||
ctx.jobs.attachController('resource-lifecycle')
|
||||
const parentCancelObserved = Promise.withResolvers<undefined>()
|
||||
const parentId = SessionId('background-parent')
|
||||
const childCancelObserved = Promise.withResolvers<undefined>()
|
||||
ctx.on('agent/created', ({ agent }) => {
|
||||
const cancel = agent.cancel.bind(agent)
|
||||
vi.spyOn(agent, 'cancel').mockImplementation((cause, options) => {
|
||||
if (cause.kind === 'parent') parentCancelObserved.resolve(undefined)
|
||||
if (agent.id !== parentId) childCancelObserved.resolve(undefined)
|
||||
cancel(cause, options)
|
||||
})
|
||||
})
|
||||
ctx.llm.registerAdapter(['mock'], new MockAdapter([textResponse('unused')]))
|
||||
const { agent: parent } = await ctx.agents.create({
|
||||
sessionId: SessionId('background-parent'), agentOptions: { provider: 'mock', model: 'mock' },
|
||||
sessionId: parentId, agentOptions: { provider: 'mock', model: 'mock' },
|
||||
})
|
||||
const requestEntered = Promise.withResolvers<undefined>()
|
||||
ctx.on('agent/request', async (event, next) => {
|
||||
@@ -101,13 +109,13 @@ it.each(['loop', 'root'] as const)('unloads %s while a background one-shot child
|
||||
const started = await ctx.tools.execute({
|
||||
agent: parent, signal: new AbortController().signal,
|
||||
callId: ToolCallId('background-start'), name: 'subagent',
|
||||
arguments: { description: 'child', prompt: 'wait', run_in_background: true },
|
||||
arguments: { description: 'child', prompt: 'wait' },
|
||||
})
|
||||
expect(started.isError).toBe(false)
|
||||
await requestEntered.promise
|
||||
const disposal = owner === 'loop' ? loop.dispose() : ctx.fiber.dispose()
|
||||
// Job teardown aborts its controller while the child is still in agent/request.
|
||||
await parentCancelObserved.promise
|
||||
// Teardown cancels the in-flight child while it is still inside agent/request.
|
||||
await childCancelObserved.promise
|
||||
release.resolve(undefined)
|
||||
await disposal
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user