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.
Before you begin
Section titled “Before you begin”- A bag with tools, as Create a bag builds.
- Add vitest to the bag’s
devDependenciesand atestscript that runsvitest run, as thenotesbag’spackage.jsondoes.
Call a tool’s handler
Section titled “Call a tool’s handler”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" } }.
Test against a temp store
Section titled “Test against a temp store”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")); } });});Replace Barry with fakes
Section titled “Replace Barry with fakes”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.
Verify
Section titled “Verify”To run the suite, run the bag’s test script from its directory:
pnpm testVitest reports each test file and test as passed. For the notes bag, that is
three files and four tests.
If it fails
Section titled “If it fails”- A test fails with
Refusing to open the notes store at ... from a test run. The test opens the store at its real location. SetBARRY_NOTES_DBto a temp path inbeforeEach, or callsetDbwith 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 callsBarryand the test installed no fake for that subject. CallBarry.usewith one. - For anything else, see Troubleshooting.
What’s next
Section titled “What’s next”- Write a tool for what the agent receives from each kind of result.
- The
/testingSDK reference for the fakes’ signatures.