vibecodingmachine-core 2026.1.3-2209 → 2026.1.23-1010
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.
- package/__tests__/provider-manager-fallback.test.js +43 -0
- package/__tests__/provider-manager-rate-limit.test.js +61 -0
- package/package.json +1 -1
- package/src/compliance/compliance-manager.js +5 -2
- package/src/database/migrations.js +135 -12
- package/src/database/user-database-client.js +63 -8
- package/src/database/user-schema.js +7 -0
- package/src/health-tracking/__tests__/ide-health-tracker.test.js +420 -0
- package/src/health-tracking/__tests__/interaction-recorder.test.js +392 -0
- package/src/health-tracking/errors.js +50 -0
- package/src/health-tracking/health-reporter.js +331 -0
- package/src/health-tracking/ide-health-tracker.js +446 -0
- package/src/health-tracking/interaction-recorder.js +161 -0
- package/src/health-tracking/json-storage.js +276 -0
- package/src/health-tracking/storage-interface.js +63 -0
- package/src/health-tracking/validators.js +277 -0
- package/src/ide-integration/applescript-manager.cjs +1062 -4
- package/src/ide-integration/applescript-manager.js +560 -11
- package/src/ide-integration/provider-manager.cjs +158 -28
- package/src/ide-integration/quota-detector.cjs +339 -16
- package/src/ide-integration/quota-detector.js +6 -1
- package/src/index.cjs +32 -1
- package/src/index.js +16 -0
- package/src/localization/translations/en.js +13 -1
- package/src/localization/translations/es.js +12 -0
- package/src/utils/admin-utils.js +33 -0
- package/src/utils/error-reporter.js +12 -4
- package/src/utils/requirement-helpers.js +34 -4
- package/src/utils/requirements-parser.js +3 -3
- package/tests/health-tracking/health-reporter.test.js +329 -0
- package/tests/health-tracking/ide-health-tracker.test.js +368 -0
- 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
|
+
});
|