Files
2026-07-13 13:32:57 +08:00

361 lines
12 KiB
TypeScript

import { containerTest, heteroPostgresTest } from "@internal/testcontainers";
import { type PrismaClient } from "@trigger.dev/database";
import { describe, expect, vi } from "vitest";
import {
displayableEnvironment,
type DisplayableInputEnvironment,
} from "~/models/runtimeEnvironment.server";
import { BatchPresenter } from "~/presenters/v3/BatchPresenter.server";
import { readThroughRun } from "~/v3/runOpsMigration/readThrough.server";
vi.setConfig({ testTimeout: 90_000 });
type SeedContext = {
organizationId: string;
projectId: string;
environmentId: string;
};
/**
* Seeds the control-plane org/project/env on one DB. The env is control-plane; it is
* resolved separately from the run-ops batch row (the cross-seam FK is physically dropped),
* so this always lives on the same DB that the injected resolveDisplayableEnvironment reads.
*/
async function seedEnvironment(
prisma: PrismaClient,
slug: string,
type: "DEVELOPMENT" | "PRODUCTION" = "PRODUCTION"
): 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}`,
},
});
let orgMemberId: string | undefined;
if (type === "DEVELOPMENT") {
const user = await prisma.user.create({
data: {
email: `user-${slug}@example.com`,
name: `User ${slug}`,
displayName: `Display ${slug}`,
authenticationMethod: "MAGIC_LINK",
},
});
const member = await prisma.orgMember.create({
data: { organizationId: organization.id, userId: user.id, role: "ADMIN" },
});
orgMemberId = member.id;
}
const environment = await prisma.runtimeEnvironment.create({
data: {
slug: `env-${slug}`,
type,
projectId: project.id,
organizationId: organization.id,
apiKey: `tr_${slug}`,
pkApiKey: `pk_${slug}`,
shortcode: `sc-${slug}`,
orgMemberId,
},
});
return {
organizationId: organization.id,
projectId: project.id,
environmentId: environment.id,
};
}
async function seedBatch(
prisma: PrismaClient,
environmentId: string,
opts: {
friendlyId: string;
status?: any;
batchVersion?: string;
runCount?: number;
withError?: boolean;
}
) {
return prisma.batchTaskRun.create({
data: {
friendlyId: opts.friendlyId,
runtimeEnvironmentId: environmentId,
status: opts.status ?? "COMPLETED",
batchVersion: opts.batchVersion ?? "v1",
runCount: opts.runCount ?? 0,
successfulRunCount: 3,
failedRunCount: 1,
idempotencyKey: `idem-${opts.friendlyId}`,
errors: opts.withError
? {
create: [
{
index: 0,
taskIdentifier: "my-task",
error: JSON.stringify({ message: "boom", stack: "x\ny" }),
errorCode: "TASK_RUN_FAILED",
},
],
}
: undefined,
},
});
}
/**
* Builds a resolveDisplayableEnvironment closure over a real control-plane container — exactly
* mirroring the production findDisplayableEnvironment (reads control-plane, returns the
* displayableEnvironment shape). This is the only injected boundary; the DB is never mocked.
*/
function makeEnvResolver(controlPlane: PrismaClient) {
return async (environmentId: string, userId: string | undefined) => {
const environment = await controlPlane.runtimeEnvironment.findFirst({
where: { id: environmentId },
select: {
id: true,
type: true,
slug: true,
orgMember: {
select: { user: { select: { id: true, name: true, displayName: true } } },
},
},
});
if (!environment) {
return undefined;
}
return displayableEnvironment(environment as DisplayableInputEnvironment, userId);
};
}
describe("BatchPresenter read-through (PG14 legacy + PG17 new)", () => {
// Batch detail resolves on run-ops NEW (split on). Legacy replica is never probed.
heteroPostgresTest(
"resolves a NEW-resident batch and never probes the legacy replica",
async ({ prisma14, prisma17 }) => {
const ctx = await seedEnvironment(prisma17, "new1");
await seedBatch(prisma17, ctx.environmentId, {
friendlyId: "batch_new1",
withError: true,
runCount: 4,
});
// Real readThroughRun. A tripwire legacy client throws if batchTaskRun is ever accessed,
// proving a NEW-resident row is served without probing the legacy replica.
const tripwireLegacy = new Proxy(prisma14, {
get(target, prop) {
if (prop === "batchTaskRun") {
throw new Error("legacy replica must not be probed for a NEW-resident batch");
}
return (target as any)[prop];
},
}) as unknown as PrismaClient;
const presenter = new BatchPresenter(undefined, undefined, {
splitEnabled: true,
newClient: prisma17,
legacyReplica: tripwireLegacy,
readThrough: readThroughRun,
resolveDisplayableEnvironment: makeEnvResolver(prisma17),
});
const result = await presenter.call({
environmentId: ctx.environmentId,
batchId: "batch_new1",
});
expect(result.friendlyId).toBe("batch_new1");
expect(result.runCount).toBe(4);
expect(result.idempotencyKey).toBe("idem-batch_new1");
expect(result.errors).toHaveLength(1);
expect(result.errors[0].errorCode).toBe("TASK_RUN_FAILED");
expect(result.environment.id).toBe(ctx.environmentId);
expect(result.environment.type).toBe("PRODUCTION");
}
);
// Batch detail resolves on run-ops OLD/legacy READ REPLICA (split on, in-retention).
// Cross-version round-trip: PG14 legacy -> presenter, JSON error payload byte-identical.
heteroPostgresTest(
"resolves a legacy-only batch via the legacy READ REPLICA, byte-identical",
async ({ prisma14, prisma17 }) => {
// Env is control-plane (lives wherever the resolver reads); batch is run-ops, legacy-only.
const ctx = await seedEnvironment(prisma14, "legacy1");
const errorPayload = JSON.stringify({ message: "legacy boom", stack: "a\nb\nc" });
await prisma14.batchTaskRun.create({
data: {
friendlyId: "batch_legacy1",
runtimeEnvironmentId: ctx.environmentId,
status: "COMPLETED",
batchVersion: "v1",
runCount: 2,
successfulRunCount: 1,
failedRunCount: 1,
idempotencyKey: "idem-batch_legacy1",
errors: {
create: [
{
index: 0,
taskIdentifier: "legacy-task",
error: errorPayload,
errorCode: "LEGACY_CODE",
},
],
},
},
});
// The structural guarantee: there is no legacy-PRIMARY handle in readThroughRun; the only
// legacy handle threaded here is the read replica (prisma14).
const presenter = new BatchPresenter(undefined, undefined, {
splitEnabled: true,
newClient: prisma17, // NEW probe misses (nothing seeded there)
legacyReplica: prisma14,
// Real readThroughRun; the NEW miss falls through to the legacy replica.
readThrough: readThroughRun,
resolveDisplayableEnvironment: makeEnvResolver(prisma14),
});
const result = await presenter.call({
environmentId: ctx.environmentId,
batchId: "batch_legacy1",
});
expect(result.friendlyId).toBe("batch_legacy1");
expect(result.runCount).toBe(2);
expect(result.errors).toHaveLength(1);
// JSON error payload round-trips byte-identically across PG14 -> presenter.
expect(result.errors[0].error).toBe(errorPayload);
expect(result.errors[0].taskIdentifier).toBe("legacy-task");
expect(result.environment.id).toBe(ctx.environmentId);
}
);
// Post-termination / not-found yields the normal "Batch not found".
heteroPostgresTest(
"throws the normal not-found when the batch is absent from both stores",
async ({ prisma14, prisma17 }) => {
const ctx = await seedEnvironment(prisma14, "missing1");
const presenter = new BatchPresenter(undefined, undefined, {
splitEnabled: true,
newClient: prisma17,
legacyReplica: prisma14,
readThrough: readThroughRun,
resolveDisplayableEnvironment: makeEnvResolver(prisma14),
});
await expect(
presenter.call({ environmentId: ctx.environmentId, batchId: "batch_does_not_exist" })
).rejects.toThrow("Batch not found");
}
);
// Env decoupling parity for a DEVELOPMENT env (userName branch).
heteroPostgresTest(
"resolves the DEVELOPMENT env userName separately from the run-ops batch row",
async ({ prisma14, prisma17 }) => {
const ctx = await seedEnvironment(prisma17, "dev1", "DEVELOPMENT");
await seedBatch(prisma17, ctx.environmentId, { friendlyId: "batch_dev1" });
const presenter = new BatchPresenter(undefined, undefined, {
splitEnabled: true,
newClient: prisma17,
legacyReplica: prisma14,
readThrough: readThroughRun,
resolveDisplayableEnvironment: makeEnvResolver(prisma17),
});
// No userId passed -> userName resolves to the member's username (the orgMember branch).
const result = await presenter.call({
environmentId: ctx.environmentId,
batchId: "batch_dev1",
});
expect(result.environment.type).toBe("DEVELOPMENT");
expect(result.environment.userName).toBe("Display dev1");
}
);
});
describe("BatchPresenter single-DB passthrough", () => {
// Passthrough + self-host collapse: one plain read, legacy closure never invoked.
containerTest(
"single-DB resolves the batch with one plain read and never touches the legacy boundary",
async ({ prisma }) => {
const ctx = await seedEnvironment(prisma, "passthrough");
await seedBatch(prisma, ctx.environmentId, {
friendlyId: "batch_passthrough",
withError: true,
runCount: 5,
});
let legacyInvoked = false;
const presenter = new BatchPresenter(prisma, prisma, {
splitEnabled: false,
// Pass the single DB as both clients; the passthrough must read NEW only.
newClient: prisma,
legacyReplica: new Proxy(prisma, {
get(target, prop) {
if (prop === "batchTaskRun") {
legacyInvoked = true;
throw new Error("legacy boundary must not be touched in single-DB passthrough");
}
return (target as any)[prop];
},
}) as unknown as PrismaClient,
resolveDisplayableEnvironment: makeEnvResolver(prisma),
});
const result = await presenter.call({
environmentId: ctx.environmentId,
batchId: "batch_passthrough",
});
expect(result.friendlyId).toBe("batch_passthrough");
expect(result.runCount).toBe(5);
expect(result.errors).toHaveLength(1);
expect(result.environment.id).toBe(ctx.environmentId);
expect(legacyInvoked).toBe(false);
}
);
// e2e #3 scoped proxy: a batch whose members span migrated + abandoned runs still resolves
// at the batch-record level. Scope: BatchPresenter reads only the batch row, not its member
// TaskRuns — the dangling-reference gate over members is owned by the migration / dangling-gate
// units, not this presenter. This unit's contribution is "batch detail loads regardless of
// which run-ops store holds the batch row."
containerTest(
"e2e #3 proxy: a batch spanning migrated + abandoned runs still resolves",
async ({ prisma }) => {
const ctx = await seedEnvironment(prisma, "e2e3");
await seedBatch(prisma, ctx.environmentId, {
friendlyId: "batch_e2e3",
runCount: 10, // implies members spanning migrated + abandoned runs
});
const presenter = new BatchPresenter(prisma, prisma, {
splitEnabled: false,
newClient: prisma,
resolveDisplayableEnvironment: makeEnvResolver(prisma),
});
const result = await presenter.call({
environmentId: ctx.environmentId,
batchId: "batch_e2e3",
});
expect(result.friendlyId).toBe("batch_e2e3");
expect(result.runCount).toBe(10);
}
);
});