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);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,750 @@
|
||||
import { heteroPostgresTest } from "@internal/testcontainers";
|
||||
import type { PrismaClient } from "@trigger.dev/database";
|
||||
import { describe, expect, it, 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 });
|
||||
|
||||
// --- test helpers ----------------------------------------------------------
|
||||
|
||||
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.
|
||||
*/
|
||||
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 };
|
||||
}
|
||||
|
||||
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}`,
|
||||
},
|
||||
});
|
||||
return { org, project, environment };
|
||||
}
|
||||
|
||||
async function seedWorker(
|
||||
prisma: PrismaClient,
|
||||
ctx: { projectId: string; environmentId: string },
|
||||
opts?: { promote?: boolean }
|
||||
) {
|
||||
const n = seedCounter++;
|
||||
const worker = await prisma.backgroundWorker.create({
|
||||
data: {
|
||||
friendlyId: `worker_${n}`,
|
||||
contentHash: `hash_${n}`,
|
||||
projectId: ctx.projectId,
|
||||
runtimeEnvironmentId: ctx.environmentId,
|
||||
version: `2024.1.${n}`,
|
||||
metadata: {},
|
||||
engine: "V2",
|
||||
},
|
||||
});
|
||||
const task = await prisma.backgroundWorkerTask.create({
|
||||
data: {
|
||||
friendlyId: `task_${n}`,
|
||||
slug: `my-task-${n}`,
|
||||
filePath: "index.ts",
|
||||
exportName: "myTask",
|
||||
workerId: worker.id,
|
||||
runtimeEnvironmentId: ctx.environmentId,
|
||||
projectId: ctx.projectId,
|
||||
},
|
||||
});
|
||||
const queue = await prisma.taskQueue.create({
|
||||
data: {
|
||||
friendlyId: `queue_${n}`,
|
||||
name: `task/my-task-${n}`,
|
||||
runtimeEnvironmentId: ctx.environmentId,
|
||||
projectId: ctx.projectId,
|
||||
workers: { connect: { id: worker.id } },
|
||||
},
|
||||
});
|
||||
let deployment = null;
|
||||
if (opts?.promote) {
|
||||
deployment = await prisma.workerDeployment.create({
|
||||
data: {
|
||||
friendlyId: `deployment_${n}`,
|
||||
contentHash: `hash_${n}`,
|
||||
version: worker.version,
|
||||
shortCode: `dep_${n}`,
|
||||
type: "MANAGED",
|
||||
status: "DEPLOYED",
|
||||
projectId: ctx.projectId,
|
||||
environmentId: ctx.environmentId,
|
||||
workerId: worker.id,
|
||||
},
|
||||
});
|
||||
await prisma.workerDeploymentPromotion.create({
|
||||
data: {
|
||||
label: "current",
|
||||
deploymentId: deployment.id,
|
||||
environmentId: ctx.environmentId,
|
||||
},
|
||||
});
|
||||
}
|
||||
return { worker, task, queue, deployment };
|
||||
}
|
||||
|
||||
// --- cache unit tests (no DB) ----------------------------------------------
|
||||
|
||||
describe("ControlPlaneCache", () => {
|
||||
it("caches null as a confirmed absence (distinct from a miss)", () => {
|
||||
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
|
||||
expect(cache.getEnv("env_x")).toBeUndefined();
|
||||
cache.setEnv("env_x", null);
|
||||
expect(cache.getEnv("env_x")).toBeNull();
|
||||
});
|
||||
|
||||
it("invalidateEnv drops the entry (next read is a miss)", () => {
|
||||
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
|
||||
cache.setEnv("env_y", { id: "env_y" } as any);
|
||||
cache.invalidateEnv("env_y");
|
||||
expect(cache.getEnv("env_y")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("invalidating one key does not affect another", () => {
|
||||
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
|
||||
cache.setEnv("env_a", { id: "env_a" } as any);
|
||||
cache.setEnv("env_b", { id: "env_b" } as any);
|
||||
cache.invalidateEnv("env_a");
|
||||
expect(cache.getEnv("env_a")).toBeUndefined();
|
||||
expect(cache.getEnv("env_b")).toMatchObject({ id: "env_b" });
|
||||
});
|
||||
});
|
||||
|
||||
// --- resolveEnv -------------------------------------------------------------
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveEnv returns the cross-DB env row and caches it",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment, org } = await seedControlPlane(prisma14);
|
||||
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
|
||||
const { client: counting, reads } = countQueries(prisma14);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: counting,
|
||||
controlPlanePrimary: counting,
|
||||
cache,
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
const first = await resolver.resolveEnv(environment.id);
|
||||
expect(first).toMatchObject({
|
||||
id: environment.id,
|
||||
projectId: environment.projectId,
|
||||
organizationId: org.id,
|
||||
type: "PRODUCTION",
|
||||
archivedAt: null,
|
||||
});
|
||||
expect(reads()).toBe(1);
|
||||
|
||||
const second = await resolver.resolveEnv(environment.id);
|
||||
expect(second).toEqual(first);
|
||||
expect(reads()).toBe(1);
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest("resolveEnv caches a null absence", async ({ prisma14 }) => {
|
||||
const cache = new ControlPlaneCache();
|
||||
const { client: counting, reads } = countQueries(prisma14);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: counting,
|
||||
controlPlanePrimary: counting,
|
||||
cache,
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
expect(await resolver.resolveEnv("env_does_not_exist")).toBeNull();
|
||||
expect(reads()).toBe(1);
|
||||
expect(await resolver.resolveEnv("env_does_not_exist")).toBeNull();
|
||||
expect(reads()).toBe(1);
|
||||
});
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveEnv passthrough (split OFF) hits the DB every time, no cache",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment } = await seedControlPlane(prisma14);
|
||||
const { client: counting, reads } = countQueries(prisma14);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: counting,
|
||||
controlPlanePrimary: counting,
|
||||
cache: new ControlPlaneCache(),
|
||||
splitEnabled: () => false,
|
||||
});
|
||||
|
||||
await resolver.resolveEnv(environment.id);
|
||||
await resolver.resolveEnv(environment.id);
|
||||
expect(reads()).toBe(2);
|
||||
}
|
||||
);
|
||||
|
||||
// --- resolveWorkerVersion ---------------------------------------------------
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveWorkerVersion (pinned) returns worker/tasks/queues and caches it",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment, project } = await seedControlPlane(prisma14);
|
||||
const { worker, task, queue } = await seedWorker(prisma14, {
|
||||
projectId: project.id,
|
||||
environmentId: environment.id,
|
||||
});
|
||||
const { client: counting, reads } = countQueries(prisma14);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: counting,
|
||||
controlPlanePrimary: counting,
|
||||
cache: new ControlPlaneCache(),
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
const first = await resolver.resolveWorkerVersion({
|
||||
environmentId: environment.id,
|
||||
backgroundWorkerId: worker.id,
|
||||
});
|
||||
expect(first?.worker.id).toBe(worker.id);
|
||||
expect(first?.tasks.map((t) => t.id)).toContain(task.id);
|
||||
expect(first?.queues.map((q) => q.id)).toContain(queue.id);
|
||||
expect(first?.deployment).toBeNull();
|
||||
const readsAfterFirst = reads();
|
||||
expect(readsAfterFirst).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const second = await resolver.resolveWorkerVersion({
|
||||
environmentId: environment.id,
|
||||
backgroundWorkerId: worker.id,
|
||||
});
|
||||
expect(second?.worker.id).toBe(worker.id);
|
||||
expect(reads()).toBe(readsAfterFirst);
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveWorkerVersion (current deployment) resolves the promoted worker",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment, project } = await seedControlPlane(prisma14);
|
||||
const { worker, deployment } = await seedWorker(
|
||||
prisma14,
|
||||
{ projectId: project.id, environmentId: environment.id },
|
||||
{ promote: true }
|
||||
);
|
||||
const { client: counting, reads } = countQueries(prisma14);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: counting,
|
||||
controlPlanePrimary: counting,
|
||||
cache: new ControlPlaneCache(),
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
const first = await resolver.resolveWorkerVersion({ environmentId: environment.id });
|
||||
expect(first?.worker.id).toBe(worker.id);
|
||||
expect(first?.deployment?.id).toBe(deployment?.id);
|
||||
const readsAfterFirst = reads();
|
||||
|
||||
const second = await resolver.resolveWorkerVersion({ environmentId: environment.id });
|
||||
expect(second?.worker.id).toBe(worker.id);
|
||||
expect(reads()).toBe(readsAfterFirst);
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveWorkerVersion passthrough (split OFF) re-reads every call",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment, project } = await seedControlPlane(prisma14);
|
||||
const { worker } = await seedWorker(prisma14, {
|
||||
projectId: project.id,
|
||||
environmentId: environment.id,
|
||||
});
|
||||
const { client: counting, reads } = countQueries(prisma14);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: counting,
|
||||
controlPlanePrimary: counting,
|
||||
cache: new ControlPlaneCache(),
|
||||
splitEnabled: () => false,
|
||||
});
|
||||
|
||||
await resolver.resolveWorkerVersion({
|
||||
environmentId: environment.id,
|
||||
backgroundWorkerId: worker.id,
|
||||
});
|
||||
const readsAfterFirst = reads();
|
||||
await resolver.resolveWorkerVersion({
|
||||
environmentId: environment.id,
|
||||
backgroundWorkerId: worker.id,
|
||||
});
|
||||
expect(reads()).toBe(readsAfterFirst * 2);
|
||||
}
|
||||
);
|
||||
|
||||
// --- assertEnvExists --------------------------------------------------------
|
||||
|
||||
heteroPostgresTest(
|
||||
"assertEnvExists resolves for a seeded env, caches, and throws for a missing one",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment } = await seedControlPlane(prisma14);
|
||||
const { client: counting, reads } = countQueries(prisma14);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: counting,
|
||||
controlPlanePrimary: counting,
|
||||
cache: new ControlPlaneCache(),
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
await expect(resolver.assertEnvExists(environment.id)).resolves.toBeUndefined();
|
||||
expect(reads()).toBe(1);
|
||||
await expect(resolver.assertEnvExists(environment.id)).resolves.toBeUndefined();
|
||||
expect(reads()).toBe(1);
|
||||
|
||||
await expect(resolver.assertEnvExists("env_missing")).rejects.toBeInstanceOf(
|
||||
ControlPlaneReferenceError
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"assertEnvExists passthrough (split OFF) is a no-op: never reads, never throws",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment } = await seedControlPlane(prisma14);
|
||||
const { client: counting, reads } = countQueries(prisma14);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: counting,
|
||||
controlPlanePrimary: counting,
|
||||
cache: new ControlPlaneCache(),
|
||||
splitEnabled: () => false,
|
||||
});
|
||||
|
||||
// Split OFF = single DB, run and env co-located, so there is nothing to assert
|
||||
// and the hot-path read is skipped entirely — resolves for present and missing.
|
||||
await expect(resolver.assertEnvExists(environment.id)).resolves.toBeUndefined();
|
||||
await expect(resolver.assertEnvExists("env_missing")).resolves.toBeUndefined();
|
||||
expect(reads()).toBe(0);
|
||||
}
|
||||
);
|
||||
|
||||
// --- resolveAuthenticatedEnv ------------------------------------------------
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveAuthenticatedEnv returns the toAuthenticated shape and caches it",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment, project, org } = await seedControlPlane(prisma14);
|
||||
const { client: counting, reads } = countQueries(prisma14);
|
||||
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: counting,
|
||||
controlPlanePrimary: counting,
|
||||
cache,
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
const first = await resolver.resolveAuthenticatedEnv(environment.id);
|
||||
expect(first).not.toBeNull();
|
||||
expect(first!.id).toBe(environment.id);
|
||||
expect(first!.slug).toBe(environment.slug);
|
||||
expect(first!.type).toBe("PRODUCTION");
|
||||
expect(first!.organizationId).toBe(org.id);
|
||||
expect(first!.projectId).toBe(project.id);
|
||||
expect(first!.project.id).toBe(project.id);
|
||||
expect(first!.project.externalRef).toBe(project.externalRef);
|
||||
expect(first!.organization.id).toBe(org.id);
|
||||
expect(first!.organization.title).toBe(org.title);
|
||||
// concurrencyLimitBurstFactor is coerced to a plain number by toAuthenticated().
|
||||
expect(typeof first!.concurrencyLimitBurstFactor).toBe("number");
|
||||
expect(reads()).toBe(1);
|
||||
|
||||
const second = await resolver.resolveAuthenticatedEnv(environment.id);
|
||||
expect(second).toEqual(first);
|
||||
expect(reads()).toBe(1);
|
||||
|
||||
expect(await resolver.resolveAuthenticatedEnv("env_missing")).toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveAuthenticatedEnv populates parentEnvironment { id, apiKey } for a branch env",
|
||||
async ({ prisma14 }) => {
|
||||
const m = seedCounter++;
|
||||
const org = await prisma14.organization.create({
|
||||
data: { title: `Org wp ${m}`, slug: `org-wp-${m}` },
|
||||
});
|
||||
const project = await prisma14.project.create({
|
||||
data: {
|
||||
name: `P wp ${m}`,
|
||||
slug: `p-wp-${m}`,
|
||||
externalRef: `proj_wp_${m}`,
|
||||
organizationId: org.id,
|
||||
},
|
||||
});
|
||||
const parent = await prisma14.runtimeEnvironment.create({
|
||||
data: {
|
||||
type: "PREVIEW",
|
||||
slug: `preview-parent-${m}`,
|
||||
projectId: project.id,
|
||||
organizationId: org.id,
|
||||
apiKey: `tr_parent_key_${m}`,
|
||||
pkApiKey: `pk_parent_${m}`,
|
||||
shortcode: `sc_parent_${m}`,
|
||||
},
|
||||
});
|
||||
const branch = await prisma14.runtimeEnvironment.create({
|
||||
data: {
|
||||
type: "PREVIEW",
|
||||
slug: `preview-branch-${m}`,
|
||||
branchName: "feat/x",
|
||||
projectId: project.id,
|
||||
organizationId: org.id,
|
||||
apiKey: `tr_branch_key_${m}`,
|
||||
pkApiKey: `pk_branch_${m}`,
|
||||
shortcode: `sc_branch_${m}`,
|
||||
parentEnvironmentId: parent.id,
|
||||
},
|
||||
});
|
||||
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: prisma14,
|
||||
controlPlanePrimary: prisma14,
|
||||
cache: new ControlPlaneCache(),
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
const env = await resolver.resolveAuthenticatedEnv(branch.id);
|
||||
expect(env).not.toBeNull();
|
||||
expect(env!.apiKey).toBe(`tr_branch_key_${m}`);
|
||||
expect(env!.parentEnvironment).not.toBeNull();
|
||||
expect(env!.parentEnvironment!.id).toBe(parent.id);
|
||||
expect(env!.parentEnvironment!.apiKey).toBe(`tr_parent_key_${m}`);
|
||||
|
||||
const noParent = await resolver.resolveAuthenticatedEnv(parent.id);
|
||||
expect(noParent!.parentEnvironment).toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveAuthenticatedEnv passthrough (split OFF) hits the DB every time, no cache",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment } = await seedControlPlane(prisma14);
|
||||
const { client: counting, reads } = countQueries(prisma14);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: counting,
|
||||
controlPlanePrimary: counting,
|
||||
cache: new ControlPlaneCache(),
|
||||
splitEnabled: () => false,
|
||||
});
|
||||
|
||||
await resolver.resolveAuthenticatedEnv(environment.id);
|
||||
await resolver.resolveAuthenticatedEnv(environment.id);
|
||||
expect(reads()).toBe(2);
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveAuthenticatedEnv carries the `git` column (cached across calls)",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment } = await seedControlPlane(prisma14);
|
||||
const gitMeta = { commitSha: "abc123", branchName: "main" };
|
||||
await prisma14.runtimeEnvironment.update({
|
||||
where: { id: environment.id },
|
||||
data: { git: gitMeta },
|
||||
});
|
||||
|
||||
const { client: counting, reads } = countQueries(prisma14);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: counting,
|
||||
controlPlanePrimary: counting,
|
||||
cache: new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 }),
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
const first = await resolver.resolveAuthenticatedEnv(environment.id);
|
||||
expect(first).not.toBeNull();
|
||||
expect(first!.git).toEqual(gitMeta);
|
||||
expect(reads()).toBe(1);
|
||||
|
||||
// Served from cache, still carrying `git`.
|
||||
const second = await resolver.resolveAuthenticatedEnv(environment.id);
|
||||
expect(second!.git).toEqual(gitMeta);
|
||||
expect(reads()).toBe(1);
|
||||
}
|
||||
);
|
||||
|
||||
// --- invalidation over the DB boundary -------------------------------------
|
||||
|
||||
heteroPostgresTest(
|
||||
"invalidateEnvironment forces resolveEnv/resolveAuthenticatedEnv to re-read after a write",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment } = await seedControlPlane(prisma14);
|
||||
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: prisma14,
|
||||
controlPlanePrimary: prisma14,
|
||||
cache,
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
// Warm both env-scoped slots.
|
||||
expect((await resolver.resolveEnv(environment.id))!.maximumConcurrencyLimit).not.toBe(999);
|
||||
expect((await resolver.resolveAuthenticatedEnv(environment.id))!.paused).toBe(false);
|
||||
|
||||
// Control-plane write + invalidation (as a write site would do).
|
||||
await prisma14.runtimeEnvironment.update({
|
||||
where: { id: environment.id },
|
||||
data: { maximumConcurrencyLimit: 999, paused: true },
|
||||
});
|
||||
resolver.invalidateEnvironment(environment.id);
|
||||
|
||||
expect((await resolver.resolveEnv(environment.id))!.maximumConcurrencyLimit).toBe(999);
|
||||
expect((await resolver.resolveAuthenticatedEnv(environment.id))!.paused).toBe(true);
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"without invalidation a cached env stays stale after a control-plane write (fail-before contrast)",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment } = await seedControlPlane(prisma14);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: prisma14,
|
||||
controlPlanePrimary: prisma14,
|
||||
cache: new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 }),
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
const before = (await resolver.resolveEnv(environment.id))!.maximumConcurrencyLimit;
|
||||
await prisma14.runtimeEnvironment.update({
|
||||
where: { id: environment.id },
|
||||
data: { maximumConcurrencyLimit: 777 },
|
||||
});
|
||||
|
||||
// No invalidation: the cache still serves the pre-write value (this is the bug the
|
||||
// write-site invalidation fixes).
|
||||
expect((await resolver.resolveEnv(environment.id))!.maximumConcurrencyLimit).toBe(before);
|
||||
|
||||
// And with invalidation it re-reads.
|
||||
resolver.invalidateEnvironment(environment.id);
|
||||
expect((await resolver.resolveEnv(environment.id))!.maximumConcurrencyLimit).toBe(777);
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"invalidateOrganization forces every env of the org to re-read after an org write",
|
||||
async ({ prisma14 }) => {
|
||||
const { org: organization, project } = await seedControlPlane(prisma14);
|
||||
// A second env in the same org.
|
||||
const m = seedCounter++;
|
||||
const secondEnv = await prisma14.runtimeEnvironment.create({
|
||||
data: {
|
||||
type: "STAGING",
|
||||
slug: `env-second-${m}`,
|
||||
projectId: project.id,
|
||||
organizationId: organization.id,
|
||||
apiKey: `tr_stg_${m}`,
|
||||
pkApiKey: `pk_stg_${m}`,
|
||||
shortcode: `short_stg_${m}`,
|
||||
},
|
||||
});
|
||||
const firstEnv = await prisma14.runtimeEnvironment.findFirstOrThrow({
|
||||
where: { projectId: project.id, type: "PRODUCTION" },
|
||||
});
|
||||
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: prisma14,
|
||||
controlPlanePrimary: prisma14,
|
||||
cache: new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 }),
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
// Warm both envs' authEnv slots.
|
||||
expect((await resolver.resolveAuthenticatedEnv(firstEnv.id))!.organization.runsEnabled).toBe(
|
||||
true
|
||||
);
|
||||
expect((await resolver.resolveAuthenticatedEnv(secondEnv.id))!.organization.runsEnabled).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
// Org-level write (runsEnabled) + a single org invalidation.
|
||||
await prisma14.organization.update({
|
||||
where: { id: organization.id },
|
||||
data: { runsEnabled: false },
|
||||
});
|
||||
resolver.invalidateOrganization(organization.id);
|
||||
|
||||
// BOTH envs re-read and now observe the org change, with no reverse org->env index.
|
||||
expect((await resolver.resolveAuthenticatedEnv(firstEnv.id))!.organization.runsEnabled).toBe(
|
||||
false
|
||||
);
|
||||
expect((await resolver.resolveAuthenticatedEnv(secondEnv.id))!.organization.runsEnabled).toBe(
|
||||
false
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// --- resolveRunLockedWorker -------------------------------------------------
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveRunLockedWorker returns lockedBy (task+worker+deployment) and lockedToVersion, caches it",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment, project } = await seedControlPlane(prisma14);
|
||||
const { worker, task, deployment } = await seedWorker(
|
||||
prisma14,
|
||||
{ projectId: project.id, environmentId: environment.id },
|
||||
{ promote: true }
|
||||
);
|
||||
const { client: counting, reads } = countQueries(prisma14);
|
||||
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: counting,
|
||||
controlPlanePrimary: counting,
|
||||
cache,
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
const first = await resolver.resolveRunLockedWorker({
|
||||
lockedById: task.id,
|
||||
lockedToVersionId: worker.id,
|
||||
});
|
||||
expect(first).not.toBeNull();
|
||||
expect(first!.lockedBy!.id).toBe(task.id);
|
||||
expect(first!.lockedBy!.filePath).toBe(task.filePath);
|
||||
expect(first!.lockedBy!.slug).toBe(task.slug);
|
||||
expect(first!.lockedBy!.exportName).toBe(task.exportName);
|
||||
expect(first!.lockedBy!.machineConfig).toEqual(task.machineConfig);
|
||||
expect(first!.lockedBy!.worker.id).toBe(worker.id);
|
||||
expect(first!.lockedBy!.worker.version).toBe(worker.version);
|
||||
expect(first!.lockedBy!.worker.deployment!.friendlyId).toBe(deployment!.friendlyId);
|
||||
expect(first!.lockedToVersion!.version).toBe(worker.version);
|
||||
expect(first!.lockedToVersion!.supportsLazyAttempts).toBe(worker.supportsLazyAttempts);
|
||||
const readsAfterFirst = reads();
|
||||
expect(readsAfterFirst).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const second = await resolver.resolveRunLockedWorker({
|
||||
lockedById: task.id,
|
||||
lockedToVersionId: worker.id,
|
||||
});
|
||||
expect(second).toEqual(first);
|
||||
expect(reads()).toBe(readsAfterFirst);
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveRunLockedWorker returns null lockedBy/lockedToVersion when ids are absent",
|
||||
async ({ prisma14 }) => {
|
||||
const { client: counting } = countQueries(prisma14);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: counting,
|
||||
controlPlanePrimary: counting,
|
||||
cache: new ControlPlaneCache(),
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
const resolved = await resolver.resolveRunLockedWorker({
|
||||
lockedById: null,
|
||||
lockedToVersionId: null,
|
||||
});
|
||||
expect(resolved).not.toBeNull();
|
||||
expect(resolved!.lockedBy).toBeNull();
|
||||
expect(resolved!.lockedToVersion).toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveRunLockedWorker resolves lockedBy only when lockedToVersionId is absent",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment, project } = await seedControlPlane(prisma14);
|
||||
const { task } = await seedWorker(
|
||||
prisma14,
|
||||
{ projectId: project.id, environmentId: environment.id },
|
||||
{ promote: true }
|
||||
);
|
||||
const { client: counting } = countQueries(prisma14);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: counting,
|
||||
controlPlanePrimary: counting,
|
||||
cache: new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 }),
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
const result = await resolver.resolveRunLockedWorker({ lockedById: task.id });
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.lockedBy!.id).toBe(task.id);
|
||||
expect(result!.lockedBy!.slug).toBe(task.slug);
|
||||
expect(result!.lockedToVersion).toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveRunLockedWorker resolves lockedToVersion only when lockedById is absent",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment, project } = await seedControlPlane(prisma14);
|
||||
const { worker } = await seedWorker(
|
||||
prisma14,
|
||||
{ projectId: project.id, environmentId: environment.id },
|
||||
{ promote: true }
|
||||
);
|
||||
const { client: counting } = countQueries(prisma14);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: counting,
|
||||
controlPlanePrimary: counting,
|
||||
cache: new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 }),
|
||||
splitEnabled: () => true,
|
||||
});
|
||||
|
||||
const result = await resolver.resolveRunLockedWorker({ lockedToVersionId: worker.id });
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.lockedToVersion!.version).toBe(worker.version);
|
||||
expect(result!.lockedBy).toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveRunLockedWorker passthrough (split OFF) re-reads every call",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment, project } = await seedControlPlane(prisma14);
|
||||
const { worker, task } = await seedWorker(prisma14, {
|
||||
projectId: project.id,
|
||||
environmentId: environment.id,
|
||||
});
|
||||
const { client: counting, reads } = countQueries(prisma14);
|
||||
const resolver = new ControlPlaneResolver({
|
||||
controlPlaneReplica: counting,
|
||||
controlPlanePrimary: counting,
|
||||
cache: new ControlPlaneCache(),
|
||||
splitEnabled: () => false,
|
||||
});
|
||||
|
||||
await resolver.resolveRunLockedWorker({ lockedById: task.id, lockedToVersionId: worker.id });
|
||||
const readsAfterFirst = reads();
|
||||
await resolver.resolveRunLockedWorker({ lockedById: task.id, lockedToVersionId: worker.id });
|
||||
expect(reads()).toBe(readsAfterFirst * 2);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,64 @@
|
||||
import { heteroPostgresTest } from "@internal/testcontainers";
|
||||
import { PrismaClient } from "@trigger.dev/database";
|
||||
import { describe, expect, vi } from "vitest";
|
||||
import { probeDistinctDatabases } from "~/v3/runOpsMigration/distinctDbSentinel.server";
|
||||
|
||||
// Spinning up two separate postgres clusters and probing each can exceed the 5s default.
|
||||
vi.setConfig({ testTimeout: 60_000 });
|
||||
|
||||
function urlWithDatabase(uri: string, database: string): string {
|
||||
const url = new URL(uri);
|
||||
url.pathname = `/${database}`;
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
describe("probeDistinctDatabases", () => {
|
||||
heteroPostgresTest(
|
||||
"reports distinct for two separate physical clusters",
|
||||
async ({ uri14, uri17 }) => {
|
||||
const result = await probeDistinctDatabases(uri14, uri17);
|
||||
expect(result).toEqual({ distinct: true });
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"reports NOT distinct, citing the same physical database, when both URLs point at it",
|
||||
async ({ uri14 }) => {
|
||||
const result = await probeDistinctDatabases(uri14, uri14);
|
||||
expect(result.distinct).toBe(false);
|
||||
if (result.distinct === false) {
|
||||
expect(result.reason).toMatch(/same physical database/i);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"reports distinct for two databases in the SAME cluster",
|
||||
async ({ postgresContainer14, uri14 }) => {
|
||||
const otherDb = `sentinel_other_${Date.now()}`;
|
||||
const admin = new PrismaClient({
|
||||
datasources: {
|
||||
db: { url: urlWithDatabase(postgresContainer14.getConnectionUri(), "postgres") },
|
||||
},
|
||||
});
|
||||
try {
|
||||
await admin.$executeRawUnsafe(`CREATE DATABASE "${otherDb}"`);
|
||||
} finally {
|
||||
await admin.$disconnect();
|
||||
}
|
||||
|
||||
const otherUrl = urlWithDatabase(uri14, otherDb);
|
||||
const result = await probeDistinctDatabases(uri14, otherUrl);
|
||||
expect(result).toEqual({ distinct: true });
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"fails closed to NOT distinct when a probe cannot reach a database",
|
||||
async ({ uri14 }) => {
|
||||
const unreachable = "postgresql://nobody:nobody@127.0.0.1:1/does_not_exist";
|
||||
const result = await probeDistinctDatabases(uri14, unreachable);
|
||||
expect(result.distinct).toBe(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,265 @@
|
||||
// The webapp adapter presents the cross-DB app ControlPlaneResolver as the run-engine seam.
|
||||
// Proven over real testcontainers (never mocked): resolveEnv maps onto the MinimalAuthenticatedEnv
|
||||
// superset; resolveWorkerVersion forwards the env type so the engine dequeue dispatch (DEV
|
||||
// most-recent / MANAGED promotion) runs; assertEnvExists delegates and rejects on a missing env.
|
||||
import { heteroPostgresTest } from "@internal/testcontainers";
|
||||
import type { PrismaClient } from "@trigger.dev/database";
|
||||
import { CURRENT_DEPLOYMENT_LABEL } from "@trigger.dev/core/v3/isomorphic";
|
||||
import { describe, expect, vi } from "vitest";
|
||||
import { ControlPlaneCache } from "~/v3/runOpsMigration/controlPlaneCache.server";
|
||||
import { ControlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
|
||||
import { RunEngineControlPlaneResolver } from "~/v3/runOpsMigration/runEngineControlPlaneResolver.server";
|
||||
|
||||
vi.setConfig({ testTimeout: 60_000 });
|
||||
|
||||
let n = 0;
|
||||
|
||||
function buildAppResolver(controlPlane: PrismaClient, opts?: { splitEnabled?: boolean }) {
|
||||
return new ControlPlaneResolver({
|
||||
controlPlanePrimary: controlPlane,
|
||||
controlPlaneReplica: controlPlane,
|
||||
cache: new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 }),
|
||||
splitEnabled: () => opts?.splitEnabled ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a real testcontainer PrismaClient with a `$extends` query hook counting DB operations.
|
||||
* Not a mock — the real query still runs; we only observe the boundary to prove cache hits.
|
||||
*/
|
||||
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 };
|
||||
}
|
||||
|
||||
async function seedEnv(prisma: PrismaClient, type: "PRODUCTION" | "DEVELOPMENT") {
|
||||
const suffix = `re-${n++}`;
|
||||
const organization = await prisma.organization.create({
|
||||
data: { title: `Org ${suffix}`, slug: `org-${suffix}` },
|
||||
});
|
||||
const project = await prisma.project.create({
|
||||
data: {
|
||||
name: `Project ${suffix}`,
|
||||
slug: `project-${suffix}`,
|
||||
externalRef: `proj_${suffix}`,
|
||||
organizationId: organization.id,
|
||||
},
|
||||
});
|
||||
const environment = await prisma.runtimeEnvironment.create({
|
||||
data: {
|
||||
type,
|
||||
slug: suffix,
|
||||
projectId: project.id,
|
||||
organizationId: organization.id,
|
||||
apiKey: `tr_${suffix}`,
|
||||
pkApiKey: `pk_${suffix}`,
|
||||
shortcode: `short_${suffix}`,
|
||||
maximumConcurrencyLimit: 9,
|
||||
},
|
||||
});
|
||||
return { organization, project, environment, suffix };
|
||||
}
|
||||
|
||||
async function seedWorker(
|
||||
prisma: PrismaClient,
|
||||
ctx: { projectId: string; environmentId: string; suffix: string },
|
||||
opts: { promote?: boolean; deploy?: boolean }
|
||||
) {
|
||||
const worker = await prisma.backgroundWorker.create({
|
||||
data: {
|
||||
friendlyId: `worker_${ctx.suffix}`,
|
||||
contentHash: `hash_${ctx.suffix}`,
|
||||
projectId: ctx.projectId,
|
||||
runtimeEnvironmentId: ctx.environmentId,
|
||||
version: `2024.1.${ctx.suffix}`,
|
||||
metadata: {},
|
||||
engine: "V2",
|
||||
},
|
||||
});
|
||||
const task = await prisma.backgroundWorkerTask.create({
|
||||
data: {
|
||||
friendlyId: `task_${ctx.suffix}`,
|
||||
slug: "my-task",
|
||||
filePath: "index.ts",
|
||||
exportName: "myTask",
|
||||
workerId: worker.id,
|
||||
runtimeEnvironmentId: ctx.environmentId,
|
||||
projectId: ctx.projectId,
|
||||
},
|
||||
});
|
||||
const queue = await prisma.taskQueue.create({
|
||||
data: {
|
||||
friendlyId: `queue_${ctx.suffix}`,
|
||||
name: "task/my-task",
|
||||
runtimeEnvironmentId: ctx.environmentId,
|
||||
projectId: ctx.projectId,
|
||||
type: "VIRTUAL",
|
||||
workers: { connect: { id: worker.id } },
|
||||
tasks: { connect: { id: task.id } },
|
||||
},
|
||||
});
|
||||
if (opts.deploy) {
|
||||
const deployment = await prisma.workerDeployment.create({
|
||||
data: {
|
||||
friendlyId: `deployment_${ctx.suffix}`,
|
||||
contentHash: worker.contentHash,
|
||||
version: worker.version,
|
||||
shortCode: `short_${ctx.suffix}`,
|
||||
imageReference: `image:${ctx.suffix}`,
|
||||
status: "DEPLOYED",
|
||||
projectId: ctx.projectId,
|
||||
environmentId: ctx.environmentId,
|
||||
workerId: worker.id,
|
||||
type: "MANAGED",
|
||||
},
|
||||
});
|
||||
if (opts.promote) {
|
||||
await prisma.workerDeploymentPromotion.create({
|
||||
data: {
|
||||
label: CURRENT_DEPLOYMENT_LABEL,
|
||||
deploymentId: deployment.id,
|
||||
environmentId: ctx.environmentId,
|
||||
},
|
||||
});
|
||||
}
|
||||
return { worker, task, queue, deployment };
|
||||
}
|
||||
return { worker, task, queue };
|
||||
}
|
||||
|
||||
describe("RunEngineControlPlaneResolver adapter", () => {
|
||||
heteroPostgresTest(
|
||||
"resolveEnv maps app ResolvedEnv onto ResolvedEngineEnv",
|
||||
async ({ prisma14 }) => {
|
||||
const { organization, project, environment } = await seedEnv(prisma14, "PRODUCTION");
|
||||
const adapter = new RunEngineControlPlaneResolver(buildAppResolver(prisma14));
|
||||
|
||||
const env = await adapter.resolveEnv(environment.id);
|
||||
expect(env).not.toBeNull();
|
||||
expect(env!.id).toBe(environment.id);
|
||||
expect(env!.type).toBe("PRODUCTION");
|
||||
expect(env!.projectId).toBe(project.id);
|
||||
expect(env!.organizationId).toBe(organization.id);
|
||||
// Nested + concurrency fields the run-engine MinimalAuthenticatedEnvironment requires.
|
||||
expect(env!.project.id).toBe(project.id);
|
||||
expect(env!.organization.id).toBe(organization.id);
|
||||
expect(env!.maximumConcurrencyLimit).toBe(9);
|
||||
expect(env!.concurrencyLimitBurstFactor.toNumber()).toBe(2);
|
||||
expect(env!.archivedAt).toBeNull();
|
||||
|
||||
expect(await adapter.resolveEnv("env_missing")).toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveWorkerVersion (deployed, no workerId) resolves the promoted MANAGED deployment",
|
||||
async ({ prisma14 }) => {
|
||||
const { project, environment, suffix } = await seedEnv(prisma14, "PRODUCTION");
|
||||
const seeded = await seedWorker(
|
||||
prisma14,
|
||||
{ projectId: project.id, environmentId: environment.id, suffix },
|
||||
{ deploy: true, promote: true }
|
||||
);
|
||||
const adapter = new RunEngineControlPlaneResolver(buildAppResolver(prisma14));
|
||||
|
||||
const version = await adapter.resolveWorkerVersion({
|
||||
environmentId: environment.id,
|
||||
type: "PRODUCTION",
|
||||
});
|
||||
expect(version).not.toBeNull();
|
||||
expect(version!.worker.id).toBe(seeded.worker.id);
|
||||
expect(version!.deployment?.id).toBe(
|
||||
"deployment" in seeded ? seeded.deployment.id : undefined
|
||||
);
|
||||
expect(version!.tasks.map((t) => t.slug)).toContain("my-task");
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveWorkerVersion (DEVELOPMENT, no workerId) resolves the most-recent worker (no deployment)",
|
||||
async ({ prisma14 }) => {
|
||||
const { project, environment, suffix } = await seedEnv(prisma14, "DEVELOPMENT");
|
||||
const seeded = await seedWorker(
|
||||
prisma14,
|
||||
{ projectId: project.id, environmentId: environment.id, suffix },
|
||||
{ deploy: false }
|
||||
);
|
||||
const adapter = new RunEngineControlPlaneResolver(buildAppResolver(prisma14));
|
||||
|
||||
const version = await adapter.resolveWorkerVersion({
|
||||
environmentId: environment.id,
|
||||
type: "DEVELOPMENT",
|
||||
});
|
||||
expect(version).not.toBeNull();
|
||||
expect(version!.worker.id).toBe(seeded.worker.id);
|
||||
expect(version!.deployment).toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"assertEnvExists (split ON) resolves for a present env, rejects for a missing one",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment } = await seedEnv(prisma14, "PRODUCTION");
|
||||
// split ON: the only mode where assertEnvExists asserts (split OFF is a no-op,
|
||||
// covered in controlPlaneResolver.server.test.ts).
|
||||
const adapter = new RunEngineControlPlaneResolver(
|
||||
buildAppResolver(prisma14, { splitEnabled: true })
|
||||
);
|
||||
|
||||
await expect(adapter.assertEnvExists(environment.id)).resolves.toBeUndefined();
|
||||
await expect(adapter.assertEnvExists("env_missing")).rejects.toThrow();
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveAuthenticatedEnv delegates to the app resolver, returns `git`, and is cached",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment } = await seedEnv(prisma14, "PRODUCTION");
|
||||
const gitMeta = { commitSha: "deadbeef", branchName: "main" };
|
||||
await prisma14.runtimeEnvironment.update({
|
||||
where: { id: environment.id },
|
||||
data: { git: gitMeta },
|
||||
});
|
||||
|
||||
// split ON so the delegated app resolver caches; the counter proves the second call
|
||||
// is a cache hit rather than re-querying $replica directly (the pre-fix behavior).
|
||||
const { client: counting, reads } = countQueries(prisma14);
|
||||
const adapter = new RunEngineControlPlaneResolver(
|
||||
buildAppResolver(counting, { splitEnabled: true })
|
||||
);
|
||||
|
||||
const first = await adapter.resolveAuthenticatedEnv(environment.id);
|
||||
expect(first).not.toBeNull();
|
||||
expect(first!.id).toBe(environment.id);
|
||||
expect(first!.git).toEqual(gitMeta);
|
||||
expect(reads()).toBe(1);
|
||||
|
||||
const second = await adapter.resolveAuthenticatedEnv(environment.id);
|
||||
expect(second!.git).toEqual(gitMeta);
|
||||
expect(reads()).toBe(1);
|
||||
}
|
||||
);
|
||||
|
||||
heteroPostgresTest(
|
||||
"resolveAuthenticatedEnv returns null for a deleted project",
|
||||
async ({ prisma14 }) => {
|
||||
const { environment, project } = await seedEnv(prisma14, "PRODUCTION");
|
||||
await prisma14.project.update({
|
||||
where: { id: project.id },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
|
||||
const adapter = new RunEngineControlPlaneResolver(buildAppResolver(prisma14));
|
||||
|
||||
expect(await adapter.resolveAuthenticatedEnv(environment.id)).toBeNull();
|
||||
}
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user