import { describe, expect, it, vi } from "vitest"; import type { Issue } from "@paperclipai/shared"; import { ATTACHMENT_NOTE } from "../src/constants.js"; import { buildForgejoIssuePayload, DEFAULT_SYNC_LABEL, isIssueSelected, syncIssueToForgejo } from "../src/paperclip-issue-sync.js"; function buildIssue(overrides: Partial = {}): Issue { return { id: "issue-1", companyId: "company-1", projectId: null, projectWorkspaceId: null, goalId: null, parentId: null, title: "Fix Forgejo sync", description: "See attached screenshot for the exact failure.", status: "in_progress", workMode: "standard", priority: "medium", assigneeAgentId: null, assigneeUserId: null, checkoutRunId: null, executionRunId: null, executionAgentNameKey: null, executionLockedAt: null, createdByAgentId: null, createdByUserId: null, issueNumber: 17, identifier: "PRIA-17", requestDepth: 0, billingCode: null, assigneeAdapterOverrides: null, executionWorkspaceId: null, executionWorkspacePreference: null, executionWorkspaceSettings: null, startedAt: null, completedAt: null, cancelledAt: null, hiddenAt: null, labels: [ { id: "label-1", companyId: "company-1", name: DEFAULT_SYNC_LABEL, color: "#000000", createdAt: new Date("2026-06-02T00:00:00Z"), updatedAt: new Date("2026-06-02T00:00:00Z") } ], blockedBy: [], blocks: [], project: { id: "project-1", companyId: "company-1", name: "Forgejo Issue Sync Plugin", description: null, status: "planned", primaryGoalId: null, createdAt: new Date("2026-06-02T00:00:00Z"), updatedAt: new Date("2026-06-02T00:00:00Z") }, goal: null, currentExecutionWorkspace: null, mentionedProjects: [], myLastTouchAt: null, lastExternalCommentAt: null, lastActivityAt: null, isUnreadForMe: false, createdAt: new Date("2026-06-02T00:00:00Z"), updatedAt: new Date("2026-06-02T00:00:00Z"), ...overrides } as Issue; } describe("paperclip issue sync", () => { it("selects issues by the configured sync label", () => { const issue = buildIssue(); expect(isIssueSelected(issue, {})).toBe(true); expect(isIssueSelected(issue, { syncIssueLabel: "custom-sync" })).toBe(false); }); it("builds a Forgejo issue body with metadata-only attachments", () => { const issue = buildIssue({ workProducts: [ { id: "wp-1", companyId: "company-1", projectId: null, issueId: "issue-1", executionWorkspaceId: null, runtimeServiceId: null, type: "artifact", provider: "paperclip", externalId: "attachment-1", title: "trace.log", url: "https://paperclip.example/artifacts/trace.log", status: "ready_for_review", reviewState: "none", isPrimary: false, healthStatus: "healthy", summary: null, metadata: { attachmentId: "attachment-1", contentType: "text/plain", byteSize: 2048, originalFilename: "trace.log" }, createdByRunId: null, createdAt: new Date("2026-06-02T00:00:00Z"), updatedAt: new Date("2026-06-02T00:00:00Z") } ] } as Partial); const payload = buildForgejoIssuePayload(issue); expect(payload.title).toContain("[PRIA-17]"); expect(payload.body).toContain(ATTACHMENT_NOTE); expect(payload.body).toContain("trace.log | text/plain | 2.0 KiB"); expect(payload.body).toContain(""); }); it("creates a Forgejo issue once and queues manual review when attachment context is required", async () => { const issue = buildIssue({ workProducts: [ { id: "wp-1", companyId: "company-1", projectId: null, issueId: "issue-1", executionWorkspaceId: null, runtimeServiceId: null, type: "artifact", provider: "paperclip", externalId: "attachment-1", title: "screenshot.png", url: "https://paperclip.example/artifacts/screenshot.png", status: "ready_for_review", reviewState: "none", isPrimary: false, healthStatus: "healthy", summary: null, metadata: { attachmentId: "attachment-1", contentType: "image/png", byteSize: 1024, originalFilename: "screenshot.png" }, createdByRunId: null, createdAt: new Date("2026-06-02T00:00:00Z"), updatedAt: new Date("2026-06-02T00:00:00Z") } ] } as Partial); const activityLog = vi.fn(async () => undefined); const reserve = vi.fn(async () => ({ kind: "reserved" as const })); const createRemoteIssue = vi.fn(async () => ({ id: 101, number: 33, url: "https://forgejo.example/acme/repo/issues/33", apiUrl: "https://forgejo.example/api/v1/repos/acme/repo/issues/33" })); const complete = vi.fn(async () => undefined); const fail = vi.fn(async () => undefined); const queueManualReview = vi.fn(async () => undefined); const result = await syncIssueToForgejo( { config: { get: async () => ({ forgejoOwner: "acme", forgejoRepo: "repo" }) }, activity: { log: activityLog } } as never, issue, { reserve, createRemoteIssue, complete, fail, queueManualReview } ); expect(result).toBe("created"); expect(createRemoteIssue).toHaveBeenCalledOnce(); expect(complete).toHaveBeenCalledWith(expect.anything(), "company-1", "issue-1", expect.objectContaining({ number: 33 })); expect(queueManualReview).toHaveBeenCalledOnce(); expect(activityLog).toHaveBeenCalledWith(expect.objectContaining({ message: "Created Forgejo issue #33 for PRIA-17." })); expect(fail).not.toHaveBeenCalled(); }); it("skips remote creation when a synced mapping already exists", async () => { const issue = buildIssue(); const createRemoteIssue = vi.fn(); const result = await syncIssueToForgejo( { config: { get: async () => ({ forgejoOwner: "acme", forgejoRepo: "repo" }) }, activity: { log: vi.fn(async () => undefined) } } as never, issue, { reserve: async () => ({ kind: "existing", mapping: { companyId: "company-1", paperclipIssueId: "issue-1", repoOwner: "acme", repoName: "repo", dedupeKey: "paperclip-issue:company-1:issue-1", sourceTitle: "[PRIA-17] Fix Forgejo sync", sourceBody: "body", attachmentMetadata: [], manualReviewRequired: false, reviewReasonCode: null, forgejoIssueId: 101, forgejoIssueNumber: 33, forgejoIssueUrl: "https://forgejo.example/acme/repo/issues/33", forgejoApiUrl: "https://forgejo.example/api/v1/repos/acme/repo/issues/33", syncStatus: "synced", lastError: null } }), createRemoteIssue } ); expect(result).toBe("existing"); expect(createRemoteIssue).not.toHaveBeenCalled(); }); });