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.
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,420 @@
1
+ /**
2
+ * Unit tests for IDEHealthTracker
3
+ * @jest-environment node
4
+ */
5
+
6
+ const fs = require('fs-extra');
7
+ const path = require('path');
8
+ const os = require('os');
9
+ const { IDEHealthTracker } = require('../ide-health-tracker');
10
+ const { ValidationError } = require('../errors');
11
+
12
+ describe('IDEHealthTracker', () => {
13
+ let tracker;
14
+ let testStorageFile;
15
+
16
+ beforeEach(() => {
17
+ // Create temporary storage file for testing
18
+ testStorageFile = path.join(os.tmpdir(), `test-health-${Date.now()}.json`);
19
+ tracker = new IDEHealthTracker({
20
+ storageFile: testStorageFile,
21
+ autoSave: false, // Disable auto-save for testing
22
+ });
23
+ });
24
+
25
+ afterEach(async () => {
26
+ // Clean up test files
27
+ if (await fs.pathExists(testStorageFile)) {
28
+ await fs.remove(testStorageFile);
29
+ }
30
+ const backupFile = `${testStorageFile}.bak`;
31
+ if (await fs.pathExists(backupFile)) {
32
+ await fs.remove(backupFile);
33
+ }
34
+ });
35
+
36
+ describe('recordSuccess()', () => {
37
+ it('should increment success count on recordSuccess', async () => {
38
+ await tracker.recordSuccess('cursor', 120000);
39
+
40
+ const metrics = await tracker.getHealthMetrics('cursor');
41
+ expect(metrics.successCount).toBe(1);
42
+ expect(metrics.failureCount).toBe(0);
43
+ expect(metrics.consecutiveFailures).toBe(0);
44
+ });
45
+
46
+ it('should reset consecutive failures on success after failure', async () => {
47
+ await tracker.recordFailure('cursor', 'Test error');
48
+ await tracker.recordFailure('cursor', 'Test error');
49
+ const metricsAfterFailures = await tracker.getHealthMetrics('cursor');
50
+ expect(metricsAfterFailures.consecutiveFailures).toBe(2);
51
+
52
+ await tracker.recordSuccess('cursor', 120000);
53
+ const metricsAfterSuccess = await tracker.getHealthMetrics('cursor');
54
+ expect(metricsAfterSuccess.consecutiveFailures).toBe(0);
55
+ expect(metricsAfterSuccess.successCount).toBe(1);
56
+ expect(metricsAfterSuccess.failureCount).toBe(2);
57
+ });
58
+
59
+ it('should add responseTime to responseTimes array', async () => {
60
+ await tracker.recordSuccess('cursor', 120000);
61
+ await tracker.recordSuccess('cursor', 115000);
62
+ await tracker.recordSuccess('cursor', 125000);
63
+
64
+ const metrics = await tracker.getHealthMetrics('cursor');
65
+ expect(metrics.successCount).toBe(3);
66
+ // Note: We can't directly access responseTimes in metrics,
67
+ // but we can verify EWMA was calculated
68
+ expect(metrics.averageResponseTime).toBeGreaterThan(0);
69
+ });
70
+
71
+ it('should enforce max 50 response times', async () => {
72
+ // Add 60 response times
73
+ for (let i = 0; i < 60; i++) {
74
+ await tracker.recordSuccess('cursor', 120000 + i * 100);
75
+ }
76
+
77
+ // Verify it doesn't throw and counts are correct
78
+ const metrics = await tracker.getHealthMetrics('cursor');
79
+ expect(metrics.successCount).toBe(60);
80
+ // Internal responseTimes array should be capped at 50
81
+ // This will be verified in internal state
82
+ });
83
+
84
+ it('should update lastSuccess timestamp', async () => {
85
+ const beforeTime = new Date().toISOString();
86
+ await tracker.recordSuccess('cursor', 120000);
87
+ const afterTime = new Date().toISOString();
88
+
89
+ const metrics = await tracker.getHealthMetrics('cursor');
90
+ expect(metrics.lastSuccess).not.toBeNull();
91
+ expect(metrics.lastSuccess >= beforeTime).toBe(true);
92
+ expect(metrics.lastSuccess <= afterTime).toBe(true);
93
+ });
94
+
95
+ it('should throw ValidationError if ideId is empty', async () => {
96
+ await expect(tracker.recordSuccess('', 120000))
97
+ .rejects
98
+ .toThrow(ValidationError);
99
+ });
100
+
101
+ it('should throw ValidationError if responseTime is negative', async () => {
102
+ await expect(tracker.recordSuccess('cursor', -1000))
103
+ .rejects
104
+ .toThrow(ValidationError);
105
+ });
106
+
107
+ it('should accept metadata with continuationPromptsDetected', async () => {
108
+ await tracker.recordSuccess('cursor', 120000, {
109
+ continuationPromptsDetected: 2,
110
+ requirementId: 'req-042',
111
+ });
112
+
113
+ const metrics = await tracker.getHealthMetrics('cursor');
114
+ expect(metrics.successCount).toBe(1);
115
+ // Interaction record should have continuationPromptsDetected
116
+ expect(metrics.recentInteractions).toHaveLength(1);
117
+ expect(metrics.recentInteractions[0].continuationPromptsDetected).toBe(2);
118
+ });
119
+
120
+ it('should recalculate EWMA on each success', async () => {
121
+ await tracker.recordSuccess('cursor', 120000);
122
+ const metrics1 = await tracker.getHealthMetrics('cursor');
123
+ const ewma1 = metrics1.averageResponseTime;
124
+
125
+ await tracker.recordSuccess('cursor', 150000);
126
+ const metrics2 = await tracker.getHealthMetrics('cursor');
127
+ const ewma2 = metrics2.averageResponseTime;
128
+
129
+ // EWMA should have changed
130
+ expect(ewma2).not.toBe(ewma1);
131
+ expect(ewma2).toBeGreaterThan(ewma1);
132
+ });
133
+
134
+ it('should emit health-updated event on success', (done) => {
135
+ tracker.on('health-updated', ({ ideId, outcome }) => {
136
+ expect(ideId).toBe('cursor');
137
+ expect(outcome).toBe('success');
138
+ done();
139
+ });
140
+
141
+ tracker.recordSuccess('cursor', 120000);
142
+ });
143
+ });
144
+
145
+ describe('recordFailure()', () => {
146
+ it('should increment failure count on recordFailure', async () => {
147
+ await tracker.recordFailure('cursor', 'Timeout exceeded');
148
+
149
+ const metrics = await tracker.getHealthMetrics('cursor');
150
+ expect(metrics.successCount).toBe(0);
151
+ expect(metrics.failureCount).toBe(1);
152
+ expect(metrics.consecutiveFailures).toBe(1);
153
+ });
154
+
155
+ it('should increment consecutive failures on multiple failures', async () => {
156
+ await tracker.recordFailure('cursor', 'Error 1');
157
+ await tracker.recordFailure('cursor', 'Error 2');
158
+ await tracker.recordFailure('cursor', 'Error 3');
159
+
160
+ const metrics = await tracker.getHealthMetrics('cursor');
161
+ expect(metrics.failureCount).toBe(3);
162
+ expect(metrics.consecutiveFailures).toBe(3);
163
+ });
164
+
165
+ it('should emit consecutive-failures event at threshold 5', (done) => {
166
+ tracker.on('consecutive-failures', ({ ideId, count, lastError }) => {
167
+ expect(ideId).toBe('cursor');
168
+ expect(count).toBe(5);
169
+ expect(lastError).toBe('Error 5');
170
+ done();
171
+ });
172
+
173
+ (async () => {
174
+ for (let i = 1; i <= 5; i++) {
175
+ await tracker.recordFailure('cursor', `Error ${i}`);
176
+ }
177
+ })();
178
+ });
179
+
180
+ it('should update lastFailure timestamp', async () => {
181
+ const beforeTime = new Date().toISOString();
182
+ await tracker.recordFailure('cursor', 'Test error');
183
+ const afterTime = new Date().toISOString();
184
+
185
+ const metrics = await tracker.getHealthMetrics('cursor');
186
+ expect(metrics.lastFailure).not.toBeNull();
187
+ expect(metrics.lastFailure >= beforeTime).toBe(true);
188
+ expect(metrics.lastFailure <= afterTime).toBe(true);
189
+ });
190
+
191
+ it('should accept metadata with timeoutUsed', async () => {
192
+ await tracker.recordFailure('cursor', 'Timeout', {
193
+ timeoutUsed: 1800000,
194
+ requirementId: 'req-043',
195
+ });
196
+
197
+ const metrics = await tracker.getHealthMetrics('cursor');
198
+ expect(metrics.failureCount).toBe(1);
199
+ expect(metrics.recentInteractions).toHaveLength(1);
200
+ expect(metrics.recentInteractions[0].errorMessage).toBe('Timeout');
201
+ });
202
+
203
+ it('should emit health-updated event on failure', (done) => {
204
+ tracker.on('health-updated', ({ ideId, outcome }) => {
205
+ expect(ideId).toBe('cursor');
206
+ expect(outcome).toBe('failure');
207
+ done();
208
+ });
209
+
210
+ tracker.recordFailure('cursor', 'Test error');
211
+ });
212
+ });
213
+
214
+ describe('recordQuota()', () => {
215
+ it('should NOT increment success or failure counters on quota', async () => {
216
+ await tracker.recordQuota('cursor', 'Monthly chat messages quota exceeded');
217
+
218
+ const metrics = await tracker.getHealthMetrics('cursor');
219
+ expect(metrics.successCount).toBe(0);
220
+ expect(metrics.failureCount).toBe(0);
221
+ expect(metrics.consecutiveFailures).toBe(0);
222
+ });
223
+
224
+ it('should add interaction record with outcome=quota', async () => {
225
+ await tracker.recordQuota('cursor', 'Quota exceeded', {
226
+ requirementId: 'req-044',
227
+ });
228
+
229
+ const metrics = await tracker.getHealthMetrics('cursor');
230
+ expect(metrics.recentInteractions).toHaveLength(1);
231
+ expect(metrics.recentInteractions[0].outcome).toBe('quota');
232
+ expect(metrics.recentInteractions[0].errorMessage).toBe('Quota exceeded');
233
+ });
234
+
235
+ it('should emit health-updated event with outcome=quota', (done) => {
236
+ tracker.on('health-updated', ({ ideId, outcome }) => {
237
+ expect(ideId).toBe('cursor');
238
+ expect(outcome).toBe('quota');
239
+ done();
240
+ });
241
+
242
+ tracker.recordQuota('cursor', 'Quota message');
243
+ });
244
+ });
245
+
246
+ describe('getHealthMetrics()', () => {
247
+ it('should return metrics for specific IDE', async () => {
248
+ await tracker.recordSuccess('cursor', 120000);
249
+ await tracker.recordFailure('cursor', 'Error');
250
+
251
+ const metrics = await tracker.getHealthMetrics('cursor');
252
+ expect(metrics.successCount).toBe(1);
253
+ expect(metrics.failureCount).toBe(1);
254
+ expect(metrics.successRate).toBeCloseTo(0.5);
255
+ expect(metrics.totalInteractions).toBe(2);
256
+ });
257
+
258
+ it('should calculate success rate correctly', async () => {
259
+ await tracker.recordSuccess('cursor', 120000);
260
+ await tracker.recordSuccess('cursor', 115000);
261
+ await tracker.recordSuccess('cursor', 125000);
262
+ await tracker.recordFailure('cursor', 'Error');
263
+
264
+ const metrics = await tracker.getHealthMetrics('cursor');
265
+ expect(metrics.successRate).toBeCloseTo(0.75); // 3/4 = 0.75
266
+ });
267
+
268
+ it('should return recent 10 interactions', async () => {
269
+ // Add 15 interactions
270
+ for (let i = 0; i < 15; i++) {
271
+ if (i % 2 === 0) {
272
+ await tracker.recordSuccess('cursor', 120000);
273
+ } else {
274
+ await tracker.recordFailure('cursor', 'Error');
275
+ }
276
+ }
277
+
278
+ const metrics = await tracker.getHealthMetrics('cursor');
279
+ expect(metrics.recentInteractions).toHaveLength(10);
280
+ });
281
+
282
+ it('should return default metrics for unknown IDE', async () => {
283
+ const metrics = await tracker.getHealthMetrics('unknown-ide');
284
+ expect(metrics.successCount).toBe(0);
285
+ expect(metrics.failureCount).toBe(0);
286
+ expect(metrics.successRate).toBe(0);
287
+ expect(metrics.totalInteractions).toBe(0);
288
+ });
289
+ });
290
+
291
+ describe('getAllHealthMetrics()', () => {
292
+ it('should return metrics for all tracked IDEs', async () => {
293
+ await tracker.recordSuccess('cursor', 120000);
294
+ await tracker.recordSuccess('windsurf', 180000);
295
+ await tracker.recordFailure('vscode', 'Error');
296
+
297
+ const allMetrics = await tracker.getAllHealthMetrics();
298
+ expect(allMetrics.size).toBe(3);
299
+ expect(allMetrics.has('cursor')).toBe(true);
300
+ expect(allMetrics.has('windsurf')).toBe(true);
301
+ expect(allMetrics.has('vscode')).toBe(true);
302
+ });
303
+
304
+ it('should return empty Map if no IDEs tracked', async () => {
305
+ const allMetrics = await tracker.getAllHealthMetrics();
306
+ expect(allMetrics.size).toBe(0);
307
+ expect(allMetrics instanceof Map).toBe(true);
308
+ });
309
+ });
310
+
311
+ describe('getRecommendedIDE()', () => {
312
+ it('should return IDE with highest success rate', async () => {
313
+ // Cursor: 80% success (4/5)
314
+ await tracker.recordSuccess('cursor', 120000);
315
+ await tracker.recordSuccess('cursor', 120000);
316
+ await tracker.recordSuccess('cursor', 120000);
317
+ await tracker.recordSuccess('cursor', 120000);
318
+ await tracker.recordFailure('cursor', 'Error');
319
+
320
+ // Windsurf: 60% success (3/5)
321
+ await tracker.recordSuccess('windsurf', 180000);
322
+ await tracker.recordSuccess('windsurf', 180000);
323
+ await tracker.recordSuccess('windsurf', 180000);
324
+ await tracker.recordFailure('windsurf', 'Error');
325
+ await tracker.recordFailure('windsurf', 'Error');
326
+
327
+ const recommended = await tracker.getRecommendedIDE({ minInteractions: 5 });
328
+ expect(recommended).toBe('cursor');
329
+ });
330
+
331
+ it('should return null if no IDEs meet minInteractions threshold', async () => {
332
+ await tracker.recordSuccess('cursor', 120000);
333
+
334
+ const recommended = await tracker.getRecommendedIDE({ minInteractions: 10 });
335
+ expect(recommended).toBeNull();
336
+ });
337
+
338
+ it('should exclude IDEs below minInteractions threshold', async () => {
339
+ // Cursor: only 2 interactions
340
+ await tracker.recordSuccess('cursor', 120000);
341
+ await tracker.recordSuccess('cursor', 120000);
342
+
343
+ // Windsurf: 12 interactions with 75% success
344
+ for (let i = 0; i < 12; i++) {
345
+ if (i < 9) {
346
+ await tracker.recordSuccess('windsurf', 180000);
347
+ } else {
348
+ await tracker.recordFailure('windsurf', 'Error');
349
+ }
350
+ }
351
+
352
+ const recommended = await tracker.getRecommendedIDE({ minInteractions: 10 });
353
+ expect(recommended).toBe('windsurf');
354
+ });
355
+ });
356
+
357
+ describe('save() and load()', () => {
358
+ it('should persist health data across instances', async () => {
359
+ const tracker1 = new IDEHealthTracker({ storageFile: testStorageFile });
360
+ await tracker1.recordSuccess('cursor', 120000);
361
+ await tracker1.save();
362
+
363
+ const tracker2 = new IDEHealthTracker({ storageFile: testStorageFile });
364
+ await tracker2.load();
365
+ const metrics = await tracker2.getHealthMetrics('cursor');
366
+
367
+ expect(metrics.successCount).toBe(1);
368
+ });
369
+
370
+ it('should load default data if file does not exist', async () => {
371
+ const tracker2 = new IDEHealthTracker({ storageFile: testStorageFile });
372
+ await tracker2.load();
373
+ const metrics = await tracker2.getHealthMetrics('cursor');
374
+
375
+ expect(metrics.successCount).toBe(0);
376
+ expect(metrics.failureCount).toBe(0);
377
+ });
378
+
379
+ it('should throw FileSystemError on corrupted data without backup', async () => {
380
+ // Create corrupted file
381
+ await fs.writeFile(testStorageFile, 'invalid json{{{');
382
+
383
+ const tracker2 = new IDEHealthTracker({ storageFile: testStorageFile });
384
+ await expect(tracker2.load()).rejects.toThrow();
385
+ });
386
+ });
387
+
388
+ describe('autoSave with debouncing', () => {
389
+ it('should auto-save after debounce period when autoSave is enabled', async () => {
390
+ const autoSaveTracker = new IDEHealthTracker({
391
+ storageFile: testStorageFile,
392
+ autoSave: true,
393
+ debounceMs: 100,
394
+ });
395
+
396
+ await autoSaveTracker.recordSuccess('cursor', 120000);
397
+
398
+ // Wait for debounce using Promise
399
+ await new Promise(resolve => setTimeout(resolve, 200));
400
+
401
+ const fileExists = await fs.pathExists(testStorageFile);
402
+ expect(fileExists).toBe(true);
403
+ });
404
+ });
405
+
406
+ describe('interaction records', () => {
407
+ it('should enforce max 100 interactions per IDE', async () => {
408
+ // Add 120 interactions
409
+ for (let i = 0; i < 120; i++) {
410
+ await tracker.recordSuccess('cursor', 120000);
411
+ }
412
+
413
+ const metrics = await tracker.getHealthMetrics('cursor');
414
+ // Recent interactions should be capped at 10 (for display)
415
+ expect(metrics.recentInteractions).toHaveLength(10);
416
+ // Internal storage should be capped at 100
417
+ // This will be verified in implementation
418
+ });
419
+ });
420
+ });