Files
Daniel Sutton 4c2c25511b test(webapp): split triggerTask engine test into per-concern files (#4167)
The engine `triggerTask` suite was a single 2447-line file with 23
`containerTest` cases, each spinning its own Postgres + Redis. vitest
shards by whole file, so all 23 container setups landed on one shard and
dominated its wall-clock. The recorded entry in `test-timings.json`
badly under-counts the real cost (it does not capture the
per-`containerTest` container startup that dominates on CI), so the
duration-sharding sequencer treated the file as light and stacked it,
producing one ~21 minute shard.

Splitting does not reduce the number of container setups; it lets those
23 cases distribute across shards instead of stacking on one. The webapp
unit-test stage is gated by its slowest shard, so this cuts the stage's
wall-clock roughly in half.

## CI timing (before vs after)

Real CI wall-clock of the `Unit Tests: Webapp` shards (`--shard=i/10`).
"Before" is sampled from recent runs on other branches (unsplit file,
from `main`); "after" is this PR.

| Shard | Before (s) | After (s) |
|------:|-----------:|----------:|
| 1  | 250 | 359 |
| 2  | 444 | 411 |
| 3  | 497 | 659 |
| 4  | **1257** | 284 |
| 5  | 545 | 641 |
| 6  | 284 | 644 |
| 7  | 244 | 214 |
| 8  | 340 | 445 |
| 9  | 188 | 395 |
| 10 | 234 | 567 |
| **Slowest shard (gates the stage)** | **~1247s (≈21m)** | **659s
(≈11m)** |
| Sum of all shards | 4283 | 4619 |

Before: shard 4 is the long pole at 1237s / 1247s / 1257s across three
sampled runs (the `triggerTask` file plus whatever else the packer put
with it). After: the six pieces spread across shards, the slowest drops
to 659s. The small rise in summed time is the extra per-file container
startup, paid in parallel across shards, so the gating number still
falls by about 10 minutes.

## Change

Split into six per-concern files that share a `triggerTaskTestHelpers`
module (the `vi.mock` calls stay per-file, since vitest hoists them):

- `triggerTask.test.ts` (3): trigger + concurrencyKey coercion
- `triggerTask.idempotency.test.ts` (4): idempotency + queue resolution
- `triggerTask.debounce.test.ts` (4): retries + debounce validation
- `triggerTask.mollifier.test.ts` (4): mollifier call-site behaviour
- `triggerTask.metadataCache.test.ts` (4): DefaultQueueManager task
metadata cache
- `triggerTask.residency.test.ts` (4): child run residency inheritance

All 23 cases are preserved. The file's `test-timings.json` entry is
split across the new files so bin-packing stays balanced.

While rewriting these files, cleanup was moved to `onTestFinished(() =>
engine.quit())` so an `engine`/`Redis` leaked on a failing assertion no
longer persists on the worker-scoped Redis and cascades into later cases
(`hookTimeout` raised to 60s so the after-cleanup gets the full budget).
Prisma lookups switched from `findUnique` to `findFirst` to match the
repo convention.

Verified: all six files run green locally (23/23), oxlint and oxfmt
clean.
2026-07-06 13:03:27 +01:00

194 lines
7.2 KiB
TypeScript

import { describe, expect, onTestFinished, vi } from "vitest";
// db.server + splitMode are mocked so the idempotency dedup client resolves to
// the container prisma passed into the concern (split stays off).
vi.mock("~/db.server", () => ({
prisma: {},
$replica: {},
runOpsNewPrisma: {},
runOpsLegacyPrisma: {},
}));
vi.mock("~/v3/runOpsMigration/splitMode.server", () => ({ isSplitEnabled: async () => false }));
vi.mock("~/services/platform.v3.server", async (importOriginal) => {
const actual = (await importOriginal()) as Record<string, unknown>;
return {
...actual,
getEntitlement: vi.fn(),
};
});
import { RunEngine } from "@internal/run-engine";
import { setupAuthenticatedEnvironment, setupBackgroundWorker } from "@internal/run-engine/tests";
import { containerTest } from "@internal/testcontainers";
import { trace } from "@opentelemetry/api";
import {
RunId,
classifyKind,
generateInternalId,
generateRunOpsId,
} from "@trigger.dev/core/v3/isomorphic";
import { IdempotencyKeyConcern } from "~/runEngine/concerns/idempotencyKeys.server";
import { DefaultQueueManager } from "~/runEngine/concerns/queues.server";
import { RunEngineTriggerTaskService } from "../../app/runEngine/services/triggerTask.server";
import {
MockPayloadProcessor,
MockTraceEventConcern,
MockTriggerTaskValidator,
} from "./triggerTaskTestHelpers";
vi.setConfig({ testTimeout: 60_000, hookTimeout: 60_000 });
describe("RunEngineTriggerTaskService — child run residency inheritance", () => {
// Helper: stand up an engine + service wired for a single (real) Postgres/Redis
// pair. Returns the service plus the authenticated environment and a registered
// task identifier.
async function setupResidencyService(prisma: any, redisOptions: any) {
const engine = new RunEngine({
prisma,
worker: { redis: redisOptions, workers: 1, tasksPerWorker: 10, pollIntervalMs: 100 },
queue: { redis: redisOptions },
runLock: { redis: redisOptions },
machines: {
defaultMachine: "small-1x",
machines: {
"small-1x": {
name: "small-1x" as const,
cpu: 0.5,
memory: 0.5,
centsPerMs: 0.0001,
},
},
baseCostInCents: 0.0005,
},
tracer: trace.getTracer("test", "0.0.0"),
});
onTestFinished(() => engine.quit());
const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION");
const taskIdentifier = "residency-task";
await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier);
const queuesManager = new DefaultQueueManager(prisma, engine);
const idempotencyKeyConcern = new IdempotencyKeyConcern(
prisma,
engine,
new MockTraceEventConcern()
);
const triggerTaskService = new RunEngineTriggerTaskService({
engine,
prisma,
payloadProcessor: new MockPayloadProcessor(),
queueConcern: queuesManager,
idempotencyKeyConcern,
validator: new MockTriggerTaskValidator(),
traceEventConcern: new MockTraceEventConcern(),
tracer: trace.getTracer("test", "0.0.0"),
metadataMaximumSize: 1024 * 1024 * 1,
});
return { engine, authenticatedEnvironment, taskIdentifier, triggerTaskService };
}
containerTest(
"root run mints by the env flag (cuid when split is off)",
async ({ prisma, redisOptions }) => {
const { authenticatedEnvironment, taskIdentifier, triggerTaskService } =
await setupResidencyService(prisma, redisOptions);
const result = await triggerTaskService.call({
taskId: taskIdentifier,
environment: authenticatedEnvironment,
body: { payload: { test: "root" } },
});
expect(result?.run.friendlyId).toBeDefined();
// Split disabled in CI ⇒ flag resolves "cuid".
expect(classifyKind(result!.run.friendlyId)).toBe("cuid");
}
);
containerTest(
"child of a LEGACY (cuid) parent is minted cuid (born LEGACY)",
async ({ prisma, redisOptions }) => {
const { authenticatedEnvironment, taskIdentifier, triggerTaskService } =
await setupResidencyService(prisma, redisOptions);
// Root parent — cuid in CI (split off).
const parent = await triggerTaskService.call({
taskId: taskIdentifier,
environment: authenticatedEnvironment,
body: { payload: { test: "parent" } },
});
expect(classifyKind(parent!.run.friendlyId)).toBe("cuid");
const child = await triggerTaskService.call({
taskId: taskIdentifier,
environment: authenticatedEnvironment,
body: { payload: { test: "child" }, options: { parentRunId: parent!.run.friendlyId } },
});
expect(classifyKind(child!.run.friendlyId)).toBe("cuid");
}
);
containerTest(
"child of a NEW (run-ops id) parent is minted run-ops id (born NEW)",
async ({ prisma, redisOptions }) => {
const { authenticatedEnvironment, taskIdentifier, triggerTaskService } =
await setupResidencyService(prisma, redisOptions);
// Construct a NEW-resident parent directly by minting a run-ops id friendlyId
// and creating its run row, so the child inherits NEW by id-shape alone
// (no marker needed). We trigger the parent with an explicit run-ops id via
// the runFriendlyId option so the row physically exists for the parent
// lookup the child path performs.
// v1 id (version "1" at index 25) → classifies NEW
const parentFriendlyId = RunId.toFriendlyId(generateRunOpsId());
expect(classifyKind(parentFriendlyId)).toBe("runOpsId");
const parent = await triggerTaskService.call({
taskId: taskIdentifier,
environment: authenticatedEnvironment,
body: { payload: { test: "parent" } },
options: { runFriendlyId: parentFriendlyId },
});
expect(parent!.run.friendlyId).toBe(parentFriendlyId);
const child = await triggerTaskService.call({
taskId: taskIdentifier,
environment: authenticatedEnvironment,
body: { payload: { test: "child" }, options: { parentRunId: parentFriendlyId } },
});
expect(classifyKind(child!.run.friendlyId)).toBe("runOpsId");
}
);
containerTest(
"caller-supplied runFriendlyId wins verbatim and skips residency inheritance",
async ({ prisma, redisOptions }) => {
const { authenticatedEnvironment, taskIdentifier, triggerTaskService } =
await setupResidencyService(prisma, redisOptions);
// Explicit cuid id for the run, and a run-ops id/NEW parent id.
const explicitFriendlyId = RunId.toFriendlyId(generateInternalId());
const parentFriendlyId = RunId.toFriendlyId(generateRunOpsId());
expect(classifyKind(explicitFriendlyId)).toBe("cuid");
expect(classifyKind(parentFriendlyId)).toBe("runOpsId");
const result = await triggerTaskService.call({
taskId: taskIdentifier,
environment: authenticatedEnvironment,
body: { payload: { test: "explicit" }, options: { parentRunId: parentFriendlyId } },
options: { runFriendlyId: explicitFriendlyId },
});
// Caller-supplied id wins verbatim — NOT re-minted to run-ops id despite the NEW parent.
expect(result!.run.friendlyId).toBe(explicitFriendlyId);
}
);
});