111 lines
3.4 KiB
TypeScript
111 lines
3.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { approvalService } from "../services/approvals.js";
|
|
|
|
const mockAgentService = vi.hoisted(() => ({
|
|
activatePendingApproval: vi.fn(),
|
|
create: vi.fn(),
|
|
terminate: vi.fn(),
|
|
}));
|
|
|
|
const mockNotifyHireApproved = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("../services/agents.js", () => ({
|
|
agentService: vi.fn(() => mockAgentService),
|
|
}));
|
|
|
|
vi.mock("../services/hire-hook.js", () => ({
|
|
notifyHireApproved: mockNotifyHireApproved,
|
|
}));
|
|
|
|
type ApprovalRecord = {
|
|
id: string;
|
|
companyId: string;
|
|
type: string;
|
|
status: string;
|
|
payload: Record<string, unknown>;
|
|
requestedByAgentId: string | null;
|
|
};
|
|
|
|
function createApproval(status: string): ApprovalRecord {
|
|
return {
|
|
id: "approval-1",
|
|
companyId: "company-1",
|
|
type: "hire_agent",
|
|
status,
|
|
payload: { agentId: "agent-1" },
|
|
requestedByAgentId: "requester-1",
|
|
};
|
|
}
|
|
|
|
function createDbStub(selectResults: ApprovalRecord[][], updateResults: ApprovalRecord[]) {
|
|
const selectWhere = vi.fn();
|
|
for (const result of selectResults) {
|
|
selectWhere.mockResolvedValueOnce(result);
|
|
}
|
|
|
|
const from = vi.fn(() => ({ where: selectWhere }));
|
|
const select = vi.fn(() => ({ from }));
|
|
|
|
const returning = vi.fn().mockResolvedValue(updateResults);
|
|
const updateWhere = vi.fn(() => ({ returning }));
|
|
const set = vi.fn(() => ({ where: updateWhere }));
|
|
const update = vi.fn(() => ({ set }));
|
|
|
|
return {
|
|
db: { select, update },
|
|
selectWhere,
|
|
returning,
|
|
};
|
|
}
|
|
|
|
describe("approvalService resolution idempotency", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockAgentService.activatePendingApproval.mockResolvedValue(undefined);
|
|
mockAgentService.create.mockResolvedValue({ id: "agent-1" });
|
|
mockAgentService.terminate.mockResolvedValue(undefined);
|
|
mockNotifyHireApproved.mockResolvedValue(undefined);
|
|
});
|
|
|
|
it("treats repeated approve retries as no-ops after another worker resolves the approval", async () => {
|
|
const dbStub = createDbStub(
|
|
[[createApproval("pending")], [createApproval("approved")]],
|
|
[],
|
|
);
|
|
|
|
const svc = approvalService(dbStub.db as any);
|
|
const result = await svc.approve("approval-1", "board", "ship it");
|
|
|
|
expect(result.applied).toBe(false);
|
|
expect(result.approval.status).toBe("approved");
|
|
expect(mockAgentService.activatePendingApproval).not.toHaveBeenCalled();
|
|
expect(mockNotifyHireApproved).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("treats repeated reject retries as no-ops after another worker resolves the approval", async () => {
|
|
const dbStub = createDbStub(
|
|
[[createApproval("pending")], [createApproval("rejected")]],
|
|
[],
|
|
);
|
|
|
|
const svc = approvalService(dbStub.db as any);
|
|
const result = await svc.reject("approval-1", "board", "not now");
|
|
|
|
expect(result.applied).toBe(false);
|
|
expect(result.approval.status).toBe("rejected");
|
|
expect(mockAgentService.terminate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("still performs side effects when the resolution update is newly applied", async () => {
|
|
const approved = createApproval("approved");
|
|
const dbStub = createDbStub([[createApproval("pending")]], [approved]);
|
|
|
|
const svc = approvalService(dbStub.db as any);
|
|
const result = await svc.approve("approval-1", "board", "ship it");
|
|
|
|
expect(result.applied).toBe(true);
|
|
expect(mockAgentService.activatePendingApproval).toHaveBeenCalledWith("agent-1");
|
|
expect(mockNotifyHireApproved).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|