import { randomUUID } from "node:crypto"; import { eq } from "drizzle-orm"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { agents, companies, createDb, executionWorkspaces, heartbeatRuns, issues, projectWorkspaces, projects, routineRuns, routines, routineTriggers, } from "@paperclipai/db"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, } from "./helpers/embedded-postgres.js"; const mockTelemetryClient = vi.hoisted(() => ({ track: vi.fn() })); const mockTrackRoutineRun = vi.hoisted(() => vi.fn()); vi.mock("../telemetry.js", () => ({ getTelemetryClient: () => mockTelemetryClient, })); vi.mock("@paperclipai/shared/telemetry", async () => { const actual = await vi.importActual( "@paperclipai/shared/telemetry", ); return { ...actual, trackRoutineRun: mockTrackRoutineRun, }; }); import { routineService } from "../services/routines.ts"; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; describeEmbeddedPostgres("routine run telemetry", () => { let db!: ReturnType; let tempDb: Awaited> | null = null; beforeAll(async () => { tempDb = await startEmbeddedPostgresTestDatabase("paperclip-routine-telemetry-"); db = createDb(tempDb.connectionString); }, 20_000); afterEach(async () => { vi.clearAllMocks(); await db.delete(routineRuns); await db.delete(routineTriggers); await db.delete(routines); await db.delete(heartbeatRuns); await db.delete(issues); await db.delete(executionWorkspaces); await db.delete(projectWorkspaces); await db.delete(projects); await db.delete(agents); await db.delete(companies); }); afterAll(async () => { await tempDb?.cleanup(); }); async function seedFixture() { const companyId = randomUUID(); const agentId = randomUUID(); const projectId = randomUUID(); await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); await db.insert(agents).values({ id: agentId, companyId, name: "CodexCoder", role: "engineer", status: "active", adapterType: "codex_local", adapterConfig: {}, runtimeConfig: {}, permissions: {}, }); await db.insert(projects).values({ id: projectId, companyId, name: "Routines", status: "in_progress", }); const svc = routineService(db, { heartbeat: { wakeup: async (wakeupAgentId, wakeupOpts) => { const issueId = (typeof wakeupOpts.payload?.issueId === "string" && wakeupOpts.payload.issueId) || (typeof wakeupOpts.contextSnapshot?.issueId === "string" && wakeupOpts.contextSnapshot.issueId) || null; if (!issueId) return null; const queuedRunId = randomUUID(); await db.insert(heartbeatRuns).values({ id: queuedRunId, companyId, agentId: wakeupAgentId, invocationSource: wakeupOpts.source ?? "assignment", triggerDetail: wakeupOpts.triggerDetail ?? null, status: "queued", contextSnapshot: { ...(wakeupOpts.contextSnapshot ?? {}), issueId }, }); await db .update(issues) .set({ executionRunId: queuedRunId, executionLockedAt: new Date(), }) .where(eq(issues.id, issueId)); return { id: queuedRunId }; }, }, }); const routine = await svc.create( companyId, { projectId, goalId: null, parentIssueId: null, title: "Run telemetry test", description: "Routine body", assigneeAgentId: agentId, priority: "medium", status: "active", concurrencyPolicy: "coalesce_if_active", catchUpPolicy: "skip_missed", }, {}, ); return { routine, svc }; } it("emits telemetry for routine runs from the service layer", async () => { const { routine, svc } = await seedFixture(); const run = await svc.runRoutine(routine.id, { source: "manual" }); expect(run.status).toBe("issue_created"); expect(mockTrackRoutineRun).toHaveBeenCalledWith(mockTelemetryClient, { source: "manual", status: "issue_created", }); }); });