From 0482a078808ce8023a188df1c762e5ac55381f8a Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Wed, 3 Sep 2025 10:33:10 -0700 Subject: [PATCH] Better workflow tests --- apps/backend/src/prisma-client.tsx | 18 ++++++++++++------ apps/e2e/tests/backend/workflows.test.ts | 15 ++++++++++++--- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/apps/backend/src/prisma-client.tsx b/apps/backend/src/prisma-client.tsx index 7b138ae38..bfe1f6fba 100644 --- a/apps/backend/src/prisma-client.tsx +++ b/apps/backend/src/prisma-client.tsx @@ -125,6 +125,16 @@ export async function retryTransaction(client: PrismaClient, fn: (tx: PrismaC // serializable transactions are currently off by default, later we may turn them on const enableSerializable = options.level === "serializable"; + const isRetryablePrismaError = (e: unknown) => { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + return [ + "P2028", // Serializable/repeatable read conflict + "P2034", // Transaction already closed (eg. timeout) + ]; + } + return false; + }; + return await traceSpan('Prisma transaction', async (span) => { const res = await Result.retry(async (attemptIndex) => { return await traceSpan(`transaction attempt #${attemptIndex}`, async (attemptSpan) => { @@ -140,7 +150,7 @@ export async function retryTransaction(client: PrismaClient, fn: (tx: PrismaC // to other (nested) transactions failing // however, we make an exception for "Transaction already closed", as those are (annoyingly) thrown on // the actual query, not the $transaction function itself - if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2028") { // Transaction already closed + if (isRetryablePrismaError(e)) { throw new TransactionErrorThatShouldBeRetried(e); } throw new TransactionErrorThatShouldNotBeRetried(e); @@ -165,11 +175,7 @@ export async function retryTransaction(client: PrismaClient, fn: (tx: PrismaC if (e instanceof TransactionErrorThatShouldNotBeRetried) { throw e.cause; } - if ([ - "Transaction failed due to a write conflict or a deadlock. Please retry your transaction", - "Transaction already closed: A commit cannot be executed on an expired transaction. The timeout for this transaction", - ].some(s => e instanceof Prisma.PrismaClientKnownRequestError && e.message.includes(s))) { - // transaction timeout, retry + if (isRetryablePrismaError(e)) { return Result.error(e); } throw e; diff --git a/apps/e2e/tests/backend/workflows.test.ts b/apps/e2e/tests/backend/workflows.test.ts index e243e114e..2bcd2dc25 100644 --- a/apps/e2e/tests/backend/workflows.test.ts +++ b/apps/e2e/tests/backend/workflows.test.ts @@ -37,6 +37,15 @@ async function waitForMailboxSubject(mailbox: Mailbox, subject: string) { throw new Error(`Message with subject ${subject} not found after 10 tries`); } +async function waitForServerMetadataNotNull(userId: string, key: string) { + for (let i = 0; i < 10; i++) { + const user = await niceBackendFetch(`/api/v1/users/${userId}`, { accessType: "server" }); + if (user.body.server_metadata?.[key]) return; + await wait(1_000); + } + throw new Error(`Server metadata for user ${userId} with key ${key} not found after 10 tries`); +} + test("onSignUp workflow sends email for client sign-up", async ({ expect }) => { await Project.createAndSwitch(); const mailbox = await bumpEmailAddress({ unindexed: true }); @@ -243,7 +252,7 @@ test("anonymous sign-up does not trigger; upgrade triggers workflow", async ({ e const { userId } = await Auth.Password.signUpWithEmail({ password: "password" }); expect(userId).toEqual(anonUserId); - await wait(16_000); + await waitForServerMetadataNotNull(anonUserId, markerKey); const me2 = await niceBackendFetch("/api/v1/users/me", { accessType: "server" }); expect(me2.body.is_anonymous).toBe(false); expect(me2.body.server_metadata?.[markerKey]).toBe(me2.body.primary_email); @@ -269,7 +278,7 @@ test("workflow source changes take effect for subsequent sign-ups", async ({ exp }); await bumpEmailAddress({ unindexed: true }); await Auth.Password.signUpWithEmail({ password: "password" }); - await wait(16_000); + await waitForServerMetadataNotNull("me", markerKey); const me1 = await niceBackendFetch("/api/v1/users/me", { accessType: "server" }); expect(me1.body.server_metadata?.[markerKey]).toBe("v1"); @@ -287,7 +296,7 @@ test("workflow source changes take effect for subsequent sign-ups", async ({ exp }); await bumpEmailAddress({ unindexed: true }); await Auth.Password.signUpWithEmail({ password: "password" }); - await wait(16_000); + await waitForServerMetadataNotNull("me", markerKey); const me2 = await niceBackendFetch("/api/v1/users/me", { accessType: "server" }); expect(me2.body.server_metadata?.[markerKey]).toBe("v2"); }, {