import { assert, assertEquals, assertRejects, assertStringIncludes } from "@std/assert"; import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context"; import { Type } from "@earendil-works/pi-ai"; import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai/providers/faux"; import { mkdir, symlink, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import { join } from "node:path"; import { defineExtension } from "@berry/api"; import { type Environment, localEnvironment } from "@berry/api/environment.ts"; import { BerryError, Settings } from "./berry.ts"; import { type Environments, type EnvironmentSettings, localOnly } from "./environment.ts"; import { lastToolResult, script, sectionsSeen, setup, turn } from "./fixture.ts"; import { tempDir } from "@berry/util"; /** * This machine as `local`, a directory of it posing as a machine named `other`, and a machine named `down` that * cannot be reached. */ function twoMachines(otherHome: string): Environments { return { open(name, cwd, during) { if (name === "down") return Promise.reject(new Error("down is unreachable")); if (name !== "other") return localOnly.open(name, cwd, during); const env: Environment = Object.assign(localEnvironment(cwd ?? otherHome), { name, home: otherHome, }); return Promise.resolve(env); }, close: () => Promise.resolve(), }; } async function withDirectories(test: (root: string) => Promise) { await using rootDir = await tempDir({ prefix: "berry-environment-" }); const root = rootDir.path; await mkdir(join(root, "a")); await mkdir(join(root, "b")); await mkdir(join(root, "other")); await writeFile(join(root, "file"), ""); await symlink(join(root, "a"), join(root, "link")); await test(root); } Deno.test("a conversation works in the local home until bound to a directory its machine resolves, which /cd moves", () => withDirectories(async (root) => { await using harness = await setup(twoMachines(join(root, "other"))); const { berry, model } = harness; const home = await berry.create({ extensions: {}, model }); assertEquals(await berry.state(home, "environment"), { environment: "local", cwd: null }); const env = await berry.environment(home); assertEquals([env.name, env.cwd], ["local", homedir()]); const tid = await berry.create({ extensions: { environment: { cwd: join(root, "link") } }, model, }); assertEquals((await berry.environment(tid)).cwd, join(root, "a")); for (const cwd of [join(root, "file"), join(root, "missing")]) { await assertRejects(() => berry.create({ extensions: { environment: { cwd } }, model })); } await assertRejects( () => berry.create({ extensions: { environment: { environment: "elsewhere" } }, model }), BerryError, "No environment named elsewhere", ); await berry.configureExtension(tid, "environment", { cwd: "../b" }); assertEquals((await berry.environment(tid)).cwd, join(root, "b")); const other = await berry.create({ extensions: { environment: { environment: "other", cwd: join(root, "a") } }, model, }); assertEquals(await berry.command(other, "cd", "../link"), join(root, "a")); assertEquals(await berry.command(other, "cd", " ../b "), join(root, "b")); await assertRejects(() => berry.command(other, "cd", "../file"), BerryError, "Not a directory"); await assertRejects(() => berry.command(other, "cd", "missing"), BerryError); // home is the machine's assertEquals(await berry.command(other, "cd", ""), join(root, "other")); assertEquals(await berry.state(other, "environment"), { environment: "other", cwd: join(root, "other"), }); })); Deno.test("the code extension's working directory carries over from old history", async () => { await using harness = await setup(); const { berry, model } = harness; const tid = await berry.create({ extensions: {}, model }); await (await berry.emit(tid, "coding", { type: "cd", cwd: "/first" })).wait(context); assertEquals(await berry.state(tid, "environment"), { environment: "local", cwd: "/first" }); await (await berry.emit(tid, "code", { type: "cd", cwd: "/second" })).wait(context); assertEquals(await berry.state(tid, "environment"), { environment: "local", cwd: "/second" }); }); Deno.test("only an idle conversation moves to another machine; a new directory waits for the boundary", () => withDirectories(async (root) => { await using harness = await setup(twoMachines(join(root, "other"))); const { berry, model, faux, gate } = harness; const tid = await berry.create({ extensions: { environment: { cwd: join(root, "a") }, gate: {} }, model, }); faux.setResponses([ fauxAssistantMessage(fauxToolCall("hold", {}), { stopReason: "toolUse" }), fauxAssistantMessage("done"), ]); const conversation = (await berry.conversation(tid))!; await conversation.submit({ type: "input", content: "go" }, context); await gate.entered.promise; await assertRejects( () => berry.configureExtension(tid, "environment", { environment: "other" }), BerryError, "busy", ); const moved = berry.configureExtension(tid, "environment", { cwd: join(root, "b") }); assertEquals(await berry.state(tid, "environment"), { environment: "local", cwd: join(root, "a"), }); gate.release.resolve("released"); assertEquals(await moved, { environment: "local", cwd: join(root, "b") }); await conversation.waitForIdle(context); // the form still holds this machine's directory, which means nothing on the other assertEquals( await berry.configureExtension(tid, "environment", { environment: "other", cwd: join(root, "b"), }), { environment: "other", cwd: join(root, "other") }, ); const env = await berry.environment(tid); assertEquals([env.name, env.cwd], ["other", join(root, "other")]); })); /** Reports the environment wherever an extension meets it. */ const probe = defineExtension({ name: "probe", initial: () => null, setup(ext) { const where = (env: Environment) => `${env.name}:${env.cwd}`; ext.tool({ name: "where", description: "where calls run", parameters: Type.Object({}), execute: (_, ctx) => where(ctx.env), }); ext.prompt("named", ({ env }) => env && `named ${where(env)}`); return (conv) => { conv.prompt(({ env }) => env && `in ${where(env)}`); conv.command({ name: "go", description: "", execute: (path, ctx) => ctx.cd(path) }); conv.command({ name: "here", description: "", execute: (_, ctx) => where(ctx.env) }); conv.command({ name: "ping", description: "", execute: () => "pong" }); }; }, }); Deno.test("calls, prompts, and commands all meet the conversation's environment, and fail with a machine out of reach", () => withDirectories(async (root) => { await using harness = await setup(twoMachines(join(root, "other"))); const { berry, model } = harness; await berry.install(probe); const tid = await berry.create({ extensions: { environment: { cwd: join(root, "a") }, probe: {} }, model, }); const seen = await turn(harness, tid, [script("text(await tools.where({}))")]); assertEquals(lastToolResult(seen[1]).text, `Script completed\nlocal:${join(root, "a")}`); const sections = sectionsSeen(seen[0])[0]; assertEquals(sections.probe, `\nin local:${join(root, "a")}\n`); assertEquals( sections.probe_named, `\nnamed local:${join(root, "a")}\n`, ); assertEquals(sections.environment, undefined); assertEquals(await berry.command(tid, "go", "../b"), join(root, "b")); assertEquals(await berry.command(tid, "here", ""), `local:${join(root, "b")}`); await berry.configureExtension(tid, "environment", { environment: "other" }); const [moved] = await turn(harness, tid, []); assertEquals( sectionsSeen(moved).at(-1)?.probe, `\nin other:${join(root, "other")}\n`, ); assertEquals( sectionsSeen(moved).at(-1)?.environment?.includes("files and commands are on other"), true, ); // as though it went down after the conversation moved there await (await berry.emit(tid, "environment", { type: "bind", environment: "down", cwd: "/" })) .wait(context); const down = await turn(harness, tid, [script("text(await tools.where({}))")]); const result = lastToolResult(down[1]); assertEquals(result.isError, true); assertEquals(result.text.includes("down is unreachable"), true); const unreachable = sectionsSeen(down[0]).at(-1)!; assertEquals(unreachable.probe, null); assertEquals(unreachable.environment?.includes("down cannot be reached"), true); assertEquals(await berry.command(tid, "ping", ""), "pong"); await assertRejects(() => berry.command(tid, "here", ""), BerryError, "down is unreachable"); })); Deno.test("disabling local fails its turns without contacting the model, but remote turns and recovery still work", async () => { await using root = await tempDir(); await using harness = await setup(twoMachines(root.path)); const { berry, model, faux } = harness; const tid = await berry.create({ extensions: {}, model }); await berry.setAllowLocalEnvironment(false); const calls = faux.state.callCount; assertEquals(await turn(harness, tid, []), []); assertEquals(faux.state.callCount, calls); const conversation = (await berry.conversation(tid))!; const entries = await conversation.entries({ order: "ascending" }, 100, undefined, context); const failure = entries.items.flatMap((entry) => entry.model ?? []).findLast((message) => message.role === "assistant" ); assert(failure?.role === "assistant"); assertEquals(failure.stopReason, "error"); assertStringIncludes(failure.errorMessage ?? "", "local environment is disabled"); await assertRejects(() => berry.environment(tid), BerryError, "local environment is disabled"); await assertRejects( () => berry.command(tid, "cd", ""), BerryError, "local environment is disabled", ); await berry.configureExtension(tid, "environment", { environment: "other" }); assertEquals((await turn(harness, tid, [])).length, 1); await assertRejects( () => berry.configureExtension(tid, "environment", { environment: "local" }), BerryError, "local environment is disabled", ); await berry.setAllowLocalEnvironment(true); await berry.configureExtension(tid, "environment", { environment: "local" }); assertEquals((await turn(harness, tid, [])).length, 1); }); Deno.test("settings name the other machines, and nothing ssh could take for an option", async () => { await using harness = await setup(); const { berry } = harness; const refusals: Record[] = [ { local: { ssh: "elsewhere" } }, { "-oProxyCommand=x": { ssh: "elsewhere" } }, { devbox: { ssh: "-oProxyCommand=x" } }, { devbox: { ssh: " " } }, { devbox: { ssh: "devbox", deno: "" } }, ]; for (const refused of refusals) { await assertRejects(() => berry.setEnvironments(refused), BerryError); } await berry.setEnvironments({ devbox: { ssh: " me@devbox ", deno: "/opt/deno" } }); assertEquals((await berry.harness.snapshot(Settings, context))?.environments, { devbox: { ssh: "me@devbox", deno: "/opt/deno" }, }); });