From e2ffae81315e1cedabd4a7a9384b836ed93d508a Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sun, 7 Jun 2026 02:06:55 +0900 Subject: Slice 3 wave A: tabs model, model selector, cache delete, localStorage - features/tabs: pure tab-workspace reducer (create/select/close/setModel/ setTitle/deriveTitle, draft=null active) + injected-persistence runes store - features/chat: mutable per-tab model (setModel) + delta routing guard (ignore foreign conversationId) + ModelSelector.svelte + DaisyUI chat bubbles / composer (keeps streaming
keying fix) - features/conversation-cache: surface delete(conversationId) on the wrapper for tab-close local-forget - adapters/local-storage: generic injected JSON localStore (quota/corrupt-safe) Verified: svelte-check 0/0, vitest 273, biome clean, build ok. --- src/features/tabs/tabs-store.test.ts | 157 +++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/features/tabs/tabs-store.test.ts (limited to 'src/features/tabs/tabs-store.test.ts') diff --git a/src/features/tabs/tabs-store.test.ts b/src/features/tabs/tabs-store.test.ts new file mode 100644 index 0000000..81ec8ad --- /dev/null +++ b/src/features/tabs/tabs-store.test.ts @@ -0,0 +1,157 @@ +import { describe, expect, it } from "vitest"; +import type { TabsState } from "./tabs"; +import type { TabsStorage } from "./tabs-store.svelte"; +import { createTabsStore } from "./tabs-store.svelte"; + +function createMemoryStorage(initial?: TabsState): TabsStorage & { data: TabsState | null } { + let data: TabsState | null = initial ?? null; + return { + get data() { + return data; + }, + set data(v: TabsState | null) { + data = v; + }, + load() { + return data; + }, + save(state: TabsState) { + data = state; + }, + }; +} + +describe("createTabsStore", () => { + it("loads persisted state on construct", () => { + const persisted: TabsState = { + tabs: [{ conversationId: "c1", model: "m1", title: "T1" }], + activeConversationId: "c1", + }; + const storage = createMemoryStorage(persisted); + const store = createTabsStore(storage); + + expect(store.tabs).toHaveLength(1); + expect(store.activeConversationId).toBe("c1"); + expect(store.activeTab?.conversationId).toBe("c1"); + }); + + it("starts with empty draft when no persisted state", () => { + const storage = createMemoryStorage(); + const store = createTabsStore(storage); + + expect(store.tabs).toHaveLength(0); + expect(store.activeConversationId).toBeNull(); + expect(store.activeTab).toBeNull(); + }); + + it("saves after every mutation", () => { + const storage = createMemoryStorage(); + const store = createTabsStore(storage); + + store.createTab({ conversationId: "c1", model: "m1", title: "T1" }); + expect(storage.data?.tabs).toHaveLength(1); + expect(storage.data?.activeConversationId).toBe("c1"); + + store.createTab({ conversationId: "c2", model: "m2", title: "T2" }); + expect(storage.data?.tabs).toHaveLength(2); + + store.selectTab("c1"); + expect(storage.data?.activeConversationId).toBe("c1"); + + store.closeTab("c1"); + expect(storage.data?.tabs).toHaveLength(1); + expect(storage.data?.activeConversationId).toBe("c2"); + + store.setModel("c2", "new-model"); + expect(storage.data?.tabs[0]?.model).toBe("new-model"); + + store.setTitle("c2", "New Title"); + expect(storage.data?.tabs[0]?.title).toBe("New Title"); + + store.newDraft(); + expect(storage.data?.activeConversationId).toBeNull(); + }); + + it("createTab appends and activates", () => { + const storage = createMemoryStorage(); + const store = createTabsStore(storage); + + store.createTab({ conversationId: "c1", model: "m1", title: "T1" }); + expect(store.tabs).toHaveLength(1); + expect(store.activeConversationId).toBe("c1"); + + store.createTab({ conversationId: "c2", model: "m2", title: "T2" }); + expect(store.tabs).toHaveLength(2); + expect(store.activeConversationId).toBe("c2"); + }); + + it("selectTab changes active", () => { + const storage = createMemoryStorage(); + const store = createTabsStore(storage); + + store.createTab({ conversationId: "c1", model: "m1", title: "T1" }); + store.createTab({ conversationId: "c2", model: "m2", title: "T2" }); + + store.selectTab("c1"); + expect(store.activeConversationId).toBe("c1"); + }); + + it("closeTab removes and activates neighbour", () => { + const storage = createMemoryStorage(); + const store = createTabsStore(storage); + + store.createTab({ conversationId: "c1", model: "m1", title: "T1" }); + store.createTab({ conversationId: "c2", model: "m2", title: "T2" }); + store.createTab({ conversationId: "c3", model: "m3", title: "T3" }); + + store.selectTab("c2"); + store.closeTab("c2"); + expect(store.tabs).toHaveLength(2); + expect(store.activeConversationId).toBe("c1"); + }); + + it("closing the last tab returns to draft", () => { + const storage = createMemoryStorage(); + const store = createTabsStore(storage); + + store.createTab({ conversationId: "c1", model: "m1", title: "T1" }); + store.closeTab("c1"); + expect(store.tabs).toHaveLength(0); + expect(store.activeConversationId).toBeNull(); + }); + + it("setModel updates the right tab", () => { + const storage = createMemoryStorage(); + const store = createTabsStore(storage); + + store.createTab({ conversationId: "c1", model: "old", title: "T1" }); + store.createTab({ conversationId: "c2", model: "m2", title: "T2" }); + + store.setModel("c1", "new-model"); + expect(store.tabs[0]?.model).toBe("new-model"); + expect(store.tabs[1]?.model).toBe("m2"); + }); + + it("setTitle updates the right tab", () => { + const storage = createMemoryStorage(); + const store = createTabsStore(storage); + + store.createTab({ conversationId: "c1", model: "m1", title: "Old" }); + + store.setTitle("c1", "New Title"); + expect(store.tabs[0]?.title).toBe("New Title"); + }); + + it("newDraft clears active but keeps tabs", () => { + const storage = createMemoryStorage(); + const store = createTabsStore(storage); + + store.createTab({ conversationId: "c1", model: "m1", title: "T1" }); + store.createTab({ conversationId: "c2", model: "m2", title: "T2" }); + + store.newDraft(); + expect(store.tabs).toHaveLength(2); + expect(store.activeConversationId).toBeNull(); + expect(store.activeTab).toBeNull(); + }); +}); -- cgit v1.2.3