summaryrefslogtreecommitdiffhomepage
path: root/packages/conversation-store/src/store-workspace.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/conversation-store/src/store-workspace.test.ts')
-rw-r--r--packages/conversation-store/src/store-workspace.test.ts1451
1 files changed, 790 insertions, 661 deletions
diff --git a/packages/conversation-store/src/store-workspace.test.ts b/packages/conversation-store/src/store-workspace.test.ts
index 3926c94..59ea990 100644
--- a/packages/conversation-store/src/store-workspace.test.ts
+++ b/packages/conversation-store/src/store-workspace.test.ts
@@ -3,674 +3,803 @@ import { beforeEach, describe, expect, it } from "vitest";
import { createConversationStore, isValidWorkspaceSlug } from "./store.js";
function createMemoryStorage(): StorageNamespace {
- const data = new Map<string, string>();
- return {
- get: async (key) => data.get(key) ?? null,
- set: async (key, value) => {
- data.set(key, value);
- },
- delete: async (key) => {
- data.delete(key);
- },
- has: async (key) => data.has(key),
- keys: async (prefix) => {
- const all = [...data.keys()];
- if (!prefix) return all;
- return all.filter((k) => k.startsWith(prefix));
- },
- };
+ const data = new Map<string, string>();
+ return {
+ get: async (key) => data.get(key) ?? null,
+ set: async (key, value) => {
+ data.set(key, value);
+ },
+ delete: async (key) => {
+ data.delete(key);
+ },
+ has: async (key) => data.has(key),
+ keys: async (prefix) => {
+ const all = [...data.keys()];
+ if (!prefix) return all;
+ return all.filter((k) => k.startsWith(prefix));
+ },
+ };
}
describe("WorkspaceStore", () => {
- let storage: StorageNamespace;
- let clock: number;
-
- beforeEach(() => {
- storage = createMemoryStorage();
- clock = 1000;
- });
-
- function makeStore(serverDefaultCwd?: string) {
- return createConversationStore(storage, undefined, () => clock, serverDefaultCwd);
- }
-
- function userMessage(text: string): ChatMessage {
- return { role: "user", chunks: [{ type: "text", text }] };
- }
-
- it("ensureWorkspace creates with defaults", async () => {
- const store = makeStore();
- clock = 1000;
- const ws = await store.ensureWorkspace("my-work");
- expect(ws).toEqual({
- id: "my-work",
- title: "my-work",
- defaultCwd: null,
- defaultComputerId: null,
- createdAt: 1000,
- lastActivityAt: 1000,
- });
- });
-
- it("ensureWorkspace returns existing as-is", async () => {
- const store = makeStore();
- clock = 1000;
- await store.ensureWorkspace("my-work");
- clock = 2000;
- const ws = await store.ensureWorkspace("my-work", {
- title: "New Title",
- defaultCwd: "/ignored",
- });
- expect(ws).toEqual({
- id: "my-work",
- title: "my-work",
- defaultCwd: null,
- defaultComputerId: null,
- createdAt: 1000,
- lastActivityAt: 1000,
- });
- });
-
- it("ensureWorkspace with custom title/defaultCwd", async () => {
- const store = makeStore();
- clock = 3000;
- const ws = await store.ensureWorkspace("my-work", {
- title: "Custom",
- defaultCwd: "/projects/dispatch",
- });
- expect(ws).toEqual({
- id: "my-work",
- title: "Custom",
- defaultCwd: "/projects/dispatch",
- defaultComputerId: null,
- createdAt: 3000,
- lastActivityAt: 3000,
- });
- });
-
- it("getWorkspace synthesizes default", async () => {
- const store = makeStore();
- const ws = await store.getWorkspace("default");
- expect(ws).toEqual({
- id: "default",
- title: "default",
- defaultCwd: null,
- defaultComputerId: null,
- createdAt: 0,
- lastActivityAt: 0,
- });
- });
-
- it("getWorkspace returns null for unknown", async () => {
- const store = makeStore();
- expect(await store.getWorkspace("unknown")).toBeNull();
- });
-
- it("setWorkspaceTitle renames", async () => {
- const store = makeStore();
- clock = 1000;
- await store.ensureWorkspace("my-work");
- clock = 2000;
- const ws = await store.setWorkspaceTitle("my-work", "Renamed");
- expect(ws).toEqual({
- id: "my-work",
- title: "Renamed",
- defaultCwd: null,
- defaultComputerId: null,
- createdAt: 1000,
- lastActivityAt: 1000,
- });
- });
-
- it("setWorkspaceDefaultCwd sets and clears", async () => {
- const store = makeStore();
- clock = 1000;
- await store.ensureWorkspace("my-work");
- clock = 2000;
- const setWs = await store.setWorkspaceDefaultCwd("my-work", "/some/path");
- expect(setWs.defaultCwd).toBe("/some/path");
- expect(setWs.lastActivityAt).toBe(1000); // does not bump on defaultCwd change
- const cleared = await store.setWorkspaceDefaultCwd("my-work", null);
- expect(cleared.defaultCwd).toBeNull();
- });
-
- it("deleteWorkspace closes conversations", async () => {
- const store = makeStore();
- clock = 1000;
- await store.ensureWorkspace("work-a");
- await store.setWorkspaceId("conv1", "work-a");
- await store.setWorkspaceId("conv2", "work-a");
- await store.setWorkspaceId("conv3", "default");
-
- clock = 2000;
- await store.append("conv1", [userMessage("hi 1")]);
- await store.append("conv2", [userMessage("hi 2")]);
- await store.append("conv3", [userMessage("hi 3")]);
-
- const result = await store.deleteWorkspace("work-a");
- expect(result.closedCount).toBe(2);
-
- const meta1 = await store.getConversationMeta("conv1");
- expect(meta1?.status).toBe("closed");
- expect(meta1?.workspaceId).toBe("default");
-
- const meta2 = await store.getConversationMeta("conv2");
- expect(meta2?.status).toBe("closed");
- expect(meta2?.workspaceId).toBe("default");
-
- const meta3 = await store.getConversationMeta("conv3");
- expect(meta3?.status).toBe("idle");
- expect(meta3?.workspaceId).toBe("default");
-
- expect(await store.getWorkspace("work-a")).toBeNull();
- });
-
- it("deleteWorkspace throws for default", async () => {
- const store = makeStore();
- await expect(store.deleteWorkspace("default")).rejects.toThrow();
- });
-
- it("listWorkspaces sorted by lastActivityAt desc", async () => {
- const store = makeStore();
- clock = 1000;
- await store.ensureWorkspace("alpha");
- clock = 2000;
- await store.ensureWorkspace("beta");
- clock = 3000;
- await store.ensureWorkspace("gamma");
-
- const list = await store.listWorkspaces();
- expect(list.map((w) => w.id)).toEqual(["gamma", "beta", "alpha", "default"]);
- });
-
- it("listWorkspaces includes conversationCount", async () => {
- const store = makeStore();
- clock = 1000;
- await store.ensureWorkspace("work-a");
- await store.ensureWorkspace("work-b");
- await store.setWorkspaceId("a1", "work-a");
- await store.setWorkspaceId("a2", "work-a");
- await store.setWorkspaceId("b1", "work-b");
- await store.append("lonely", [userMessage("hi")]); // defaults to "default"
-
- const list = await store.listWorkspaces();
- const counts = Object.fromEntries(list.map((w) => [w.id, w.conversationCount]));
- expect(counts["work-a"]).toBe(2);
- expect(counts["work-b"]).toBe(1);
- expect(counts.default).toBe(1);
- });
-
- it("listWorkspaces always includes default", async () => {
- const store = makeStore();
- clock = 1000;
- await store.ensureWorkspace("only");
- // No append or explicit default creation — default is synthesized.
- const list = await store.listWorkspaces();
- const ids = list.map((w) => w.id);
- expect(ids).toContain("default");
- const defaultWs = list.find((w) => w.id === "default");
- expect(defaultWs).toEqual({
- id: "default",
- title: "default",
- defaultCwd: null,
- defaultComputerId: null,
- createdAt: 0,
- lastActivityAt: 0,
- conversationCount: 0,
- });
- });
-
- it("getWorkspaceId returns default for legacy", async () => {
- const store = makeStore();
- await store.append("conv1", [userMessage("hi")]);
- expect(await store.getWorkspaceId("conv1")).toBe("default");
- expect(await store.getWorkspaceId("never-seen")).toBe("default");
- });
-
- it("setWorkspaceId persists and reads back", async () => {
- const store = makeStore();
- clock = 1000;
- await store.setWorkspaceId("conv1", "my-work");
- expect(await store.getWorkspaceId("conv1")).toBe("my-work");
- const meta = await store.getConversationMeta("conv1");
- expect(meta?.workspaceId).toBe("my-work");
- expect(meta?.status).toBe("idle");
- });
-
- it("getEffectiveCwd: absolute conversation cwd overrides workspace defaultCwd", async () => {
- const store = makeStore("/server/default");
- await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/default" });
- await store.setWorkspaceId("conv1", "my-work");
- await store.setCwd("conv1", "/explicit/path");
- expect(await store.getEffectiveCwd("conv1")).toBe("/explicit/path");
- });
-
- it("getEffectiveCwd: workspace defaultCwd used when conversation cwd is unset (bug fix)", async () => {
- const store = makeStore("/server/default");
- await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/default" });
- await store.setWorkspaceId("conv1", "my-work");
- expect(await store.getEffectiveCwd("conv1")).toBe("/workspace/default");
- });
-
- it("getEffectiveCwd: serverDefaultCwd fallback when both conversation and workspace cwd are null", async () => {
- const store = makeStore("/server/default");
- await store.ensureWorkspace("my-work");
- await store.setWorkspaceId("conv1", "my-work");
- expect(await store.getEffectiveCwd("conv1")).toBe("/server/default");
- });
-
- it("getEffectiveCwd: relative conversation cwd resolved against workspace defaultCwd", async () => {
- const store = makeStore("/server/default");
- await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/root" });
- await store.setWorkspaceId("conv1", "my-work");
- await store.setCwd("conv1", "subdir");
- expect(await store.getEffectiveCwd("conv1")).toBe("/workspace/root/subdir");
- });
-
- it("getEffectiveCwd: relative conversation cwd resolved against serverDefaultCwd when workspace defaultCwd is null", async () => {
- const store = makeStore("/server/default");
- await store.ensureWorkspace("my-work");
- await store.setWorkspaceId("conv1", "my-work");
- await store.setCwd("conv1", "subdir");
- expect(await store.getEffectiveCwd("conv1")).toBe("/server/default/subdir");
- });
-
- it("getEffectiveCwd: relative cwd with nested segments resolved against workspace defaultCwd", async () => {
- const store = makeStore("/server/default");
- await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/root" });
- await store.setWorkspaceId("conv1", "my-work");
- await store.setCwd("conv1", "a/b/c");
- expect(await store.getEffectiveCwd("conv1")).toBe("/workspace/root/a/b/c");
- });
-
- it("getEffectiveCwd: relative cwd with .. segments normalizes via path.resolve", async () => {
- const store = makeStore("/server/default");
- await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/root/sub" });
- await store.setWorkspaceId("conv1", "my-work");
- await store.setCwd("conv1", "../sibling");
- expect(await store.getEffectiveCwd("conv1")).toBe("/workspace/root/sibling");
- });
-
- it("getEffectiveCwd: default workspace (no defaultCwd) falls through to serverDefaultCwd", async () => {
- const store = makeStore("/server/default");
- // No explicit workspace assignment — defaults to "default" workspace
- // which has defaultCwd null.
- expect(await store.getEffectiveCwd("conv1")).toBe("/server/default");
- });
-
- // --- overrideCwd (per-turn cwd override) ---
-
- it("getEffectiveCwd: overrideCwd absolute (starts with /) returned as-is, overriding workspace defaultCwd", async () => {
- const store = makeStore("/server/default");
- await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/default" });
- await store.setWorkspaceId("conv1", "my-work");
- // An absolute override wins outright, even over a workspace defaultCwd.
- expect(await store.getEffectiveCwd("conv1", "/override/abs")).toBe("/override/abs");
- });
-
- it("getEffectiveCwd: overrideCwd relative resolved against workspace defaultCwd", async () => {
- const store = makeStore("/server/default");
- await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/root" });
- await store.setWorkspaceId("conv1", "my-work");
- expect(await store.getEffectiveCwd("conv1", "subdir")).toBe("/workspace/root/subdir");
- });
-
- it("getEffectiveCwd: overrideCwd relative resolved against serverDefaultCwd when workspace defaultCwd is null", async () => {
- const store = makeStore("/server/default");
- await store.ensureWorkspace("my-work");
- await store.setWorkspaceId("conv1", "my-work");
- expect(await store.getEffectiveCwd("conv1", "subdir")).toBe("/server/default/subdir");
- });
-
- it("getEffectiveCwd: overrideCwd does NOT read the persisted getCwd (override wins over persisted)", async () => {
- const store = makeStore("/server/default");
- await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/root" });
- await store.setWorkspaceId("conv1", "my-work");
- // Persist a cwd that differs from the override — the override must win.
- await store.setCwd("conv1", "/persisted/path");
- expect(await store.getEffectiveCwd("conv1", "override-rel")).toBe(
- "/workspace/root/override-rel",
- );
- });
-
- it("getEffectiveCwd: overrideCwd omitted behaves as today (uses persisted cwd)", async () => {
- const store = makeStore("/server/default");
- await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/root" });
- await store.setWorkspaceId("conv1", "my-work");
- await store.setCwd("conv1", "persisted-rel");
- // No second arg — persisted cwd is used.
- expect(await store.getEffectiveCwd("conv1")).toBe("/workspace/root/persisted-rel");
- });
-
- it("clearCwd → getEffectiveCwd falls through to workspace defaultCwd (un-shadows it)", async () => {
- const store = makeStore("/server/default");
- await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/default" });
- await store.setWorkspaceId("conv1", "my-work");
- await store.setCwd("conv1", "/explicit/path");
- // Before clear: the conversation cwd shadows the workspace defaultCwd.
- expect(await store.getEffectiveCwd("conv1")).toBe("/explicit/path");
- // After clear: the workspace defaultCwd is used (fall-through).
- await store.clearCwd("conv1");
- expect(await store.getEffectiveCwd("conv1")).toBe("/workspace/default");
- });
-
- it("getEffectiveCwd: an empty-string cwd does NOT fall through (proving clear ≠ setCwd(''))", async () => {
- const store = makeStore("/server/default");
- await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/default" });
- await store.setWorkspaceId("conv1", "my-work");
- // An empty string is a non-null explicit cwd — it is resolved (not
- // treated as absent), so it does NOT fall through to the workspace
- // defaultCwd. This is the gap clearCwd fixes.
- await store.setCwd("conv1", "");
- expect(await store.getCwd("conv1")).toBe("");
- // path.resolve("/workspace/default", "") === "/workspace/default" —
- // but this is a RELATIVE cwd resolution, not a fall-through. The point
- // is that getCwd returns "" (not null), so the relative branch runs.
- // With a clearCwd, getCwd returns null and the fall-through branch runs.
- await store.clearCwd("conv1");
- expect(await store.getCwd("conv1")).toBeNull();
- expect(await store.getEffectiveCwd("conv1")).toBe("/workspace/default");
- });
-
- it("listConversations filtered by workspaceId", async () => {
- const store = makeStore();
- await store.ensureWorkspace("work-a");
- await store.ensureWorkspace("work-b");
- await store.append("a1", [userMessage("a1")]);
- await store.append("a2", [userMessage("a2")]);
- await store.append("b1", [userMessage("b1")]);
- await store.setWorkspaceId("a1", "work-a");
- await store.setWorkspaceId("a2", "work-a");
- await store.setWorkspaceId("b1", "work-b");
-
- const aConvs = await store.listConversations({ workspaceId: "work-a" });
- expect(aConvs.map((c) => c.id).sort()).toEqual(["a1", "a2"]);
-
- const bConvs = await store.listConversations({ workspaceId: "work-b" });
- expect(bConvs.map((c) => c.id)).toEqual(["b1"]);
- });
-
- it("append updates workspace lastActivityAt", async () => {
- const store = makeStore();
- clock = 1000;
- await store.ensureWorkspace("my-work");
- clock = 2000;
- await store.setWorkspaceId("conv1", "my-work");
- clock = 3000;
- await store.append("conv1", [userMessage("hi")]);
- const ws = await store.getWorkspace("my-work");
- expect(ws?.lastActivityAt).toBe(3000);
- });
-
- it("forkHistory copies workspaceId", async () => {
- const store = makeStore();
- await store.ensureWorkspace("my-work");
- await store.setWorkspaceId("source", "my-work");
- await store.append("source", [userMessage("hello")]);
- await store.forkHistory("source", "target");
- const targetMeta = await store.getConversationMeta("target");
- expect(targetMeta?.workspaceId).toBe("my-work");
- });
-
- it("replaceHistory preserves workspaceId", async () => {
- const store = makeStore();
- await store.ensureWorkspace("my-work");
- await store.setWorkspaceId("conv1", "my-work");
- await store.append("conv1", [userMessage("original")]);
- await store.replaceHistory("conv1", [userMessage("replaced")]);
- const meta = await store.getConversationMeta("conv1");
- expect(meta?.workspaceId).toBe("my-work");
- });
+ let storage: StorageNamespace;
+ let clock: number;
+
+ beforeEach(() => {
+ storage = createMemoryStorage();
+ clock = 1000;
+ });
+
+ function makeStore(serverDefaultCwd?: string) {
+ return createConversationStore(storage, undefined, () => clock, serverDefaultCwd);
+ }
+
+ function userMessage(text: string): ChatMessage {
+ return { role: "user", chunks: [{ type: "text", text }] };
+ }
+
+ it("ensureWorkspace creates with defaults", async () => {
+ const store = makeStore();
+ clock = 1000;
+ const ws = await store.ensureWorkspace("my-work");
+ expect(ws).toEqual({
+ id: "my-work",
+ title: "my-work",
+ defaultCwd: null,
+ defaultComputerId: null,
+ starred: false,
+ createdAt: 1000,
+ lastActivityAt: 1000,
+ });
+ });
+
+ it("ensureWorkspace returns existing as-is", async () => {
+ const store = makeStore();
+ clock = 1000;
+ await store.ensureWorkspace("my-work");
+ clock = 2000;
+ const ws = await store.ensureWorkspace("my-work", {
+ title: "New Title",
+ defaultCwd: "/ignored",
+ });
+ expect(ws).toEqual({
+ id: "my-work",
+ title: "my-work",
+ defaultCwd: null,
+ defaultComputerId: null,
+ starred: false,
+ createdAt: 1000,
+ lastActivityAt: 1000,
+ });
+ });
+
+ it("ensureWorkspace with custom title/defaultCwd", async () => {
+ const store = makeStore();
+ clock = 3000;
+ const ws = await store.ensureWorkspace("my-work", {
+ title: "Custom",
+ defaultCwd: "/projects/dispatch",
+ });
+ expect(ws).toEqual({
+ id: "my-work",
+ title: "Custom",
+ defaultCwd: "/projects/dispatch",
+ defaultComputerId: null,
+ starred: false,
+ createdAt: 3000,
+ lastActivityAt: 3000,
+ });
+ });
+
+ it("getWorkspace synthesizes default", async () => {
+ const store = makeStore();
+ const ws = await store.getWorkspace("default");
+ expect(ws).toEqual({
+ id: "default",
+ title: "default",
+ defaultCwd: null,
+ defaultComputerId: null,
+ starred: false,
+ createdAt: 0,
+ lastActivityAt: 0,
+ });
+ });
+
+ it("getWorkspace returns null for unknown", async () => {
+ const store = makeStore();
+ expect(await store.getWorkspace("unknown")).toBeNull();
+ });
+
+ it("setWorkspaceTitle renames", async () => {
+ const store = makeStore();
+ clock = 1000;
+ await store.ensureWorkspace("my-work");
+ clock = 2000;
+ const ws = await store.setWorkspaceTitle("my-work", "Renamed");
+ expect(ws).toEqual({
+ id: "my-work",
+ title: "Renamed",
+ defaultCwd: null,
+ defaultComputerId: null,
+ starred: false,
+ createdAt: 1000,
+ lastActivityAt: 1000,
+ });
+ });
+
+ it("setWorkspaceDefaultCwd sets and clears", async () => {
+ const store = makeStore();
+ clock = 1000;
+ await store.ensureWorkspace("my-work");
+ clock = 2000;
+ const setWs = await store.setWorkspaceDefaultCwd("my-work", "/some/path");
+ expect(setWs.defaultCwd).toBe("/some/path");
+ expect(setWs.lastActivityAt).toBe(1000); // does not bump on defaultCwd change
+ const cleared = await store.setWorkspaceDefaultCwd("my-work", null);
+ expect(cleared.defaultCwd).toBeNull();
+ });
+
+ it("deleteWorkspace closes conversations", async () => {
+ const store = makeStore();
+ clock = 1000;
+ await store.ensureWorkspace("work-a");
+ await store.setWorkspaceId("conv1", "work-a");
+ await store.setWorkspaceId("conv2", "work-a");
+ await store.setWorkspaceId("conv3", "default");
+
+ clock = 2000;
+ await store.append("conv1", [userMessage("hi 1")]);
+ await store.append("conv2", [userMessage("hi 2")]);
+ await store.append("conv3", [userMessage("hi 3")]);
+
+ const result = await store.deleteWorkspace("work-a");
+ expect(result.closedCount).toBe(2);
+
+ const meta1 = await store.getConversationMeta("conv1");
+ expect(meta1?.status).toBe("closed");
+ expect(meta1?.workspaceId).toBe("default");
+
+ const meta2 = await store.getConversationMeta("conv2");
+ expect(meta2?.status).toBe("closed");
+ expect(meta2?.workspaceId).toBe("default");
+
+ const meta3 = await store.getConversationMeta("conv3");
+ expect(meta3?.status).toBe("idle");
+ expect(meta3?.workspaceId).toBe("default");
+
+ expect(await store.getWorkspace("work-a")).toBeNull();
+ });
+
+ it("deleteWorkspace throws for default", async () => {
+ const store = makeStore();
+ await expect(store.deleteWorkspace("default")).rejects.toThrow();
+ });
+
+ it("listWorkspaces sorted by lastActivityAt desc", async () => {
+ const store = makeStore();
+ clock = 1000;
+ await store.ensureWorkspace("alpha");
+ clock = 2000;
+ await store.ensureWorkspace("beta");
+ clock = 3000;
+ await store.ensureWorkspace("gamma");
+
+ const list = await store.listWorkspaces();
+ expect(list.map((w) => w.id)).toEqual(["gamma", "beta", "alpha", "default"]);
+ });
+
+ it("listWorkspaces includes conversationCount", async () => {
+ const store = makeStore();
+ clock = 1000;
+ await store.ensureWorkspace("work-a");
+ await store.ensureWorkspace("work-b");
+ await store.setWorkspaceId("a1", "work-a");
+ await store.setWorkspaceId("a2", "work-a");
+ await store.setWorkspaceId("b1", "work-b");
+ await store.append("lonely", [userMessage("hi")]); // defaults to "default"
+
+ const list = await store.listWorkspaces();
+ const counts = Object.fromEntries(list.map((w) => [w.id, w.conversationCount]));
+ expect(counts["work-a"]).toBe(2);
+ expect(counts["work-b"]).toBe(1);
+ expect(counts.default).toBe(1);
+ });
+
+ it("listWorkspaces always includes default", async () => {
+ const store = makeStore();
+ clock = 1000;
+ await store.ensureWorkspace("only");
+ // No append or explicit default creation — default is synthesized.
+ const list = await store.listWorkspaces();
+ const ids = list.map((w) => w.id);
+ expect(ids).toContain("default");
+ const defaultWs = list.find((w) => w.id === "default");
+ expect(defaultWs).toEqual({
+ id: "default",
+ title: "default",
+ defaultCwd: null,
+ defaultComputerId: null,
+ starred: false,
+ createdAt: 0,
+ lastActivityAt: 0,
+ conversationCount: 0,
+ });
+ });
+
+ it("getWorkspaceId returns default for legacy", async () => {
+ const store = makeStore();
+ await store.append("conv1", [userMessage("hi")]);
+ expect(await store.getWorkspaceId("conv1")).toBe("default");
+ expect(await store.getWorkspaceId("never-seen")).toBe("default");
+ });
+
+ it("setWorkspaceId persists and reads back", async () => {
+ const store = makeStore();
+ clock = 1000;
+ await store.setWorkspaceId("conv1", "my-work");
+ expect(await store.getWorkspaceId("conv1")).toBe("my-work");
+ const meta = await store.getConversationMeta("conv1");
+ expect(meta?.workspaceId).toBe("my-work");
+ expect(meta?.status).toBe("idle");
+ });
+
+ it("getEffectiveCwd: absolute conversation cwd overrides workspace defaultCwd", async () => {
+ const store = makeStore("/server/default");
+ await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/default" });
+ await store.setWorkspaceId("conv1", "my-work");
+ await store.setCwd("conv1", "/explicit/path");
+ expect(await store.getEffectiveCwd("conv1")).toBe("/explicit/path");
+ });
+
+ it("getEffectiveCwd: workspace defaultCwd used when conversation cwd is unset (bug fix)", async () => {
+ const store = makeStore("/server/default");
+ await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/default" });
+ await store.setWorkspaceId("conv1", "my-work");
+ expect(await store.getEffectiveCwd("conv1")).toBe("/workspace/default");
+ });
+
+ it("getEffectiveCwd: serverDefaultCwd fallback when both conversation and workspace cwd are null", async () => {
+ const store = makeStore("/server/default");
+ await store.ensureWorkspace("my-work");
+ await store.setWorkspaceId("conv1", "my-work");
+ expect(await store.getEffectiveCwd("conv1")).toBe("/server/default");
+ });
+
+ it("getEffectiveCwd: relative conversation cwd resolved against workspace defaultCwd", async () => {
+ const store = makeStore("/server/default");
+ await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/root" });
+ await store.setWorkspaceId("conv1", "my-work");
+ await store.setCwd("conv1", "subdir");
+ expect(await store.getEffectiveCwd("conv1")).toBe("/workspace/root/subdir");
+ });
+
+ it("getEffectiveCwd: relative conversation cwd resolved against serverDefaultCwd when workspace defaultCwd is null", async () => {
+ const store = makeStore("/server/default");
+ await store.ensureWorkspace("my-work");
+ await store.setWorkspaceId("conv1", "my-work");
+ await store.setCwd("conv1", "subdir");
+ expect(await store.getEffectiveCwd("conv1")).toBe("/server/default/subdir");
+ });
+
+ it("getEffectiveCwd: relative cwd with nested segments resolved against workspace defaultCwd", async () => {
+ const store = makeStore("/server/default");
+ await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/root" });
+ await store.setWorkspaceId("conv1", "my-work");
+ await store.setCwd("conv1", "a/b/c");
+ expect(await store.getEffectiveCwd("conv1")).toBe("/workspace/root/a/b/c");
+ });
+
+ it("getEffectiveCwd: relative cwd with .. segments normalizes via path.resolve", async () => {
+ const store = makeStore("/server/default");
+ await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/root/sub" });
+ await store.setWorkspaceId("conv1", "my-work");
+ await store.setCwd("conv1", "../sibling");
+ expect(await store.getEffectiveCwd("conv1")).toBe("/workspace/root/sibling");
+ });
+
+ it("getEffectiveCwd: default workspace (no defaultCwd) falls through to serverDefaultCwd", async () => {
+ const store = makeStore("/server/default");
+ // No explicit workspace assignment — defaults to "default" workspace
+ // which has defaultCwd null.
+ expect(await store.getEffectiveCwd("conv1")).toBe("/server/default");
+ });
+
+ // --- overrideCwd (per-turn cwd override) ---
+
+ it("getEffectiveCwd: overrideCwd absolute (starts with /) returned as-is, overriding workspace defaultCwd", async () => {
+ const store = makeStore("/server/default");
+ await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/default" });
+ await store.setWorkspaceId("conv1", "my-work");
+ // An absolute override wins outright, even over a workspace defaultCwd.
+ expect(await store.getEffectiveCwd("conv1", "/override/abs")).toBe("/override/abs");
+ });
+
+ it("getEffectiveCwd: overrideCwd relative resolved against workspace defaultCwd", async () => {
+ const store = makeStore("/server/default");
+ await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/root" });
+ await store.setWorkspaceId("conv1", "my-work");
+ expect(await store.getEffectiveCwd("conv1", "subdir")).toBe("/workspace/root/subdir");
+ });
+
+ it("getEffectiveCwd: overrideCwd relative resolved against serverDefaultCwd when workspace defaultCwd is null", async () => {
+ const store = makeStore("/server/default");
+ await store.ensureWorkspace("my-work");
+ await store.setWorkspaceId("conv1", "my-work");
+ expect(await store.getEffectiveCwd("conv1", "subdir")).toBe("/server/default/subdir");
+ });
+
+ it("getEffectiveCwd: overrideCwd does NOT read the persisted getCwd (override wins over persisted)", async () => {
+ const store = makeStore("/server/default");
+ await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/root" });
+ await store.setWorkspaceId("conv1", "my-work");
+ // Persist a cwd that differs from the override — the override must win.
+ await store.setCwd("conv1", "/persisted/path");
+ expect(await store.getEffectiveCwd("conv1", "override-rel")).toBe(
+ "/workspace/root/override-rel",
+ );
+ });
+
+ it("getEffectiveCwd: overrideCwd omitted behaves as today (uses persisted cwd)", async () => {
+ const store = makeStore("/server/default");
+ await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/root" });
+ await store.setWorkspaceId("conv1", "my-work");
+ await store.setCwd("conv1", "persisted-rel");
+ // No second arg — persisted cwd is used.
+ expect(await store.getEffectiveCwd("conv1")).toBe("/workspace/root/persisted-rel");
+ });
+
+ it("clearCwd → getEffectiveCwd falls through to workspace defaultCwd (un-shadows it)", async () => {
+ const store = makeStore("/server/default");
+ await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/default" });
+ await store.setWorkspaceId("conv1", "my-work");
+ await store.setCwd("conv1", "/explicit/path");
+ // Before clear: the conversation cwd shadows the workspace defaultCwd.
+ expect(await store.getEffectiveCwd("conv1")).toBe("/explicit/path");
+ // After clear: the workspace defaultCwd is used (fall-through).
+ await store.clearCwd("conv1");
+ expect(await store.getEffectiveCwd("conv1")).toBe("/workspace/default");
+ });
+
+ it("getEffectiveCwd: an empty-string cwd does NOT fall through (proving clear ≠ setCwd(''))", async () => {
+ const store = makeStore("/server/default");
+ await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/default" });
+ await store.setWorkspaceId("conv1", "my-work");
+ // An empty string is a non-null explicit cwd — it is resolved (not
+ // treated as absent), so it does NOT fall through to the workspace
+ // defaultCwd. This is the gap clearCwd fixes.
+ await store.setCwd("conv1", "");
+ expect(await store.getCwd("conv1")).toBe("");
+ // path.resolve("/workspace/default", "") === "/workspace/default" —
+ // but this is a RELATIVE cwd resolution, not a fall-through. The point
+ // is that getCwd returns "" (not null), so the relative branch runs.
+ // With a clearCwd, getCwd returns null and the fall-through branch runs.
+ await store.clearCwd("conv1");
+ expect(await store.getCwd("conv1")).toBeNull();
+ expect(await store.getEffectiveCwd("conv1")).toBe("/workspace/default");
+ });
+
+ it("listConversations filtered by workspaceId", async () => {
+ const store = makeStore();
+ await store.ensureWorkspace("work-a");
+ await store.ensureWorkspace("work-b");
+ await store.append("a1", [userMessage("a1")]);
+ await store.append("a2", [userMessage("a2")]);
+ await store.append("b1", [userMessage("b1")]);
+ await store.setWorkspaceId("a1", "work-a");
+ await store.setWorkspaceId("a2", "work-a");
+ await store.setWorkspaceId("b1", "work-b");
+
+ const aConvs = await store.listConversations({ workspaceId: "work-a" });
+ expect(aConvs.map((c) => c.id).sort()).toEqual(["a1", "a2"]);
+
+ const bConvs = await store.listConversations({ workspaceId: "work-b" });
+ expect(bConvs.map((c) => c.id)).toEqual(["b1"]);
+ });
+
+ it("append updates workspace lastActivityAt", async () => {
+ const store = makeStore();
+ clock = 1000;
+ await store.ensureWorkspace("my-work");
+ clock = 2000;
+ await store.setWorkspaceId("conv1", "my-work");
+ clock = 3000;
+ await store.append("conv1", [userMessage("hi")]);
+ const ws = await store.getWorkspace("my-work");
+ expect(ws?.lastActivityAt).toBe(3000);
+ });
+
+ it("forkHistory copies workspaceId", async () => {
+ const store = makeStore();
+ await store.ensureWorkspace("my-work");
+ await store.setWorkspaceId("source", "my-work");
+ await store.append("source", [userMessage("hello")]);
+ await store.forkHistory("source", "target");
+ const targetMeta = await store.getConversationMeta("target");
+ expect(targetMeta?.workspaceId).toBe("my-work");
+ });
+
+ it("replaceHistory preserves workspaceId", async () => {
+ const store = makeStore();
+ await store.ensureWorkspace("my-work");
+ await store.setWorkspaceId("conv1", "my-work");
+ await store.append("conv1", [userMessage("original")]);
+ await store.replaceHistory("conv1", [userMessage("replaced")]);
+ const meta = await store.getConversationMeta("conv1");
+ expect(meta?.workspaceId).toBe("my-work");
+ });
+
+ // --- starred (priority for concurrency limiting) ---
+
+ it("ensureWorkspace creates with starred: false", async () => {
+ const store = makeStore();
+ const ws = await store.ensureWorkspace("star-work");
+ expect(ws.starred).toBe(false);
+ });
+
+ it("setWorkspaceStarred true persists and reads back", async () => {
+ const store = makeStore();
+ clock = 1000;
+ await store.ensureWorkspace("star-work");
+ clock = 2000;
+ const ws = await store.setWorkspaceStarred("star-work", true);
+ expect(ws.starred).toBe(true);
+ expect(ws.id).toBe("star-work");
+ const reRead = await store.getWorkspace("star-work");
+ expect(reRead?.starred).toBe(true);
+ });
+
+ it("setWorkspaceStarred false unstars a previously starred workspace", async () => {
+ const store = makeStore();
+ await store.ensureWorkspace("star-work");
+ await store.setWorkspaceStarred("star-work", true);
+ await store.setWorkspaceStarred("star-work", false);
+ const ws = await store.getWorkspace("star-work");
+ expect(ws?.starred).toBe(false);
+ });
+
+ it("setWorkspaceStarred creates the workspace if missing", async () => {
+ const store = makeStore();
+ clock = 5000;
+ const ws = await store.setWorkspaceStarred("brand-new", true);
+ expect(ws).toEqual({
+ id: "brand-new",
+ title: "brand-new",
+ defaultCwd: null,
+ defaultComputerId: null,
+ starred: true,
+ createdAt: 5000,
+ lastActivityAt: 5000,
+ });
+ });
+
+ it("setWorkspaceStarred preserves title/defaultCwd/defaultComputerId", async () => {
+ const store = makeStore();
+ clock = 1000;
+ await store.ensureWorkspace("my-work", {
+ title: "Custom",
+ defaultCwd: "/projects",
+ defaultComputerId: "myserver",
+ });
+ clock = 2000;
+ const ws = await store.setWorkspaceStarred("my-work", true);
+ expect(ws.title).toBe("Custom");
+ expect(ws.defaultCwd).toBe("/projects");
+ expect(ws.defaultComputerId).toBe("myserver");
+ expect(ws.starred).toBe(true);
+ expect(ws.createdAt).toBe(1000);
+ });
+
+ it("listWorkspaces includes starred field", async () => {
+ const store = makeStore();
+ clock = 1000;
+ await store.ensureWorkspace("alpha");
+ await store.setWorkspaceStarred("alpha", true);
+ clock = 2000;
+ await store.ensureWorkspace("beta");
+ const list = await store.listWorkspaces();
+ const alpha = list.find((w) => w.id === "alpha");
+ expect(alpha?.starred).toBe(true);
+ const beta = list.find((w) => w.id === "beta");
+ expect(beta?.starred).toBe(false);
+ });
+
+ it("setWorkspaceTitle preserves starred state", async () => {
+ const store = makeStore();
+ await store.ensureWorkspace("my-work");
+ await store.setWorkspaceStarred("my-work", true);
+ const ws = await store.setWorkspaceTitle("my-work", "Renamed");
+ expect(ws.starred).toBe(true);
+ expect(ws.title).toBe("Renamed");
+ });
+
+ it("setWorkspaceDefaultCwd preserves starred state", async () => {
+ const store = makeStore();
+ await store.ensureWorkspace("my-work");
+ await store.setWorkspaceStarred("my-work", true);
+ const ws = await store.setWorkspaceDefaultCwd("my-work", "/new/path");
+ expect(ws.starred).toBe(true);
+ expect(ws.defaultCwd).toBe("/new/path");
+ });
+
+ it("setWorkspaceDefaultComputerId preserves starred state", async () => {
+ const store = makeStore();
+ await store.ensureWorkspace("my-work");
+ await store.setWorkspaceStarred("my-work", true);
+ const ws = await store.setWorkspaceDefaultComputerId("my-work", "new-host");
+ expect(ws.starred).toBe(true);
+ expect(ws.defaultComputerId).toBe("new-host");
+ });
+
+ it("a legacy WorkspaceRow without starred reads back as false", async () => {
+ const store = makeStore();
+ // Simulate a legacy row persisted before `starred` existed.
+ await storage.set(
+ "workspace:legacy",
+ JSON.stringify({
+ title: "legacy",
+ defaultCwd: "/legacy/cwd",
+ defaultComputerId: null,
+ createdAt: 100,
+ lastActivityAt: 200,
+ }),
+ );
+ const ws = await store.getWorkspace("legacy");
+ expect(ws?.starred).toBe(false);
+ });
});
describe("ComputerStore", () => {
- let storage: StorageNamespace;
- let clock: number;
-
- beforeEach(() => {
- storage = createMemoryStorage();
- clock = 1000;
- });
-
- function makeStore() {
- return createConversationStore(storage, undefined, () => clock);
- }
-
- // --- per-conversation computerId (mirror getCwd/setCwd/clearCwd) ---
-
- it("setComputerId/getComputerId round-trips an alias", async () => {
- const store = makeStore();
- expect(await store.getComputerId("conv1")).toBeNull();
- await store.setComputerId("conv1", "myserver");
- expect(await store.getComputerId("conv1")).toBe("myserver");
- });
-
- it("setComputerId(null) clears (is idempotent local sentinel, like clearComputerId)", async () => {
- const store = makeStore();
- await store.setComputerId("conv1", "myserver");
- expect(await store.getComputerId("conv1")).toBe("myserver");
- // null is the "local" sentinel: it clears the persisted key so it does
- // NOT linger to shadow the workspace defaultComputerId.
- await store.setComputerId("conv1", null);
- expect(await store.getComputerId("conv1")).toBeNull();
- // idempotent — clearing an already-absent key is a no-op.
- await store.setComputerId("conv1", null);
- expect(await store.getComputerId("conv1")).toBeNull();
- });
-
- it("clearComputerId is idempotent and un-shadows the workspace default", async () => {
- const store = makeStore();
- await store.ensureWorkspace("my-work", { defaultComputerId: "ws-host" });
- await store.setWorkspaceId("conv1", "my-work");
- await store.setComputerId("conv1", "per-conv-host");
- expect(await store.getEffectiveComputer("conv1")).toBe("per-conv-host");
- // After clear: the workspace defaultComputerId is used (fall-through).
- await store.clearComputerId("conv1");
- expect(await store.getComputerId("conv1")).toBeNull();
- expect(await store.getEffectiveComputer("conv1")).toBe("ws-host");
- // idempotent — deleting an already-absent key is a no-op.
- await store.clearComputerId("conv1");
- expect(await store.getComputerId("conv1")).toBeNull();
- });
-
- // --- setWorkspaceDefaultComputerId (mirror setWorkspaceDefaultCwd) ---
-
- it("setWorkspaceDefaultComputerId sets and clears", async () => {
- const store = makeStore();
- clock = 1000;
- await store.ensureWorkspace("my-work");
- clock = 2000;
- const setWs = await store.setWorkspaceDefaultComputerId("my-work", "remote-host");
- expect(setWs.defaultComputerId).toBe("remote-host");
- // does not bump lastActivityAt on defaultComputerId change (mirrors defaultCwd).
- expect(setWs.lastActivityAt).toBe(1000);
- const cleared = await store.setWorkspaceDefaultComputerId("my-work", null);
- expect(cleared.defaultComputerId).toBeNull();
- });
-
- it("setWorkspaceDefaultComputerId creates the workspace if missing", async () => {
- const store = makeStore();
- clock = 5000;
- const ws = await store.setWorkspaceDefaultComputerId("brand-new", "remote-host");
- expect(ws).toEqual({
- id: "brand-new",
- title: "brand-new",
- defaultCwd: null,
- defaultComputerId: "remote-host",
- createdAt: 5000,
- lastActivityAt: 5000,
- });
- });
-
- it("setWorkspaceDefaultComputerId preserves defaultCwd on an existing workspace", async () => {
- const store = makeStore();
- clock = 1000;
- await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/root" });
- clock = 2000;
- const ws = await store.setWorkspaceDefaultComputerId("my-work", "remote-host");
- expect(ws.defaultCwd).toBe("/workspace/root");
- expect(ws.defaultComputerId).toBe("remote-host");
- });
-
- it("the synthesized 'default' workspace still returns defaultComputerId: null (local)", async () => {
- const store = makeStore();
- const ws = await store.getWorkspace("default");
- expect(ws).toEqual({
- id: "default",
- title: "default",
- defaultCwd: null,
- defaultComputerId: null,
- createdAt: 0,
- lastActivityAt: 0,
- });
- // And it surfaces null in listWorkspaces too.
- const list = await store.listWorkspaces();
- const defaultWs = list.find((w) => w.id === "default");
- expect(defaultWs?.defaultComputerId).toBeNull();
- });
-
- // --- getEffectiveComputer resolution ladder (mirror getEffectiveCwd) ---
-
- it("getEffectiveComputer: per-conversation computerId overrides workspace defaultComputerId", async () => {
- const store = makeStore();
- await store.ensureWorkspace("my-work", { defaultComputerId: "ws-host" });
- await store.setWorkspaceId("conv1", "my-work");
- await store.setComputerId("conv1", "per-conv-host");
- expect(await store.getEffectiveComputer("conv1")).toBe("per-conv-host");
- });
-
- it("getEffectiveComputer: workspace defaultComputerId used when conversation computerId is unset", async () => {
- const store = makeStore();
- await store.ensureWorkspace("my-work", { defaultComputerId: "ws-host" });
- await store.setWorkspaceId("conv1", "my-work");
- expect(await store.getEffectiveComputer("conv1")).toBe("ws-host");
- });
-
- it("getEffectiveComputer: null (LOCAL) when both conversation and workspace computerId are unset", async () => {
- const store = makeStore();
- await store.ensureWorkspace("my-work");
- await store.setWorkspaceId("conv1", "my-work");
- expect(await store.getEffectiveComputer("conv1")).toBeNull();
- });
-
- it("getEffectiveComputer: default workspace (no defaultComputerId) falls through to null (local)", async () => {
- const store = makeStore();
- // No explicit workspace assignment — defaults to "default" workspace
- // which has defaultComputerId null.
- expect(await store.getEffectiveComputer("conv1")).toBeNull();
- });
-
- it("getEffectiveComputer: clearComputerId falls through to workspace defaultComputerId (un-shadows it)", async () => {
- const store = makeStore();
- await store.ensureWorkspace("my-work", { defaultComputerId: "ws-host" });
- await store.setWorkspaceId("conv1", "my-work");
- await store.setComputerId("conv1", "per-conv-host");
- // Before clear: the conversation computerId shadows the workspace default.
- expect(await store.getEffectiveComputer("conv1")).toBe("per-conv-host");
- // After clear: the workspace defaultComputerId is used (fall-through).
- await store.clearComputerId("conv1");
- expect(await store.getEffectiveComputer("conv1")).toBe("ws-host");
- });
-
- // --- overrideAlias (per-turn computer override, mirror overrideCwd) ---
-
- it("getEffectiveComputer: overrideAlias string wins outright, overriding workspace defaultComputerId", async () => {
- const store = makeStore();
- await store.ensureWorkspace("my-work", { defaultComputerId: "ws-host" });
- await store.setWorkspaceId("conv1", "my-work");
- // A string override wins outright, even over a workspace defaultComputerId.
- expect(await store.getEffectiveComputer("conv1", "override-host")).toBe("override-host");
- });
-
- it("getEffectiveComputer: overrideAlias string wins over the persisted per-conversation computerId", async () => {
- const store = makeStore();
- await store.ensureWorkspace("my-work", { defaultComputerId: "ws-host" });
- await store.setWorkspaceId("conv1", "my-work");
- await store.setComputerId("conv1", "persisted-host");
- // The override must win over the persisted computerId.
- expect(await store.getEffectiveComputer("conv1", "override-host")).toBe("override-host");
- });
-
- it("getEffectiveComputer: overrideAlias null is explicitly local and does NOT fall through", async () => {
- const store = makeStore();
- await store.ensureWorkspace("my-work", { defaultComputerId: "ws-host" });
- await store.setWorkspaceId("conv1", "my-work");
- await store.setComputerId("conv1", "persisted-host");
- // An explicit null override = "local for this turn": it wins outright and
- // does NOT fall through to the persisted value or the workspace default.
- expect(await store.getEffectiveComputer("conv1", null)).toBeNull();
- });
-
- it("getEffectiveComputer: overrideAlias omitted behaves as today (uses persisted computerId)", async () => {
- const store = makeStore();
- await store.ensureWorkspace("my-work", { defaultComputerId: "ws-host" });
- await store.setWorkspaceId("conv1", "my-work");
- await store.setComputerId("conv1", "persisted-host");
- // No second arg — persisted computerId is used.
- expect(await store.getEffectiveComputer("conv1")).toBe("persisted-host");
- });
-
- // --- round-trip through persistence (parse/toWorkspace) ---
-
- it("a Workspace with defaultComputerId round-trips through parse/toWorkspace", async () => {
- const store = makeStore();
- clock = 1000;
- // Create with a defaultComputerId via ensureWorkspace, then read it back
- // (exercises parseWorkspaceRow -> toWorkspace round-trip).
- const created = await store.ensureWorkspace("remote-work", {
- title: "Remote",
- defaultComputerId: "prod-server",
- });
- expect(created.defaultComputerId).toBe("prod-server");
- const roundTripped = await store.getWorkspace("remote-work");
- expect(roundTripped).toEqual({
- id: "remote-work",
- title: "Remote",
- defaultCwd: null,
- defaultComputerId: "prod-server",
- createdAt: 1000,
- lastActivityAt: 1000,
- });
- });
-
- it("a legacy WorkspaceRow without defaultComputerId reads back as null (local)", async () => {
- const store = makeStore();
- // Simulate a legacy row persisted before defaultComputerId existed:
- // write a raw WorkspaceRow JSON lacking the field, then read it back.
- await storage.set(
- "workspace:legacy",
- JSON.stringify({
- title: "legacy",
- defaultCwd: "/legacy/cwd",
- createdAt: 100,
- lastActivityAt: 200,
- }),
- );
- const ws = await store.getWorkspace("legacy");
- expect(ws).toEqual({
- id: "legacy",
- title: "legacy",
- defaultCwd: "/legacy/cwd",
- defaultComputerId: null,
- createdAt: 100,
- lastActivityAt: 200,
- });
- });
+ let storage: StorageNamespace;
+ let clock: number;
+
+ beforeEach(() => {
+ storage = createMemoryStorage();
+ clock = 1000;
+ });
+
+ function makeStore() {
+ return createConversationStore(storage, undefined, () => clock);
+ }
+
+ // --- per-conversation computerId (mirror getCwd/setCwd/clearCwd) ---
+
+ it("setComputerId/getComputerId round-trips an alias", async () => {
+ const store = makeStore();
+ expect(await store.getComputerId("conv1")).toBeNull();
+ await store.setComputerId("conv1", "myserver");
+ expect(await store.getComputerId("conv1")).toBe("myserver");
+ });
+
+ it("setComputerId(null) clears (is idempotent local sentinel, like clearComputerId)", async () => {
+ const store = makeStore();
+ await store.setComputerId("conv1", "myserver");
+ expect(await store.getComputerId("conv1")).toBe("myserver");
+ // null is the "local" sentinel: it clears the persisted key so it does
+ // NOT linger to shadow the workspace defaultComputerId.
+ await store.setComputerId("conv1", null);
+ expect(await store.getComputerId("conv1")).toBeNull();
+ // idempotent — clearing an already-absent key is a no-op.
+ await store.setComputerId("conv1", null);
+ expect(await store.getComputerId("conv1")).toBeNull();
+ });
+
+ it("clearComputerId is idempotent and un-shadows the workspace default", async () => {
+ const store = makeStore();
+ await store.ensureWorkspace("my-work", { defaultComputerId: "ws-host" });
+ await store.setWorkspaceId("conv1", "my-work");
+ await store.setComputerId("conv1", "per-conv-host");
+ expect(await store.getEffectiveComputer("conv1")).toBe("per-conv-host");
+ // After clear: the workspace defaultComputerId is used (fall-through).
+ await store.clearComputerId("conv1");
+ expect(await store.getComputerId("conv1")).toBeNull();
+ expect(await store.getEffectiveComputer("conv1")).toBe("ws-host");
+ // idempotent — deleting an already-absent key is a no-op.
+ await store.clearComputerId("conv1");
+ expect(await store.getComputerId("conv1")).toBeNull();
+ });
+
+ // --- setWorkspaceDefaultComputerId (mirror setWorkspaceDefaultCwd) ---
+
+ it("setWorkspaceDefaultComputerId sets and clears", async () => {
+ const store = makeStore();
+ clock = 1000;
+ await store.ensureWorkspace("my-work");
+ clock = 2000;
+ const setWs = await store.setWorkspaceDefaultComputerId("my-work", "remote-host");
+ expect(setWs.defaultComputerId).toBe("remote-host");
+ // does not bump lastActivityAt on defaultComputerId change (mirrors defaultCwd).
+ expect(setWs.lastActivityAt).toBe(1000);
+ const cleared = await store.setWorkspaceDefaultComputerId("my-work", null);
+ expect(cleared.defaultComputerId).toBeNull();
+ });
+
+ it("setWorkspaceDefaultComputerId creates the workspace if missing", async () => {
+ const store = makeStore();
+ clock = 5000;
+ const ws = await store.setWorkspaceDefaultComputerId("brand-new", "remote-host");
+ expect(ws).toEqual({
+ id: "brand-new",
+ title: "brand-new",
+ defaultCwd: null,
+ defaultComputerId: "remote-host",
+ starred: false,
+ createdAt: 5000,
+ lastActivityAt: 5000,
+ });
+ });
+
+ it("setWorkspaceDefaultComputerId preserves defaultCwd on an existing workspace", async () => {
+ const store = makeStore();
+ clock = 1000;
+ await store.ensureWorkspace("my-work", { defaultCwd: "/workspace/root" });
+ clock = 2000;
+ const ws = await store.setWorkspaceDefaultComputerId("my-work", "remote-host");
+ expect(ws.defaultCwd).toBe("/workspace/root");
+ expect(ws.defaultComputerId).toBe("remote-host");
+ });
+
+ it("the synthesized 'default' workspace still returns defaultComputerId: null (local)", async () => {
+ const store = makeStore();
+ const ws = await store.getWorkspace("default");
+ expect(ws).toEqual({
+ id: "default",
+ title: "default",
+ defaultCwd: null,
+ defaultComputerId: null,
+ starred: false,
+ createdAt: 0,
+ lastActivityAt: 0,
+ });
+ // And it surfaces null in listWorkspaces too.
+ const list = await store.listWorkspaces();
+ const defaultWs = list.find((w) => w.id === "default");
+ expect(defaultWs?.defaultComputerId).toBeNull();
+ });
+
+ // --- getEffectiveComputer resolution ladder (mirror getEffectiveCwd) ---
+
+ it("getEffectiveComputer: per-conversation computerId overrides workspace defaultComputerId", async () => {
+ const store = makeStore();
+ await store.ensureWorkspace("my-work", { defaultComputerId: "ws-host" });
+ await store.setWorkspaceId("conv1", "my-work");
+ await store.setComputerId("conv1", "per-conv-host");
+ expect(await store.getEffectiveComputer("conv1")).toBe("per-conv-host");
+ });
+
+ it("getEffectiveComputer: workspace defaultComputerId used when conversation computerId is unset", async () => {
+ const store = makeStore();
+ await store.ensureWorkspace("my-work", { defaultComputerId: "ws-host" });
+ await store.setWorkspaceId("conv1", "my-work");
+ expect(await store.getEffectiveComputer("conv1")).toBe("ws-host");
+ });
+
+ it("getEffectiveComputer: null (LOCAL) when both conversation and workspace computerId are unset", async () => {
+ const store = makeStore();
+ await store.ensureWorkspace("my-work");
+ await store.setWorkspaceId("conv1", "my-work");
+ expect(await store.getEffectiveComputer("conv1")).toBeNull();
+ });
+
+ it("getEffectiveComputer: default workspace (no defaultComputerId) falls through to null (local)", async () => {
+ const store = makeStore();
+ // No explicit workspace assignment — defaults to "default" workspace
+ // which has defaultComputerId null.
+ expect(await store.getEffectiveComputer("conv1")).toBeNull();
+ });
+
+ it("getEffectiveComputer: clearComputerId falls through to workspace defaultComputerId (un-shadows it)", async () => {
+ const store = makeStore();
+ await store.ensureWorkspace("my-work", { defaultComputerId: "ws-host" });
+ await store.setWorkspaceId("conv1", "my-work");
+ await store.setComputerId("conv1", "per-conv-host");
+ // Before clear: the conversation computerId shadows the workspace default.
+ expect(await store.getEffectiveComputer("conv1")).toBe("per-conv-host");
+ // After clear: the workspace defaultComputerId is used (fall-through).
+ await store.clearComputerId("conv1");
+ expect(await store.getEffectiveComputer("conv1")).toBe("ws-host");
+ });
+
+ // --- overrideAlias (per-turn computer override, mirror overrideCwd) ---
+
+ it("getEffectiveComputer: overrideAlias string wins outright, overriding workspace defaultComputerId", async () => {
+ const store = makeStore();
+ await store.ensureWorkspace("my-work", { defaultComputerId: "ws-host" });
+ await store.setWorkspaceId("conv1", "my-work");
+ // A string override wins outright, even over a workspace defaultComputerId.
+ expect(await store.getEffectiveComputer("conv1", "override-host")).toBe("override-host");
+ });
+
+ it("getEffectiveComputer: overrideAlias string wins over the persisted per-conversation computerId", async () => {
+ const store = makeStore();
+ await store.ensureWorkspace("my-work", { defaultComputerId: "ws-host" });
+ await store.setWorkspaceId("conv1", "my-work");
+ await store.setComputerId("conv1", "persisted-host");
+ // The override must win over the persisted computerId.
+ expect(await store.getEffectiveComputer("conv1", "override-host")).toBe("override-host");
+ });
+
+ it("getEffectiveComputer: overrideAlias null is explicitly local and does NOT fall through", async () => {
+ const store = makeStore();
+ await store.ensureWorkspace("my-work", { defaultComputerId: "ws-host" });
+ await store.setWorkspaceId("conv1", "my-work");
+ await store.setComputerId("conv1", "persisted-host");
+ // An explicit null override = "local for this turn": it wins outright and
+ // does NOT fall through to the persisted value or the workspace default.
+ expect(await store.getEffectiveComputer("conv1", null)).toBeNull();
+ });
+
+ it("getEffectiveComputer: overrideAlias omitted behaves as today (uses persisted computerId)", async () => {
+ const store = makeStore();
+ await store.ensureWorkspace("my-work", { defaultComputerId: "ws-host" });
+ await store.setWorkspaceId("conv1", "my-work");
+ await store.setComputerId("conv1", "persisted-host");
+ // No second arg — persisted computerId is used.
+ expect(await store.getEffectiveComputer("conv1")).toBe("persisted-host");
+ });
+
+ // --- round-trip through persistence (parse/toWorkspace) ---
+
+ it("a Workspace with defaultComputerId round-trips through parse/toWorkspace", async () => {
+ const store = makeStore();
+ clock = 1000;
+ // Create with a defaultComputerId via ensureWorkspace, then read it back
+ // (exercises parseWorkspaceRow -> toWorkspace round-trip).
+ const created = await store.ensureWorkspace("remote-work", {
+ title: "Remote",
+ defaultComputerId: "prod-server",
+ });
+ expect(created.defaultComputerId).toBe("prod-server");
+ const roundTripped = await store.getWorkspace("remote-work");
+ expect(roundTripped).toEqual({
+ id: "remote-work",
+ title: "Remote",
+ defaultCwd: null,
+ defaultComputerId: "prod-server",
+ starred: false,
+ createdAt: 1000,
+ lastActivityAt: 1000,
+ });
+ });
+
+ it("a legacy WorkspaceRow without defaultComputerId reads back as null (local)", async () => {
+ const store = makeStore();
+ // Simulate a legacy row persisted before defaultComputerId existed:
+ // write a raw WorkspaceRow JSON lacking the field, then read it back.
+ await storage.set(
+ "workspace:legacy",
+ JSON.stringify({
+ title: "legacy",
+ defaultCwd: "/legacy/cwd",
+ createdAt: 100,
+ lastActivityAt: 200,
+ }),
+ );
+ const ws = await store.getWorkspace("legacy");
+ expect(ws).toEqual({
+ id: "legacy",
+ title: "legacy",
+ defaultCwd: "/legacy/cwd",
+ defaultComputerId: null,
+ starred: false,
+ createdAt: 100,
+ lastActivityAt: 200,
+ });
+ });
});
describe("isValidWorkspaceSlug", () => {
- it("accepts valid slugs", () => {
- expect(isValidWorkspaceSlug("my-work")).toBe(true);
- expect(isValidWorkspaceSlug("default")).toBe(true);
- expect(isValidWorkspaceSlug("a1b2")).toBe(true);
- });
-
- it("rejects invalid slugs", () => {
- expect(isValidWorkspaceSlug("My-Work")).toBe(false);
- expect(isValidWorkspaceSlug("-leading")).toBe(false);
- expect(isValidWorkspaceSlug("trailing-")).toBe(false);
- expect(isValidWorkspaceSlug("")).toBe(false);
- expect(isValidWorkspaceSlug("a".repeat(41))).toBe(false);
- expect(isValidWorkspaceSlug("has space")).toBe(false);
- });
+ it("accepts valid slugs", () => {
+ expect(isValidWorkspaceSlug("my-work")).toBe(true);
+ expect(isValidWorkspaceSlug("default")).toBe(true);
+ expect(isValidWorkspaceSlug("a1b2")).toBe(true);
+ });
+
+ it("rejects invalid slugs", () => {
+ expect(isValidWorkspaceSlug("My-Work")).toBe(false);
+ expect(isValidWorkspaceSlug("-leading")).toBe(false);
+ expect(isValidWorkspaceSlug("trailing-")).toBe(false);
+ expect(isValidWorkspaceSlug("")).toBe(false);
+ expect(isValidWorkspaceSlug("a".repeat(41))).toBe(false);
+ expect(isValidWorkspaceSlug("has space")).toBe(false);
+ });
});