// Real heterogeneous legacy + new Postgres proof for the public waitpoint retrieve read. // The DB is never mocked: reads hit the two real containers. Only pure boundaries // (splitEnabled, isPastRetention) and recording client wrappers are // injected. heteroPostgresTest runs the legacy and new databases on different major versions. import { heteroPostgresTest, heteroRunOpsPostgresTest, postgresTest, } from "@internal/testcontainers"; import type { RunOpsPrismaClient } from "@internal/run-ops-database"; import type { PrismaClient, WaitpointType } from "@trigger.dev/database"; import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; import type { PrismaReplicaClient } from "~/db.server"; import { ApiWaitpointPresenter } from "~/presenters/v3/ApiWaitpointPresenter.server"; vi.setConfig({ testTimeout: 60_000 }); // 25-char cuid body (no v1 version marker) → LEGACY residency. function generateLegacyCuid() { const suffix = Array.from( { length: 24 }, () => "0123456789abcdefghijklmnopqrstuvwxyz"[Math.floor(Math.random() * 36)] ).join(""); return `c${suffix}`; } // A read client whose waitpoint.findFirst is recorded; throws if used after being marked // forbidden, so we can prove a store was NEVER read. function recording(client: PrismaClient | RunOpsPrismaClient, opts: { forbidden?: boolean } = {}) { const calls: unknown[] = []; const waitpoint = { findFirst: (args: unknown) => { calls.push(args); if (opts.forbidden) { throw new Error("this store must never be read"); } return (client as unknown as PrismaReplicaClient).waitpoint.findFirst(args as never); }, }; return { handle: { ...client, waitpoint } as unknown as PrismaReplicaClient, calls }; } async function seedOrgProjectEnv(prisma: PrismaClient, suffix: string) { const organization = await prisma.organization.create({ data: { title: `test-${suffix}`, slug: `test-${suffix}` }, }); const project = await prisma.project.create({ data: { name: `test-${suffix}`, slug: `test-${suffix}`, organizationId: organization.id, externalRef: `test-${suffix}`, }, }); const environment = await prisma.runtimeEnvironment.create({ data: { slug: `test-${suffix}`, type: "PRODUCTION", projectId: project.id, organizationId: organization.id, apiKey: `apikey-${suffix}`, pkApiKey: `pk-${suffix}`, shortcode: `test-${suffix}`, }, }); return { organization, project, environment }; } async function seedWaitpoint( prisma: PrismaClient, id: string, env: { id: string; projectId: string }, overrides: Partial<{ status: "PENDING" | "COMPLETED"; type: WaitpointType; output: string; outputType: string; outputIsError: boolean; completedAt: Date; completedAfter: Date; tags: string[]; }> = {} ) { return prisma.waitpoint.create({ data: { id, friendlyId: `waitpoint_${id}`, type: overrides.type ?? "MANUAL", status: overrides.status ?? "COMPLETED", idempotencyKey: `idem-${id}`, userProvidedIdempotencyKey: false, output: overrides.output ?? JSON.stringify({ hello: "world" }), outputType: overrides.outputType ?? "application/json", outputIsError: overrides.outputIsError ?? false, completedAt: overrides.completedAt ?? new Date(), completedAfter: overrides.completedAfter, tags: overrides.tags ?? ["a", "b"], projectId: env.projectId, environmentId: env.id, }, }); } const environmentArg = (env: { id: string; projectId: string }) => ({ id: env.id, type: "PRODUCTION" as const, project: { id: env.projectId, engine: "V2" as const }, apiKey: "tr_test_apikey", }); describe("ApiWaitpointPresenter read-through (heterogeneous legacy + new Postgres)", () => { heteroPostgresTest( "resolves on run-ops NEW (run-ops id), legacy replica never touched", async ({ prisma17, prisma14 }) => { const id = generateRunOpsId(); expect(id.length).toBe(26); const { project, environment } = await seedOrgProjectEnv(prisma17, "new"); const seeded = await seedWaitpoint( prisma17, id, { id: environment.id, projectId: project.id }, { tags: ["x", "y", "z"], output: JSON.stringify({ n: 42 }) } ); const newClient = recording(prisma17); const legacy = recording(prisma14, { forbidden: true }); const presenter = new ApiWaitpointPresenter(undefined, undefined, { splitEnabled: true, newClient: newClient.handle, legacyReplica: legacy.handle, }); const result = await presenter.call(environmentArg(environment), id); expect(result.id).toBe(seeded.friendlyId); expect(result.tags).toEqual(["x", "y", "z"]); expect(result.output).toBe(JSON.stringify({ n: 42 })); expect(result.type).toBe("MANUAL"); // run-ops id → NEW: new store served the read, legacy never touched (fast-path). expect(newClient.calls.length).toBe(1); expect(legacy.calls.length).toBe(0); } ); heteroPostgresTest( "resolves off the LEGACY replica (cuid), never a legacy primary", async ({ prisma17, prisma14 }) => { const id = generateLegacyCuid(); expect(id.length).toBe(25); const { project, environment } = await seedOrgProjectEnv(prisma14, "legacy"); const seeded = await seedWaitpoint(prisma14, id, { id: environment.id, projectId: project.id, }); const newClient = recording(prisma17); // The deps expose only legacyReplica — there is NO legacy-primary handle at all. const legacy = recording(prisma14); const presenter = new ApiWaitpointPresenter(undefined, undefined, { splitEnabled: true, newClient: newClient.handle, legacyReplica: legacy.handle, }); const result = await presenter.call(environmentArg(environment), id); expect(result.id).toBe(seeded.friendlyId); expect(result.tags).toEqual(["a", "b"]); // NEW probed first (miss) → resolved off the LEGACY REPLICA handle. expect(newClient.calls.length).toBe(1); expect(legacy.calls.length).toBe(1); } ); heteroPostgresTest( "not-found maps to the existing ServiceValidationError surface", async ({ prisma17, prisma14 }) => { const id = generateLegacyCuid(); const { environment } = await seedOrgProjectEnv(prisma14, "nf"); const presenter = new ApiWaitpointPresenter(undefined, undefined, { splitEnabled: true, newClient: recording(prisma17).handle, legacyReplica: recording(prisma14).handle, }); await expect(presenter.call(environmentArg(environment), id)).rejects.toThrow( "Waitpoint not found" ); } ); heteroPostgresTest( "past-retention maps to the same not-found surface", async ({ prisma17, prisma14 }) => { const id = generateLegacyCuid(); const { environment } = await seedOrgProjectEnv(prisma14, "pr"); const presenter = new ApiWaitpointPresenter(undefined, undefined, { splitEnabled: true, newClient: recording(prisma17).handle, legacyReplica: recording(prisma14).handle, isPastRetention: () => true, }); await expect(presenter.call(environmentArg(environment), id)).rejects.toThrow( "Waitpoint not found" ); } ); heteroPostgresTest( "cross-seam — new-resident served from NEW (legacy untouched); in-retention served from legacy", async ({ prisma17, prisma14 }) => { // New-resident waitpoint: lives on NEW, the new probe hits, legacy must never be touched. const newId = generateRunOpsId(); const newEnv = await seedOrgProjectEnv(prisma17, "x2new"); await seedWaitpoint(prisma17, newId, { id: newEnv.environment.id, projectId: newEnv.project.id, }); const newLegacy = recording(prisma14, { forbidden: true }); const migratedPresenter = new ApiWaitpointPresenter(undefined, undefined, { splitEnabled: true, newClient: recording(prisma17).handle, legacyReplica: newLegacy.handle, }); const migratedResult = await migratedPresenter.call( environmentArg(newEnv.environment), newId ); expect(migratedResult.id).toBe(`waitpoint_${newId}`); expect(newLegacy.calls.length).toBe(0); // In-retention waitpoint: lives on the legacy replica, served from it. const oldId = generateLegacyCuid(); const oldEnv = await seedOrgProjectEnv(prisma14, "x2old"); await seedWaitpoint(prisma14, oldId, { id: oldEnv.environment.id, projectId: oldEnv.project.id, }); const retentionPresenter = new ApiWaitpointPresenter(undefined, undefined, { splitEnabled: true, newClient: recording(prisma17).handle, legacyReplica: recording(prisma14).handle, }); const retentionResult = await retentionPresenter.call( environmentArg(oldEnv.environment), oldId ); expect(retentionResult.id).toBe(`waitpoint_${oldId}`); } ); }); // Regression: the split-mode NEW client is the REAL scalar-only run-ops client (prisma17). A cuid // classifies LEGACY, so readThroughRun probes NEW first — a relation in hydrate() (connectedRuns) // throws PrismaClientValidationError there (the 500) before the legacy fallback runs. describe("ApiWaitpointPresenter read-through (dedicated scalar-only run-ops NEW client)", () => { heteroRunOpsPostgresTest( "cuid token: hydrate() select is valid against the scalar-only run-ops client, resolves via legacy", async ({ prisma17, prisma14 }) => { const id = generateLegacyCuid(); expect(id.length).toBe(25); const { project, environment } = await seedOrgProjectEnv(prisma14, "scalar-legacy"); const seeded = await seedWaitpoint( prisma14, id, { id: environment.id, projectId: project.id }, { tags: ["p", "q"], output: JSON.stringify({ ok: true }) } ); const newClient = recording(prisma17); const legacy = recording(prisma14); const presenter = new ApiWaitpointPresenter(undefined, undefined, { splitEnabled: true, newClient: newClient.handle, legacyReplica: legacy.handle, }); // Must NOT throw PrismaClientValidationError; resolves the token off the legacy side. const result = await presenter.call(environmentArg(environment), id); expect(result.id).toBe(seeded.friendlyId); expect(result.tags).toEqual(["p", "q"]); expect(result.output).toBe(JSON.stringify({ ok: true })); expect(newClient.calls.length).toBe(1); expect(legacy.calls.length).toBe(1); } ); }); describe("ApiWaitpointPresenter passthrough (single-DB)", () => { postgresTest( "no read-through deps → one plain replica read; legacy never touched", async ({ prisma }) => { const id = generateRunOpsId(); const { project, environment } = await seedOrgProjectEnv(prisma, "pt"); const seeded = await seedWaitpoint( prisma, id, { id: environment.id, projectId: project.id }, { tags: ["one"], output: JSON.stringify({ ok: true }) } ); const single = recording(prisma); const legacy = recording(prisma, { forbidden: true }); // No splitEnabled → passthrough. newClient defaults to the single recording handle so we // can assert exactly one read against it; legacy must never fire. const presenter = new ApiWaitpointPresenter(undefined, undefined, { newClient: single.handle, legacyReplica: legacy.handle, }); const result = await presenter.call(environmentArg(environment), id); expect(result.id).toBe(seeded.friendlyId); expect(result.tags).toEqual(["one"]); expect(result.output).toBe(JSON.stringify({ ok: true })); // Passthrough: exactly one read on the single client; legacy untouched. expect(single.calls.length).toBe(1); expect(legacy.calls.length).toBe(0); } ); });