char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somadd opt-in conversation moves when removing project workspaces6276dba

main
12.2 KiB308 linesraw
1import { assertEquals, assertRejects } from "@std/assert";
2import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context";
3import { localEnvironment } from "@berry/api/environment.ts";
4import { projectFor, type ProjectIndex } from "@berry/api/projects.ts";
5import { homedir } from "node:os";
6import { join } from "node:path";
7import { type Environments, localOnly } from "./environment.ts";
8import { Projects } from "./projects.ts";
9import { setup } from "./fixture.ts";
10import { BerryError } from "./berry.ts";
11import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai/providers/faux";
12import { summaries, type Summary } from "./summaries.ts";
13import { tempDir } from "@berry/util";
14
15Deno.test("projects match workspace boundaries and the most specific root on the right machine", () => {
16  const projects: ProjectIndex = {
17    berry: {
18      name: "Berry",
19      defaultRoot: 0,
20      roots: [
21        { environment: "local", cwd: "/work/berry", platform: "linux" },
22        { environment: "local", cwd: "/workspaces/feature", platform: "linux" },
23        { environment: "windows", cwd: "C:\\Work\\Berry", platform: "win32" },
24        { environment: "local", cwd: "/work/berry:odd", platform: "linux" },
25      ],
26    },
27    nested: {
28      name: "Nested",
29      defaultRoot: 0,
30      roots: [{ environment: "local", cwd: "/work/berry/tools/", platform: "linux" }],
31    },
32  };
33  for (
34    const [environment, cwd, expected] of [
35      ["local", "/work/berry", "berry"],
36      ["local", "/work/berry/src", "berry"],
37      ["local", "/workspaces/feature/src", "berry"],
38      ["local", "/work/berry-old", null],
39      ["other", "/work/berry", null],
40      ["local", "/work/berry/tools/parser", "nested"],
41      ["windows", "c:/WORK/berry/src", "berry"],
42      ["windows", "C:\\Work\\Berry-old", null],
43      ["local", "/work/berry:odd/src", "berry"],
44      ["local", "/Work/Berry", null],
45    ] as const
46  ) assertEquals(projectFor(projects, { environment, cwd }), expected);
47  assertEquals(projectFor(projects, undefined), null);
48});
49
50Deno.test("project workspaces validate and canonicalize without changing any conversation", async () => {
51  await using rootDir = await tempDir();
52  const root = rootDir.path;
53  const main = join(root, "main");
54  const workspace = join(root, "workspace");
55  await Deno.mkdir(main);
56  await Deno.mkdir(workspace);
57  await Deno.writeTextFile(join(root, "file"), "");
58  await Deno.symlink(main, join(root, "link"));
59  let offline = false;
60  const environments: Environments = {
61    open(name, cwd, during) {
62      if (name !== "other") return localOnly.open(name, cwd, during);
63      if (offline) return Promise.reject(new Error("offline"));
64      return Promise.resolve(Object.assign(localEnvironment(cwd ?? root), { name, home: root }));
65    },
66    close: () => Promise.resolve(),
67  };
68  await using harness = await setup(environments);
69  const { berry } = harness;
70  const tid = await berry.create({ extensions: { environment: { cwd: main } } });
71  const binding = await berry.state(tid, "environment");
72  const id = await berry.saveProject(undefined, {
73    name: " Berry ",
74    defaultRoot: 1,
75    roots: [
76      { environment: "local", cwd: join(root, "link") },
77      { environment: "other", cwd: "workspace" },
78    ],
79  });
80  const project = (await berry.harness.snapshot(Projects, context))![id];
81  assertEquals(project.name, "Berry");
82  assertEquals(project.roots.map(({ environment, cwd }) => ({ environment, cwd })), [
83    { environment: "local", cwd: await Deno.realPath(main) },
84    { environment: "other", cwd: await Deno.realPath(workspace) },
85  ]);
86  assertEquals(project.defaultRoot, 1);
87  for (const cwd of [join(root, "file"), join(root, "missing"), main]) {
88    await assertRejects(() =>
89      berry.saveProject(undefined, {
90        name: "invalid",
91        defaultRoot: 0,
92        roots: [{ environment: "local", cwd }],
93      })
94    );
95  }
96  await assertRejects(() =>
97    berry.saveProject(undefined, { name: "invalid", roots: [], defaultRoot: 0 })
98  );
99  await assertRejects(() =>
100    berry.saveProject(undefined, {
101      name: "invalid",
102      roots: [{ environment: "local", cwd: workspace }],
103      defaultRoot: 2,
104    })
105  );
106  await assertRejects(() =>
107    berry.saveProject(undefined, {
108      name: "invalid",
109      roots: [{ environment: "local", cwd: workspace }, { environment: "local", cwd: workspace }],
110      defaultRoot: 0,
111    })
112  );
113  offline = true;
114  await berry.saveProject(id, { ...project, name: "Renamed" });
115  assertEquals(await berry.state(tid, "environment"), binding);
116  await berry.deleteProject(id);
117  assertEquals(await berry.harness.snapshot(Projects, context), {});
118  assertEquals(await berry.state(tid, "environment"), binding);
119});
120
121Deno.test("live sidebar bindings follow cd, independently moved forks, branch switches, and unreachable machines", async () => {
122  await using rootDir = await tempDir();
123  const root = rootDir.path;
124  await Deno.mkdir(join(root, "a"));
125  await Deno.mkdir(join(root, "b"));
126  await using harness = await setup();
127  const { berry } = harness;
128  let watched: Awaited<ReturnType<typeof summaries>> | undefined;
129  try {
130    const tid = await berry.create({ extensions: { environment: { cwd: join(root, "a") } } });
131    const misc = await berry.create({ extensions: {} });
132    const conversation = (await berry.conversation(tid))!;
133    const point = (await conversation.context(context)).entries.at(-1)!.id;
134    const fork = await conversation.fork(point, { ownership: { kind: "ownerless" } }, context);
135    const forkTid = await berry.tidOf(fork.id);
136    watched = await summaries(berry);
137    const feed = watched.feed();
138    let current = feed.value as Record<string, Summary>;
139    let wake = () => {};
140    await feed.start((value) => {
141      current = value as Record<string, Summary>;
142      wake();
143      return Promise.resolve();
144    });
145    const until = async (id: string, cwd: string) => {
146      while (current[id]?.cwd !== cwd) await new Promise<void>((resolve) => wake = resolve);
147    };
148    assertEquals(current[misc].cwd, homedir());
149    await berry.command(forkTid, "cd", join(root, "b"));
150    await until(forkTid, join(root, "b"));
151    assertEquals(current[tid].cwd, join(root, "a"));
152    await berry.branch(forkTid, point);
153    await until(forkTid, join(root, "a"));
154    await (await berry.emit(forkTid, "environment", {
155      type: "bind",
156      environment: "unreachable",
157      cwd: "/work",
158    })).wait(context);
159    await until(forkTid, "/work");
160    assertEquals(current[forkTid].environment, "unreachable");
161    assertEquals(current[tid].environment, "local");
162    await feed.stop();
163  } finally {
164    watched?.stop();
165  }
166});
167
168Deno.test("removing a workspace leaves its conversations in place unless moving is requested", async () => {
169  await using root = await tempDir();
170  const main = join(root.path, "main");
171  const work = join(root.path, "work");
172  await Deno.mkdir(main);
173  await Deno.mkdir(work);
174  await using harness = await setup();
175  const { berry } = harness;
176  const id = await berry.saveProject(undefined, {
177    name: "Project",
178    roots: [{ environment: "local", cwd: main }, { environment: "local", cwd: work }],
179    defaultRoot: 0,
180  });
181  const tid = await berry.create({ extensions: { environment: { cwd: work } } });
182  await berry.saveProject(id, {
183    name: "Project",
184    roots: [{ environment: "local", cwd: main }],
185    defaultRoot: 0,
186  });
187  assertEquals(await berry.state(tid, "environment"), { environment: "local", cwd: work });
188});
189
190Deno.test("workspace removal moves current conversations and descendants, respecting nested workspaces and projects", async () => {
191  await using root = await tempDir();
192  const main = join(root.path, "main");
193  const work = join(root.path, "work");
194  const second = join(root.path, "second");
195  const child = join(work, "src");
196  const retained = join(work, "retained");
197  const nested = join(work, "nested");
198  const sibling = join(root.path, "work-old");
199  for (const path of [main, child, retained, nested, sibling, second]) {
200    await Deno.mkdir(path, { recursive: true });
201  }
202  let offline = false;
203  const environments: Environments = {
204    open(name, cwd, during) {
205      if (name !== "destination") return localOnly.open(name, cwd, during);
206      if (offline) return Promise.reject(new Error("offline"));
207      return Promise.resolve(Object.assign(localEnvironment(cwd ?? main), { name }));
208    },
209    close: () => Promise.resolve(),
210  };
211  await using harness = await setup(environments);
212  const { berry } = harness;
213  const roots = [
214    { environment: "local", cwd: work },
215    { environment: "local", cwd: retained },
216    { environment: "local", cwd: second },
217    { environment: "destination", cwd: main },
218    { environment: "local", cwd: homedir() },
219  ];
220  const id = await berry.saveProject(undefined, { name: "Project", roots, defaultRoot: 0 });
221  await berry.saveProject(undefined, {
222    name: "Nested project",
223    roots: [{ environment: "local", cwd: nested }],
224    defaultRoot: 0,
225  });
226  const moved = await Promise.all(
227    [work, child, second].map((cwd) => berry.create({ extensions: { environment: { cwd } } })),
228  );
229  const untouched = await Promise.all([retained, nested, sibling, main].map(async (cwd) => {
230    const tid = await berry.create({ extensions: { environment: { cwd } } });
231    return { tid, binding: await berry.state(tid, "environment") };
232  }));
233  const wrongMachine = await berry.create({
234    extensions: { environment: { environment: "destination", cwd: work } },
235  });
236  untouched.push({ tid: wrongMachine, binding: await berry.state(wrongMachine, "environment") });
237  const conversation = (await berry.conversation(moved[0]))!;
238  const point = (await conversation.context(context)).entries.at(-1)!.id;
239  const fork = await conversation.fork(point, { ownership: { kind: "ownerless" } }, context);
240  moved.push(await berry.tidOf(fork.id));
241  moved.push(await berry.create({ extensions: {} }));
242  await berry.command(moved[0], "cd", child);
243  await berry.branch(moved[0], point);
244  await berry.archive(moved[1], true);
245  await Deno.remove(child, { recursive: true });
246  offline = true;
247  await berry.saveProject(id, {
248    name: "Project",
249    roots: [roots[1], roots[3]],
250    defaultRoot: 1,
251    moveConversations: true,
252  });
253  for (const tid of moved) {
254    assertEquals(await berry.state(tid, "environment"), { environment: "destination", cwd: main });
255  }
256  for (const { tid, binding } of untouched) {
257    assertEquals(await berry.state(tid, "environment"), binding);
258  }
259  assertEquals(await berry.state(moved[0], "environment", point), {
260    environment: "local",
261    cwd: work,
262  });
263});
264
265Deno.test("a busy affected conversation prevents workspace removal and rolls back other moves", async () => {
266  await using root = await tempDir();
267  const main = join(root.path, "main");
268  const work = join(root.path, "work");
269  await Deno.mkdir(main);
270  await Deno.mkdir(work);
271  await using harness = await setup();
272  const { berry, faux, model, gate } = harness;
273  const id = await berry.saveProject(undefined, {
274    name: "Project",
275    roots: [{ environment: "local", cwd: main }, { environment: "local", cwd: work }],
276    defaultRoot: 0,
277  });
278  const before = await berry.harness.snapshot(Projects, context);
279  const idle = await berry.create({ extensions: { environment: { cwd: work } } });
280  const busy = await berry.create({ extensions: { environment: { cwd: work }, gate: {} }, model });
281  const conversation = (await berry.conversation(busy))!;
282  faux.setResponses([
283    fauxAssistantMessage(fauxToolCall("hold", {}), { stopReason: "toolUse" }),
284    fauxAssistantMessage("done"),
285  ]);
286  const submission = await conversation.submit({ type: "input", content: "go" }, context);
287  try {
288    await gate.entered.promise;
289    await assertRejects(
290      () =>
291        berry.saveProject(id, {
292          name: "Project",
293          roots: [{ environment: "local", cwd: main }],
294          defaultRoot: 0,
295          moveConversations: true,
296        }),
297      BerryError,
298      "busy",
299    );
300    assertEquals(await berry.harness.snapshot(Projects, context), before);
301    assertEquals(await berry.state(idle, "environment"), { environment: "local", cwd: work });
302    assertEquals(await berry.state(busy, "environment"), { environment: "local", cwd: work });
303  } finally {
304    gate.release.resolve("released");
305    await submission.wait(context);
306    await conversation.waitForIdle(context);
307  }
308});