summaryrefslogtreecommitdiffhomepage
path: root/src/features/workspaces/adapter
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-10-19 13:38:57 +0900
committerAdam Malczewski <[email protected]>2026-10-19 13:38:57 +0900
commitc4397f633415a46852f0e3b49c78ab7b2d87d4c1 (patch)
tree047a5687565059422cbb38aeda8988502a66b78d /src/features/workspaces/adapter
parentf6d96289470ac888670050ff64c54bbe69b973cb (diff)
downloaddispatch-web-c4397f633415a46852f0e3b49c78ab7b2d87d4c1.tar.gz
dispatch-web-c4397f633415a46852f0e3b49c78ab7b2d87d4c1.zip
feat(workspaces): block creating a workspace whose slug is archived
Diffstat (limited to 'src/features/workspaces/adapter')
-rw-r--r--src/features/workspaces/adapter/http.test.ts30
-rw-r--r--src/features/workspaces/adapter/http.ts24
2 files changed, 43 insertions, 11 deletions
diff --git a/src/features/workspaces/adapter/http.test.ts b/src/features/workspaces/adapter/http.test.ts
index 2e34822..d6409ca 100644
--- a/src/features/workspaces/adapter/http.test.ts
+++ b/src/features/workspaces/adapter/http.test.ts
@@ -79,15 +79,37 @@ describe("createWorkspaceHttp", () => {
expect(result).toEqual({ ok: false, error: "invalid slug" });
});
- it("get returns null on 404", async () => {
+ it("get reports a missing lookup on 404", async () => {
const http = createWorkspaceHttp(BASE, fakeFetch([{ status: 404 }]));
- expect(await http.get("nope")).toBeNull();
+ expect(await http.get("nope")).toEqual({ kind: "missing" });
});
it("get returns the workspace on 200", async () => {
- const ws = { id: "x", title: "X", defaultCwd: "/home", createdAt: 1, lastActivityAt: 2 };
+ const ws = {
+ id: "x",
+ title: "X",
+ defaultCwd: "/home",
+ defaultComputerId: null,
+ starred: false,
+ archived: false,
+ createdAt: 1,
+ lastActivityAt: 2,
+ };
const http = createWorkspaceHttp(BASE, fakeFetch([{ body: ws }]));
- expect(await http.get("x")).toEqual(ws);
+ expect(await http.get("x")).toEqual({ kind: "found", workspace: ws });
+ });
+
+ it("get distinguishes a failed response from 404 (not 'missing')", async () => {
+ const http = createWorkspaceHttp(
+ BASE,
+ fakeFetch([{ status: 500, body: { error: "db down" } }]),
+ );
+ expect(await http.get("x")).toEqual({ kind: "failed", error: "db down" });
+ });
+
+ it("get reports a network error as failed (not 'missing')", async () => {
+ const http = createWorkspaceHttp(BASE, fakeFetch([new Error("network")]));
+ expect(await http.get("x")).toEqual({ kind: "failed", error: "network" });
});
it("setTitle PUTs the title", async () => {
diff --git a/src/features/workspaces/adapter/http.ts b/src/features/workspaces/adapter/http.ts
index de95a61..3f7f058 100644
--- a/src/features/workspaces/adapter/http.ts
+++ b/src/features/workspaces/adapter/http.ts
@@ -9,6 +9,7 @@ import type {
WorkspaceListResponse,
WorkspaceResponse,
} from "@dispatch/transport-contract";
+import type { WorkspaceLookup } from "../logic/create";
/**
* Workspace HTTP effects — the injected edge that talks to the backend's
@@ -21,7 +22,7 @@ import type {
* Endpoints ([email protected]):
* - `GET /workspaces` (optional `?archived=true|false|all`, default `false` = active only) → list
* - `PUT /workspaces/:id` (create-on-miss, idempotent) → ensure
- * - `GET /workspaces/:id` (404 → null) → get
+ * - `GET /workspaces/:id` (found | 404-missing | failed) → get
* - `PUT /workspaces/:id/title` → rename
* - `PUT /workspaces/:id/default-cwd` → set/clear default cwd
* - `PUT /workspaces/:id/default-computer` → set/clear default computer (SSH handoff #2)
@@ -41,7 +42,12 @@ export type WorkspaceArchivedFilter = "true" | "false" | "all";
export interface WorkspaceHttp {
list(archived?: WorkspaceArchivedFilter): Promise<readonly WorkspaceEntry[]>;
ensure(id: string, body?: EnsureWorkspaceRequest): Promise<WorkspaceResult<Workspace>>;
- get(id: string): Promise<Workspace | null>;
+ /**
+ * Authoritative existence probe. 404 → `missing`, a network/server failure →
+ * `failed` (distinct on purpose: the create gate must not treat a broken
+ * check as "doesn't exist"), 200 → the workspace (`found`).
+ */
+ get(id: string): Promise<WorkspaceLookup>;
setTitle(id: string, title: string): Promise<WorkspaceResult<Workspace>>;
setDefaultCwd(id: string, defaultCwd: string | null): Promise<WorkspaceResult<Workspace>>;
setDefaultComputer(id: string, computerId: string | null): Promise<WorkspaceResult<Workspace>>;
@@ -96,13 +102,17 @@ export function createWorkspaceHttp(httpBase: string, fetchImpl: typeof fetch):
}
},
- async get(id): Promise<Workspace | null> {
+ async get(id): Promise<WorkspaceLookup> {
try {
const res = await fetchImpl(`${httpBase}/workspaces/${encodeURIComponent(id)}`);
- if (res.status === 404 || !res.ok) return null;
- return (await res.json()) as WorkspaceResponse;
- } catch {
- return null;
+ if (res.status === 404) return { kind: "missing" };
+ if (!res.ok) return { kind: "failed", error: await errText(res) };
+ return { kind: "found", workspace: (await res.json()) as WorkspaceResponse };
+ } catch (err) {
+ return {
+ kind: "failed",
+ error: err instanceof Error ? err.message : "Workspace lookup failed",
+ };
}
},