import { afterEach, describe, expect, it } from "bun:test"; import { mkdirSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { MemoryStore } from "../src/agent/memory.ts"; import { CronScheduler } from "../src/agent/scheduler.ts"; import { SkillsManager } from "../src/agent/skills.ts"; import { type AgentInfo, type AgentManager, createTools, type ToolDeps } from "../src/agent/tools.ts"; function makeMemoryStore(): MemoryStore { const id = `test-agent-${crypto.randomUUID().slice(0, 8)}`; return new MemoryStore(join(tmpdir(), "mozart-agent-test", id, "memory.db")); } function makeDeps(overrides: Partial = {}): ToolDeps { const scheduler = new CronScheduler(); return { agentId: "test-agent", memoryStore: makeMemoryStore(), getConversationId: () => "conv-1", getRouter: () => null, getScheduler: () => scheduler, registerScheduleJob: () => true, getAgentManager: () => null, translateToCron: async () => "*/5 * * * *", getSkillsManager: () => ({ resolve: async () => ({}), resolveAll: async () => [], listInstalled: () => [], removeInstalled: () => false, search: async () => "", }) as any, getActiveSkills: () => [], addActiveSkill: () => {}, emitToolEvent: () => {}, ...overrides, }; } let cleanups: Array<() => void> = []; afterEach(() => { for (const cleanup of cleanups) { try { cleanup(); } catch {} } cleanups = []; }); describe("memory tools", () => { it("save_to_memory + search_memory round-trip", async () => { const deps = makeDeps(); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); const saveResult = await tools.save_to_memory.execute!({ content: "The user likes TypeScript" }, { toolCallId: "tc1", messages: [], } as any); expect(saveResult).toBe("Saved to memory."); const searchResult = await tools.search_memory.execute!({ query: "TypeScript" }, { toolCallId: "tc2", messages: [], } as any); expect(searchResult).toContain("TypeScript"); }); it("search_memory returns no results for empty store", async () => { const deps = makeDeps(); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); const result = await tools.search_memory.execute!({ query: "nonexistent" }, { toolCallId: "tc1", messages: [], } as any); expect(result).toBe("No relevant memories found."); }); }); describe("send tool", () => { it("routes message via router callback", async () => { let routedFrom = ""; let routedTo = ""; let routedMessage = ""; const deps = makeDeps({ getRouter: () => async (from: string, to: string, msg: string) => { routedFrom = from; routedTo = to; routedMessage = msg; return "Response from other agent"; }, }); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); const result = await tools.send.execute!({ agent_id: "other-agent", message: "Hello other" }, { toolCallId: "tc1", messages: [], } as any); expect(routedFrom).toBe("test-agent"); expect(routedTo).toBe("other-agent"); expect(routedMessage).toBe("Hello other"); expect(result).toBe("Response from other agent"); }); it("returns error when no router is set", async () => { const deps = makeDeps(); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); expect( tools.send.execute!({ agent_id: "x", message: "hi" }, { toolCallId: "tc1", messages: [] } as any), ).rejects.toThrow("No router available"); }); }); describe("schedule tools", () => { it("schedule tool registers the cron job locally", async () => { let registered = false; const deps = makeDeps({ registerScheduleJob: (_scheduleId, _cron, _task) => { registered = true; return true; }, }); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); const result = await tools.schedule.execute!({ timing: "every 5 minutes", task: "check health" }, { toolCallId: "tc1", messages: [], } as any); expect(result).toContain("Created schedule"); expect(result).toContain("*/5 * * * *"); expect(registered).toBe(true); }); it("delete_schedule unschedules the cron job locally", async () => { const scheduler = new CronScheduler(); const deps = makeDeps({ getScheduler: () => scheduler, }); cleanups.push(() => deps.memoryStore.delete()); deps.memoryStore.saveSchedule("s1", "*/5 * * * *", "every 5 min", "check health"); scheduler.schedule( { id: "s1", name: "test/s1", schedule: "*/5 * * * *", task: "check health", enabled: true }, async () => {}, ); const tools = createTools(deps); const result = await tools.delete_schedule.execute!({ schedule_id: "s1" }, { toolCallId: "tc1", messages: [], } as any); expect(result).toContain("Deleted schedule"); expect(scheduler.getAllJobs().length).toBe(0); }); it("delete_schedule returns message for non-existent schedule", async () => { const deps = makeDeps(); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); const result = await tools.delete_schedule.execute!({ schedule_id: "ghost" }, { toolCallId: "tc1", messages: [], } as any); expect(result).toContain("No schedule found"); }); it("list_schedules returns formatted list", async () => { const deps = makeDeps(); cleanups.push(() => deps.memoryStore.delete()); deps.memoryStore.saveSchedule("s1", "*/5 * * * *", "every 5 min", "check health"); const tools = createTools(deps); const result = await tools.list_schedules.execute!({}, { toolCallId: "tc1", messages: [] } as any); expect(result).toContain("s1"); expect(result).toContain("check health"); }); }); function makeMockManager(overrides: Partial = {}): AgentManager { const fakeInfo: AgentInfo = { id: "other-agent", state: "running", model: "test/model", identity: "test agent", uptime: 5000, messageCount: 3, restartCount: 0, scheduleCount: 0, }; return { listAgents: async () => [fakeInfo], stopAgent: async () => true, startAgent: async () => fakeInfo, removeAgent: async () => true, spawnAgent: async () => fakeInfo, getApiKey: () => "fake-key", ...overrides, }; } describe("orchestration tools", () => { it("list_agents returns formatted agent list", async () => { const deps = makeDeps({ getAgentManager: () => makeMockManager() }); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); const result = await tools.list_agents.execute!({}, { toolCallId: "tc1", messages: [] } as any); expect(result).toContain("other-agent"); expect(result).toContain("running"); }); it("stop_agent stops another agent", async () => { let stoppedId = ""; const deps = makeDeps({ getAgentManager: () => makeMockManager({ stopAgent: async (id) => { stoppedId = id; return true; }, }), }); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); const result = await tools.stop_agent.execute!({ agent_id: "other-agent" }, { toolCallId: "tc1", messages: [], } as any); expect(result).toContain("stopped"); expect(stoppedId).toBe("other-agent"); }); it("stop_agent rejects self-targeting", async () => { const deps = makeDeps({ getAgentManager: () => makeMockManager() }); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); expect( tools.stop_agent.execute!({ agent_id: "test-agent" }, { toolCallId: "tc1", messages: [] } as any), ).rejects.toThrow("cannot stop yourself"); }); it("start_agent restarts a stopped agent", async () => { const deps = makeDeps({ getAgentManager: () => makeMockManager() }); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); const result = await tools.start_agent.execute!({ agent_id: "other-agent" }, { toolCallId: "tc1", messages: [], } as any); expect(result).toContain("started"); }); it("remove_agent removes another agent", async () => { let removedId = ""; const deps = makeDeps({ getAgentManager: () => makeMockManager({ removeAgent: async (id) => { removedId = id; return true; }, }), }); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); const result = await tools.remove_agent.execute!({ agent_id: "other-agent" }, { toolCallId: "tc1", messages: [], } as any); expect(result).toContain("permanently removed"); expect(removedId).toBe("other-agent"); }); it("remove_agent rejects self-targeting", async () => { const deps = makeDeps({ getAgentManager: () => makeMockManager() }); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); expect( tools.remove_agent.execute!({ agent_id: "test-agent" }, { toolCallId: "tc1", messages: [] } as any), ).rejects.toThrow("cannot remove yourself"); }); it("spawn_agent creates a new agent from content", async () => { let spawnedName = ""; let spawnedContent = ""; const agentContent = "MODEL openai/gpt-4.1-mini\nSOUL A test helper."; const deps = makeDeps({ getAgentManager: () => makeMockManager({ spawnAgent: async (name, content, _apiKey) => { spawnedName = name; spawnedContent = content; return { id: name, state: "running", model: "openai/gpt-4.1-mini", identity: "A test helper.", uptime: 0, messageCount: 0, restartCount: 0, scheduleCount: 0, }; }, }), }); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); const result = await tools.spawn_agent.execute!({ name: "helper", content: agentContent }, { toolCallId: "tc1", messages: [], } as any); expect(result).toContain("spawned"); expect(result).toContain("helper"); expect(spawnedName).toBe("helper"); expect(spawnedContent).toBe(agentContent); }); }); describe("create_skill tool", () => { it("creates a skill with valid name", async () => { const skillsDir = join(tmpdir(), `mozart-skill-test-${Date.now()}`); const mgr = new SkillsManager(skillsDir); const activeSkills: any[] = []; const deps = makeDeps({ getSkillsManager: () => mgr, getActiveSkills: () => activeSkills, addActiveSkill: (s: any) => activeSkills.push(s), }); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); const result = await tools.create_skill.execute!( { name: "my-skill", content: "# My Skill\nDoes stuff." }, { toolCallId: "tc1", messages: [] } as any, ); expect(result).toContain("Created and activated"); expect(activeSkills).toHaveLength(1); expect(activeSkills[0].name).toBe("my-skill"); }); it("rejects invalid name", async () => { const skillsDir = join(tmpdir(), `mozart-skill-test-${Date.now()}`); const mgr = new SkillsManager(skillsDir); const deps = makeDeps({ getSkillsManager: () => mgr }); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); await expect( tools.create_skill.execute!( { name: "INVALID NAME!", content: "content" }, { toolCallId: "tc1", messages: [] } as any, ), ).rejects.toThrow("lowercase alphanumeric"); }); it("activates the created skill", async () => { const skillsDir = join(tmpdir(), `mozart-skill-test-${Date.now()}`); const mgr = new SkillsManager(skillsDir); const activeSkills: any[] = []; const deps = makeDeps({ getSkillsManager: () => mgr, getActiveSkills: () => activeSkills, addActiveSkill: (s: any) => activeSkills.push(s), }); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); await tools.create_skill.execute!( { name: "activated-skill", content: "---\nname: activated-skill\ndescription: Test\n---\nContent here" }, { toolCallId: "tc1", messages: [] } as any, ); expect(activeSkills).toHaveLength(1); expect(activeSkills[0].content).toContain("Content here"); }); }); describe("install_skill tool", () => { it("rejects invalid ref format", async () => { const deps = makeDeps(); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); await expect( tools.install_skill.execute!( { package: "not-valid-ref" }, { toolCallId: "tc1", messages: [] } as any, ), ).rejects.toThrow("Invalid package reference"); }); it("detects duplicate installation", async () => { const activeSkills: any[] = [{ name: "code-review", ref: "vercel-labs/skills@code-review" }]; const deps = makeDeps({ getActiveSkills: () => activeSkills, }); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); const result = await tools.install_skill.execute!( { package: "vercel-labs/skills@code-review" }, { toolCallId: "tc1", messages: [] } as any, ); expect(result).toContain("already installed"); }); }); describe("read_skill tool", () => { it("throws when skill not found", async () => { const deps = makeDeps({ getActiveSkills: () => [] }); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); await expect( tools.read_skill.execute!( { name: "nonexistent" }, { toolCallId: "tc1", messages: [] } as any, ), ).rejects.toThrow("No active skills"); }); it("returns skill content for active skill", async () => { const activeSkills: any[] = [ { name: "my-skill", ref: "", content: "# My Skill\nDetailed content." }, ]; const deps = makeDeps({ getActiveSkills: () => activeSkills }); cleanups.push(() => deps.memoryStore.delete()); const tools = createTools(deps); const result = await tools.read_skill.execute!( { name: "my-skill" }, { toolCallId: "tc1", messages: [] } as any, ); expect(result).toContain("My Skill"); expect(result).toContain("Detailed content"); }); });