char-slop/berry

multipurpose self-modifiable agentic harness

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

Charlotte Somallow editing conflicted commit diffs80904d5

main
23.8 KiB609 linesraw
1import { assert, assertEquals, assertRejects, assertStringIncludes } from "@std/assert";
2import { chmod, readFile, symlink, unlink, 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 diff from "./mod.ts";
7import type { Reply, Shown } from "./types.ts";
8import type { OpenFile, Save, Saved } from "./edit-types.ts";
9import { onPath, tempDir } from "@berry/util";
10
11const IDENTITY = {
12  JJ_USER: "Test",
13  JJ_EMAIL: "test@example.com",
14  GIT_AUTHOR_NAME: "Test",
15  GIT_AUTHOR_EMAIL: "test@example.com",
16  GIT_COMMITTER_NAME: "Test",
17  GIT_COMMITTER_EMAIL: "test@example.com",
18  GIT_CONFIG_GLOBAL: "/dev/null",
19  JJ_CONFIG: "/dev/null",
20};
21
22/** For setting repositories up; the extension reads them with whatever configuration it finds. */
23async function run(cwd: string, program: string, ...args: string[]) {
24  const result = await new Deno.Command(program, {
25    args,
26    cwd,
27    env: IDENTITY,
28    stdout: "piped",
29    stderr: "piped",
30  }).output();
31  if (!result.success) throw new Error(new TextDecoder().decode(result.stderr));
32  return new TextDecoder().decode(result.stdout).trim();
33}
34
35async function repository(test: (root: string) => Promise<void>) {
36  await using rootDir = await tempDir({ prefix: "diff-test-" });
37  const root = rootDir.path;
38  await test(root);
39}
40
41/** What a view that had nothing would be shown. */
42const shown = async (root: string, route = ""): Promise<Shown> => {
43  const reply = await testExtension(diff, { env: localEnvironment(root) })
44    .request("show", { route }) as Reply;
45  return { ...reply, files: reply.files.map(({ diff }) => diff!) };
46};
47
48/** A save from the session "review" of `content` over what `opened` read. */
49const saving = (opened: OpenFile, content: string, path = opened.file.path): Save => ({
50  session: "review",
51  id: "edit",
52  working: opened.working,
53  path,
54  version: opened.version,
55  content,
56});
57
58const summary = ({ files }: Shown) =>
59  files.map(({ path, status, hunks }) => ({ path, status, lines: hunks.flatMap((h) => h.lines) }));
60
61Deno.test({
62  name:
63    "jj: the working copy's changes come whole, its history is browsable, and edits show at once",
64  ignore: !onPath("jj"),
65  fn: () =>
66    repository(async (root) => {
67      await run(root, "jj", "git", "init");
68      await writeFile(join(root, "a.txt"), "one\ntwo\nthree\n");
69      await run(root, "jj", "commit", "-m", "first\n\nwith a body");
70      await writeFile(join(root, "a.txt"), "one\n2\nthree\n");
71
72      const working = await shown(root);
73      assertEquals(working.vcs, "jj");
74      assertEquals(working.root, root);
75      assertEquals(working.revision.workingCopy, true);
76      assertEquals(
77        working.revision.change!.full,
78        await run(
79          root,
80          "jj",
81          "log",
82          "--ignore-working-copy",
83          "--no-graph",
84          "-r",
85          "@",
86          "-T",
87          "change_id",
88        ),
89      );
90      assertEquals(
91        working.revision.commit!.full,
92        await run(
93          root,
94          "jj",
95          "log",
96          "--ignore-working-copy",
97          "--no-graph",
98          "-r",
99          "@",
100          "-T",
101          "commit_id",
102        ),
103      );
104      assertEquals(summary(working), [
105        { path: "a.txt", status: "modified", lines: [" one", "-two", "+2", " three"] },
106      ]);
107      // the root commit, which never has changes, is left out
108      assertEquals(working.log.map((r) => [r.route === "", r.description]), [
109        [true, ""],
110        [false, "first\n\nwith a body\n"],
111      ]);
112
113      const first = await shown(root, working.log[1].route);
114      assertEquals(first.revision.description, "first\n\nwith a body\n");
115      assertEquals(summary(first), [
116        { path: "a.txt", status: "added", lines: ["+one", "+two", "+three"] },
117      ]);
118
119      await writeFile(join(root, "b.txt"), "new\n");
120      assertEquals((await shown(root)).files.map(({ path }) => path), ["a.txt", "b.txt"]);
121    }),
122});
123
124Deno.test({
125  name: "git: the working tree includes untracked files, and each commit shows against its parent",
126  ignore: !onPath("git"),
127  fn: () =>
128    repository(async (root) => {
129      await run(root, "git", "init", "--quiet");
130      const before = await shown(root);
131      assertEquals(before.vcs, "git");
132      assertEquals(before.log.length, 1);
133      await writeFile(join(root, "a.txt"), "one\ntwo\n");
134      await writeFile(join(root, ".gitignore"), "ignored\n");
135      await run(root, "git", "add", "-A");
136      await run(root, "git", "commit", "--quiet", "-m", "first");
137      await run(root, "git", "config", "core.abbrev", "12");
138      await writeFile(join(root, "a.txt"), "one\n2\n");
139      await writeFile(join(root, "new file.txt"), "hello\n");
140      await writeFile(join(root, "ignored"), "secret\n");
141
142      const working = await shown(root);
143      assertEquals(summary(working), [
144        { path: "a.txt", status: "modified", lines: [" one", "-two", "+2"] },
145        { path: "new file.txt", status: "added", lines: ["+hello"] },
146      ]);
147      assertEquals(working.log.map((r) => [r.route === "", r.description.trim()]), [
148        [true, ""],
149        [false, "first"],
150      ]);
151      const first = await shown(root, working.log[1].route);
152      assertEquals(first.revision.refs.some((ref) => ref.includes("HEAD")), true);
153      assertEquals(first.revision.commit, {
154        full: await run(root, "git", "rev-parse", "HEAD"),
155        prefix: "",
156        rest: await run(root, "git", "rev-parse", "--short", "HEAD"),
157      });
158      assertEquals(first.files.map(({ path, status }) => [path, status]), [
159        [".gitignore", "added"],
160        ["a.txt", "added"],
161      ]);
162    }),
163});
164
165Deno.test({
166  name: "a view is sent only the keys of the changes it has already, until they change",
167  ignore: !onPath("git"),
168  fn: () =>
169    repository(async (root) => {
170      await run(root, "git", "init", "--quiet");
171      await writeFile(join(root, "a.txt"), "one\n");
172      await writeFile(join(root, "b.txt"), "two\n");
173      const view = testExtension(diff, { env: localEnvironment(root) });
174      const ask = async (known: string[]) =>
175        (await view.request("show", { route: "", known }) as Reply).files;
176      const first = await ask([]);
177      assertEquals(first.map(({ diff }) => diff?.path), ["a.txt", "b.txt"]);
178      const known = first.map(({ key }) => key);
179      assertEquals(await ask(known), known.map((key) => ({ key, diff: null })));
180      await writeFile(join(root, "b.txt"), "three\n");
181      assertEquals((await ask(known)).map(({ diff }) => diff?.path ?? null), [null, "b.txt"]);
182    }),
183});
184
185Deno.test("outside a repository, or given a route that names no revision, it says so", () =>
186  repository(async (root) => {
187    await assertRejects(() => shown(root), Error, "neither a jj nor a git repository");
188    if (!onPath("git")) return;
189    await run(root, "git", "init", "--quiet");
190    for (const route of ["--output=/tmp/x", "HEAD~1", "main"]) {
191      await assertRejects(() => shown(root, route), Error, "Not a commit id");
192    }
193  }));
194
195Deno.test({
196  name: "human saves report only their changes on exit, and retries do not repeat saves or reports",
197  ignore: !onPath("git"),
198}, () =>
199  repository(async (root) => {
200    await run(root, "git", "init", "--quiet");
201    await writeFile(join(root, "a.txt"), "base\nshared\nend\n");
202    await run(root, "git", "add", "-A");
203    await run(root, "git", "commit", "--quiet", "-m", "first");
204    await writeFile(join(root, "a.txt"), "agent\nshared\nend\n");
205    const view = testExtension(diff, { env: localEnvironment(root) });
206    const opened = await view.request("open", { root, path: "a.txt" }) as OpenFile;
207    const save = saving(opened, "agent\nhuman\nend\n");
208    const [saved, duplicate] = await Promise.all([
209      view.request("save", save),
210      view.request("save", save),
211    ]) as Saved[];
212    assertEquals(saved, duplicate);
213    assertEquals(await view.prompt(), "");
214    assertEquals(view.messages, []);
215    assertEquals(await readFile(join(root, "a.txt"), "utf8"), save.content);
216    await writeFile(join(root, "other.txt"), "later agent edit\n");
217    await view.command(
218      "diff-finish",
219      JSON.stringify({ session: "review", saves: [saved] }),
220    );
221    const messages = view.messages;
222    assertEquals(messages.length, 1);
223    const report = messages[0].content;
224    assert(typeof report === "string");
225    assertStringIncludes(report, "+human");
226    assert(
227      !report.includes("-base") && !report.includes("+agent") && !report.includes("other.txt"),
228    );
229    assertEquals(await view.request("save", save), saved);
230    await view.command("diff-finish", "review");
231    assertEquals(await view.prompt(), "");
232    assertEquals(view.messages, messages);
233
234    const next = await view.request("open", { root, path: "a.txt" }) as OpenFile;
235    const secondSave = await view.request("save", {
236      ...saving(next, "agent\nhuman again\nend\n"),
237      session: "second",
238    }) as Saved;
239    await view.command(
240      "diff-finish",
241      JSON.stringify({ session: "second", saves: [secondSave] }),
242    );
243    assertEquals(view.messages.slice(0, messages.length), messages);
244    const second = view.messages[1].content;
245    assert(typeof second === "string");
246    assertStringIncludes(second, "+human again");
247    assertEquals(await view.prompt(), "");
248
249    const unchanged = await view.request("open", { root, path: "a.txt" }) as OpenFile;
250    const unchangedSave = await view.request("save", {
251      ...saving(unchanged, unchanged.content),
252      session: "unchanged",
253    }) as Saved;
254    await view.command(
255      "diff-finish",
256      JSON.stringify({ session: "unchanged", saves: [unchangedSave] }),
257    );
258    assertEquals(view.messages.length, 2);
259  }));
260
261Deno.test({
262  name:
263    "jj edits use complete files even when the displayed patch omits lines, preserving executable CRLF text",
264  ignore: !onPath("jj"),
265}, () =>
266  repository(async (root) => {
267    await run(root, "jj", "git", "init");
268    const path = "script.txt";
269    const baseline = "\ufeff" + Array.from({ length: 21_000 }, (_, i) => `line ${i}`).join("\r\n");
270    await writeFile(join(root, path), baseline);
271    await chmod(join(root, path), 0o755);
272    await run(root, "jj", "commit", "-m", "first");
273    const content = baseline.replace("line 10000\r", "agent\r");
274    await writeFile(join(root, path), content);
275    const view = testExtension(diff, { env: localEnvironment(root) });
276    const display = await view.request("show", {}) as Reply;
277    assert(display.files[0].diff!.hunks.flatMap((h) => h.lines).length < 30);
278    const opened = await view.request("open", { root, path }) as OpenFile;
279    assertEquals(opened.original, baseline);
280    assertEquals(opened.content, content);
281    const edited = content.replace("line 20000\r", "human\r");
282    await view.request("save", saving(opened, edited));
283    assertEquals(await readFile(join(root, path), "utf8"), edited);
284    assertEquals((await Deno.stat(join(root, path))).mode! & 0o777, 0o755);
285  }));
286
287Deno.test({
288  name: "a stale file or a different jj working-copy change refuses a save without overwriting",
289  ignore: !onPath("jj"),
290}, () =>
291  repository(async (root) => {
292    await run(root, "jj", "git", "init");
293    await writeFile(join(root, "a.txt"), "initial\n");
294    const view = testExtension(diff, { env: localEnvironment(root) });
295    const opened = await view.request("open", { root, path: "a.txt" }) as OpenFile;
296    const save = saving(opened, "human\n");
297    await writeFile(join(root, "a.txt"), "external\n");
298    await assertRejects(
299      () => view.request("save", save),
300      Error,
301      "changed on disk",
302    );
303    assertEquals(await readFile(join(root, "a.txt"), "utf8"), "external\n");
304    await writeFile(join(root, "a.txt"), opened.content);
305    await run(root, "jj", "new");
306    await assertRejects(
307      () => view.request("save", save),
308      Error,
309      "working copy changed",
310    );
311    assertEquals(await readFile(join(root, "a.txt"), "utf8"), opened.content);
312    assertEquals(await view.prompt(), "");
313  }));
314
315Deno.test({
316  name:
317    "diff saves cannot escape the repository, edit metadata, or follow a replaced symbolic link",
318  ignore: !onPath("git"),
319}, () =>
320  repository(async (root) => {
321    await run(root, "git", "init", "--quiet");
322    await writeFile(join(root, "a.txt"), "initial\n");
323    const view = testExtension(diff, { env: localEnvironment(root) });
324    const opened = await view.request("open", { root, path: "a.txt" }) as OpenFile;
325    const save = saving(opened, "human\n", "../outside");
326    await assertRejects(
327      () => view.request("save", save),
328      Error,
329      "repository path",
330    );
331    await assertRejects(
332      () => view.request("save", { ...save, path: ".git/config" }),
333      Error,
334      "repository path",
335    );
336    await using outside = await tempDir();
337    const target = join(outside.path, "target.txt");
338    await writeFile(target, opened.content);
339    await unlink(join(root, "a.txt"));
340    await symlink(target, join(root, "a.txt"));
341    await assertRejects(
342      () => view.request("save", { ...save, path: "a.txt" }),
343      Error,
344      "symbolic links",
345    );
346    assertEquals(await readFile(target, "utf8"), opened.content);
347  }));
348
349Deno.test({
350  name: "git merge conflicts are editable, and a save resolves the markers on disk",
351  ignore: !onPath("git"),
352}, () =>
353  repository(async (root) => {
354    await run(root, "git", "init", "--initial-branch=main", "--quiet");
355    await writeFile(join(root, "a.txt"), "base\n");
356    await run(root, "git", "add", "-A");
357    await run(root, "git", "commit", "--quiet", "-m", "base");
358    await run(root, "git", "checkout", "-qb", "branch");
359    await writeFile(join(root, "a.txt"), "branch\n");
360    await run(root, "git", "commit", "-am", "branch", "--quiet");
361    await run(root, "git", "checkout", "main", "--quiet");
362    await writeFile(join(root, "a.txt"), "main\n");
363    await run(root, "git", "commit", "-am", "main", "--quiet");
364    await assertRejects(() => run(root, "git", "merge", "branch"));
365    const view = testExtension(diff, { env: localEnvironment(root) });
366    const current = await view.request("show", {}) as Reply;
367    assertEquals(current.revision.conflict, true);
368    const opened = await view.request("open", { root, path: "a.txt" }) as OpenFile;
369    assertStringIncludes(opened.content, "<<<<<<<");
370    assertEquals(opened.original, "main\n");
371    await view.request("save", saving(opened, "resolved\n"));
372    assertEquals(await readFile(join(root, "a.txt"), "utf8"), "resolved\n");
373  }));
374
375/** A jj repository whose commit "theirs" changes a.txt the way "ours" does, rebased onto it to conflict. */
376async function conflicting(root: string) {
377  await run(root, "jj", "git", "init");
378  await writeFile(join(root, "a.txt"), "one\ntwo\nthree\n");
379  await run(root, "jj", "describe", "-m", "base");
380  await run(root, "jj", "new");
381  await writeFile(join(root, "a.txt"), "ONE\ntwo\nthree\n");
382  await run(root, "jj", "describe", "-m", "ours");
383  const ours = await run(root, "jj", "log", "--no-graph", "-r", "@", "-T", "change_id");
384  await run(root, "jj", "new", "root()");
385  await writeFile(join(root, "a.txt"), "one\ntwo\nTHREE\n");
386  await run(root, "jj", "describe", "-m", "theirs");
387  const theirs = await run(root, "jj", "log", "--no-graph", "-r", "@", "-T", "change_id");
388  await run(root, "jj", "new", ours);
389  await run(root, "jj", "rebase", "-s", theirs, "-d", ours);
390  return { ours, theirs };
391}
392
393Deno.test({
394  name: "jj: a conflicted commit's file can be edited, resolving its conflict",
395  ignore: !onPath("jj"),
396}, () =>
397  repository(async (root) => {
398    const { theirs } = await conflicting(root);
399    const revision = (await shown(root, theirs)).revision;
400    assertEquals([revision.route, revision.conflict], [theirs, true]);
401    const view = testExtension(diff, { env: localEnvironment(root) });
402    const opened = await view.request("open", {
403      root,
404      path: "a.txt",
405      route: theirs,
406    }) as OpenFile;
407    assertStringIncludes(opened.content, "<<<<<<<");
408    assertEquals(opened.original, "ONE\ntwo\nthree\n");
409    const saved = await view.request("save", saving(opened, "ONE\ntwo\nTHREE\n")) as Saved;
410    assert(saved.working.identity !== opened.working.identity);
411    assertEquals(
412      await run(root, "jj", "file", "show", "-r", theirs, "--", "a.txt"),
413      "ONE\ntwo\nTHREE",
414    );
415    assertEquals(await run(root, "jj", "log", "--no-graph", "-r", "conflicts()"), "");
416  }));
417
418Deno.test({
419  name: "jj: a conflicted working copy's file can be edited, resolving its conflict",
420  ignore: !onPath("jj"),
421}, () =>
422  repository(async (root) => {
423    const { theirs } = await conflicting(root);
424    await run(root, "jj", "edit", theirs);
425    const view = testExtension(diff, { env: localEnvironment(root) });
426    const current = await shown(root);
427    assertEquals([current.revision.workingCopy, current.revision.conflict], [true, true]);
428    const opened = await view.request("open", { root, path: "a.txt" }) as OpenFile;
429    assertStringIncludes(opened.content, "<<<<<<<");
430    await view.request("save", saving(opened, "ONE\ntwo\nTHREE\n"));
431    assertEquals(await readFile(join(root, "a.txt"), "utf8"), "ONE\ntwo\nTHREE\n");
432    assertEquals(await run(root, "jj", "log", "--no-graph", "-r", "conflicts()"), "");
433  }));
434
435Deno.test({
436  name:
437    "an exit waits for an accepted save even when its receipt was not in the exit's starting state",
438  ignore: !onPath("git"),
439}, () =>
440  repository(async (root) => {
441    await run(root, "git", "init", "--quiet");
442    await writeFile(join(root, "a.txt"), "initial\n");
443    const env = localEnvironment(root);
444    const entered = Promise.withResolvers<void>();
445    const release = Promise.withResolvers<void>();
446    const write = env.writeFile.bind(env);
447    env.writeFile = async (...args) => {
448      entered.resolve();
449      await release.promise;
450      return await write(...args);
451    };
452    const view = testExtension(diff, { env });
453    const opened = await view.request("open", { root, path: "a.txt" }) as OpenFile;
454    const saved = view.request("save", saving(opened, "human\n"));
455    try {
456      await entered.promise;
457      const finishing = view.command("diff-finish", "review");
458      let finished = false;
459      void finishing.then(() => {
460        finished = true;
461      });
462      await Promise.resolve();
463      await Promise.resolve();
464      assertEquals(finished, false);
465      release.resolve();
466      await saved;
467      await finishing;
468      const report = view.messages[0]?.content;
469      assert(typeof report === "string");
470      assertStringIncludes(report, "+human");
471    } finally {
472      release.resolve();
473      await saved;
474    }
475  }));
476
477Deno.test({
478  name:
479    "a jj commit's file can be edited, rebasing its descendants, and a save that would conflict them is undone unless forced",
480  ignore: !onPath("jj"),
481}, () =>
482  repository(async (root) => {
483    await run(root, "jj", "git", "init");
484    await writeFile(join(root, "a.txt"), "one\ntwo\nthree\nfour\nfive\n");
485    await run(root, "jj", "commit", "-m", "first");
486    await writeFile(join(root, "a.txt"), "one\ntwo\nthree\nfour\nFIVE\n");
487    await run(root, "jj", "commit", "-m", "second");
488    await writeFile(join(root, "b.txt"), "working\n");
489    const view = testExtension(diff, { env: localEnvironment(root) });
490    const first = (await shown(root)).log.find((r) => r.description.startsWith("first"))!;
491    const opened = await view.request("open", {
492      root,
493      path: "a.txt",
494      route: first.route,
495    }) as OpenFile;
496    assertEquals(opened.content, "one\ntwo\nthree\nfour\nfive\n");
497    const saved = await view.request(
498      "save",
499      saving(opened, "ONE\ntwo\nthree\nfour\nfive\n"),
500    ) as Saved;
501    assertEquals(await readFile(join(root, "a.txt"), "utf8"), "ONE\ntwo\nthree\nfour\nFIVE\n");
502    assertEquals(await readFile(join(root, "b.txt"), "utf8"), "working\n");
503    assertEquals(await run(root, "jj", "log", "--no-graph", "-r", "conflicts()"), "");
504    await assertRejects(
505      () => view.request("save", { ...saving(opened, "x\n"), id: "stale" }),
506      Error,
507      "changed since you opened it",
508    );
509
510    const again = { ...saving(opened, "ONE\ntwo\nthree\nfour\nfive!\n"), id: "conflicting" };
511    again.working = saved.working;
512    const operation = await run(root, "jj", "op", "log", "--no-graph", "--limit=1", "-T", "id");
513    await assertRejects(
514      () => view.request("save", again),
515      Error,
516      "would cause conflicts in 2 later commits",
517    );
518    assertEquals(await readFile(join(root, "a.txt"), "utf8"), "ONE\ntwo\nthree\nfour\nFIVE\n");
519    const restored = await run(root, "jj", "op", "log", "--no-graph", "--limit=1", "-T", "id");
520    assert(restored !== operation);
521    assertEquals(await run(root, "jj", "log", "--no-graph", "-r", "conflicts()"), "");
522    await view.request("save", { ...again, force: true });
523    assertStringIncludes(await readFile(join(root, "a.txt"), "utf8"), "<<<<<<<");
524
525    await view.command("diff-finish", "review");
526    const report = view.messages[0]?.content;
527    assert(typeof report === "string");
528    assertStringIncludes(report, `"commit":"${first.route}"`);
529    await assertRejects(
530      () =>
531        view.request("open", { root, path: "a.txt", route: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" }),
532    );
533  }));
534
535Deno.test({
536  name: "jj: rewording targets the opened change and preserves its files and dirty descendants",
537  ignore: !onPath("jj"),
538  fn: () =>
539    repository(async (root) => {
540      await run(root, "jj", "git", "init");
541      await writeFile(join(root, "a.txt"), "base\n");
542      await run(root, "jj", "describe", "-m", "original\n\nbody");
543      const opened = await shown(root);
544      const view = testExtension(diff, { env: localEnvironment(root) });
545      await run(root, "jj", "new", "-m", "descendant");
546      await writeFile(join(root, "a.txt"), "dirty descendant\n");
547      const change = opened.revision.change!.full;
548      await view.request("reword", {
549        root,
550        change,
551        before: opened.revision.description,
552        description: "--new subject\n\nnew body",
553      });
554      const updated = await shown(root, change);
555      assertEquals(updated.revision.description, "--new subject\n\nnew body\n");
556      assertEquals(updated.revision.change, opened.revision.change);
557      assertEquals(summary(updated), summary(opened));
558      const working = await shown(root);
559      assertEquals(working.revision.description, "descendant\n");
560      assertEquals(working.log[1].change!.full, change);
561      assertEquals(await readFile(join(root, "a.txt"), "utf8"), "dirty descendant\n");
562
563      await view.request("reword", {
564        root,
565        change: working.revision.change!.full,
566        before: working.revision.description,
567        description: "",
568      });
569      assertEquals((await shown(root)).revision.description, "");
570    }),
571});
572
573Deno.test({
574  name: "jj: rewording rejects stale descriptions, another repository, and immutable commits",
575  ignore: !onPath("jj"),
576  fn: () =>
577    repository(async (root) => {
578      await run(root, "jj", "git", "init");
579      await run(root, "jj", "describe", "-m", "original");
580      const opened = await shown(root);
581      const view = testExtension(diff, { env: localEnvironment(root) });
582      const args = {
583        root,
584        change: opened.revision.change!.full,
585        before: opened.revision.description,
586        description: "replacement",
587      };
588      await assertRejects(
589        () => view.request("reword", { ...args, root: `${root}/elsewhere` }),
590        Error,
591        "another repository",
592      );
593      await run(root, "jj", "describe", "-m", "someone else's edit");
594      await assertRejects(() => view.request("reword", args), Error, "description changed");
595      assertEquals((await shown(root)).revision.description, "someone else's edit\n");
596      await run(root, "jj", "bookmark", "create", "frozen");
597      await run(root, "jj", "new");
598      await run(
599        root,
600        "jj",
601        "config",
602        "set",
603        "--repo",
604        'revset-aliases."immutable_heads()"',
605        "frozen",
606      );
607      await assertRejects(() => view.request("reword", args), Error, "immutable");
608    }),
609});