# HealthPoller ## Purpose `HealthPoller` repeatedly calls a `ServiceFixture`'s `healthCheck()` method at a configurable interval until the service reports healthy or a timeout elapses. It is used internally by `TestEnvironment` during the setup phase, but can also be used standalone when you need fine-grained control over the polling strategy for a single fixture. The injectable `sleep` function makes it fully deterministic in unit tests — no real timers are required. ## Features - **Configurable timeout** — `timeoutMs` (default 120 000 ms) controls the maximum wait window. - **Configurable interval** — `intervalMs` (default 1 000 ms) controls the delay between successive probes. - **Injectable sleep** — supply a fake `sleep` function in tests to avoid real delays and assert exact call counts. - **Structured error** — throws `TestEnvironmentError` (not a generic `Error`) so callers can discriminate on the error type. - **Zero dependencies** — relies only on `ServiceFixture` from the same package; no external runtime dependencies. - **Protected members** — `timeoutMs`, `intervalMs`, and `sleep` are `protected` so subclasses can override behavior. ## Usage: Basic Example ```ts import { HealthPoller } from "@suaveplan/testing-integration/helpers/health-poller"; import type { ServiceFixture } from "@suaveplan/testing-integration/fixture/service-fixture"; const poller = new HealthPoller({ timeoutMs: 30_000, intervalMs: 500 }); // myFixture.healthCheck() is called every 500 ms until true or 30 s elapses await poller.poll(myFixture); ``` ## Usage: Advanced Example Inject a fake sleep function in tests so polling logic can be verified without real delays: ```ts import { describe, it, expect } from "vitest"; import { HealthPoller, TestEnvironmentError } from "@suaveplan/testing-integration/helpers/health-poller"; import type { ServiceFixture } from "@suaveplan/testing-integration/fixture/service-fixture"; function makeFixture(responses: boolean[]): ServiceFixture> { let i = 0; return { name: "db", start: async () => ({}), stop: async () => {}, healthCheck: async () => responses[i++] ?? false, }; } it("retries until healthy", async () => { const sleepCalls: number[] = []; const poller = new HealthPoller({ timeoutMs: 5_000, intervalMs: 200, sleep: async (ms) => { sleepCalls.push(ms); }, }); await poller.poll(makeFixture([false, false, true])); expect(sleepCalls).toEqual([200, 200]); }); it("throws TestEnvironmentError on timeout", async () => { const poller = new HealthPoller({ timeoutMs: 400, intervalMs: 200, sleep: async () => {}, }); await expect(poller.poll(makeFixture([]))).rejects.toBeInstanceOf(TestEnvironmentError); }); ``` ## API Reference ### `HealthPoller` **Constructor:** `new HealthPoller(options?: HealthPollerOptions)` | Option | Type | Default | Description | |---|---|---|---| | `timeoutMs` | `number` | `120_000` | Maximum milliseconds to wait before throwing. | | `intervalMs` | `number` | `1_000` | Milliseconds between `healthCheck()` calls. | | `sleep` | `(ms: number) => Promise` | `setTimeout`-based | Injectable sleep for deterministic testing. | **Methods:** | Method | Signature | Description | |---|---|---| | `poll` | `(fixture: ServiceFixture<…>) => Promise` | Polls the fixture until healthy or timeout. Throws `TestEnvironmentError` on timeout. | ### `TestEnvironmentError` Extends `Error`. The `name` property is always `"TestEnvironmentError"`. Thrown by `HealthPoller.poll()` when the timeout elapses before the fixture becomes healthy. ### `HealthPollerOptions` | Property | Type | Required | Description | |---|---|---|---| | `timeoutMs` | `number` | No | Maximum wait time in milliseconds. | | `intervalMs` | `number` | No | Polling interval in milliseconds. | | `sleep` | `(ms: number) => Promise` | No | Sleep implementation (injectable for tests). | ## Implementation Notes The polling loop tracks `elapsed` time by accumulating `intervalMs` on each iteration rather than reading a real wall clock. This makes the poller's behavior fully deterministic when a fake `sleep` is injected — the timeout fires exactly when `elapsed > timeoutMs`, regardless of real time. In production use (default `setTimeout` sleep), elapsed time approximates real time closely enough for integration test purposes. The timeout check is performed *after* sleeping rather than before to avoid a race where the first call returns unhealthy but elapsed immediately hits the timeout. With the check after sleep, there is always at least one full `intervalMs` of wait before a timeout is declared. The strict `>` comparison (rather than `>=`) means a fixture whose response latency equals exactly `timeoutMs` still gets one final health check after its sleep window, ensuring the timeout budget is truly exhausted before giving up.