import { randomUUID } from "node:crypto"; import { and, eq } from "drizzle-orm"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; import { agents, agentRuntimeState, agentWakeupRequests, activityLog, companies, companySkills, createDb, environmentLeases, environments, heartbeatRunEvents, heartbeatRuns, } from "@paperclipai/db"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, } from "./helpers/embedded-postgres.js"; import { heartbeatService } from "../services/heartbeat.ts"; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; if (!embeddedPostgresSupport.supported) { console.warn( `Skipping embedded Postgres heartbeat environment tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, ); } async function waitForRunToFinish( heartbeat: ReturnType, runId: string, timeoutMs = 5_000, ) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const run = await heartbeat.getRun(runId); if (run && !["queued", "running"].includes(run.status)) return run; await new Promise((resolve) => setTimeout(resolve, 50)); } return await heartbeat.getRun(runId); } async function waitForRunLeasesToRelease( db: ReturnType, runId: string, timeoutMs = 5_000, ) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const leases = await db .select() .from(environmentLeases) .where(eq(environmentLeases.heartbeatRunId, runId)); if (leases.length > 0 && leases.every((lease) => lease.status !== "active")) return leases; await new Promise((resolve) => setTimeout(resolve, 50)); } return await db .select() .from(environmentLeases) .where(eq(environmentLeases.heartbeatRunId, runId)); } describeEmbeddedPostgres("heartbeat local environment lifecycle", () => { let db!: ReturnType; let tempDb: Awaited> | null = null; beforeAll(async () => { tempDb = await startEmbeddedPostgresTestDatabase("heartbeat-local-environment-"); db = createDb(tempDb.connectionString); }, 20_000); afterEach(async () => { await db.delete(environmentLeases); await db.delete(environments); await db.delete(activityLog); await db.delete(heartbeatRunEvents); await db.delete(heartbeatRuns); await db.delete(agentWakeupRequests); await db.delete(agentRuntimeState); await db.delete(companySkills); await db.delete(agents); await db.delete(companies); }); afterAll(async () => { await tempDb?.cleanup(); }); it("runs work through the default Local environment lease", async () => { const companyId = randomUUID(); const agentId = randomUUID(); const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`; await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix, requireBoardApprovalForNewAgents: false, }); await db.insert(agents).values({ id: agentId, companyId, name: "ProcessAgent", role: "engineer", status: "idle", adapterType: "process", adapterConfig: { command: process.execPath, args: ["-e", "process.exit(0)"], }, runtimeConfig: {}, permissions: {}, }); const heartbeat = heartbeatService(db); const queued = await heartbeat.invoke(agentId, "on_demand", {}, "manual"); expect(queued).not.toBeNull(); const finished = await waitForRunToFinish(heartbeat, queued!.id); expect(finished?.status).toBe("succeeded"); const localRows = await db .select() .from(environments) .where(and(eq(environments.companyId, companyId), eq(environments.driver, "local"))); expect(localRows).toHaveLength(1); expect(localRows[0]?.name).toBe("Local"); const leases = await waitForRunLeasesToRelease(db, queued!.id); expect(leases).toHaveLength(1); expect(leases[0]?.environmentId).toBe(localRows[0]?.id); expect(leases[0]?.status).toBe("released"); expect(leases[0]?.provider).toBe("local"); expect(leases[0]?.releasedAt).not.toBeNull(); const context = finished?.contextSnapshot as Record; expect(context.paperclipEnvironment).toMatchObject({ id: localRows[0]?.id, name: "Local", driver: "local", leaseId: leases[0]?.id, }); }); });