626 lines
22 KiB
TypeScript
626 lines
22 KiB
TypeScript
import { containerTest, heteroPostgresTest } from "@internal/testcontainers";
|
|
import { PostgresRunStore } from "@internal/run-store";
|
|
import type { Prisma, PrismaClient } from "@trigger.dev/database";
|
|
import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic";
|
|
import { beforeEach, describe, expect, vi } from "vitest";
|
|
|
|
// `resolveSchedule` reads the module-level `prisma` (control-plane handle).
|
|
// Point `~/db.server`'s handles at a hoisted mutable holder each test sets to a
|
|
// real container client. Not a DB mock: reads still hit a real Postgres
|
|
// container — this only redirects which real client the module resolves.
|
|
const dbHolder = vi.hoisted(() => ({
|
|
prisma: null as PrismaClient | null,
|
|
$replica: null as PrismaClient | null,
|
|
}));
|
|
|
|
vi.mock("~/db.server", () => ({
|
|
get prisma() {
|
|
return dbHolder.prisma;
|
|
},
|
|
get $replica() {
|
|
return dbHolder.$replica;
|
|
},
|
|
}));
|
|
|
|
// Inert: payloads are inline JSON, never `application/store`, so no presign.
|
|
vi.mock("~/v3/objectStore.server", () => ({
|
|
generatePresignedUrl: vi.fn(async () => ({ success: false, error: "not-used" })),
|
|
}));
|
|
|
|
import { ApiRetrieveRunPresenter } from "~/presenters/v3/ApiRetrieveRunPresenter.server";
|
|
import type { FoundRun } from "~/presenters/v3/ApiRetrieveRunPresenter.server";
|
|
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
|
import { CURRENT_API_VERSION } from "~/api/versions";
|
|
|
|
vi.setConfig({ testTimeout: 60_000 });
|
|
|
|
// The presenter's EXACT read shape — pinned so a refactor that changes
|
|
// `findRun`'s `where`/`select` must update this test in lockstep.
|
|
const commonRunSelect = {
|
|
id: true,
|
|
friendlyId: true,
|
|
status: true,
|
|
taskIdentifier: true,
|
|
createdAt: true,
|
|
startedAt: true,
|
|
updatedAt: true,
|
|
completedAt: true,
|
|
expiredAt: true,
|
|
delayUntil: true,
|
|
metadata: true,
|
|
metadataType: true,
|
|
ttl: true,
|
|
costInCents: true,
|
|
baseCostInCents: true,
|
|
usageDurationMs: true,
|
|
idempotencyKey: true,
|
|
idempotencyKeyOptions: true,
|
|
isTest: true,
|
|
depth: true,
|
|
scheduleId: true,
|
|
workerQueue: true,
|
|
region: true,
|
|
lockedToVersionId: true,
|
|
resumeParentOnCompletion: true,
|
|
batch: { select: { id: true, friendlyId: true } },
|
|
runTags: true,
|
|
} satisfies Prisma.TaskRunSelect;
|
|
|
|
const findRunSelect = {
|
|
...commonRunSelect,
|
|
traceId: true,
|
|
payload: true,
|
|
payloadType: true,
|
|
output: true,
|
|
outputType: true,
|
|
error: true,
|
|
attempts: { select: { id: true } },
|
|
attemptNumber: true,
|
|
engine: true,
|
|
taskEventStore: true,
|
|
parentTaskRun: { select: commonRunSelect },
|
|
rootTaskRun: { select: commonRunSelect },
|
|
childRuns: { select: commonRunSelect },
|
|
} satisfies Prisma.TaskRunSelect;
|
|
|
|
// Drive the read exactly as `findRun` does: the RunStore.findRun contract over
|
|
// the given store with the presenter's where+select. The scalar `lockedToVersionId`
|
|
// folds to a resolved `lockedToVersion` per node, matching the presenter's shape;
|
|
// seeded runs carry no locked version, so every node resolves to null.
|
|
async function readFoundRunViaStore(
|
|
store: PostgresRunStore,
|
|
friendlyId: string,
|
|
runtimeEnvironmentId: string
|
|
): Promise<FoundRun | null> {
|
|
const pgRow = (await store.findRun(
|
|
{ friendlyId, runtimeEnvironmentId },
|
|
{ select: findRunSelect }
|
|
)) as Record<string, any> | null;
|
|
if (!pgRow) return null;
|
|
const foldVersion = (run: Record<string, any>) => ({ ...run, lockedToVersion: null });
|
|
return {
|
|
...pgRow,
|
|
lockedToVersion: null,
|
|
parentTaskRun: pgRow.parentTaskRun ? foldVersion(pgRow.parentTaskRun) : null,
|
|
rootTaskRun: pgRow.rootTaskRun ? foldVersion(pgRow.rootTaskRun) : null,
|
|
childRuns: (pgRow.childRuns ?? []).map(foldVersion),
|
|
isBuffered: false,
|
|
} as FoundRun;
|
|
}
|
|
|
|
async function seedOrgProjectEnv(prisma: PrismaClient, suffix: string) {
|
|
const organization = await prisma.organization.create({
|
|
data: { title: `test-${suffix}`, slug: `test-${suffix}` },
|
|
});
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name: `test-${suffix}`,
|
|
slug: `test-${suffix}`,
|
|
organizationId: organization.id,
|
|
externalRef: `test-${suffix}`,
|
|
},
|
|
});
|
|
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
slug: `test-${suffix}`,
|
|
type: "DEVELOPMENT",
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: `tr_dev_${suffix}`,
|
|
pkApiKey: `pk_dev_${suffix}`,
|
|
shortcode: `short-${suffix}`,
|
|
},
|
|
});
|
|
return { organization, project, runtimeEnvironment };
|
|
}
|
|
|
|
// Build the AuthenticatedEnvironment shape `call()` reads (id, organizationId,
|
|
// slug, project.externalRef). Only those fields are touched on the happy path.
|
|
function authEnv(
|
|
organization: { id: string },
|
|
project: { id: string; externalRef: string },
|
|
runtimeEnvironment: { id: string; slug: string }
|
|
): AuthenticatedEnvironment {
|
|
return {
|
|
id: runtimeEnvironment.id,
|
|
slug: runtimeEnvironment.slug,
|
|
organizationId: organization.id,
|
|
organization: { id: organization.id },
|
|
project: { id: project.id, externalRef: project.externalRef },
|
|
} as unknown as AuthenticatedEnvironment;
|
|
}
|
|
|
|
interface SeedRunOpts {
|
|
id: string;
|
|
friendlyId: string;
|
|
runtimeEnvironmentId: string;
|
|
projectId: string;
|
|
organizationId: string;
|
|
scheduleId?: string;
|
|
runTags?: string[];
|
|
parentTaskRunId?: string;
|
|
rootTaskRunId?: string;
|
|
metadata?: string;
|
|
}
|
|
|
|
async function seedRun(prisma: PrismaClient, opts: SeedRunOpts) {
|
|
return prisma.taskRun.create({
|
|
data: {
|
|
id: opts.id,
|
|
friendlyId: opts.friendlyId,
|
|
taskIdentifier: "my-task",
|
|
payload: JSON.stringify({ hello: "world" }),
|
|
payloadType: "application/json",
|
|
traceId: `trace_${opts.id}`,
|
|
spanId: `span_${opts.id}`,
|
|
queue: "task/my-task",
|
|
runtimeEnvironmentId: opts.runtimeEnvironmentId,
|
|
projectId: opts.projectId,
|
|
organizationId: opts.organizationId,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
runTags: opts.runTags ?? [],
|
|
scheduleId: opts.scheduleId,
|
|
parentTaskRunId: opts.parentTaskRunId,
|
|
rootTaskRunId: opts.rootTaskRunId,
|
|
metadata: opts.metadata,
|
|
metadataType: opts.metadata ? "application/json" : undefined,
|
|
},
|
|
});
|
|
}
|
|
|
|
// A TaskRunAttempt requires the BackgroundWorker -> BackgroundWorkerTask ->
|
|
// TaskQueue FK chain; seed the minimum of each for one attempt.
|
|
async function seedAttempt(
|
|
prisma: PrismaClient,
|
|
opts: { runId: string; runtimeEnvironmentId: string; projectId: string; suffix: string }
|
|
) {
|
|
const worker = await prisma.backgroundWorker.create({
|
|
data: {
|
|
friendlyId: `worker_${opts.runId}`,
|
|
contentHash: `hash_${opts.suffix}`,
|
|
version: "20260627.1",
|
|
metadata: {},
|
|
projectId: opts.projectId,
|
|
runtimeEnvironmentId: opts.runtimeEnvironmentId,
|
|
},
|
|
});
|
|
const task = await prisma.backgroundWorkerTask.create({
|
|
data: {
|
|
friendlyId: `task_${opts.runId}`,
|
|
slug: "my-task",
|
|
filePath: "src/trigger/my-task.ts",
|
|
workerId: worker.id,
|
|
projectId: opts.projectId,
|
|
runtimeEnvironmentId: opts.runtimeEnvironmentId,
|
|
},
|
|
});
|
|
const queue = await prisma.taskQueue.create({
|
|
data: {
|
|
friendlyId: `queue_${opts.runId}`,
|
|
name: "task/my-task",
|
|
projectId: opts.projectId,
|
|
runtimeEnvironmentId: opts.runtimeEnvironmentId,
|
|
},
|
|
});
|
|
return prisma.taskRunAttempt.create({
|
|
data: {
|
|
friendlyId: `attempt_${opts.runId}`,
|
|
number: 1,
|
|
taskRunId: opts.runId,
|
|
runtimeEnvironmentId: opts.runtimeEnvironmentId,
|
|
backgroundWorkerId: worker.id,
|
|
backgroundWorkerTaskId: task.id,
|
|
queueId: queue.id,
|
|
status: "EXECUTING",
|
|
},
|
|
});
|
|
}
|
|
|
|
// Seed a run plus a parent, a root, a child and one attempt — the tree that must
|
|
// round-trip. `seedTestRun.ts` only seeds a single root run, so the tree + attempt
|
|
// rows are created inline here.
|
|
async function seedRunWithTree(
|
|
prisma: PrismaClient,
|
|
base: {
|
|
runtimeEnvironmentId: string;
|
|
projectId: string;
|
|
organizationId: string;
|
|
suffix: string;
|
|
}
|
|
) {
|
|
const parentId = generateRunOpsId();
|
|
const rootId = generateRunOpsId();
|
|
const runId = generateRunOpsId();
|
|
const childId = generateRunOpsId();
|
|
|
|
await seedRun(prisma, {
|
|
id: rootId,
|
|
friendlyId: `run_${rootId}`,
|
|
runtimeEnvironmentId: base.runtimeEnvironmentId,
|
|
projectId: base.projectId,
|
|
organizationId: base.organizationId,
|
|
});
|
|
await seedRun(prisma, {
|
|
id: parentId,
|
|
friendlyId: `run_${parentId}`,
|
|
runtimeEnvironmentId: base.runtimeEnvironmentId,
|
|
projectId: base.projectId,
|
|
organizationId: base.organizationId,
|
|
rootTaskRunId: rootId,
|
|
});
|
|
|
|
const run = await seedRun(prisma, {
|
|
id: runId,
|
|
friendlyId: `run_${runId}`,
|
|
runtimeEnvironmentId: base.runtimeEnvironmentId,
|
|
projectId: base.projectId,
|
|
organizationId: base.organizationId,
|
|
parentTaskRunId: parentId,
|
|
rootTaskRunId: rootId,
|
|
runTags: ["alpha", "beta"],
|
|
metadata: JSON.stringify({ k: base.suffix }),
|
|
});
|
|
|
|
await seedRun(prisma, {
|
|
id: childId,
|
|
friendlyId: `run_${childId}`,
|
|
runtimeEnvironmentId: base.runtimeEnvironmentId,
|
|
projectId: base.projectId,
|
|
organizationId: base.organizationId,
|
|
parentTaskRunId: runId,
|
|
rootTaskRunId: rootId,
|
|
});
|
|
|
|
const attempt = await seedAttempt(prisma, {
|
|
runId,
|
|
runtimeEnvironmentId: base.runtimeEnvironmentId,
|
|
projectId: base.projectId,
|
|
suffix: base.suffix,
|
|
});
|
|
|
|
return {
|
|
run,
|
|
runFriendlyId: `run_${runId}`,
|
|
parentFriendlyId: `run_${parentId}`,
|
|
rootFriendlyId: `run_${rootId}`,
|
|
childFriendlyId: `run_${childId}`,
|
|
attemptId: attempt.id,
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
dbHolder.prisma = null;
|
|
dbHolder.$replica = null;
|
|
});
|
|
|
|
describe("ApiRetrieveRunPresenter.findRun store-routed read (single-DB invariant)", () => {
|
|
containerTest(
|
|
"returns run + attempts + tree from the store read; resolveSchedule reads control-plane prisma",
|
|
async ({ prisma }) => {
|
|
// Single-DB shape: one PostgresRunStore over the one prisma/replica pair,
|
|
// exactly as the production `runStore.server.ts` singleton constructs it.
|
|
dbHolder.prisma = prisma;
|
|
dbHolder.$replica = prisma;
|
|
const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma });
|
|
|
|
const { project, organization, runtimeEnvironment } = await seedOrgProjectEnv(
|
|
prisma,
|
|
"single"
|
|
);
|
|
|
|
// Control-plane schedule on the SAME single client.
|
|
const scheduleId = generateRunOpsId();
|
|
await prisma.taskSchedule.create({
|
|
data: {
|
|
id: scheduleId,
|
|
friendlyId: `sched_${scheduleId}`,
|
|
externalId: "my-external-schedule",
|
|
taskIdentifier: "my-task",
|
|
generatorExpression: "0 * * * *",
|
|
generatorDescription: "Every hour",
|
|
projectId: project.id,
|
|
},
|
|
});
|
|
|
|
const tree = await seedRunWithTree(prisma, {
|
|
runtimeEnvironmentId: runtimeEnvironment.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
suffix: "single",
|
|
});
|
|
|
|
await prisma.taskRun.update({
|
|
where: { id: tree.run.id },
|
|
data: { scheduleId },
|
|
});
|
|
|
|
const env = authEnv(organization, project, runtimeEnvironment);
|
|
const found = await readFoundRunViaStore(store, tree.runFriendlyId, env.id);
|
|
|
|
expect(found).not.toBeNull();
|
|
expect(found!.isBuffered).toBe(false);
|
|
expect(found!.friendlyId).toBe(tree.runFriendlyId);
|
|
expect(found!.parentTaskRun?.friendlyId).toBe(tree.parentFriendlyId);
|
|
expect(found!.rootTaskRun?.friendlyId).toBe(tree.rootFriendlyId);
|
|
expect(found!.childRuns.map((c) => c.friendlyId)).toEqual([tree.childFriendlyId]);
|
|
expect(found!.attempts.map((a) => a.id)).toEqual([tree.attemptId]);
|
|
expect([...found!.runTags].sort()).toEqual(["alpha", "beta"]);
|
|
|
|
// Drive the full presenter `call()` — exercises the real control-plane
|
|
// `resolveSchedule` against the module `prisma` (the container client).
|
|
const out = await new ApiRetrieveRunPresenter(CURRENT_API_VERSION).call(found!, env);
|
|
|
|
expect(out.schedule).toBeDefined();
|
|
expect(out.schedule?.externalId).toBe("my-external-schedule");
|
|
expect(out.schedule?.generator.expression).toBe("0 * * * *");
|
|
expect(out.relatedRuns.parent?.id).toBe(tree.parentFriendlyId);
|
|
expect(out.relatedRuns.root?.id).toBe(tree.rootFriendlyId);
|
|
expect(out.relatedRuns.children.map((c) => c.id)).toEqual([tree.childFriendlyId]);
|
|
expect(out.attemptCount).toBe(found!.attemptNumber ?? 0);
|
|
}
|
|
);
|
|
|
|
containerTest(
|
|
"resolveSchedule re-reads TaskSchedule off the control-plane prisma on every call (no caching)",
|
|
async ({ prisma }) => {
|
|
// Single-DB: this proves resolveSchedule re-reads `prisma.taskSchedule`
|
|
// on each call() and reflects a delete (no stale cache). The structural
|
|
// "schedule comes from the control-plane client, not the run-ops store"
|
|
// separation is proven by the cross-DB test below (distinct schedule
|
|
// client + an onLegacy=null check); in single-DB both views are the
|
|
// same physical row, so this case cannot discriminate the two paths.
|
|
dbHolder.prisma = prisma;
|
|
dbHolder.$replica = prisma;
|
|
const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma });
|
|
|
|
const { project, organization, runtimeEnvironment } = await seedOrgProjectEnv(
|
|
prisma,
|
|
"cp-inv"
|
|
);
|
|
|
|
const scheduleId = generateRunOpsId();
|
|
await prisma.taskSchedule.create({
|
|
data: {
|
|
id: scheduleId,
|
|
friendlyId: `sched_${scheduleId}`,
|
|
taskIdentifier: "my-task",
|
|
generatorExpression: "*/5 * * * *",
|
|
projectId: project.id,
|
|
},
|
|
});
|
|
|
|
const runId = generateRunOpsId();
|
|
await seedRun(prisma, {
|
|
id: runId,
|
|
friendlyId: `run_${runId}`,
|
|
runtimeEnvironmentId: runtimeEnvironment.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
scheduleId,
|
|
});
|
|
|
|
const env = authEnv(organization, project, runtimeEnvironment);
|
|
const found = await readFoundRunViaStore(store, `run_${runId}`, env.id);
|
|
expect(found).not.toBeNull();
|
|
expect(found!.scheduleId).toBe(scheduleId);
|
|
|
|
const presenter = new ApiRetrieveRunPresenter(CURRENT_API_VERSION);
|
|
const out = await presenter.call(found!, env);
|
|
expect(out.schedule?.id).toBe(`sched_${scheduleId}`);
|
|
|
|
// Delete the control-plane TaskSchedule row; the run-ops row is
|
|
// untouched. resolveSchedule must now return undefined — proving the
|
|
// schedule comes from `prisma.taskSchedule`, NOT from the run-ops store.
|
|
await prisma.taskSchedule.delete({ where: { id: scheduleId } });
|
|
const out2 = await presenter.call(found!, env);
|
|
expect(out2.schedule).toBeUndefined();
|
|
}
|
|
);
|
|
});
|
|
|
|
describe("ApiRetrieveRunPresenter.findRun cross-version read (PG14 + PG17)", () => {
|
|
heteroPostgresTest(
|
|
"single retrieve returns run + attempts + tree byte-identically from NEW (PG17) and LEGACY (PG14) stores",
|
|
async ({ prisma17, prisma14 }) => {
|
|
const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 });
|
|
const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 });
|
|
|
|
// NEW residency subgraph on PG17.
|
|
const newEnv = await seedOrgProjectEnv(prisma17, "new");
|
|
const newTree = await seedRunWithTree(prisma17, {
|
|
runtimeEnvironmentId: newEnv.runtimeEnvironment.id,
|
|
projectId: newEnv.project.id,
|
|
organizationId: newEnv.organization.id,
|
|
suffix: "new",
|
|
});
|
|
|
|
// Distinct LEGACY residency subgraph on PG14.
|
|
const legacyEnv = await seedOrgProjectEnv(prisma14, "legacy");
|
|
const legacyTree = await seedRunWithTree(prisma14, {
|
|
runtimeEnvironmentId: legacyEnv.runtimeEnvironment.id,
|
|
projectId: legacyEnv.project.id,
|
|
organizationId: legacyEnv.organization.id,
|
|
suffix: "legacy",
|
|
});
|
|
|
|
// Read the NEW run from the PG17 store.
|
|
const foundNew = await readFoundRunViaStore(
|
|
newStore,
|
|
newTree.runFriendlyId,
|
|
newEnv.runtimeEnvironment.id
|
|
);
|
|
expect(foundNew).not.toBeNull();
|
|
expect(foundNew!.friendlyId).toBe(newTree.runFriendlyId);
|
|
expect(foundNew!.parentTaskRun?.friendlyId).toBe(newTree.parentFriendlyId);
|
|
expect(foundNew!.rootTaskRun?.friendlyId).toBe(newTree.rootFriendlyId);
|
|
expect(foundNew!.childRuns.map((c) => c.friendlyId)).toEqual([newTree.childFriendlyId]);
|
|
expect(foundNew!.attempts.map((a) => a.id)).toEqual([newTree.attemptId]);
|
|
expect([...foundNew!.runTags].sort()).toEqual(["alpha", "beta"]);
|
|
expect(foundNew!.metadata).toBe(JSON.stringify({ k: "new" }));
|
|
|
|
// Sanity: the two stores are genuinely distinct DBs — the NEW run's key
|
|
// is absent from PG14 (it was only ever written to PG17).
|
|
const leaked = await readFoundRunViaStore(
|
|
legacyStore,
|
|
newTree.runFriendlyId,
|
|
newEnv.runtimeEnvironment.id
|
|
);
|
|
expect(leaked).toBeNull();
|
|
|
|
// Read the LEGACY run from the PG14 store — byte-identical tree.
|
|
const foundLegacy = await readFoundRunViaStore(
|
|
legacyStore,
|
|
legacyTree.runFriendlyId,
|
|
legacyEnv.runtimeEnvironment.id
|
|
);
|
|
expect(foundLegacy).not.toBeNull();
|
|
expect(foundLegacy!.friendlyId).toBe(legacyTree.runFriendlyId);
|
|
expect(foundLegacy!.parentTaskRun?.friendlyId).toBe(legacyTree.parentFriendlyId);
|
|
expect(foundLegacy!.rootTaskRun?.friendlyId).toBe(legacyTree.rootFriendlyId);
|
|
expect(foundLegacy!.childRuns.map((c) => c.friendlyId)).toEqual([legacyTree.childFriendlyId]);
|
|
expect(foundLegacy!.attempts.map((a) => a.id)).toEqual([legacyTree.attemptId]);
|
|
expect(foundLegacy!.metadata).toBe(JSON.stringify({ k: "legacy" }));
|
|
}
|
|
);
|
|
|
|
heteroPostgresTest(
|
|
"schedule resolved cross-DB: run hydrated from run-ops store (PG14), schedule from a distinct control-plane client (PG17)",
|
|
async ({ prisma17, prisma14 }) => {
|
|
// Run-ops residency = LEGACY (PG14). Control-plane (TaskSchedule) lives
|
|
// on a DISTINCT client — PG17 — and is the handle the module `prisma`
|
|
// resolves to in this test. This proves the run-ops-row -> control-plane
|
|
// -schedule cross-seam read: the run comes from one DB, the schedule
|
|
// from another.
|
|
const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 });
|
|
|
|
// Module control-plane handle -> PG17.
|
|
dbHolder.prisma = prisma17;
|
|
dbHolder.$replica = prisma17;
|
|
|
|
const legacyEnv = await seedOrgProjectEnv(prisma14, "x-run");
|
|
// The control-plane project the schedule hangs off lives on PG17.
|
|
const cpEnv = await seedOrgProjectEnv(prisma17, "x-cp");
|
|
|
|
const scheduleId = generateRunOpsId();
|
|
await prisma17.taskSchedule.create({
|
|
data: {
|
|
id: scheduleId,
|
|
friendlyId: `sched_${scheduleId}`,
|
|
externalId: "cross-db-schedule",
|
|
taskIdentifier: "my-task",
|
|
generatorExpression: "0 0 * * *",
|
|
projectId: cpEnv.project.id,
|
|
},
|
|
});
|
|
|
|
const runId = generateRunOpsId();
|
|
await seedRun(prisma14, {
|
|
id: runId,
|
|
friendlyId: `run_${runId}`,
|
|
runtimeEnvironmentId: legacyEnv.runtimeEnvironment.id,
|
|
projectId: legacyEnv.project.id,
|
|
organizationId: legacyEnv.organization.id,
|
|
scheduleId,
|
|
});
|
|
|
|
const env = authEnv(legacyEnv.organization, legacyEnv.project, legacyEnv.runtimeEnvironment);
|
|
const found = await readFoundRunViaStore(
|
|
legacyStore,
|
|
`run_${runId}`,
|
|
legacyEnv.runtimeEnvironment.id
|
|
);
|
|
expect(found).not.toBeNull();
|
|
expect(found!.scheduleId).toBe(scheduleId);
|
|
|
|
// The schedule row does NOT exist on the run-ops (PG14) client.
|
|
const onLegacy = await prisma14.taskSchedule.findFirst({ where: { id: scheduleId } });
|
|
expect(onLegacy).toBeNull();
|
|
|
|
const out = await new ApiRetrieveRunPresenter(CURRENT_API_VERSION).call(found!, env);
|
|
expect(out.schedule).toBeDefined();
|
|
expect(out.schedule?.id).toBe(`sched_${scheduleId}`);
|
|
expect(out.schedule?.externalId).toBe("cross-db-schedule");
|
|
expect(out.schedule?.generator.expression).toBe("0 0 * * *");
|
|
}
|
|
);
|
|
|
|
heteroPostgresTest(
|
|
"correct past-retention / not-found response: both stores miss => findRun returns null",
|
|
async ({ prisma17, prisma14 }) => {
|
|
const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 });
|
|
const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 });
|
|
|
|
const newEnv = await seedOrgProjectEnv(prisma17, "miss-new");
|
|
const legacyEnv = await seedOrgProjectEnv(prisma14, "miss-legacy");
|
|
|
|
// A run that exists on NEITHER store (terminated + past-retention,
|
|
// observed at this layer as a miss on both underlying stores).
|
|
const goneFriendlyId = `run_${generateRunOpsId()}`;
|
|
|
|
const fromNew = await readFoundRunViaStore(
|
|
newStore,
|
|
goneFriendlyId,
|
|
newEnv.runtimeEnvironment.id
|
|
);
|
|
const fromLegacy = await readFoundRunViaStore(
|
|
legacyStore,
|
|
goneFriendlyId,
|
|
legacyEnv.runtimeEnvironment.id
|
|
);
|
|
|
|
expect(fromNew).toBeNull();
|
|
expect(fromLegacy).toBeNull();
|
|
}
|
|
);
|
|
|
|
heteroPostgresTest(
|
|
"single-DB passthrough: one PostgresRunStore over one client hydrates run + tree (self-host collapse)",
|
|
async ({ prisma17 }) => {
|
|
// The single-DB collapse: one plain PostgresRunStore over one client.
|
|
// No legacy probe / known-migrated machinery at this layer.
|
|
const store = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 });
|
|
|
|
const onlyEnv = await seedOrgProjectEnv(prisma17, "passthrough");
|
|
const tree = await seedRunWithTree(prisma17, {
|
|
runtimeEnvironmentId: onlyEnv.runtimeEnvironment.id,
|
|
projectId: onlyEnv.project.id,
|
|
organizationId: onlyEnv.organization.id,
|
|
suffix: "passthrough",
|
|
});
|
|
|
|
const found = await readFoundRunViaStore(
|
|
store,
|
|
tree.runFriendlyId,
|
|
onlyEnv.runtimeEnvironment.id
|
|
);
|
|
expect(found).not.toBeNull();
|
|
expect(found!.isBuffered).toBe(false);
|
|
expect(found!.parentTaskRun?.friendlyId).toBe(tree.parentFriendlyId);
|
|
expect(found!.rootTaskRun?.friendlyId).toBe(tree.rootFriendlyId);
|
|
expect(found!.childRuns.map((c) => c.friendlyId)).toEqual([tree.childFriendlyId]);
|
|
expect(found!.attempts.map((a) => a.id)).toEqual([tree.attemptId]);
|
|
}
|
|
);
|
|
});
|