From 04b3c6483bd9a4f56398763196b7cb3f3b18dae0 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 23 Jun 2026 01:12:39 +0000 Subject: [PATCH] Clean external references for tombstoned comments --- .../issue-comment-cancel-routes.test.ts | 26 +++++++++++++++++++ server/src/routes/issues.ts | 6 ++++- server/src/services/external-objects.ts | 4 +-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/server/src/__tests__/issue-comment-cancel-routes.test.ts b/server/src/__tests__/issue-comment-cancel-routes.test.ts index 8c841f08d1a..bd46a12ce3d 100644 --- a/server/src/__tests__/issue-comment-cancel-routes.test.ts +++ b/server/src/__tests__/issue-comment-cancel-routes.test.ts @@ -57,6 +57,24 @@ const mockIssueReferenceService = vi.hoisted(() => ({ syncDocument: vi.fn(async () => undefined), syncIssue: vi.fn(async () => undefined), })); +const mockExternalObjectService = vi.hoisted(() => ({ + getIssueSummaries: vi.fn(async () => ({ summaries: {} })), + getIssueSummary: vi.fn(async () => ({ + authRequiredCount: 0, + byLiveness: {}, + byStatusCategory: {}, + highestSeverity: "muted", + objects: [], + staleCount: 0, + total: 0, + unreachableCount: 0, + })), + listForIssue: vi.fn(async () => []), + refreshIssueObjects: vi.fn(async () => []), + syncCommentSafely: vi.fn(async () => undefined), + syncDocumentSafely: vi.fn(async () => undefined), + syncIssueSafely: vi.fn(async () => undefined), +})); function registerModuleMocks() { vi.doMock("@paperclipai/shared/telemetry", () => ({ @@ -92,6 +110,10 @@ function registerModuleMocks() { issueService: () => mockIssueService, })); + vi.doMock("../services/external-objects.js", () => ({ + externalObjectService: () => mockExternalObjectService, + })); + vi.doMock("../services/index.js", () => ({ companyService: () => ({ getById: vi.fn(async () => ({ id: "company-1", attachmentMaxBytes: 10 * 1024 * 1024 })), @@ -181,6 +203,7 @@ describe.sequential("issue comment cancel routes", () => { vi.doUnmock("../telemetry.js"); vi.doUnmock("../services/access.js"); vi.doUnmock("../services/activity-log.js"); + vi.doUnmock("../services/external-objects.js"); vi.doUnmock("../services/feedback.js"); vi.doUnmock("../services/heartbeat.js"); vi.doUnmock("../services/index.js"); @@ -240,6 +263,7 @@ describe.sequential("issue comment cancel routes", () => { }); mockIssueReferenceService.deleteCommentSource.mockResolvedValue(undefined); mockIssueReferenceService.syncComment.mockResolvedValue(undefined); + mockExternalObjectService.syncCommentSafely.mockResolvedValue(undefined); }); it("cancels a queued comment from its author and restores the deleted body", async () => { @@ -352,6 +376,7 @@ describe.sequential("issue comment cancel routes", () => { expect.objectContaining({ afterTombstone: expect.any(Function) }), ); expect(mockIssueReferenceService.syncComment).toHaveBeenCalledWith("comment-1", "tx"); + expect(mockExternalObjectService.syncCommentSafely).toHaveBeenCalledWith("comment-1", "tx"); expect(mockDocumentAnnotationService.cleanupForIssueCommentDeletion).toHaveBeenCalledWith( "11111111-1111-4111-8111-111111111111", "comment-1", @@ -362,6 +387,7 @@ describe.sequential("issue comment cancel routes", () => { "tx", ); expect(mockIssueReferenceService.deleteCommentSource).toHaveBeenCalledWith("annotation-comment-1", "tx"); + expect(mockExternalObjectService.syncCommentSafely).toHaveBeenCalledWith("annotation-comment-1", "tx"); const deletedActivity = mockLogActivity.mock.calls.find((call) => call[1]?.action === "issue.comment_deleted")?.[1]; expect(deletedActivity).toEqual(expect.objectContaining({ action: "issue.comment_deleted", diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index ac5fc95e2ef..5788267197f 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -7309,6 +7309,7 @@ export function issueRoutes( { afterTombstone: async (deletedComment, tx) => { await issueReferencesSvc.syncComment(deletedComment.id, tx); + await externalObjectsSvc.syncCommentSafely(deletedComment.id, tx); annotationCleanup = await documentAnnotationsSvc.cleanupForIssueCommentDeletion(issue.id, deletedComment.id, { actorType: actor.actorType, actorId: actor.actorId, @@ -7318,7 +7319,10 @@ export function issueRoutes( }, tx); await Promise.all( annotationCleanup.deletedCommentIds.map((annotationCommentId) => - issueReferencesSvc.deleteCommentSource(annotationCommentId, tx) + Promise.all([ + issueReferencesSvc.deleteCommentSource(annotationCommentId, tx), + externalObjectsSvc.syncCommentSafely(annotationCommentId, tx), + ]) ), ); }, diff --git a/server/src/services/external-objects.ts b/server/src/services/external-objects.ts index 0cd4f306ce6..ad7204275ce 100644 --- a/server/src/services/external-objects.ts +++ b/server/src/services/external-objects.ts @@ -581,8 +581,8 @@ export function externalObjectService( await safeSync("issue", () => syncIssue(issueId)); } - async function syncCommentSafely(commentId: string) { - await safeSync("comment", () => syncComment(commentId)); + async function syncCommentSafely(commentId: string, dbOrTx: any = db) { + await safeSync("comment", () => syncComment(commentId, dbOrTx)); } async function syncDocumentSafely(documentId: string) {