180 lines
5.7 KiB
TypeScript
180 lines
5.7 KiB
TypeScript
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<Parameters<typeof handlePendingAssistantApproval>[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();
|
|
});
|
|
});
|