Initial Commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
// Ensure `globalThis.crypto` is available in bare-Node vitest runs.
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
type WritableGlobal = { crypto?: Crypto };
|
||||
const g = globalThis as WritableGlobal;
|
||||
if (g.crypto === undefined) {
|
||||
g.crypto = webcrypto as unknown as Crypto;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Real in-package `DiscoveryStrategy` fixture used by unit tests.
|
||||
*
|
||||
* This is NOT a mock. It is a complete, deterministic implementation of the
|
||||
* `@suaveplan/network-bootstrap` `DiscoveryStrategy` contract whose
|
||||
* behaviour is controlled entirely by constructor options. Tests configure
|
||||
* the peer list, the failure mode, and the announce delay to engineer
|
||||
* specific orchestrator-level scenarios (e.g. priority fallback, peer
|
||||
* dedup commutativity, race-to-fastest correctness) without ever calling
|
||||
* `vi.mock` or registering a module-level stub.
|
||||
*
|
||||
* The fixture lives under `tests/setup/` so the coverage gate ignores it
|
||||
* (the `vitest.config.ts` `coverage.exclude` glob covers `tests/**`).
|
||||
*/
|
||||
import type { DiscoveryStrategy, PeerInfo } from "@suaveplan/network-bootstrap";
|
||||
|
||||
export interface TestStrategyOptions {
|
||||
readonly name: string;
|
||||
/** Peers to return from `discover()`. Defaults to `[]`. */
|
||||
readonly peers?: readonly PeerInfo[];
|
||||
/** Wall-clock delay before `discover()` resolves (ms). Default 0. */
|
||||
readonly delayMs?: number;
|
||||
/** If true, `discover()` rejects with `lastError`. */
|
||||
readonly failsDiscover?: boolean;
|
||||
/** If true, `start()` rejects with `lastError`. */
|
||||
readonly failsStart?: boolean;
|
||||
/** If true, `isAvailable()` returns false. */
|
||||
readonly unavailable?: boolean;
|
||||
/** Error to throw when `failsDiscover` or `failsStart` is true. */
|
||||
readonly error?: Error;
|
||||
}
|
||||
|
||||
export class TestStrategy implements DiscoveryStrategy {
|
||||
public readonly name: string;
|
||||
public startedCount = 0;
|
||||
public stoppedCount = 0;
|
||||
public discoverCount = 0;
|
||||
|
||||
private peers: PeerInfo[];
|
||||
private readonly delayMs: number;
|
||||
private failsDiscover: boolean;
|
||||
private readonly failsStart: boolean;
|
||||
private available: boolean;
|
||||
private readonly error: Error;
|
||||
|
||||
public constructor(options: TestStrategyOptions) {
|
||||
this.name = options.name;
|
||||
this.peers = [...(options.peers ?? [])];
|
||||
this.delayMs = options.delayMs ?? 0;
|
||||
this.failsDiscover = options.failsDiscover ?? false;
|
||||
this.failsStart = options.failsStart ?? false;
|
||||
this.available = !(options.unavailable ?? false);
|
||||
this.error =
|
||||
options.error ?? new Error("TestStrategy: induced failure");
|
||||
}
|
||||
|
||||
public async start(_signal?: AbortSignal): Promise<void> {
|
||||
this.startedCount += 1;
|
||||
if (this.failsStart) throw this.error;
|
||||
}
|
||||
|
||||
public async stop(_signal?: AbortSignal): Promise<void> {
|
||||
this.stoppedCount += 1;
|
||||
}
|
||||
|
||||
public async discover(
|
||||
_timeoutMs: number,
|
||||
signal?: AbortSignal
|
||||
): Promise<PeerInfo[]> {
|
||||
this.discoverCount += 1;
|
||||
if (this.delayMs > 0) await wait(this.delayMs, signal);
|
||||
if (this.failsDiscover) throw this.error;
|
||||
if (signal?.aborted) throw makeAbortError();
|
||||
return [...this.peers];
|
||||
}
|
||||
|
||||
public async isAvailable(_signal?: AbortSignal): Promise<boolean> {
|
||||
return this.available;
|
||||
}
|
||||
|
||||
// Test-only setters so a single fixture instance can be re-purposed
|
||||
// mid-run (e.g. to simulate degradation between calls).
|
||||
public setAvailable(value: boolean): void {
|
||||
this.available = value;
|
||||
}
|
||||
|
||||
public setFailsDiscover(value: boolean): void {
|
||||
this.failsDiscover = value;
|
||||
}
|
||||
|
||||
public setPeers(peers: readonly PeerInfo[]): void {
|
||||
this.peers = [...peers];
|
||||
}
|
||||
}
|
||||
|
||||
function wait(ms: number, signal?: AbortSignal): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (signal?.aborted) {
|
||||
reject(makeAbortError());
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
cleanup();
|
||||
resolve();
|
||||
}, ms);
|
||||
const onAbort = (): void => {
|
||||
cleanup();
|
||||
reject(makeAbortError());
|
||||
};
|
||||
signal?.addEventListener("abort", onAbort, { once: true });
|
||||
function cleanup(): void {
|
||||
clearTimeout(timer);
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function makeAbortError(): Error {
|
||||
const err = new Error("Aborted");
|
||||
err.name = "AbortError";
|
||||
return err;
|
||||
}
|
||||
|
||||
export function makePeer(
|
||||
peerId: string,
|
||||
source: string,
|
||||
lastSeen = 0
|
||||
): PeerInfo {
|
||||
return {
|
||||
peerId,
|
||||
addresses: [],
|
||||
lastSeen,
|
||||
source,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user