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

334 lines
12 KiB
TypeScript

// 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);
}
);
});