86 lines
3.5 KiB
TypeScript
86 lines
3.5 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
// Importing `~/v3/mollifier/mollifierDrainer.server` (below) transitively
|
|
// loads `~/v3/runEngine.server`, whose top-level `singleton(...)` call
|
|
// eagerly constructs a RunEngine. That spins up Prisma + Redis workers
|
|
// that try to connect to localhost — which in CI (no PG, no Redis)
|
|
// produces an unhandled `PrismaClientInitializationError` that fails
|
|
// the test run even though the assertions all pass. Mocking the
|
|
// runEngine module short-circuits the singleton so no worker starts.
|
|
vi.mock("~/v3/runEngine.server", () => ({ engine: {} }));
|
|
// Same problem: prisma.server.ts's top-level singleton tries to open a
|
|
// PG client. The test never makes a query; an empty stub is enough.
|
|
vi.mock("~/db.server", () => ({ prisma: {}, $replica: {} }));
|
|
|
|
import { MollifierConfigurationError } from "~/v3/mollifier/mollifierDrainer.server";
|
|
import { initMollifierDrainerWorker } from "~/v3/mollifierDrainerWorker.server";
|
|
|
|
// Pins the error-classification policy inside the bootstrap's catch:
|
|
// deterministic misconfig errors propagate (so a deploy fails loud
|
|
// rather than silently disabling the drainer), and anything else is
|
|
// logged-and-swallowed (so a transient Redis blip during boot doesn't
|
|
// take the whole webapp down). The corresponding production-path
|
|
// integration is the call at `entry.server.tsx`: a sync throw out of
|
|
// `initMollifierDrainerWorker` propagates to the module top level
|
|
// BEFORE `process.on("uncaughtException", ...)` is registered, so Node
|
|
// crashes with a stack trace and exit code 1 — which is exactly what we
|
|
// want from the orchestrator's health-check perspective.
|
|
describe("initMollifierDrainerWorker error classification", () => {
|
|
it("rethrows MollifierConfigurationError so the process can crash on misconfig", () => {
|
|
const misconfig = new MollifierConfigurationError(
|
|
"TRIGGER_MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS must be at least 1000ms below GRACEFUL_SHUTDOWN_TIMEOUT"
|
|
);
|
|
|
|
expect(() =>
|
|
initMollifierDrainerWorker({
|
|
isEnabled: () => true,
|
|
getDrainer: () => {
|
|
throw misconfig;
|
|
},
|
|
})
|
|
).toThrow(MollifierConfigurationError);
|
|
});
|
|
|
|
it("rethrows when the error carries the marker name even if instanceof fails (dev-realm hot-reload fallback)", () => {
|
|
// Simulate the cross-realm case where the consumer's instanceof
|
|
// check sees a different class instance from the one the throw
|
|
// site used. The bootstrap's `.name === "MollifierConfigurationError"`
|
|
// fallback must catch this so dev hot-reload doesn't silently
|
|
// suppress misconfig errors.
|
|
const cousin = new Error("buffer not initialised");
|
|
cousin.name = "MollifierConfigurationError";
|
|
|
|
expect(() =>
|
|
initMollifierDrainerWorker({
|
|
isEnabled: () => true,
|
|
getDrainer: () => {
|
|
throw cousin;
|
|
},
|
|
})
|
|
).toThrow(cousin);
|
|
});
|
|
|
|
it("swallows non-configuration errors so transient init failures don't take the webapp down", () => {
|
|
expect(() =>
|
|
initMollifierDrainerWorker({
|
|
isEnabled: () => true,
|
|
getDrainer: () => {
|
|
throw new Error("transient redis blip during buffer init");
|
|
},
|
|
})
|
|
).not.toThrow();
|
|
});
|
|
|
|
it("is a no-op when the drainer is disabled for this replica", () => {
|
|
let factoryCalled = false;
|
|
initMollifierDrainerWorker({
|
|
isEnabled: () => false,
|
|
getDrainer: () => {
|
|
factoryCalled = true;
|
|
return null;
|
|
},
|
|
});
|
|
expect(factoryCalled).toBe(false);
|
|
});
|
|
});
|