import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("../lib/audit.js", () => ({ createAuditEntry: vi.fn().mockResolvedValue(undefined), })); vi.mock("../router/assistant-approvals.js", () => ({ clearPendingAssistantApproval: vi.fn().mockResolvedValue(undefined), consumePendingAssistantApproval: vi.fn(), toApprovalPayload: vi.fn((approval: { id: string; toolName: string; summary: string }, status: string) => ({ id: approval.id, toolName: approval.toolName, summary: approval.summary, status, })), })); vi.mock("../router/assistant-confirmation.js", () => ({ canExecuteMutationTool: vi.fn(), isCancellationReply: vi.fn(), parseToolArguments: vi.fn(() => ({ name: "Apollo" })), })); vi.mock("../router/assistant-insights.js", () => ({ buildAssistantInsight: vi.fn(), })); vi.mock("../router/assistant-tool-results.js", () => ({ readToolError: vi.fn(), readToolSuccessMessage: vi.fn(), })); vi.mock("../router/assistant-tools.js", () => ({ executeTool: vi.fn(), })); import { createAuditEntry } from "../lib/audit.js"; import { clearPendingAssistantApproval, consumePendingAssistantApproval, } from "../router/assistant-approvals.js"; import { canExecuteMutationTool, isCancellationReply, } from "../router/assistant-confirmation.js"; import { buildAssistantInsight } from "../router/assistant-insights.js"; import { handlePendingAssistantApproval } from "../router/assistant-chat-response.js"; import { readToolError, readToolSuccessMessage, } from "../router/assistant-tool-results.js"; import { executeTool } from "../router/assistant-tools.js"; function createPendingApproval() { return { id: "approval_1", userId: "user_1", conversationId: "conv_1", toolName: "create_project", toolArguments: "{\"name\":\"Apollo\"}", summary: "create project (name=Apollo)", createdAt: Date.now(), expiresAt: Date.now() + 60_000, }; } function createHandleInput(overrides: Partial[0]> = {}) { return { db: {} as never, dbUserId: "user_1", toolCtx: { db: {} as never, userId: "user_1", userRole: "USER", permissions: new Set(), session: null, dbUser: null, roleDefaults: null, }, conversationId: "conv_1", pendingApproval: createPendingApproval(), lastUserMessage: { role: "user" as const, content: "ja" }, messages: [ { role: "assistant" as const, content: "__CAPAKRAKEN_CONFIRM__ create project (name=Apollo). Bitte bestätigen." }, { role: "user" as const, content: "ja" }, ], collectedActions: [], collectedInsights: [], ...overrides, }; } describe("assistant pending approval handling", () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(isCancellationReply).mockReturnValue(false); vi.mocked(canExecuteMutationTool).mockReturnValue(true); vi.mocked(readToolError).mockReturnValue(null); vi.mocked(readToolSuccessMessage).mockReturnValue(null); vi.mocked(buildAssistantInsight).mockReturnValue(undefined); }); it("cancels pending approvals when the user aborts", async () => { vi.mocked(isCancellationReply).mockReturnValue(true); const result = await handlePendingAssistantApproval(createHandleInput({ lastUserMessage: { role: "user", content: "nein, abbrechen" }, })); expect(result).toMatchObject({ response: { content: "Aktion verworfen: create project (name=Apollo)", approval: { id: "approval_1", status: "cancelled", }, }, }); expect(clearPendingAssistantApproval).toHaveBeenCalledWith({}, "user_1", "conv_1"); expect(consumePendingAssistantApproval).not.toHaveBeenCalled(); expect(executeTool).not.toHaveBeenCalled(); }); it("executes the confirmed mutation and returns its success response", async () => { vi.mocked(consumePendingAssistantApproval).mockResolvedValue({ ...createPendingApproval(), summary: "create project (name=Apollo, status=DRAFT)", } as never); vi.mocked(executeTool).mockResolvedValue({ content: "{\"message\":\"Projekt Apollo angelegt\"}", data: { message: "Projekt Apollo angelegt" }, action: { type: "refresh" }, } as never); vi.mocked(buildAssistantInsight).mockReturnValue({ kind: "holiday_region", title: "Berlin", metrics: [{ label: "Resolved holidays", value: "1" }], }); vi.mocked(readToolSuccessMessage).mockReturnValue("Projekt Apollo angelegt"); const result = await handlePendingAssistantApproval(createHandleInput()); expect(result).toMatchObject({ response: { content: "Ausgeführt: Projekt Apollo angelegt", approval: { id: "approval_1", status: "approved", }, actions: [{ type: "refresh" }], insights: [{ kind: "holiday_region", title: "Berlin", }], }, }); expect(executeTool).toHaveBeenCalledWith( "create_project", "{\"name\":\"Apollo\"}", expect.objectContaining({ userId: "user_1" }), ); expect(createAuditEntry).toHaveBeenCalledWith(expect.objectContaining({ entityName: "create_project", summary: "AI executed previously approved tool: create_project", })); }); it("does nothing when the user reply is not a valid confirmation", async () => { vi.mocked(canExecuteMutationTool).mockReturnValue(false); const result = await handlePendingAssistantApproval(createHandleInput({ lastUserMessage: { role: "user", content: "vielleicht" }, })); expect(result).toBeNull(); expect(consumePendingAssistantApproval).not.toHaveBeenCalled(); expect(executeTool).not.toHaveBeenCalled(); }); });