chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
import { heteroPostgresTest } from "@internal/testcontainers";
|
||||
import type { PrismaClient } from "@trigger.dev/database";
|
||||
import { expect, vi } from "vitest";
|
||||
import { ControlPlaneCache } from "~/v3/runOpsMigration/controlPlaneCache.server";
|
||||
import { ControlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
|
||||
|
||||
// Control-plane datasource repoint (legacy DB -> new DB).
|
||||
//
|
||||
// Post-repoint the control plane lives on the new DB, so we model the new topology by seeding the
|
||||
// control-plane rows on the new side (`prisma17`) and injecting it as both the resolver's primary
|
||||
// and replica. `prisma14` stands in for the pre-repoint legacy source for the cross-version
|
||||
// transition test. NEVER mock — we seed and read the real testcontainer clients, and we observe
|
||||
// the DB boundary via a $extends query counter.
|
||||
|
||||
// Cross-DB testcontainer spin-up + queries can exceed the 5s default on the first test.
|
||||
vi.setConfig({ testTimeout: 60_000 });
|
||||
|
||||
let seedCounter = 0;
|
||||
|
||||
/**
|
||||
* Wraps a real testcontainer PrismaClient with a `$extends` query hook that increments a counter
|
||||
* on every actual operation. NOT a mock: the returned client still issues the real query and
|
||||
* returns real rows — we only observe the DB boundary (the countQueries pattern).
|
||||
*/
|
||||
function countQueries(client: PrismaClient): { client: PrismaClient; reads: () => number } {
|
||||
let count = 0;
|
||||
const extended = client.$extends({
|
||||
query: {
|
||||
async $allOperations({ args, query }) {
|
||||
count++;
|
||||
return query(args);
|
||||
},
|
||||
},
|
||||
}) as unknown as PrismaClient;
|
||||
return { client: extended, reads: () => count };
|
||||
}
|
||||
|
||||
/** Seeds org -> project -> env + a pinned BackgroundWorker (+task) + TaskQueue + TaskSchedule. */
|
||||
async function seedControlPlane(prisma: PrismaClient) {
|
||||
const n = seedCounter++;
|
||||
const org = 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: org.id,
|
||||
},
|
||||
});
|
||||
const environment = await prisma.runtimeEnvironment.create({
|
||||
data: {
|
||||
type: "PRODUCTION",
|
||||
slug: `env-${n}`,
|
||||
projectId: project.id,
|
||||
organizationId: org.id,
|
||||
apiKey: `tr_prod_${n}`,
|
||||
pkApiKey: `pk_prod_${n}`,
|
||||
shortcode: `short_${n}`,
|
||||
},
|
||||
});
|
||||
const worker = await prisma.backgroundWorker.create({
|
||||
data: {
|
||||
friendlyId: `worker_${n}`,
|
||||
contentHash: `hash_${n}`,
|
||||
projectId: project.id,
|
||||
runtimeEnvironmentId: environment.id,
|
||||
version: `2024.1.${n}`,
|
||||
metadata: {},
|
||||
engine: "V2",
|
||||
},
|
||||
});
|
||||
await prisma.backgroundWorkerTask.create({
|
||||
data: {
|
||||
friendlyId: `task_${n}`,
|
||||
slug: `my-task-${n}`,
|
||||
filePath: "index.ts",
|
||||
exportName: "myTask",
|
||||
workerId: worker.id,
|
||||
runtimeEnvironmentId: environment.id,
|
||||
projectId: project.id,
|
||||
},
|
||||
});
|
||||
const queue = await prisma.taskQueue.create({
|
||||
data: {
|
||||
friendlyId: `queue_${n}`,
|
||||
name: `task/my-task-${n}`,
|
||||
runtimeEnvironmentId: environment.id,
|
||||
projectId: project.id,
|
||||
workers: { connect: { id: worker.id } },
|
||||
},
|
||||
});
|
||||
const schedule = await prisma.taskSchedule.create({
|
||||
data: {
|
||||
friendlyId: `schedule_${n}`,
|
||||
taskIdentifier: `my-task-${n}`,
|
||||
generatorExpression: "0 * * * *",
|
||||
projectId: project.id,
|
||||
},
|
||||
});
|
||||
return { org, project, environment, worker, queue, schedule };
|
||||
}
|
||||
|
||||
// --- Repoint resolution (split ON, CP on the new DB) ---------
|
||||
|
||||
heteroPostgresTest(
|
||||
"control-plane references resolve against the repointed (new-DB) CP client",
|
||||
async ({ prisma17 }) => {
|
||||
const { environment, worker } = await seedControlPlane(prisma17);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlanePrimary: prisma17,
|
||||
controlPlaneReplica: prisma17,
|
||||
cache: new ControlPlaneCache(),
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
expect(await resolver.resolveEnv(environment.id)).toMatchObject({ id: environment.id });
|
||||
expect(
|
||||
await resolver.resolveWorkerVersion({
|
||||
environmentId: environment.id,
|
||||
backgroundWorkerId: worker.id,
|
||||
})
|
||||
).not.toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
// --- Relaxed-cache (no latency regression) -------------------------
|
||||
|
||||
heteroPostgresTest("relaxed (longer TTL) cache still hits on the new DB", async ({ prisma17 }) => {
|
||||
const { environment } = await seedControlPlane(prisma17);
|
||||
const { client: counting, reads } = countQueries(prisma17);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlanePrimary: counting,
|
||||
controlPlaneReplica: counting,
|
||||
// Relaxed: a much longer TTL than the default — same-provider resolution is cheap.
|
||||
cache: new ControlPlaneCache({ ttlMs: 300_000, maxEntries: 10_000 }),
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
expect(await resolver.resolveEnv(environment.id)).toMatchObject({ id: environment.id });
|
||||
expect(reads()).toBe(1);
|
||||
// Second read served from the relaxed cache — no extra DB round-trip.
|
||||
await resolver.resolveEnv(environment.id);
|
||||
expect(reads()).toBe(1);
|
||||
});
|
||||
|
||||
// --- Cross-version transition (legacy DB -> new DB) -----------------------
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolution is byte-identical across the legacy-DB -> new-DB host transition",
|
||||
async ({ prisma14, prisma17, pinnedCollation }) => {
|
||||
// Seed identical control-plane shapes on the pre-repoint (legacy) and post-repoint
|
||||
// (new) sides.
|
||||
const before = await seedControlPlane(prisma14);
|
||||
const after = await seedControlPlane(prisma17);
|
||||
|
||||
const resolver14 = new ControlPlaneResolver({
|
||||
controlPlanePrimary: prisma14,
|
||||
controlPlaneReplica: prisma14,
|
||||
cache: new ControlPlaneCache(),
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
const resolver17 = new ControlPlaneResolver({
|
||||
controlPlanePrimary: prisma17,
|
||||
controlPlaneReplica: prisma17,
|
||||
cache: new ControlPlaneCache(),
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
const env14 = await resolver14.resolveEnv(before.environment.id);
|
||||
const env17 = await resolver17.resolveEnv(after.environment.id);
|
||||
// Same resolution shape across the version boundary (ids differ per-seed; structure identical).
|
||||
expect(Object.keys(env14 ?? {}).sort()).toEqual(Object.keys(env17 ?? {}).sort());
|
||||
expect(env14?.type).toBe(env17?.type);
|
||||
expect(env14?.archivedAt).toBe(env17?.archivedAt);
|
||||
|
||||
// ORDER BY on a representative text-heavy column must agree across the version boundary, using
|
||||
// the pinned ICU collation the hetero fixture exposes so the comparison is apples-to-apples.
|
||||
const slugs = ["banana", "Apple", "cherry", "Äpfel", "apple"];
|
||||
const orderBy = async (prisma: PrismaClient) => {
|
||||
const rows = await prisma.$queryRawUnsafe<{ s: string }[]>(
|
||||
`SELECT s FROM (VALUES ('${slugs.join("'),('")}')) AS t(s) ORDER BY s COLLATE "${pinnedCollation}"`
|
||||
);
|
||||
return rows.map((r) => r.s);
|
||||
};
|
||||
expect(await orderBy(prisma14)).toEqual(await orderBy(prisma17));
|
||||
}
|
||||
);
|
||||
|
||||
// --- Single-DB no-op (passthrough preserved) -----------------------
|
||||
|
||||
heteroPostgresTest(
|
||||
"single-DB passthrough (split OFF) runs plain in-DB joins with no cache",
|
||||
async ({ prisma17 }) => {
|
||||
const { environment } = await seedControlPlane(prisma17);
|
||||
const { client: counting, reads } = countQueries(prisma17);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlanePrimary: counting,
|
||||
controlPlaneReplica: counting,
|
||||
cache: new ControlPlaneCache(),
|
||||
splitEnabled: () => false,
|
||||
});
|
||||
|
||||
await resolver.resolveEnv(environment.id);
|
||||
await resolver.resolveEnv(environment.id);
|
||||
// No cache when split is OFF — every call hits the DB, identical to today's passthrough.
|
||||
expect(reads()).toBe(2);
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user