import { randomUUID } from "node:crypto"; import fs from "node:fs"; import net from "node:net"; import os from "node:os"; import path from "node:path"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; import { activityLog, agents, applyPendingMigrations, companies, createDb, ensurePostgresDatabase, issueComments, issues, } from "@paperclipai/db"; import { issueService } from "../services/issues.ts"; type EmbeddedPostgresInstance = { initialise(): Promise; start(): Promise; stop(): Promise; }; type EmbeddedPostgresCtor = new (opts: { databaseDir: string; user: string; password: string; port: number; persistent: boolean; initdbFlags?: string[]; onLog?: (message: unknown) => void; onError?: (message: unknown) => void; }) => EmbeddedPostgresInstance; async function getEmbeddedPostgresCtor(): Promise { const mod = await import("embedded-postgres"); return mod.default as EmbeddedPostgresCtor; } async function getAvailablePort(): Promise { return await new Promise((resolve, reject) => { const server = net.createServer(); server.unref(); server.on("error", reject); server.listen(0, "127.0.0.1", () => { const address = server.address(); if (!address || typeof address === "string") { server.close(() => reject(new Error("Failed to allocate test port"))); return; } const { port } = address; server.close((error) => { if (error) reject(error); else resolve(port); }); }); }); } async function startTempDatabase() { const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-issues-service-")); const port = await getAvailablePort(); const EmbeddedPostgres = await getEmbeddedPostgresCtor(); const instance = new EmbeddedPostgres({ databaseDir: dataDir, user: "paperclip", password: "paperclip", port, persistent: true, initdbFlags: ["--encoding=UTF8", "--locale=C"], onLog: () => {}, onError: () => {}, }); await instance.initialise(); await instance.start(); const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/postgres`; await ensurePostgresDatabase(adminConnectionString, "paperclip"); const connectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`; await applyPendingMigrations(connectionString); return { connectionString, dataDir, instance }; } describe("issueService.list participantAgentId", () => { let db!: ReturnType; let svc!: ReturnType; let instance: EmbeddedPostgresInstance | null = null; let dataDir = ""; beforeAll(async () => { const started = await startTempDatabase(); db = createDb(started.connectionString); svc = issueService(db); instance = started.instance; dataDir = started.dataDir; }, 20_000); afterEach(async () => { await db.delete(issueComments); await db.delete(activityLog); await db.delete(issues); await db.delete(agents); await db.delete(companies); }); afterAll(async () => { await instance?.stop(); if (dataDir) { fs.rmSync(dataDir, { recursive: true, force: true }); } }); it("returns issues an agent participated in across the supported signals", async () => { const companyId = randomUUID(); const agentId = randomUUID(); const otherAgentId = 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: {}, }, { id: otherAgentId, companyId, name: "OtherAgent", role: "engineer", status: "active", adapterType: "codex_local", adapterConfig: {}, runtimeConfig: {}, permissions: {}, }, ]); const assignedIssueId = randomUUID(); const createdIssueId = randomUUID(); const commentedIssueId = randomUUID(); const activityIssueId = randomUUID(); const excludedIssueId = randomUUID(); await db.insert(issues).values([ { id: assignedIssueId, companyId, title: "Assigned issue", status: "todo", priority: "medium", assigneeAgentId: agentId, createdByAgentId: otherAgentId, }, { id: createdIssueId, companyId, title: "Created issue", status: "todo", priority: "medium", createdByAgentId: agentId, }, { id: commentedIssueId, companyId, title: "Commented issue", status: "todo", priority: "medium", createdByAgentId: otherAgentId, }, { id: activityIssueId, companyId, title: "Activity issue", status: "todo", priority: "medium", createdByAgentId: otherAgentId, }, { id: excludedIssueId, companyId, title: "Excluded issue", status: "todo", priority: "medium", createdByAgentId: otherAgentId, assigneeAgentId: otherAgentId, }, ]); await db.insert(issueComments).values({ companyId, issueId: commentedIssueId, authorAgentId: agentId, body: "Investigating this issue.", }); await db.insert(activityLog).values({ companyId, actorType: "agent", actorId: agentId, action: "issue.updated", entityType: "issue", entityId: activityIssueId, agentId, details: { changed: true }, }); const result = await svc.list(companyId, { participantAgentId: agentId }); const resultIds = new Set(result.map((issue) => issue.id)); expect(resultIds).toEqual(new Set([ assignedIssueId, createdIssueId, commentedIssueId, activityIssueId, ])); expect(resultIds.has(excludedIssueId)).toBe(false); }); it("combines participation filtering with search", async () => { const companyId = randomUUID(); const agentId = 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: {}, }); const matchedIssueId = randomUUID(); const otherIssueId = randomUUID(); await db.insert(issues).values([ { id: matchedIssueId, companyId, title: "Invoice reconciliation", status: "todo", priority: "medium", createdByAgentId: agentId, }, { id: otherIssueId, companyId, title: "Weekly planning", status: "todo", priority: "medium", createdByAgentId: agentId, }, ]); const result = await svc.list(companyId, { participantAgentId: agentId, q: "invoice", }); expect(result.map((issue) => issue.id)).toEqual([matchedIssueId]); }); });