/** * Contract tests for `TauriAgentGateway`: they assert the exact `invoke()` * payload shape, which the mock gateway (used by feature tests) does NOT * exercise. This guards the class of "works in mock, broken in the real app" * bugs — e.g. forgetting to nest `projectId` inside the command's `request` DTO. */ import { describe, it, expect, vi, beforeEach } from "vitest"; const invoke = vi.fn(); vi.mock("@tauri-apps/api/core", () => ({ invoke: (...args: unknown[]) => invoke(...args), Channel: class { onmessage: ((c: number[]) => void) | null = null; }, })); import { TauriAgentGateway } from "./agent"; describe("TauriAgentGateway invoke payloads", () => { beforeEach(() => invoke.mockReset().mockResolvedValue({})); it("create_agent nests projectId inside the request DTO", async () => { await new TauriAgentGateway().createAgent("proj-1", { name: "Backend", profileId: "prof-9", }); expect(invoke).toHaveBeenCalledWith("create_agent", { request: { projectId: "proj-1", name: "Backend", profileId: "prof-9", initialContent: null, }, }); }); it("update_agent_context wraps fields in the request DTO", async () => { await new TauriAgentGateway().updateContext("proj-1", "agent-2", "# ctx"); expect(invoke).toHaveBeenCalledWith("update_agent_context", { request: { projectId: "proj-1", agentId: "agent-2", content: "# ctx" }, }); }); it("list_agents / read / delete pass top-level args (no request wrapper)", async () => { const gw = new TauriAgentGateway(); await gw.listAgents("p"); expect(invoke).toHaveBeenCalledWith("list_agents", { projectId: "p" }); await gw.readContext("p", "a"); expect(invoke).toHaveBeenCalledWith("read_agent_context", { projectId: "p", agentId: "a", }); await gw.deleteAgent("p", "a"); expect(invoke).toHaveBeenCalledWith("delete_agent", { projectId: "p", agentId: "a", }); }); it("attach_live_agent wraps project, agent and target node in the request DTO", async () => { invoke.mockResolvedValueOnce([ { agentId: "agent-2", nodeId: "node-3", sessionId: "session-4" }, ]); await new TauriAgentGateway().attachLiveAgent("proj-1", "agent-2", "node-3"); expect(invoke).toHaveBeenCalledWith("attach_live_agent", { request: { projectId: "proj-1", agentId: "agent-2", nodeId: "node-3" }, }); }); it("change_agent_profile wraps all five fields in the request DTO (camelCase)", async () => { invoke.mockResolvedValueOnce({ agent: { id: "agent-2" } }); await new TauriAgentGateway().changeAgentProfile( "proj-1", "agent-2", "prof-9", 30, 120, ); expect(invoke).toHaveBeenCalledWith("change_agent_profile", { request: { projectId: "proj-1", agentId: "agent-2", profileId: "prof-9", rows: 30, cols: 120, }, }); }); it("change_agent_profile returns the mutated agent and the relaunched session", async () => { const response = { agent: { id: "agent-2", profileId: "prof-9" }, relaunchedSession: { sessionId: "sess-7", cwd: "/p", rows: 30, cols: 120 }, }; invoke.mockResolvedValueOnce(response); const out = await new TauriAgentGateway().changeAgentProfile( "proj-1", "agent-2", "prof-9", 30, 120, ); expect(out.agent.profileId).toBe("prof-9"); expect(out.relaunchedSession?.sessionId).toBe("sess-7"); }); it("change_agent_profile leaves relaunchedSession undefined when the backend omits it", async () => { invoke.mockResolvedValueOnce({ agent: { id: "agent-2" } }); const out = await new TauriAgentGateway().changeAgentProfile( "proj-1", "agent-2", "prof-9", 30, 120, ); expect(out.relaunchedSession).toBeUndefined(); }); });