Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions packages/core-backend/src/BackendWebSocketService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -817,14 +817,17 @@ describe('BackendWebSocketService', () => {
it('should skip connect when reconnect timer is already scheduled', async () => {
await withService(
async ({ service, getMockWebSocket, completeAsyncOperations }) => {
// Mock Math.random to make Cockatiel's jitter deterministic
jest.spyOn(Math, 'random').mockReturnValue(0);

// Connect successfully first
await service.connect();

const mockWs = getMockWebSocket();

// Simulate unexpected close to trigger scheduleReconnect
mockWs.simulateClose(1006, 'Abnormal closure');
await completeAsyncOperations(10);
await completeAsyncOperations(0);

// Verify reconnect timer is scheduled
const attemptsBefore = service.getConnectionInfo().reconnectAttempts;
Expand Down Expand Up @@ -894,14 +897,22 @@ describe('BackendWebSocketService', () => {
await withService(
{ mockWebSocketOptions: { autoConnect: false } },
async ({ service, getMockWebSocket, completeAsyncOperations }) => {
// Mock Math.random to make Cockatiel's jitter deterministic
jest.spyOn(Math, 'random').mockReturnValue(0);

// eslint-disable-next-line @typescript-eslint/no-floating-promises
service.connect();
await completeAsyncOperations(10);

// Close during connection phase
// Verify we're in CONNECTING state
const mockWs = getMockWebSocket();
expect(service.getConnectionInfo().state).toBe(
WebSocketState.CONNECTING,
);

// Close during connection phase
mockWs.simulateClose(1006, 'Connection failed');
await completeAsyncOperations(10);
await completeAsyncOperations(0);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: WebSocket Test Flakiness Due to Timer Advancement

The 'should handle WebSocket onclose during connection phase' test advances timers by 10ms before asserting the CONNECTING state. This introduces a race condition, as the connection could progress beyond CONNECTING during this time, leading to flakiness and undermining the PR's goal of deterministic timing.

Fix in Cursor Fix in Web


// Should schedule reconnect and be in ERROR state
expect(service.getConnectionInfo().state).toBe(WebSocketState.ERROR);
Expand All @@ -916,6 +927,9 @@ describe('BackendWebSocketService', () => {
mockWebSocketOptions: { autoConnect: false },
},
async ({ service, getMockWebSocket, completeAsyncOperations }) => {
// Mock Math.random to make Cockatiel's jitter deterministic
jest.spyOn(Math, 'random').mockReturnValue(0);

// Start connection (this sets connectionTimeout)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
service.connect();
Expand All @@ -939,7 +953,7 @@ describe('BackendWebSocketService', () => {
// Since state is ERROR (not CONNECTING), onclose will call handleClose
// which will clear connectionTimeout
mockWs.simulateClose(1006, 'Close after timeout');
await completeAsyncOperations(10);
await completeAsyncOperations(0);

// State should still be ERROR or DISCONNECTED
expect([WebSocketState.ERROR, WebSocketState.DISCONNECTED]).toContain(
Expand All @@ -952,6 +966,9 @@ describe('BackendWebSocketService', () => {
it('should not schedule multiple reconnects when scheduleReconnect called multiple times', async () => {
await withService(
async ({ service, getMockWebSocket, completeAsyncOperations }) => {
// Mock Math.random to make Cockatiel's jitter deterministic
jest.spyOn(Math, 'random').mockReturnValue(0);

await service.connect();
expect(service.getConnectionInfo().state).toBe(
WebSocketState.CONNECTED,
Expand All @@ -961,15 +978,15 @@ describe('BackendWebSocketService', () => {

// First close to trigger scheduleReconnect
mockWs.simulateClose(1006, 'Connection lost');
await completeAsyncOperations(10);
await completeAsyncOperations(0);

const attemptsBefore = service.getConnectionInfo().reconnectAttempts;
expect(attemptsBefore).toBeGreaterThan(0);

// Second close should trigger scheduleReconnect again,
// but it should return early since timer already exists
mockWs.simulateClose(1006, 'Connection lost again');
await completeAsyncOperations(10);
await completeAsyncOperations(0);

// Attempts should not have increased again due to idempotency
expect(service.getConnectionInfo().reconnectAttempts).toBe(
Expand All @@ -987,6 +1004,9 @@ describe('BackendWebSocketService', () => {
it('should force reconnection and schedule connect', async () => {
await withService(
async ({ service, getMockWebSocket, completeAsyncOperations }) => {
// Mock Math.random to make Cockatiel's jitter deterministic
jest.spyOn(Math, 'random').mockReturnValue(0);

await service.connect();
expect(service.getConnectionInfo().state).toBe(
WebSocketState.CONNECTED,
Expand All @@ -1001,7 +1021,7 @@ describe('BackendWebSocketService', () => {

// Force reconnection
await service.forceReconnection();
await completeAsyncOperations(10);
await completeAsyncOperations(0);

// Should be disconnected after forceReconnection
expect(service.getConnectionInfo().state).toBe(
Expand All @@ -1018,14 +1038,17 @@ describe('BackendWebSocketService', () => {
await withService(
{ mockWebSocketOptions: { autoConnect: false } },
async ({ service, getMockWebSocket, completeAsyncOperations }) => {
// Mock Math.random to make Cockatiel's jitter deterministic
jest.spyOn(Math, 'random').mockReturnValue(0);

// Trigger a connection failure to schedule a reconnect
// eslint-disable-next-line @typescript-eslint/no-floating-promises
service.connect();
await completeAsyncOperations(10);

const mockWs = getMockWebSocket();
mockWs.simulateError();
await completeAsyncOperations(10);
await completeAsyncOperations(0);

const attemptsBefore = service.getConnectionInfo().reconnectAttempts;

Expand Down
Loading