char-slop/berry

multipurpose self-modifiable agentic harness

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

Charlotte Somcut a bunch of code volumecb11725

main
7.3 KiB169 linesraw
1import { assertEquals, assertRejects, assertStringIncludes } from "@std/assert";
2import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context";
3import code from "../extensions/code/mod.ts";
4import { ShellDoc, ShellEntry } from "./shell.ts";
5import { setup, turn } from "./fixture.ts";
6import { tempDir } from "@berry/util";
7
8async function prepare() {
9  const harness = await setup();
10  const temporary = await tempDir({ prefix: "user-shell-test-" });
11  const root = temporary.path;
12  await harness.berry.install(code);
13  const tid = await harness.berry.create({
14    extensions: { environment: { cwd: root }, code: {} },
15    model: harness.model,
16  });
17  const conversation = (await harness.berry.conversation(tid))!;
18  return {
19    ...harness,
20    root,
21    tid,
22    conversation,
23    async entries() {
24      return (await conversation.entries({ order: "ascending" }, 100, undefined, context)).items
25        .filter(ShellEntry.is);
26    },
27    async run(command: string, excluded = false) {
28      const task = await harness.berry.shell(tid, command, excluded);
29      const settled = await harness.berry.harness.waitForTask(task, context);
30      assertEquals(settled.state.outcome.status, "completed");
31      await conversation.waitForIdle(context);
32    },
33    async output() {
34      for (let i = 0; i < 200; i++) {
35        const run = (await harness.berry.harness.snapshot(ShellDoc, conversation.id, context))?.run;
36        if ((run?.details as { rawOutput?: string } | null)?.rawOutput) return run!;
37        await new Promise((resolve) => setTimeout(resolve, 10));
38      }
39      throw new Error("Shell command did not produce live output");
40    },
41    async [Symbol.asyncDispose]() {
42      await conversation.abort(context);
43      await harness.berry.close();
44      await temporary[Symbol.asyncDispose]();
45    },
46  };
47}
48
49Deno.test("user shell commands persist output at the working directory without starting the model", async () => {
50  await using h = await prepare();
51  await h.run("printf 'visible'; printf ' stderr' >&2; pwd; exit 7");
52  await h.run("printf 'private'", true);
53  assertEquals(h.faux.state.callCount, 0);
54  const entries = await h.entries();
55  assertEquals(entries.length, 2);
56  const first = entries[0].data.result as { output: string; exitCode: number };
57  assertStringIncludes(first.output, "visible");
58  assertStringIncludes(first.output, " stderr");
59  assertStringIncludes(first.output, h.root);
60  assertEquals(first.exitCode, 7);
61  assertEquals(entries[0].data.status, "ok");
62  assertEquals(entries[1].model, undefined);
63  assertEquals(entries[1].data.excludeFromContext, true);
64  assertEquals(entries[1].data.result, { output: "private", exitCode: 0 });
65  const [messages] = await turn(h, h.tid, []);
66  const text = JSON.stringify(messages);
67  assertStringIncludes(text, "visible");
68  assertStringIncludes(text, "exit code 7");
69  assertEquals(text.includes("private"), false);
70  const fork = await h.conversation.fork(
71    entries[1].id,
72    { ownership: { kind: "ownerless" } },
73    context,
74  );
75  const forkTid = await h.berry.tidOf(fork.id);
76  const [forkMessages] = await turn(h, forkTid, []);
77  assertStringIncludes(JSON.stringify(forkMessages), "visible");
78  assertEquals(JSON.stringify(forkMessages).includes("private"), false);
79});
80
81Deno.test("user shell output streams with ANSI intact, refuses concurrent commands and can be aborted", async () => {
82  await using h = await prepare();
83  const task = await h.berry.shell(h.tid, "printf '\\033[31mred'; sleep 30");
84  const live = await h.output();
85  assertEquals(live.status, "running");
86  assertEquals(live.details, { rawOutput: "\x1b[31mred" });
87  await assertRejects(() => h.berry.shell(h.tid, "touch should-not-run"), Error, "Busy");
88  await h.conversation.abort(context);
89  await h.berry.harness.waitForTask(task, context);
90  const [entry] = await h.entries();
91  assertEquals(entry.data.status, "cancelled");
92  assertEquals(entry.data.details, live.details);
93  assertStringIncludes(JSON.stringify(entry.model), "command cancelled");
94  assertEquals(
95    (await h.berry.harness.snapshot(ShellDoc, h.conversation.id, context))?.run,
96    undefined,
97  );
98  assertEquals(h.faux.state.callCount, 0);
99  await h.run("printf 'after cancellation'");
100  assertEquals((await h.entries()).length, 2);
101});
102
103Deno.test("shell commands require enabled code, nonempty input and an idle assistant", async () => {
104  await using h = await prepare();
105  let running: ReturnType<typeof turn> | undefined;
106  try {
107    await assertRejects(() => h.berry.shell(h.tid, " \n "), Error, "Expected a shell command");
108    const disabled = await h.berry.create({ extensions: {} });
109    await assertRejects(
110      () => h.berry.shell(disabled, "touch should-not-run"),
111      Error,
112      "code extension",
113    );
114    await h.berry.configure(h.tid, { extensions: ["gate", "code"] });
115    const { fauxAssistantMessage, fauxToolCall } = await import(
116      "@earendil-works/pi-ai/providers/faux"
117    );
118    running = turn(h, h.tid, [
119      fauxAssistantMessage(fauxToolCall("hold", {}), { stopReason: "toolUse" }),
120    ]);
121    await h.gate.entered.promise;
122    await assertRejects(() => h.berry.shell(h.tid, "touch should-not-run"), Error, "Busy");
123    h.gate.release.resolve("done");
124    await running;
125    assertEquals(await h.entries(), []);
126  } finally {
127    h.gate.release.resolve("done");
128    await running;
129  }
130});
131
132Deno.test("a restart records interrupted shell execution instead of replaying its side effects", async () => {
133  const { openNodeSqliteStorage } = await import("@earendil-works/pi-durable/storage/sqlite/node");
134  const { createModels } = await import("@earendil-works/pi-ai/models");
135  const { fauxProvider } = await import("@earendil-works/pi-ai/providers/faux");
136  const { openBerry } = await import("./berry.ts");
137  const models = createModels();
138  models.setProvider(fauxProvider().provider);
139  await using rootDir = await tempDir({ prefix: "shell-restart-test-" });
140  const root = rootDir.path;
141  const path = `${root}/berry.sqlite`;
142  let berry = await openBerry(await openNodeSqliteStorage(path), models);
143  try {
144    await berry.install(code);
145    const tid = await berry.create({ extensions: { environment: { cwd: root }, code: {} } });
146    const conversation = (await berry.conversation(tid))!;
147    await berry.shell(tid, "echo effect >> effects; printf started; sleep 30");
148    for (let i = 0; i < 200; i++) {
149      const run = (await berry.harness.snapshot(ShellDoc, conversation.id, context))?.run;
150      if ((run?.details as { rawOutput?: string } | null)?.rawOutput === "started") break;
151      if (i === 199) throw new Error("Command never started");
152      await new Promise((resolve) => setTimeout(resolve, 10));
153    }
154    await berry.close();
155    berry = await openBerry(await openNodeSqliteStorage(path), models);
156    await berry.install(code);
157    await berry.harness.waitForIdle(context);
158    const reopened = (await berry.conversation(tid))!;
159    const entries = (await reopened.entries({ order: "ascending" }, 100, undefined, context)).items;
160    const shell = entries.filter(ShellEntry.is);
161    assertEquals(shell.length, 1);
162    assertEquals(shell[0].data.status, "error");
163    assertStringIncludes(shell[0].data.error!, "not rerun");
164    assertEquals(await Deno.readTextFile(`${root}/effects`), "effect\n");
165    assertEquals((await berry.harness.snapshot(ShellDoc, reopened.id, context))?.run, undefined);
166  } finally {
167    await berry.close();
168  }
169});