test: restore 100% per-file coverage for agent-loop and the acp bridge

agent-loop: behavior tests for retry-while-busy, cancelled recovery
windows, no-facts stream failures, idle-listener preemption, rejected
driver promises under whenIdle, finish-chunk failures after step close,
presentationMeta persistence, pre-aborted and torn-down create/resume
signals, and configured-start failures over existing artifacts or after
teardown. The remaining guards that no public path can reach carry
justified v8 ignore annotations naming the invariant that starves them.

acp bridge: cover the retry-adoption path (a retry turn resolves the
prompt the failed turn deferred), the no-retry quiescence rejection, and
the admission-blocked cancelled settlement; the synchronous send-throw
catch is annotated as a future-proofing guard since the machine's send()
contains listener failures.
This commit is contained in:
_Kerman
2026-07-26 14:57:24 +08:00
parent fc8d339bf0
commit 6a068ec28d
6 changed files with 216 additions and 3 deletions

View File

@@ -498,3 +498,48 @@ describe('unrenderable failure settlement', () => {
}
})
})
describe('driver bookkeeping edges', () => {
it('a whenIdle waiter survives a rejected driver promise', async () => {
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('waiter-chain'), { provider: 'mock', model: 'mock' })
// A persistent step/end veto escapes even the catch block's own close
// attempt, so the driver promise REJECTS; the waiter's catch arm must
// treat that rejection as quiescence instead of propagating it.
ctx.on('internal/dispatch', (_mode, name, args) => {
if (name !== 'session/event') return
const event = args[1] as SessionEvent
if (event.type === 'step/end') throw new Error('step close permanently rejected')
})
send(agent, 'one')
// Entered while the run owns the abort slot, the waiter awaits the
// driver promise; its rejection must count as quiescence and resolve.
await expect(agent.whenIdle()).resolves.toBeUndefined()
})
it('a request failure that concludes recovery after step/end closed keeps the boundary balanced', async () => {
const { LlmError } = await import('@deepseek-ai/dsh-llm')
// The failure finish-chunk path returns request-failed AFTER step() has
// already appended step/end, so the request-failed branch's own
// step-close guard must see stepOpen === false and skip the append.
const adapter = new MockAdapter([
[
{ type: 'usage' as const, usage: { inputTokens: 1, outputTokens: 0 } },
{ type: 'finish' as const, reason: { kind: 'error' as const, failure: { message: 'empty', code: 'EMPTY_RESPONSE' } } },
] satisfies StreamChunk[],
])
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('finish-after-close'), { provider: 'mock', model: 'mock' })
void LlmError
send(agent, 'go')
await agent.whenIdle()
const types = agent.session.events.map(e => e.type)
expect(types.filter(t => t === 'step/end')).toHaveLength(1)
const end = agent.session.events.findLast(e => e.type === 'turn/end')
expect(end?.type === 'turn/end' && end.data.reason.kind).toBe('error')
})
})

View File

@@ -649,3 +649,110 @@ describe('creation and resume cancellation edges', () => {
await disposal
})
})
describe('configured-start failure edges', () => {
it('a non-Error mid-load abort reason is wrapped for the resume caller', async () => {
const sessionId = SessionId('resume-string-mid-abort')
const root = await persistSession(sessionId)
const ctx = await mountPersistentHarness(root, new MockAdapter([]))
const gate = Promise.withResolvers<never>()
gate.promise.catch(() => undefined)
const loadStarted = Promise.withResolvers<undefined>()
ctx.sessionPersistence.load = () => {
loadStarted.resolve(undefined)
return gate.promise
}
const controller = new AbortController()
const resuming = ctx.agents.resume({
resumeSessionId: sessionId,
agentOptions: { provider: 'mock', model: 'mock' },
signal: controller.signal,
})
await loadStarted.promise
controller.abort('operator string reason')
await expect(promptly(resuming)).rejects.toThrow(/creation aborted/)
expect(ctx.agents.get(sessionId)).toBeUndefined()
await ctx.fiber.dispose()
})
it('a failing exact-id restore over an existing artifact stays loud', async () => {
const sessionId = SessionId('config-existing-corrupt')
const root = await persistSession(sessionId)
const ctx = await mountPersistentHarness(root, new MockAdapter([]))
// The artifact exists (list reports it) but its load fails: this is
// corruption, not first creation — the failure must be reported, and no
// fresh same-id session may shadow the broken one.
ctx.sessionPersistence.load = () => Promise.reject(new Error('artifact corrupt'))
const configured = new Context()
await configured.plugin(LlmService)
await configured.plugin(SessionStore)
await configured.plugin(SystemPrompt)
await configured.plugin(ToolRegistry)
await configured.plugin(AgentRegistry)
await configured.plugin(SessionPersistenceJsonl, { root })
configured.llm.registerAdapter(['mock'], new MockAdapter([]))
configured.sessionPersistence.load = id => ctx.sessionPersistence.load(id)
const configFailures: unknown[] = []
configured.on('agent-loop/config-start-failed', (_id, error) => { configFailures.push(error) })
const configWarnings: string[] = []
const configWarn = configured.logger.warn.bind(configured.logger)
configured.logger.warn = ((...args: unknown[]) => {
if (typeof args[0] === 'string') configWarnings.push(args[0])
return (configWarn as (...a: unknown[]) => unknown)(...args)
}) as typeof configured.logger.warn
const loop = await configured.plugin(AgentLoop, {
agents: [{ id: 'main', sessionId, provider: 'mock', model: 'mock' }],
})
await expect.poll(() => configFailures.length).toBe(1)
expect(configFailures[0]).toBeInstanceOf(Error)
expect((configFailures[0] as Error).message).toBe('artifact corrupt')
expect(configWarnings.some(w => w.includes('config-driven restore'))).toBe(true)
expect(configured.agents.get(sessionId)).toBeUndefined()
await loop.dispose()
await configured.fiber.dispose()
await ctx.fiber.dispose()
})
it('suppresses a configured-resume failure that lands after teardown', async () => {
const sessionId = SessionId('config-late-resume-failure')
const root = await persistSession(sessionId)
const ctx = await mountPersistentHarness(root, new MockAdapter([]))
const gate = Promise.withResolvers<never>()
gate.promise.catch(() => undefined)
const loadStarted = Promise.withResolvers<undefined>()
ctx.sessionPersistence.load = () => {
loadStarted.resolve(undefined)
return gate.promise
}
const failures: unknown[] = []
ctx.on('agent-loop/config-start-failed', (_id, error) => { failures.push(error) })
const configured = new Context()
await configured.plugin(LlmService)
await configured.plugin(SessionStore)
await configured.plugin(SystemPrompt)
await configured.plugin(ToolRegistry)
await configured.plugin(AgentRegistry)
await configured.plugin(SessionPersistenceJsonl, { root })
configured.llm.registerAdapter(['mock'], new MockAdapter([]))
configured.sessionPersistence.load = id => ctx.sessionPersistence.load(id)
configured.on('agent-loop/config-start-failed', (_id, error) => { failures.push(error) })
const loop = await configured.plugin(AgentLoop, {
agents: [{ id: 'main', resumeSessionId: sessionId, provider: 'mock', model: 'mock' }],
})
await loadStarted.promise
const disposal = loop.dispose()
gate.reject(new Error('late backend failure'))
await disposal
await new Promise(r => setTimeout(r, 20))
// Ownership deactivated before the failure landed: the report is dropped.
expect(failures).toEqual([])
await configured.fiber.dispose()
await ctx.fiber.dispose()
})
})