char-slop/berry

multipurpose self-modifiable agentic harness

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

Charlotte Somdiff: use a request instead of a command for saving diffse9d146d

main
3.5 KiB97 linesraw
1import { assert, assertEquals, assertStringIncludes } from "@std/assert";
2import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context";
3import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai/providers/faux";
4import { readFile, writeFile } from "node:fs/promises";
5import { join } from "node:path";
6import diff from "../extensions/diff/mod.ts";
7import type { OpenFile, Saved } from "../extensions/diff/edit-types.ts";
8import { onPath, tempDir } from "@berry/util";
9import { setup } from "./fixture.ts";
10
11const IDENTITY = {
12  GIT_AUTHOR_NAME: "Test",
13  GIT_AUTHOR_EMAIL: "test@example.com",
14  GIT_COMMITTER_NAME: "Test",
15  GIT_COMMITTER_EMAIL: "test@example.com",
16  GIT_CONFIG_GLOBAL: "/dev/null",
17};
18
19async function run(cwd: string, ...args: string[]) {
20  const result = await new Deno.Command("git", {
21    args,
22    cwd,
23    env: IDENTITY,
24    stdout: "piped",
25    stderr: "piped",
26  }).output();
27  if (!result.success) throw new Error(new TextDecoder().decode(result.stderr));
28}
29
30Deno.test({
31  name: "a diff save lands while the conversation is busy, and its report follows at the boundary",
32  ignore: !onPath("git"),
33  fn: async () => {
34    await using harness = await setup();
35    await using rootDir = await tempDir({ prefix: "diff-busy-test-" });
36    const root = rootDir.path;
37    await run(root, "init", "--quiet");
38    await writeFile(join(root, "a.txt"), "base\nshared\nend\n");
39    await run(root, "add", "-A");
40    await run(root, "commit", "--quiet", "-m", "first");
41    await writeFile(join(root, "a.txt"), "agent\nshared\nend\n");
42
43    const { berry, faux, gate, model } = harness;
44    await berry.install(diff);
45    const tid = await berry.create({
46      extensions: { environment: { cwd: root }, gate: {}, diff: {} },
47      model,
48    });
49    const conversation = (await berry.conversation(tid))!;
50    const opened = await berry.request(tid, "diff", "open", {
51      root,
52      path: "a.txt",
53    }) as unknown as OpenFile;
54
55    faux.setResponses([
56      fauxAssistantMessage(fauxToolCall("hold", {}), { stopReason: "toolUse" }),
57      fauxAssistantMessage("done"),
58    ]);
59    const running = await conversation.submit({ type: "input", content: "go" }, context);
60    await gate.entered.promise;
61    let finishing: Promise<unknown> | undefined;
62    try {
63      // the agent's turn is held open; the save must still write the file and answer
64      const saved = await berry.request(tid, "diff", "save", {
65        session: "review",
66        id: "edit",
67        working: opened.working,
68        path: "a.txt",
69        version: opened.version,
70        content: "agent\nhuman\nend\n",
71      }) as unknown as Saved;
72      assertEquals(await readFile(join(root, "a.txt"), "utf8"), "agent\nhuman\nend\n");
73      // reporting it may wait for the boundary, and is not awaited here
74      finishing = berry.command(
75        tid,
76        "diff-finish",
77        JSON.stringify({ session: "review", saves: [saved] }),
78      );
79      assertEquals(
80        (await conversation.context(context)).messages.some((message) =>
81          typeof message.content === "string" && message.content.includes("human edits")
82        ),
83        false,
84      );
85    } finally {
86      gate.release.resolve("released");
87    }
88    await finishing;
89    await running.wait(context);
90    await conversation.waitForIdle(context);
91    const report = (await conversation.context(context)).messages.findLast((message) =>
92      typeof message.content === "string" && message.content.includes("Historical human edits")
93    );
94    assert(report);
95    assertStringIncludes(report.content as string, "+human");
96  },
97});