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 | 38db3827870960f466be89afbc49f91238d46144 (patch) | |
| tree | 24cb1b896dfadc31e72552dbe67f00530881242e /src/app/App.test.ts | |
| parent | 17ce47987e673b6618454d033885b17b2a01912e (diff) | |
| download | dispatch-web-38db3827870960f466be89afbc49f91238d46144.tar.gz dispatch-web-38db3827870960f466be89afbc49f91238d46144.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
(c95cc77) 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/App.test.ts')
| -rw-r--r-- | src/app/App.test.ts | 108 |
1 files changed, 107 insertions, 1 deletions
diff --git a/src/app/App.test.ts b/src/app/App.test.ts index 6a39296..1a93948 100644 --- a/src/app/App.test.ts +++ b/src/app/App.test.ts @@ -1,4 +1,4 @@ -import type { WsServerMessage } from "@dispatch/transport-contract"; +import type { SetCwdRequest, WsServerMessage } from "@dispatch/transport-contract"; import type { SurfaceServerMessage } from "@dispatch/ui-contract"; import { render, screen } from "@testing-library/svelte"; import userEvent from "@testing-library/user-event"; @@ -62,6 +62,9 @@ function fakeFetchImpl(): typeof fetch { status: 200, }); } + if (url.includes("/conversations?status=")) { + return new Response(JSON.stringify({ conversations: [] }), { status: 200 }); + } if (url.endsWith("/cwd")) { return new Response(JSON.stringify({ conversationId: "c", cwd: null }), { status: 200 }); } @@ -415,4 +418,107 @@ describe("App component interaction tests", () => { store.dispose(); }); + + it("shows a full-screen error modal when fetchOpenConversations fails", async () => { + // A fetch that throws for the conversations list endpoint (simulating a + // network failure / unreachable backend on a new device). + const failingFetch = async (input: string | URL | Request): Promise<Response> => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + if (url.endsWith("/models")) { + return new Response(JSON.stringify({ models: ["opencode/deepseek-v4-flash"] }), { + status: 200, + }); + } + if (url.includes("/conversations?status=")) { + throw new TypeError("Failed to fetch: network error"); + } + if (url.endsWith("/cwd")) { + return new Response(JSON.stringify({ conversationId: "c", cwd: null }), { status: 200 }); + } + if (url.endsWith("/lsp")) { + return new Response(JSON.stringify({ conversationId: "c", cwd: null, servers: [] }), { + status: 200, + }); + } + return new Response(JSON.stringify({ chunks: [], latestSeq: 0 }), { status: 200 }); + }; + + const ws = fakeSocket(); + const store = createAppStore({ + socketFactory: () => ws, + fetchImpl: failingFetch, + localStorage: createFakeStorage(), + }); + ws.resolveOpen(); + + render(App, { props: { store } }); + + // The modal should appear with the error text + const dialog = await screen.findByRole("dialog", { name: "Error" }, { timeout: 3000 }); + expect(dialog).toBeInTheDocument(); + expect(dialog).toHaveTextContent("Failed to load conversations"); + expect(dialog).toHaveTextContent("TypeError"); + expect(dialog).toHaveTextContent("network error"); + + // Dismiss button clears the modal + const dismissBtn = screen.getByRole("button", { name: "Dismiss error" }); + await userEvent.setup().click(dismissBtn); + expect(store.fatalError).toBeNull(); + + store.dispose(); + }); + + it("does not show the error modal when fetchOpenConversations succeeds", async () => { + const ws = fakeSocket(); + const store = createAppStore({ + socketFactory: () => ws, + fetchImpl: fakeFetchImpl(), // returns valid empty conversations list + localStorage: createFakeStorage(), + }); + ws.resolveOpen(); + + render(App, { props: { store } }); + + // Wait a tick for boot async to settle + await new Promise((resolve) => setTimeout(resolve, 200)); + + expect(store.fatalError).toBeNull(); + + store.dispose(); + }); + + it("sends workspaceId when setting cwd", async () => { + let capturedBody: SetCwdRequest | undefined; + const fetchWithCapture = async ( + input: string | URL | Request, + init?: RequestInit, + ): Promise<Response> => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + if (url.endsWith("/cwd") && init?.method === "PUT") { + capturedBody = JSON.parse(init.body as string) as SetCwdRequest; + return new Response(JSON.stringify({ conversationId: "c", cwd: capturedBody.cwd }), { + status: 200, + }); + } + return fakeFetchImpl()(input); + }; + + const ws = fakeSocket(); + const store = createAppStore({ + socketFactory: () => ws, + fetchImpl: fetchWithCapture, + localStorage: createFakeStorage(), + workspaceId: "my-team", + }); + ws.resolveOpen(); + + // Let async boot settle before mutating cwd. + await new Promise((resolve) => setTimeout(resolve, 50)); + + const result = await store.setCwd("arch-rewrite"); + expect(result).toMatchObject({ ok: true, cwd: "arch-rewrite" }); + expect(capturedBody).toEqual({ cwd: "arch-rewrite", workspaceId: "my-team" }); + + store.dispose(); + }); }); |
