8.0 KiB
Analysis of Test Failures in Network Library
Failing Tests Overview
When running bun test libs/network/src, these tests fail:
✗ MemoryTransport > Simulated network conditions > should simulate connection delay
✗ MemoryTransport > Simulated network conditions > should simulate latency
✗ VotingManager > session management > should get vote history
✗ NetworkNodePhase4 > initialization > should register core network reliability calculator
✗ NetworkNodePhase4 > host selection > should select the best host
✗ MessageBatch > Age tracking > should track batch age
✗ TransportTester > testMessageDelivery > should return true when message is delivered
✗ TransportTester > testReliability > should report perfect reliability when all messages delivered
✗ TransportTester > testReliability > should report partial reliability when some messages lost
However, when running each test file individually, all tests pass. This indicates an issue with test isolation.
Root Cause Analysis for Each Failing Test
1. MemoryTransport - Connection Delay & Latency Tests
File: libs/network/src/transport/memory-transport.spec.ts
Issues:
- Both tests use real
setTimeoutfor time-based assertions - The
MemoryTransportclass uses a static_instancesMap that isn't properly reset between tests - Tests make timing assertions that can be affected by other running tests
Relevant code:
it('should simulate connection delay', async () => {
const simulationOptions: MemoryTransportSimulationOptions = {
connectionDelay: 50
};
const transport = new MemoryTransport('delayed-transport', {
simulationOptions,
eventEmitter: new NetworkEventEmitter()
});
const startTime = Date.now();
await transport.connect();
const endTime = Date.now();
expect(endTime - startTime).toBeGreaterThanOrEqual(50);
});
Solution:
- Update
MemoryTransportto accept aTimerControllerinstance - Add a static
resetRegistrymethod to clear all instances - Refactor tests to use
TimerControllerfor time simulation
2. VotingManager - Vote History Test
File: libs/network/src/voting/voting-manager.spec.ts
Issues:
- The test creates and completes multiple sessions
- Some sessions may be added to history out of order due to timing issues
- Depends on proper cleanup between tests
Relevant code:
it('should get vote history', () => {
// Create a vote session
const sessionId = votingManager.createVoteSession(VoteType.HOST_SELECTION, 'session');
// Complete the session to move it to history
const session = votingManager.getSession(sessionId);
if (session) {
// Add some votes
(session as any)._testAddVote({
voteId: 'vote1',
voterId: 'node1',
value: { selectedHost: 'node1' },
timestamp: Date.now()
});
// Force completion
(session as any)._testSetStatus(VoteSessionStatus.COMPLETED);
}
// Get vote history
const history = votingManager.getVoteHistory();
// Verify history
expect(history.length).toBe(1);
expect(history[0].id).toBe(sessionId);
});
Solution:
- Ensure
VotingManagerproperly cleans up between tests - Use a consistent timestamp source that can be controlled
- Improve assertion to check if the expected session is in the history, not just the count
3. NetworkNodePhase4 - Initialization and Host Selection
File: libs/network/src/node/network-node-phase4.spec.ts
Issues:
- The test creates multiple extensions in the same test file
- Each extension registers calculators with shared resources
- The host selection test depends on mocked responses that could be affected by other tests
Relevant code:
it('should register core network reliability calculator', () => {
// Spy on metrics manager
const registerCalculatorSpy = jest.spyOn(nodePhase4.metricsManager, 'registerCalculator');
// Create a new extension to verify registration
const newExtension = new NetworkNodePhase4(mockNode, eventEmitter);
// Verify calculator was registered
expect(registerCalculatorSpy).toHaveBeenCalled();
// Clean up
newExtension.dispose();
});
Solution:
- Ensure proper cleanup between tests with more thorough
dispose()methods - Isolate dependencies so that new instances don't interfere with each other
- Replace event emitter instances in each test
4. MessageBatch - Age Tracking
File: libs/network/src/router/message-batch.spec.ts
Issues:
- Uses real
setTimeoutwhich can be affected by other tests - Depends on actual timing for assertions
Relevant code:
it('should track batch age', async () => {
// Create a batch and wait a bit
const initialAge = batch.age;
// Wait at least 10ms
await new Promise(resolve => setTimeout(resolve, 10));
// Age should have increased
expect(batch.age).toBeGreaterThan(initialAge);
expect(batch.age).toBeGreaterThanOrEqual(10);
});
Solution:
- Update
MessageBatchto optionally accept aTimerController - Refactor test to use controlled time advancement
5. TransportTester - Message Delivery and Reliability Tests
File: libs/network/src/transport/pdk/transport-testing.spec.ts
Issues:
- Tests rely on asynchronous callbacks and real timing
- MockTransport implementation uses real
setTimeout - Race conditions can cause tests to fail when run concurrently
Relevant code:
it('should return true when message is delivered', async () => {
// Setup receiver to simulate receiving the message
const testMessage = { test: true };
// We need to simulate the receipt of the message when the sender sends it
sender.on(TransportEvents.MESSAGE_SENT, (msg) => {
receiver.simulateMessageReceived(msg, sender.id);
});
const result = await TransportTester.testMessageDelivery(sender, receiver, testMessage);
expect(result).toBe(true);
});
Solution:
- Update
MockTransportto useTimerController - Modify
TransportTesterto optionally accept aTimerController - Use controlled time advancement rather than actual timeouts
Common Patterns and Solution Strategy
Core Issues
- Shared Static State: Several classes use static properties or singletons
- Real Timing Dependency: Tests rely on actual setTimeout/setInterval
- Timestamp Variations: Inconsistent approaches to time tracking
Solution Strategy
-
Refactor TimerController:
- Remove the singleton pattern
- Create factory for isolated instances
- Ensure proper cleanup methods
-
Update Key Classes:
interface TimeOptions { timerController?: TimerController; useRealTime?: boolean; // Default to false in tests } -
Consistent Cleanup Pattern:
beforeEach(() => { // Create isolated timer instances for each test const timer = TimerController.createInstance(); // Reset any static state MemoryTransport.resetRegistry(); // Create test instances with isolated timers testInstance = new ClassUnderTest({ timerController: timer }); }); afterEach(() => { // Clean up instances testInstance.dispose(); // Reset any static state again MemoryTransport.resetRegistry(); }); -
Time-based Assertions:
// Before const startTime = Date.now(); await someAsyncOperation(); const elapsed = Date.now() - startTime; expect(elapsed).toBeGreaterThanOrEqual(expectedTime); // After const timer = TimerController.createInstance(); const startTime = timer.getCurrentTime(); // Start the operation without await const operation = someAsyncOperation(); // Advance time timer.advanceTime(expectedTime + 10); // Now wait for the operation to complete await operation; // Check the operation had the expected effect // But don't assert based on actual elapsed time
By implementing these changes systematically across the test suite, we can ensure proper test isolation and eliminate the timing-related test failures.