diff options
| author | Adam Malczewski <[email protected]> | 2026-06-07 00:21:04 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-07 00:21:04 +0900 |
| commit | 2faccf3482922385c2de47f210e48a8d2968762f (patch) | |
| tree | 215c4d6e690bc542e24b8ede2b55ea7381c3998a /src/features/chat/test-helpers.ts | |
| parent | 312ed5a9fe0d7f2660a66d61baf429b8e021ffb1 (diff) | |
| download | dispatch-web-2faccf3482922385c2de47f210e48a8d2968762f.tar.gz dispatch-web-2faccf3482922385c2de47f210e48a8d2968762f.zip | |
Slice 2 wave 2: IndexedDB cache adapter + chat feature
- adapters/idb: createIdbChunkStore implements the ConversationChunkStore port
over IndexedDB (compound [conversationId,seq] key, idempotent append, meta
store for lastAccess); 8 tests with fake-indexeddb
- features/chat: createChatStore (runes-thin over the core/chunks reducer, all
effects injected via ChatTransport/HistorySync/ConversationCache ports) +
ChatView/Composer svelte-thin UI; folds chat.delta, syncs on turn-sealed,
hydrates from cache then catches up; 25 tests
Verified green: svelte-check 0/0, vitest 202, biome clean, build ok.
Diffstat (limited to 'src/features/chat/test-helpers.ts')
| -rw-r--r-- | src/features/chat/test-helpers.ts | 80 |
1 files changed, 80 insertions, 0 deletions
diff --git a/src/features/chat/test-helpers.ts b/src/features/chat/test-helpers.ts new file mode 100644 index 0000000..e58818a --- /dev/null +++ b/src/features/chat/test-helpers.ts @@ -0,0 +1,80 @@ +import type { StoredChunk } from "@dispatch/wire"; +import type { ConversationCache } from "../conversation-cache"; +import type { ChatTransport, HistorySync } from "./ports"; + +export interface FakeTransport { + readonly sent: import("@dispatch/transport-contract").ChatSendMessage[]; + readonly impl: ChatTransport; +} + +export function createFakeTransport(): FakeTransport { + const sent: import("@dispatch/transport-contract").ChatSendMessage[] = []; + return { + sent, + impl: { + send(msg) { + sent.push(msg); + }, + }, + }; +} + +export interface FakeHistorySync { + readonly calls: Array<{ conversationId: string; sinceSeq: number }>; + /** Set the chunks to return on the next call. */ + returnChunks: readonly StoredChunk[]; + readonly impl: HistorySync; +} + +export function createFakeHistorySync(): FakeHistorySync { + const calls: Array<{ conversationId: string; sinceSeq: number }> = []; + let returnChunks: readonly StoredChunk[] = []; + return { + calls, + get returnChunks() { + return returnChunks; + }, + set returnChunks(v: readonly StoredChunk[]) { + returnChunks = v; + }, + impl: async (conversationId, sinceSeq) => { + calls.push({ conversationId, sinceSeq }); + const chunks = returnChunks; + const latestSeq = chunks.length > 0 ? Math.max(...chunks.map((c) => c.seq)) : sinceSeq; + return { chunks, latestSeq }; + }, + }; +} + +export interface FakeCache { + readonly store: Map<string, StoredChunk[]>; + readonly impl: ConversationCache; +} + +export function createFakeCache(): FakeCache { + const store = new Map<string, StoredChunk[]>(); + return { + store, + impl: { + async load(conversationId) { + return store.get(conversationId) ?? []; + }, + async commit(conversationId, incoming) { + const existing = store.get(conversationId) ?? []; + const seen = new Set(existing.map((c) => c.seq)); + const toAppend = incoming.filter((c) => !seen.has(c.seq)); + const merged = [...existing, ...toAppend].sort((a, b) => a.seq - b.seq); + store.set(conversationId, merged); + return merged; + }, + async sinceSeq(conversationId) { + const chunks = store.get(conversationId) ?? []; + if (chunks.length === 0) return 0; + return Math.max(...chunks.map((c) => c.seq)); + }, + async evictIfOverBudget() { + return []; + }, + }, + }; +} |
