diff --git a/sessions-utils.test.mjs b/sessions-utils.test.mjs new file mode 100644 index 0000000..cb05942 --- /dev/null +++ b/sessions-utils.test.mjs @@ -0,0 +1,62 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +// Keep in sync with src/utils/sessions.ts merge rules for regression coverage. +function pickNewerIsoTimestamp(left, right) { + const leftTime = Date.parse(left ?? ''); + const rightTime = Date.parse(right ?? ''); + const leftValid = Number.isFinite(leftTime); + const rightValid = Number.isFinite(rightTime); + if (!leftValid && !rightValid) return left ?? right; + if (!leftValid) return right; + if (!rightValid) return left; + return leftTime >= rightTime ? left : right; +} + +function mergeSessionRecord(existing, incoming) { + if (!existing) return incoming; + const merged = { ...existing, ...incoming, id: incoming.id }; + return { + ...merged, + updated_at: pickNewerIsoTimestamp(existing.updated_at, incoming.updated_at), + message_count: Math.max(Number(existing.message_count ?? 0), Number(incoming.message_count ?? 0)), + }; +} + +test('mergeSessionRecord keeps the newer updated_at when refreshing stale server summaries', () => { + const merged = mergeSessionRecord( + { + id: 'sess-1', + name: 'New Chat', + message_count: 4, + updated_at: '2026-09-02T14:48:00.000Z', + }, + { + id: 'sess-1', + name: 'New Chat', + message_count: 3, + updated_at: '2026-09-02T14:40:00.000Z', + }, + ); + assert.equal(merged.updated_at, '2026-09-02T14:48:00.000Z'); + assert.equal(merged.message_count, 4); +}); + +test('mergeSessionRecord adopts newer server timestamps when local cache is older', () => { + const merged = mergeSessionRecord( + { + id: 'sess-2', + name: 'New Chat', + message_count: 2, + updated_at: '2026-09-02T14:30:00.000Z', + }, + { + id: 'sess-2', + name: 'New Chat', + message_count: 5, + updated_at: '2026-09-02T14:48:00.000Z', + }, + ); + assert.equal(merged.updated_at, '2026-09-02T14:48:00.000Z'); + assert.equal(merged.message_count, 5); +}); diff --git a/src/hooks/useTKMindChat.ts b/src/hooks/useTKMindChat.ts index 356d636..5e9bf9b 100644 --- a/src/hooks/useTKMindChat.ts +++ b/src/hooks/useTKMindChat.ts @@ -88,6 +88,7 @@ import { appendSessionLists, prependUnique, shouldShowNewChatTitle, + sortAndTrim, toSessionSummary, touchSession, } from '../utils/sessions'; @@ -750,7 +751,9 @@ export function useTKMindChat( offset: 0, query: options?.query ?? sessionSearchQueryRef.current, }); - const merged = options?.preserveExisting ? appendSessionLists(sessionsRef.current, items) : items; + const merged = options?.preserveExisting + ? appendSessionLists(sessionsRef.current, items) + : sortAndTrim(items); const nextOffset = Number(page.offset ?? 0) + items.length; const total = Math.max(Number(page.total ?? merged.length), merged.length); sessionsOffsetRef.current = options?.preserveExisting diff --git a/src/utils/sessions.ts b/src/utils/sessions.ts index c340233..4cf4b9c 100644 --- a/src/utils/sessions.ts +++ b/src/utils/sessions.ts @@ -43,6 +43,17 @@ export function toSessionSummary(session: Session | SessionSummary): SessionSumm }; } +function pickNewerIsoTimestamp(left?: string, right?: string): string | undefined { + const leftTime = Date.parse(left ?? ''); + const rightTime = Date.parse(right ?? ''); + const leftValid = Number.isFinite(leftTime); + const rightValid = Number.isFinite(rightTime); + if (!leftValid && !rightValid) return left ?? right; + if (!leftValid) return right; + if (!rightValid) return left; + return leftTime >= rightTime ? left : right; +} + export function mergeSessionRecord( existing: SessionSummary | undefined, incoming: SessionSummary, @@ -51,15 +62,21 @@ export function mergeSessionRecord( const keepExistingTitle = hasMeaningfulSessionTitle(existing) && !hasMeaningfulSessionTitle(incoming); - if (!keepExistingTitle) return { ...existing, ...incoming, id: incoming.id }; + const merged = keepExistingTitle + ? { + ...existing, + ...incoming, + id: incoming.id, + name: existing.name, + user_set_name: existing.user_set_name, + recipe: existing.recipe ?? incoming.recipe, + } + : { ...existing, ...incoming, id: incoming.id }; return { - ...existing, - ...incoming, - id: incoming.id, - name: existing.name, - user_set_name: existing.user_set_name, - recipe: existing.recipe ?? incoming.recipe, + ...merged, + updated_at: pickNewerIsoTimestamp(existing.updated_at, incoming.updated_at), + message_count: Math.max(Number(existing.message_count ?? 0), Number(incoming.message_count ?? 0)), }; }