From 2985bc5267f3bff9415988bc4e7e10c2d4541ac6 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Sun, 26 Jul 2026 19:13:47 +0800 Subject: [PATCH] test(goal-session): pin the unsettled-attempt yield with a real failure path The drive-pass guard for an unsettled attempt was wrongly annotated unreachable after admission joined the running interval. It is reachable: a contained turn-close failure (pre-commit turn/end rejection) reaches idle with the attempt's turn open and no terminal reason, and the idle drive pass must yield to that attempt instead of misreading the absent reason as settled. Replace the ignore with a test driving exactly that path; the guard's comment now names the real producer. --- packages/goal/goal-session/src/index.ts | 7 +++--- .../goal-session/tests/goal-session.spec.ts | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/goal/goal-session/src/index.ts b/packages/goal/goal-session/src/index.ts index 604b117aa3..002fd90a24 100644 --- a/packages/goal/goal-session/src/index.ts +++ b/packages/goal/goal-session/src/index.ts @@ -176,10 +176,9 @@ export function apply(ctx: Context): void { const attempt = state.attempt if (attempt !== undefined) { - // Prompt admission joined the loop's running interval, so a drive pass - // (which requires idle) can no longer observe an unsettled attempt; - // kept as a backstop against a future trigger inside the interval. - /* v8 ignore next -- unreachable yield, see above */ + // Still unsettled: a contained turn-close failure reaches idle with the + // attempt's turn open in the log and no terminal reason recorded, so + // the drive pass must yield rather than misread it as settled. if (attempt.reason === undefined) return state.attempt = undefined const turn = attempt.turn diff --git a/packages/goal/goal-session/tests/goal-session.spec.ts b/packages/goal/goal-session/tests/goal-session.spec.ts index 2b48be61d4..a7b3f59373 100644 --- a/packages/goal/goal-session/tests/goal-session.spec.ts +++ b/packages/goal/goal-session/tests/goal-session.spec.ts @@ -712,6 +712,31 @@ describe('same-session goal driving', () => { expect(test.adapter.requests).toHaveLength(1) }) + it('yields to a round whose turn/end never committed instead of misreading it as settled', async () => { + const test = await harness([textResponse('round ran')]) + // A persistent pre-commit turn/end rejection: the loop contains the close + // failure and reaches idle, but the round's attempt holds a turn with no + // terminal reason. The idle drive pass must yield to that unsettled + // attempt rather than classify an absent reason or crash into disarm. + test.ctx.on('internal/dispatch', (_mode, name, args) => { + if (name !== 'session/event') return + const event = args[1] as { type: string } + if (event.type === 'turn/end') throw new Error('turn close permanently rejected') + }) + test.ctx.goals.create(test.agent, { objective: 'survive a lost turn end' }) + await waitForRequests(test.adapter, 1) + await test.agent.whenIdle() + await new Promise((resolve) => { setImmediate(resolve) }) + + // One request ran; the unsettled attempt parked the driver without a + // second reservation and without disarming the goal. + expect(test.adapter.requests).toHaveLength(1) + expect(test.ctx.goals.get(test.agent)).toMatchObject({ + phase: 'active', + activation: 'armed', + }) + }) + it('disarms instead of continuing when a plugin reports a post-turn persistence failure', async () => { const test = await harness([textResponse('round one')]) test.ctx.on('session/event', (session, event) => {