// 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) & Record; 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(); // 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) { // @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( "~/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}`, }));