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.
- 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,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for InteractionRecorder
|
|
3
|
+
* @jest-environment node
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { InteractionRecorder } = require('../interaction-recorder');
|
|
7
|
+
const { ValidationError } = require('../errors');
|
|
8
|
+
|
|
9
|
+
describe('InteractionRecorder', () => {
|
|
10
|
+
let recorder;
|
|
11
|
+
let mockStorage;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
// Create mock storage
|
|
15
|
+
mockStorage = {
|
|
16
|
+
data: {
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
lastUpdated: new Date().toISOString(),
|
|
19
|
+
ides: {},
|
|
20
|
+
timeoutConfig: {},
|
|
21
|
+
defaultRequirement: null,
|
|
22
|
+
},
|
|
23
|
+
async read() {
|
|
24
|
+
return JSON.parse(JSON.stringify(this.data));
|
|
25
|
+
},
|
|
26
|
+
async write(data) {
|
|
27
|
+
this.data = JSON.parse(JSON.stringify(data));
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
recorder = new InteractionRecorder(mockStorage);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('record()', () => {
|
|
35
|
+
it('should record a success interaction', async () => {
|
|
36
|
+
const interaction = {
|
|
37
|
+
ideId: 'cursor',
|
|
38
|
+
timestamp: new Date(),
|
|
39
|
+
outcome: 'success',
|
|
40
|
+
responseTime: 120000,
|
|
41
|
+
timeoutUsed: 180000,
|
|
42
|
+
continuationPromptsDetected: 1,
|
|
43
|
+
requirementId: 'req-001',
|
|
44
|
+
errorMessage: null,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
await recorder.record(interaction);
|
|
48
|
+
|
|
49
|
+
const data = await mockStorage.read();
|
|
50
|
+
expect(data.ides.cursor).toBeDefined();
|
|
51
|
+
expect(data.ides.cursor.interactions).toHaveLength(1);
|
|
52
|
+
expect(data.ides.cursor.interactions[0].outcome).toBe('success');
|
|
53
|
+
expect(data.ides.cursor.interactions[0].responseTime).toBe(120000);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should record a failure interaction', async () => {
|
|
57
|
+
const interaction = {
|
|
58
|
+
ideId: 'windsurf',
|
|
59
|
+
timestamp: new Date(),
|
|
60
|
+
outcome: 'failure',
|
|
61
|
+
responseTime: null,
|
|
62
|
+
timeoutUsed: 1800000,
|
|
63
|
+
continuationPromptsDetected: 0,
|
|
64
|
+
requirementId: 'req-002',
|
|
65
|
+
errorMessage: 'Timeout exceeded',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
await recorder.record(interaction);
|
|
69
|
+
|
|
70
|
+
const data = await mockStorage.read();
|
|
71
|
+
expect(data.ides.windsurf.interactions).toHaveLength(1);
|
|
72
|
+
expect(data.ides.windsurf.interactions[0].outcome).toBe('failure');
|
|
73
|
+
expect(data.ides.windsurf.interactions[0].errorMessage).toBe('Timeout exceeded');
|
|
74
|
+
expect(data.ides.windsurf.interactions[0].responseTime).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should record a quota interaction', async () => {
|
|
78
|
+
const interaction = {
|
|
79
|
+
ideId: 'cursor',
|
|
80
|
+
timestamp: new Date(),
|
|
81
|
+
outcome: 'quota',
|
|
82
|
+
responseTime: null,
|
|
83
|
+
timeoutUsed: 1800000,
|
|
84
|
+
continuationPromptsDetected: 0,
|
|
85
|
+
requirementId: 'req-003',
|
|
86
|
+
errorMessage: 'Monthly quota exceeded',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await recorder.record(interaction);
|
|
90
|
+
|
|
91
|
+
const data = await mockStorage.read();
|
|
92
|
+
expect(data.ides.cursor.interactions).toHaveLength(1);
|
|
93
|
+
expect(data.ides.cursor.interactions[0].outcome).toBe('quota');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should convert Date timestamp to ISO string', async () => {
|
|
97
|
+
const timestamp = new Date('2025-01-21T12:00:00Z');
|
|
98
|
+
const interaction = {
|
|
99
|
+
ideId: 'cursor',
|
|
100
|
+
timestamp,
|
|
101
|
+
outcome: 'success',
|
|
102
|
+
responseTime: 120000,
|
|
103
|
+
timeoutUsed: 180000,
|
|
104
|
+
continuationPromptsDetected: 0,
|
|
105
|
+
requirementId: null,
|
|
106
|
+
errorMessage: null,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
await recorder.record(interaction);
|
|
110
|
+
|
|
111
|
+
const data = await mockStorage.read();
|
|
112
|
+
expect(data.ides.cursor.interactions[0].timestamp).toBe('2025-01-21T12:00:00.000Z');
|
|
113
|
+
expect(typeof data.ides.cursor.interactions[0].timestamp).toBe('string');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should initialize IDE record if it does not exist', async () => {
|
|
117
|
+
const interaction = {
|
|
118
|
+
ideId: 'new-ide',
|
|
119
|
+
timestamp: new Date(),
|
|
120
|
+
outcome: 'success',
|
|
121
|
+
responseTime: 120000,
|
|
122
|
+
timeoutUsed: 180000,
|
|
123
|
+
continuationPromptsDetected: 0,
|
|
124
|
+
requirementId: null,
|
|
125
|
+
errorMessage: null,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
await recorder.record(interaction);
|
|
129
|
+
|
|
130
|
+
const data = await mockStorage.read();
|
|
131
|
+
expect(data.ides['new-ide']).toBeDefined();
|
|
132
|
+
expect(data.ides['new-ide'].interactions).toHaveLength(1);
|
|
133
|
+
expect(data.ides['new-ide'].successCount).toBe(0);
|
|
134
|
+
expect(data.ides['new-ide'].failureCount).toBe(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should enforce max 100 interactions per IDE', async () => {
|
|
138
|
+
// Add 120 interactions
|
|
139
|
+
for (let i = 0; i < 120; i++) {
|
|
140
|
+
const interaction = {
|
|
141
|
+
ideId: 'cursor',
|
|
142
|
+
timestamp: new Date(),
|
|
143
|
+
outcome: 'success',
|
|
144
|
+
responseTime: 120000 + i,
|
|
145
|
+
timeoutUsed: 180000,
|
|
146
|
+
continuationPromptsDetected: 0,
|
|
147
|
+
requirementId: null,
|
|
148
|
+
errorMessage: null,
|
|
149
|
+
};
|
|
150
|
+
await recorder.record(interaction);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const data = await mockStorage.read();
|
|
154
|
+
expect(data.ides.cursor.interactions.length).toBeLessThanOrEqual(100);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should remove oldest interaction when exceeding max', async () => {
|
|
158
|
+
// Add interactions with distinct response times
|
|
159
|
+
for (let i = 0; i < 101; i++) {
|
|
160
|
+
const interaction = {
|
|
161
|
+
ideId: 'cursor',
|
|
162
|
+
timestamp: new Date(Date.now() + i * 1000),
|
|
163
|
+
outcome: 'success',
|
|
164
|
+
responseTime: 100000 + i,
|
|
165
|
+
timeoutUsed: 180000,
|
|
166
|
+
continuationPromptsDetected: 0,
|
|
167
|
+
requirementId: `req-${i}`,
|
|
168
|
+
errorMessage: null,
|
|
169
|
+
};
|
|
170
|
+
await recorder.record(interaction);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const data = await mockStorage.read();
|
|
174
|
+
expect(data.ides.cursor.interactions).toHaveLength(100);
|
|
175
|
+
// First interaction (100000ms) should be removed
|
|
176
|
+
const firstResponseTime = data.ides.cursor.interactions[0].responseTime;
|
|
177
|
+
expect(firstResponseTime).toBe(100001); // Second interaction is now first
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should throw ValidationError for invalid outcome', async () => {
|
|
181
|
+
const interaction = {
|
|
182
|
+
ideId: 'cursor',
|
|
183
|
+
timestamp: new Date(),
|
|
184
|
+
outcome: 'invalid-outcome',
|
|
185
|
+
responseTime: 120000,
|
|
186
|
+
timeoutUsed: 180000,
|
|
187
|
+
continuationPromptsDetected: 0,
|
|
188
|
+
requirementId: null,
|
|
189
|
+
errorMessage: null,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
await expect(recorder.record(interaction))
|
|
193
|
+
.rejects
|
|
194
|
+
.toThrow(ValidationError);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should throw ValidationError for empty ideId', async () => {
|
|
198
|
+
const interaction = {
|
|
199
|
+
ideId: '',
|
|
200
|
+
timestamp: new Date(),
|
|
201
|
+
outcome: 'success',
|
|
202
|
+
responseTime: 120000,
|
|
203
|
+
timeoutUsed: 180000,
|
|
204
|
+
continuationPromptsDetected: 0,
|
|
205
|
+
requirementId: null,
|
|
206
|
+
errorMessage: null,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
await expect(recorder.record(interaction))
|
|
210
|
+
.rejects
|
|
211
|
+
.toThrow(ValidationError);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should throw ValidationError for negative responseTime', async () => {
|
|
215
|
+
const interaction = {
|
|
216
|
+
ideId: 'cursor',
|
|
217
|
+
timestamp: new Date(),
|
|
218
|
+
outcome: 'success',
|
|
219
|
+
responseTime: -1000,
|
|
220
|
+
timeoutUsed: 180000,
|
|
221
|
+
continuationPromptsDetected: 0,
|
|
222
|
+
requirementId: null,
|
|
223
|
+
errorMessage: null,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
await expect(recorder.record(interaction))
|
|
227
|
+
.rejects
|
|
228
|
+
.toThrow(ValidationError);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should allow null responseTime for failures', async () => {
|
|
232
|
+
const interaction = {
|
|
233
|
+
ideId: 'cursor',
|
|
234
|
+
timestamp: new Date(),
|
|
235
|
+
outcome: 'failure',
|
|
236
|
+
responseTime: null,
|
|
237
|
+
timeoutUsed: 180000,
|
|
238
|
+
continuationPromptsDetected: 0,
|
|
239
|
+
requirementId: null,
|
|
240
|
+
errorMessage: 'Timeout',
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
await recorder.record(interaction);
|
|
244
|
+
|
|
245
|
+
const data = await mockStorage.read();
|
|
246
|
+
expect(data.ides.cursor.interactions[0].responseTime).toBeNull();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should handle multiple IDEs independently', async () => {
|
|
250
|
+
const cursorInteraction = {
|
|
251
|
+
ideId: 'cursor',
|
|
252
|
+
timestamp: new Date(),
|
|
253
|
+
outcome: 'success',
|
|
254
|
+
responseTime: 120000,
|
|
255
|
+
timeoutUsed: 180000,
|
|
256
|
+
continuationPromptsDetected: 0,
|
|
257
|
+
requirementId: null,
|
|
258
|
+
errorMessage: null,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const windsurfInteraction = {
|
|
262
|
+
ideId: 'windsurf',
|
|
263
|
+
timestamp: new Date(),
|
|
264
|
+
outcome: 'failure',
|
|
265
|
+
responseTime: null,
|
|
266
|
+
timeoutUsed: 1800000,
|
|
267
|
+
continuationPromptsDetected: 0,
|
|
268
|
+
requirementId: null,
|
|
269
|
+
errorMessage: 'Error',
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
await recorder.record(cursorInteraction);
|
|
273
|
+
await recorder.record(windsurfInteraction);
|
|
274
|
+
|
|
275
|
+
const data = await mockStorage.read();
|
|
276
|
+
expect(data.ides.cursor.interactions).toHaveLength(1);
|
|
277
|
+
expect(data.ides.windsurf.interactions).toHaveLength(1);
|
|
278
|
+
expect(data.ides.cursor.interactions[0].outcome).toBe('success');
|
|
279
|
+
expect(data.ides.windsurf.interactions[0].outcome).toBe('failure');
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('getInteractions()', () => {
|
|
284
|
+
it('should retrieve interactions for a specific IDE', async () => {
|
|
285
|
+
const interaction1 = {
|
|
286
|
+
ideId: 'cursor',
|
|
287
|
+
timestamp: new Date(),
|
|
288
|
+
outcome: 'success',
|
|
289
|
+
responseTime: 120000,
|
|
290
|
+
timeoutUsed: 180000,
|
|
291
|
+
continuationPromptsDetected: 0,
|
|
292
|
+
requirementId: null,
|
|
293
|
+
errorMessage: null,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const interaction2 = {
|
|
297
|
+
ideId: 'cursor',
|
|
298
|
+
timestamp: new Date(),
|
|
299
|
+
outcome: 'failure',
|
|
300
|
+
responseTime: null,
|
|
301
|
+
timeoutUsed: 180000,
|
|
302
|
+
continuationPromptsDetected: 0,
|
|
303
|
+
requirementId: null,
|
|
304
|
+
errorMessage: 'Error',
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
await recorder.record(interaction1);
|
|
308
|
+
await recorder.record(interaction2);
|
|
309
|
+
|
|
310
|
+
const interactions = await recorder.getInteractions('cursor');
|
|
311
|
+
expect(interactions).toHaveLength(2);
|
|
312
|
+
expect(interactions[0].outcome).toBe('success');
|
|
313
|
+
expect(interactions[1].outcome).toBe('failure');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should return empty array for IDE with no interactions', async () => {
|
|
317
|
+
const interactions = await recorder.getInteractions('nonexistent-ide');
|
|
318
|
+
expect(interactions).toEqual([]);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should support limiting number of returned interactions', async () => {
|
|
322
|
+
// Add 15 interactions
|
|
323
|
+
for (let i = 0; i < 15; i++) {
|
|
324
|
+
await recorder.record({
|
|
325
|
+
ideId: 'cursor',
|
|
326
|
+
timestamp: new Date(),
|
|
327
|
+
outcome: 'success',
|
|
328
|
+
responseTime: 120000,
|
|
329
|
+
timeoutUsed: 180000,
|
|
330
|
+
continuationPromptsDetected: 0,
|
|
331
|
+
requirementId: null,
|
|
332
|
+
errorMessage: null,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const recentTen = await recorder.getInteractions('cursor', 10);
|
|
337
|
+
expect(recentTen).toHaveLength(10);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe('clearInteractions()', () => {
|
|
342
|
+
it('should clear all interactions for a specific IDE', async () => {
|
|
343
|
+
await recorder.record({
|
|
344
|
+
ideId: 'cursor',
|
|
345
|
+
timestamp: new Date(),
|
|
346
|
+
outcome: 'success',
|
|
347
|
+
responseTime: 120000,
|
|
348
|
+
timeoutUsed: 180000,
|
|
349
|
+
continuationPromptsDetected: 0,
|
|
350
|
+
requirementId: null,
|
|
351
|
+
errorMessage: null,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
await recorder.clearInteractions('cursor');
|
|
355
|
+
|
|
356
|
+
const interactions = await recorder.getInteractions('cursor');
|
|
357
|
+
expect(interactions).toEqual([]);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should not affect other IDEs when clearing one', async () => {
|
|
361
|
+
await recorder.record({
|
|
362
|
+
ideId: 'cursor',
|
|
363
|
+
timestamp: new Date(),
|
|
364
|
+
outcome: 'success',
|
|
365
|
+
responseTime: 120000,
|
|
366
|
+
timeoutUsed: 180000,
|
|
367
|
+
continuationPromptsDetected: 0,
|
|
368
|
+
requirementId: null,
|
|
369
|
+
errorMessage: null,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
await recorder.record({
|
|
373
|
+
ideId: 'windsurf',
|
|
374
|
+
timestamp: new Date(),
|
|
375
|
+
outcome: 'success',
|
|
376
|
+
responseTime: 180000,
|
|
377
|
+
timeoutUsed: 180000,
|
|
378
|
+
continuationPromptsDetected: 0,
|
|
379
|
+
requirementId: null,
|
|
380
|
+
errorMessage: null,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await recorder.clearInteractions('cursor');
|
|
384
|
+
|
|
385
|
+
const cursorInteractions = await recorder.getInteractions('cursor');
|
|
386
|
+
const windsurfInteractions = await recorder.getInteractions('windsurf');
|
|
387
|
+
|
|
388
|
+
expect(cursorInteractions).toEqual([]);
|
|
389
|
+
expect(windsurfInteractions).toHaveLength(1);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Error Classes for Health Tracking Module
|
|
3
|
+
*
|
|
4
|
+
* @module errors
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Thrown when input validation fails
|
|
9
|
+
* @extends Error
|
|
10
|
+
*/
|
|
11
|
+
class ValidationError extends Error {
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} message - Description of validation failure
|
|
14
|
+
* @param {string} field - Field name that failed validation
|
|
15
|
+
* @param {*} value - Invalid value
|
|
16
|
+
*/
|
|
17
|
+
constructor(message, field, value) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'ValidationError';
|
|
20
|
+
this.field = field;
|
|
21
|
+
this.value = value;
|
|
22
|
+
Error.captureStackTrace(this, this.constructor);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Thrown when file system operations fail
|
|
28
|
+
* @extends Error
|
|
29
|
+
*/
|
|
30
|
+
class FileSystemError extends Error {
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} message - Description of file system error
|
|
33
|
+
* @param {string} path - File path that caused the error
|
|
34
|
+
* @param {string} operation - Operation that failed ('read' | 'write' | 'delete' | 'backup' | 'restore')
|
|
35
|
+
* @param {Error} [originalError] - Original error that caused this error
|
|
36
|
+
*/
|
|
37
|
+
constructor(message, path, operation, originalError) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'FileSystemError';
|
|
40
|
+
this.path = path;
|
|
41
|
+
this.operation = operation;
|
|
42
|
+
this.originalError = originalError;
|
|
43
|
+
Error.captureStackTrace(this, this.constructor);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
ValidationError,
|
|
49
|
+
FileSystemError,
|
|
50
|
+
};
|