tests/
Put test files in tests/. Same runner as agent tests — bun:test, with the Axon()
harness available as a global. The difference: you are not testing the module in
isolation. Modules have no standalone runtime. You boot a parent agent with the module
loaded and test the contribution surface — what tools appeared, what prompts render,
how hooks behave.
tests/
├── hooks.test.ts
├── prompts.test.ts
└── tools.test.ts
The harness
Run tests from inside an agent that has your module installed. The harness boots that
agent's full runtime — your module's setup() runs, its tools are registered, its
prompts are available.
import { Mock } from "@arcforge/engines/mock"
describe("discord module", () => {
it("registers the discord tool namespace", async () => {
const runtime = await Axon({
blueprint: { config: { engine: Mock() } },
})
expect(runtime.axon.tools.discord).toBeDefined()
await runtime.shutdown()
})
})
Testing the tool surface
Verify that the tools your module declares are present and callable. Call them directly
via runtime.axon.tools.* — no agent loop involved, just the function.
it("queue returns empty state when nothing is playing", async () => {
const runtime = await Axon({ blueprint: { config: { engine: Mock() } } })
const state = await runtime.axon.tools.discord.queue(guildId)
expect(state).toEqual({ current: null, upcoming: [] })
await runtime.shutdown()
})
Tools that depend on external connections (Discord client, databases, IMAP) will throw in test environments where those services aren't present. Test the logic you control — state management, return shapes, guard conditions — not the external service.
Testing prompts
Render the prompts your module contributes and assert on their content. This catches template regressions — broken variable interpolation, missing sections, stale copy.
it("renders the discord prompt with required props", async () => {
const runtime = await Axon({ blueprint: { config: { engine: Mock() } } })
const prompt = await runtime.axon.prompt("discord", {
content: "hello",
username: "cody",
channelId: "123",
})
expect(prompt.content).toContain("cody")
expect(prompt.content).toContain("hello")
await runtime.shutdown()
})
Test the dynamic cases — prompts that branch on data. If your prompt surfaces now-playing state conditionally, assert it appears when the data is present and doesn't when it isn't.
Testing hooks
Your module emits hooks — fire them directly via callHook() and assert on the
outcome. This is how you test the full path: hook fires, agent processes it, callback
receives a reply.
it("fires a reply when a message hook is triggered", async () => {
const runtime = await Axon({
blueprint: { config: { engine: Mock({ "": "hey cody" }) } },
})
let replied: string | null = null
await runtime.axon.hooks.callHook("discord:message.received", {
content: "hello barry",
username: "cody",
reply: async (text: string) => { replied = text },
})
expect(replied).toBe("hey cody")
await runtime.shutdown()
})
Testing tool calls through the loop
When the agent should respond to a message by calling one of your module's tools, script the model with a sequence — one step per loop tick. The tool genuinely executes in the capsule; the second tick sees its result and speaks.
import { Mock, run } from "@arcforge/engines/mock"
it("agent calls play tool when asked", async () => {
const runtime = await Axon({
blueprint: {
config: {
engine: Mock({
"play something": [
run(`discord.play("track", userId, guildId)`),
"Now playing.",
],
}),
},
},
})
const result = await runtime.axon.request("play something")
expect(result.text).toBe("Now playing.")
await runtime.shutdown()
})
Running tests
Run from inside the agent directory that has your module installed:
bun test
bun test tests/hooks.test.ts
bun test --watch