diff --git a/src/main/codex/codex-structured-dispatch-boundary.test.ts b/src/main/codex/codex-structured-dispatch-boundary.test.ts index 7e92abc06318..8f2d5d566c14 100644 --- a/src/main/codex/codex-structured-dispatch-boundary.test.ts +++ b/src/main/codex/codex-structured-dispatch-boundary.test.ts @@ -62,8 +62,10 @@ it('stops a refused steer before its fallback write', async () => { } await expect(sending).rejects.toBe(reason) expect(connection.calls.some((call) => call.method === 'turn/start')).toBe(false) + // An exact echo is passed on; the journal keeps the stopped send rejected (see + // structured-agent-session-late-settlement.test.ts). echoUserMessage(connection, { turnId: 'running', itemId: 'stopped', clientId: 'stopped' }) - expect(settlements).toEqual([]) + expect(settlements.map((entry) => entry.clientMessageId)).toEqual(['stopped']) await adapter.closeAll() }) @@ -89,8 +91,9 @@ it('abandons only the stopped message while waiting for a previous turn to open' await expect(stopped).rejects.toBe(reason) expect(connection.calls.filter((call) => call.method === 'turn/start')).toHaveLength(1) startTurn(connection, 'opening') - echoUserMessage(connection, { turnId: 'opening', itemId: 'ignored', clientId: 'waiting' }) - expect(settlements).toEqual([]) + echoUserMessage(connection, { turnId: 'opening', itemId: 'first', clientId: 'first' }) + // The abandoned wait leaves the earlier send's receipt intact. + expect(settlements.map((entry) => entry.clientMessageId)).toEqual(['first']) await adapter.closeAll() }) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-late-settlement.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-late-settlement.test.ts index e6538318e2e4..9598a533fc0b 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-late-settlement.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-late-settlement.test.ts @@ -281,6 +281,55 @@ describe('settling a send the provider proves it received after the ack window', ]) }) + it('a receipt after Stop withdrew an unwritten send keeps it cancelled', async () => { + dispatch.mockImplementationOnce( + (input) => + new Promise((_resolve, reject) => { + input.signal?.addEventListener('abort', () => reject(input.signal?.reason), { + once: true + }) + }) + ) + const params = sendParams('stopped before its write') + await host.send(CALLER, params) + await handedOver(params.envelope.clientOperationId) + expect(dispatch.mock.calls[0]?.[0].signal).toBeDefined() + + const stopped = await host.cancel(CALLER, { + envelope: { + sessionId: SESSION, + clientOperationId: hostTestOperationId(), + expectedRuntimeFence: store.getRecord(SESSION)?.lease.runtimeFence ?? 1, + payloadFingerprint: computeAgentSessionPayloadFingerprint({ + method: 'agentSession.cancel', + sessionId: SESSION, + fields: {} + }) + } + }) + expect(stopped).toMatchObject({ ok: true }) + await vi.waitFor(async () => + expect(await submissions()).toMatchObject([{ dispatchState: 'rejected' }]) + ) + const before = journal().cursor() + await host.settleLateDispatch({ + sessionId: SESSION, + clientMessageId: params.envelope.clientOperationId, + providerIdentity: { provider: 'codex', threadId: THREAD, turnId: 'turn-1', ordinal: 1 } + }) + + const after = journal().readSince(before) + expect(after.ok && after.rows.filter((row) => row.kind === 'dispatch')).toEqual([]) + + expect(await submissions()).toMatchObject([ + { + clientMessageId: params.envelope.clientOperationId, + dispatchState: 'rejected', + rejection: { kind: 'cancelled' } + } + ]) + }) + it('accepts from the durable echo row when the direct settlement write fails', async () => { dispatch.mockResolvedValueOnce({ state: 'admitted' }) const params = sendParams('settle from provider echo')