mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 10:50:38 +09:00
Add versioned telemetry events
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
2ac40aba56
commit
37b6ad42ea
16 changed files with 577 additions and 0 deletions
163
server/src/__tests__/routine-run-telemetry.test.ts
Normal file
163
server/src/__tests__/routine-run-telemetry.test.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
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.ts", () => ({
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue