refactor(api): modularize assistant router workflow
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
AssistantApprovalStorageUnavailableError,
|
||||
clearPendingAssistantApproval,
|
||||
consumePendingAssistantApproval,
|
||||
createPendingAssistantApproval,
|
||||
listPendingAssistantApprovals,
|
||||
peekPendingAssistantApproval,
|
||||
resetAssistantApprovalStorageWarningStateForTests,
|
||||
} from "../router/assistant-approvals.js";
|
||||
import { logger } from "../lib/logger.js";
|
||||
import {
|
||||
createApprovalStoreMock,
|
||||
createMissingApprovalTableError,
|
||||
TEST_CONVERSATION_ID,
|
||||
TEST_USER_ID,
|
||||
} from "./assistant-approval-test-helpers.js";
|
||||
|
||||
describe("assistant approvals", () => {
|
||||
let approvalStore = createApprovalStoreMock();
|
||||
|
||||
beforeEach(() => {
|
||||
approvalStore = createApprovalStoreMock();
|
||||
resetAssistantApprovalStorageWarningStateForTests();
|
||||
});
|
||||
|
||||
it("stores and consumes pending approvals independently from chat text", async () => {
|
||||
const approval = await createPendingAssistantApproval(
|
||||
approvalStore,
|
||||
TEST_USER_ID,
|
||||
TEST_CONVERSATION_ID,
|
||||
"create_project",
|
||||
JSON.stringify({ name: "Gelddruckmaschine", status: "DRAFT" }),
|
||||
);
|
||||
|
||||
await expect(peekPendingAssistantApproval(approvalStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toMatchObject({
|
||||
id: approval.id,
|
||||
toolName: "create_project",
|
||||
summary: expect.stringContaining("create project"),
|
||||
});
|
||||
|
||||
await expect(consumePendingAssistantApproval(approvalStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toMatchObject({
|
||||
id: approval.id,
|
||||
toolName: "create_project",
|
||||
});
|
||||
await expect(peekPendingAssistantApproval(approvalStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("expires stale pending approvals", async () => {
|
||||
await createPendingAssistantApproval(
|
||||
approvalStore,
|
||||
TEST_USER_ID,
|
||||
TEST_CONVERSATION_ID,
|
||||
"create_project",
|
||||
JSON.stringify({ name: "Apollo" }),
|
||||
{ ttlMs: -1 },
|
||||
);
|
||||
|
||||
await expect(peekPendingAssistantApproval(approvalStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("clears pending approvals for cancellation semantics", async () => {
|
||||
await createPendingAssistantApproval(
|
||||
approvalStore,
|
||||
TEST_USER_ID,
|
||||
TEST_CONVERSATION_ID,
|
||||
"create_project",
|
||||
JSON.stringify({ name: "Apollo" }),
|
||||
);
|
||||
|
||||
await clearPendingAssistantApproval(approvalStore, TEST_USER_ID, TEST_CONVERSATION_ID);
|
||||
|
||||
await expect(peekPendingAssistantApproval(approvalStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("isolates pending approvals by conversation", async () => {
|
||||
const otherConversationId = `${TEST_CONVERSATION_ID}-other`;
|
||||
|
||||
await createPendingAssistantApproval(
|
||||
approvalStore,
|
||||
TEST_USER_ID,
|
||||
TEST_CONVERSATION_ID,
|
||||
"create_project",
|
||||
JSON.stringify({ name: "Apollo" }),
|
||||
);
|
||||
await createPendingAssistantApproval(
|
||||
approvalStore,
|
||||
TEST_USER_ID,
|
||||
otherConversationId,
|
||||
"create_project",
|
||||
JSON.stringify({ name: "Hermes" }),
|
||||
);
|
||||
|
||||
await clearPendingAssistantApproval(approvalStore, TEST_USER_ID, TEST_CONVERSATION_ID);
|
||||
|
||||
await expect(peekPendingAssistantApproval(approvalStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeNull();
|
||||
await expect(peekPendingAssistantApproval(approvalStore, TEST_USER_ID, otherConversationId)).resolves.toMatchObject({
|
||||
toolName: "create_project",
|
||||
summary: expect.stringContaining("Hermes"),
|
||||
});
|
||||
});
|
||||
|
||||
it("lists only still-pending approvals for the current user across conversations", async () => {
|
||||
const otherConversationId = `${TEST_CONVERSATION_ID}-other`;
|
||||
|
||||
await createPendingAssistantApproval(
|
||||
approvalStore,
|
||||
TEST_USER_ID,
|
||||
TEST_CONVERSATION_ID,
|
||||
"create_project",
|
||||
JSON.stringify({ name: "Apollo" }),
|
||||
);
|
||||
await createPendingAssistantApproval(
|
||||
approvalStore,
|
||||
TEST_USER_ID,
|
||||
otherConversationId,
|
||||
"create_project",
|
||||
JSON.stringify({ name: "Hermes" }),
|
||||
);
|
||||
|
||||
const cancelled = await createPendingAssistantApproval(
|
||||
approvalStore,
|
||||
TEST_USER_ID,
|
||||
`${TEST_CONVERSATION_ID}-cancelled`,
|
||||
"create_project",
|
||||
JSON.stringify({ name: "Cancelled" }),
|
||||
);
|
||||
await approvalStore.assistantApproval.updateMany({
|
||||
where: { id: cancelled.id, userId: TEST_USER_ID, status: "PENDING" },
|
||||
data: { status: "CANCELLED", cancelledAt: new Date() },
|
||||
});
|
||||
|
||||
await createPendingAssistantApproval(
|
||||
approvalStore,
|
||||
"other-user",
|
||||
`${TEST_CONVERSATION_ID}-foreign`,
|
||||
"create_project",
|
||||
JSON.stringify({ name: "Foreign" }),
|
||||
);
|
||||
await createPendingAssistantApproval(
|
||||
approvalStore,
|
||||
TEST_USER_ID,
|
||||
`${TEST_CONVERSATION_ID}-expired`,
|
||||
"create_project",
|
||||
JSON.stringify({ name: "Expired" }),
|
||||
{ ttlMs: -1 },
|
||||
);
|
||||
|
||||
const approvals = await listPendingAssistantApprovals(approvalStore, TEST_USER_ID);
|
||||
const approvalSummaries = approvals.map((approval) => approval.summary).join(" ");
|
||||
|
||||
expect(approvals).toHaveLength(2);
|
||||
expect([...approvals.map((approval) => approval.conversationId)].sort()).toEqual([
|
||||
otherConversationId,
|
||||
TEST_CONVERSATION_ID,
|
||||
].sort());
|
||||
expect(approvals.every((approval) => approval.userId === TEST_USER_ID)).toBe(true);
|
||||
expect(approvalSummaries).toContain("Apollo");
|
||||
expect(approvalSummaries).toContain("Hermes");
|
||||
expect(approvalSummaries).not.toContain("Cancelled");
|
||||
expect(approvalSummaries).not.toContain("Expired");
|
||||
expect(approvalSummaries).not.toContain("Foreign");
|
||||
});
|
||||
|
||||
it("degrades approval reads gracefully when approval storage is missing", async () => {
|
||||
const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => logger);
|
||||
const missingTableError = createMissingApprovalTableError();
|
||||
const missingStore = {
|
||||
assistantApproval: {
|
||||
findFirst: vi.fn(async () => {
|
||||
throw missingTableError;
|
||||
}),
|
||||
findMany: vi.fn(async () => {
|
||||
throw missingTableError;
|
||||
}),
|
||||
create: vi.fn(async () => {
|
||||
throw missingTableError;
|
||||
}),
|
||||
updateMany: vi.fn(async () => {
|
||||
throw missingTableError;
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
await expect(listPendingAssistantApprovals(missingStore, TEST_USER_ID)).resolves.toEqual([]);
|
||||
await expect(peekPendingAssistantApproval(missingStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeNull();
|
||||
await expect(consumePendingAssistantApproval(missingStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeNull();
|
||||
await expect(clearPendingAssistantApproval(missingStore, TEST_USER_ID, TEST_CONVERSATION_ID)).resolves.toBeUndefined();
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns an explicit error when approval storage is missing for mutation confirmation", async () => {
|
||||
const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => logger);
|
||||
const missingTableError = createMissingApprovalTableError();
|
||||
const missingStore = {
|
||||
assistantApproval: {
|
||||
findFirst: vi.fn(async () => {
|
||||
throw missingTableError;
|
||||
}),
|
||||
findMany: vi.fn(async () => {
|
||||
throw missingTableError;
|
||||
}),
|
||||
create: vi.fn(async () => {
|
||||
throw missingTableError;
|
||||
}),
|
||||
updateMany: vi.fn(async () => {
|
||||
throw missingTableError;
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
await expect(createPendingAssistantApproval(
|
||||
missingStore,
|
||||
TEST_USER_ID,
|
||||
TEST_CONVERSATION_ID,
|
||||
"create_project",
|
||||
JSON.stringify({ name: "Apollo" }),
|
||||
)).rejects.toBeInstanceOf(AssistantApprovalStorageUnavailableError);
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user