import { randomUUID } from "node:crypto"; import { eq, inArray } from "drizzle-orm"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; import { agents, agentWakeupRequests, companies, createDb, heartbeatRuns, issueComments, issueTreeHoldMembers, issueTreeHolds, issues, } from "@paperclipai/db"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, } from "./helpers/embedded-postgres.js"; import { issueTreeControlService } from "../services/issue-tree-control.js"; import { issueService } from "../services/issues.js"; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; if (!embeddedPostgresSupport.supported) { console.warn( `Skipping embedded Postgres issue tree control service tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, ); } describeEmbeddedPostgres("issueTreeControlService", () => { let db!: ReturnType; let tempDb: Awaited> | null = null; beforeAll(async () => { tempDb = await startEmbeddedPostgresTestDatabase("paperclip-issue-tree-control-"); db = createDb(tempDb.connectionString); }, 20_000); afterEach(async () => { await db.delete(issueTreeHoldMembers); await db.delete(issueTreeHolds); await db.delete(issueComments); await db.delete(issues); await db.delete(heartbeatRuns); await db.delete(agentWakeupRequests); await db.delete(agents); await db.delete(companies); }); afterAll(async () => { await tempDb?.cleanup(); }); it("previews a subtree without changing issue statuses", async () => { const companyId = randomUUID(); const otherCompanyId = randomUUID(); const agentId = randomUUID(); const runId = randomUUID(); const rootIssueId = randomUUID(); const runningChildId = randomUUID(); const doneChildId = randomUUID(); const cancelledChildId = randomUUID(); await db.insert(companies).values([ { id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }, { id: otherCompanyId, name: "OtherCo", issuePrefix: `T${otherCompanyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }, ]); await db.insert(agents).values({ id: agentId, companyId, name: "CodexCoder", role: "engineer", status: "running", adapterType: "codex_local", adapterConfig: {}, runtimeConfig: {}, permissions: {}, }); await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, invocationSource: "assignment", status: "running", contextSnapshot: { issueId: runningChildId }, }); await db.insert(issues).values([ { id: rootIssueId, companyId, title: "Root", status: "todo", priority: "medium", createdAt: new Date("2026-04-21T10:00:00.000Z"), }, { id: runningChildId, companyId, parentId: rootIssueId, title: "Running child", status: "in_progress", priority: "medium", assigneeAgentId: agentId, executionRunId: runId, createdAt: new Date("2026-04-21T10:01:00.000Z"), }, { id: doneChildId, companyId, parentId: rootIssueId, title: "Done child", status: "done", priority: "medium", createdAt: new Date("2026-04-21T10:02:00.000Z"), }, { id: cancelledChildId, companyId, parentId: rootIssueId, title: "Cancelled child", status: "cancelled", priority: "medium", createdAt: new Date("2026-04-21T10:03:00.000Z"), }, ]); const svc = issueTreeControlService(db); const preview = await svc.preview(companyId, rootIssueId, { mode: "pause" }); expect(preview.issues.map((issue) => [issue.id, issue.depth, issue.skipped, issue.skipReason])).toEqual([ [rootIssueId, 0, false, null], [runningChildId, 1, false, null], [doneChildId, 1, true, "terminal_status"], [cancelledChildId, 1, true, "terminal_status"], ]); expect(preview.totals).toMatchObject({ totalIssues: 4, affectedIssues: 2, skippedIssues: 2, activeRuns: 1, queuedRuns: 0, affectedAgents: 1, }); expect(preview.countsByStatus).toMatchObject({ todo: 1, in_progress: 1, done: 1, cancelled: 1 }); expect(preview.activeRuns).toEqual([ expect.objectContaining({ id: runId, issueId: runningChildId, agentId, status: "running" }), ]); expect(preview.warnings.map((warning) => warning.code)).toContain("running_runs_present"); const [runningChildAfterPreview] = await db .select() .from(issues) .where(eq(issues.id, runningChildId)); expect(runningChildAfterPreview.status).toBe("in_progress"); await expect(svc.preview(otherCompanyId, rootIssueId, { mode: "pause" })).rejects.toMatchObject({ status: 404, }); }); it("creates and releases normalized hold snapshots", async () => { const companyId = randomUUID(); const rootIssueId = randomUUID(); await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); await db.insert(issues).values({ id: rootIssueId, companyId, title: "Root", status: "todo", priority: "medium", }); const svc = issueTreeControlService(db); const created = await svc.createHold(companyId, rootIssueId, { mode: "pause", reason: "operator requested pause", actor: { actorType: "user", actorId: "board-user", userId: "board-user" }, }); expect(created.hold.status).toBe("active"); expect(created.hold.members).toHaveLength(1); expect(created.hold.members?.[0]).toMatchObject({ issueId: rootIssueId, issueStatus: "todo", skipped: false, }); const released = await svc.releaseHold(companyId, rootIssueId, created.hold.id, { reason: "operator resumed", actor: { actorType: "user", actorId: "board-user", userId: "board-user" }, }); expect(released.status).toBe("released"); expect(released.releaseReason).toBe("operator resumed"); expect(released.members).toHaveLength(1); }); it("cancels non-terminal issue statuses and restores from the cancel snapshot", async () => { const companyId = randomUUID(); const rootIssueId = randomUUID(); const runningChildId = randomUUID(); const todoChildId = randomUUID(); const doneChildId = randomUUID(); await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); await db.insert(issues).values([ { id: rootIssueId, companyId, title: "Root", status: "done", priority: "medium", createdAt: new Date("2026-04-21T10:00:00.000Z"), }, { id: runningChildId, companyId, parentId: rootIssueId, title: "Running child", status: "in_progress", priority: "medium", createdAt: new Date("2026-04-21T10:01:00.000Z"), }, { id: todoChildId, companyId, parentId: rootIssueId, title: "Todo child", status: "todo", priority: "medium", createdAt: new Date("2026-04-21T10:02:00.000Z"), }, { id: doneChildId, companyId, parentId: rootIssueId, title: "Done child", status: "done", priority: "medium", createdAt: new Date("2026-04-21T10:03:00.000Z"), }, ]); const svc = issueTreeControlService(db); const cancel = await svc.createHold(companyId, rootIssueId, { mode: "cancel", reason: "bad plan", actor: { actorType: "user", actorId: "board-user", userId: "board-user" }, }); expect(cancel.preview.issues.map((issue) => [issue.id, issue.skipped, issue.skipReason])).toEqual([ [rootIssueId, true, "terminal_status"], [runningChildId, false, null], [todoChildId, false, null], [doneChildId, true, "terminal_status"], ]); const cancelled = await svc.cancelIssueStatusesForHold(companyId, rootIssueId, cancel.hold.id); expect(cancelled.updatedIssueIds.sort()).toEqual([runningChildId, todoChildId].sort()); const afterCancel = await db .select({ id: issues.id, status: issues.status }) .from(issues) .where(inArray(issues.id, [runningChildId, todoChildId, doneChildId])); expect(Object.fromEntries(afterCancel.map((issue) => [issue.id, issue.status]))).toMatchObject({ [runningChildId]: "cancelled", [todoChildId]: "cancelled", [doneChildId]: "done", }); await db .update(issues) .set({ status: "blocked", cancelledAt: null, updatedAt: new Date() }) .where(eq(issues.id, todoChildId)); const restorePreview = await svc.preview(companyId, rootIssueId, { mode: "restore" }); expect(restorePreview.issues.map((issue) => [issue.id, issue.skipped, issue.skipReason])).toEqual([ [rootIssueId, true, "not_cancelled"], [runningChildId, false, null], [todoChildId, true, "changed_after_cancel"], [doneChildId, true, "not_cancelled"], ]); expect(restorePreview.warnings.map((warning) => warning.code)).toContain("restore_conflicts_present"); const restore = await svc.createHold(companyId, rootIssueId, { mode: "restore", reason: "resume useful work", actor: { actorType: "user", actorId: "board-user", userId: "board-user" }, }); const restored = await svc.restoreIssueStatusesForHold(companyId, rootIssueId, restore.hold.id, { reason: "resume useful work", actor: { actorType: "user", actorId: "board-user", userId: "board-user" }, }); expect(restored.updatedIssueIds).toEqual([runningChildId]); const afterRestore = await db .select({ id: issues.id, status: issues.status, checkoutRunId: issues.checkoutRunId, executionRunId: issues.executionRunId }) .from(issues) .where(inArray(issues.id, [runningChildId, todoChildId, doneChildId])); expect(Object.fromEntries(afterRestore.map((issue) => [issue.id, issue.status]))).toMatchObject({ [runningChildId]: "todo", [todoChildId]: "blocked", [doneChildId]: "done", }); const holds = await db .select({ id: issueTreeHolds.id, mode: issueTreeHolds.mode, status: issueTreeHolds.status }) .from(issueTreeHolds) .where(inArray(issueTreeHolds.id, [cancel.hold.id, restore.hold.id])); expect(Object.fromEntries(holds.map((hold) => [hold.mode, hold.status]))).toMatchObject({ cancel: "released", restore: "released", }); }); it("walks pause-hold ancestry beyond 15 levels for checkout and interaction waives", async () => { const companyId = randomUUID(); const agentId = randomUUID(); const issuePath = Array.from({ length: 17 }, () => randomUUID()); const rootIssueId = issuePath[0]; const deepDescendantIssueId = issuePath.at(-1)!; const rootRunId = randomUUID(); const deepDescendantRunId = randomUUID(); const forgedRunId = randomUUID(); const rootWakeupRequestId = randomUUID(); const deepDescendantWakeupRequestId = randomUUID(); const forgedWakeupRequestId = randomUUID(); const rootCommentId = randomUUID(); const deepDescendantCommentId = 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: "SecurityEngineer", role: "engineer", status: "active", adapterType: "codex_local", adapterConfig: {}, runtimeConfig: {}, permissions: {}, }); await db.insert(issues).values( issuePath.map((issueId, index) => ({ id: issueId, companyId, parentId: index > 0 ? issuePath[index - 1] : null, title: `Issue ${index}`, status: "todo", priority: "medium", assigneeAgentId: agentId, })), ); await db.insert(issueComments).values([ { id: rootCommentId, companyId, issueId: rootIssueId, authorUserId: "board-user", body: "Please answer this root issue question.", }, { id: deepDescendantCommentId, companyId, issueId: deepDescendantIssueId, authorUserId: "board-user", body: "Please answer this deep descendant issue question.", }, ]); await db.insert(agentWakeupRequests).values([ { id: rootWakeupRequestId, companyId, agentId, source: "automation", triggerDetail: "system", reason: "issue_commented", payload: { issueId: rootIssueId, commentId: rootCommentId }, status: "queued", requestedByActorType: "user", requestedByActorId: "board-user", runId: rootRunId, }, { id: forgedWakeupRequestId, companyId, agentId, source: "on_demand", triggerDetail: "manual", reason: "issue_commented", payload: { issueId: deepDescendantIssueId, commentId: deepDescendantCommentId }, status: "queued", requestedByActorType: "agent", requestedByActorId: agentId, runId: forgedRunId, }, { id: deepDescendantWakeupRequestId, companyId, agentId, source: "automation", triggerDetail: "system", reason: "issue_commented", payload: { issueId: deepDescendantIssueId, commentId: deepDescendantCommentId }, status: "queued", requestedByActorType: "user", requestedByActorId: "board-user", runId: deepDescendantRunId, }, ]); await db.insert(heartbeatRuns).values([ { id: rootRunId, companyId, agentId, invocationSource: "automation", triggerDetail: "system", status: "queued", wakeupRequestId: rootWakeupRequestId, contextSnapshot: { issueId: rootIssueId, wakeReason: "issue_commented", commentId: rootCommentId, wakeCommentId: rootCommentId, source: "issue.comment", }, }, { id: forgedRunId, companyId, agentId, invocationSource: "on_demand", triggerDetail: "manual", status: "queued", wakeupRequestId: forgedWakeupRequestId, contextSnapshot: { issueId: deepDescendantIssueId, wakeReason: "issue_commented", commentId: deepDescendantCommentId, wakeCommentId: deepDescendantCommentId, }, }, { id: deepDescendantRunId, companyId, agentId, invocationSource: "automation", triggerDetail: "system", status: "queued", wakeupRequestId: deepDescendantWakeupRequestId, contextSnapshot: { issueId: deepDescendantIssueId, wakeReason: "issue_commented", commentId: deepDescendantCommentId, wakeCommentId: deepDescendantCommentId, source: "issue.comment", }, }, ]); const treeSvc = issueTreeControlService(db); await treeSvc.createHold(companyId, rootIssueId, { mode: "pause", reason: "operator requested pause", actor: { actorType: "user", actorId: "board-user", userId: "board-user" }, }); const deepDescendantGate = await treeSvc.getActivePauseHoldGate(companyId, deepDescendantIssueId); expect(deepDescendantGate).toMatchObject({ holdId: expect.any(String), rootIssueId, issueId: deepDescendantIssueId, isRoot: false, mode: "pause", }); const issueSvc = issueService(db); await expect( issueSvc.checkout(deepDescendantIssueId, agentId, ["todo"], randomUUID()), ).rejects.toMatchObject({ status: 409, details: expect.objectContaining({ rootIssueId, mode: "pause", }), }); await expect( issueSvc.checkout(deepDescendantIssueId, agentId, ["todo"], forgedRunId), ).rejects.toMatchObject({ status: 409, details: expect.objectContaining({ rootIssueId, mode: "pause", }), }); const checkedOutChild = await issueSvc.checkout(deepDescendantIssueId, agentId, ["todo"], deepDescendantRunId); expect(checkedOutChild.status).toBe("in_progress"); expect(checkedOutChild.checkoutRunId).toBe(deepDescendantRunId); const checkedOutRoot = await issueSvc.checkout(rootIssueId, agentId, ["todo"], rootRunId); expect(checkedOutRoot.status).toBe("in_progress"); expect(checkedOutRoot.checkoutRunId).toBe(rootRunId); await db.update(issues).set({ status: "todo", checkoutRunId: null, executionRunId: null, executionAgentNameKey: null, executionLockedAt: null, updatedAt: new Date(), }).where(eq(issues.id, rootIssueId)); await db.update(issueTreeHolds).set({ status: "released", releasedAt: new Date(), releasedByActorType: "user", releasedByUserId: "board-user", releaseReason: "switch to full pause", updatedAt: new Date(), }).where(eq(issueTreeHolds.rootIssueId, rootIssueId)); await treeSvc.createHold(companyId, rootIssueId, { mode: "pause", reason: "full pause", releasePolicy: { strategy: "manual", note: "full_pause" }, actor: { actorType: "user", actorId: "board-user", userId: "board-user" }, }); const checkedOutLegacyFullPauseRoot = await issueSvc.checkout(rootIssueId, agentId, ["todo"], rootRunId); expect(checkedOutLegacyFullPauseRoot.status).toBe("in_progress"); expect(checkedOutLegacyFullPauseRoot.checkoutRunId).toBe(rootRunId); }); it("resumes subtree pauses by releasing matching pause holds", async () => { const companyId = randomUUID(); const rootIssueId = randomUUID(); const childIssueId = randomUUID(); const nonSubtreeIssueId = randomUUID(); await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); await db.insert(issues).values([ { id: rootIssueId, companyId, title: "Root", status: "todo", priority: "medium", }, { id: childIssueId, companyId, parentId: rootIssueId, title: "Child", status: "todo", priority: "medium", }, { id: nonSubtreeIssueId, companyId, title: "Unrelated", status: "todo", priority: "medium", }, ]); const treeSvc = issueTreeControlService(db); const subtreePause = await treeSvc.createHold(companyId, childIssueId, { mode: "pause", reason: "pause child only", actor: { actorType: "user", actorId: "board-user", userId: "board-user" }, }); const nonSubtreePause = await treeSvc.createHold(companyId, nonSubtreeIssueId, { mode: "pause", reason: "pause unrelated issue", actor: { actorType: "user", actorId: "board-user", userId: "board-user" }, }); const resumed = await treeSvc.createHold(companyId, rootIssueId, { mode: "resume", reason: "resume subtree", actor: { actorType: "user", actorId: "board-user", userId: "board-user" }, }); expect(resumed.hold.mode).toBe("resume"); expect(resumed.hold.status).toBe("released"); expect(resumed.resumedPauseHoldIds).toEqual([subtreePause.hold.id]); const rows = await db .select({ id: issueTreeHolds.id, status: issueTreeHolds.status, releaseMetadata: issueTreeHolds.releaseMetadata }) .from(issueTreeHolds) .where(eq(issueTreeHolds.companyId, companyId)); const byId = new Map(rows.map((row) => [row.id, row] as const)); expect(byId.get(subtreePause.hold.id)?.status).toBe("released"); expect(byId.get(nonSubtreePause.hold.id)?.status).toBe("active"); expect(byId.get(resumed.hold.id)?.status).toBe("released"); const releaseMetadata = byId.get(subtreePause.hold.id)?.releaseMetadata as | Record | null; expect(releaseMetadata).toMatchObject({ resumedByResumeHoldId: resumed.hold.id, resumeHoldMode: "tree_resume", resumedPauseHoldId: subtreePause.hold.id, }); expect((byId.get(resumed.hold.id)?.releaseMetadata as Record | null)).toMatchObject({ resumedPauseHoldIds: [subtreePause.hold.id], resumeMode: "subtree", }); }); });