Preserve Forgejo mappings across retry failures
This commit is contained in:
parent
b0c38705ce
commit
fa24063483
5 changed files with 291 additions and 14 deletions
|
|
@ -121,6 +121,7 @@ describe("paperclip issue sync", () => {
|
|||
expect(payload.body).toContain(ATTACHMENT_NOTE);
|
||||
expect(payload.body).toContain("trace.log | text/plain | 2.0 KiB");
|
||||
expect(payload.body).toContain("<!-- paperclip-sync:company-1:issue-1 -->");
|
||||
expect(payload.body).not.toContain("https://paperclip.example/artifacts/trace.log");
|
||||
});
|
||||
|
||||
it("creates a Forgejo issue once and queues manual review when attachment context is required", async () => {
|
||||
|
|
@ -163,8 +164,10 @@ describe("paperclip issue sync", () => {
|
|||
url: "https://forgejo.example/acme/repo/issues/33",
|
||||
apiUrl: "https://forgejo.example/api/v1/repos/acme/repo/issues/33"
|
||||
}));
|
||||
const recordRemote = vi.fn(async () => undefined);
|
||||
const complete = vi.fn(async () => undefined);
|
||||
const fail = vi.fn(async () => undefined);
|
||||
const failAfterRemote = vi.fn(async () => undefined);
|
||||
const queueManualReview = vi.fn(async () => undefined);
|
||||
|
||||
const result = await syncIssueToForgejo(
|
||||
|
|
@ -173,22 +176,25 @@ describe("paperclip issue sync", () => {
|
|||
activity: { log: activityLog }
|
||||
} as never,
|
||||
issue,
|
||||
{ reserve, createRemoteIssue, complete, fail, queueManualReview }
|
||||
{ reserve, createRemoteIssue, recordRemote, complete, fail, failAfterRemote, queueManualReview }
|
||||
);
|
||||
|
||||
expect(result).toBe("created");
|
||||
expect(createRemoteIssue).toHaveBeenCalledOnce();
|
||||
expect(complete).toHaveBeenCalledWith(expect.anything(), "company-1", "issue-1", expect.objectContaining({ number: 33 }));
|
||||
expect(recordRemote).toHaveBeenCalledWith(expect.anything(), "company-1", "issue-1", expect.objectContaining({ number: 33 }));
|
||||
expect(complete).toHaveBeenCalledWith(expect.anything(), "company-1", "issue-1");
|
||||
expect(queueManualReview).toHaveBeenCalledOnce();
|
||||
expect(activityLog).toHaveBeenCalledWith(expect.objectContaining({
|
||||
message: "Created Forgejo issue #33 for PRIA-17."
|
||||
}));
|
||||
expect(fail).not.toHaveBeenCalled();
|
||||
expect(failAfterRemote).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips remote creation when a synced mapping already exists", async () => {
|
||||
const issue = buildIssue();
|
||||
const createRemoteIssue = vi.fn();
|
||||
const complete = vi.fn(async () => undefined);
|
||||
|
||||
const result = await syncIssueToForgejo(
|
||||
{
|
||||
|
|
@ -218,11 +224,105 @@ describe("paperclip issue sync", () => {
|
|||
lastError: null
|
||||
}
|
||||
}),
|
||||
createRemoteIssue
|
||||
createRemoteIssue,
|
||||
complete,
|
||||
failAfterRemote: vi.fn(async () => undefined)
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBe("existing");
|
||||
expect(createRemoteIssue).not.toHaveBeenCalled();
|
||||
expect(complete).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not create a duplicate Forgejo issue after remote success and local mapping failure", async () => {
|
||||
const issue = buildIssue();
|
||||
const createdRemoteIssue = {
|
||||
id: 101,
|
||||
number: 33,
|
||||
url: "https://forgejo.example/acme/repo/issues/33",
|
||||
apiUrl: "https://forgejo.example/api/v1/repos/acme/repo/issues/33"
|
||||
};
|
||||
const activityLog = vi.fn(async () => undefined);
|
||||
const createRemoteIssue = vi.fn(async () => createdRemoteIssue);
|
||||
const recordRemote = vi.fn(async () => {
|
||||
throw new Error("failed to complete local mapping");
|
||||
});
|
||||
const failAfterRemote = vi.fn(async () => undefined);
|
||||
|
||||
await expect(syncIssueToForgejo(
|
||||
{
|
||||
config: { get: async () => ({ forgejoOwner: "acme", forgejoRepo: "repo" }) },
|
||||
activity: { log: activityLog }
|
||||
} as never,
|
||||
issue,
|
||||
{
|
||||
reserve: async () => ({ kind: "reserved" as const }),
|
||||
createRemoteIssue,
|
||||
recordRemote,
|
||||
complete: vi.fn(async () => undefined),
|
||||
fail: vi.fn(async () => undefined),
|
||||
failAfterRemote
|
||||
}
|
||||
)).rejects.toThrow("failed to complete local mapping");
|
||||
|
||||
expect(createRemoteIssue).toHaveBeenCalledTimes(1);
|
||||
expect(failAfterRemote).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
"company-1",
|
||||
"issue-1",
|
||||
expect.objectContaining({ number: 33 }),
|
||||
"failed to complete local mapping"
|
||||
);
|
||||
|
||||
const retryCreateRemoteIssue = vi.fn(async () => {
|
||||
throw new Error("should not create a duplicate remote issue");
|
||||
});
|
||||
const retryRecordRemote = vi.fn(async () => undefined);
|
||||
const retryQueueManualReview = vi.fn(async () => undefined);
|
||||
const retryComplete = vi.fn(async () => undefined);
|
||||
|
||||
const result = await syncIssueToForgejo(
|
||||
{
|
||||
config: { get: async () => ({ forgejoOwner: "acme", forgejoRepo: "repo" }) },
|
||||
activity: { log: activityLog }
|
||||
} as never,
|
||||
issue,
|
||||
{
|
||||
reserve: async () => ({
|
||||
kind: "existing" as const,
|
||||
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: "remote_created",
|
||||
lastError: "failed to complete local mapping"
|
||||
}
|
||||
}),
|
||||
createRemoteIssue: retryCreateRemoteIssue,
|
||||
recordRemote: retryRecordRemote,
|
||||
complete: retryComplete,
|
||||
fail: vi.fn(async () => undefined),
|
||||
failAfterRemote: vi.fn(async () => undefined),
|
||||
queueManualReview: retryQueueManualReview
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBe("existing");
|
||||
expect(retryCreateRemoteIssue).not.toHaveBeenCalled();
|
||||
expect(retryRecordRemote).not.toHaveBeenCalled();
|
||||
expect(retryComplete).toHaveBeenCalledOnce();
|
||||
expect(retryQueueManualReview).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue