test: close the per-file coverage gaps for the scoping surface
Every subject-extractor row of the invariants carrier table is exercised with a matching and a foreign-keyed carrier; the HMR re-apply seed path (sessions of agents that predate the plugin are marked started) is pinned; the scoped tool-provider disposal, plural restrict() validation, singular scopeHost absentee, tool-subagent passthrough, stale-stage drop, and disposing-parent spawn (INACTIVE_EFFECT, no orphan) each gain their test. Two genuinely defensive branches carry justified v8-ignore markers.
This commit is contained in:
@@ -203,6 +203,8 @@ export function startInProcessRun(
|
||||
try {
|
||||
unlink = request.parent.ctx.effect(() => () => handle.dispose())
|
||||
} catch (error: unknown) {
|
||||
// Fire-and-forget: start() must rethrow synchronously; the child's
|
||||
// teardown (stop → unregister → detach) reaches quiescence on its own.
|
||||
void handle.dispose()
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -184,6 +184,9 @@ export function attachStructuredRuntime(childCtx: Context, schema: StructuredOut
|
||||
if (decision.kind === 'accept') captured = { value: staged.value }
|
||||
return decision
|
||||
} finally {
|
||||
/* v8 ignore next -- defensive false branch: a concurrent re-stage
|
||||
* would need a second capture call INSIDE the first's post-execute
|
||||
* chain */
|
||||
if (pending === staged) pending = undefined
|
||||
}
|
||||
}, { prepend: true })
|
||||
|
||||
@@ -489,4 +489,45 @@ describe('in-process structured output', () => {
|
||||
expect(result.isError).toBe(true)
|
||||
expect(result.error?.code).toBe('UNKNOWN_TOOL')
|
||||
})
|
||||
|
||||
it('drops a stale stage from a short-circuited chain: a later call never promotes it (call-keyed commit)', async () => {
|
||||
const { ctx, parent } = await setup([
|
||||
toolCallResponse('c1', STRUCTURED_OUTPUT_TOOL, { answer: 1 }),
|
||||
])
|
||||
const run = ctx.subagents.start('spawn', structuredRequest(parent))
|
||||
const child = ctx.agents.get(run.id)!
|
||||
// An OUTER post-execute listener (registered after attach, prepend ⇒
|
||||
// outermost) that BLOCKS the first capture WITHOUT delegating: the commit
|
||||
// listener never runs for c1, so its staged value would linger.
|
||||
let blocks = 1
|
||||
ctx.on('tools/post-execute', (exec, _result, next) => {
|
||||
if (exec.name === STRUCTURED_OUTPUT_TOOL && blocks > 0) {
|
||||
blocks -= 1
|
||||
return Promise.resolve({ kind: 'block' as const, feedback: [{ type: 'text' as const, text: 'rejected' }] })
|
||||
}
|
||||
return next()
|
||||
}, { prepend: true })
|
||||
const result = await run.result
|
||||
// The blocked capture must NOT surface as structured success…
|
||||
expect(result.stopReason).toBe('error')
|
||||
expect(result.structured).toBeUndefined()
|
||||
// …and a LATER invalid call (its own body staged nothing) must not
|
||||
// resurrect c1's orphaned value: drive the pipeline directly.
|
||||
const invalid = await ctx.tools.execute({
|
||||
callId: 'c2' as never,
|
||||
name: STRUCTURED_OUTPUT_TOOL,
|
||||
arguments: { answer: 'not-a-number' },
|
||||
agent: child,
|
||||
})
|
||||
expect(invalid.isError).toBe(true)
|
||||
// A fresh valid call still captures ITS OWN value.
|
||||
const valid = await ctx.tools.execute({
|
||||
callId: 'c3' as never,
|
||||
name: STRUCTURED_OUTPUT_TOOL,
|
||||
arguments: { answer: 9 },
|
||||
agent: child,
|
||||
})
|
||||
expect(valid.isError).toBeFalsy()
|
||||
await run.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import LlmService from '@deepseek-ai/dsh-llm'
|
||||
@@ -377,4 +377,23 @@ describe('dsh-subagent-spawn', () => {
|
||||
expect(ctx.agents.list().length).toBe(before)
|
||||
})
|
||||
})
|
||||
|
||||
it('spawning from a DISPOSING parent fails loud with no orphaned child (INACTIVE_EFFECT teaching error)', async () => {
|
||||
const { ctx } = await setup([])
|
||||
// A handle-owned parent we can dispose (config agents dispose with the loop fiber).
|
||||
const parentHandle = ctx.agents.create({
|
||||
agentId: AgentId('doomed-parent'),
|
||||
sessionId: SessionId('doomed-s'),
|
||||
agentOptions: { model: 'mock' },
|
||||
})
|
||||
await parentHandle.dispose()
|
||||
const before = ctx.agents.list().length
|
||||
expect(() => ctx.subagents.start('spawn', {
|
||||
prompt: [{ type: 'text', text: 'do X' }],
|
||||
parent: parentHandle.agent,
|
||||
})).toThrow(/inactive context/)
|
||||
// The freshly created child's disposal was initiated before the rethrow
|
||||
// (fire-and-forget — start() throws synchronously); quiescence follows.
|
||||
await vi.waitFor(() => { expect(ctx.agents.list().length).toBe(before) })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -451,4 +451,37 @@ describe('dsh-tool-subagent', () => {
|
||||
expect(typeof unwrapped.apply).toBe('function')
|
||||
expect(unwrapped.Config).toBeDefined()
|
||||
})
|
||||
|
||||
it('passes persona/toolFilter/maxDepth config through to the start request', async () => {
|
||||
let seen: { persona?: string; toolFilter?: unknown; maxDepth?: number } | undefined
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(SubagentService)
|
||||
ctx.subagents.registerProvider({
|
||||
name: 'capture2',
|
||||
capabilities: { outputSchema: false, depthLimit: true, toolFilter: true, persona: true },
|
||||
inheritsParentContext: false,
|
||||
start: (request) => {
|
||||
seen = request
|
||||
return {
|
||||
id: AgentId('capture2-child'),
|
||||
result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }),
|
||||
cancel() {},
|
||||
dispose: async () => {},
|
||||
}
|
||||
},
|
||||
})
|
||||
await ctx.plugin(tool, {
|
||||
provider: 'capture2',
|
||||
persona: 'You are the child.',
|
||||
toolFilter: { deny: ['subagent'] },
|
||||
maxDepth: 2,
|
||||
})
|
||||
|
||||
await callSubagent(ctx, { description: 'd', prompt: 'p' })
|
||||
expect(seen?.persona).toBe('You are the child.')
|
||||
expect(seen?.toolFilter).toMatchObject({ deny: ['subagent'] })
|
||||
expect(seen?.maxDepth).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user