summaryrefslogtreecommitdiffhomepage
path: root/src/app/store.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-25 10:55:51 +0900
committerAdam Malczewski <[email protected]>2026-06-25 10:55:51 +0900
commite7d8cdbd68954034d2a4f6979a233b216212967b (patch)
tree89450470d669358277e7fb122236e298a0f32059 /src/app/store.test.ts
parentcd7f6cdfbd62bf4cf13c953bdb79d02158b6019e (diff)
downloaddispatch-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.ts62
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();