WellNuo/services/ble/__tests__/BLEManager.concurrent.test.ts
Sergei 2c1b37877d Add concurrent connection protection to BLE managers
Prevents race conditions when multiple connection attempts are made
to the same device simultaneously by:
- Adding connectingDevices Set to track in-progress connections
- Checking for concurrent connections before starting new attempt
- Returning early if device is already connected
- Using finally block to ensure cleanup of connecting state
- Clearing connectingDevices set on cleanup

Includes comprehensive test suite for concurrent connection scenarios
including edge cases like rapid connect/disconnect cycles and cleanup
during connection attempts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-31 15:39:36 -08:00

257 lines
9.1 KiB
TypeScript

/**
* Tests for BLE Concurrent Connection Protection
*/
import { MockBLEManager } from '../MockBLEManager';
import { BLEConnectionState } from '../types';
describe('BLE Concurrent Connection Protection', () => {
let manager: MockBLEManager;
beforeEach(() => {
manager = new MockBLEManager();
});
afterEach(async () => {
await manager.cleanup();
});
describe('Concurrent Connection Prevention', () => {
it('should prevent concurrent connection attempts to the same device', async () => {
const deviceId = 'test-device';
// Start two connection attempts simultaneously
const connection1Promise = manager.connectDevice(deviceId);
const connection2Promise = manager.connectDevice(deviceId);
// Wait for both to complete
const [result1, result2] = await Promise.all([connection1Promise, connection2Promise]);
// One should succeed, one should fail
expect(result1 !== result2).toBe(true);
// The device should end up connected (from the successful attempt)
const finalState = manager.getConnectionState(deviceId);
expect([BLEConnectionState.READY, BLEConnectionState.ERROR]).toContain(finalState);
});
it('should allow reconnection after first connection completes', async () => {
const deviceId = 'test-device';
// First connection
const result1 = await manager.connectDevice(deviceId);
expect(result1).toBe(true);
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
// Disconnect
await manager.disconnectDevice(deviceId);
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.DISCONNECTED);
// Second connection should succeed
const result2 = await manager.connectDevice(deviceId);
expect(result2).toBe(true);
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
});
it('should handle multiple concurrent connections to different devices', async () => {
const deviceIds = ['device-1', 'device-2', 'device-3'];
// Connect to all devices simultaneously
const connectionPromises = deviceIds.map(id => manager.connectDevice(id));
const results = await Promise.all(connectionPromises);
// All connections should succeed
expect(results).toEqual([true, true, true]);
// All devices should be in READY state
deviceIds.forEach(id => {
expect(manager.getConnectionState(id)).toBe(BLEConnectionState.READY);
});
});
it('should allow connection if device is already connected', async () => {
const deviceId = 'test-device';
// First connection
const result1 = await manager.connectDevice(deviceId);
expect(result1).toBe(true);
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
// Second connection attempt while already connected should return true
const result2 = await manager.connectDevice(deviceId);
expect(result2).toBe(true);
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
});
it('should emit connection_failed event for concurrent connection attempt', async () => {
const deviceId = 'test-device';
const events: Array<{ event: string; error?: string }> = [];
const listener = (id: string, event: string, data?: any) => {
if (id === deviceId) {
events.push({ event, error: data?.error });
}
};
manager.addEventListener(listener);
// Start two connection attempts simultaneously
await Promise.all([
manager.connectDevice(deviceId),
manager.connectDevice(deviceId),
]);
// Should have at least one connection_failed event
const failedEvents = events.filter(e => e.event === 'connection_failed');
expect(failedEvents.length).toBeGreaterThanOrEqual(1);
// The error should mention concurrent connection
const concurrentError = failedEvents.find(e =>
e.error?.includes('Connection already in progress')
);
expect(concurrentError).toBeDefined();
});
});
describe('Connection State During Concurrent Attempts', () => {
it('should maintain consistent state during concurrent attempts', async () => {
const deviceId = 'test-device';
const states: BLEConnectionState[] = [];
const listener = (id: string, event: string, data?: any) => {
if (id === deviceId && event === 'state_changed') {
states.push(data.state);
}
};
manager.addEventListener(listener);
// Start concurrent connections
await Promise.all([
manager.connectDevice(deviceId),
manager.connectDevice(deviceId),
]);
// States should follow valid transitions
// Should not have duplicate CONNECTING states (only one should enter)
const connectingCount = states.filter(s => s === BLEConnectionState.CONNECTING).length;
expect(connectingCount).toBeLessThanOrEqual(2); // Max 2: one success, one immediate error
// Final state should be either READY or ERROR
const finalState = manager.getConnectionState(deviceId);
expect([BLEConnectionState.READY, BLEConnectionState.ERROR]).toContain(finalState);
});
it('should track connection attempts correctly with getAllConnections', async () => {
const deviceId = 'test-device';
// Start concurrent connections
await Promise.all([
manager.connectDevice(deviceId),
manager.connectDevice(deviceId),
]);
const connections = manager.getAllConnections();
const deviceConnection = connections.get(deviceId);
expect(deviceConnection).toBeDefined();
expect(deviceConnection?.deviceId).toBe(deviceId);
expect([BLEConnectionState.READY, BLEConnectionState.ERROR]).toContain(deviceConnection?.state);
});
});
describe('Cleanup During Connection', () => {
it('should properly cleanup concurrent connection attempts', async () => {
const deviceIds = ['device-1', 'device-2', 'device-3'];
// Start connections
const connectionPromises = deviceIds.map(id => manager.connectDevice(id));
// Don't wait for connections, cleanup immediately
await manager.cleanup();
// All connections should be cleaned up
const connections = manager.getAllConnections();
expect(connections.size).toBe(0);
// All devices should be disconnected
deviceIds.forEach(id => {
expect(manager.getConnectionState(id)).toBe(BLEConnectionState.DISCONNECTED);
});
});
it('should clear connecting devices set on cleanup', async () => {
const deviceId = 'test-device';
// Start connection (but don't await)
const connectionPromise = manager.connectDevice(deviceId);
// Cleanup before connection completes
await manager.cleanup();
// Try to connect again after cleanup - should work
const result = await manager.connectDevice(deviceId);
expect(result).toBe(true);
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
});
});
describe('Edge Cases', () => {
it('should handle rapid connect/disconnect/connect cycles', async () => {
const deviceId = 'test-device';
// Connect
await manager.connectDevice(deviceId);
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
// Disconnect
await manager.disconnectDevice(deviceId);
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.DISCONNECTED);
// Reconnect immediately
const result = await manager.connectDevice(deviceId);
expect(result).toBe(true);
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
});
it('should handle connection attempt during disconnection', async () => {
const deviceId = 'test-device';
// Connect first
await manager.connectDevice(deviceId);
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
// Start disconnection (don't await)
const disconnectPromise = manager.disconnectDevice(deviceId);
// Try to connect while disconnecting
const connectResult = await manager.connectDevice(deviceId);
// Wait for disconnect to complete
await disconnectPromise;
// Connection attempt should have a defined result
expect(typeof connectResult).toBe('boolean');
});
it('should handle multiple rapid concurrent attempts', async () => {
const deviceId = 'test-device';
// Start 5 concurrent connection attempts
const attempts = Array(5).fill(null).map(() => manager.connectDevice(deviceId));
const results = await Promise.all(attempts);
// At least one should succeed or all should fail consistently
const successCount = results.filter(r => r === true).length;
const failureCount = results.filter(r => r === false).length;
expect(successCount + failureCount).toBe(5);
// If any succeeded, device should be connected
if (successCount > 0) {
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
}
});
});
});