import { randomUUID } from "node:crypto"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; import { activityLog, agents, companies, createDb, executionWorkspaces, issueComments, issueInboxArchives, issues, projects, } from "@paperclipai/db"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, } from "./helpers/embedded-postgres.js"; import { issueService } from "../services/issues.ts"; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; if (!embeddedPostgresSupport.supported) { console.warn( `Skipping embedded Postgres issue service tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, ); } describeEmbeddedPostgres("issueService.list participantAgentId", () => { let db!: ReturnType; let svc!: ReturnType; let tempDb: Awaited> | null = null; beforeAll(async () => { tempDb = await startEmbeddedPostgresTestDatabase("paperclip-issues-service-"); db = createDb(tempDb.connectionString); svc = issueService(db); }, 20_000); afterEach(async () => { await db.delete(issueComments); await db.delete(issueInboxArchives); await db.delete(activityLog); await db.delete(issues); await db.delete(executionWorkspaces); await db.delete(projects); await db.delete(agents); await db.delete(companies); }); afterAll(async () => { await tempDb?.cleanup(); }); 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]); }); it("filters issues by execution workspace id", async () => { const companyId = randomUUID(); const projectId = randomUUID(); const targetWorkspaceId = randomUUID(); const otherWorkspaceId = randomUUID(); const linkedIssueId = randomUUID(); const otherLinkedIssueId = randomUUID(); const unlinkedIssueId = randomUUID(); await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); await db.insert(projects).values({ id: projectId, companyId, name: "Workspace project", status: "in_progress", }); await db.insert(executionWorkspaces).values([ { id: targetWorkspaceId, companyId, projectId, mode: "shared_workspace", strategyType: "project_primary", name: "Target workspace", status: "active", providerType: "local_fs", }, { id: otherWorkspaceId, companyId, projectId, mode: "shared_workspace", strategyType: "project_primary", name: "Other workspace", status: "active", providerType: "local_fs", }, ]); await db.insert(issues).values([ { id: linkedIssueId, companyId, projectId, title: "Linked issue", status: "todo", priority: "medium", executionWorkspaceId: targetWorkspaceId, }, { id: otherLinkedIssueId, companyId, projectId, title: "Other linked issue", status: "todo", priority: "medium", executionWorkspaceId: otherWorkspaceId, }, { id: unlinkedIssueId, companyId, projectId, title: "Unlinked issue", status: "todo", priority: "medium", }, ]); const result = await svc.list(companyId, { executionWorkspaceId: targetWorkspaceId }); expect(result.map((issue) => issue.id)).toEqual([linkedIssueId]); }); it("hides archived inbox issues until new external activity arrives", async () => { const companyId = randomUUID(); const userId = "user-1"; const otherUserId = "user-2"; await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); const visibleIssueId = randomUUID(); const archivedIssueId = randomUUID(); const resurfacedIssueId = randomUUID(); await db.insert(issues).values([ { id: visibleIssueId, companyId, title: "Visible issue", status: "todo", priority: "medium", createdByUserId: userId, createdAt: new Date("2026-03-26T10:00:00.000Z"), updatedAt: new Date("2026-03-26T10:00:00.000Z"), }, { id: archivedIssueId, companyId, title: "Archived issue", status: "todo", priority: "medium", createdByUserId: userId, createdAt: new Date("2026-03-26T11:00:00.000Z"), updatedAt: new Date("2026-03-26T11:00:00.000Z"), }, { id: resurfacedIssueId, companyId, title: "Resurfaced issue", status: "todo", priority: "medium", createdByUserId: userId, createdAt: new Date("2026-03-26T12:00:00.000Z"), updatedAt: new Date("2026-03-26T12:00:00.000Z"), }, ]); await svc.archiveInbox( companyId, archivedIssueId, userId, new Date("2026-03-26T12:30:00.000Z"), ); await svc.archiveInbox( companyId, resurfacedIssueId, userId, new Date("2026-03-26T13:00:00.000Z"), ); await db.insert(issueComments).values({ companyId, issueId: resurfacedIssueId, authorUserId: otherUserId, body: "This should bring the issue back into Mine.", createdAt: new Date("2026-03-26T13:30:00.000Z"), updatedAt: new Date("2026-03-26T13:30:00.000Z"), }); const archivedFiltered = await svc.list(companyId, { touchedByUserId: userId, inboxArchivedByUserId: userId, }); expect(archivedFiltered.map((issue) => issue.id)).toEqual([ resurfacedIssueId, visibleIssueId, ]); await svc.unarchiveInbox(companyId, archivedIssueId, userId); const afterUnarchive = await svc.list(companyId, { touchedByUserId: userId, inboxArchivedByUserId: userId, }); expect(new Set(afterUnarchive.map((issue) => issue.id))).toEqual(new Set([ visibleIssueId, archivedIssueId, resurfacedIssueId, ])); }); });