Files
orca/src/shared/structured-agent-session-send-settlement.test.ts
T
Brennan Benson 6440d3c520 fix(mobile): keep a not-sent message in place on the phone, using the desktop's logic (#27936)
* fix(native-chat): list not-sent rows at their journal places

The shared projection appended rows the host recorded but never delivered after every other row,
leaving the reader to sort them. A reader that draws the list as given showed an old not-sent
message under newer turns. Merge them in at their journal places, after the stop rows are built,
so the list is already in order. The desktop sorts by the same order, so what it draws is unchanged.

* refactor(native-chat): move the recorded-rejection line into shared words

The desktop's words for a message the host recorded and then rejected now live beside the other
rejection words, returning notice parts each client renders its own way, so the phone can say the
same thing. The desktop calls it with the same inputs; its words are unchanged.

* refactor(native-chat): one projection behaviour for rejected rows

Every reader now draws a message the host recorded and then rejected where it was sent, marked
not sent, so the rejectedInPlace option goes. The phone was its last off caller apart from the
host's conversation outline, which now leaves not-sent rows out itself; a test proves the outline
is the same as if the rejected message had never been sent.

* fix(mobile): show a not-sent message in place with the reason under it

The phone now draws a message the host recorded and then rejected where it was sent, as the desktop
does, with one small line under the bubble in the desktop's words. The line is built from the
journal the view already receives; when a start-failure or sign-in row already gives the reason,
it says only that the message was not sent. The load-earlier header moves to its own file to keep
the view under its line limit.

* fix(mobile): a send the host recorded is never handed back

When the send's own reply said the host recorded the message and then rejected it, the phone put
the text back in the composer with a banner, though the host and the desktop keep it in the chat.
A recorded send now settles like any other: its row draws it in place as not sent (or its kept
card holds it), and the composer stays empty. The banner's failure-fact plumbing, fed only by that
branch, goes with it; the transcript states the sign-in guidance once.

* fix(mobile): keep each structured send on the shared optimistic list

Once a not-sent row stays in the chat, matching a pending bubble by its text breaks: resending the
same words after a not-sent message never reached the count the bubble waited for, so a stray copy
stayed, and an acknowledgement lost on that resend showed a false "Delivery unconfirmed".

A structured send is now one record, kept by the structured session under the id it is sent with.
Three parts match the desktop's sender: the id, the drawing (the shared projection draws it at the
tail until the host's row under that id lands, in the same pass) and the settle rule (the host's own
record of it, a submission, a card, or a card's hand-off, drops it by the one rule the desktop now
shares). The rest stays the phone's own: the bubble shows once the host accepts the send, not from
the tap; a send whose answer was lost is held undrawn and says "Delivery unconfirmed" only if its
deadline passes first, instead of going back to the composer; records are kept whether or not a
chat view is open; and one send in flight is the composer's rule, not the sender's. Its photo is
kept under the row id it shares with the host's row, so it shows on the bubble, the landed row and
a not-sent row. The drafts' text matching, with its older queued-card matching, now serves only
terminal-backed chats, whose transcripts carry no ids.

* fix(native-chat): merge not-sent rows by the transcript order alone

The merge kept a rule for rows with no journal place that no caller reaches and that the desktop's
sort does not have; drop it, and say what merging after the stop rows actually keeps. A test pins
that the list the phone draws as given is already in the desktop's sorted order, with a moved stop
row, a retry run, a cut-turn notice, a held card and two not-sent rows.

* fix(mobile): draw the not-sent line as the desktop does

Red and capped to the bubble's width, like the desktop's line and the phone's own returned-card
caption, and only when there is a notice: the row had a second, unreachable source of words. A
send a Stop took back is no failure, so it no longer rebuilds the notices on every streaming frame.
2026-10-12 15:16:23 -07:00

30 lines
1.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { AgentJournalSubmission } from './agent-session-journal-types'
import { structuredAgentSessionSettledSends } from './structured-agent-session-send-settlement'
function submission(clientMessageId: string, queuedMessageId?: string): AgentJournalSubmission {
return {
clientMessageId,
...(queuedMessageId ? { queuedMessageId } : {}),
fence: 1,
payloadFingerprint: clientMessageId,
dispatchState: 'accepted',
providerItemId: null,
reason: null,
submittedAt: 1,
resolvedAt: 1
}
}
describe('structuredAgentSessionSettledSends', () => {
it('reads a send as a card while queued or once handed off, else as recorded under its id', () => {
const handOff = submission('hand-off', 'was-queued')
const sent = submission('sent')
const { cards, recorded } = structuredAgentSessionSettledSends([handOff, sent], ['on-queue'])
expect([...cards]).toEqual(['on-queue', 'was-queued'])
expect(recorded.get('sent')).toBe(sent)
expect(recorded.get('hand-off')).toBe(handOff)
expect(recorded.has('on-queue')).toBe(false)
})
})