fix(chat): keep messages chronological across ID rollover

This commit is contained in:
Bohdan Triapitsyn
2026-08-14 16:53:05 +03:00
parent 7cf869d5eb
commit fe1f6130d6
23 changed files with 405 additions and 132 deletions
+30 -5
View File
@@ -40,11 +40,11 @@ describe('shouldFetchSessionForRenderableSync', () => {
// already-committed messages (no re-render churn, no reference breaks).
// 3. mergeOptimisticPage + clearOptimistic is idempotent across commits.
function assistantMessage(id: string): Message {
return { id, sessionID: 'ses_1', role: 'assistant', time: { created: 1 } } as Message
function assistantMessage(id: string, created = 1): Message {
return { id, sessionID: 'ses_1', role: 'assistant', time: { created } } as Message
}
function userMessage(id: string): Message {
return { id, sessionID: 'ses_1', role: 'user', time: { created: 1 } } as Message
function userMessage(id: string, created = 1): Message {
return { id, sessionID: 'ses_1', role: 'user', time: { created } } as Message
}
function assistantMessageWithClientRole(id: string): Message {
// OpenCode sets clientRole on the wire; role may be absent.
@@ -79,11 +79,26 @@ describe('hasUserMessage', () => {
describe('incremental materialization of superset pages (#2084)', () => {
const SKIP_PARTS = new Set(['patch', 'step-start', 'step-finish'])
test('preserves authoritative part order across the part ID rollover', () => {
const msg = assistantMessage('msg_1')
const legacy = textPart('prt_ffffffffffffLegacy', msg.id)
const current = textPart('prt_000000000000Current', msg.id)
const result = materializeSessionSnapshots(
{ message: {}, part: {} },
'ses_1',
[{ info: msg, parts: [legacy, current] }],
{ skipPartTypes: SKIP_PARTS },
)
expect(result.part[msg.id]).toEqual([legacy, current])
})
test('preserves message references for already-committed messages', () => {
// Simulate expansion: commit 50 (assistant-only), then 100 (with user), then 150.
// Pages are supersets: the 100-page includes all 50 from the first page,
// the 150-page includes all 100 from the second.
// Messages are sorted by id in the store (mergeMessages uses cmp by id),
// Messages are chronological in the store,
// so look them up by id rather than positional index.
const a1 = assistantMessage('a_1')
const a2 = assistantMessage('a_2')
@@ -168,6 +183,16 @@ describe('incremental materialization of superset pages (#2084)', () => {
})
describe('mergeOptimisticPage idempotency across commits (#2084)', () => {
test('places a post-rollover optimistic message at the chronological tail', () => {
const legacy = userMessage('msg_ffffffffffffLegacy', 100)
const current = userMessage('msg_000000000000Current', 200)
const page = { session: [legacy], part: [], cursor: undefined, complete: true }
const merged = mergeOptimisticPage(page, [{ message: current, parts: [] }])
expect(merged.session).toEqual([legacy, current])
})
test('second call after clearOptimistic returns the page unchanged', () => {
// Simulate: first commit confirmed an optimistic item, clearOptimistic
// removed it; second commit (expansion) finds no optimistic items.