252 lines
7.0 KiB
Markdown
252 lines
7.0 KiB
Markdown
# OpenSpec Change: enterprise-network-testing
|
||
|
||
- **Change ID**: enterprise-network-testing
|
||
- **Package**: `@suaveplan/network-testing`
|
||
- **Category**: universal/network
|
||
- **Target path**: `universal/network/network-testing`
|
||
- **Status**: PROPOSED
|
||
- **Date**: 2026-04-24
|
||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/network-testing.md`
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
Deliver `@suaveplan/network-testing` on the genesis branch from scratch at enterprise quality. The package provides network testing utilities: in-memory transport implementations, fake WebSocket adapters, network condition simulators (latency, jitter, packet loss, bandwidth), test peer factories, clock-controlled timing helpers for network scenarios, and assertion utilities for network events. Genesis adds structured errors via `@suaveplan/error`, Zod schemas for condition simulators, OTel integration for verifying instrumentation in tests, and a comprehensive test harness API.
|
||
|
||
---
|
||
|
||
## Current-state audit (genesis)
|
||
|
||
The package does not exist on genesis yet — this is a fresh build.
|
||
|
||
---
|
||
|
||
## Available workspace deps on genesis
|
||
|
||
| Package | Role |
|
||
|---|---|
|
||
| `@suaveplan/error` (`workspace:^`) | Base class for `NetworkTestingError`; stable error codes |
|
||
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock`, `EventBusLike` — all injectable |
|
||
| `@suaveplan/events` (`workspace:^`) | Real `EventBus` for test event capture |
|
||
| `@suaveplan/testing` (`workspace:^`) | `FakeClock`, `MemoryStore`, testing helpers |
|
||
| `zod` (`catalog:forms`) | Zod schemas for condition simulator config |
|
||
| `@opentelemetry/api` (`catalog:observability`) | OTel no-op for instrumentation testing |
|
||
| `tslib` (`catalog:build`) | Runtime helpers |
|
||
|
||
Dev deps: `vitest`, `@vitest/coverage-istanbul` (all `catalog:testing`).
|
||
|
||
---
|
||
|
||
## Public API surface
|
||
|
||
### In-memory transport
|
||
|
||
```ts
|
||
function createMemoryTransport(options?: MemoryTransportOptions): MemoryTransportPair
|
||
|
||
interface MemoryTransportPair {
|
||
readonly client: MemoryTransport
|
||
readonly server: MemoryTransport
|
||
connect(): void
|
||
disconnect(): void
|
||
}
|
||
|
||
interface MemoryTransport {
|
||
readonly id: string
|
||
readonly state: TransportState
|
||
send(message: unknown, signal?: AbortSignal): Promise<void>
|
||
connect(signal?: AbortSignal): Promise<void>
|
||
disconnect(): void
|
||
on(event: string, handler: (data: unknown) => void): () => void
|
||
messages: unknown[] // captured sent messages
|
||
}
|
||
```
|
||
|
||
### Fake WebSocket adapter
|
||
|
||
```ts
|
||
function createFakeWebSocketAdapter(options?: FakeWebSocketOptions): FakeWebSocketAdapter
|
||
|
||
interface FakeWebSocketAdapter {
|
||
connect(url: string, protocols?: string[]): FakeWebSocketHandle
|
||
simulateMessage(data: string | Uint8Array): void
|
||
simulateError(error: Error): void
|
||
simulateClose(code?: number, reason?: string): void
|
||
readonly connections: ReadonlyArray<{ url: string; handle: FakeWebSocketHandle }>
|
||
}
|
||
|
||
interface FakeWebSocketHandle {
|
||
readonly readyState: number
|
||
readonly sentMessages: ReadonlyArray<string | Uint8Array>
|
||
send(data: string | Uint8Array): void
|
||
close(code?: number, reason?: string): void
|
||
onopen: (() => void) | null
|
||
onclose: ((code: number, reason: string) => void) | null
|
||
onmessage: ((data: string | Uint8Array) => void) | null
|
||
onerror: ((error: Error) => void) | null
|
||
}
|
||
```
|
||
|
||
### Network condition simulator
|
||
|
||
```ts
|
||
function createConditionSimulator(conditions: NetworkConditions): ConditionSimulator
|
||
|
||
interface NetworkConditions {
|
||
latencyMs?: number | LatencyDistribution
|
||
jitterMs?: number
|
||
packetLossRate?: number // 0–1
|
||
bandwidthBps?: number
|
||
disconnectAfterMs?: number
|
||
}
|
||
|
||
interface LatencyDistribution {
|
||
min: number
|
||
max: number
|
||
distribution: 'uniform' | 'normal' | 'bimodal'
|
||
}
|
||
|
||
interface ConditionSimulator {
|
||
wrap<T extends { send: (...args: unknown[]) => Promise<unknown> }>(transport: T): T
|
||
setConditions(conditions: Partial<NetworkConditions>): void
|
||
reset(): void
|
||
readonly stats: SimulationStats
|
||
}
|
||
|
||
interface SimulationStats {
|
||
readonly sentMessages: number
|
||
readonly droppedMessages: number
|
||
readonly totalLatencyMs: number
|
||
readonly avgLatencyMs: number
|
||
}
|
||
```
|
||
|
||
### Test peer factory
|
||
|
||
```ts
|
||
function createTestPeer(options?: TestPeerOptions): TestPeer
|
||
|
||
interface TestPeer {
|
||
readonly id: string
|
||
readonly address: string
|
||
readonly capabilities: string[]
|
||
asRegistryEntry(): RegistryEntry
|
||
asBootstrapSeed(): SeedConfig
|
||
withCapabilities(capabilities: string[]): TestPeer
|
||
withRegion(region: string): TestPeer
|
||
withHealth(healthy: boolean, latencyMs?: number): TestPeer
|
||
}
|
||
|
||
interface TestPeerOptions {
|
||
id?: string
|
||
address?: string
|
||
capabilities?: string[]
|
||
region?: string
|
||
clock?: Clock
|
||
}
|
||
```
|
||
|
||
### Network event assertions
|
||
|
||
```ts
|
||
function createNetworkEventCapture(eventBus: EventBusLike): NetworkEventCapture
|
||
|
||
interface NetworkEventCapture {
|
||
events: unknown[]
|
||
waitFor(event: string, timeout?: number): Promise<unknown>
|
||
assertEventEmitted(event: string, predicate?: (data: unknown) => boolean): void
|
||
assertEventCount(event: string, count: number): void
|
||
clear(): void
|
||
}
|
||
```
|
||
|
||
### Options and error codes
|
||
|
||
```ts
|
||
interface MemoryTransportOptions {
|
||
latencyMs?: number
|
||
clock?: Clock
|
||
}
|
||
|
||
interface FakeWebSocketOptions {
|
||
autoConnect?: boolean
|
||
clock?: Clock
|
||
}
|
||
|
||
type NetworkTestingErrorCode =
|
||
| 'NET_TEST_TIMEOUT'
|
||
| 'NET_TEST_EVENT_NOT_EMITTED'
|
||
| 'NET_TEST_CONNECT_FAILED'
|
||
|
||
function registerNetworkTestingErrorCodes(): void
|
||
```
|
||
|
||
---
|
||
|
||
## Genesis additions
|
||
|
||
1. **Structured `NetworkTestingError`** with stable `NET_TEST.*` codes
|
||
2. **Zod schemas** for `NetworkConditions`, `MemoryTransportOptions`
|
||
3. **`MemoryTransportPair`** — bidirectional in-process transport for unit tests
|
||
4. **`FakeWebSocketAdapter`** — injectable into `@suaveplan/network-websockets` clients
|
||
5. **`ConditionSimulator`** — wraps any transport and injects latency/jitter/packet-loss via injectable `Clock` and `Rng`
|
||
6. **`TestPeer` factory** — fluent builder compatible with `@suaveplan/network-registry` and `@suaveplan/network-bootstrap`
|
||
7. **`NetworkEventCapture`** — wraps an `EventBus` to capture and assert emitted events in tests
|
||
|
||
---
|
||
|
||
## File layout
|
||
|
||
```text
|
||
universal/network/network-testing/
|
||
package.json
|
||
tsconfig.json
|
||
vitest.config.ts
|
||
src/
|
||
index.ts
|
||
error-codes.ts
|
||
constants/
|
||
index.ts
|
||
types/
|
||
common.ts
|
||
common.md
|
||
transports/
|
||
memory-transport/
|
||
memory-transport.ts
|
||
memory-transport.md
|
||
memory-transport.test.ts
|
||
index.ts
|
||
fake-websocket/
|
||
fake-websocket.ts
|
||
fake-websocket.md
|
||
fake-websocket.test.ts
|
||
index.ts
|
||
simulators/
|
||
condition-simulator/
|
||
condition-simulator.ts
|
||
condition-simulator.md
|
||
condition-simulator.test.ts
|
||
index.ts
|
||
factories/
|
||
test-peer/
|
||
test-peer.ts
|
||
test-peer.md
|
||
test-peer.test.ts
|
||
index.ts
|
||
assertions/
|
||
event-capture/
|
||
event-capture.ts
|
||
event-capture.md
|
||
event-capture.test.ts
|
||
index.ts
|
||
tests/
|
||
integration.test.ts
|
||
```
|
||
|
||
---
|
||
|
||
## Impact
|
||
|
||
**Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/testing` must be published.
|