import express from "express"; import request from "supertest"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { approvalRoutes } from "../routes/approvals.js"; import { errorHandler } from "../middleware/index.js"; const mockApprovalService = vi.hoisted(() => ({ list: vi.fn(), getById: vi.fn(), create: vi.fn(), approve: vi.fn(), reject: vi.fn(), requestRevision: vi.fn(), resubmit: vi.fn(), listComments: vi.fn(), addComment: vi.fn(), })); const mockHeartbeatService = vi.hoisted(() => ({ wakeup: vi.fn(), })); const mockIssueApprovalService = vi.hoisted(() => ({ listIssuesForApproval: vi.fn(), linkManyForApproval: vi.fn(), })); const mockSecretService = vi.hoisted(() => ({ normalizeHireApprovalPayloadForPersistence: vi.fn(), })); const mockLogActivity = vi.hoisted(() => vi.fn()); vi.mock("../services/index.js", () => ({ approvalService: () => mockApprovalService, heartbeatService: () => mockHeartbeatService, issueApprovalService: () => mockIssueApprovalService, logActivity: mockLogActivity, secretService: () => mockSecretService, })); function createApp() { const app = express(); app.use(express.json()); app.use((req, _res, next) => { (req as any).actor = { type: "board", userId: "user-1", companyIds: ["company-1"], source: "session", isInstanceAdmin: false, }; next(); }); app.use("/api", approvalRoutes({} as any)); app.use(errorHandler); return app; } describe("approval routes idempotent retries", () => { beforeEach(() => { vi.clearAllMocks(); mockHeartbeatService.wakeup.mockResolvedValue({ id: "wake-1" }); mockIssueApprovalService.listIssuesForApproval.mockResolvedValue([{ id: "issue-1" }]); mockLogActivity.mockResolvedValue(undefined); }); it("does not emit duplicate approval side effects when approve is already resolved", async () => { mockApprovalService.approve.mockResolvedValue({ approval: { id: "approval-1", companyId: "company-1", type: "hire_agent", status: "approved", payload: {}, requestedByAgentId: "agent-1", }, applied: false, }); const res = await request(createApp()) .post("/api/approvals/approval-1/approve") .send({}); expect(res.status).toBe(200); expect(mockIssueApprovalService.listIssuesForApproval).not.toHaveBeenCalled(); expect(mockHeartbeatService.wakeup).not.toHaveBeenCalled(); expect(mockLogActivity).not.toHaveBeenCalled(); }); it("does not emit duplicate rejection logs when reject is already resolved", async () => { mockApprovalService.reject.mockResolvedValue({ approval: { id: "approval-1", companyId: "company-1", type: "hire_agent", status: "rejected", payload: {}, }, applied: false, }); const res = await request(createApp()) .post("/api/approvals/approval-1/reject") .send({}); expect(res.status).toBe(200); expect(mockLogActivity).not.toHaveBeenCalled(); }); });