chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
// 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);
|
||||
}
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user