import { describe, expect, it, vi } from "vitest"; // Mock the db module so the BaseService default prisma doesn't try to // open a real connection at module load. Each test wires its own // prisma stub. vi.mock("~/db.server", () => ({ prisma: {}, $replica: {} })); // Prevent the runEngine singleton from instantiating and spinning up // PG/Redis workers at module load — without this CI fails with // unhandled `PrismaClientInitializationError`s even though the // assertions all pass (see `mollifierDrainerWorker.test.ts`). vi.mock("~/v3/runEngine.server", () => ({ engine: {} })); // Hoisted mock state so we can swap the buffer per test without // re-importing modules. const bufferMock: { current: unknown } = { current: null }; vi.mock("~/v3/mollifier/mollifierBuffer.server", () => ({ getMollifierBuffer: () => bufferMock.current, })); import { ResetIdempotencyKeyService } from "~/v3/services/resetIdempotencyKey.server"; import { ServiceValidationError } from "~/v3/services/baseService.server"; type FakePrisma = { taskRun: { updateMany: (...args: unknown[]) => Promise<{ count: number }> }; }; function makePrisma(pgCount: number): FakePrisma { return { taskRun: { updateMany: vi.fn(async () => ({ count: pgCount })), }, }; } const env = { id: "env_a", organizationId: "org_1", } as unknown as Parameters[2]; describe("ResetIdempotencyKeyService — buffer-outage handling", () => { it("returns success when PG cleared >=1 run, even if the buffer reset throws", async () => { bufferMock.current = { resetIdempotency: vi.fn(async () => { throw new Error("ECONNREFUSED"); }), }; const prisma = makePrisma(1); const service = new ResetIdempotencyKeyService(prisma as never); const result = await service.call("ikey", "task", env); expect(result).toEqual({ id: "ikey" }); }); it("returns success when PG cleared nothing but the buffer cleared a run", async () => { bufferMock.current = { resetIdempotency: vi.fn(async () => ({ clearedRunId: "run_x" })), }; const prisma = makePrisma(0); const service = new ResetIdempotencyKeyService(prisma as never); const result = await service.call("ikey", "task", env); expect(result).toEqual({ id: "ikey" }); }); it("404s when PG and buffer both legitimately report 'nothing to clear'", async () => { bufferMock.current = { resetIdempotency: vi.fn(async () => ({ clearedRunId: null })), }; const prisma = makePrisma(0); const service = new ResetIdempotencyKeyService(prisma as never); await expect(service.call("ikey", "task", env)).rejects.toMatchObject({ status: 404, }); }); // Regression for the silent-not-found hazard CodeRabbit flagged: if PG // sees nothing AND we can't read the buffer (Redis outage), the // previous behaviour was to 404 — masking a partial outage and // leaving a buffered key effectively un-reset while the caller was // told "doesn't exist." We now surface 503 so the caller retries. it("503s when PG cleared nothing AND the buffer reset failed (partial outage)", async () => { bufferMock.current = { resetIdempotency: vi.fn(async () => { throw new Error("ECONNREFUSED"); }), }; const prisma = makePrisma(0); const service = new ResetIdempotencyKeyService(prisma as never); const error = await service.call("ikey", "task", env).then( () => null, (err) => err ); expect(error).toBeInstanceOf(ServiceValidationError); expect(error.status).toBe(503); expect(error.message).toMatch(/retry/i); }); it("404s normally when buffer is null (mollifier disabled) and PG cleared nothing", async () => { bufferMock.current = null; const prisma = makePrisma(0); const service = new ResetIdempotencyKeyService(prisma as never); await expect(service.call("ikey", "task", env)).rejects.toMatchObject({ status: 404, }); }); // Regression for the PG↔buffer handoff race CodeRabbit flagged on PR #3756. // // Sequence the test models (deterministic, by setup): // 1. ResetIdempotencyKeyService.call begins while the run is still // buffered. The initial pg.updateMany sees no PG row → count=0. // 2. Between that update and the buffer reset, the drainer materialises // the buffered run into PG (engine.trigger writes the row with the // original idempotencyKey intact) AND `buffer.ack` clears the // associated Redis idempotency lookup — that's part of ack's // atomic contract (see `buffer.ts:493` comment). // 3. buffer.resetIdempotency runs after ack → returns // `{ clearedRunId: null }` because the lookup is gone. // // Without the handoff re-check, totalCount = 0 + 0 = 0 → the service // throws 404 for a key that genuinely still exists on the now- // materialised PG row. The customer's reset is silently lost. // // Correct behaviour: the service must discover the materialised row // and clear its key, returning success. This test pins that contract. it("succeeds when a buffered run materialises into PG between the initial pgUpdate and the buffer reset (handoff race)", async () => { let updateManyCalls = 0; const prisma: FakePrisma = { taskRun: { // First call: pre-materialisation, no PG row yet → 0. // Second call (the fix's re-check after both surfaces report // nothing): post-materialisation, drainer wrote the row → 1. updateMany: vi.fn(async () => { updateManyCalls += 1; return updateManyCalls === 1 ? { count: 0 } : { count: 1 }; }), }, }; const resetIdempotency = vi.fn(async () => ({ clearedRunId: null as string | null })); bufferMock.current = { resetIdempotency }; const service = new ResetIdempotencyKeyService(prisma as never); const result = await service.call("ikey", "task", env); expect(result).toEqual({ id: "ikey" }); // Load-bearing pieces of the fix: // - The buffer path was consulted (we didn't bypass the normal // handoff window check), and // - A second pg.updateMany fired AFTER the buffer's null result, // catching the now-materialised row. expect(resetIdempotency).toHaveBeenCalledOnce(); expect(updateManyCalls).toBe(2); }); });