char-slop/berry

multipurpose self-modifiable agentic harness

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

Charlotte Somtests: cut test sprawl1c0a335

main
7.9 KiB197 linesraw
1import { assertEquals, assertRejects } from "@std/assert";
2import { mkdir, readFile, writeFile } from "node:fs/promises";
3import { join } from "node:path";
4import { localEnvironment } from "@berry/api/environment.ts";
5import { testExtension } from "@berry/api/testing.ts";
6import code from "./mod.ts";
7import { onPath, tempDir } from "@berry/util";
8
9const jjAvailable = onPath("jj");
10
11async function jj(cwd: string, ...args: string[]): Promise<string> {
12  const result = await new Deno.Command("jj", {
13    args: ["--no-pager", "--color=never", ...args],
14    cwd,
15    stdin: "null",
16    stdout: "piped",
17    stderr: "piped",
18  }).output();
19  if (!result.success) throw new Error(new TextDecoder().decode(result.stderr));
20  return new TextDecoder().decode(result.stdout).trim();
21}
22
23/** The commit that `dir`'s working copy sits on. */
24const base = (dir: string) =>
25  jj(dir, "--ignore-working-copy", "log", "-r", "@-", "--no-graph", "-T", 'commit_id ++ "\n"');
26
27async function project(
28  run: (root: string, repo: string, workspaces: string) => Promise<void>,
29) {
30  await using rootDir = await tempDir({ prefix: "workspace-test-" });
31  const root = rootDir.path;
32  const saved = new Map(
33    ["BERRY_WORKSPACES_DIR", "JJ_CONFIG"].map((key) => [key, Deno.env.get(key)]),
34  );
35  try {
36    const config = join(root, "config.toml");
37    await writeFile(config, '[user]\nname = "Test"\nemail = "test@example.com"\n');
38    Deno.env.set("JJ_CONFIG", config);
39    const workspaces = join(root, "workspaces");
40    Deno.env.set("BERRY_WORKSPACES_DIR", workspaces);
41    const repo = join(root, "repo with spaces");
42    await jj(root, "git", "init", repo);
43    await writeFile(join(repo, "baseline.txt"), "shared base\n");
44    await jj(repo, "new");
45    await run(root, repo, workspaces);
46  } finally {
47    for (const [key, value] of saved) {
48      if (value === undefined) Deno.env.delete(key);
49      else Deno.env.set(key, value);
50    }
51  }
52}
53
54Deno.test({
55  name:
56    "workspace skips empty ancestors and stays independent when the source staging commit changes",
57  ignore: !jjAvailable,
58  fn: () =>
59    project(async (_root, repo) => {
60      const before = await base(repo);
61      await jj(repo, "describe", "--message", "empty staging");
62      await jj(repo, "new", "--message", "another empty stage");
63      const ext = testExtension(code, { env: localEnvironment(repo) });
64      const target = await ext.command("workspace") as string;
65      assertEquals(await base(target), before);
66
67      await writeFile(join(repo, "baseline.txt"), "new source work\n");
68      await jj(repo, "status");
69      await jj(target, "status");
70      assertEquals(await readFile(join(target, "baseline.txt"), "utf8"), "shared base\n");
71      assertEquals(await base(target), before);
72    }),
73});
74
75Deno.test({
76  name: "workspace uses only the nearest nonempty ancestors of an empty merge",
77  ignore: !jjAvailable,
78  fn: () =>
79    project(async (_root, repo) => {
80      await jj(repo, "new", "@-", "--message", "left");
81      await writeFile(join(repo, "left.txt"), "left\n");
82      const left = await jj(repo, "log", "-r", "@", "--no-graph", "-T", "commit_id");
83      await jj(repo, "new", "@-", "--message", "right");
84      await writeFile(join(repo, "right.txt"), "right\n");
85      const right = await jj(repo, "log", "-r", "@", "--no-graph", "-T", "commit_id");
86      await jj(repo, "new", left, right);
87      const ext = testExtension(code, { env: localEnvironment(repo) });
88      const target = await ext.command("workspace") as string;
89      const parents = await base(target);
90      assertEquals(parents.split("\n").sort(), [left, right].sort());
91      assertEquals(await readFile(join(target, "left.txt"), "utf8"), "left\n");
92      assertEquals(await readFile(join(target, "right.txt"), "utf8"), "right\n");
93    }),
94});
95
96Deno.test({
97  name: "workspace refuses a repository with no nonempty history before creating a destination",
98  ignore: !jjAvailable,
99  fn: () =>
100    project(async (root, _repo, workspaces) => {
101      const empty = join(root, "empty");
102      await jj(root, "git", "init", empty);
103      const ext = testExtension(code, { env: localEnvironment(empty) });
104      await assertRejects(
105        () => ext.command("workspace"),
106        Error,
107        "no nonempty ancestor other than root",
108      );
109      assertEquals(ext.env.cwd, empty);
110      await assertRejects(() => Deno.stat(workspaces), Deno.errors.NotFound);
111    }),
112});
113
114Deno.test({
115  name: "workspace reuses its registered directory without resetting unfinished work",
116  ignore: !jjAvailable,
117  fn: () =>
118    project(async (root, repo) => {
119      const ext = testExtension(code, { env: localEnvironment(repo) });
120      const target = await ext.command("workspace") as string;
121      await writeFile(join(target, "unfinished.txt"), "keep me\n");
122      assertEquals(await ext.command("workspace"), target);
123      ext.env.cwd = repo;
124      await writeFile(join(repo, "later.txt"), "not part of the task\n");
125      Deno.env.set("BERRY_WORKSPACES_DIR", join(root, "different-location"));
126      assertEquals(await ext.command("workspace"), target);
127      assertEquals(await readFile(join(target, "unfinished.txt"), "utf8"), "keep me\n");
128      await assertRejects(() => readFile(join(target, "later.txt")));
129    }),
130});
131
132Deno.test({
133  name:
134    "workspace identifies the shared repo from another workspace even after the default is renamed",
135  ignore: !jjAvailable,
136  fn: () =>
137    project(async (_root, repo, workspaces) => {
138      await jj(repo, "workspace", "rename", "main");
139      const first = testExtension(code, { tid: "first", env: localEnvironment(repo) });
140      const firstPath = await first.command("workspace") as string;
141      const second = testExtension(code, {
142        tid: "second",
143        env: localEnvironment(firstPath),
144      });
145      const secondPath = join(workspaces, "repo with spaces", "second");
146      assertEquals(await second.command("workspace"), secondPath);
147      await writeFile(join(firstPath, "private.txt"), "first only\n");
148      await assertRejects(() => readFile(join(secondPath, "private.txt")));
149      assertEquals(
150        (await jj(repo, "--ignore-working-copy", "workspace", "list", "-T", 'name ++ "\n"'))
151          .split("\n").sort(),
152        ["first", "main", "second"],
153      );
154    }),
155});
156
157Deno.test({
158  name: "workspace refuses an occupied destination and leaves both cwd and existing files alone",
159  ignore: !jjAvailable,
160  fn: () =>
161    project(async (_root, repo, workspaces) => {
162      const tid = "occupied";
163      const target = join(workspaces, "repo with spaces", tid);
164      await mkdir(target, { recursive: true });
165      await writeFile(join(target, "keep.txt"), "do not overwrite\n");
166      const ext = testExtension(code, { tid, env: localEnvironment(repo) });
167      await assertRejects(() => ext.command("workspace"));
168      assertEquals(ext.env.cwd, repo);
169      assertEquals(await readFile(join(target, "keep.txt"), "utf8"), "do not overwrite\n");
170      assertEquals(
171        await jj(repo, "--ignore-working-copy", "workspace", "list", "-T", "name"),
172        "default",
173      );
174    }),
175});
176
177Deno.test({
178  name:
179    "workspace refuses non-jj directories, unexpected arguments, and unsafe conversation names without creating files or moving cwd",
180  ignore: !jjAvailable,
181  fn: () =>
182    project(async (root, repo, workspaces) => {
183      const ext = testExtension(code, { env: localEnvironment(root) });
184      await assertRejects(() => ext.command("workspace"), Error, "No command /workspace");
185      assertEquals(ext.env.cwd, root);
186      ext.env.cwd = repo;
187      await assertRejects(() => ext.command("workspace", "extra"), Error, "Usage: /workspace");
188      assertEquals(ext.env.cwd, repo);
189      const unsafe = testExtension(code, {
190        tid: "../outside",
191        env: localEnvironment(repo),
192      });
193      await assertRejects(() => unsafe.command("workspace"), Error, "Invalid conversation ID");
194      assertEquals(unsafe.env.cwd, repo);
195      await assertRejects(() => Deno.stat(workspaces), Deno.errors.NotFound);
196    }),
197});