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

166 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Load apps/webapp/.env into process.env so env.server's top-level
// EnvironmentSchema.parse(process.env) succeeds in vitest workers.
import { config } from "dotenv";
import path from "node:path";
import { vi } from "vitest";
import type * as IORedisModule from "ioredis";
import type * as TaskMetadataCacheModule from "~/services/taskMetadataCache.server";
config({ path: path.resolve(__dirname, "../.env") });
// CI has no .env and no REDIS_HOST/REDIS_PORT, so import-time guards like
// autoIncrementCounter.server.ts throw and their suites fail to collect. Default
// the pair — the ioredis mock below forces lazyConnect, so nothing ever dials.
process.env.REDIS_HOST ??= "localhost";
process.env.REDIS_PORT ??= "6379";
// Worker singletons construct a RedisWorker at import time whose ioredis client
// connects eagerly, so any test importing the service graph opens real Redis
// connections on import — which floods and fails in CI (no Redis). Mock them to
// no-op stubs. Only the worker modules are mocked, never the run store
// (~/v3/runStore.server, ~/db.server), which store-routing tests need real.
function createWorkerStub() {
return {
start: vi.fn(),
stop: vi.fn(),
enqueue: vi.fn().mockResolvedValue(undefined),
enqueueOnce: vi.fn().mockResolvedValue(undefined),
reschedule: vi.fn().mockResolvedValue(undefined),
cancel: vi.fn().mockResolvedValue(undefined),
ack: vi.fn().mockResolvedValue(undefined),
};
}
vi.mock("~/v3/commonWorker.server", () => ({ commonWorker: createWorkerStub() }));
vi.mock("~/v3/batchTriggerWorker.server", () => ({ batchTriggerWorker: createWorkerStub() }));
vi.mock("~/v3/legacyRunEngineWorker.server", () => ({
legacyRunEngineWorker: createWorkerStub(),
}));
vi.mock("~/v3/alertsWorker.server", () => ({ alertsWorker: createWorkerStub() }));
// RunEngine, MarQS, devPubSub and the socket.io server are further singletons
// that open eager ioredis connections at import via the same pattern. No test
// uses these app-level singletons directly (store-routing tests build their own
// engine and run store), so stub them to no-op proxies.
// Recursive no-op proxy: property access at any depth returns another callable
// no-op proxy, so real service tests reaching nested singleton methods (e.g.
// engine.runQueue.updateEnvConcurrencyLimits) don't break on an intermediate stub.
type NoopProxyFn = ((...args: unknown[]) => Promise<undefined>) & Record<string, unknown>;
const noopProxy = (): NoopProxyFn => {
const fn = () => Promise.resolve(undefined);
return new Proxy(fn, {
get: (_target, prop) => (prop === "then" ? undefined : noopProxy()),
apply: () => Promise.resolve(undefined),
}) as unknown as NoopProxyFn;
};
// Beyond the modules mocked above, dozens more app modules construct an
// ioredis client at import time pointed at env-configured Redis, and ioredis
// dials on construction — in CI (no Redis service) that floods ECONNREFUSED at
// shard scale. Force `lazyConnect: true` on every client instead: import-time
// singletons construct but never dial, while anything that actually issues a
// command (tests against live testcontainers) connects on first command
// exactly as before.
vi.mock("ioredis", async (importOriginal) => {
const actual = await importOriginal<typeof IORedisModule>();
// Normalize ioredis's overloaded ctor args — (), (port), (path),
// (port, host), (opts), (port, opts), (port, host, opts), (path, opts) —
// so lazyConnect lands in the options object in every form.
function withLazyConnect(args: unknown[]): unknown[] {
if (args.length === 0) {
return [{ lazyConnect: true }];
}
const last = args[args.length - 1];
if (typeof last === "object" && last !== null) {
return [...args.slice(0, -1), { ...last, lazyConnect: true }];
}
return [...args, { lazyConnect: true }];
}
class LazyRedis extends actual.Redis {
constructor(...args: unknown[]) {
// @ts-expect-error forwarding ioredis's overloaded ctor args
super(...withLazyConnect(args));
}
}
class LazyCluster extends actual.Cluster {
constructor(startupNodes: unknown, options?: Record<string, unknown>) {
// @ts-expect-error forwarding ioredis's ctor args
super(startupNodes, { ...options, lazyConnect: true });
}
}
// Keep the `Redis.Cluster` static alias (`new Redis.Cluster(...)`) working.
// The base class exposes `Cluster` as a getter-only static, so define our
// own property rather than assigning through the inherited getter.
Object.defineProperty(LazyRedis, "Cluster", { value: LazyCluster });
return {
...actual,
default: LazyRedis,
Redis: LazyRedis,
Cluster: LazyCluster,
};
});
// alertsRateLimiter.check() is invoked at runtime by deliverAlert; against
// env-configured Redis each check burns ~20 reconnect cycles before its
// caught error, stalling alert-path tests into timeouts. Allow everything.
vi.mock("~/v3/alertsRateLimiter.server", () => ({
alertsRateLimiter: { check: vi.fn().mockResolvedValue({ allowed: true }) },
}));
// tracePubSub.publish() runs inside eventRepository writes; each publish to
// env-configured Redis stalls ~20 reconnect cycles (errors are allSettled-
// swallowed but awaited), timing out any test that records trace events.
vi.mock("~/v3/services/tracePubSub.server", async () => {
const { EventEmitter } = await import("node:events");
return {
tracePubSub: {
publish: vi.fn().mockResolvedValue(undefined),
subscribeToTrace: vi.fn().mockResolvedValue({
unsubscribe: vi.fn().mockResolvedValue(undefined),
eventEmitter: new EventEmitter(),
}),
},
TracePubSub: class {},
};
});
// Same runtime-stall shape for the task metadata cache (queues concern). CI
// leaves TASK_META_CACHE_REDIS_HOST unset and gets the Noop implementation;
// pin the Noop cache here so env-configured local runs behave identically.
vi.mock("~/services/taskMetadataCacheInstance.server", async () => {
const { NoopTaskMetadataCache } = await vi.importActual<typeof TaskMetadataCacheModule>(
"~/services/taskMetadataCache.server"
);
return { taskMetadataCacheInstance: new NoopTaskMetadataCache() };
});
// The org-data-stores registry singleton is constructed at import (transitively via
// the ClickHouse factory instance, which many presenters pull in). Its ctor fires a
// `forever` pRetry(loadFromDatabase) plus a setInterval reload against db.server's
// $replica; in CI (no Postgres) those retry forever, blocking the worker until any
// awaiting test's hook times out. Stub the instance to a no-op — no unit test uses
// this singleton (the registry-behavior tests construct the class directly).
vi.mock("~/services/dataStores/organizationDataStoresRegistryInstance.server", () => ({
organizationDataStoresRegistry: {
isReady: Promise.resolve(),
isLoaded: true,
get: vi.fn().mockReturnValue(null),
reload: vi.fn().mockResolvedValue(undefined),
loadFromDatabase: vi.fn().mockResolvedValue(undefined),
},
}));
vi.mock("~/v3/runEngine.server", () => ({ engine: noopProxy() }));
vi.mock("~/v3/marqs/index.server", () => ({ marqs: noopProxy(), MarQS: class {} }));
vi.mock("~/v3/marqs/devPubSub.server", () => ({ devPubSub: noopProxy() }));
vi.mock("~/v3/handleSocketIo.server", () => ({
socketIo: noopProxy(),
roomFromFriendlyRunId: (id: string) => `room:${id}`,
}));