Files
triggerdotdev--trigger.dev/apps/webapp/test/presenters/ApiBatchResultsPresenter.test.ts
T
2026-07-13 13:32:57 +08:00

281 lines
9.1 KiB
TypeScript

import { containerTest } from "@internal/testcontainers";
import type {
Organization,
PrismaClient,
Project,
RuntimeEnvironment,
} from "@trigger.dev/database";
import { customAlphabet } from "nanoid";
import { expect, vi } from "vitest";
import { ApiBatchResultsPresenter } from "~/presenters/v3/ApiBatchResultsPresenter.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { seedTestEnvironment } from "../helpers/seedTestEnvironment";
// Neutralize the db.server singleton so importing the presenter (via BasePresenter) does not try
// to connect to the env database; the test uses the injected testcontainer prisma for all reads.
vi.mock("~/db.server", () => ({ prisma: {}, $replica: {} }));
vi.setConfig({ testTimeout: 60_000 });
const idGenerator = customAlphabet("123456789abcdefghijkmnopqrstuvwxyz", 21);
function authEnv(
environment: RuntimeEnvironment,
project: Project,
organization: Organization
): AuthenticatedEnvironment {
return { ...environment, project, organization, orgMember: null } as AuthenticatedEnvironment;
}
type SeedContext = {
environmentId: string;
projectId: string;
organizationId: string;
backgroundWorkerId: string;
backgroundWorkerTaskId: string;
queueId: string;
};
async function seedWorker(
prisma: PrismaClient,
ctx: Omit<SeedContext, "backgroundWorkerId" | "backgroundWorkerTaskId" | "queueId">
) {
const queue = await prisma.taskQueue.create({
data: {
friendlyId: `queue_${idGenerator()}`,
name: "task/test-task",
projectId: ctx.projectId,
runtimeEnvironmentId: ctx.environmentId,
},
});
const worker = await prisma.backgroundWorker.create({
data: {
friendlyId: `worker_${idGenerator()}`,
contentHash: "hash",
projectId: ctx.projectId,
runtimeEnvironmentId: ctx.environmentId,
version: "20240101.1",
metadata: {},
},
});
const task = await prisma.backgroundWorkerTask.create({
data: {
friendlyId: `task_${idGenerator()}`,
slug: "test-task",
filePath: "src/test.ts",
exportName: "testTask",
workerId: worker.id,
projectId: ctx.projectId,
runtimeEnvironmentId: ctx.environmentId,
},
});
return { queueId: queue.id, backgroundWorkerId: worker.id, backgroundWorkerTaskId: task.id };
}
async function seedRunWithAttempt(
prisma: PrismaClient,
ctx: SeedContext,
opts: {
status: "COMPLETED_SUCCESSFULLY" | "COMPLETED_WITH_ERRORS" | "CANCELED" | "EXECUTING";
attempt?: {
status: "COMPLETED" | "FAILED";
output?: string;
outputType?: string;
error?: unknown;
};
}
) {
const runInternalId = idGenerator();
const run = await prisma.taskRun.create({
data: {
id: runInternalId,
friendlyId: `run_${runInternalId}`,
taskIdentifier: "test-task",
payload: "{}",
payloadType: "application/json",
traceId: idGenerator(),
spanId: idGenerator(),
queue: "task/test-task",
runtimeEnvironmentId: ctx.environmentId,
projectId: ctx.projectId,
status: opts.status,
},
});
if (opts.attempt) {
await prisma.taskRunAttempt.create({
data: {
friendlyId: `attempt_${idGenerator()}`,
taskRunId: run.id,
backgroundWorkerId: ctx.backgroundWorkerId,
backgroundWorkerTaskId: ctx.backgroundWorkerTaskId,
runtimeEnvironmentId: ctx.environmentId,
queueId: ctx.queueId,
status: opts.attempt.status,
output: opts.attempt.output,
outputType: opts.attempt.outputType ?? "application/json",
error: opts.attempt.error as any,
},
});
}
return run;
}
containerTest(
"ApiBatchResultsPresenter returns ordered results matching pre-decompose behavior",
async ({ prisma }) => {
const { environment, project, organization } = await seedTestEnvironment(prisma);
const worker = await seedWorker(prisma, {
environmentId: environment.id,
projectId: project.id,
organizationId: organization.id,
});
const ctx: SeedContext = {
environmentId: environment.id,
projectId: project.id,
organizationId: organization.id,
...worker,
};
// A successful run, a failed run, and an executing run (no terminal attempt → undefined).
const successRun = await seedRunWithAttempt(prisma, ctx, {
status: "COMPLETED_SUCCESSFULLY",
attempt: { status: "COMPLETED", output: '"hello"', outputType: "application/json" },
});
const failedRun = await seedRunWithAttempt(prisma, ctx, {
status: "COMPLETED_WITH_ERRORS",
attempt: {
status: "FAILED",
error: { type: "BUILT_IN_ERROR", name: "Error", message: "boom", stackTrace: "boom" },
},
});
const executingRun = await seedRunWithAttempt(prisma, ctx, {
status: "EXECUTING",
});
const batchInternalId = idGenerator();
const batchFriendlyId = `batch_${batchInternalId}`;
await prisma.batchTaskRun.create({
data: {
id: batchInternalId,
friendlyId: batchFriendlyId,
runtimeEnvironmentId: environment.id,
},
});
// Items inserted in a deterministic order: success, failed, executing.
for (const run of [successRun, failedRun, executingRun]) {
await prisma.batchTaskRunItem.create({
data: {
batchTaskRunId: batchInternalId,
taskRunId: run.id,
},
});
}
// Pass the testcontainer prisma as both the primary and the replica: the passthrough path
// reads the batch row off the replica and the member runs off the primary (via the run store).
const presenter = new ApiBatchResultsPresenter(prisma, prisma);
const result = await presenter.call(
batchFriendlyId,
authEnv(environment, project, organization)
);
expect(result).toBeDefined();
expect(result?.id).toBe(batchFriendlyId);
// executing run yields no execution result → filtered out. Order preserved: success then failed.
expect(result?.items).toHaveLength(2);
const [first, second] = result!.items;
expect(first.ok).toBe(true);
expect(first.id).toBe(successRun.friendlyId);
if (first.ok) {
expect(first.output).toBe('"hello"');
expect(first.taskIdentifier).toBe("test-task");
}
expect(second.ok).toBe(false);
expect(second.id).toBe(failedRun.friendlyId);
}
);
containerTest(
"ApiBatchResultsPresenter filters runs without an execution result but keeps order",
async ({ prisma }) => {
const { environment, project, organization } = await seedTestEnvironment(prisma);
const worker = await seedWorker(prisma, {
environmentId: environment.id,
projectId: project.id,
organizationId: organization.id,
});
const ctx: SeedContext = {
environmentId: environment.id,
projectId: project.id,
organizationId: organization.id,
...worker,
};
// Pending run → executionResultForTaskRun returns undefined → filtered out, like the
// pre-decompose code did via `.filter(Boolean)`.
const pendingRun = await seedRunWithAttempt(prisma, ctx, { status: "EXECUTING" });
const successRun = await seedRunWithAttempt(prisma, ctx, {
status: "COMPLETED_SUCCESSFULLY",
attempt: { status: "COMPLETED", output: '"ok"', outputType: "application/json" },
});
const batchInternalId = idGenerator();
const batchFriendlyId = `batch_${batchInternalId}`;
await prisma.batchTaskRun.create({
data: {
id: batchInternalId,
friendlyId: batchFriendlyId,
runtimeEnvironmentId: environment.id,
},
});
// pending first, success second — only the success result should survive, in order.
for (const run of [pendingRun, successRun]) {
await prisma.batchTaskRunItem.create({
data: { batchTaskRunId: batchInternalId, taskRunId: run.id },
});
}
// Pass the testcontainer prisma as both the primary and the replica: the passthrough path
// reads the batch row off the replica and the member runs off the primary (via the run store).
const presenter = new ApiBatchResultsPresenter(prisma, prisma);
const result = await presenter.call(
batchFriendlyId,
authEnv(environment, project, organization)
);
expect(result?.items).toHaveLength(1);
expect(result?.items[0]?.id).toBe(successRun.friendlyId);
}
);
containerTest("ApiBatchResultsPresenter short-circuits an empty batch", async ({ prisma }) => {
const { environment, project, organization } = await seedTestEnvironment(prisma);
const batchInternalId = idGenerator();
const batchFriendlyId = `batch_${batchInternalId}`;
await prisma.batchTaskRun.create({
data: {
id: batchInternalId,
friendlyId: batchFriendlyId,
runtimeEnvironmentId: environment.id,
},
});
// Pass the testcontainer prisma as both the primary and the replica: the passthrough path
// reads the batch row off the replica and the member runs off the primary (via the run store).
const presenter = new ApiBatchResultsPresenter(prisma, prisma);
const result = await presenter.call(batchFriendlyId, authEnv(environment, project, organization));
expect(result).toEqual({ id: batchFriendlyId, items: [] });
});