import express from "express"; import request from "supertest"; import { beforeEach, describe, expect, it, vi } from "vitest"; const mockIssueService = vi.hoisted(() => ({ getById: vi.fn(), getWakeableParentAfterChildCompletion: vi.fn(), listWakeableBlockedDependents: vi.fn(), update: vi.fn(), })); const mockAgentService = vi.hoisted(() => ({ getById: vi.fn(), })); const mockTrackAgentTaskCompleted = vi.hoisted(() => vi.fn()); const mockGetTelemetryClient = vi.hoisted(() => vi.fn()); function registerModuleMocks() { vi.doMock("@paperclipai/shared/telemetry", () => ({ trackAgentTaskCompleted: mockTrackAgentTaskCompleted, trackErrorHandlerCrash: vi.fn(), })); vi.doMock("../telemetry.js", () => ({ getTelemetryClient: mockGetTelemetryClient, })); vi.doMock("../services/index.js", () => ({ companyService: () => ({ getById: vi.fn(async () => ({ id: "company-1", attachmentMaxBytes: 10 * 1024 * 1024 })), }), accessService: () => ({ canUser: vi.fn(), hasPermission: vi.fn(), }), agentService: () => mockAgentService, documentService: () => ({}), executionWorkspaceService: () => ({}), feedbackService: () => ({}), goalService: () => ({}), heartbeatService: () => ({ wakeup: vi.fn(async () => undefined), reportRunActivity: vi.fn(async () => undefined), }), instanceSettingsService: () => ({}), issueApprovalService: () => ({}), issueReferenceService: () => ({ deleteDocumentSource: async () => undefined, diffIssueReferenceSummary: () => ({ addedReferencedIssues: [], removedReferencedIssues: [], currentReferencedIssues: [], }), emptySummary: () => ({ outbound: [], inbound: [] }), listIssueReferenceSummary: async () => ({ outbound: [], inbound: [] }), syncComment: async () => undefined, syncDocument: async () => undefined, syncIssue: async () => undefined, }), issueRecoveryActionService: () => ({ getActiveForIssue: vi.fn(async () => null), listActiveForIssues: vi.fn(async () => new Map()), }), issueService: () => mockIssueService, logActivity: vi.fn(async () => undefined), projectService: () => ({}), routineService: () => ({ syncRunStatusForIssue: vi.fn(async () => undefined), }), workProductService: () => ({}), })); } function makeIssue(status: "todo" | "done") { return { id: "11111111-1111-4111-8111-111111111111", companyId: "company-1", status, assigneeAgentId: "agent-1", assigneeUserId: null, createdByUserId: "local-board", identifier: "PAP-1018", title: "Telemetry test", }; } async function createApp(actor: Record) { const [{ errorHandler }, { issueRoutes }] = await Promise.all([ vi.importActual("../middleware/index.js"), vi.importActual("../routes/issues.js"), ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { (req as any).actor = actor; next(); }); app.use("/api", issueRoutes({} as any, {} as any)); app.use(errorHandler); return app; } describe("issue telemetry routes", () => { beforeEach(() => { vi.resetModules(); vi.doUnmock("@paperclipai/shared/telemetry"); vi.doUnmock("../telemetry.js"); vi.doUnmock("../services/index.js"); vi.doUnmock("../routes/issues.js"); vi.doUnmock("../routes/authz.js"); vi.doUnmock("../middleware/index.js"); registerModuleMocks(); vi.clearAllMocks(); mockGetTelemetryClient.mockReturnValue({ track: vi.fn() }); mockIssueService.getById.mockResolvedValue(makeIssue("todo")); mockIssueService.getWakeableParentAfterChildCompletion.mockResolvedValue(null); mockIssueService.listWakeableBlockedDependents.mockResolvedValue([]); mockIssueService.update.mockImplementation(async (_id: string, patch: Record) => ({ ...makeIssue("todo"), ...patch, })); }); it("emits task-completed telemetry with the agent role, adapter type, and model", async () => { mockAgentService.getById.mockResolvedValue({ id: "agent-1", companyId: "company-1", role: "engineer", adapterType: "codex_local", adapterConfig: { model: "claude-sonnet-4-6" }, }); const app = await createApp({ type: "agent", agentId: "agent-1", companyId: "company-1", runId: null, }); const res = await request(app) .patch("/api/issues/11111111-1111-4111-8111-111111111111") .send({ status: "done" }); expect(res.status).toBe(200); await vi.waitFor(() => { expect(mockTrackAgentTaskCompleted).toHaveBeenCalledWith(expect.anything(), { agentRole: "engineer", agentId: "agent-1", adapterType: "codex_local", model: "claude-sonnet-4-6", }); }); }, 10_000); it("does not emit agent task-completed telemetry for board-driven completions", async () => { const app = await createApp({ type: "board", userId: "local-board", companyIds: ["company-1"], source: "local_implicit", isInstanceAdmin: false, }); const res = await request(app) .patch("/api/issues/11111111-1111-4111-8111-111111111111") .send({ status: "done" }); expect(res.status).toBe(200); expect(mockTrackAgentTaskCompleted).not.toHaveBeenCalled(); expect(mockAgentService.getById).not.toHaveBeenCalled(); }); });