chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
import { containerTest } from "@internal/testcontainers";
|
||||
import type { CreateBackgroundWorkerRequestBody } from "@trigger.dev/core/v3";
|
||||
import type { PrismaClient } from "@trigger.dev/database";
|
||||
import { describe, expect, vi } from "vitest";
|
||||
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { ServiceValidationError } from "~/v3/services/common.server";
|
||||
import { findOrCreateBackgroundWorker } from "~/v3/services/createDeploymentBackgroundWorkerV4/findOrCreateBackgroundWorker.server";
|
||||
|
||||
vi.setConfig({ testTimeout: 30_000 });
|
||||
|
||||
async function seedDeployment(prisma: PrismaClient, version = "20260528.1") {
|
||||
const slug = `s${Math.random().toString(36).slice(2, 10)}`;
|
||||
const organization = await prisma.organization.create({
|
||||
data: { title: slug, slug },
|
||||
});
|
||||
|
||||
const project = await prisma.project.create({
|
||||
data: {
|
||||
name: slug,
|
||||
slug,
|
||||
organizationId: organization.id,
|
||||
externalRef: slug,
|
||||
},
|
||||
});
|
||||
|
||||
const environment = await prisma.runtimeEnvironment.create({
|
||||
data: {
|
||||
slug,
|
||||
type: "PRODUCTION",
|
||||
projectId: project.id,
|
||||
organizationId: organization.id,
|
||||
apiKey: slug,
|
||||
pkApiKey: slug,
|
||||
shortcode: slug,
|
||||
},
|
||||
});
|
||||
|
||||
const deployment = await prisma.workerDeployment.create({
|
||||
data: {
|
||||
friendlyId: `deployment_${slug}`,
|
||||
shortCode: slug,
|
||||
contentHash: "h_initial",
|
||||
status: "BUILDING",
|
||||
version,
|
||||
projectId: project.id,
|
||||
environmentId: environment.id,
|
||||
},
|
||||
});
|
||||
|
||||
// The helper only reads `id` and `projectId`; the rest of the type is
|
||||
// unused here so we cast a partial.
|
||||
const authEnv = { id: environment.id, projectId: project.id } as AuthenticatedEnvironment;
|
||||
|
||||
return { project, environment, deployment, authEnv };
|
||||
}
|
||||
|
||||
function bodyWithHash(contentHash: string): CreateBackgroundWorkerRequestBody {
|
||||
return {
|
||||
localOnly: false,
|
||||
metadata: {
|
||||
contentHash,
|
||||
packageVersion: "0.0.0",
|
||||
cliPackageVersion: "0.0.0",
|
||||
tasks: [],
|
||||
queues: [],
|
||||
sourceFiles: [],
|
||||
runtime: "node",
|
||||
runtimeVersion: "21.0.0",
|
||||
},
|
||||
engine: "V2",
|
||||
supportsLazyAttempts: true,
|
||||
};
|
||||
}
|
||||
|
||||
describe("findOrCreateBackgroundWorker", () => {
|
||||
containerTest("creates a new BackgroundWorker on the first call", async ({ prisma }) => {
|
||||
const { authEnv, deployment } = await seedDeployment(prisma);
|
||||
|
||||
const worker = await findOrCreateBackgroundWorker(
|
||||
authEnv,
|
||||
deployment,
|
||||
bodyWithHash("h1"),
|
||||
prisma
|
||||
);
|
||||
|
||||
expect(worker.projectId).toBe(authEnv.projectId);
|
||||
expect(worker.runtimeEnvironmentId).toBe(authEnv.id);
|
||||
expect(worker.version).toBe(deployment.version);
|
||||
expect(worker.contentHash).toBe("h1");
|
||||
|
||||
const rowCount = await prisma.backgroundWorker.count({
|
||||
where: { projectId: authEnv.projectId, version: deployment.version },
|
||||
});
|
||||
expect(rowCount).toBe(1);
|
||||
});
|
||||
|
||||
containerTest(
|
||||
"returns the existing row on a second call with the same contentHash (no duplicate)",
|
||||
async ({ prisma }) => {
|
||||
const { authEnv, deployment } = await seedDeployment(prisma);
|
||||
|
||||
const first = await findOrCreateBackgroundWorker(
|
||||
authEnv,
|
||||
deployment,
|
||||
bodyWithHash("h1"),
|
||||
prisma
|
||||
);
|
||||
const second = await findOrCreateBackgroundWorker(
|
||||
authEnv,
|
||||
deployment,
|
||||
bodyWithHash("h1"),
|
||||
prisma
|
||||
);
|
||||
|
||||
expect(second.id).toBe(first.id);
|
||||
|
||||
const rowCount = await prisma.backgroundWorker.count({
|
||||
where: { projectId: authEnv.projectId, version: deployment.version },
|
||||
});
|
||||
expect(rowCount).toBe(1);
|
||||
}
|
||||
);
|
||||
|
||||
containerTest(
|
||||
"throws 409 ServiceValidationError when an existing row has a different contentHash",
|
||||
async ({ prisma }) => {
|
||||
const { authEnv, deployment } = await seedDeployment(prisma);
|
||||
|
||||
await findOrCreateBackgroundWorker(authEnv, deployment, bodyWithHash("h1"), prisma);
|
||||
|
||||
await expect(
|
||||
findOrCreateBackgroundWorker(authEnv, deployment, bodyWithHash("h2"), prisma)
|
||||
).rejects.toMatchObject({
|
||||
name: "ServiceValidationError",
|
||||
status: 409,
|
||||
});
|
||||
|
||||
// Also assert the constructor so callers can `catch (e instanceof ServiceValidationError)`.
|
||||
await expect(
|
||||
findOrCreateBackgroundWorker(authEnv, deployment, bodyWithHash("h2"), prisma)
|
||||
).rejects.toBeInstanceOf(ServiceValidationError);
|
||||
}
|
||||
);
|
||||
|
||||
containerTest(
|
||||
"concurrent create race surfaces as plain Error (not ServiceValidationError)",
|
||||
async ({ prisma }) => {
|
||||
// The class distinction matters: the V4 service uses `instanceof ServiceValidationError`
|
||||
// to decide whether to fail-deploy. A transient race must not fail-deploy.
|
||||
const { authEnv, deployment } = await seedDeployment(prisma);
|
||||
|
||||
const [first, second] = await Promise.allSettled([
|
||||
findOrCreateBackgroundWorker(authEnv, deployment, bodyWithHash("h-race"), prisma),
|
||||
findOrCreateBackgroundWorker(authEnv, deployment, bodyWithHash("h-race"), prisma),
|
||||
]);
|
||||
|
||||
const fulfilled = [first, second].filter((r) => r.status === "fulfilled");
|
||||
const rejected = [first, second].filter(
|
||||
(r): r is PromiseRejectedResult => r.status === "rejected"
|
||||
);
|
||||
|
||||
// Exactly one row in the database regardless of who won.
|
||||
const rowCount = await prisma.backgroundWorker.count({
|
||||
where: { projectId: authEnv.projectId, version: deployment.version },
|
||||
});
|
||||
expect(rowCount).toBe(1);
|
||||
|
||||
// If the schedule produced an actual race (one wins, one loses), the loser
|
||||
// must surface a non-SVE error. If the schedule serialised them by accident,
|
||||
// both fulfilled is also acceptable — this test is about the error *class*,
|
||||
// not the rate of races.
|
||||
if (rejected.length > 0) {
|
||||
expect(fulfilled).toHaveLength(1);
|
||||
for (const r of rejected) {
|
||||
expect(r.reason).not.toBeInstanceOf(ServiceValidationError);
|
||||
expect((r.reason as Error).message).toMatch(/concurrent/i);
|
||||
}
|
||||
} else {
|
||||
expect(fulfilled).toHaveLength(2);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user