From 093bbafe7661a686a51d090f56b167b87cc16bb6 Mon Sep 17 00:00:00 2001 From: Dave Date: Sun, 30 Aug 2026 08:40:17 +0200 Subject: [PATCH] fix(orders): cover the after() fallback path and log when it engages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mock for next/server's after() detaches the callback regardless of what production code does, which left the outer catch's fire-and-forget fallback untested and the runQuoteMatch inner catch tested for nothing. - New test 'falls back to an inline match when after() is unavailable' forces after() to throw and asserts suggestQuoteForOrder was called anyway, plus that the fallback log fired. Mutation: deleting the 'void runQuoteMatch()' line breaks it. - 'never fails order creation when the matcher throws' now spies on console.error and asserts the inner catch's swallow actually logged. Mutation: deleting the inner console.error breaks it. - The outer catch now logs [createOrder] after() unavailable... so a silent switch to fire-and-forget (after upgrade, waitUntil missing on deploy target) is impossible to miss in production logs. - Comment names error code E468, Next 16.2.6, and node_modules/next/dist/server/after/after.js, and corrects the justification: createOrder is an exported service function reachable from multiple layers, not 'a smoke script or another background job'. Verified against the documented baseline — same single pre-existing failure (splitOrderForContinuation > moves pending items...), no new failures introduced. Co-Authored-By: Claude --- src/lib/server/orders-service.test.ts | 48 +++++++++++++++++++++++++++ src/lib/server/orders-service.ts | 27 ++++++++++----- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/lib/server/orders-service.test.ts b/src/lib/server/orders-service.test.ts index ae8ffe6..94b289f 100644 --- a/src/lib/server/orders-service.test.ts +++ b/src/lib/server/orders-service.test.ts @@ -2904,6 +2904,9 @@ describe("createOrder quote auto-detection", () => { }); it("never fails order creation when the matcher throws", async () => { + const consoleErrorSpy = vi + .spyOn(console, "error") + .mockImplementation(() => {}); const { suggestQuoteForOrder } = await import("./order-quote-link-service"); vi.mocked(suggestQuoteForOrder).mockClear(); vi.mocked(suggestQuoteForOrder).mockRejectedValueOnce(new Error("PB down")); @@ -2918,5 +2921,50 @@ describe("createOrder quote auto-detection", () => { expect(result.order.id).toBeDefined(); expect(result.items_created.length).toBe(1); + // Verifies the inner catch swallowed the matcher failure and + // logged it — without this assertion the test passes purely + // because the `after` mock detaches the promise regardless of + // what production code does. + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("[createOrder] quote auto-detection failed for order"), + expect.stringContaining("PB down"), + ); + consoleErrorSpy.mockRestore(); + }); + + it("falls back to an inline match when after() is unavailable", async () => { + const consoleErrorSpy = vi + .spyOn(console, "error") + .mockImplementation(() => {}); + const { after } = await import("next/server"); + const { suggestQuoteForOrder } = await import("./order-quote-link-service"); + vi.mocked(suggestQuoteForOrder).mockClear(); + vi.mocked(after).mockClear(); + vi.mocked(after).mockImplementationOnce(() => { + throw new Error("`after` was called outside a request scope."); + }); + await pbMock.collection("customers").create({ id: "cust-x", name: "Acme" }); + await pbMock.collection("companies").create({ id: "comp-x", name: "Acme (Pty) Ltd" }); + + const r = await createOrder({ + customer_id: "cust-x", + company_id: "comp-x", + items: [{ description: "M16 Bolt", quantity: 50 }], + }); + + expect(r.order.id).toBeDefined(); + // Proves the catch fired AND ran the inline fallback — without + // the call assertion the test would pass on the spy alone even + // if `void runQuoteMatch()` was deleted. + expect(suggestQuoteForOrder).toHaveBeenCalledWith(r.order.id); + // The fallback logs its engagement so a silent switch to + // fire-and-forget is impossible to miss in production logs. + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining( + "[createOrder] after() unavailable, running quote match inline", + ), + expect.anything(), + ); + consoleErrorSpy.mockRestore(); }); }); diff --git a/src/lib/server/orders-service.ts b/src/lib/server/orders-service.ts index 955319d..4cb1ffa 100644 --- a/src/lib/server/orders-service.ts +++ b/src/lib/server/orders-service.ts @@ -2235,16 +2235,25 @@ export async function createOrder(input: CreateOrderInput): Promise