163 lines
4.6 KiB
TypeScript
163 lines
4.6 KiB
TypeScript
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<typeof import("@paperclipai/shared/telemetry")>(
|
|
"@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<typeof createDb>;
|
|
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | 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",
|
|
});
|
|
});
|
|
});
|