Initial Commit
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
# 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.
|
||||
106
openspec/archive/2026-05-12-enterprise-network-testing/tasks.md
Normal file
106
openspec/archive/2026-05-12-enterprise-network-testing/tasks.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Tasks: enterprise-network-testing
|
||||
|
||||
**Change ID**: enterprise-network-testing
|
||||
**Package**: `@suaveplan/network-testing`
|
||||
**Target path**: `universal/network/network-testing`
|
||||
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/testing`
|
||||
|
||||
---
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [x] Create `universal/network/network-testing/` with `src/`, `tests/`, and all feature subdirectories.
|
||||
- [x] Write `package.json` at version `0.1.0`: `"sideEffects": false`, exports map, deps per `proposal.md`.
|
||||
- [x] Write `tsconfig.json`, `vitest.config.ts`, scripts.
|
||||
|
||||
---
|
||||
|
||||
## 2. Types and error codes
|
||||
|
||||
- [x] Define all public types in `src/types/common.ts`: `MemoryTransport`, `MemoryTransportPair`, `MemoryTransportOptions`, `FakeWebSocketAdapter`, `FakeWebSocketHandle`, `FakeWebSocketOptions`, `NetworkConditions`, `LatencyDistribution`, `ConditionSimulator`, `SimulationStats`, `TestPeer`, `TestPeerOptions`, `NetworkEventCapture`.
|
||||
- [x] Write `src/types/common.md` (≥200 words, simple + advanced examples).
|
||||
- [x] Define error codes in `src/error-codes.ts`.
|
||||
- [x] Write Zod schemas in `src/types/schemas.ts` + `src/types/schemas.md`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Memory transport (`src/transports/memory-transport/`)
|
||||
|
||||
- [x] Implement `createMemoryTransport(options?)` — returns `MemoryTransportPair`:
|
||||
- Two ends share a queue; `client.send(m)` pushes to `server.messages`, calls `server`'s `onmessage`.
|
||||
- `connect()` sets both ends to `connected`.
|
||||
- `disconnect()` sets both ends to `disconnected`, fires `ondisconnect`.
|
||||
- Injectable `Clock` for `latencyMs` simulation (use `setTimeout` equivalent on clock).
|
||||
- [x] Write `memory-transport.test.ts` (≤500 lines): send client→server, server→client, disconnect, latency.
|
||||
- [x] Write `memory-transport.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 4. Fake WebSocket adapter (`src/transports/fake-websocket/`)
|
||||
|
||||
- [x] Implement `createFakeWebSocketAdapter(options?)`:
|
||||
- `connect(url, protocols?)` — creates a `FakeWebSocketHandle`, stores in `connections`.
|
||||
- `simulateMessage(data)` — calls `onmessage` on all handles.
|
||||
- `simulateError(error)` — calls `onerror` on all handles.
|
||||
- `simulateClose(code?, reason?)` — calls `onclose` on all handles.
|
||||
- `FakeWebSocketHandle.send(data)` — pushes to `sentMessages`.
|
||||
- `autoConnect: true` (default) calls `onopen` immediately after `connect`.
|
||||
- [x] Write `fake-websocket.test.ts` (≤500 lines): connect, send tracked, simulateMessage received, close, error.
|
||||
- [x] Write `fake-websocket.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 5. Condition simulator (`src/simulators/condition-simulator/`)
|
||||
|
||||
- [x] Implement `createConditionSimulator(conditions)`:
|
||||
- `wrap(transport)` — returns a proxy that intercepts `send()`:
|
||||
- Roll packet drop: if `rng.next() < conditions.packetLossRate`, drop (return without sending).
|
||||
- Compute latency: sample from `conditions.latencyMs` distribution using injectable `Rng`.
|
||||
- Add jitter: ± `conditions.jitterMs` random offset.
|
||||
- After delay, call real `transport.send()`.
|
||||
- `setConditions(partial)` — update conditions on the fly.
|
||||
- `reset()` — restore original conditions, clear stats.
|
||||
- Track `sentMessages`, `droppedMessages`, `totalLatencyMs`.
|
||||
- [x] Write `condition-simulator.test.ts` (≤500 lines): packet drop rate, latency added, stats tracked.
|
||||
- [x] Write `condition-simulator.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 6. Test peer factory (`src/factories/test-peer/`)
|
||||
|
||||
- [x] Implement `createTestPeer(options?)`:
|
||||
- Auto-generates `id` (`test-peer-${n}`) and `address` (`127.0.0.1:${port}`).
|
||||
- Fluent builder: `withCapabilities`, `withRegion`, `withHealth`.
|
||||
- `asRegistryEntry()` — returns `RegistryEntry` compatible shape.
|
||||
- `asBootstrapSeed()` — returns `SeedConfig` compatible shape.
|
||||
- [x] Write `test-peer.test.ts` + `test-peer.md`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Network event capture (`src/assertions/event-capture/`)
|
||||
|
||||
- [x] Implement `createNetworkEventCapture(eventBus)`:
|
||||
- Subscribes to all events on the bus; pushes to `events` array.
|
||||
- `waitFor(event, timeout?)` — returns a `Promise` that resolves when the event fires; rejects after timeout.
|
||||
- `assertEventEmitted(event, predicate?)` — throws `NetworkTestingError(NET_TEST_EVENT_NOT_EMITTED)` if not found.
|
||||
- `assertEventCount(event, count)` — throws if actual count !== expected.
|
||||
- `clear()` — empties `events`.
|
||||
- [x] Write `event-capture.test.ts` + `event-capture.md`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Integration test (`tests/integration.test.ts`)
|
||||
|
||||
- [x] Create a `MemoryTransportPair`, send messages, verify receipt.
|
||||
- [x] Wrap a transport with `ConditionSimulator` (50% packet loss), send 100 messages, verify ~50 dropped.
|
||||
- [x] Use `FakeWebSocketAdapter` with a hypothetical client, simulate messages, verify received.
|
||||
|
||||
---
|
||||
|
||||
## 9. Docs and verify
|
||||
|
||||
- [x] All per-file `.md` files per layout in `proposal.md`.
|
||||
- [x] `universal/network/network-testing/README.md`.
|
||||
- [x] `bunx turbo lint typecheck test test:coverage --filter=@suaveplan/network-testing` — all green, 100%.
|
||||
- [x] `bun scripts/publish.ts --package @suaveplan/network-testing`.
|
||||
- [x] Update `CLAUDE.md`. Archive change folder.
|
||||
Reference in New Issue
Block a user