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 { 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(); } ); });