|
| 1 | +// A standalone wait token is minted with a cuid id and, having no owning run, is written to the |
| 2 | +// control-plane store. Under the split topology the run-ops read replica is a distinct database |
| 3 | +// that does not hold it, so the public token routes must fan out to the control-plane replica or |
| 4 | +// the token is reported missing. These reads run as real queries against two containers. |
| 5 | +import { heteroPostgresTest } from "@internal/testcontainers"; |
| 6 | +import { WaitpointId } from "@trigger.dev/core/v3/isomorphic"; |
| 7 | +import { describe, expect, vi } from "vitest"; |
| 8 | +import type { PrismaClient } from "@trigger.dev/database"; |
| 9 | +import type { PrismaReplicaClient } from "~/db.server"; |
| 10 | +import { resolveWaitpointThroughReadThrough } from "~/runEngine/concerns/resolveWaitpointThroughReadThrough.server"; |
| 11 | +import { readThroughRun } from "./readThrough.server"; |
| 12 | + |
| 13 | +vi.setConfig({ testTimeout: 60_000 }); |
| 14 | + |
| 15 | +async function seedControlPlaneEnv(prisma: PrismaClient, suffix: string) { |
| 16 | + const organization = await prisma.organization.create({ |
| 17 | + data: { title: `Org ${suffix}`, slug: `org-${suffix}` }, |
| 18 | + }); |
| 19 | + const project = await prisma.project.create({ |
| 20 | + data: { |
| 21 | + name: `Project ${suffix}`, |
| 22 | + slug: `project-${suffix}`, |
| 23 | + externalRef: `proj_${suffix}`, |
| 24 | + organizationId: organization.id, |
| 25 | + }, |
| 26 | + }); |
| 27 | + const environment = await prisma.runtimeEnvironment.create({ |
| 28 | + data: { |
| 29 | + type: "PRODUCTION", |
| 30 | + slug: `prod-${suffix}`, |
| 31 | + projectId: project.id, |
| 32 | + organizationId: organization.id, |
| 33 | + apiKey: `tr_prod_${suffix}`, |
| 34 | + pkApiKey: `pk_prod_${suffix}`, |
| 35 | + shortcode: `short_${suffix}`, |
| 36 | + maximumConcurrencyLimit: 10, |
| 37 | + }, |
| 38 | + }); |
| 39 | + return { organization, project, environment }; |
| 40 | +} |
| 41 | + |
| 42 | +async function seedStandaloneToken(prisma: PrismaClient, environmentId: string, projectId: string) { |
| 43 | + const { id, friendlyId } = WaitpointId.generate(); |
| 44 | + await prisma.waitpoint.create({ |
| 45 | + data: { |
| 46 | + id, |
| 47 | + friendlyId, |
| 48 | + type: "MANUAL", |
| 49 | + status: "PENDING", |
| 50 | + idempotencyKey: id, |
| 51 | + userProvidedIdempotencyKey: false, |
| 52 | + environmentId, |
| 53 | + projectId, |
| 54 | + }, |
| 55 | + }); |
| 56 | + return { id, friendlyId }; |
| 57 | +} |
| 58 | + |
| 59 | +describe("public wait-token resolution across the split boundary", () => { |
| 60 | + heteroPostgresTest( |
| 61 | + "a control-plane-resident standalone token is found when the run-ops replica does not hold it", |
| 62 | + async ({ prisma14, prisma17 }) => { |
| 63 | + const { project, environment } = await seedControlPlaneEnv(prisma14, "token_cp"); |
| 64 | + const { id: waitpointId } = await seedStandaloneToken(prisma14, environment.id, project.id); |
| 65 | + |
| 66 | + const waitpoint = await resolveWaitpointThroughReadThrough({ |
| 67 | + waitpointId, |
| 68 | + environmentId: environment.id, |
| 69 | + read: (client: PrismaReplicaClient) => |
| 70 | + client.waitpoint.findFirst({ |
| 71 | + where: { id: waitpointId, environmentId: environment.id }, |
| 72 | + }), |
| 73 | + deps: { |
| 74 | + newClient: prisma17 as unknown as PrismaReplicaClient, |
| 75 | + legacyReplica: prisma14 as unknown as PrismaReplicaClient, |
| 76 | + }, |
| 77 | + }); |
| 78 | + |
| 79 | + expect(waitpoint).not.toBeNull(); |
| 80 | + expect(waitpoint?.id).toBe(waitpointId); |
| 81 | + } |
| 82 | + ); |
| 83 | + |
| 84 | + heteroPostgresTest( |
| 85 | + "pinning both reads at the run-ops replica misses the control-plane token", |
| 86 | + async ({ prisma14, prisma17 }) => { |
| 87 | + const { project, environment } = await seedControlPlaneEnv(prisma14, "token_miss"); |
| 88 | + const { id: waitpointId } = await seedStandaloneToken(prisma14, environment.id, project.id); |
| 89 | + |
| 90 | + const waitpoint = await resolveWaitpointThroughReadThrough({ |
| 91 | + waitpointId, |
| 92 | + environmentId: environment.id, |
| 93 | + read: (client: PrismaReplicaClient) => |
| 94 | + client.waitpoint.findFirst({ |
| 95 | + where: { id: waitpointId, environmentId: environment.id }, |
| 96 | + }), |
| 97 | + deps: { |
| 98 | + newClient: prisma17 as unknown as PrismaReplicaClient, |
| 99 | + legacyReplica: prisma17 as unknown as PrismaReplicaClient, |
| 100 | + }, |
| 101 | + }); |
| 102 | + |
| 103 | + expect(waitpoint).toBeNull(); |
| 104 | + } |
| 105 | + ); |
| 106 | + |
| 107 | + heteroPostgresTest( |
| 108 | + "the read gate forces fan-out so a control-plane token resolves while the mint flag is off", |
| 109 | + async ({ prisma14, prisma17 }) => { |
| 110 | + const { project, environment } = await seedControlPlaneEnv(prisma14, "token_gate"); |
| 111 | + const { id: waitpointId } = await seedStandaloneToken(prisma14, environment.id, project.id); |
| 112 | + |
| 113 | + const read = (client: PrismaReplicaClient) => |
| 114 | + client.waitpoint.findFirst({ |
| 115 | + where: { id: waitpointId, environmentId: environment.id }, |
| 116 | + }); |
| 117 | + |
| 118 | + const gated = await resolveWaitpointThroughReadThrough({ |
| 119 | + waitpointId, |
| 120 | + environmentId: environment.id, |
| 121 | + read, |
| 122 | + deps: { |
| 123 | + splitEnabled: true, |
| 124 | + newClient: prisma17 as unknown as PrismaReplicaClient, |
| 125 | + legacyReplica: prisma14 as unknown as PrismaReplicaClient, |
| 126 | + }, |
| 127 | + }); |
| 128 | + |
| 129 | + expect(gated).not.toBeNull(); |
| 130 | + expect(gated?.id).toBe(waitpointId); |
| 131 | + |
| 132 | + const passthrough = await readThroughRun({ |
| 133 | + runId: waitpointId, |
| 134 | + environmentId: environment.id, |
| 135 | + readNew: (c) => read(c), |
| 136 | + readLegacy: (r) => read(r), |
| 137 | + deps: { |
| 138 | + splitEnabled: false, |
| 139 | + newClient: prisma17 as unknown as PrismaReplicaClient, |
| 140 | + legacyReplica: prisma14 as unknown as PrismaReplicaClient, |
| 141 | + }, |
| 142 | + }); |
| 143 | + |
| 144 | + expect(gated).not.toBeNull(); |
| 145 | + expect(passthrough.source).toBe("not-found"); |
| 146 | + } |
| 147 | + ); |
| 148 | +}); |
0 commit comments