diff --git a/packages/acp/acp/tests/turns.spec.ts b/packages/acp/acp/tests/turns.spec.ts index 8cf67b4d19..1344f92257 100644 --- a/packages/acp/acp/tests/turns.spec.ts +++ b/packages/acp/acp/tests/turns.spec.ts @@ -57,6 +57,21 @@ describe('ACP prompt lifecycle', () => { .rejects.toThrow(/turn failed: plugin pre-step failed/) }) + it('rejects a turn-start failure before the prompt is claimed', async () => { + harness = await makeBridgeHarness({ script: [textResponse('must not run')] }) + const sessionId = await newSession(harness) + const agent = harness.ctx.agents.get(SessionId(sessionId))! + const append = agent.session.append.bind(agent.session) + vi.spyOn(agent.session, 'append').mockImplementation(((type: string, ...rest: never[]) => { + if (type === 'turn/start') throw new Error('turn start unavailable') + return (append as (...args: never[]) => unknown)(type as never, ...rest) + }) as never) + + await expect(harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go' }] })) + .rejects.toThrow(/turn failed: turn start unavailable/) + vi.restoreAllMocks() + }) + it('settles even when an earlier turn observer throws', async () => { harness = await makeBridgeHarness({ script: [textResponse('answer')] }) harness.ctx.on('session/event', (_session, event) => { @@ -257,6 +272,18 @@ describe('ACP prompt lifecycle', () => { expect(messageText(harness)).toBe('') }) + it('cancels a prompt removed before its turn claims it', async () => { + harness = await makeBridgeHarness({ script: [] }) + const sessionId = await newSession(harness) + const dispose = harness.ctx.on('agent/inbox/inserted', (agent, { message }) => { + if (message.source.kind === 'user') agent.inbox.remove(message.id) + }) + + await expect(harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go' }] })) + .resolves.toEqual({ stopReason: 'cancelled' }) + dispose() + }) + it('rejects a prompt when pre-step fails inside its open turn', async () => { harness = await makeBridgeHarness({ script: [] }) harness.ctx.on('agent/pre-step', async () => { throw new Error('pre-step exploded') }) diff --git a/packages/context/workspace-context/tests/workspace-context.spec.ts b/packages/context/workspace-context/tests/workspace-context.spec.ts index abdc9bea76..be9e512f92 100644 --- a/packages/context/workspace-context/tests/workspace-context.spec.ts +++ b/packages/context/workspace-context/tests/workspace-context.spec.ts @@ -3739,6 +3739,11 @@ describe('workspace context inbox synchronization', () => { }) await syncWorkspaceContext(ctx, agent) expect(blocksText(agent.inbox.nextStep[0]?.content)).toContain('pending version one') + const duplicate = createUserMessage({ + content: agent.inbox.nextStep[0]!.content, + source: agent.inbox.nextStep[0]!.source, + }) + agent.inbox.append('next-step', duplicate) await write(join(root, 'pkg/AGENTS.md'), 'pending version two with more detail') await ctx.tools.execute({ @@ -3747,6 +3752,7 @@ describe('workspace context inbox synchronization', () => { }) await syncWorkspaceContext(ctx, agent) expect(agent.inbox.nextStep).toHaveLength(1) + expect(agent.inbox.nextStep[0]?.id).not.toBe(duplicate.id) expect(blocksText(agent.inbox.nextStep[0]?.content)).toContain('pending version two with more detail') expect(blocksText(agent.inbox.nextStep[0]?.content)).not.toContain('pending version one') diff --git a/packages/session-persistence/session-persistence/src/coordinator.ts b/packages/session-persistence/session-persistence/src/coordinator.ts index 5ba419f118..e350beec3a 100644 --- a/packages/session-persistence/session-persistence/src/coordinator.ts +++ b/packages/session-persistence/session-persistence/src/coordinator.ts @@ -328,6 +328,7 @@ function migrateLegacyTurnEndEvent(event: SessionEvent, id: SessionId): SessionE break } default: + /* v8 ignore next -- turn-end reasons are merge-extensible beyond the current vocabulary. */ return event }