summaryrefslogtreecommitdiffhomepage
path: root/src/features/workspaces/store.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-09-20 16:55:12 +0900
committerAdam Malczewski <[email protected]>2026-09-20 16:55:12 +0900
commitcaf7060ec571039102acd7b4fd73008fdecbe8a9 (patch)
tree7eac6c55556d20021de4a6e19f63d8efaa21ce84 /src/features/workspaces/store.test.ts
parent72663945ef0a0053fbfd83b168e90f12169a5c3a (diff)
downloaddispatch-web-caf7060ec571039102acd7b4fd73008fdecbe8a9.tar.gz
dispatch-web-caf7060ec571039102acd7b4fd73008fdecbe8a9.zip
feat(workspaces): archive option + archived-workspaces view
Consume the backend's archive contract (feature/archive-workspaces): - adapter/http: archive(id)/unarchive(id) (PUT/DELETE /workspaces/:id/archive); list(archived?) passes the ?archived= query - logic/view-model: pure applyArchived(list, id, archived) - store.svelte: setArchived(id, archived) (optimistic + revert-on-error + reconciling refresh); archivedList + refreshArchived() for the archived view - ui/TileActionBar: new Archive action (the right-click→select action bar) - ui/HomeHeader: new Archived button opens the archived view - ui/ArchivedWorkspaces (new): modal listing archived workspaces with Unarchive + View (navigates to /<id>) - ui/WorkspacesHome: wire the Archive action + the Archived modal - .dispatch/wire.reference.md + backend-handoff.md: record the contract delta
Diffstat (limited to 'src/features/workspaces/store.test.ts')
-rw-r--r--src/features/workspaces/store.test.ts105
1 files changed, 104 insertions, 1 deletions
diff --git a/src/features/workspaces/store.test.ts b/src/features/workspaces/store.test.ts
index 4caac9f..a44d8a6 100644
--- a/src/features/workspaces/store.test.ts
+++ b/src/features/workspaces/store.test.ts
@@ -10,6 +10,7 @@ function entry(overrides: Partial<WorkspaceEntry> = {}): WorkspaceEntry {
defaultCwd: null,
defaultComputerId: null,
starred: false,
+ archived: false,
createdAt: 1,
lastActivityAt: 2,
conversationCount: 0,
@@ -22,9 +23,14 @@ function fakeHttp(opts: {
list?: readonly WorkspaceEntry[];
star?: (id: string) => Promise<WorkspaceResult<Workspace>>;
unstar?: (id: string) => Promise<WorkspaceResult<Workspace>>;
+ archive?: (id: string) => Promise<WorkspaceResult<Workspace>>;
+ unarchive?: (id: string) => Promise<WorkspaceResult<Workspace>>;
}) {
return {
- list: vi.fn(async (): Promise<readonly WorkspaceEntry[]> => opts.list ?? []),
+ list: vi.fn(
+ async (_archived?: "true" | "false" | "all"): Promise<readonly WorkspaceEntry[]> =>
+ opts.list ?? [],
+ ),
ensure: vi.fn(),
get: vi.fn(),
setTitle: vi.fn(),
@@ -46,6 +52,22 @@ function fakeHttp(opts: {
value: entry({ id, starred: false }),
}),
),
+ archive:
+ opts.archive ??
+ vi.fn(
+ async (id: string): Promise<WorkspaceResult<Workspace>> => ({
+ ok: true,
+ value: entry({ id, archived: true }),
+ }),
+ ),
+ unarchive:
+ opts.unarchive ??
+ vi.fn(
+ async (id: string): Promise<WorkspaceResult<Workspace>> => ({
+ ok: true,
+ value: entry({ id, archived: false }),
+ }),
+ ),
delete: vi.fn(),
};
}
@@ -143,3 +165,84 @@ describe("createWorkspaceStore — setStarred", () => {
expect(store.list).toHaveLength(0);
});
});
+
+describe("createWorkspaceStore — setArchived", () => {
+ it("optimistically flips archived to true before the request resolves", async () => {
+ const http = fakeHttp({ list: [entry({ id: "a", archived: false })] });
+ const store = createWorkspaceStore(http);
+ await store.refresh();
+
+ let observedDuringCall = false;
+ http.archive = vi.fn(async (_id: string): Promise<WorkspaceResult<Workspace>> => {
+ // While the request is in flight, the store already shows the new state.
+ observedDuringCall = store.list[0]?.archived === true;
+ return { ok: true, value: entry({ id: "a", archived: true }) };
+ });
+
+ await store.setArchived("a", true);
+
+ expect(observedDuringCall).toBe(true);
+ expect(http.archive).toHaveBeenCalledWith("a");
+ });
+
+ it("calls unarchive (DELETE) when archiving false", async () => {
+ const http = fakeHttp({ list: [entry({ id: "a", archived: true })] });
+ const store = createWorkspaceStore(http);
+ await store.refresh();
+
+ await store.setArchived("a", false);
+
+ expect(http.unarchive).toHaveBeenCalledWith("a");
+ expect(http.archive).not.toHaveBeenCalled();
+ });
+
+ it("reverts the optimistic flip on error", async () => {
+ const http = fakeHttp({ list: [entry({ id: "a", archived: false })] });
+ const store = createWorkspaceStore(http);
+ await store.refresh();
+
+ http.archive = vi.fn(
+ async (): Promise<WorkspaceResult<Workspace>> => ({ ok: false, error: "boom" }),
+ );
+
+ const result = await store.setArchived("a", true);
+
+ expect(result).toEqual({ ok: false, error: "boom" });
+ expect(store.list[0]?.archived).toBe(false);
+ });
+
+ it("refreshes the active list after a successful archive", async () => {
+ const http = fakeHttp({ list: [entry({ id: "a", archived: false })] });
+ const store = createWorkspaceStore(http);
+ await store.refresh();
+ const listCallsBefore = http.list.mock.calls.length;
+
+ await store.setArchived("a", true);
+
+ // setArchived triggers a refresh() to reconcile the active-only list.
+ expect(http.list.mock.calls.length).toBeGreaterThan(listCallsBefore);
+ });
+});
+
+describe("createWorkspaceStore — refreshArchived", () => {
+ it("loads the archived list via list('true') without touching the main list", async () => {
+ const http = fakeHttp({ list: [entry({ id: "active", archived: false })] });
+ const store = createWorkspaceStore(http);
+ await store.refresh();
+
+ // Point the archived call at a different payload.
+ http.list = vi.fn(
+ async (archived?: "true" | "false" | "all"): Promise<readonly WorkspaceEntry[]> =>
+ archived === "true"
+ ? [entry({ id: "done", archived: true })]
+ : [entry({ id: "active", archived: false })],
+ );
+
+ await store.refreshArchived();
+
+ expect(http.list).toHaveBeenCalledWith("true");
+ expect(store.archivedList.map((w) => w.id)).toEqual(["done"]);
+ // The main (active) list is untouched.
+ expect(store.list.map((w) => w.id)).toEqual(["active"]);
+ });
+});