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:
Tianyi Cui
2026-07-09 03:41:37 +08:00
parent cc24e79cd2
commit e7b712453a
10 changed files with 169 additions and 3 deletions

View File

@@ -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
}

View File

@@ -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 })

View File

@@ -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()
})
})

View File

@@ -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) })
})
})

View File

@@ -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)
})
})