From 6d323393a3cff558a78915e5b265652133606a56 Mon Sep 17 00:00:00 2001 From: Paperclip Bot Date: Tue, 2 Jun 2026 10:56:57 +0000 Subject: [PATCH] Add synthetic dry-run sync regression test --- tests/paperclip-issue-sync.spec.ts | 84 ++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/tests/paperclip-issue-sync.spec.ts b/tests/paperclip-issue-sync.spec.ts index 0d42df3..54bb75b 100644 --- a/tests/paperclip-issue-sync.spec.ts +++ b/tests/paperclip-issue-sync.spec.ts @@ -1,3 +1,5 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; import { describe, expect, it, vi } from "vitest"; import type { Issue } from "@paperclipai/shared"; import { ATTACHMENT_NOTE } from "../src/constants.js"; @@ -76,6 +78,11 @@ function buildIssue(overrides: Partial = {}): Issue { } as Issue; } +function readExampleIssue(name: string): Issue { + const filePath = resolve(import.meta.dirname, "..", "examples", name); + return JSON.parse(readFileSync(filePath, "utf8")) as Issue; +} + describe("paperclip issue sync", () => { it("selects issues by the configured sync label", () => { const issue = buildIssue(); @@ -325,4 +332,81 @@ describe("paperclip issue sync", () => { expect(retryComplete).toHaveBeenCalledOnce(); expect(retryQueueManualReview).not.toHaveBeenCalled(); }); + + it("reproduces the synthetic dry-run payload and reuses the stored mapping on retry", async () => { + const issue = readExampleIssue("first-dry-run-paperclip-issue.json"); + const payload = buildForgejoIssuePayload(issue); + const createdRemoteIssue = { + id: 401, + number: 77, + url: "https://forgejo.example/acme/repo/issues/77", + apiUrl: "https://forgejo.example/api/v1/repos/acme/repo/issues/77" + }; + const createRemoteIssue = vi.fn(async () => createdRemoteIssue); + const complete = vi.fn(async () => undefined); + + expect(payload.title).toBe("[PRIA-DRY-1] Synthetic dry-run: reproduce outbound issue creation"); + expect(payload.body).toContain("A controlled dry-run payload for the first Forgejo sync rehearsal."); + expect(payload.body).toContain(ATTACHMENT_NOTE); + expect(payload.body).toContain("trace.log | text/plain | 2.0 KiB"); + expect(payload.body).toContain(""); + + const firstResult = await syncIssueToForgejo( + { + config: { get: async () => ({ forgejoOwner: "acme", forgejoRepo: "repo" }) }, + activity: { log: vi.fn(async () => undefined) } + } as never, + issue, + { + reserve: async () => ({ kind: "reserved" as const }), + createRemoteIssue, + recordRemote: vi.fn(async () => undefined), + complete, + fail: vi.fn(async () => undefined), + failAfterRemote: vi.fn(async () => undefined), + queueManualReview: vi.fn(async () => undefined) + } + ); + + const retryResult = await syncIssueToForgejo( + { + config: { get: async () => ({ forgejoOwner: "acme", forgejoRepo: "repo" }) }, + activity: { log: vi.fn(async () => undefined) } + } as never, + issue, + { + reserve: async () => ({ + kind: "existing" as const, + mapping: { + companyId: "company-dry-run", + paperclipIssueId: "issue-dry-run-1", + repoOwner: "acme", + repoName: "repo", + dedupeKey: "paperclip-issue:company-dry-run:issue-dry-run-1", + sourceTitle: payload.title, + sourceBody: payload.body, + attachmentMetadata: [], + manualReviewRequired: false, + reviewReasonCode: null, + forgejoIssueId: createdRemoteIssue.id, + forgejoIssueNumber: createdRemoteIssue.number, + forgejoIssueUrl: createdRemoteIssue.url, + forgejoApiUrl: createdRemoteIssue.apiUrl, + syncStatus: "synced", + lastError: null + } + }), + createRemoteIssue, + complete, + fail: vi.fn(async () => undefined), + failAfterRemote: vi.fn(async () => undefined), + queueManualReview: vi.fn(async () => undefined) + } + ); + + expect(firstResult).toBe("created"); + expect(retryResult).toBe("existing"); + expect(createRemoteIssue).toHaveBeenCalledTimes(1); + expect(complete).toHaveBeenCalledTimes(2); + }); });