170 lines
6.1 KiB
TypeScript
170 lines
6.1 KiB
TypeScript
// Proof for dropping the canonical BatchTaskRun -> RuntimeEnvironment FK
|
|
// (constraint "BatchTaskRun_runtimeEnvironmentId_fkey", onDelete: Cascade) while keeping the
|
|
// runtimeEnvironmentId scalar and its compound @@unique/@@index. BatchTaskRun is run-ops and
|
|
// RuntimeEnvironment is control-plane, so the two may live on different servers; create-time
|
|
// integrity is preserved app-side via the ControlPlaneResolver's assertEnvExists. Env-delete
|
|
// orphan cleanup is handled separately — here the batch row is tolerated.
|
|
|
|
import { heteroPostgresTest, postgresTest } from "@internal/testcontainers";
|
|
import type { PrismaClient } from "@trigger.dev/database";
|
|
import { describe, expect, vi } from "vitest";
|
|
import { ControlPlaneCache } from "~/v3/runOpsMigration/controlPlaneCache.server";
|
|
import {
|
|
ControlPlaneReferenceError,
|
|
ControlPlaneResolver,
|
|
} from "~/v3/runOpsMigration/controlPlaneResolver.server";
|
|
|
|
// Cross-DB testcontainer spin-up + queries can exceed the 5s default on the first test.
|
|
vi.setConfig({ testTimeout: 60_000 });
|
|
|
|
let seedCounter = 0;
|
|
|
|
async function seedEnvironment(prisma: PrismaClient) {
|
|
const n = seedCounter++;
|
|
const organization = await prisma.organization.create({
|
|
data: { title: `Org ${n}`, slug: `org-${n}` },
|
|
});
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name: `Project ${n}`,
|
|
slug: `project-${n}`,
|
|
externalRef: `proj_${n}`,
|
|
organizationId: organization.id,
|
|
},
|
|
});
|
|
const environment = await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
type: "PRODUCTION",
|
|
slug: `env-${n}`,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: `tr_prod_${n}`,
|
|
pkApiKey: `pk_prod_${n}`,
|
|
shortcode: `short_${n}`,
|
|
},
|
|
});
|
|
return { organization, project, environment };
|
|
}
|
|
|
|
let batchCounter = 0;
|
|
|
|
async function createBatch(prisma: PrismaClient, runtimeEnvironmentId: string) {
|
|
const n = batchCounter++;
|
|
return prisma.batchTaskRun.create({
|
|
data: {
|
|
friendlyId: `batch_${n}`,
|
|
runtimeEnvironmentId,
|
|
runCount: 1,
|
|
runIds: [],
|
|
batchVersion: "runengine:v2",
|
|
},
|
|
});
|
|
}
|
|
|
|
// Asserts the post-migration state of BatchTaskRun on a given client: the FK is gone, but the
|
|
// scalar and both compound constraints are retained. Shared by the single-version and the
|
|
// cross-version suites.
|
|
async function assertSchemaState(prisma: PrismaClient) {
|
|
const foreignKeys = await prisma.$queryRaw<{ constraint_name: string }[]>`
|
|
SELECT constraint_name
|
|
FROM information_schema.table_constraints
|
|
WHERE table_name = 'BatchTaskRun'
|
|
AND constraint_type = 'FOREIGN KEY'
|
|
`;
|
|
expect(foreignKeys.map((c) => c.constraint_name)).not.toContain(
|
|
"BatchTaskRun_runtimeEnvironmentId_fkey"
|
|
);
|
|
|
|
const columns = await prisma.$queryRaw<{ column_name: string }[]>`
|
|
SELECT column_name
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'BatchTaskRun'
|
|
AND column_name = 'runtimeEnvironmentId'
|
|
`;
|
|
expect(columns).toHaveLength(1);
|
|
|
|
// The @@unique([runtimeEnvironmentId, idempotencyKey]) and
|
|
// @@index([runtimeEnvironmentId, id(sort: Desc)]) both survive the FK drop.
|
|
const indexes = await prisma.$queryRaw<{ indexdef: string }[]>`
|
|
SELECT indexdef FROM pg_indexes WHERE tablename = 'BatchTaskRun'
|
|
`;
|
|
const defs = indexes.map((i) => i.indexdef);
|
|
const hasUnique = defs.some(
|
|
(d) => /UNIQUE/i.test(d) && d.includes("runtimeEnvironmentId") && d.includes("idempotencyKey")
|
|
);
|
|
const hasIndex = defs.some(
|
|
(d) => !/UNIQUE/i.test(d) && d.includes("runtimeEnvironmentId") && /\bid\b/.test(d)
|
|
);
|
|
expect(hasUnique).toBe(true);
|
|
expect(hasIndex).toBe(true);
|
|
}
|
|
|
|
// Inserts an env + batch, deletes the env, and asserts the batch survives (cascade gone).
|
|
async function assertOrphanTolerated(prisma: PrismaClient) {
|
|
const { environment } = await seedEnvironment(prisma);
|
|
const batch = await createBatch(prisma, environment.id);
|
|
|
|
await prisma.runtimeEnvironment.delete({ where: { id: environment.id } });
|
|
|
|
const survivor = await prisma.batchTaskRun.findFirst({ where: { id: batch.id } });
|
|
expect(survivor).not.toBeNull();
|
|
expect(survivor?.runtimeEnvironmentId).toBe(environment.id);
|
|
}
|
|
|
|
describe("drop BatchTaskRun -> RuntimeEnvironment FK", () => {
|
|
postgresTest("FK constraint absent; scalar + unique + index retained", async ({ prisma }) => {
|
|
await assertSchemaState(prisma);
|
|
});
|
|
|
|
postgresTest(
|
|
"deleting the env leaves the BatchTaskRun row alive (no cascade; orphan cleanup handled separately)",
|
|
async ({ prisma }) => {
|
|
await assertOrphanTolerated(prisma);
|
|
}
|
|
);
|
|
|
|
postgresTest(
|
|
"app-side env validation: assertEnvExists rejects an invalid env and a valid-env create succeeds by scalar",
|
|
async ({ prisma }) => {
|
|
const { environment } = await seedEnvironment(prisma);
|
|
|
|
const resolver = new ControlPlaneResolver({
|
|
controlPlanePrimary: prisma,
|
|
controlPlaneReplica: prisma,
|
|
cache: new ControlPlaneCache(),
|
|
splitEnabled: () => true,
|
|
});
|
|
|
|
// The exact guard call the create services place before batchTaskRun.create.
|
|
await expect(resolver.assertEnvExists("env_does_not_exist")).rejects.toBeInstanceOf(
|
|
ControlPlaneReferenceError
|
|
);
|
|
|
|
await expect(resolver.assertEnvExists(environment.id)).resolves.toBeUndefined();
|
|
|
|
// Once the guard passes, the batch is linked by the runtimeEnvironmentId scalar (no FK).
|
|
const batch = await createBatch(prisma, environment.id);
|
|
expect(batch.runtimeEnvironmentId).toBe(environment.id);
|
|
}
|
|
);
|
|
});
|
|
|
|
// Cross-version gate: the migration applies and the post-state is identical across major versions.
|
|
describe("drop BatchTaskRun -> RuntimeEnvironment FK — cross-version (legacy + new Postgres)", () => {
|
|
heteroPostgresTest(
|
|
"migration applies and FK is absent on both the legacy and new databases",
|
|
async ({ prisma14, prisma17 }) => {
|
|
await assertSchemaState(prisma14);
|
|
await assertSchemaState(prisma17);
|
|
}
|
|
);
|
|
|
|
heteroPostgresTest(
|
|
"env delete leaves the batch orphaned on both the legacy and new databases",
|
|
async ({ prisma14, prisma17 }) => {
|
|
await assertOrphanTolerated(prisma14);
|
|
await assertOrphanTolerated(prisma17);
|
|
}
|
|
);
|
|
});
|