diff options
| author | Adam Malczewski <[email protected]> | 2026-06-12 19:00:29 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-12 19:00:29 +0900 |
| commit | d66585333ee5764700c67a81eaec015b0026f8f1 (patch) | |
| tree | 6e1ac455c2ecbf3c442fce9f73fdaed8fb71fade /src/features/chat/store.test.ts | |
| parent | 1764e3e5dff836255d121a933dd92542368346f9 (diff) | |
| download | dispatch-web-d66585333ee5764700c67a81eaec015b0026f8f1.tar.gz dispatch-web-d66585333ee5764700c67a81eaec015b0026f8f1.zip | |
feat(chat): consume CR-5 history windowing — server-windowed cold loads + show-earlier backfill
Re-pinned [email protected]>0.10.0 + [email protected]>0.6.1 (reply
frontend-history-windowing-handoff.md); re-mirrored both .dispatch references.
- HistorySync port gains optional { limit?, beforeSeq? } (CR-5 params); the
app's createHistorySync appends them to GET /conversations/:id.
- COLD-cache fresh load now fetches ?sinceSeq=0&limit=<floor(0.75xL)> — a huge
conversation no longer ships whole to show 192 chunks. A warm-cache tail sync
stays unwindowed (windowing a tail that outgrew the limit would leave a
silent seq gap behind the cache).
- hasEarlier now derives from the [email protected] CONTRACT (1-based gap-free seqs):
loaded window starting above seq 1 => older history exists — covering both
locally-trimmed AND server-windowed transcripts (the watermark stays as the
merge floor only).
- showEarlier(): local cache first; when the cache doesn't reach far enough
back, backfills the missing older run via ?beforeSeq=<oldestKnown>&limit=
and persists it (next page-in is local). latestSeq windowed-read caveat is
satisfied structurally (tail cursor derives from the cache's max seq).
- live-probe: +6 CR-5 checks (seq origin, newest-k ascending, short-chat
exactness, beforeSeq paging, 400 validation x2). NOT yet run live — backend
was down at commit time; run pending.
- backend-handoff.md: CR-5 RESOLVED, pins/mirrors current. 602 tests green x2.
Diffstat (limited to 'src/features/chat/store.test.ts')
| -rw-r--r-- | src/features/chat/store.test.ts | 75 |
1 files changed, 71 insertions, 4 deletions
diff --git a/src/features/chat/store.test.ts b/src/features/chat/store.test.ts index 5c798d6..3232009 100644 --- a/src/features/chat/store.test.ts +++ b/src/features/chat/store.test.ts @@ -1054,12 +1054,12 @@ describe("createChatStore", () => { store.dispose(); }); - it("chat limit: a cold cache (fresh browser) windows the full server history to 75%", async () => { + it("chat limit: a cold cache (fresh browser) asks the SERVER for the 75% window (CR-5 ?limit=)", async () => { const transport = createFakeTransport(); const historySync = createFakeHistorySync(); const metricsSync = createFakeMetricsSync(); const cache = createFakeCache(); - // Backend has no limit param yet (CR-5): sinceSeq=0 returns EVERYTHING. + // The server holds 500 chunks; the windowed fetch returns the newest 75. historySync.returnChunks = Array.from({ length: 500 }, (_, i) => makeStoredChunk(i + 1)); const store = createChatStore({ @@ -1073,12 +1073,77 @@ describe("createChatStore", () => { await store.load(); + // The cold-cache initial sync carried the window (`?sinceSeq=0&limit=75`). + expect(historySync.calls[0]?.sinceSeq).toBe(0); + expect(historySync.calls[0]?.window).toEqual({ limit: 75 }); + expect(store.chunks).toHaveLength(75); expect(store.chunks[0]?.seq).toBe(426); + // hasEarlier derives from the 1-based gap-free seq contract (426 > 1) — + // no local watermark was ever set. + expect(store.hasEarlier).toBe(true); + // Only the window was shipped + cached (the point of CR-5). + const cached = await cache.impl.load(CONV_ID); + expect(cached).toHaveLength(75); + + store.dispose(); + }); + + it("chat limit: a warm cache syncs the tail UNWINDOWED (no seq gap behind the cache)", async () => { + const transport = createFakeTransport(); + const historySync = createFakeHistorySync(); + const metricsSync = createFakeMetricsSync(); + const cache = createFakeCache(); + await cache.impl.commit(CONV_ID, [makeStoredChunk(1), makeStoredChunk(2)]); + historySync.returnChunks = [makeStoredChunk(3)]; + + const store = createChatStore({ + conversationId: CONV_ID, + transport: transport.impl, + historySync: historySync.impl, + metricsSync: metricsSync.impl, + cache: cache.impl, + chatLimit: 100, + }); + + await store.load(); + + expect(historySync.calls[0]?.sinceSeq).toBe(2); + expect(historySync.calls[0]?.window).toBeUndefined(); + + store.dispose(); + }); + + it("chat limit: showEarlier backfills from the server when the cache is too shallow (CR-5 ?beforeSeq=)", async () => { + const transport = createFakeTransport(); + const historySync = createFakeHistorySync(); + const metricsSync = createFakeMetricsSync(); + const cache = createFakeCache(); + historySync.returnChunks = Array.from({ length: 500 }, (_, i) => makeStoredChunk(i + 1)); + + const store = createChatStore({ + conversationId: CONV_ID, + transport: transport.impl, + historySync: historySync.impl, + metricsSync: metricsSync.impl, + cache: cache.impl, + chatLimit: 100, + }); + + await store.load(); // server-windowed: loaded + cached = 426..500 + expect(store.chunks[0]?.seq).toBe(426); + + await store.showEarlier(); + + // Nothing below 426 was cached → fetched the missing run from the server. + const backfill = historySync.calls[1]; + expect(backfill?.window).toEqual({ beforeSeq: 426, limit: 25 }); + expect(store.chunks).toHaveLength(100); + expect(store.chunks[0]?.seq).toBe(401); expect(store.hasEarlier).toBe(true); - // The full history is still CACHED locally (show-earlier pages from it). + // The backfilled run is persisted: the NEXT page-in is cache-local. const cached = await cache.impl.load(CONV_ID); - expect(cached).toHaveLength(500); + expect(cached).toHaveLength(100); store.dispose(); }); @@ -1109,6 +1174,8 @@ describe("createChatStore", () => { expect(store.chunks).toHaveLength(100); expect(store.chunks[0]?.seq).toBe(401); expect(store.hasEarlier).toBe(true); + // The cache reached deep enough — no server backfill was needed. + expect(historySync.calls).toHaveLength(1); store.dispose(); }); |
