import { describe, expect, it, vi } from "vitest"; // Stub `~/db.server` before importing the concern — the real module // eagerly calls `prisma.$connect()` at singleton construction, which // would fail without a database. The concern under test receives its // prisma via the constructor, so these empty stubs are never used by the // tested path; the run-ops singletons only satisfy the concern's static // imports (vitest validates every named import against the mock). vi.mock("~/db.server", () => ({ prisma: {}, $replica: {}, runOpsNewPrisma: {}, runOpsLegacyPrisma: {}, runOpsNewReplica: {}, runOpsLegacyReplica: {}, })); // The IdempotencyKeyConcern resolves the pre-gate claim through the // global mollifier buffer (`getMollifierBuffer`), shared by both // `claimOrAwait` and `findBufferedRunWithIdempotency`. Control it via a // hoisted handle so each test can script the claim/lookup responses. const h = vi.hoisted(() => ({ buffer: null as unknown, orgFlag: true })); vi.mock("~/v3/mollifier/mollifierBuffer.server", () => ({ getMollifierBuffer: () => h.buffer, })); // Stub `mollifierGate.server` so loading the concern doesn't drag in // `env.server` (which fails to parse without a populated environment in // CI). The concern only uses `makeResolveMollifierFlag` to gate the // claim; tests flip `h.orgFlag` to cover both opted-in and opted-out // orgs without touching real env or feature-flag wiring. vi.mock("~/v3/mollifier/mollifierGate.server", () => ({ makeResolveMollifierFlag: () => async () => h.orgFlag, })); // Pin the idempotency dedup routing to the injected fake prisma: split OFF // makes resolveIdempotencyDedupClient return the concern's constructor client, // so these tests exercise claim resolution deterministically regardless of the // ambient RUN_OPS_SPLIT_ENABLED (the split path routes to the empty runOps mocks). vi.mock("~/v3/runOpsMigration/splitMode.server", () => ({ isSplitEnabled: async () => false, })); import type { MollifierBuffer } from "@trigger.dev/redis-worker"; import { IdempotencyKeyConcern } from "~/runEngine/concerns/idempotencyKeys.server"; import type { TriggerTaskRequest } from "~/runEngine/types"; function makeConcern(prisma: { findFirst: () => Promise }) { return new IdempotencyKeyConcern( { taskRun: { findFirst: prisma.findFirst } } as never, {} as never, // engine — unused on this path {} as never // traceEventConcern — unused on this path ); } function makeRequest(): TriggerTaskRequest { return { taskId: "my-task", environment: { id: "env_a", organizationId: "org_1", // The pre-gate claim is gated by the per-org mollifier flag // (mirroring evaluateGate's gating) so non-opted-in orgs don't pay // the Redis SETNX. Tests covering the claim path must opt this // fake org in, otherwise the concern skips claimOrAwait entirely // and the resolution branches under test never run. organization: { featureFlags: { mollifierEnabled: true } }, }, options: {}, body: { options: { idempotencyKey: "k-1" } }, } as unknown as TriggerTaskRequest; } describe("IdempotencyKeyConcern · claim resolution", () => { it("resolved-but-unfindable falls through to a fresh trigger (no cached run, no claim held)", async () => { // The claim slot holds a runId that is gone from both stores: the PG // findFirst misses and the buffer lookup misses. Regression guard for // the resolved-but-unfindable terminal case — the concern must fall // through to a fresh trigger rather than throw, hand back a bogus // cached run, or claim ownership it doesn't hold. const lookupIdempotency = vi.fn(async () => null); h.buffer = { claimIdempotency: vi.fn(async () => ({ kind: "resolved", runId: "run_gone" })), lookupIdempotency, } as unknown as MollifierBuffer; const findFirst = vi.fn(async () => null); // PG misses on every call const concern = makeConcern({ findFirst }); const result = await concern.handleTriggerRequest(makeRequest(), undefined); expect(result.isCached).toBe(false); if (result.isCached === false) { // No claim held — we resolved someone else's (stale) claim, we did // not win one. The caller must NOT publish/release on our behalf. expect(result.claim).toBeUndefined(); expect(result.idempotencyKey).toBe("k-1"); } // We attempted the buffer fallback before giving up. expect(lookupIdempotency).toHaveBeenCalled(); }); it("resolved-and-findable returns the existing run as a cached hit", async () => { // Guard the happy resolved path: when the claimed runId IS findable // (writer-side PG), the fall-through change must not swallow it. h.buffer = { claimIdempotency: vi.fn(async () => ({ kind: "resolved", runId: "run_winner" })), lookupIdempotency: vi.fn(async () => null), } as unknown as MollifierBuffer; const winner = { id: "run_winner", friendlyId: "run_winner" }; // First findFirst (initial existingRun check) misses so we enter the // claim path; the second (writer-side re-resolve) finds the winner. let calls = 0; const findFirst = vi.fn(async () => { calls += 1; return calls >= 2 ? winner : null; }); const concern = makeConcern({ findFirst }); const result = await concern.handleTriggerRequest(makeRequest(), undefined); expect(result.isCached).toBe(true); if (result.isCached === true) { expect(result.run).toBe(winner); } }); it("non-opted-in org skips claimOrAwait entirely (no buffer round-trip, no claim held)", async () => { // Regression guard for the per-org gating that keeps the claim's // Redis SETNX off the hot path for orgs that haven't opted into the // mollifier — even when `TRIGGER_MOLLIFIER_ENABLED=1` globally and // the buffer singleton exists. The concern should NOT touch // `claimIdempotency` for these orgs; PG's unique constraint already // deduplicates same-key races on the pass-through path. h.orgFlag = false; const claimIdempotency = vi.fn(async () => ({ kind: "claimed" as const })); const lookupIdempotency = vi.fn(async () => null); h.buffer = { claimIdempotency, lookupIdempotency, } as unknown as MollifierBuffer; const findFirst = vi.fn(async () => null); const concern = makeConcern({ findFirst }); try { const result = await concern.handleTriggerRequest(makeRequest(), undefined); expect(result.isCached).toBe(false); if (result.isCached === false) { // No claim returned — the caller must NOT publish/release. expect(result.claim).toBeUndefined(); expect(result.idempotencyKey).toBe("k-1"); } // The headline guarantee: zero Redis claim activity for this org. expect(claimIdempotency).not.toHaveBeenCalled(); } finally { h.orgFlag = true; // restore for any later tests in this file } }); });