Repository navigation
Expand file tree
/
Copy pathbatchTaskRunEnvironmentFkDrop.test.ts
More file actions
169 lines (148 loc) · 6.13 KB
/
Copy pathbatchTaskRunEnvironmentFkDrop.test.ts
File metadata and controls
169 lines (148 loc) · 6.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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);
}
);
});