348 lines
12 KiB
TypeScript
348 lines
12 KiB
TypeScript
import { describe, expect, vi } from "vitest";
|
|
|
|
// The sessions.server module graph imports `~/db.server` (and the run-store
|
|
// singleton) at load. Stub `~/db.server` so importing the module under test does
|
|
// not construct the real boot clients — the serializer is driven entirely through
|
|
// an explicitly injected RunStore built from the real test containers.
|
|
vi.mock("~/db.server", () => ({
|
|
prisma: {},
|
|
$replica: {},
|
|
}));
|
|
|
|
import { heteroRunOpsPostgresTest, postgresTest } from "@internal/testcontainers";
|
|
import { buildRunStore } from "~/v3/runStore.server";
|
|
import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic";
|
|
import type { RunOpsPrismaClient } from "@internal/run-ops-database";
|
|
import type { PrismaClient } from "@trigger.dev/database";
|
|
import {
|
|
resolveSessionByIdOrExternalId,
|
|
serializeSessionsWithFriendlyRunIds,
|
|
serializeSessionWithFriendlyRunId,
|
|
} from "~/services/realtime/sessions.server";
|
|
|
|
vi.setConfig({ testTimeout: 90_000 });
|
|
|
|
type SeedContext = {
|
|
organizationId: string;
|
|
projectId: string;
|
|
environmentId: string;
|
|
};
|
|
|
|
/**
|
|
* Creates the org/project/env parents on the control-plane client. `Session`
|
|
* and the legacy `TaskRun` both need these FK parents; the dedicated run-ops
|
|
* schema (`prisma17`) is FK-free, so NEW runs only need the scalar tenant ids.
|
|
*/
|
|
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: "DEVELOPMENT",
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: `tr_dev_${slug}`,
|
|
pkApiKey: `pk_dev_${slug}`,
|
|
shortcode: `sc-${slug}`,
|
|
},
|
|
});
|
|
|
|
return {
|
|
organizationId: organization.id,
|
|
projectId: project.id,
|
|
environmentId: runtimeEnvironment.id,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a legacy (control-plane) TaskRun. A default cuid id classifies LEGACY.
|
|
*/
|
|
async function createLegacyRun(
|
|
prisma: PrismaClient,
|
|
ctx: SeedContext,
|
|
run: { friendlyId: string }
|
|
) {
|
|
return prisma.taskRun.create({
|
|
data: {
|
|
friendlyId: run.friendlyId,
|
|
taskIdentifier: "my-task",
|
|
status: "PENDING",
|
|
payload: JSON.stringify({ foo: run.friendlyId }),
|
|
traceId: run.friendlyId,
|
|
spanId: run.friendlyId,
|
|
queue: "test",
|
|
runtimeEnvironmentId: ctx.environmentId,
|
|
projectId: ctx.projectId,
|
|
organizationId: ctx.organizationId,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a NEW (dedicated run-ops) TaskRun with a run-ops id — classifies NEW and
|
|
* lives only on the run-ops DB. Scalar tenant columns only (the subset schema is
|
|
* FK-free, so no org/project/env rows are required here).
|
|
*/
|
|
async function createNewRun(
|
|
prisma: RunOpsPrismaClient,
|
|
ctx: SeedContext,
|
|
run: { friendlyId: string; id: string }
|
|
) {
|
|
return prisma.taskRun.create({
|
|
data: {
|
|
id: run.id,
|
|
friendlyId: run.friendlyId,
|
|
taskIdentifier: "my-task",
|
|
status: "PENDING",
|
|
payload: JSON.stringify({ foo: run.friendlyId }),
|
|
traceId: run.friendlyId,
|
|
spanId: run.friendlyId,
|
|
queue: "test",
|
|
runtimeEnvironmentId: ctx.environmentId,
|
|
projectId: ctx.projectId,
|
|
organizationId: ctx.organizationId,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
},
|
|
});
|
|
}
|
|
|
|
async function createSession(
|
|
prisma: PrismaClient,
|
|
ctx: SeedContext,
|
|
session: { friendlyId: string; externalId?: string; currentRunId?: string | null }
|
|
) {
|
|
return prisma.session.create({
|
|
data: {
|
|
friendlyId: session.friendlyId,
|
|
externalId: session.externalId,
|
|
type: "chat",
|
|
projectId: ctx.projectId,
|
|
runtimeEnvironmentId: ctx.environmentId,
|
|
environmentType: "DEVELOPMENT",
|
|
organizationId: ctx.organizationId,
|
|
taskIdentifier: "my-task",
|
|
triggerConfig: {},
|
|
currentRunId: session.currentRunId ?? null,
|
|
},
|
|
});
|
|
}
|
|
|
|
describe("sessions serializer currentRunId resolution", () => {
|
|
// --- Passthrough single-run (single-DB) ---
|
|
postgresTest(
|
|
"single-run passthrough resolves currentRunId -> friendlyId; null stays null",
|
|
async ({ prisma }) => {
|
|
const ctx = await seedParents(prisma as PrismaClient, "single-pass");
|
|
const run = await createLegacyRun(prisma as PrismaClient, ctx, { friendlyId: "run_single" });
|
|
const session = await createSession(prisma as PrismaClient, ctx, {
|
|
friendlyId: "session_single",
|
|
currentRunId: run.id,
|
|
});
|
|
const nullSession = await createSession(prisma as PrismaClient, ctx, {
|
|
friendlyId: "session_null",
|
|
currentRunId: null,
|
|
});
|
|
|
|
const store = buildRunStore({
|
|
splitEnabled: false,
|
|
singleWriter: prisma as PrismaClient,
|
|
singleReplica: prisma as PrismaClient,
|
|
});
|
|
|
|
const item = await serializeSessionWithFriendlyRunId(session, store);
|
|
expect(item.currentRunId).toBe("run_single");
|
|
|
|
const nullItem = await serializeSessionWithFriendlyRunId(nullSession, store);
|
|
expect(nullItem.currentRunId).toBeNull();
|
|
}
|
|
);
|
|
|
|
// --- Passthrough batched (single-DB) + tenant scope ---
|
|
postgresTest(
|
|
"batched passthrough resolves each currentRunId; null stays null; cross-env is dropped",
|
|
async ({ prisma }) => {
|
|
const ctx = await seedParents(prisma as PrismaClient, "batch-pass");
|
|
const otherCtx = await seedParents(prisma as PrismaClient, "batch-pass-other");
|
|
|
|
const runA = await createLegacyRun(prisma as PrismaClient, ctx, { friendlyId: "run_A" });
|
|
const runB = await createLegacyRun(prisma as PrismaClient, ctx, { friendlyId: "run_B" });
|
|
// A run in a DIFFERENT env — pointer must not resolve under our scope.
|
|
const crossEnvRun = await createLegacyRun(prisma as PrismaClient, otherCtx, {
|
|
friendlyId: "run_cross",
|
|
});
|
|
|
|
const sessionA = await createSession(prisma as PrismaClient, ctx, {
|
|
friendlyId: "session_A",
|
|
currentRunId: runA.id,
|
|
});
|
|
const sessionB = await createSession(prisma as PrismaClient, ctx, {
|
|
friendlyId: "session_B",
|
|
currentRunId: runB.id,
|
|
});
|
|
const sessionNull = await createSession(prisma as PrismaClient, ctx, {
|
|
friendlyId: "session_n",
|
|
currentRunId: null,
|
|
});
|
|
const sessionCross = await createSession(prisma as PrismaClient, ctx, {
|
|
friendlyId: "session_x",
|
|
currentRunId: crossEnvRun.id,
|
|
});
|
|
|
|
const store = buildRunStore({
|
|
splitEnabled: false,
|
|
singleWriter: prisma as PrismaClient,
|
|
singleReplica: prisma as PrismaClient,
|
|
});
|
|
|
|
const items = await serializeSessionsWithFriendlyRunIds(
|
|
[sessionA, sessionB, sessionNull, sessionCross],
|
|
{ projectId: ctx.projectId, runtimeEnvironmentId: ctx.environmentId },
|
|
store
|
|
);
|
|
|
|
const byFriendly = new Map(items.map((i) => [i.id, i.currentRunId]));
|
|
expect(byFriendly.get("session_A")).toBe("run_A");
|
|
expect(byFriendly.get("session_B")).toBe("run_B");
|
|
expect(byFriendly.get("session_n")).toBeNull();
|
|
// cross-env run exists, but the tenant-scoped find drops it -> null.
|
|
expect(byFriendly.get("session_x")).toBeNull();
|
|
}
|
|
);
|
|
|
|
// --- Control-plane Session resolve is not routed ---
|
|
postgresTest(
|
|
"resolveSessionByIdOrExternalId resolves the Session row by friendlyId and by externalId",
|
|
async ({ prisma }) => {
|
|
const ctx = await seedParents(prisma as PrismaClient, "controlplane");
|
|
const session = await createSession(prisma as PrismaClient, ctx, {
|
|
friendlyId: "session_cp",
|
|
externalId: "ext-cp-1",
|
|
currentRunId: null,
|
|
});
|
|
|
|
const byFriendly = await resolveSessionByIdOrExternalId(
|
|
prisma as PrismaClient,
|
|
ctx.environmentId,
|
|
session.friendlyId
|
|
);
|
|
expect(byFriendly?.id).toBe(session.id);
|
|
|
|
const byExternal = await resolveSessionByIdOrExternalId(
|
|
prisma as PrismaClient,
|
|
ctx.environmentId,
|
|
"ext-cp-1"
|
|
);
|
|
expect(byExternal?.id).toBe(session.id);
|
|
}
|
|
);
|
|
|
|
// --- Split single-run across two physical DBs (the production-shaped break) ---
|
|
// run-ops id (NEW-DB) session run must serialize a non-null friendlyId, and a cuid
|
|
// (LEGACY) run must still resolve — proving the asymmetry is gone.
|
|
heteroRunOpsPostgresTest(
|
|
"split single-run resolves a NEW-run-ops run from the run-ops DB and a LEGACY-cuid run from control-plane",
|
|
async ({ prisma14, prisma17 }) => {
|
|
const ctx = await seedParents(prisma14, "split-single");
|
|
|
|
const newRun = await createNewRun(prisma17, ctx, {
|
|
friendlyId: "run_new",
|
|
id: generateRunOpsId(),
|
|
});
|
|
const legacyRun = await createLegacyRun(prisma14, ctx, { friendlyId: "run_legacy" });
|
|
|
|
const newSession = await createSession(prisma14, ctx, {
|
|
friendlyId: "session_new",
|
|
currentRunId: newRun.id,
|
|
});
|
|
const legacySession = await createSession(prisma14, ctx, {
|
|
friendlyId: "session_legacy",
|
|
currentRunId: legacyRun.id,
|
|
});
|
|
|
|
const store = buildRunStore({
|
|
splitEnabled: true,
|
|
newWriter: prisma17,
|
|
newReplica: prisma17,
|
|
legacyWriter: prisma14,
|
|
legacyReplica: prisma14,
|
|
singleWriter: prisma14,
|
|
singleReplica: prisma14,
|
|
});
|
|
|
|
const newItem = await serializeSessionWithFriendlyRunId(newSession, store);
|
|
expect(newItem.currentRunId).toBe("run_new");
|
|
|
|
const legacyItem = await serializeSessionWithFriendlyRunId(legacySession, store);
|
|
expect(legacyItem.currentRunId).toBe("run_legacy");
|
|
}
|
|
);
|
|
|
|
// --- Split batched — NEW + legacy union; null + cross-env dropped ---
|
|
heteroRunOpsPostgresTest(
|
|
"split batched resolves runs across NEW + legacy; null stays null; cross-env dropped",
|
|
async ({ prisma14, prisma17 }) => {
|
|
const ctx = await seedParents(prisma14, "split-batch");
|
|
const otherCtx = await seedParents(prisma14, "split-batch-other");
|
|
|
|
const newRun = await createNewRun(prisma17, ctx, {
|
|
friendlyId: "run_bnew",
|
|
id: generateRunOpsId(),
|
|
});
|
|
const legacyRun = await createLegacyRun(prisma14, ctx, { friendlyId: "run_blegacy" });
|
|
const crossEnvRun = await createLegacyRun(prisma14, otherCtx, { friendlyId: "run_bcross" });
|
|
|
|
const sessionNew = await createSession(prisma14, ctx, {
|
|
friendlyId: "session_bnew",
|
|
currentRunId: newRun.id,
|
|
});
|
|
const sessionLegacy = await createSession(prisma14, ctx, {
|
|
friendlyId: "session_blegacy",
|
|
currentRunId: legacyRun.id,
|
|
});
|
|
const sessionNull = await createSession(prisma14, ctx, {
|
|
friendlyId: "session_bnull",
|
|
currentRunId: null,
|
|
});
|
|
const sessionCross = await createSession(prisma14, ctx, {
|
|
friendlyId: "session_bcross",
|
|
currentRunId: crossEnvRun.id,
|
|
});
|
|
|
|
const store = buildRunStore({
|
|
splitEnabled: true,
|
|
newWriter: prisma17,
|
|
newReplica: prisma17,
|
|
legacyWriter: prisma14,
|
|
legacyReplica: prisma14,
|
|
singleWriter: prisma14,
|
|
singleReplica: prisma14,
|
|
});
|
|
|
|
const items = await serializeSessionsWithFriendlyRunIds(
|
|
[sessionNew, sessionLegacy, sessionNull, sessionCross],
|
|
{ projectId: ctx.projectId, runtimeEnvironmentId: ctx.environmentId },
|
|
store
|
|
);
|
|
|
|
const byFriendly = new Map(items.map((i) => [i.id, i.currentRunId]));
|
|
expect(byFriendly.get("session_bnew")).toBe("run_bnew");
|
|
expect(byFriendly.get("session_blegacy")).toBe("run_blegacy");
|
|
expect(byFriendly.get("session_bnull")).toBeNull();
|
|
expect(byFriendly.get("session_bcross")).toBeNull();
|
|
}
|
|
);
|
|
});
|