Skip to content
WIP

This guide is a work in progress. Barry is also a work in progress.

Test a bag

Use this when you want a bag’s tools, store, and calls to Barry checked by tests that run without a live Barry. A tool is a plain object with a handler, so a test calls it directly; a store opens a temp file or an in-memory database; and @barry-rocks/sdk/testing supplies fakes for every subject Barry answers for. The examples test the notes bag from Add a store and the pulse bag from Call Barry from a bag.

  • A bag with tools, as Create a bag builds.
  • Add vitest to the bag’s devDependencies and a test script that runs vitest run, as the notes bag’s package.json does.

Import the tool from tools.ts and call its handler with the parameters. Point the store at an in-memory database first, so each test starts empty:

import Database from "better-sqlite3";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { add, list } from "../tools.js";
import { closeDb, setDb } from "./db.js";
beforeEach(() => setDb(new Database(":memory:")));
afterEach(() => closeDb());
describe("notes tools", () => {
it("adds a note and lists it", async () => {
const note = await add.handler({ text: "buy milk" });
expect(note.text).toBe("buy milk");
expect(await list.handler({})).toEqual([note]);
});
it("respects the limit", async () => {
await add.handler({ text: "one" });
await add.handler({ text: "two" });
const notes = await list.handler({ limit: 1 });
expect(notes.map((n) => n.text)).toEqual(["two"]);
});
});

The result is the handler’s own return value, typed, not the text an agent receives. Calling a handler directly skips the schema: zod doesn’t validate the parameters or apply defaults, so pass every parameter the handler reads. When the handler reads its context, pass one as the second argument, such as { secrets: { GITHUB_TOKEN: "test" } }.

setDb replaces the store’s handle and migrates it. To test the code that opens the store, point it at a file in the temp directory instead, through the environment variable the bag’s path option reads:

import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { closeDb } from "./db.js";
import { addNote, listNotes } from "./store.js";
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "notes-test-"));
process.env.BARRY_NOTES_DB = join(dir, "notes.db");
closeDb();
});
afterEach(() => {
closeDb();
delete process.env.BARRY_NOTES_DB;
rmSync(dir, { recursive: true, force: true });
});
describe("notes store", () => {
it("lists the newest note first", () => {
addNote("first");
addNote("second");
expect(listNotes().map((note) => note.text)).toEqual(["second", "first"]);
});
});

Under vitest, a store opened with a scratchGuard refuses any path outside the temp directory, and bagDataDir refuses to create a directory in the real installation. A test that forgets to set the path fails loudly instead of writing to your real data.

The same suite can check that the SQL the bag carries in code matches the migration files Barry reads, since Barry checksums both:

import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { MIGRATIONS } from "./migrations.js";
describe("migrations", () => {
it("match the SQL files Barry reads", () => {
for (const migration of MIGRATIONS) {
const file = new URL(`../migrations/notes/${migration.name}`, import.meta.url);
expect(migration.sql).toBe(readFileSync(file, "utf8"));
}
});
});

A tool that calls Barry needs something to answer. Each fake in @barry-rocks/sdk/testing stands in for one subject: fakeIdentities, fakeSessions, fakeActions, fakeEvents, fakeSchedules, fakeBags, fakeTracker, and fakeServices. Pass the methods the test depends on; every other method throws when called, naming itself, so a test states exactly what it relies on. Install the fakes with Barry.use, and clear them with Barry.reset:

import { Barry, type BarryEvent, type EventInput, type Schedule } from "@barry-rocks/sdk";
import { fakeEvents, fakeSchedules } from "@barry-rocks/sdk/testing";
import { afterEach, describe, expect, it } from "vitest";
import { announce, scheduleHealth } from "../tools.js";
afterEach(() => Barry.reset());
describe("announce", () => {
it("records a notification for the calling session", async () => {
const recorded: EventInput[] = [];
Barry.use(fakeEvents({
record: async (input) => {
recorded.push(input);
return event("evt_1", input);
},
}));
const result = await announce.handler({ title: "Deployed" }, { secrets: {}, sessionId: "ses_1" });
expect(result).toEqual({ id: "evt_1", createdAt: "2026-10-14T09:00:00.000Z" });
expect(recorded).toEqual([{ type: "notification", severity: "info", title: "Deployed", session: "ses_1" }]);
});
});
describe("schedule_health", () => {
it("names the schedules that keep failing", async () => {
Barry.use(fakeSchedules({
scheduler: async () => ({ status: "ok", reason: null, heartbeatAt: null, loadErrors: [] }),
list: async () => [schedule("standup/morning", 0), schedule("standup/sweep", 3)],
}));
expect(await scheduleHealth.handler({})).toEqual({ scheduler: "ok", failing: ["standup/sweep"] });
});
});
function event(id: string, input: EventInput): BarryEvent {
return {
id, type: input.type, sessionId: input.session ?? null, source: "pulse", title: input.title,
body: input.body ?? null, severity: input.severity ?? "info", data: {}, metadata: {}, deliveredVia: [],
readAt: null, createdAt: "2026-10-14T09:00:00.000Z",
};
}
function schedule(id: string, consecutiveFailures: number): Schedule {
return {
id, trigger: "0 9 * * 1-5", action: "standup-notes", enabled: true, paused: false, nextFireAt: null,
lastFiredAt: null, lastStatus: null, lastSummary: null, lastRunId: null, consecutiveFailures,
skippedFires: 0, missedFires: 0,
};
}

Barry.reset forgets every installed subject, any connection, and the caller resolver, so one test’s fakes never answer another’s calls. The fake receives the same arguments a real subject would, with the caller last, so a test can assert on exactly what the tool sent.

To run the suite, run the bag’s test script from its directory:

Terminal
pnpm test

Vitest reports each test file and test as passed. For the notes bag, that is three files and four tests.

  • A test fails with Refusing to open the notes store at ... from a test run. The test opens the store at its real location. Set BARRY_NOTES_DB to a temp path in beforeEach, or call setDb with an in-memory database.
  • A test fails with fake ... was called, but this test did not supply it. The tool calls a method the fake doesn’t define. Pass that method to the fake.
  • A test fails with Nothing answers for ... in this process. The tool calls Barry and the test installed no fake for that subject. Call Barry.use with one.
  • For anything else, see Troubleshooting.