import { assertEquals, assertRejects } from "@std/assert"; import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context"; import { type Message, Type } from "@earendil-works/pi-ai"; import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai/providers/faux"; import { defineExtension, type JsonValue } from "@berry/api"; import { BerryError } from "./berry.ts"; import { sectionsSeen, setup } from "./fixture.ts"; const configurable = defineExtension< { cwd: string; verbose: boolean; edits: number }, { cwd: string; verbose: boolean } >({ name: "configurable", initial: () => ({ cwd: "/", verbose: false, edits: 0 }), apply: (state, event) => ({ ...state, ...event, edits: state.edits + 1 }), setup(ext) { ext.configuration({ parameters: Type.Object({ cwd: Type.String({ default: "/" }), verbose: Type.Boolean({ default: false }), }), values: ({ cwd, verbose }) => ({ cwd, verbose }), events: ({ cwd, verbose }) => { if (!cwd.startsWith("/")) throw new Error(`not absolute: ${cwd}`); return [{ cwd, verbose }]; }, }); return (conv) => conv.prompt(() => `cwd: ${conv.state.cwd}`); }, }); Deno.test("configuration seeds new conversations and exposes only editable state", async () => { await using harness = await setup(); const { berry } = harness; await berry.install(configurable); const tid = await berry.create({ extensions: { configurable: { cwd: "/seed" } } }); const feed = (await berry.watchConfiguration(tid, "configurable"))!; assertEquals(feed.value, { cwd: "/seed", verbose: false }); await feed.stop(); const defaults = await berry.create({ extensions: { configurable: {} } }); assertEquals(await berry.state(defaults, "configurable"), { cwd: "/", verbose: false, edits: 1, }); await assertRejects( () => berry.create({ extensions: { configurable: { cwd: 5 } } }), BerryError, "cwd", ); await assertRejects( () => berry.create({ extensions: { configurable: { cwd: "relative" } } }), BerryError, "not absolute", ); }); Deno.test("configuration patches preserve other fields, validate before writing, and replay from events", async () => { await using harness = await setup(); const { berry } = harness; await berry.install(configurable); const tid = await berry.create({ extensions: { configurable: { cwd: "/seed" } } }); assertEquals(await berry.configureExtension(tid, "configurable", { verbose: true }), { cwd: "/seed", verbose: true, }); const invalid: Record[] = [{ cwd: 5 }, { cwd: "relative" }, { edits: 999 }]; for (const change of invalid) { await assertRejects(() => berry.configureExtension(tid, "configurable", change), BerryError); } await berry.install(configurable); assertEquals(await berry.state(tid, "configurable"), { cwd: "/seed", verbose: true, edits: 2 }); await berry.configure(tid, { extensions: [] }); await assertRejects( () => berry.configureExtension(tid, "configurable", { cwd: "/disabled" }), BerryError, "no enabled configuration", ); await assertRejects(() => berry.configureExtension(tid, "missing", {}), BerryError); }); Deno.test("a configuration edit lands at once while busy and reaches the turn's next request", async () => { await using harness = await setup(); const { berry, faux, model, gate } = harness; try { await berry.install(configurable); const tid = await berry.create({ extensions: { configurable: {}, gate: {} }, model }); let next: readonly Message[] = []; faux.setResponses([ fauxAssistantMessage(fauxToolCall("hold", {}), { stopReason: "toolUse" }), (ctx) => (next = ctx.messages, fauxAssistantMessage("done")), ]); const conversation = (await berry.conversation(tid))!; const submission = await conversation.submit({ type: "input", content: "go" }, context); await gate.entered.promise; assertEquals(await berry.configureExtension(tid, "configurable", { cwd: "/mid" }), { cwd: "/mid", verbose: false, }); assertEquals(await berry.state(tid, "configurable"), { cwd: "/mid", verbose: false, edits: 2 }); gate.release.resolve("released"); await submission.wait(context); await conversation.waitForIdle(context); assertEquals( sectionsSeen(next).at(-1)?.configurable, "\ncwd: /mid\n", ); } finally { gate.release.resolve("released"); } });