vibecodingmachine-core 2026.1.3-2209 → 2026.1.22-1441

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/__tests__/provider-manager-fallback.test.js +43 -0
  2. package/__tests__/provider-manager-rate-limit.test.js +61 -0
  3. package/package.json +1 -1
  4. package/src/compliance/compliance-manager.js +5 -2
  5. package/src/database/migrations.js +135 -12
  6. package/src/database/user-database-client.js +63 -8
  7. package/src/database/user-schema.js +7 -0
  8. package/src/health-tracking/__tests__/ide-health-tracker.test.js +420 -0
  9. package/src/health-tracking/__tests__/interaction-recorder.test.js +392 -0
  10. package/src/health-tracking/errors.js +50 -0
  11. package/src/health-tracking/health-reporter.js +331 -0
  12. package/src/health-tracking/ide-health-tracker.js +446 -0
  13. package/src/health-tracking/interaction-recorder.js +161 -0
  14. package/src/health-tracking/json-storage.js +276 -0
  15. package/src/health-tracking/storage-interface.js +63 -0
  16. package/src/health-tracking/validators.js +277 -0
  17. package/src/ide-integration/applescript-manager.cjs +1062 -4
  18. package/src/ide-integration/applescript-manager.js +560 -11
  19. package/src/ide-integration/provider-manager.cjs +158 -28
  20. package/src/ide-integration/quota-detector.cjs +339 -16
  21. package/src/ide-integration/quota-detector.js +6 -1
  22. package/src/index.cjs +32 -1
  23. package/src/index.js +16 -0
  24. package/src/localization/translations/en.js +13 -1
  25. package/src/localization/translations/es.js +12 -0
  26. package/src/utils/admin-utils.js +33 -0
  27. package/src/utils/error-reporter.js +12 -4
  28. package/src/utils/requirement-helpers.js +34 -4
  29. package/src/utils/requirements-parser.js +3 -3
  30. package/tests/health-tracking/health-reporter.test.js +329 -0
  31. package/tests/health-tracking/ide-health-tracker.test.js +368 -0
  32. package/tests/health-tracking/interaction-recorder.test.js +309 -0
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Unit Tests for IDEHealthTracker
3
+ *
4
+ * Tests for health tracking functionality including success/failure recording,
5
+ * quota detection, and consecutive failure handling.
6
+ */
7
+
8
+ const { IDEHealthTracker, MAX_RESPONSE_TIMES, CONSECUTIVE_FAILURE_THRESHOLD } = require('../../../src/health-tracking/ide-health-tracker');
9
+ const { JSONStorage } = require('../../../src/health-tracking/json-storage');
10
+ const { ValidationError } = require('../../../src/health-tracking/errors');
11
+
12
+ describe('IDEHealthTracker', () => {
13
+ let tracker;
14
+ let mockStorage;
15
+
16
+ beforeEach(() => {
17
+ mockStorage = {
18
+ read: jest.fn().mockResolvedValue({
19
+ version: '1.0.0',
20
+ lastUpdated: new Date().toISOString(),
21
+ ides: {},
22
+ timeoutConfig: {
23
+ mode: 'fixed',
24
+ defaultTimeout: 1800000,
25
+ bufferPercentage: 0.4,
26
+ minSamplesForAdaptive: 10,
27
+ ewmaAlpha: 0.3
28
+ },
29
+ defaultRequirement: null
30
+ }),
31
+ write: jest.fn().mockResolvedValue(),
32
+ exists: jest.fn().mockResolvedValue(true)
33
+ };
34
+
35
+ tracker = new IDEHealthTracker({ storage: mockStorage });
36
+ });
37
+
38
+ describe('recordSuccess', () => {
39
+ it('should increment success count and reset consecutive failures', async () => {
40
+ // First record a failure to test reset
41
+ await tracker.recordFailure('cursor', 'Test error');
42
+ let metrics = await tracker.getHealthMetrics('cursor');
43
+ expect(metrics.consecutiveFailures).toBe(1);
44
+
45
+ // Now record success
46
+ await tracker.recordSuccess('cursor', 120000);
47
+
48
+ metrics = await tracker.getHealthMetrics('cursor');
49
+ expect(metrics.successCount).toBe(1);
50
+ expect(metrics.failureCount).toBe(1); // Previous failure remains
51
+ expect(metrics.consecutiveFailures).toBe(0); // Reset on success
52
+ expect(metrics.responseTimes).toContain(120000);
53
+ expect(metrics.lastSuccess).toBeTruthy();
54
+ });
55
+
56
+ it('should add response time to array and maintain max size', async () => {
57
+ // Add max number of response times
58
+ for (let i = 0; i < MAX_RESPONSE_TIMES + 5; i++) {
59
+ await tracker.recordSuccess('cursor', 100000 + i);
60
+ }
61
+
62
+ const metrics = await tracker.getHealthMetrics('cursor');
63
+ expect(metrics.responseTimes.length).toBe(MAX_RESPONSE_TIMES);
64
+ expect(metrics.responseTimes).not.toContain(100000); // Oldest removed
65
+ expect(metrics.responseTimes).toContain(100000 + MAX_RESPONSE_TIMES + 4); // Newest included
66
+ });
67
+
68
+ it('should recalculate EWMA after each success', async () => {
69
+ await tracker.recordSuccess('cursor', 100000);
70
+ let metrics = await tracker.getHealthMetrics('cursor');
71
+ const firstEWMA = metrics.ewma;
72
+ expect(firstEWMA).toBe(100000); // First value becomes initial EWMA
73
+
74
+ await tracker.recordSuccess('cursor', 120000);
75
+ metrics = await tracker.getHealthMetrics('cursor');
76
+ const secondEWMA = metrics.ewma;
77
+ expect(secondEWMA).toBeGreaterThan(firstEWMA);
78
+ expect(secondEWMA).toBeLessThan(120000); // Weighted average
79
+ });
80
+
81
+ it('should record interaction details', async () => {
82
+ const options = {
83
+ continuationPromptsDetected: 2,
84
+ requirementId: 'req-042',
85
+ timeoutUsed: 180000
86
+ };
87
+
88
+ await tracker.recordSuccess('cursor', 120000, options);
89
+
90
+ const metrics = await tracker.getHealthMetrics('cursor');
91
+ expect(metrics.interactions).toHaveLength(1);
92
+
93
+ const interaction = metrics.interactions[0];
94
+ expect(interaction.outcome).toBe('success');
95
+ expect(interaction.responseTime).toBe(120000);
96
+ expect(interaction.continuationPromptsDetected).toBe(2);
97
+ expect(interaction.requirementId).toBe('req-042');
98
+ expect(interaction.timeoutUsed).toBe(180000);
99
+ expect(interaction.timestamp).toBeTruthy();
100
+ });
101
+
102
+ it('should validate IDE identifier', async () => {
103
+ await expect(tracker.recordSuccess('', 120000))
104
+ .rejects.toThrow(ValidationError);
105
+
106
+ await expect(tracker.recordSuccess(null, 120000))
107
+ .rejects.toThrow(ValidationError);
108
+ });
109
+
110
+ it('should validate response time', async () => {
111
+ await expect(tracker.recordSuccess('cursor', -1000))
112
+ .rejects.toThrow(ValidationError);
113
+
114
+ await expect(tracker.recordSuccess('cursor', 'invalid'))
115
+ .rejects.toThrow(ValidationError);
116
+ });
117
+ });
118
+
119
+ describe('recordFailure', () => {
120
+ it('should increment failure count and consecutive failures', async () => {
121
+ await tracker.recordFailure('cursor', 'Timeout exceeded');
122
+
123
+ const metrics = await tracker.getHealthMetrics('cursor');
124
+ expect(metrics.successCount).toBe(0);
125
+ expect(metrics.failureCount).toBe(1);
126
+ expect(metrics.consecutiveFailures).toBe(1);
127
+ expect(metrics.lastFailure).toBeTruthy();
128
+ });
129
+
130
+ it('should accumulate consecutive failures', async () => {
131
+ for (let i = 0; i < 3; i++) {
132
+ await tracker.recordFailure('cursor', `Error ${i}`);
133
+ }
134
+
135
+ const metrics = await tracker.getHealthMetrics('cursor');
136
+ expect(metrics.failureCount).toBe(3);
137
+ expect(metrics.consecutiveFailures).toBe(3);
138
+ });
139
+
140
+ it('should emit consecutive-failures event at threshold', async () => {
141
+ const eventSpy = jest.fn();
142
+ tracker.on('consecutive-failures', eventSpy);
143
+
144
+ // Add failures up to threshold
145
+ for (let i = 0; i < CONSECUTIVE_FAILURE_THRESHOLD; i++) {
146
+ await tracker.recordFailure('cursor', `Error ${i}`);
147
+ }
148
+
149
+ expect(eventSpy).toHaveBeenCalledTimes(1);
150
+ expect(eventSpy).toHaveBeenCalledWith({
151
+ ideId: 'cursor',
152
+ consecutiveFailures: CONSECUTIVE_FAILURE_THRESHOLD,
153
+ threshold: CONSECUTIVE_FAILURE_THRESHOLD
154
+ });
155
+ });
156
+
157
+ it('should record failure interaction details', async () => {
158
+ const options = {
159
+ requirementId: 'req-043',
160
+ timeoutUsed: 180000
161
+ };
162
+
163
+ await tracker.recordFailure('cursor', 'Network error', options);
164
+
165
+ const metrics = await tracker.getHealthMetrics('cursor');
166
+ expect(metrics.interactions).toHaveLength(1);
167
+
168
+ const interaction = metrics.interactions[0];
169
+ expect(interaction.outcome).toBe('failure');
170
+ expect(interaction.responseTime).toBeNull();
171
+ expect(interaction.errorMessage).toBe('Network error');
172
+ expect(interaction.requirementId).toBe('req-043');
173
+ expect(interaction.timeoutUsed).toBe(180000);
174
+ });
175
+
176
+ it('should validate error message', async () => {
177
+ await expect(tracker.recordFailure('cursor', ''))
178
+ .rejects.toThrow(ValidationError);
179
+
180
+ await expect(tracker.recordFailure('cursor', null))
181
+ .rejects.toThrow(ValidationError);
182
+ });
183
+ });
184
+
185
+ describe('recordQuota', () => {
186
+ it('should record quota without changing counters', async () => {
187
+ await tracker.recordSuccess('cursor', 120000);
188
+ await tracker.recordFailure('cursor', 'Test error');
189
+
190
+ let metrics = await tracker.getHealthMetrics('cursor');
191
+ const successCount = metrics.successCount;
192
+ const failureCount = metrics.failureCount;
193
+
194
+ await tracker.recordQuota('cursor', 'Rate limit exceeded');
195
+
196
+ metrics = await tracker.getHealthMetrics('cursor');
197
+ expect(metrics.successCount).toBe(successCount); // Unchanged
198
+ expect(metrics.failureCount).toBe(failureCount); // Unchanged
199
+ expect(metrics.consecutiveFailures).toBe(1); // Unchanged
200
+ });
201
+
202
+ it('should record quota interaction details', async () => {
203
+ const options = {
204
+ requirementId: 'req-044',
205
+ timeoutUsed: 180000
206
+ };
207
+
208
+ await tracker.recordQuota('cursor', 'Rate limit exceeded', options);
209
+
210
+ const metrics = await tracker.getHealthMetrics('cursor');
211
+ expect(metrics.interactions).toHaveLength(1);
212
+
213
+ const interaction = metrics.interactions[0];
214
+ expect(interaction.outcome).toBe('quota');
215
+ expect(interaction.responseTime).toBeNull();
216
+ expect(interaction.errorMessage).toBe('Rate limit exceeded');
217
+ expect(interaction.requirementId).toBe('req-044');
218
+ expect(interaction.timeoutUsed).toBe(180000);
219
+ });
220
+
221
+ it('should validate quota message', async () => {
222
+ await expect(tracker.recordQuota('cursor', ''))
223
+ .rejects.toThrow(ValidationError);
224
+
225
+ await expect(tracker.recordQuota('cursor', null))
226
+ .rejects.toThrow(ValidationError);
227
+ });
228
+ });
229
+
230
+ describe('getHealthMetrics', () => {
231
+ it('should return default record for unknown IDE', async () => {
232
+ const metrics = await tracker.getHealthMetrics('unknown-ide');
233
+
234
+ expect(metrics.successCount).toBe(0);
235
+ expect(metrics.failureCount).toBe(0);
236
+ expect(metrics.consecutiveFailures).toBe(0);
237
+ expect(metrics.responseTimes).toEqual([]);
238
+ expect(metrics.interactions).toEqual([]);
239
+ expect(metrics.ewma).toBe(0);
240
+ expect(metrics.currentTimeout).toBe(1800000); // Default timeout
241
+ });
242
+
243
+ it('should return recorded metrics for known IDE', async () => {
244
+ await tracker.recordSuccess('cursor', 120000);
245
+ await tracker.recordFailure('cursor', 'Test error');
246
+
247
+ const metrics = await tracker.getHealthMetrics('cursor');
248
+ expect(metrics.successCount).toBe(1);
249
+ expect(metrics.failureCount).toBe(1);
250
+ expect(metrics.consecutiveFailures).toBe(1);
251
+ expect(metrics.responseTimes).toContain(120000);
252
+ expect(metrics.interactions).toHaveLength(2);
253
+ });
254
+ });
255
+
256
+ describe('getAllHealthMetrics', () => {
257
+ it('should return metrics for all IDEs', async () => {
258
+ await tracker.recordSuccess('cursor', 120000);
259
+ await tracker.recordFailure('windsurf', 'Test error');
260
+
261
+ const allMetrics = await tracker.getAllHealthMetrics();
262
+
263
+ expect(allMetrics).toHaveProperty('cursor');
264
+ expect(allMetrics).toHaveProperty('windsurf');
265
+ expect(allMetrics.cursor.successCount).toBe(1);
266
+ expect(allMetrics.windsurf.failureCount).toBe(1);
267
+ });
268
+ });
269
+
270
+ describe('getRecommendedIDE', () => {
271
+ it('should return IDE with highest success rate', async () => {
272
+ // Setup IDEs with different success rates
273
+ await tracker.recordSuccess('cursor', 120000);
274
+ await tracker.recordSuccess('cursor', 130000);
275
+ await tracker.recordFailure('cursor', 'Error'); // 2/3 = 66.7%
276
+
277
+ await tracker.recordSuccess('windsurf', 150000);
278
+ await tracker.recordFailure('windsurf', 'Error'); // 1/2 = 50%
279
+
280
+ const recommended = await tracker.getRecommendedIDE();
281
+ expect(recommended).toBe('cursor');
282
+ });
283
+
284
+ it('should return null for no IDEs with sufficient data', async () => {
285
+ // Only failures, no successes
286
+ await tracker.recordFailure('cursor', 'Error');
287
+
288
+ const recommended = await tracker.getRecommendedIDE();
289
+ expect(recommended).toBeNull();
290
+ });
291
+
292
+ it('should require minimum interactions for recommendation', async () => {
293
+ // Only one interaction - not enough data
294
+ await tracker.recordSuccess('cursor', 120000);
295
+
296
+ const recommended = await tracker.getRecommendedIDE();
297
+ expect(recommended).toBeNull();
298
+ });
299
+ });
300
+
301
+ describe('save and load', () => {
302
+ it('should save data to storage', async () => {
303
+ await tracker.recordSuccess('cursor', 120000);
304
+ await tracker.save();
305
+
306
+ expect(mockStorage.write).toHaveBeenCalledWith(
307
+ expect.objectContaining({
308
+ version: '1.0.0',
309
+ ides: expect.objectContaining({
310
+ cursor: expect.objectContaining({
311
+ successCount: 1,
312
+ failureCount: 0
313
+ })
314
+ })
315
+ })
316
+ );
317
+ });
318
+
319
+ it('should load data from storage', async () => {
320
+ const storedData = {
321
+ version: '1.0.0',
322
+ lastUpdated: new Date().toISOString(),
323
+ ides: {
324
+ cursor: {
325
+ successCount: 5,
326
+ failureCount: 2,
327
+ responseTimes: [120000, 130000],
328
+ ewma: 125000,
329
+ lastSuccess: new Date().toISOString(),
330
+ lastFailure: new Date().toISOString(),
331
+ consecutiveFailures: 0,
332
+ currentTimeout: 175000,
333
+ interactions: []
334
+ }
335
+ },
336
+ timeoutConfig: {
337
+ mode: 'fixed',
338
+ defaultTimeout: 1800000,
339
+ bufferPercentage: 0.4,
340
+ minSamplesForAdaptive: 10,
341
+ ewmaAlpha: 0.3
342
+ },
343
+ defaultRequirement: null
344
+ };
345
+
346
+ mockStorage.read.mockResolvedValue(storedData);
347
+
348
+ await tracker.load();
349
+
350
+ const metrics = await tracker.getHealthMetrics('cursor');
351
+ expect(metrics.successCount).toBe(5);
352
+ expect(metrics.failureCount).toBe(2);
353
+ expect(metrics.responseTimes).toEqual([120000, 130000]);
354
+ });
355
+ });
356
+
357
+ describe('interaction array management', () => {
358
+ it('should maintain maximum interaction array size', async () => {
359
+ // Add more interactions than max
360
+ for (let i = 0; i < 105; i++) {
361
+ await tracker.recordSuccess('cursor', 120000);
362
+ }
363
+
364
+ const metrics = await tracker.getHealthMetrics('cursor');
365
+ expect(metrics.interactions.length).toBeLessThanOrEqual(100);
366
+ });
367
+ });
368
+ });
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Unit Tests for InteractionRecorder
3
+ *
4
+ * Tests for recording individual IDE interactions with timestamps,
5
+ * outcomes, and metadata.
6
+ */
7
+
8
+ const { InteractionRecorder, MAX_INTERACTIONS, DEFAULT_IDE_RECORD } = require('../../../src/health-tracking/interaction-recorder');
9
+ const { ValidationError } = require('../../../src/health-tracking/errors');
10
+
11
+ describe('InteractionRecorder', () => {
12
+ let recorder;
13
+
14
+ beforeEach(() => {
15
+ recorder = new InteractionRecorder();
16
+ });
17
+
18
+ describe('recordInteraction', () => {
19
+ it('should record a successful interaction', () => {
20
+ const interaction = recorder.recordInteraction('cursor', 'success', {
21
+ responseTime: 120000,
22
+ timeoutUsed: 180000,
23
+ continuationPromptsDetected: 1,
24
+ requirementId: 'req-042'
25
+ });
26
+
27
+ expect(interaction.outcome).toBe('success');
28
+ expect(interaction.responseTime).toBe(120000);
29
+ expect(interaction.timeoutUsed).toBe(180000);
30
+ expect(interaction.continuationPromptsDetected).toBe(1);
31
+ expect(interaction.requirementId).toBe('req-042');
32
+ expect(interaction.errorMessage).toBeNull();
33
+ expect(interaction.timestamp).toBeTruthy();
34
+ });
35
+
36
+ it('should record a failed interaction', () => {
37
+ const interaction = recorder.recordInteraction('windsurf', 'failure', {
38
+ errorMessage: 'Timeout exceeded',
39
+ timeoutUsed: 1800000,
40
+ requirementId: 'req-043'
41
+ });
42
+
43
+ expect(interaction.outcome).toBe('failure');
44
+ expect(interaction.responseTime).toBeNull();
45
+ expect(interaction.errorMessage).toBe('Timeout exceeded');
46
+ expect(interaction.timeoutUsed).toBe(1800000);
47
+ expect(interaction.requirementId).toBe('req-043');
48
+ });
49
+
50
+ it('should record a quota detection', () => {
51
+ const interaction = recorder.recordInteraction('cursor', 'quota', {
52
+ errorMessage: 'Rate limit exceeded',
53
+ timeoutUsed: 180000,
54
+ requirementId: 'req-044'
55
+ });
56
+
57
+ expect(interaction.outcome).toBe('quota');
58
+ expect(interaction.responseTime).toBeNull();
59
+ expect(interaction.errorMessage).toBe('Rate limit exceeded');
60
+ });
61
+
62
+ it('should validate outcome', () => {
63
+ expect(() => {
64
+ recorder.recordInteraction('cursor', 'invalid');
65
+ }).toThrow(ValidationError);
66
+ });
67
+
68
+ it('should validate IDE identifier', () => {
69
+ expect(() => {
70
+ recorder.recordInteraction('', 'success');
71
+ }).toThrow(ValidationError);
72
+
73
+ expect(() => {
74
+ recorder.recordInteraction(null, 'success');
75
+ }).toThrow(ValidationError);
76
+ });
77
+
78
+ it('should validate response time for success', () => {
79
+ expect(() => {
80
+ recorder.recordInteraction('cursor', 'success', { responseTime: -1000 });
81
+ }).toThrow(ValidationError);
82
+
83
+ expect(() => {
84
+ recorder.recordInteraction('cursor', 'success', { responseTime: 'invalid' });
85
+ }).toThrow(ValidationError);
86
+ });
87
+
88
+ it('should set responseTime to null for non-success outcomes', () => {
89
+ const interaction = recorder.recordInteraction('cursor', 'failure', {
90
+ responseTime: 120000 // Should be ignored
91
+ });
92
+
93
+ expect(interaction.responseTime).toBeNull();
94
+ });
95
+
96
+ it('should validate timeoutUsed', () => {
97
+ expect(() => {
98
+ recorder.recordInteraction('cursor', 'success', { timeoutUsed: -1000 });
99
+ }).toThrow(ValidationError);
100
+
101
+ expect(() => {
102
+ recorder.recordInteraction('cursor', 'success', { timeoutUsed: 'invalid' });
103
+ }).toThrow(ValidationError);
104
+ });
105
+
106
+ it('should validate continuationPromptsDetected', () => {
107
+ expect(() => {
108
+ recorder.recordInteraction('cursor', 'success', { continuationPromptsDetected: -1 });
109
+ }).toThrow(ValidationError);
110
+
111
+ expect(() => {
112
+ recorder.recordInteraction('cursor', 'success', { continuationPromptsDetected: 'invalid' });
113
+ }).toThrow(ValidationError);
114
+ });
115
+
116
+ it('should validate errorMessage for failure/quota', () => {
117
+ expect(() => {
118
+ recorder.recordInteraction('cursor', 'failure', { errorMessage: '' });
119
+ }).toThrow(ValidationError);
120
+
121
+ expect(() => {
122
+ recorder.recordInteraction('cursor', 'quota', { errorMessage: null });
123
+ }).toThrow(ValidationError);
124
+ });
125
+
126
+ it('should not require errorMessage for success', () => {
127
+ expect(() => {
128
+ recorder.recordInteraction('cursor', 'success');
129
+ }).not.toThrow();
130
+ });
131
+
132
+ it('should set default values', () => {
133
+ const interaction = recorder.recordInteraction('cursor', 'success');
134
+
135
+ expect(interaction.responseTime).toBe(0); // Default
136
+ expect(interaction.timeoutUsed).toBe(0); // Default
137
+ expect(interaction.continuationPromptsDetected).toBe(0); // Default
138
+ expect(interaction.requirementId).toBeNull(); // Default
139
+ expect(interaction.errorMessage).toBeNull(); // Default
140
+ });
141
+ });
142
+
143
+ describe('getInteractions', () => {
144
+ it('should return empty array for new recorder', () => {
145
+ const interactions = recorder.getInteractions('cursor');
146
+ expect(interactions).toEqual([]);
147
+ });
148
+
149
+ it('should return interactions for specific IDE', () => {
150
+ recorder.recordInteraction('cursor', 'success');
151
+ recorder.recordInteraction('windsurf', 'failure');
152
+ recorder.recordInteraction('cursor', 'success');
153
+
154
+ const cursorInteractions = recorder.getInteractions('cursor');
155
+ const windsurfInteractions = recorder.getInteractions('windsurf');
156
+
157
+ expect(cursorInteractions).toHaveLength(2);
158
+ expect(windsurfInteractions).toHaveLength(1);
159
+ });
160
+
161
+ it('should return interactions in chronological order', () => {
162
+ const now = Date.now();
163
+
164
+ const interaction1 = recorder.recordInteraction('cursor', 'success');
165
+ // Small delay to ensure different timestamps
166
+ const interaction2 = recorder.recordInteraction('cursor', 'failure');
167
+ const interaction3 = recorder.recordInteraction('cursor', 'success');
168
+
169
+ const interactions = recorder.getInteractions('cursor');
170
+
171
+ expect(interactions[0]).toEqual(interaction1);
172
+ expect(interactions[1]).toEqual(interaction2);
173
+ expect(interactions[2]).toEqual(interaction3);
174
+ });
175
+ });
176
+
177
+ describe('getAllInteractions', () => {
178
+ it('should return all interactions grouped by IDE', () => {
179
+ recorder.recordInteraction('cursor', 'success');
180
+ recorder.recordInteraction('windsurf', 'failure');
181
+ recorder.recordInteraction('cursor', 'success');
182
+
183
+ const allInteractions = recorder.getAllInteractions();
184
+
185
+ expect(allInteractions).toHaveProperty('cursor');
186
+ expect(allInteractions).toHaveProperty('windsurf');
187
+ expect(allInteractions.cursor).toHaveLength(2);
188
+ expect(allInteractions.windsurf).toHaveLength(1);
189
+ });
190
+
191
+ it('should return empty object for no interactions', () => {
192
+ const allInteractions = recorder.getAllInteractions();
193
+ expect(allInteractions).toEqual({});
194
+ });
195
+ });
196
+
197
+ describe('clearInteractions', () => {
198
+ it('should clear interactions for specific IDE', () => {
199
+ recorder.recordInteraction('cursor', 'success');
200
+ recorder.recordInteraction('windsurf', 'failure');
201
+
202
+ recorder.clearInteractions('cursor');
203
+
204
+ expect(recorder.getInteractions('cursor')).toEqual([]);
205
+ expect(recorder.getInteractions('windsurf')).toHaveLength(1);
206
+ });
207
+
208
+ it('should clear all interactions when no IDE specified', () => {
209
+ recorder.recordInteraction('cursor', 'success');
210
+ recorder.recordInteraction('windsurf', 'failure');
211
+
212
+ recorder.clearInteractions();
213
+
214
+ expect(recorder.getAllInteractions()).toEqual({});
215
+ });
216
+ });
217
+
218
+ describe('interaction limit management', () => {
219
+ it('should maintain maximum interactions per IDE', () => {
220
+ // Add more interactions than max
221
+ for (let i = 0; i < MAX_INTERACTIONS + 10; i++) {
222
+ recorder.recordInteraction('cursor', 'success');
223
+ }
224
+
225
+ const interactions = recorder.getInteractions('cursor');
226
+ expect(interactions.length).toBe(MAX_INTERACTIONS);
227
+ });
228
+
229
+ it('should remove oldest interactions when limit exceeded', () => {
230
+ const firstInteraction = recorder.recordInteraction('cursor', 'success');
231
+
232
+ // Add max interactions
233
+ for (let i = 0; i < MAX_INTERACTIONS; i++) {
234
+ recorder.recordInteraction('cursor', 'success');
235
+ }
236
+
237
+ const interactions = recorder.getInteractions('cursor');
238
+ expect(interactions).not.toContain(firstInteraction);
239
+ expect(interactions.length).toBe(MAX_INTERACTIONS);
240
+ });
241
+ });
242
+
243
+ describe('getInteractionStats', () => {
244
+ it('should return stats for IDE with interactions', () => {
245
+ recorder.recordInteraction('cursor', 'success', { responseTime: 120000 });
246
+ recorder.recordInteraction('cursor', 'failure');
247
+ recorder.recordInteraction('cursor', 'quota');
248
+
249
+ const stats = recorder.getInteractionStats('cursor');
250
+
251
+ expect(stats.total).toBe(3);
252
+ expect(stats.success).toBe(1);
253
+ expect(stats.failure).toBe(1);
254
+ expect(stats.quota).toBe(1);
255
+ expect(stats.successRate).toBeCloseTo(0.33, 2);
256
+ });
257
+
258
+ it('should return zero stats for IDE with no interactions', () => {
259
+ const stats = recorder.getInteractionStats('unknown-ide');
260
+
261
+ expect(stats.total).toBe(0);
262
+ expect(stats.success).toBe(0);
263
+ expect(stats.failure).toBe(0);
264
+ expect(stats.quota).toBe(0);
265
+ expect(stats.successRate).toBe(0);
266
+ });
267
+
268
+ it('should calculate average response time for successful interactions', () => {
269
+ recorder.recordInteraction('cursor', 'success', { responseTime: 120000 });
270
+ recorder.recordInteraction('cursor', 'success', { responseTime: 180000 });
271
+ recorder.recordInteraction('cursor', 'failure'); // No response time
272
+
273
+ const stats = recorder.getInteractionStats('cursor');
274
+
275
+ expect(stats.averageResponseTime).toBe(150000); // (120000 + 180000) / 2
276
+ });
277
+
278
+ it('should handle average response time with no successful interactions', () => {
279
+ recorder.recordInteraction('cursor', 'failure');
280
+ recorder.recordInteraction('cursor', 'quota');
281
+
282
+ const stats = recorder.getInteractionStats('cursor');
283
+
284
+ expect(stats.averageResponseTime).toBeNull();
285
+ });
286
+ });
287
+
288
+ describe('DEFAULT_IDE_RECORD', () => {
289
+ it('should provide default structure for new IDE records', () => {
290
+ const defaultRecord = DEFAULT_IDE_RECORD;
291
+
292
+ expect(defaultRecord.successCount).toBe(0);
293
+ expect(defaultRecord.failureCount).toBe(0);
294
+ expect(defaultRecord.responseTimes).toEqual([]);
295
+ expect(defaultRecord.ewma).toBe(0);
296
+ expect(defaultRecord.lastSuccess).toBeNull();
297
+ expect(defaultRecord.lastFailure).toBeNull();
298
+ expect(defaultRecord.consecutiveFailures).toBe(0);
299
+ expect(defaultRecord.currentTimeout).toBe(1800000); // 30 minutes
300
+ expect(defaultRecord.interactions).toEqual([]);
301
+ });
302
+ });
303
+
304
+ describe('MAX_INTERACTIONS constant', () => {
305
+ it('should define maximum interactions per IDE', () => {
306
+ expect(MAX_INTERACTIONS).toBe(100);
307
+ });
308
+ });
309
+ });