402 lines
15 KiB
TypeScript
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"]);
|
|
}
|
|
);
|
|
});
|