diff options
| author | Adam Malczewski <[email protected]> | 2026-06-25 10:55:51 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-25 10:55:51 +0900 |
| commit | e7d8cdbd68954034d2a4f6979a233b216212967b (patch) | |
| tree | 89450470d669358277e7fb122236e298a0f32059 /src/app/store.test.ts | |
| parent | cd7f6cdfbd62bf4cf13c953bdb79d02158b6019e (diff) | |
| download | dispatch-web-e7d8cdbd68954034d2a4f6979a233b216212967b.tar.gz dispatch-web-e7d8cdbd68954034d2a4f6979a233b216212967b.zip | |
feat: workspaces shell + cwd-lsp rename + mcp/settings/system-prompt features + app wiring
- workspaces: URL-driven conversation grouping (home listing at /, routing,
store, http adapter, WorkspaceCard) wired into the App.svelte shell
- rename features/workspace -> features/cwd-lsp (the cwd/lsp status feature)
- new features: mcp (status view), settings (chat-limit field), system-prompt
(prompt builder), all rendered via the generic surface host
- chat: store + ChatView updates
- tabs: tabs-store updates
- app wiring: ErrorModal (full-screen error surface), app/App.svelte + store.svelte
This commit makes HEAD typecheck clean for the first time: the prior HEAD
(dfe1711) imported features/settings from app/App.svelte but never committed
the feature, so only the full working tree was green.
Diffstat (limited to 'src/app/store.test.ts')
| -rw-r--r-- | src/app/store.test.ts | 62 |
1 files changed, 58 insertions, 4 deletions
diff --git a/src/app/store.test.ts b/src/app/store.test.ts index db6fdaa..5711442 100644 --- a/src/app/store.test.ts +++ b/src/app/store.test.ts @@ -103,6 +103,7 @@ function reconnectableSocket(): ReconnectableSocket { interface FakeFetchOptions { models?: readonly string[]; history?: Record<string, ConversationHistoryResponse>; + model?: string | null; } function fakeFetchImpl(opts?: FakeFetchOptions): typeof fetch { @@ -113,6 +114,15 @@ function fakeFetchImpl(opts?: FakeFetchOptions): typeof fetch { if (url.endsWith("/models")) { return new Response(JSON.stringify({ models }), { status: 200 }); } + if (url.endsWith("/model")) { + return new Response( + JSON.stringify({ + conversationId: "ignored", + model: opts?.model ?? null, + }), + { status: 200 }, + ); + } const body = history[url] ?? ({ chunks: [], latestSeq: 0 } satisfies ConversationHistoryResponse); return new Response(JSON.stringify(body), { status: 200 }); @@ -583,20 +593,64 @@ describe("createAppStore", () => { store.dispose(); }); - it("selecting a model updates the active tab", () => { + it("selecting a model persists it to the backend", () => { const ws = fakeSocket(); + const fetchImpl = fakeFetchImpl(); const store = createAppStore({ socketFactory: () => ws, - fetchImpl: fakeFetchImpl(), + fetchImpl, localStorage: createFakeStorage(), }); ws.resolveOpen(); store.send("hello"); - store.selectModel("openai/gpt-4o"); - expect(store.activeModel).toBe("openai/gpt-4o"); + const put = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ conversationId: "ignored", model: "openai/gpt-4o" }), { + status: 200, + }), + ); + const capturingStore = createAppStore({ + socketFactory: () => fakeSocket(), + fetchImpl: async (input, init) => { + const url = + typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + if (init?.method === "PUT" && url.endsWith("/model")) { + put(url, init); + } + return fetchImpl(input, init); + }, + localStorage: createFakeStorage(), + }); + capturingStore.send("hello"); + capturingStore.selectModel("openai/gpt-4o"); + + expect(put).toHaveBeenCalledOnce(); + const [callUrl, callInit] = put.mock.calls[0] as [string, RequestInit]; + expect(callUrl.endsWith("/model")).toBe(true); + expect(JSON.parse(callInit.body as string)).toEqual({ model: "openai/gpt-4o" }); + + store.dispose(); + capturingStore.dispose(); + }); + + it("focuses a conversation with a persisted model", async () => { + const ws = fakeSocket(); + const store = createAppStore({ + socketFactory: () => ws, + fetchImpl: fakeFetchImpl({ model: "openai/gpt-4o" }), + localStorage: createFakeStorage(), + }); + ws.resolveOpen(); + + store.send("first message"); + + // New tab opens with the default model until the persisted-model fetch resolves. + expect(store.activeModel).toBe("opencode/deepseek-v4-flash"); + + // Wait for the persisted model fetch to resolve. + await vi.waitFor(() => expect(store.activeModel).toBe("openai/gpt-4o")); expect(store.tabs[0]?.model).toBe("openai/gpt-4o"); store.dispose(); |
