diff --git a/server/src/__tests__/documents.test.ts b/server/src/__tests__/documents.test.ts index c36b7265ac..35bb81b552 100644 --- a/server/src/__tests__/documents.test.ts +++ b/server/src/__tests__/documents.test.ts @@ -1,5 +1,6 @@ -import { describe, expect, it } from "vitest"; -import { extractLegacyPlanBody } from "../services/documents.js"; +import type { Db } from "@paperclipai/db"; +import { describe, expect, it, vi } from "vitest"; +import { documentService, extractLegacyPlanBody } from "../services/documents.js"; describe("extractLegacyPlanBody", () => { it("returns null when no plan block exists", () => { @@ -27,3 +28,36 @@ intro expect(extractLegacyPlanBody(" ")).toBeNull(); }); }); + +describe("document write conflicts", () => { + function failingService(error: Error) { + const db = { + select: () => ({ + from: () => ({ + where: () => Promise.resolve([{ id: "issue", companyId: "company" }]), + }), + }), + transaction: vi.fn().mockRejectedValue(error), + }; + return documentService(db as unknown as Db); + } + + const input = { issueId: "issue", key: "review", format: "markdown", body: "Review" }; + + it("reports a wrapped unique violation as a retryable document conflict", async () => { + const driverError = Object.assign(new Error("duplicate key"), { code: "23505" }); + const queryError = new Error("Failed query: insert into issue_documents", { cause: driverError }); + + await expect(failingService(queryError).upsertIssueDocument(input)).rejects.toMatchObject({ + status: 409, + message: "Document key already exists on this issue", + }); + }); + + it("does not disguise other database errors as document conflicts", async () => { + const driverError = Object.assign(new Error("connection unavailable"), { code: "08006" }); + const queryError = new Error("Failed query", { cause: driverError }); + + await expect(failingService(queryError).upsertIssueDocument(input)).rejects.toBe(queryError); + }); +}); diff --git a/server/src/services/documents.ts b/server/src/services/documents.ts index b30f79b22b..467be98b32 100644 --- a/server/src/services/documents.ts +++ b/server/src/services/documents.ts @@ -3,6 +3,7 @@ import { and, asc, desc, eq } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { documentRevisions, documents, issueDocuments, issues } from "@paperclipai/db"; import { isSystemIssueDocumentKey, issueDocumentKeySchema } from "@paperclipai/shared"; +import { isUniqueViolation } from "../db-errors.js"; import { conflict, notFound, unprocessable } from "../errors.js"; import { insertRowsInChunks } from "./batch-insert.js"; import type { ImportIssueDocumentRow } from "./import-write-types.js"; @@ -16,10 +17,6 @@ function normalizeDocumentKey(key: string) { return parsed.data; } -function isUniqueViolation(error: unknown): boolean { - return !!error && typeof error === "object" && "code" in error && (error as { code?: string }).code === "23505"; -} - function nextAvailableDocumentKey(sourceKey: string, existingKeys: string[]) { const usedKeys = new Set(existingKeys); for (let index = 2; index < 1000; index += 1) {