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; } function createAgentApp() { const app = express(); app.use(express.json()); app.use((req, _res, next) => { (req as any).actor = { type: "agent", agentId: "agent-1", companyId: "company-1", source: "api_key", 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(); }); it("lets agents create generic issue-linked board approval requests", async () => { mockApprovalService.create.mockResolvedValue({ id: "approval-1", companyId: "company-1", type: "request_board_approval", requestedByAgentId: "agent-1", requestedByUserId: null, status: "pending", payload: { title: "Approve hosting spend" }, decisionNote: null, decidedByUserId: null, decidedAt: null, createdAt: new Date("2026-04-06T00:00:00.000Z"), updatedAt: new Date("2026-04-06T00:00:00.000Z"), }); const res = await request(createAgentApp()) .post("/api/companies/company-1/approvals") .send({ type: "request_board_approval", issueIds: ["00000000-0000-0000-0000-000000000001"], payload: { title: "Approve hosting spend" }, }); expect(res.status).toBe(201); expect(mockApprovalService.create).toHaveBeenCalledWith( "company-1", expect.objectContaining({ type: "request_board_approval", requestedByAgentId: "agent-1", requestedByUserId: null, status: "pending", decisionNote: null, }), ); expect(mockSecretService.normalizeHireApprovalPayloadForPersistence).not.toHaveBeenCalled(); expect(mockIssueApprovalService.linkManyForApproval).toHaveBeenCalledWith( "approval-1", ["00000000-0000-0000-0000-000000000001"], { agentId: "agent-1", userId: null }, ); expect(mockLogActivity).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ companyId: "company-1", actorType: "agent", actorId: "agent-1", action: "approval.created", }), ); }); });