import { randomUUID } from "node:crypto"; import { eq } from "drizzle-orm"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; import { activityLog, agents, companies, companySkills, createDb, heartbeatRuns, issueComments, issueExecutionDecisions, issueReadStates, issues, } from "@paperclipai/db"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, } from "./helpers/embedded-postgres.js"; import { agentService } from "../services/agents.ts"; import { companyService } from "../services/companies.ts"; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; if (!embeddedPostgresSupport.supported) { console.warn( `Skipping cleanup removal service tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, ); } describeEmbeddedPostgres("cleanup removal services", () => { let db!: ReturnType; let tempDb: Awaited> | null = null; beforeAll(async () => { tempDb = await startEmbeddedPostgresTestDatabase("paperclip-cleanup-removal-"); db = createDb(tempDb.connectionString); }, 20_000); afterEach(async () => { await db.delete(activityLog); await db.delete(issueReadStates); await db.delete(issueComments); await db.delete(issueExecutionDecisions); await db.delete(companySkills); await db.delete(heartbeatRuns); await db.delete(issues); await db.delete(agents); await db.delete(companies); }); afterAll(async () => { await tempDb?.cleanup(); }); async function seedFixture() { const companyId = randomUUID(); const agentId = randomUUID(); const issueId = randomUUID(); const runId = 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: "CodexCoder", role: "engineer", status: "active", adapterType: "codex_local", adapterConfig: {}, runtimeConfig: {}, permissions: {}, }); await db.insert(issues).values({ id: issueId, companyId, title: "Regression fixture", status: "todo", priority: "medium", assigneeAgentId: agentId, createdByUserId: "user-1", }); await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, invocationSource: "assignment", status: "completed", contextSnapshot: { issueId }, }); return { agentId, companyId, issueId, runId }; } it("removes agent-owned issue comments and run-linked activity before deleting the agent", async () => { const { agentId, companyId, issueId, runId } = await seedFixture(); await db.insert(issueComments).values({ id: randomUUID(), companyId, issueId, authorAgentId: agentId, body: "Agent-authored comment", }); await db.insert(activityLog).values({ id: randomUUID(), companyId, actorType: "agent", actorId: agentId, action: "heartbeat.completed", entityType: "issue", entityId: issueId, runId, details: {}, }); await db.insert(issueExecutionDecisions).values({ id: randomUUID(), companyId, issueId, stageId: randomUUID(), stageType: "review", actorAgentId: agentId, outcome: "approved", body: "Looks good", createdByRunId: runId, }); const removed = await agentService(db).remove(agentId); expect(removed?.id).toBe(agentId); await expect(db.select().from(agents).where(eq(agents.id, agentId))).resolves.toHaveLength(0); await expect(db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId))).resolves.toHaveLength(0); await expect(db.select().from(issueComments).where(eq(issueComments.issueId, issueId))).resolves.toHaveLength(0); await expect(db.select().from(activityLog).where(eq(activityLog.companyId, companyId))).resolves.toHaveLength(0); }); it("removes issue read states and activity rows before deleting the company", async () => { const { companyId, issueId, runId } = await seedFixture(); await db.insert(issueReadStates).values({ id: randomUUID(), companyId, issueId, userId: "user-1", }); await db.insert(companySkills).values({ id: randomUUID(), companyId, key: "paperclipai/paperclip/paperclip", slug: "paperclip", name: "Paperclip", markdown: "# Paperclip", }); await db.insert(activityLog).values({ id: randomUUID(), companyId, actorType: "system", actorId: "system", action: "run.created", entityType: "run", entityId: runId, runId, details: {}, }); const removed = await companyService(db).remove(companyId); expect(removed?.id).toBe(companyId); await expect(db.select().from(companies).where(eq(companies.id, companyId))).resolves.toHaveLength(0); await expect(db.select().from(issues).where(eq(issues.id, issueId))).resolves.toHaveLength(0); await expect(db.select().from(issueReadStates).where(eq(issueReadStates.companyId, companyId))).resolves.toHaveLength(0); await expect(db.select().from(activityLog).where(eq(activityLog.companyId, companyId))).resolves.toHaveLength(0); }); });