281 lines
9.1 KiB
TypeScript
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: [] });
|
|
});
|