import { assertEquals, assertRejects } from "@std/assert"; import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context"; import { localEnvironment } from "@berry/api/environment.ts"; import { projectFor, type ProjectIndex } from "@berry/api/projects.ts"; import { homedir } from "node:os"; import { join } from "node:path"; import { type Environments, localOnly } from "./environment.ts"; import { Projects } from "./projects.ts"; import { setup } from "./fixture.ts"; import { BerryError } from "./berry.ts"; import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai/providers/faux"; import { summaries, type Summary } from "./summaries.ts"; import { tempDir } from "@berry/util"; Deno.test("projects match workspace boundaries and the most specific root on the right machine", () => { const projects: ProjectIndex = { berry: { name: "Berry", defaultRoot: 0, roots: [ { environment: "local", cwd: "/work/berry", platform: "linux" }, { environment: "local", cwd: "/workspaces/feature", platform: "linux" }, { environment: "windows", cwd: "C:\\Work\\Berry", platform: "win32" }, { environment: "local", cwd: "/work/berry:odd", platform: "linux" }, ], }, nested: { name: "Nested", defaultRoot: 0, roots: [{ environment: "local", cwd: "/work/berry/tools/", platform: "linux" }], }, }; for ( const [environment, cwd, expected] of [ ["local", "/work/berry", "berry"], ["local", "/work/berry/src", "berry"], ["local", "/workspaces/feature/src", "berry"], ["local", "/work/berry-old", null], ["other", "/work/berry", null], ["local", "/work/berry/tools/parser", "nested"], ["windows", "c:/WORK/berry/src", "berry"], ["windows", "C:\\Work\\Berry-old", null], ["local", "/work/berry:odd/src", "berry"], ["local", "/Work/Berry", null], ] as const ) assertEquals(projectFor(projects, { environment, cwd }), expected); assertEquals(projectFor(projects, undefined), null); }); Deno.test("project workspaces validate and canonicalize without changing any conversation", async () => { await using rootDir = await tempDir(); const root = rootDir.path; const main = join(root, "main"); const workspace = join(root, "workspace"); await Deno.mkdir(main); await Deno.mkdir(workspace); await Deno.writeTextFile(join(root, "file"), ""); await Deno.symlink(main, join(root, "link")); let offline = false; const environments: Environments = { open(name, cwd, during) { if (name !== "other") return localOnly.open(name, cwd, during); if (offline) return Promise.reject(new Error("offline")); return Promise.resolve(Object.assign(localEnvironment(cwd ?? root), { name, home: root })); }, close: () => Promise.resolve(), }; await using harness = await setup(environments); const { berry } = harness; const tid = await berry.create({ extensions: { environment: { cwd: main } } }); const binding = await berry.state(tid, "environment"); const id = await berry.saveProject(undefined, { name: " Berry ", defaultRoot: 1, roots: [ { environment: "local", cwd: join(root, "link") }, { environment: "other", cwd: "workspace" }, ], }); const project = (await berry.harness.snapshot(Projects, context))![id]; assertEquals(project.name, "Berry"); assertEquals(project.roots.map(({ environment, cwd }) => ({ environment, cwd })), [ { environment: "local", cwd: await Deno.realPath(main) }, { environment: "other", cwd: await Deno.realPath(workspace) }, ]); assertEquals(project.defaultRoot, 1); for (const cwd of [join(root, "file"), join(root, "missing"), main]) { await assertRejects(() => berry.saveProject(undefined, { name: "invalid", defaultRoot: 0, roots: [{ environment: "local", cwd }], }) ); } await assertRejects(() => berry.saveProject(undefined, { name: "invalid", roots: [], defaultRoot: 0 }) ); await assertRejects(() => berry.saveProject(undefined, { name: "invalid", roots: [{ environment: "local", cwd: workspace }], defaultRoot: 2, }) ); await assertRejects(() => berry.saveProject(undefined, { name: "invalid", roots: [{ environment: "local", cwd: workspace }, { environment: "local", cwd: workspace }], defaultRoot: 0, }) ); offline = true; await berry.saveProject(id, { ...project, name: "Renamed" }); assertEquals(await berry.state(tid, "environment"), binding); await berry.deleteProject(id); assertEquals(await berry.harness.snapshot(Projects, context), {}); assertEquals(await berry.state(tid, "environment"), binding); }); Deno.test("live sidebar bindings follow cd, independently moved forks, branch switches, and unreachable machines", async () => { await using rootDir = await tempDir(); const root = rootDir.path; await Deno.mkdir(join(root, "a")); await Deno.mkdir(join(root, "b")); await using harness = await setup(); const { berry } = harness; let watched: Awaited> | undefined; try { const tid = await berry.create({ extensions: { environment: { cwd: join(root, "a") } } }); const misc = await berry.create({ extensions: {} }); const conversation = (await berry.conversation(tid))!; const point = (await conversation.context(context)).entries.at(-1)!.id; const fork = await conversation.fork(point, { ownership: { kind: "ownerless" } }, context); const forkTid = await berry.tidOf(fork.id); watched = await summaries(berry); const feed = watched.feed(); let current = feed.value as Record; let wake = () => {}; await feed.start((value) => { current = value as Record; wake(); return Promise.resolve(); }); const until = async (id: string, cwd: string) => { while (current[id]?.cwd !== cwd) await new Promise((resolve) => wake = resolve); }; assertEquals(current[misc].cwd, homedir()); await berry.command(forkTid, "cd", join(root, "b")); await until(forkTid, join(root, "b")); assertEquals(current[tid].cwd, join(root, "a")); await berry.branch(forkTid, point); await until(forkTid, join(root, "a")); await (await berry.emit(forkTid, "environment", { type: "bind", environment: "unreachable", cwd: "/work", })).wait(context); await until(forkTid, "/work"); assertEquals(current[forkTid].environment, "unreachable"); assertEquals(current[tid].environment, "local"); await feed.stop(); } finally { watched?.stop(); } }); Deno.test("removing a workspace leaves its conversations in place unless moving is requested", async () => { await using root = await tempDir(); const main = join(root.path, "main"); const work = join(root.path, "work"); await Deno.mkdir(main); await Deno.mkdir(work); await using harness = await setup(); const { berry } = harness; const id = await berry.saveProject(undefined, { name: "Project", roots: [{ environment: "local", cwd: main }, { environment: "local", cwd: work }], defaultRoot: 0, }); const tid = await berry.create({ extensions: { environment: { cwd: work } } }); await berry.saveProject(id, { name: "Project", roots: [{ environment: "local", cwd: main }], defaultRoot: 0, }); assertEquals(await berry.state(tid, "environment"), { environment: "local", cwd: work }); }); Deno.test("workspace removal moves current conversations and descendants, respecting nested workspaces and projects", async () => { await using root = await tempDir(); const main = join(root.path, "main"); const work = join(root.path, "work"); const second = join(root.path, "second"); const child = join(work, "src"); const retained = join(work, "retained"); const nested = join(work, "nested"); const sibling = join(root.path, "work-old"); for (const path of [main, child, retained, nested, sibling, second]) { await Deno.mkdir(path, { recursive: true }); } let offline = false; const environments: Environments = { open(name, cwd, during) { if (name !== "destination") return localOnly.open(name, cwd, during); if (offline) return Promise.reject(new Error("offline")); return Promise.resolve(Object.assign(localEnvironment(cwd ?? main), { name })); }, close: () => Promise.resolve(), }; await using harness = await setup(environments); const { berry } = harness; const roots = [ { environment: "local", cwd: work }, { environment: "local", cwd: retained }, { environment: "local", cwd: second }, { environment: "destination", cwd: main }, { environment: "local", cwd: homedir() }, ]; const id = await berry.saveProject(undefined, { name: "Project", roots, defaultRoot: 0 }); await berry.saveProject(undefined, { name: "Nested project", roots: [{ environment: "local", cwd: nested }], defaultRoot: 0, }); const moved = await Promise.all( [work, child, second].map((cwd) => berry.create({ extensions: { environment: { cwd } } })), ); const untouched = await Promise.all([retained, nested, sibling, main].map(async (cwd) => { const tid = await berry.create({ extensions: { environment: { cwd } } }); return { tid, binding: await berry.state(tid, "environment") }; })); const wrongMachine = await berry.create({ extensions: { environment: { environment: "destination", cwd: work } }, }); untouched.push({ tid: wrongMachine, binding: await berry.state(wrongMachine, "environment") }); const conversation = (await berry.conversation(moved[0]))!; const point = (await conversation.context(context)).entries.at(-1)!.id; const fork = await conversation.fork(point, { ownership: { kind: "ownerless" } }, context); moved.push(await berry.tidOf(fork.id)); moved.push(await berry.create({ extensions: {} })); await berry.command(moved[0], "cd", child); await berry.branch(moved[0], point); await berry.archive(moved[1], true); await Deno.remove(child, { recursive: true }); offline = true; await berry.saveProject(id, { name: "Project", roots: [roots[1], roots[3]], defaultRoot: 1, moveConversations: true, }); for (const tid of moved) { assertEquals(await berry.state(tid, "environment"), { environment: "destination", cwd: main }); } for (const { tid, binding } of untouched) { assertEquals(await berry.state(tid, "environment"), binding); } assertEquals(await berry.state(moved[0], "environment", point), { environment: "local", cwd: work, }); }); Deno.test("a busy affected conversation prevents workspace removal and rolls back other moves", async () => { await using root = await tempDir(); const main = join(root.path, "main"); const work = join(root.path, "work"); await Deno.mkdir(main); await Deno.mkdir(work); await using harness = await setup(); const { berry, faux, model, gate } = harness; const id = await berry.saveProject(undefined, { name: "Project", roots: [{ environment: "local", cwd: main }, { environment: "local", cwd: work }], defaultRoot: 0, }); const before = await berry.harness.snapshot(Projects, context); const idle = await berry.create({ extensions: { environment: { cwd: work } } }); const busy = await berry.create({ extensions: { environment: { cwd: work }, gate: {} }, model }); const conversation = (await berry.conversation(busy))!; faux.setResponses([ fauxAssistantMessage(fauxToolCall("hold", {}), { stopReason: "toolUse" }), fauxAssistantMessage("done"), ]); const submission = await conversation.submit({ type: "input", content: "go" }, context); try { await gate.entered.promise; await assertRejects( () => berry.saveProject(id, { name: "Project", roots: [{ environment: "local", cwd: main }], defaultRoot: 0, moveConversations: true, }), BerryError, "busy", ); assertEquals(await berry.harness.snapshot(Projects, context), before); assertEquals(await berry.state(idle, "environment"), { environment: "local", cwd: work }); assertEquals(await berry.state(busy, "environment"), { environment: "local", cwd: work }); } finally { gate.release.resolve("released"); await submission.wait(context); await conversation.waitForIdle(context); } });