Files
triggerdotdev--trigger.dev/apps/webapp/test/waitpointPresenter.readthrough.test.ts
T
2026-07-13 13:32:57 +08:00

402 lines
15 KiB
TypeScript

import { describe, expect, vi } from "vitest";
// The presenter graph imports `~/services/clickhouse/clickhouseFactoryInstance.server` and, via
// NextRunListPresenter -> RunsRepository / runStore, reaches `~/db.server`'s `prisma`/`$replica`
// singletons (through findDisplayableEnvironment + getTaskIdentifiers). We stub those two wiring
// boundaries so the module loads + the connected-runs hydrate can be pointed at the per-test REAL
// containers. The DB is NEVER mocked: every assertion runs against real Postgres/ClickHouse.
//
// * `~/db.server` — `prisma`/`$replica`/`runOpsNewPrisma` singletons. The read-through cases pass
// explicit client handles to the presenter ctor so these proxies are never read on those paths.
// For the connected-runs hydrate the proxies forward lazily to the test's real legacy handle.
// The run-ops new/legacy clients forward to the new/legacy per-test holders respectively.
// * clickhouseFactory singleton — overridden to hand back the per-test ClickHouse so the
// connected-runs hydrate uses a real CH container (a wiring override, not a DB mock).
const legacyReplicaHolder = vi.hoisted(() => ({ client: undefined as any }));
const newClientHolder = vi.hoisted(() => ({ client: undefined as any }));
const clickhouseHolder = vi.hoisted(() => ({ client: undefined as any }));
vi.mock("~/db.server", async () => {
const { Prisma } = await import("@trigger.dev/database");
const lazyProxy = (holder: { client: any }, label: string) =>
new Proxy(
{},
{
get(_t, prop) {
if (!holder.client) {
throw new Error(`${label} not set for this test`);
}
return holder.client[prop];
},
}
);
const replicaProxy = lazyProxy(legacyReplicaHolder, "legacyReplicaHolder.client");
return {
prisma: replicaProxy,
$replica: replicaProxy,
runOpsNewPrisma: lazyProxy(newClientHolder, "newClientHolder.client"),
runOpsNewReplica: lazyProxy(newClientHolder, "newClientHolder.client"),
runOpsLegacyPrisma: replicaProxy,
runOpsLegacyReplica: replicaProxy,
sqlDatabaseSchema: Prisma.sql([`public`]),
};
});
vi.mock("~/services/clickhouse/clickhouseFactoryInstance.server", () => ({
clickhouseFactory: {
getClickhouseForOrganization: async () => {
if (!clickhouseHolder.client) {
throw new Error("clickhouseHolder.client not set for this test");
}
return clickhouseHolder.client;
},
},
}));
import {
createPostgresContainer,
heteroPostgresTest,
replicationContainerTest,
} from "@internal/testcontainers";
import type { PrismaClient, WaitpointType } from "@trigger.dev/database";
import { PrismaClient as PrismaClientCtor } from "@trigger.dev/database";
import { setTimeout } from "node:timers/promises";
import type { PrismaReplicaClient } from "~/db.server";
import { WaitpointPresenter } from "~/presenters/v3/WaitpointPresenter.server";
import { setupClickhouseReplication } from "./utils/replicationUtils";
vi.setConfig({ testTimeout: 90_000 });
// A read client whose waitpoint.findFirst is recorded; throws if used after being marked
// forbidden, so we can prove a store was NEVER read. Every other access forwards to the real
// client (so the inlined `environment` join + connectedRuns relation still resolve).
function recording(client: PrismaClient, opts: { forbidden?: boolean } = {}) {
const calls: unknown[] = [];
return {
handle: new Proxy(client, {
get(target, prop) {
if (prop === "waitpoint") {
return new Proxy((target as any).waitpoint, {
get(wpTarget, wpProp) {
if (wpProp === "findFirst") {
return (args: unknown) => {
calls.push(args);
if (opts.forbidden) {
throw new Error("this store must never be read");
}
return (wpTarget as any).findFirst(args);
};
}
return (wpTarget as any)[wpProp];
},
});
}
return (target as any)[prop];
},
}) as unknown as PrismaReplicaClient,
calls,
};
}
type SeedContext = {
organizationId: string;
projectId: string;
environmentId: string;
};
async function seedParents(prisma: PrismaClient, slug: string): Promise<SeedContext> {
const organization = await prisma.organization.create({
data: { title: `org-${slug}`, slug: `org-${slug}` },
});
const project = await prisma.project.create({
data: {
name: `proj-${slug}`,
slug: `proj-${slug}`,
organizationId: organization.id,
externalRef: `proj-${slug}`,
},
});
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
data: {
slug: `env-${slug}`,
type: "DEVELOPMENT",
projectId: project.id,
organizationId: organization.id,
apiKey: `tr_dev_${slug}`,
pkApiKey: `pk_dev_${slug}`,
shortcode: `sc-${slug}`,
},
});
return {
organizationId: organization.id,
projectId: project.id,
environmentId: runtimeEnvironment.id,
};
}
/** Mirrors the org/project/env parents onto a second DB with the SAME ids (FKs need them). */
async function mirrorParents(prisma: PrismaClient, ctx: SeedContext, slug: string): Promise<void> {
await prisma.organization.create({
data: { id: ctx.organizationId, title: `org-${slug}`, slug: `org-${slug}` },
});
await prisma.project.create({
data: {
id: ctx.projectId,
name: `proj-${slug}`,
slug: `proj-${slug}`,
organizationId: ctx.organizationId,
externalRef: `proj-${slug}`,
},
});
await prisma.runtimeEnvironment.create({
data: {
id: ctx.environmentId,
slug: `env-${slug}`,
type: "DEVELOPMENT",
projectId: ctx.projectId,
organizationId: ctx.organizationId,
apiKey: `tr_dev_${slug}_b`,
pkApiKey: `pk_dev_${slug}_b`,
shortcode: `sc-${slug}-b`,
},
});
}
async function seedWaitpoint(
prisma: PrismaClient,
ctx: SeedContext,
friendlyId: string,
overrides: Partial<{
type: WaitpointType;
tags: string[];
output: string;
connectedRunFriendlyIds: string[];
}> = {}
) {
return prisma.waitpoint.create({
data: {
friendlyId,
type: overrides.type ?? "MANUAL",
status: "COMPLETED",
idempotencyKey: `idem-${friendlyId}`,
userProvidedIdempotencyKey: false,
output: overrides.output ?? JSON.stringify({ hello: "world" }),
outputType: "application/json",
outputIsError: false,
completedAt: new Date(),
tags: overrides.tags ?? ["a", "b"],
projectId: ctx.projectId,
environmentId: ctx.environmentId,
...(overrides.connectedRunFriendlyIds
? {
connectedRuns: {
connect: overrides.connectedRunFriendlyIds.map((friendlyId) => ({ friendlyId })),
},
}
: {}),
},
});
}
async function createRun(
prisma: PrismaClient,
ctx: SeedContext,
run: { friendlyId: string; taskIdentifier?: string }
) {
return prisma.taskRun.create({
data: {
friendlyId: run.friendlyId,
taskIdentifier: run.taskIdentifier ?? "my-task",
status: "PENDING",
payload: JSON.stringify({ foo: run.friendlyId }),
traceId: run.friendlyId,
spanId: run.friendlyId,
queue: "test",
runtimeEnvironmentId: ctx.environmentId,
projectId: ctx.projectId,
organizationId: ctx.organizationId,
environmentType: "DEVELOPMENT",
engine: "V2",
},
});
}
const callArgs = (ctx: SeedContext, friendlyId: string) => ({
friendlyId,
environmentId: ctx.environmentId,
projectId: ctx.projectId,
});
describe("WaitpointPresenter dual-DB read-through (hetero PG14 + PG17, no connected runs)", () => {
// new-DB short-circuit. Waitpoint on NEW (PG17), legacy
// wrapped so its waitpoint.findFirst throws if invoked. The lookup answers from NEW and must
// NEVER fall through to legacy.
heteroPostgresTest(
"waitpoint on the new DB resolves without touching the legacy replica",
async ({ prisma14, prisma17 }) => {
const ctx = await seedParents(prisma17, "newonly");
const seeded = await seedWaitpoint(prisma17, ctx, "waitpoint_newonly");
// The env lives on the new DB here; the resolver singleton reads it through the
// `~/db.server` proxy.
legacyReplicaHolder.client = prisma17;
const newClient = recording(prisma17);
const legacy = recording(prisma14, { forbidden: true });
const presenter = new WaitpointPresenter(undefined, undefined, {
splitEnabled: true,
newClient: newClient.handle,
legacyReplica: legacy.handle,
});
const result = await presenter.call(callArgs(ctx, seeded.friendlyId));
expect(result?.id).toBe(seeded.friendlyId);
// New-first short-circuit: legacy never probed (the throwing handle proves it).
expect(newClient.calls.length).toBe(1);
expect(legacy.calls.length).toBe(0);
}
);
// single-DB passthrough. No read-through deps -> exactly one plain
// findFirst against the single `_replica` handle; the split branch is structurally never entered
// (no second handle is injected). The connected-runs hydrate forwards `undefined` deps.
heteroPostgresTest(
"no read-through deps -> one plain findFirst on the single replica (passthrough)",
async ({ prisma14, prisma17 }) => {
const ctx = await seedParents(prisma14, "passthrough");
const seeded = await seedWaitpoint(prisma14, ctx, "waitpoint_passthrough", {
tags: ["one"],
});
const single = recording(prisma14);
const second = recording(prisma17, { forbidden: true });
legacyReplicaHolder.client = single.handle;
newClientHolder.client = second.handle;
// No readThroughDeps -> ctor defaults _replica to the (mocked) `$replica` singleton, which
// forwards to `single.handle`. The split branch needs an injected second handle to fire, so
// it cannot: passthrough is structural.
const presenter = new WaitpointPresenter();
const result = await presenter.call(callArgs(ctx, seeded.friendlyId));
expect(result?.id).toBe(seeded.friendlyId);
expect(result?.tags).toEqual(["one"]);
// Exactly one read on the single client; the second handle is never touched.
expect(single.calls.length).toBe(1);
expect(second.calls.length).toBe(0);
}
);
});
describe("WaitpointPresenter connected-runs hydrate routed through read-through (PG14 + PG17 + CH)", () => {
// Waitpoint detail + connected runs resolve on run-ops NEW. The
// waitpoint + its 2 connected runs live on the new (PG17) DB; CH gets the run id-set so the
// threaded NextRunListPresenter hydrate returns them. Proves the read-through deps are forwarded
// so the connected-runs hydrate flows through the routed store.
replicationContainerTest(
"waitpoint + 2 connected runs resolve on the new DB via the routed hydrate",
async ({ clickhouseContainer, redisOptions, postgresContainer, prisma, network }) => {
// `prisma`/`postgresContainer` is the PG14 legacy + CH replication source. The new DB (PG17)
// is created alongside; we seed the waitpoint + runs on it so CH replicates from PG14 — so we
// mirror the runs onto PG14 (replication source) and the waitpoint+runs onto PG17 (authoritative).
const { clickhouse } = await setupClickhouseReplication({
prisma,
databaseUrl: postgresContainer.getConnectionUri(),
clickhouseUrl: clickhouseContainer.getConnectionUrl(),
redisOptions,
});
const { url: newUrl } = await createPostgresContainer(network, {
imageTag: "docker.io/postgres:17",
});
const prismaNew = new PrismaClientCtor({ datasources: { db: { url: newUrl } } });
legacyReplicaHolder.client = prisma;
newClientHolder.client = prismaNew;
clickhouseHolder.client = clickhouse;
try {
const ctx = await seedParents(prisma, "connectednew");
await mirrorParents(prismaNew, ctx, "connectednew");
// The connected runs land on PG14 (CH replication source) AND on PG17 (authoritative,
// same ids/friendlyIds) so the routed hydrate resolves them from NEW.
const runA = await createRun(prisma, ctx, { friendlyId: "run_connA" });
const runB = await createRun(prisma, ctx, { friendlyId: "run_connB" });
const newRunA = await createRun(prismaNew, ctx, {
friendlyId: "run_connA",
taskIdentifier: "my-task-NEW",
});
const newRunB = await createRun(prismaNew, ctx, {
friendlyId: "run_connB",
taskIdentifier: "my-task-NEW",
});
await prismaNew.taskRun.update({
where: { friendlyId: "run_connA" },
data: { id: runA.id },
});
await prismaNew.taskRun.update({
where: { friendlyId: "run_connB" },
data: { id: runB.id },
});
// Waitpoint authoritative on NEW, connected to the 2 runs.
const seeded = await seedWaitpoint(prismaNew, ctx, "waitpoint_connectednew", {
connectedRunFriendlyIds: ["run_connA", "run_connB"],
});
// Wait for CH replication so the connected-run id-set page is non-empty.
await setTimeout(1500);
const presenter = new WaitpointPresenter(prisma, prisma, {
splitEnabled: true,
newClient: prismaNew,
legacyReplica: prisma,
});
const result = await presenter.call(callArgs(ctx, seeded.friendlyId));
expect(result?.id).toBe(seeded.friendlyId);
expect(result?.connectedRuns.map((r) => r.friendlyId).sort()).toEqual([
"run_connA",
"run_connB",
]);
// The connected runs carry the PG17-only taskIdentifier -> they hydrated from the threaded
// newClient (PG17), proving the routed store is armed.
expect(result?.connectedRuns.every((r) => r.taskIdentifier === "my-task-NEW")).toBe(true);
void newRunA;
void newRunB;
} finally {
await prismaNew.$disconnect();
}
}
);
});
describe("WaitpointPresenter bare-ctor production default activates readThroughRun", () => {
heteroPostgresTest(
"run-ops waitpoint on the new DB resolves via readThroughRun production defaults",
async ({ prisma14, prisma17 }) => {
const ctx = await seedParents(prisma17, "proddefault");
const seeded = await seedWaitpoint(prisma17, ctx, "waitpoint_proddefault", {
tags: ["p", "q"],
});
// runOpsNewReplica default -> PG17; env resolver reads via legacyReplicaHolder.
newClientHolder.client = prisma17;
legacyReplicaHolder.client = prisma17;
// No newClient/legacyReplica injected — production ctor shape.
const presenter = new WaitpointPresenter(undefined, undefined, { splitEnabled: true });
const result = await presenter.call(callArgs(ctx, seeded.friendlyId));
expect(result?.id).toBe(seeded.friendlyId);
expect(result?.tags).toEqual(["p", "q"]);
}
);
});