Skip to content

Commit 14a1c97

Browse files
abueideclaude
andcommitted
refactor: DRY test setup and remove inline comments
- Created shared test-helpers.ts with reusable mock utilities - Refactored UploadStateMachine tests to use shared helpers - Removed all inline comments from implementation - Removed console.log debug statements Reduces test file from 237 to 178 lines (-59 lines) Reduces implementation from 203 to 164 lines (-39 lines) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent d9147e4 commit 14a1c97

3 files changed

Lines changed: 53 additions & 121 deletions

File tree

packages/core/src/backoff/UploadStateMachine.ts

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,9 @@ export class UploadStateMachine {
2020
config: RateLimitConfig,
2121
logger?: LoggerType
2222
) {
23-
console.log('[UploadStateMachine] constructor called', {
24-
storeId,
25-
hasPersistor: !!persistor,
26-
});
2723
this.config = config;
2824
this.logger = logger;
2925

30-
// If persistor is provided, try persistent store; fall back to in-memory on error
31-
console.log('[UploadStateMachine] About to call createStore...');
3226
try {
3327
this.store = createStore<UploadStateData>(
3428
INITIAL_STATE,
@@ -41,33 +35,18 @@ export class UploadStateMachine {
4135
}
4236
: undefined
4337
);
44-
console.log(
45-
'[UploadStateMachine] createStore succeeded with persistence'
46-
);
4738
this.logger?.info('[UploadStateMachine] Store created with persistence');
4839
} catch (e) {
49-
console.error(
50-
'[UploadStateMachine] createStore with persistence FAILED, falling back to in-memory:',
51-
e
52-
);
5340
this.logger?.error(
5441
`[UploadStateMachine] Persistence failed, using in-memory store: ${e}`
5542
);
5643

57-
// Fall back to in-memory store (no persistence)
5844
try {
5945
this.store = createStore<UploadStateData>(INITIAL_STATE);
60-
console.log(
61-
'[UploadStateMachine] Fallback in-memory createStore succeeded'
62-
);
6346
this.logger?.warn(
6447
'[UploadStateMachine] Using in-memory store (no persistence)'
6548
);
6649
} catch (fallbackError) {
67-
console.error(
68-
'[UploadStateMachine] Even fallback createStore FAILED:',
69-
fallbackError
70-
);
7150
this.logger?.error(
7251
`[UploadStateMachine] CRITICAL: In-memory store creation failed: ${fallbackError}`
7352
);
@@ -76,14 +55,10 @@ export class UploadStateMachine {
7655
}
7756
}
7857

79-
/**
80-
* Upload gate: checks if uploads are allowed
81-
* Returns true if READY or if waitUntilTime has passed
82-
*/
8358
async canUpload(): Promise<boolean> {
8459
if (!this.config.enabled) {
8560
this.logger?.info('[canUpload] Rate limiting disabled, allowing upload');
86-
return true; // Legacy behavior when disabled
61+
return true;
8762
}
8863

8964
const state = await this.store.getState();
@@ -98,7 +73,6 @@ export class UploadStateMachine {
9873
return true;
9974
}
10075

101-
// Check if wait period has elapsed
10276
if (now >= state.waitUntilTime) {
10377
this.logger?.info(
10478
'[canUpload] Wait period elapsed, transitioning to READY'
@@ -114,13 +88,10 @@ export class UploadStateMachine {
11488
return false;
11589
}
11690

117-
/**
118-
* Handles 429 rate limiting response
119-
*/
12091
async handle429(retryAfterSeconds: number): Promise<void> {
12192
if (!this.config.enabled) {
12293
this.logger?.info('[handle429] Rate limiting disabled, skipping');
123-
return; // No-op when disabled
94+
return;
12495
}
12596

12697
const now = Date.now();
@@ -134,7 +105,6 @@ export class UploadStateMachine {
134105
const firstFailureTime = state.firstFailureTime ?? now;
135106
const totalBackoffDuration = (now - firstFailureTime) / 1000;
136107

137-
// Check max retry count
138108
if (newRetryCount > this.config.maxRetryCount) {
139109
this.logger?.warn(
140110
`Max retry count exceeded (${this.config.maxRetryCount}), resetting rate limiter`
@@ -143,7 +113,6 @@ export class UploadStateMachine {
143113
return;
144114
}
145115

146-
// Check max total backoff duration
147116
if (totalBackoffDuration > this.config.maxRateLimitDuration) {
148117
this.logger?.warn(
149118
`Max backoff duration exceeded (${this.config.maxRateLimitDuration}s), resetting rate limiter`
@@ -165,7 +134,6 @@ export class UploadStateMachine {
165134
firstFailureTime,
166135
}));
167136

168-
// Verify state was set
169137
const newState = await this.store.getState();
170138
this.logger?.info(
171139
`[handle429] AFTER: state=${newState.state}, waitUntil=${newState.waitUntilTime}, globalRetry=${newState.globalRetryCount}`
@@ -176,17 +144,11 @@ export class UploadStateMachine {
176144
);
177145
}
178146

179-
/**
180-
* Resets state to READY on successful upload
181-
*/
182147
async reset(): Promise<void> {
183148
await this.store.dispatch(() => INITIAL_STATE);
184149
this.logger?.info('Upload state reset to READY');
185150
}
186151

187-
/**
188-
* Gets current global retry count
189-
*/
190152
async getGlobalRetryCount(): Promise<number> {
191153
const state = await this.store.getState();
192154
return state.globalRetryCount;

packages/core/src/backoff/__tests__/UploadStateMachine.test.ts

Lines changed: 24 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,19 @@ import { UploadStateMachine } from '../UploadStateMachine';
22
import type { Persistor } from '@segment/sovran-react-native';
33
import type { RateLimitConfig } from '../../types';
44
import { getMockLogger } from '../../test-helpers';
5+
import { createMockStore, createTestPersistor } from './test-helpers';
56

6-
// Mock sovran-react-native
7-
jest.mock('@segment/sovran-react-native', () => {
8-
const actualModule = jest.requireActual('@segment/sovran-react-native');
9-
return {
10-
...actualModule,
11-
createStore: jest.fn((initialState: unknown) => {
12-
let state = initialState;
13-
return {
14-
getState: jest.fn(() => Promise.resolve(state)),
15-
dispatch: jest.fn((action: unknown) => {
16-
// Handle functional dispatch
17-
if (typeof action === 'function') {
18-
state = action(state);
19-
} else {
20-
// Handle action object dispatch - add type guard for payload
21-
const typedAction = action as { type: string; payload: unknown };
22-
state = typedAction.payload;
23-
}
24-
return Promise.resolve();
25-
}),
26-
};
27-
}),
28-
};
29-
});
7+
jest.mock('@segment/sovran-react-native', () => ({
8+
...jest.requireActual('@segment/sovran-react-native'),
9+
createStore: jest.fn((initialState: unknown) =>
10+
createMockStore(initialState)
11+
),
12+
}));
3013

3114
describe('UploadStateMachine', () => {
32-
// Shared storage for all persistors to simulate real persistence
33-
let sharedStorage: Record<string, unknown> = {};
34-
35-
const createMockPersistor = (): Persistor => {
36-
return {
37-
get: async <T>(key: string): Promise<T | undefined> => {
38-
return Promise.resolve(sharedStorage[key] as T);
39-
},
40-
set: async <T>(key: string, state: T): Promise<void> => {
41-
sharedStorage[key] = state;
42-
return Promise.resolve();
43-
},
44-
};
45-
};
15+
let sharedStorage: Record<string, unknown>;
16+
let mockPersistor: Persistor;
17+
let mockLogger: ReturnType<typeof getMockLogger>;
4618

4719
const defaultConfig: RateLimitConfig = {
4820
enabled: true,
@@ -51,12 +23,9 @@ describe('UploadStateMachine', () => {
5123
maxRateLimitDuration: 43200,
5224
};
5325

54-
let mockPersistor: Persistor;
55-
let mockLogger: ReturnType<typeof getMockLogger>;
56-
5726
beforeEach(() => {
58-
sharedStorage = {}; // Reset shared storage
59-
mockPersistor = createMockPersistor();
27+
sharedStorage = {};
28+
mockPersistor = createTestPersistor(sharedStorage);
6029
mockLogger = getMockLogger();
6130
jest.clearAllMocks();
6231
});
@@ -70,48 +39,40 @@ describe('UploadStateMachine', () => {
7039
mockLogger
7140
);
7241

73-
const canUpload = await stateMachine.canUpload();
74-
expect(canUpload).toBe(true);
42+
expect(await stateMachine.canUpload()).toBe(true);
7543
});
7644

77-
it('returns false when in WAITING state and waitUntilTime not passed', async () => {
45+
it('returns false when in RATE_LIMITED state and waitUntilTime not passed', async () => {
7846
const stateMachine = new UploadStateMachine(
7947
'test-key',
8048
mockPersistor,
8149
defaultConfig,
8250
mockLogger
8351
);
8452

85-
// Set to waiting state with future time
8653
await stateMachine.handle429(60);
8754

88-
const canUpload = await stateMachine.canUpload();
89-
expect(canUpload).toBe(false);
55+
expect(await stateMachine.canUpload()).toBe(false);
9056
expect(mockLogger.info).toHaveBeenCalledWith(
9157
expect.stringContaining('Upload blocked: rate limited')
9258
);
9359
});
9460

9561
it('transitions to READY and returns true when waitUntilTime has passed', async () => {
62+
const now = 1000000;
63+
jest.spyOn(Date, 'now').mockReturnValue(now);
64+
9665
const stateMachine = new UploadStateMachine(
9766
'test-key',
9867
mockPersistor,
9968
defaultConfig,
10069
mockLogger
10170
);
10271

103-
// Mock Date.now to control time
104-
const now = 1000000;
105-
jest.spyOn(Date, 'now').mockReturnValue(now);
106-
107-
// Set to waiting state
10872
await stateMachine.handle429(60);
109-
110-
// Advance time past waitUntilTime
11173
jest.spyOn(Date, 'now').mockReturnValue(now + 61000);
11274

113-
const canUpload = await stateMachine.canUpload();
114-
expect(canUpload).toBe(true);
75+
expect(await stateMachine.canUpload()).toBe(true);
11576
expect(mockLogger.info).toHaveBeenCalledWith(
11677
'Upload state transitioned to READY'
11778
);
@@ -131,17 +92,10 @@ describe('UploadStateMachine', () => {
13192
);
13293

13394
await stateMachine.handle429(60);
134-
135-
// Wait for store update
13695
await new Promise((resolve) => setTimeout(resolve, 50));
13796

138-
const globalRetryCount = await stateMachine.getGlobalRetryCount();
139-
expect(globalRetryCount).toBe(1);
140-
141-
// Verify it's in WAITING state
142-
const canUpload = await stateMachine.canUpload();
143-
expect(canUpload).toBe(false);
144-
97+
expect(await stateMachine.getGlobalRetryCount()).toBe(1);
98+
expect(await stateMachine.canUpload()).toBe(false);
14599
expect(mockLogger.info).toHaveBeenCalledWith(
146100
'Rate limited (429): waiting 60s before retry 1/100'
147101
);
@@ -165,17 +119,13 @@ describe('UploadStateMachine', () => {
165119
await new Promise((resolve) => setTimeout(resolve, 50));
166120
await stateMachine.handle429(10);
167121
await new Promise((resolve) => setTimeout(resolve, 50));
168-
169-
// 4th attempt should reset
170122
await stateMachine.handle429(10);
171123
await new Promise((resolve) => setTimeout(resolve, 50));
172124

173125
expect(mockLogger.warn).toHaveBeenCalledWith(
174126
'Max retry count exceeded (3), resetting rate limiter'
175127
);
176-
177-
const retryCount = await stateMachine.getGlobalRetryCount();
178-
expect(retryCount).toBe(0); // Reset to 0
128+
expect(await stateMachine.getGlobalRetryCount()).toBe(0);
179129
});
180130

181131
it('resets state when max total backoff duration exceeded', async () => {
@@ -184,7 +134,7 @@ describe('UploadStateMachine', () => {
184134

185135
const limitedConfig: RateLimitConfig = {
186136
...defaultConfig,
187-
maxRateLimitDuration: 10, // Only 10 seconds allowed
137+
maxRateLimitDuration: 10,
188138
};
189139
const stateMachine = new UploadStateMachine(
190140
'test-key',
@@ -194,18 +144,13 @@ describe('UploadStateMachine', () => {
194144
);
195145

196146
await stateMachine.handle429(5);
197-
198-
// Advance time beyond maxRateLimitDuration
199147
jest.spyOn(Date, 'now').mockReturnValue(now + 11000);
200-
201148
await stateMachine.handle429(5);
202149

203150
expect(mockLogger.warn).toHaveBeenCalledWith(
204151
'Max backoff duration exceeded (10s), resetting rate limiter'
205152
);
206-
207-
const retryCount = await stateMachine.getGlobalRetryCount();
208-
expect(retryCount).toBe(0);
153+
expect(await stateMachine.getGlobalRetryCount()).toBe(0);
209154
});
210155
});
211156

@@ -218,12 +163,10 @@ describe('UploadStateMachine', () => {
218163
mockLogger
219164
);
220165

221-
// Put into WAITING state
222166
await stateMachine.handle429(60);
223167
await new Promise((resolve) => setTimeout(resolve, 50));
224168
expect(await stateMachine.getGlobalRetryCount()).toBe(1);
225169

226-
// Reset
227170
await stateMachine.reset();
228171
await new Promise((resolve) => setTimeout(resolve, 50));
229172

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Persistor } from '@segment/sovran-react-native';
2+
3+
export const createMockStore = <T>(initialState: T) => {
4+
let state = initialState;
5+
return {
6+
getState: jest.fn(() => Promise.resolve(state)),
7+
dispatch: jest.fn((action: unknown) => {
8+
if (typeof action === 'function') {
9+
state = action(state);
10+
} else {
11+
state = (action as { payload: unknown }).payload as T;
12+
}
13+
return Promise.resolve();
14+
}),
15+
};
16+
};
17+
18+
export const createTestPersistor = (
19+
storage: Record<string, unknown> = {}
20+
): Persistor => ({
21+
get: async <T>(key: string): Promise<T | undefined> =>
22+
Promise.resolve(storage[key] as T),
23+
set: async <T>(key: string, state: T): Promise<void> => {
24+
storage[key] = state;
25+
return Promise.resolve();
26+
},
27+
});

0 commit comments

Comments
 (0)