Files
orca/src/shared/structured-agent-session-send-disposition.test.ts
T
Brennan BensonandMerge Sim f55b7ba680 fix(native-chat): cancel pending prompts precisely (#20601)
* fix(native-chat): hide activity while awaiting input

* fix(native-chat): keep approval turns cancellable

* test(native-chat): satisfy split PR quality gate

* fix(native-chat): catalog approval cancellation label

* fix(native-chat): include approval cancellation runtime label

* fix(codex): settle prompts when cancelled turns complete

* fix(codex): settle prompt registry fallbacks

* test(native-chat): cover pending interaction fallbacks

* test(native-chat): split prompt state coverage

* test(native-chat): keep prompt state isolated

* fix(native-chat): bound prompt turn backfill

* refactor(codex): centralize prompt registry bounds

* fix(native-chat): cancel pending prompts precisely

* fix(native-chat): consolidate capability imports

* fix(native-chat): harden precise prompt cancellation

* fix claude cancellation teardown races

* retry claude prompt lifecycle admission

* bound claude prompt cancellation retry work

* fix(codex): bound prompt turn identity on registration

* fix(native-chat): route rejected late dispatch settlements

* fix(codex): retain exact cancellable prompt turn ids

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-14 14:59:03 -07:00

154 lines
5.2 KiB
TypeScript

// What a rejection puts on the user's screen.
//
// The module is pure so this can be asserted directly instead of through the hook,
// which is the whole reason it was split out.
import { describe, expect, it } from 'vitest'
import type { AgentJournalSubmission } from './agent-session-journal-types'
import type { AgentSessionMutationResult, AgentSessionSendResult } from './agent-session-wire'
import {
dispatchWriteFailureReason,
DISPATCH_REJECTED_CANCELLED,
DISPATCH_REJECTED_QUEUE_FULL
} from './structured-agent-session-dispatch-rejection'
import { disposeStructuredAgentSessionSendResult } from './structured-agent-session-send-disposition'
import {
createStructuredAgentSessionOutboxEntry,
reconcileStructuredAgentSessionOutbox,
type StructuredAgentSessionOutboxEntry
} from './structured-agent-session-outbox'
const entry: StructuredAgentSessionOutboxEntry = createStructuredAgentSessionOutboxEntry({
clientMessageId: 'client-1',
sessionId: 'session-1',
text: 'hello',
attachments: [],
queuedAt: 1
})
function rejectedWith(reason: string | null): AgentSessionMutationResult<AgentSessionSendResult> {
const submission: AgentJournalSubmission = {
clientMessageId: 'client-1',
fence: 1,
payloadFingerprint: 'fingerprint',
dispatchState: 'rejected',
providerItemId: null,
reason,
submittedAt: 10,
resolvedAt: 10
}
return {
ok: true,
replayed: false,
fence: 1,
cursor: { epoch: 'epoch-1', sequence: 10 },
value: { clientMessageId: 'client-1', submission }
} as AgentSessionMutationResult<AgentSessionSendResult>
}
function notice(reason: string | null): string | null {
return disposeStructuredAgentSessionSendResult({
entries: [entry],
entry,
blockedClientMessageId: null,
result: rejectedWith(reason),
createOperationId: () => 'unused'
}).error
}
describe('what a rejection shows the user', () => {
it('removes a queued message the provider confirms Stop cancelled', () => {
const result = rejectedWith(DISPATCH_REJECTED_CANCELLED)
if (!result.ok) {
throw new Error('expected rejected submission fixture')
}
expect(reconcileStructuredAgentSessionOutbox([entry], [result.value.submission])).toEqual([])
})
it('never puts the transport marker on screen', () => {
const shown = notice(dispatchWriteFailureReason(new Error('broken pipe')))
// `provider_write_failed: broken pipe` names nothing a person can act on.
expect(shown).not.toContain('provider_write_failed')
expect(shown).not.toContain('broken pipe')
// And it says the message is safe to send again, which it is: the frame never left.
expect(shown).toBe(
"Couldn't reach the agent. Your message was not sent — Retry to send it again."
)
})
it('shows a content rejection in the provider own words', () => {
// The provider explaining itself IS the answer; a generic string throws it away.
expect(notice('Claude messages support at most 20 images')).toBe(
'Claude messages support at most 20 images'
)
})
it('claims no cause when the rejection names none', () => {
expect(notice(null)).toBe('Message was not sent.')
})
it('never puts a local-capacity marker on screen either', () => {
// Neither the provider's words nor a transport failure: a refusal we minted
// ourselves. It has no user-facing meaning, so it gets copy rather than the token.
const shown = notice(DISPATCH_REJECTED_QUEUE_FULL)
expect(shown).not.toContain('queue is full')
expect(shown).toBe('Orca could not send your message — Retry to send it again.')
})
})
describe('ambiguous operation refusals', () => {
it.each([
{ ...entry, state: 'unconfirmed' as const, lastAttemptAt: 10 },
{ ...entry, state: 'queued' as const, lastAttemptAt: 10, retryAfterUnknownSubmittedAt: 10 }
])('never rotates $state operation after its host tombstone expires', (ambiguous) => {
const disposition = disposeStructuredAgentSessionSendResult({
entries: [ambiguous],
entry: ambiguous,
blockedClientMessageId: null,
result: {
ok: false,
refusal: {
code: 'agent_session_operation_expired',
message: 'Operation expired.'
}
},
createOperationId: () => 'fresh-id'
})
expect(disposition.entries).toMatchObject([
{ clientMessageId: entry.clientMessageId, state: 'queued' }
])
expect(disposition.blockedClientMessageId).toBe(entry.clientMessageId)
})
it('parks a recovered missing submission without polling forever', () => {
const result = rejectedWith(null)
if (!result.ok) {
throw new Error('expected a send result')
}
result.value.submission = {
...result.value.submission,
dispatchState: 'unknown',
reason: 'durable_send_submission_missing',
recovered: true
}
const disposition = disposeStructuredAgentSessionSendResult({
entries: [entry],
entry,
blockedClientMessageId: null,
result,
createOperationId: () => 'unused'
})
expect(disposition.entries).toMatchObject([
{
clientMessageId: entry.clientMessageId,
state: 'unconfirmed',
retryAfterUnknownSubmittedAt: -1
}
])
})
})