import type { PrismaClient } from "@rakazo/db"; import { describe, expect, it } from "vitest"; import { COACH_TOOL_NAMES, executeCoachTool } from "./coach-tools.js"; import { selectBuiltinToolsForRun } from "./executor-tools.js"; function toolNames(options: { mainBot?: boolean; /** A project's main bot rather than the user's personal agent. */ projectLead?: boolean; messagingChannelRun?: boolean; foreignAgentRun?: boolean; trigger?: string; }) { return selectBuiltinToolsForRun({ graphicalToolsAllowed: false, groupId: null, trigger: options.trigger ?? "user", semanticMemoryEnabled: false, messagingChannelRun: options.messagingChannelRun ?? false, foreignAgentRun: options.foreignAgentRun, bot: { kind: options.mainBot ? "main" : "specialist", restrictBuiltinTools: false, allowedBuiltinTools: [], personalAgent: options.mainBot && !options.projectLead, }, }).map((tool) => tool.name); } /** * Only the space lookup that tells the personal agent from a project lead may reach the * database: refusals and invalid input must not write anything. */ const lookupOnlyPrisma = new Proxy( { space: { findUnique: async ({ where }: { where: { id: string } }) => ({ isDefault: where.id === "space-home", }), }, }, { get(target, property) { if (property === "space") return target.space; throw new Error(`unexpected database access: ${String(property)}`); }, }, ) as unknown as PrismaClient; const context = (kind: string, spaceId = "space-home") => ({ userId: "user-1", spaceId, bot: { id: "bot-1", kind }, }); describe("coach tools", () => { it("are the personal main agent's alone, also in its check-in routines, and never for other people", () => { for (const name of COACH_TOOL_NAMES) { expect(toolNames({ mainBot: true })).toContain(name); expect(toolNames({ mainBot: true, trigger: "routine" })).toContain(name); expect(toolNames({ mainBot: false })).not.toContain(name); expect(toolNames({ mainBot: true, projectLead: true })).not.toContain(name); expect(toolNames({ mainBot: true, messagingChannelRun: true })).not.toContain(name); expect(toolNames({ mainBot: true, foreignAgentRun: true })).not.toContain(name); } }); it("refuse specialists and project leads even when they call a tool anyway", async () => { for (const name of COACH_TOOL_NAMES) { for (const caller of [context("specialist"), context("main", "space-project")]) { await expect( executeCoachTool(lookupOnlyPrisma, caller, name, { title: "Run" }), ).resolves.toEqual({ error: "Only the user's main bot can coach the user." }); } } }); it("reject invalid or incomplete arguments before writing", async () => { const main = context("main"); await expect(executeCoachTool(lookupOnlyPrisma, main, "update_goal", {})).resolves.toEqual({ error: "A new goal needs a title.", }); await expect( executeCoachTool(lookupOnlyPrisma, main, "update_goal", { title: "x", target_date: "soon" }), ).resolves.toEqual({ error: expect.stringContaining("target_date") }); await expect( executeCoachTool(lookupOnlyPrisma, main, "update_habit", { title: "Walk", cadence: "hourly", }), ).resolves.toEqual({ error: expect.stringContaining("cadence") }); await expect( executeCoachTool(lookupOnlyPrisma, main, "log_check_in", { habit: "Walk", goal: "10k" }), ).resolves.toEqual({ error: "Log a habit and a goal in separate calls." }); await expect( executeCoachTool(lookupOnlyPrisma, main, "log_check_in", { mood: 9 }), ).resolves.toEqual({ error: expect.stringContaining("mood") }); await expect( executeCoachTool(lookupOnlyPrisma, main, "write_daily_brief", { text: " " }), ).resolves.toEqual({ error: expect.stringContaining("text") }); }); });