185 lines
5.7 KiB
TypeScript
185 lines
5.7 KiB
TypeScript
import { heteroPostgresTest } from "@internal/testcontainers";
|
|
import type { PrismaClient } from "@trigger.dev/database";
|
|
import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic";
|
|
import { describe, expect, vi } from "vitest";
|
|
|
|
// Stub so the runStore singleton doesn't eagerly connect at import.
|
|
vi.mock("~/db.server", () => ({
|
|
prisma: {},
|
|
$replica: {},
|
|
runOpsNewPrisma: {},
|
|
runOpsLegacyPrisma: {},
|
|
}));
|
|
// Keep split off so resolveIdempotencyDedupClient returns this.prisma (the hetero fixture client).
|
|
vi.mock("~/v3/runOpsMigration/splitMode.server", () => ({ isSplitEnabled: async () => false }));
|
|
|
|
import { IdempotencyKeyConcern } from "~/runEngine/concerns/idempotencyKeys.server";
|
|
import type { TriggerTaskRequest } from "~/runEngine/types";
|
|
|
|
vi.setConfig({ testTimeout: 60_000 });
|
|
|
|
function makeConcern(client: PrismaClient) {
|
|
return new IdempotencyKeyConcern(client as never, {} as never, {} as never);
|
|
}
|
|
|
|
function makeRequest(opts: {
|
|
environmentId: string;
|
|
organizationId: string;
|
|
projectId: string;
|
|
taskId: string;
|
|
idempotencyKey: string;
|
|
}): TriggerTaskRequest {
|
|
return {
|
|
taskId: opts.taskId,
|
|
environment: {
|
|
id: opts.environmentId,
|
|
organizationId: opts.organizationId,
|
|
projectId: opts.projectId,
|
|
organization: { featureFlags: {} },
|
|
},
|
|
options: {},
|
|
body: { options: { idempotencyKey: opts.idempotencyKey } },
|
|
} as unknown as TriggerTaskRequest;
|
|
}
|
|
|
|
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: `test-${suffix}`,
|
|
pkApiKey: `test-${suffix}`,
|
|
shortcode: `test-${suffix}`,
|
|
},
|
|
});
|
|
return { organization, project, runtimeEnvironment };
|
|
}
|
|
|
|
async function seedRun(
|
|
prisma: PrismaClient,
|
|
args: {
|
|
runtimeEnvironmentId: string;
|
|
projectId: string;
|
|
organizationId: string;
|
|
taskIdentifier: string;
|
|
idempotencyKey: string;
|
|
status?: "PENDING" | "EXECUTING" | "COMPLETED_SUCCESSFULLY" | "COMPLETED_WITH_ERRORS";
|
|
}
|
|
) {
|
|
const runId = generateRunOpsId();
|
|
return prisma.taskRun.create({
|
|
data: {
|
|
id: runId,
|
|
friendlyId: `run_${runId}`,
|
|
taskIdentifier: args.taskIdentifier,
|
|
idempotencyKey: args.idempotencyKey,
|
|
idempotencyKeyExpiresAt: null,
|
|
status: args.status ?? "EXECUTING",
|
|
payload: JSON.stringify({ foo: "bar" }),
|
|
payloadType: "application/json",
|
|
traceId: "1234",
|
|
spanId: "1234",
|
|
queue: "test",
|
|
runtimeEnvironmentId: args.runtimeEnvironmentId,
|
|
projectId: args.projectId,
|
|
organizationId: args.organizationId,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
},
|
|
});
|
|
}
|
|
|
|
describe("IdempotencyKeyConcern · residency-routed dedup (cross-DB)", () => {
|
|
heteroPostgresTest(
|
|
"a would-be-new run resolves its key against the new (PG17) DB, not the legacy (PG14) DB",
|
|
async ({ prisma14, prisma17 }) => {
|
|
// Same env shape on both DBs.
|
|
const legacy = await seedOrgProjectEnv(prisma14, "resid-legacy");
|
|
const next = await seedOrgProjectEnv(prisma17, "resid-new");
|
|
|
|
const key = "idem-resid-1";
|
|
|
|
const newRun = await seedRun(prisma17, {
|
|
runtimeEnvironmentId: next.runtimeEnvironment.id,
|
|
projectId: next.project.id,
|
|
organizationId: next.organization.id,
|
|
taskIdentifier: "my-task",
|
|
idempotencyKey: key,
|
|
status: "EXECUTING",
|
|
});
|
|
|
|
const concernOnNew = makeConcern(prisma17);
|
|
const hit = await concernOnNew.handleTriggerRequest(
|
|
makeRequest({
|
|
environmentId: next.runtimeEnvironment.id,
|
|
organizationId: next.organization.id,
|
|
projectId: next.project.id,
|
|
taskId: "my-task",
|
|
idempotencyKey: key,
|
|
}),
|
|
undefined
|
|
);
|
|
expect(hit.isCached).toBe(true);
|
|
if (hit.isCached === true) {
|
|
expect(hit.run.id).toBe(newRun.id);
|
|
}
|
|
|
|
// The legacy DB holds no row for this key — a legacy-pinned probe would miss it.
|
|
const legacyMatches = await prisma14.taskRun.count({
|
|
where: {
|
|
runtimeEnvironmentId: legacy.runtimeEnvironment.id,
|
|
taskIdentifier: "my-task",
|
|
idempotencyKey: key,
|
|
},
|
|
});
|
|
expect(legacyMatches).toBe(0);
|
|
}
|
|
);
|
|
|
|
heteroPostgresTest(
|
|
"a would-be-legacy run still resolves its key against the legacy (PG14) DB",
|
|
async ({ prisma14 }) => {
|
|
const legacy = await seedOrgProjectEnv(prisma14, "resid-legacy-only");
|
|
const key = "idem-resid-legacy";
|
|
|
|
const legacyRun = await seedRun(prisma14, {
|
|
runtimeEnvironmentId: legacy.runtimeEnvironment.id,
|
|
projectId: legacy.project.id,
|
|
organizationId: legacy.organization.id,
|
|
taskIdentifier: "my-task",
|
|
idempotencyKey: key,
|
|
status: "EXECUTING",
|
|
});
|
|
|
|
const concernOnLegacy = makeConcern(prisma14);
|
|
const hit = await concernOnLegacy.handleTriggerRequest(
|
|
makeRequest({
|
|
environmentId: legacy.runtimeEnvironment.id,
|
|
organizationId: legacy.organization.id,
|
|
projectId: legacy.project.id,
|
|
taskId: "my-task",
|
|
idempotencyKey: key,
|
|
}),
|
|
undefined
|
|
);
|
|
expect(hit.isCached).toBe(true);
|
|
if (hit.isCached === true) {
|
|
expect(hit.run.id).toBe(legacyRun.id);
|
|
}
|
|
}
|
|
);
|
|
});
|