summaryrefslogtreecommitdiffhomepage
path: root/src/features/chat/test-helpers.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-07 00:21:04 +0900
committerAdam Malczewski <[email protected]>2026-06-07 00:21:04 +0900
commit2faccf3482922385c2de47f210e48a8d2968762f (patch)
tree215c4d6e690bc542e24b8ede2b55ea7381c3998a /src/features/chat/test-helpers.ts
parent312ed5a9fe0d7f2660a66d61baf429b8e021ffb1 (diff)
downloaddispatch-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.ts80
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 [];
+ },
+ },
+ };
+}