Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
# 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<Record<string, unknown>> {
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<void>` | `setTimeout`-based | Injectable sleep for deterministic testing. |
**Methods:**
| Method | Signature | Description |
|---|---|---|
| `poll` | `(fixture: ServiceFixture<…>) => Promise<void>` | 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<void>` | 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.