Files
suaveplan/docs/old-network-planning/test-isolation-implementation-guide.md
2026-05-27 23:29:53 +01:00

572 lines
16 KiB
Markdown

# Implementation Guide: Fixing Test Isolation Issues
This guide provides a step-by-step approach to resolving the test isolation issues in the network library. By implementing these changes, the tests that currently fail when run together will pass, maintaining proper isolation between test cases.
## Phase 1: Refactor TimerController
### Step 1: Update TimerController Class
First, we need to modify the TimerController to support multiple instances:
```typescript
// libs/network/src/utils/timer-controller.ts
export class TimerController {
// Remove static instance
// private static _instance: TimerController;
private _currentTime = 0;
private _timers: Map<number, ScheduledTimer> = new Map();
private _nextTimerId = 1;
/**
* Create a new TimerController instance
* This replaces the singleton getInstance method
*/
public static createInstance(): TimerController {
return new TimerController();
}
// Keep the original getInstance for backward compatibility
// but have it create a new instance each time in test environment
public static getInstance(): TimerController {
if (process.env.NODE_ENV === 'test') {
console.warn('Using TimerController.getInstance() in tests may cause isolation issues.');
return new TimerController();
}
if (!TimerController._instance) {
TimerController._instance = new TimerController();
}
return TimerController._instance;
}
// Remaining methods stay the same
}
// Add factory function for tests
export function createTestTimerController(): TimerController {
return TimerController.createInstance();
}
```
### Step 2: Create Timer Context Utility
Next, create a utility to provide contextual timers for tests:
```typescript
// libs/network/src/utils/test/timer-context.ts
import { TimerController } from '../timer-controller';
interface TimerContext {
timer: TimerController;
getCurrentTime: () => number;
setTimeout: (callback: () => void, delay: number) => number;
setInterval: (callback: () => void, interval: number) => number;
clearTimeout: (id: number) => void;
clearInterval: (id: number) => void;
advanceTime: (timeMs: number) => void;
reset: () => void;
}
/**
* Create a timer context for use in tests
* This provides an isolated timer instance with utility methods
*/
export function createTimerContext(): TimerContext {
const timer = TimerController.createInstance();
return {
timer,
getCurrentTime: () => timer.getCurrentTime(),
setTimeout: (callback, delay) => timer.setTimeout(callback, delay),
setInterval: (callback, interval) => timer.setInterval(callback, interval),
clearTimeout: (id) => timer.clearTimeout(id),
clearInterval: (id) => timer.clearInterval(id),
advanceTime: (timeMs) => timer.advanceTime(timeMs),
reset: () => timer.reset()
};
}
```
## Phase 2: Fix MemoryTransport Tests
### Step 1: Update MemoryTransport Class
Modify MemoryTransport to accept a TimerController and add registry reset:
```typescript
// libs/network/src/transport/memory-transport.ts
export interface MemoryTransportOptions {
eventEmitter?: INetworkEventEmitter;
simulationOptions?: MemoryTransportSimulationOptions;
timerController?: TimerController; // Add this
}
export class MemoryTransport extends BaseTransport {
private static _instances: Map<string, MemoryTransport> = new Map();
private _simOptions: MemoryTransportSimulationOptions;
private _timer: TimerController;
constructor(id: string, options?: MemoryTransportOptions) {
super(id, options?.eventEmitter);
this._simOptions = options?.simulationOptions || {};
this._timer = options?.timerController || TimerController.getInstance();
MemoryTransport._instances.set(id, this);
}
// Add this static method for test cleanup
public static resetRegistry(): void {
MemoryTransport._instances.clear();
}
// Update time-sensitive methods to use the timer
private async _simulateDelay(baseDelay: number): Promise<void> {
let delay = baseDelay;
if (this._simOptions.jitter) {
const jitterAmount = (Math.random() * 2 - 1) * this._simOptions.jitter;
delay = Math.max(0, delay + jitterAmount);
}
if (delay > 0) {
// Replace setTimeout with timer
return new Promise<void>(resolve => {
this._timer.setTimeout(() => resolve(), delay);
this._timer.advanceTime(delay);
});
}
}
}
```
### Step 2: Update MemoryTransport Tests
Modify the tests to use isolated timers:
```typescript
// libs/network/src/transport/memory-transport.spec.ts
import { TimerController } from '../utils/timer-controller';
describe('MemoryTransport', () => {
let transport1: MemoryTransport;
let transport2: MemoryTransport;
let eventEmitter1: NetworkEventEmitter;
let eventEmitter2: NetworkEventEmitter;
let timer1: TimerController;
let timer2: TimerController;
beforeEach(() => {
// Reset static registry
MemoryTransport.resetRegistry();
// Create isolated timer controllers
timer1 = TimerController.createInstance();
timer2 = TimerController.createInstance();
eventEmitter1 = new NetworkEventEmitter();
eventEmitter2 = new NetworkEventEmitter();
// Create transports with isolated timers
transport1 = new MemoryTransport('transport-1', {
eventEmitter: eventEmitter1,
timerController: timer1
});
transport2 = new MemoryTransport('transport-2', {
eventEmitter: eventEmitter2,
timerController: timer2
});
});
afterEach(() => {
// Clean up
transport1.disconnect().catch(e => console.error('Cleanup error:', e));
transport2.disconnect().catch(e => console.error('Cleanup error:', e));
MemoryTransport.resetRegistry();
// Reset timers
timer1.reset();
timer2.reset();
});
// Update the time-sensitive tests
describe('Simulated network conditions', () => {
it('should simulate connection delay', async () => {
const testTimer = TimerController.createInstance();
const simulationOptions: MemoryTransportSimulationOptions = {
connectionDelay: 50
};
const transport = new MemoryTransport('delayed-transport', {
simulationOptions,
eventEmitter: new NetworkEventEmitter(),
timerController: testTimer
});
// Capture current timer time instead of Date.now()
const startTime = testTimer.getCurrentTime();
// Start connection but don't await yet
const connectPromise = transport.connect();
// Advance time
testTimer.advanceTime(60); // More than the delay
// Now wait for connection to complete
await connectPromise;
const endTime = testTimer.getCurrentTime();
expect(endTime - startTime).toBeGreaterThanOrEqual(50);
// Clean up
await transport.disconnect();
});
it('should simulate latency', async () => {
const testTimer = TimerController.createInstance();
const simulationOptions: MemoryTransportSimulationOptions = {
latency: 50
};
const testTransport = new MemoryTransport('latency-transport', {
simulationOptions,
eventEmitter: new NetworkEventEmitter(),
timerController: testTimer
});
const receiver = new MemoryTransport('receiver', {
eventEmitter: new NetworkEventEmitter(),
timerController: testTimer
});
// Connect both transports
await testTransport.connect();
await receiver.connect();
// Capture start time
const startTime = testTimer.getCurrentTime();
// Start send but don't await yet
const sendPromise = testTransport.sendTo('receiver', { test: true });
// Advance time
testTimer.advanceTime(60); // More than the latency
// Now wait for send to complete
await sendPromise;
const endTime = testTimer.getCurrentTime();
expect(endTime - startTime).toBeGreaterThanOrEqual(50);
// Clean up
await testTransport.disconnect();
await receiver.disconnect();
});
});
});
```
## Phase 3: Fix MessageBatch Tests
### Step 1: Update MessageBatch Class
Modify MessageBatch to support TimerController:
```typescript
// libs/network/src/router/message-batch.ts
import { TimerController } from '../utils/timer-controller';
export interface MessageBatchOptions {
timerController?: TimerController;
}
export class MessageBatch {
private _timer: TimerController;
private _creationTime: number;
constructor(
private _priority: MessagePriority,
private _maxSize: number,
options?: MessageBatchOptions
) {
this._timer = options?.timerController || TimerController.getInstance();
this._creationTime = this._timer.getCurrentTime();
}
// Update age getter to use timer
get age(): number {
return this._timer.getCurrentTime() - this._creationTime;
}
// Other methods stay the same
}
```
### Step 2: Update MessageBatch Tests
```typescript
// libs/network/src/router/message-batch.spec.ts
import { TimerController } from '../utils/timer-controller';
describe('MessageBatch', () => {
let batch: MessageBatch;
let timer: TimerController;
beforeEach(() => {
timer = TimerController.createInstance();
batch = new MessageBatch(MessagePriority.MEDIUM, 3, { timerController: timer });
});
afterEach(() => {
timer.reset();
});
// Update the time-sensitive test
describe('Age tracking', () => {
it('should track batch age', () => {
// Get initial age
const initialAge = batch.age;
expect(initialAge).toBe(0);
// Advance timer instead of waiting
timer.advanceTime(10);
// Check age increased
expect(batch.age).toBeGreaterThan(initialAge);
expect(batch.age).toBe(10);
// Advance more
timer.advanceTime(15);
expect(batch.age).toBe(25);
});
});
});
```
## Phase 4: Fix TransportTester Tests
### Step 1: Update TransportTester Class
```typescript
// libs/network/src/transport/pdk/transport-testing.ts
import { TimerController } from '../../utils/timer-controller';
export interface TestOptions {
timerController?: TimerController;
}
export class TransportTester {
/**
* Test message delivery between two transports
*/
public static async testMessageDelivery(
sender: ITransport,
receiver: ITransport,
message: any = { test: true },
timeoutMs: number = 1000,
options?: TestOptions
): Promise<boolean> {
const timer = options?.timerController || TimerController.getInstance();
let messageReceived = false;
// Connect both transports
await sender.connect();
await receiver.connect();
// Listen for message received event
const onMessageReceived = (msg: any, senderId: string) => {
if (senderId === sender.id) {
messageReceived = true;
}
};
receiver.on(TransportEvents.MESSAGE_RECEIVED, onMessageReceived);
try {
// Send the message
await sender.sendTo(receiver.id, message);
// Instead of a timeout, use timer advancement
if (!messageReceived) {
timer.advanceTime(timeoutMs);
await timer.waitForPromises();
}
return messageReceived;
} catch (error) {
console.error('Error in testMessageDelivery:', error);
return false;
} finally {
// Clean up
receiver.off(TransportEvents.MESSAGE_RECEIVED, onMessageReceived);
}
}
// Update other methods similarly
}
```
### Step 2: Update MockTransport for Tests
```typescript
// Update in transport-testing.spec.ts
class MockTransport implements ITransport {
private _timer: TimerController;
constructor(
id: string,
eventEmitter?: NetworkEventEmitter,
timerController?: TimerController
) {
this.id = id;
this._eventEmitter = eventEmitter || new NetworkEventEmitter();
this._timer = timerController || TimerController.getInstance();
}
// Update delay method
private async _delay(ms: number): Promise<void> {
if (ms <= 0) return;
return new Promise<void>(resolve => {
this._timer.setTimeout(() => resolve(), ms);
this._timer.advanceTime(ms);
});
}
}
```
### Step 3: Update TransportTester Tests
```typescript
// libs/network/src/transport/pdk/transport-testing.spec.ts
describe('TransportTester', () => {
let sender: MockTransport;
let receiver: MockTransport;
let timer: TimerController;
beforeEach(() => {
timer = TimerController.createInstance();
sender = new MockTransport('sender', undefined, timer);
receiver = new MockTransport('receiver', undefined, timer);
});
afterEach(() => {
timer.reset();
});
// Update time-sensitive tests
describe('testMessageDelivery', () => {
it('should return true when message is delivered', async () => {
// Setup receiver to simulate receiving messages
sender.on(TransportEvents.MESSAGE_SENT, (msg) => {
receiver.simulateMessageReceived(msg, sender.id);
});
const result = await TransportTester.testMessageDelivery(
sender,
receiver,
{ test: true },
1000,
{ timerController: timer }
);
expect(result).toBe(true);
});
});
});
```
## Phase 5: Fix NetworkNodePhase4 Tests
Ensure proper cleanup in tests and use isolated timer instances:
```typescript
// libs/network/src/node/network-node-phase4.spec.ts
describe('NetworkNodePhase4', () => {
let nodePhase4: NetworkNodePhase4;
let eventEmitter: NetworkEventEmitter;
let timer: TimerController;
beforeEach(() => {
// Reset any shared state
MemoryTransport.resetRegistry();
// Create isolated timer
timer = TimerController.createInstance();
// Create a new event emitter for each test
eventEmitter = new NetworkEventEmitter();
// Reset mocks
jest.clearAllMocks();
// Create the Phase 4 node extension with timer
nodePhase4 = new NetworkNodePhase4(mockNode, eventEmitter, { timerController: timer });
});
afterEach(() => {
nodePhase4.dispose();
timer.reset();
MemoryTransport.resetRegistry();
});
// Update time-sensitive tests
});
```
## Phase 6: Fix VotingManager Tests
Update the VotingManager to use TimerController:
```typescript
// libs/network/src/voting/voting-manager.spec.ts
describe('VotingManager', () => {
let votingManager: VotingManager;
let eventEmitter: NetworkEventEmitter;
let timer: TimerController;
beforeEach(() => {
// Create isolated timer
timer = TimerController.createInstance();
// Create a new event emitter for each test
eventEmitter = new NetworkEventEmitter();
// Reset mocks
jest.clearAllMocks();
// Set test environment variable
process.env['NODE_ENV'] = 'test';
// Create the voting manager with timer
votingManager = new VotingManager(
mockNode,
eventEmitter,
mockMetricsManager,
{
timerController: timer
}
);
});
afterEach(() => {
votingManager.dispose();
timer.reset();
});
// Update timing-dependent tests
});
```
## Final Steps
1. After implementing these changes, run individual test suites to verify they still pass
2. Run the full test suite to ensure isolation issues are resolved
3. Document the approach in the project README to ensure future tests maintain proper isolation
By systematically implementing these changes, we will ensure proper test isolation and eliminate the test failures that occur when running the full test suite.