550 lines
20 KiB
TypeScript
550 lines
20 KiB
TypeScript
import { describe, expect, vi } from "vitest";
|
|
|
|
// The SpanPresenter module graph imports `~/v3/runStore.server`, which imports `~/db.server`
|
|
// at load (and a large transitive graph: runEngine, eventRepository, mollifier, ...). We stub the
|
|
// two boundaries the presenter reads through so the file loads under test, then drive it entirely
|
|
// against real Postgres containers — NEVER mocking a DB client.
|
|
//
|
|
// * `~/db.server` — the module-level `prisma`/`$replica` exports. The presenter receives its
|
|
// control-plane handle through the BasePresenter constructor (`new SpanPresenter(cp, cp)`), so
|
|
// these stubs are never read on the path under test.
|
|
// * `~/v3/runStore.server` — the run-ops store singleton. This is the ONE wiring boundary we
|
|
// override: the test injects a routing-shaped store (a RoutingRunStore over the two-DB hetero
|
|
// fixture) in its place. This is a wiring override, not a DB mock — every run-ops read still
|
|
// executes against a real container.
|
|
vi.mock("~/db.server", () => ({
|
|
prisma: {},
|
|
$replica: {},
|
|
}));
|
|
|
|
const routingStoreRef = vi.hoisted(() => ({ current: undefined as unknown }));
|
|
vi.mock("~/v3/runStore.server", () => ({
|
|
get runStore() {
|
|
return routingStoreRef.current;
|
|
},
|
|
}));
|
|
|
|
import { PostgresRunStore, RoutingRunStore } from "@internal/run-store";
|
|
import type { RunStore } from "@internal/run-store";
|
|
import { heteroPostgresTest } from "@internal/testcontainers";
|
|
import type { Prisma, PrismaClient } from "@trigger.dev/database";
|
|
import { SpanPresenter } from "~/presenters/v3/SpanPresenter.server";
|
|
|
|
vi.setConfig({ testTimeout: 90_000 });
|
|
|
|
// 25-char internal id → cuid → LEGACY; v1 internal id (26 chars, version "1" at index 25) → NEW (the residency
|
|
// classifier shared with the RoutingRunStore's default `ownerEngine`).
|
|
const CUID_25 = "c".repeat(25);
|
|
const NEW_ID_26 = "k".repeat(24) + "01";
|
|
|
|
type SeedContext = {
|
|
organizationId: string;
|
|
projectId: string;
|
|
environmentId: string;
|
|
};
|
|
|
|
async function seedParents(prisma: PrismaClient, slug: string): Promise<SeedContext> {
|
|
const organization = await prisma.organization.create({
|
|
data: { title: `org-${slug}`, slug: `org-${slug}` },
|
|
});
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name: `proj-${slug}`,
|
|
slug: `proj-${slug}`,
|
|
organizationId: organization.id,
|
|
externalRef: `proj-${slug}`,
|
|
},
|
|
});
|
|
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
slug: `env-${slug}`,
|
|
type: "PRODUCTION",
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: `tr_prod_${slug}`,
|
|
pkApiKey: `pk_prod_${slug}`,
|
|
shortcode: `sc-${slug}`,
|
|
},
|
|
});
|
|
|
|
return {
|
|
organizationId: organization.id,
|
|
projectId: project.id,
|
|
environmentId: runtimeEnvironment.id,
|
|
};
|
|
}
|
|
|
|
/** Mirror the org/project/env parents onto a second DB with the SAME ids (TaskRun FKs need them
|
|
* on every DB a run is hydrated from). */
|
|
async function mirrorParents(prisma: PrismaClient, ctx: SeedContext, slug: string): Promise<void> {
|
|
await prisma.organization.create({
|
|
data: { id: ctx.organizationId, title: `org-${slug}`, slug: `org-${slug}` },
|
|
});
|
|
await prisma.project.create({
|
|
data: {
|
|
id: ctx.projectId,
|
|
name: `proj-${slug}`,
|
|
slug: `proj-${slug}`,
|
|
organizationId: ctx.organizationId,
|
|
externalRef: `proj-${slug}`,
|
|
},
|
|
});
|
|
await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
id: ctx.environmentId,
|
|
slug: `env-${slug}`,
|
|
type: "PRODUCTION",
|
|
projectId: ctx.projectId,
|
|
organizationId: ctx.organizationId,
|
|
apiKey: `tr_prod_${slug}_b`,
|
|
pkApiKey: `pk_prod_${slug}_b`,
|
|
shortcode: `sc-${slug}-b`,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function createRun(
|
|
prisma: PrismaClient,
|
|
ctx: SeedContext,
|
|
run: {
|
|
id: string;
|
|
friendlyId: string;
|
|
spanId: string;
|
|
parentSpanId?: string;
|
|
taskIdentifier?: string;
|
|
status?: Prisma.TaskRunCreateInput["status"];
|
|
parentTaskRunId?: string;
|
|
rootTaskRunId?: string;
|
|
}
|
|
) {
|
|
return prisma.taskRun.create({
|
|
data: {
|
|
id: run.id,
|
|
friendlyId: run.friendlyId,
|
|
taskIdentifier: run.taskIdentifier ?? "my-task",
|
|
status: run.status ?? "COMPLETED_SUCCESSFULLY",
|
|
payload: JSON.stringify({ foo: run.friendlyId }),
|
|
payloadType: "application/json",
|
|
traceId: `trace_${run.friendlyId}`,
|
|
spanId: run.spanId,
|
|
parentSpanId: run.parentSpanId,
|
|
parentTaskRunId: run.parentTaskRunId,
|
|
rootTaskRunId: run.rootTaskRunId,
|
|
queue: "task/my-task",
|
|
runTags: ["alpha", "beta"],
|
|
runtimeEnvironmentId: ctx.environmentId,
|
|
projectId: ctx.projectId,
|
|
organizationId: ctx.organizationId,
|
|
environmentType: "PRODUCTION",
|
|
engine: "V2",
|
|
taskEventStore: "taskEvent",
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Test-only wiring shim. In production the run-ops store's DB selection is the store's own
|
|
* concern, but `SpanPresenter` still passes `this._replica`/`this._prisma` (the control-plane
|
|
* handle) as the explicit `client` arg to `runStore.findRun`/`findRuns`. `PostgresRunStore`
|
|
* honours an explicit client (`client ?? this.readOnlyPrisma`), so without this shim a run-ops
|
|
* read would execute against the control-plane DB. Reconciling that explicit-client override
|
|
* with split routing is the job of the runStore.server.ts wiring seam, explicitly OUT of this
|
|
* unit's scope. The shim represents that reconciliation: it drops the
|
|
* presenter's client arg so each underlying PostgresRunStore reads from its OWN bound DB — the
|
|
* residency-routed behaviour the presenter will inherit once the seam is wired. It fakes ONLY the
|
|
* client wiring; every DB read still hits a real container.
|
|
*/
|
|
function ownDbStore(prisma: PrismaClient): RunStore {
|
|
const inner = new PostgresRunStore({ prisma, readOnlyPrisma: prisma });
|
|
return new Proxy(inner, {
|
|
get(target, prop) {
|
|
if (prop === "findRun" || prop === "findRuns") {
|
|
return (...args: unknown[]) => {
|
|
// Strip a trailing explicit `client` arg so the store reads from its own DB.
|
|
const stripped = stripTrailingClient(prop, args);
|
|
return (target[prop] as (...a: unknown[]) => unknown).apply(target, stripped);
|
|
};
|
|
}
|
|
const value = Reflect.get(target, prop, target);
|
|
return typeof value === "function" ? value.bind(target) : value;
|
|
},
|
|
}) as unknown as RunStore;
|
|
}
|
|
|
|
function stripTrailingClient(method: "findRun" | "findRuns", args: unknown[]): unknown[] {
|
|
// findRun(where, argsOrClient?, client?) ; findRuns(args, client?). The last arg is the
|
|
// presenter's explicit client when it is not a projection object.
|
|
const last = args[args.length - 1] as { select?: unknown; include?: unknown } | undefined;
|
|
const isProjection =
|
|
typeof last === "object" && last !== null && ("select" in last || "include" in last);
|
|
if (args.length === 0 || isProjection) {
|
|
return args;
|
|
}
|
|
return args.slice(0, -1);
|
|
}
|
|
|
|
/** A read-only client wrapper: throws on any write, asserting the legacy slot is replica-only. */
|
|
function asReplica(prisma: PrismaClient): PrismaClient {
|
|
return new Proxy(prisma, {
|
|
get(target, prop, receiver) {
|
|
if (prop === "taskRun") {
|
|
return new Proxy((target as any).taskRun, {
|
|
get(trTarget, trProp) {
|
|
if (
|
|
["create", "update", "updateMany", "upsert", "delete", "deleteMany"].includes(
|
|
String(trProp)
|
|
)
|
|
) {
|
|
return () => {
|
|
throw new Error(`legacy slot is read-replica-only; ${String(trProp)} is forbidden`);
|
|
};
|
|
}
|
|
return (trTarget as any)[trProp];
|
|
},
|
|
});
|
|
}
|
|
return Reflect.get(target, prop, receiver);
|
|
},
|
|
}) as unknown as PrismaClient;
|
|
}
|
|
|
|
describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => {
|
|
// Span detail resolves run + children through the run-ops store, region/schedule/session
|
|
// on control-plane, no cross-DB join.
|
|
heteroPostgresTest(
|
|
"findRun hydrates the run through the run-ops store (new-first) and the children-by-parentSpanId set; region/schedule/session resolve from the control-plane client",
|
|
async ({ prisma14, prisma17 }) => {
|
|
// prisma17 = NEW run-ops; prisma14 = LEGACY run-ops replica AND, for this partition proof,
|
|
// the control-plane DB (a physically distinct DB from the NEW run-ops store).
|
|
const cp = prisma14;
|
|
|
|
// Seed the env/project/org parents on BOTH run-ops DBs (FKs) and on the CP DB.
|
|
const ctxNew = await seedParents(prisma17, "partn");
|
|
await mirrorParents(prisma14, ctxNew, "partn"); // legacy run-ops + CP parents share ids
|
|
|
|
const runId = `run_${NEW_ID_26}`; // run-ops id → NEW residency
|
|
const childMigratedId = `run_a${NEW_ID_26.slice(1)}`; // also NEW
|
|
const parentFriendlyId = `run_p${NEW_ID_26.slice(1)}`; // v1 body → routes NEW by friendlyId
|
|
await createRun(prisma17, ctxNew, {
|
|
id: runId,
|
|
friendlyId: parentFriendlyId,
|
|
spanId: "span_parent",
|
|
taskIdentifier: "parent-task",
|
|
});
|
|
// A child whose parentSpanId points at the parent's span — lives on NEW.
|
|
await createRun(prisma17, ctxNew, {
|
|
id: childMigratedId,
|
|
friendlyId: "run_child_new",
|
|
spanId: "span_child_new",
|
|
parentSpanId: "span_parent",
|
|
parentTaskRunId: runId,
|
|
taskIdentifier: "child-task",
|
|
});
|
|
|
|
// Control-plane rows live on the CP DB only.
|
|
const workerGroup = await cp.workerInstanceGroup.create({
|
|
data: {
|
|
name: "us-east-1-group",
|
|
location: "N. Virginia, USA",
|
|
masterQueue: "main",
|
|
type: "MANAGED",
|
|
token: { create: { tokenHash: `tok-${ctxNew.projectId}` } },
|
|
},
|
|
});
|
|
const schedule = await cp.taskSchedule.create({
|
|
data: {
|
|
friendlyId: "sched_1234",
|
|
taskIdentifier: "parent-task",
|
|
projectId: ctxNew.projectId,
|
|
deduplicationKey: "dedup-1",
|
|
type: "DECLARATIVE",
|
|
generatorExpression: "0 * * * *",
|
|
generatorDescription: "every hour",
|
|
timezone: "UTC",
|
|
},
|
|
});
|
|
|
|
routingStoreRef.current = new RoutingRunStore({
|
|
new: ownDbStore(prisma17),
|
|
legacy: ownDbStore(prisma14),
|
|
});
|
|
|
|
const presenter = new SpanPresenter(cp, cp);
|
|
|
|
// (a) run hydrated through the run-ops store (NEW), byte-identical to the source row incl.
|
|
// the run-ops self-relations.
|
|
const run = await presenter.findRun({
|
|
originalRunId: parentFriendlyId,
|
|
spanId: "span_parent",
|
|
environmentId: ctxNew.environmentId,
|
|
});
|
|
expect(run?.id).toBe(runId);
|
|
expect(run?.friendlyId).toBe(parentFriendlyId);
|
|
expect(run?.taskIdentifier).toBe("parent-task");
|
|
expect(run?.runTags).toEqual(["alpha", "beta"]);
|
|
// Nested run-ops self-relation resolved on the same (NEW) store.
|
|
expect(run?.parentTaskRun).toBeNull();
|
|
|
|
// (b) the run does NOT exist on the CP DB — the run-ops read could only have come from the
|
|
// run-ops store, never a CP join.
|
|
expect(await cp.taskRun.findFirst({ where: { friendlyId: parentFriendlyId } })).toBeNull();
|
|
|
|
// (c) the control-plane standalone reads resolve from the CP client.
|
|
const region = await cp.workerInstanceGroup.findFirst({ where: { masterQueue: "main" } });
|
|
expect(region?.name).toBe(workerGroup.name);
|
|
expect(await presenter.resolveSchedule(schedule.id)).toMatchObject({
|
|
friendlyId: "sched_1234",
|
|
timezone: "UTC",
|
|
});
|
|
}
|
|
);
|
|
|
|
// Children set served by runStore.findRuns through the routing store.
|
|
heteroPostgresTest(
|
|
"triggeredRuns (children-by-parentSpanId) is served by runStore.findRuns with the presenter's exact select",
|
|
async ({ prisma14, prisma17 }) => {
|
|
const ctx = await seedParents(prisma17, "kids");
|
|
|
|
await createRun(prisma17, ctx, {
|
|
id: `run_${NEW_ID_26}`,
|
|
friendlyId: "run_parent2",
|
|
spanId: "span_p2",
|
|
});
|
|
await createRun(prisma17, ctx, {
|
|
id: `run_b${NEW_ID_26.slice(1)}`,
|
|
friendlyId: "run_kid_a",
|
|
spanId: "span_kid_a",
|
|
parentSpanId: "span_p2",
|
|
});
|
|
await createRun(prisma17, ctx, {
|
|
id: `run_c${NEW_ID_26.slice(1)}`,
|
|
friendlyId: "run_kid_b",
|
|
spanId: "span_kid_b",
|
|
parentSpanId: "span_p2",
|
|
});
|
|
|
|
const store = new RoutingRunStore({
|
|
new: ownDbStore(prisma17),
|
|
legacy: ownDbStore(prisma14),
|
|
});
|
|
|
|
const triggeredRuns = await store.findRuns({
|
|
where: { parentSpanId: "span_p2" },
|
|
select: {
|
|
friendlyId: true,
|
|
taskIdentifier: true,
|
|
spanId: true,
|
|
createdAt: true,
|
|
status: true,
|
|
},
|
|
});
|
|
|
|
expect(triggeredRuns.map((r) => r.friendlyId).sort()).toEqual(["run_kid_a", "run_kid_b"]);
|
|
// select projection holds: no `id`/`payload` leaked through.
|
|
expect(triggeredRuns[0]).not.toHaveProperty("id");
|
|
expect(triggeredRuns[0]).not.toHaveProperty("payload");
|
|
}
|
|
);
|
|
|
|
// Old in-retention run served from the legacy replica, never the primary.
|
|
heteroPostgresTest(
|
|
"a legacy-residency run resolves through the store's LEGACY slot, which exposes only a replica handle",
|
|
async ({ prisma14, prisma17 }) => {
|
|
const ctx = await seedParents(prisma14, "legacy");
|
|
|
|
const legacyRunId = `run_${CUID_25}`; // cuid → LEGACY residency
|
|
await createRun(prisma14, ctx, {
|
|
id: legacyRunId,
|
|
friendlyId: "run_legacy",
|
|
spanId: "span_legacy",
|
|
taskIdentifier: "legacy-task",
|
|
});
|
|
|
|
// The LEGACY slot is wired over a replica (read-only) handle; the NEW slot over the new DB.
|
|
const store = new RoutingRunStore({
|
|
new: ownDbStore(prisma17),
|
|
legacy: ownDbStore(asReplica(prisma14)),
|
|
});
|
|
|
|
// Routed by `id` residency (cuid → LEGACY). The presenter's findRun keys by friendlyId/spanId
|
|
// (which route NEW-default through the store today); routing by id is the store-level proof
|
|
// that the LEGACY slot serves in-retention runs. The legacy slot's replica handle forbids
|
|
// writes — proving the read route can never touch a legacy writer.
|
|
const found = await store.findRun(
|
|
{ id: legacyRunId },
|
|
{ select: { id: true, friendlyId: true, taskIdentifier: true } }
|
|
);
|
|
expect(found?.id).toBe(legacyRunId);
|
|
expect(found?.taskIdentifier).toBe("legacy-task");
|
|
}
|
|
);
|
|
|
|
// A known-migrated run is not re-probed on legacy.
|
|
heteroPostgresTest(
|
|
"a NEW-residency id is served by the NEW slot and the LEGACY slot is never invoked",
|
|
async ({ prisma14, prisma17 }) => {
|
|
const ctx = await seedParents(prisma17, "knownmig");
|
|
|
|
const newRunId = `run_${NEW_ID_26}`; // run-ops id → NEW residency
|
|
await createRun(prisma17, ctx, {
|
|
id: newRunId,
|
|
friendlyId: "run_known_new",
|
|
spanId: "span_known_new",
|
|
taskIdentifier: "new-task",
|
|
});
|
|
|
|
// LEGACY slot throws on ANY read — asserting the residency short-circuit never probes it.
|
|
const legacyThrows = new Proxy({} as RunStore, {
|
|
get(_t, prop) {
|
|
if (prop === "findRun" || prop === "findRuns") {
|
|
return () => {
|
|
throw new Error(`LEGACY slot must not be probed for a NEW id (${String(prop)})`);
|
|
};
|
|
}
|
|
return undefined;
|
|
},
|
|
});
|
|
|
|
const store = new RoutingRunStore({
|
|
new: ownDbStore(prisma17),
|
|
legacy: legacyThrows,
|
|
});
|
|
|
|
const found = await store.findRun(
|
|
{ id: newRunId },
|
|
{ select: { id: true, taskIdentifier: true } }
|
|
);
|
|
expect(found?.id).toBe(newRunId);
|
|
expect(found?.taskIdentifier).toBe("new-task");
|
|
}
|
|
);
|
|
|
|
// Passthrough (single-DB): NEW and LEGACY slots are the same store over one client.
|
|
heteroPostgresTest(
|
|
"single-DB collapses both slots to one PostgresRunStore; the presenter resolves run + children + control-plane from the one client",
|
|
async ({ prisma14 }) => {
|
|
const cp = prisma14;
|
|
const ctx = await seedParents(prisma14, "passthru");
|
|
|
|
const runId = `run_${NEW_ID_26}`;
|
|
await createRun(prisma14, ctx, {
|
|
id: runId,
|
|
friendlyId: "run_solo",
|
|
spanId: "span_solo",
|
|
taskIdentifier: "solo-task",
|
|
});
|
|
await createRun(prisma14, ctx, {
|
|
id: `run_d${NEW_ID_26.slice(1)}`,
|
|
friendlyId: "run_solo_kid",
|
|
spanId: "span_solo_kid",
|
|
parentSpanId: "span_solo",
|
|
});
|
|
const schedule = await cp.taskSchedule.create({
|
|
data: {
|
|
friendlyId: "sched_solo",
|
|
taskIdentifier: "solo-task",
|
|
projectId: ctx.projectId,
|
|
deduplicationKey: "dedup-solo",
|
|
type: "DECLARATIVE",
|
|
generatorExpression: "0 * * * *",
|
|
generatorDescription: "every hour",
|
|
timezone: "UTC",
|
|
},
|
|
});
|
|
|
|
// Both slots are the same store over the one client — the single-DB collapse.
|
|
const solo = ownDbStore(prisma14);
|
|
routingStoreRef.current = new RoutingRunStore({ new: solo, legacy: solo });
|
|
|
|
const presenter = new SpanPresenter(cp, cp);
|
|
|
|
const run = await presenter.findRun({
|
|
originalRunId: "run_solo",
|
|
spanId: "span_solo",
|
|
environmentId: ctx.environmentId,
|
|
});
|
|
expect(run?.id).toBe(runId);
|
|
expect(run?.taskIdentifier).toBe("solo-task");
|
|
|
|
// Children resolve from the same single store.
|
|
const children = await (routingStoreRef.current as RunStore).findRuns({
|
|
where: { parentSpanId: "span_solo" },
|
|
select: {
|
|
friendlyId: true,
|
|
taskIdentifier: true,
|
|
spanId: true,
|
|
createdAt: true,
|
|
status: true,
|
|
},
|
|
});
|
|
expect(children.map((c) => c.friendlyId)).toEqual(["run_solo_kid"]);
|
|
|
|
// Control-plane read from the same single client.
|
|
expect(await presenter.resolveSchedule(schedule.id)).toMatchObject({
|
|
friendlyId: "sched_solo",
|
|
});
|
|
}
|
|
);
|
|
|
|
// Cross-seam tree shape: parent on LEGACY (in-retention), child on NEW (born-new).
|
|
heteroPostgresTest(
|
|
"parent run on the legacy replica, child run on new — relations resolve across the seam, no cross-DB join",
|
|
async ({ prisma14, prisma17 }) => {
|
|
const ctx = await seedParents(prisma14, "e2e4");
|
|
await mirrorParents(prisma17, ctx, "e2e4");
|
|
|
|
const parentId = `run_${CUID_25}`; // cuid → LEGACY (in-retention)
|
|
const childId = `run_${NEW_ID_26}`; // run-ops id → NEW (born-new)
|
|
|
|
await createRun(prisma14, ctx, {
|
|
id: parentId,
|
|
friendlyId: "run_e2e_parent",
|
|
spanId: "span_e2e_parent",
|
|
taskIdentifier: "parent",
|
|
rootTaskRunId: parentId,
|
|
});
|
|
// The child lives on NEW; it links to the parent across the seam ONLY by `parentSpanId`
|
|
// (a plain indexed column — the exact key `triggeredRuns` uses), NOT by a cross-DB FK
|
|
// (`parentTaskRunId`/`rootTaskRunId` would violate the FK since the parent is on LEGACY;
|
|
// a tree's FK self-relations stay single-DB).
|
|
await createRun(prisma17, ctx, {
|
|
id: childId,
|
|
friendlyId: "run_e2e_child",
|
|
spanId: "span_e2e_child",
|
|
parentSpanId: "span_e2e_parent",
|
|
taskIdentifier: "child",
|
|
});
|
|
|
|
const store = new RoutingRunStore({
|
|
new: ownDbStore(prisma17),
|
|
legacy: ownDbStore(asReplica(prisma14)),
|
|
});
|
|
|
|
// The parent resolves from the LEGACY slot (routed by its cuid id).
|
|
const parent = await store.findRun(
|
|
{ id: parentId },
|
|
{
|
|
select: {
|
|
id: true,
|
|
friendlyId: true,
|
|
rootTaskRun: { select: { friendlyId: true } },
|
|
},
|
|
}
|
|
);
|
|
expect(parent?.id).toBe(parentId);
|
|
// Run-ops self-relation (rootTaskRun) resolves on the parent's own (LEGACY) store — a
|
|
// tree's FK self-relations stay single-DB.
|
|
expect(parent?.rootTaskRun?.friendlyId).toBe("run_e2e_parent");
|
|
|
|
// The child resolves from the NEW slot (routed by its run-ops id) and points back at the parent
|
|
// span — the cross-the-line parent/child shape, with no cross-DB join.
|
|
const child = await store.findRun(
|
|
{ id: childId },
|
|
{ select: { id: true, parentSpanId: true, friendlyId: true } }
|
|
);
|
|
expect(child?.id).toBe(childId);
|
|
expect(child?.parentSpanId).toBe("span_e2e_parent");
|
|
}
|
|
);
|
|
});
|