Files
HealthLog/vitest.setup.ts
T
Marc-André Bombeck bdf3ffb5da test: settle detached background work before a test file tears down
The unit suite reported all 1937 files green and then failed the job with
`EnvironmentTeardownError: Closing rpc while "onUserConsoleLog" was
pending`, naming a different scoring test each time.

Vitest replaces globalThis.console with a sink that forwards every write
to the main thread as an onUserConsoleLog RPC call. When a worker
finishes a test file it awaits the calls in flight and then rejects
whatever was started after that point, so a console write from work that
outlived its test lands in the gap and fails the run. The blame goes to
the file being torn down rather than to the test that started the work,
which is why the named file never logged anything itself.

Three paths detach a promise and log on the handle: fireAndForget, the
stale-while-revalidate cache refresh, and the offline query persister.
They now register the handled handle in a set that both suite setups
drain in afterEach, so no test leaves work in flight. The registry is
inert outside test: nothing there awaits it, and holding a reference to
an unsettled promise would retain it for the life of a worker. No
production behaviour changes, so there is no changelog entry.

Evidence. A focused harness that starts failing background tasks and
returns hit the teardown error 10 runs out of 10 before the change and
0 out of 10 after. Instrumenting the real suite showed two detached
console writes per run escaping their test, both from the rollup tail;
after the change, none, and thirteen writes per run now complete inside
the test that started them.

The guard asserts the behaviour, not a count: a registered task must
keep settleBackgroundTasks from resolving ahead of it, and the set must
drain back to empty. It goes red on unregistered work, on a disabled
registry, on an unwrapped call site, on a sweep that matches nothing,
and on a setup that drops the hook.
2026-09-03 05:30:08 +02:00

50 lines
2.2 KiB
TypeScript

/**
* Unit-test setup.
*
* Seed the client-side locale cache with every message bundle. In
* production only EN ships statically; the active locale arrives as an
* RSC prop from the root layout and other locales load via dynamic
* import on switch (see `src/lib/i18n/load-locale.ts`). Tests mount
* `I18nProvider` standalone — often with `renderToStaticMarkup`, which
* is synchronous and cannot await a dynamic import — so the cache is
* primed up front and `t()` resolves every locale on the first render,
* exactly like the server-handoff path does in the app.
*/
import { afterEach } from "vitest";
import { settleBackgroundTasks } from "@/lib/logging/background-tasks";
import { primeMessages } from "@/lib/i18n/load-locale";
import deMessages from "./messages/de.json";
import enMessages from "./messages/en.json";
import esMessages from "./messages/es.json";
import frMessages from "./messages/fr.json";
import itMessages from "./messages/it.json";
import plMessages from "./messages/pl.json";
import koMessages from "./messages/ko.json";
primeMessages("de", deMessages);
primeMessages("en", enMessages);
primeMessages("es", esMessages);
primeMessages("fr", frMessages);
primeMessages("it", itMessages);
primeMessages("pl", plMessages);
primeMessages("ko", koMessages);
/**
* Leave no background work in flight when a test ends.
*
* Several paths start a promise, return without awaiting it, and log on the
* detached handle so the failure is not swallowed. Vitest forwards every
* console write to the main thread as an `onUserConsoleLog` RPC call and
* rejects whatever is still pending on that channel while it tears a test file
* down, so a write that lands in that window fails the whole run with
* `EnvironmentTeardownError: Closing rpc while "onUserConsoleLog" was pending`
* — blamed on the file the worker was tearing down, which is not the test that
* started the work and need not log anything of its own. Awaiting the
* registered tasks here removes the precondition instead of muting the
* console, which would have thrown away a real signal and left the
* non-determinism in place. See `src/lib/logging/background-tasks.ts`.
*/
afterEach(async () => {
await settleBackgroundTasks();
});