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
package/src/index.cjs CHANGED
@@ -24,8 +24,22 @@ const requirementNumbering = require('./requirement-numbering.js');
24
24
  const gitBranchManager = require('./utils/git-branch-manager.js');
25
25
  const updateChecker = require('./utils/update-checker.js');
26
26
  const electronUpdateChecker = require('./utils/electron-update-checker.js');
27
+ const { errorReporter, ErrorReporter } = require('./utils/error-reporter.js');
27
28
  const localization = require('./localization/index.js');
28
29
 
30
+ // Health Tracking (Foundational)
31
+ const { ValidationError, FileSystemError } = require('./health-tracking/errors.js');
32
+ const { JSONStorage, DEFAULT_STORAGE_FILE, DEFAULT_HEALTH_DATA } = require('./health-tracking/json-storage.js');
33
+ const { StorageInterface } = require('./health-tracking/storage-interface.js');
34
+ const validators = require('./health-tracking/validators.js');
35
+
36
+ // Health Tracking (User Story 1)
37
+ const { InteractionRecorder, MAX_INTERACTIONS, DEFAULT_IDE_RECORD } = require('./health-tracking/interaction-recorder.js');
38
+ const { IDEHealthTracker, MAX_RESPONSE_TIMES, CONSECUTIVE_FAILURE_THRESHOLD } = require('./health-tracking/ide-health-tracker.js');
39
+
40
+ // Health Tracking (User Story 2)
41
+ const { HealthReporter } = require('./health-tracking/health-reporter.js');
42
+
29
43
  module.exports = {
30
44
  CDPManager,
31
45
  AppleScriptManager,
@@ -51,5 +65,22 @@ module.exports = {
51
65
  ...gitBranchManager,
52
66
  ...updateChecker,
53
67
  ...electronUpdateChecker,
54
- ...localization
68
+ errorReporter,
69
+ ErrorReporter,
70
+ ...localization,
71
+ // Health Tracking
72
+ ValidationError,
73
+ FileSystemError,
74
+ JSONStorage,
75
+ DEFAULT_STORAGE_FILE,
76
+ DEFAULT_HEALTH_DATA,
77
+ StorageInterface,
78
+ ...validators,
79
+ InteractionRecorder,
80
+ MAX_INTERACTIONS,
81
+ DEFAULT_IDE_RECORD,
82
+ IDEHealthTracker,
83
+ MAX_RESPONSE_TIMES,
84
+ CONSECUTIVE_FAILURE_THRESHOLD,
85
+ HealthReporter
55
86
  };
package/src/index.js CHANGED
@@ -28,3 +28,19 @@ export * from './localization/index.js';
28
28
 
29
29
  // Error Reporting
30
30
  export * from './utils/error-reporter.js';
31
+
32
+ // Admin Utilities
33
+ export * from './utils/admin-utils.js';
34
+
35
+ // Health Tracking (Foundational)
36
+ export { ValidationError, FileSystemError } from './health-tracking/errors.js';
37
+ export { JSONStorage, DEFAULT_STORAGE_FILE, DEFAULT_HEALTH_DATA } from './health-tracking/json-storage.js';
38
+ export { StorageInterface } from './health-tracking/storage-interface.js';
39
+ export * from './health-tracking/validators.js';
40
+
41
+ // Health Tracking (User Story 1)
42
+ export { InteractionRecorder, MAX_INTERACTIONS, DEFAULT_IDE_RECORD } from './health-tracking/interaction-recorder.js';
43
+ export { IDEHealthTracker, MAX_RESPONSE_TIMES, CONSECUTIVE_FAILURE_THRESHOLD } from './health-tracking/ide-health-tracker.js';
44
+
45
+ // Health Tracking (User Story 2)
46
+ export { HealthReporter } from './health-tracking/health-reporter.js';
@@ -303,6 +303,18 @@ module.exports = {
303
303
  'interactive.sync.now': 'Sync Now',
304
304
  'interactive.logout': 'Logout',
305
305
  'interactive.exit': 'Exit',
306
+ 'interactive.feedback': 'Feedback',
307
+ 'interactive.feedback.title': 'Vibe Coding Machine Feedback',
308
+ 'interactive.feedback.instructions': 'Your feedback helps us make Vibe Coding Machine better! Please let us know what you think.',
309
+ 'interactive.feedback.email': 'Email (optional):',
310
+ 'interactive.feedback.comment': 'Comment:',
311
+ 'interactive.feedback.comment.instructions': 'Enter your comments below (press Enter twice on an empty line to finish):',
312
+ 'interactive.feedback.comment.required': 'Comment is required',
313
+ 'interactive.feedback.submitting': 'Submitting feedback',
314
+ 'interactive.feedback.success': 'Feedback submitted successfully!',
315
+ 'interactive.feedback.error': 'Failed to submit feedback',
316
+ 'interactive.feedback.error.network': 'Network error - please check your internet connection or try again later.',
317
+ 'interactive.feedback.cancelled': 'Feedback cancelled',
306
318
  'interactive.initialize': 'Initialize repository (.vibecodingmachine)',
307
319
  'interactive.goodbye': 'Goodbye! Be dreaming about what requirements to add!',
308
320
  'interactive.confirm.exit': 'Are you sure you want to exit? (x/y/N)',
@@ -323,7 +335,7 @@ module.exports = {
323
335
  'provider.available': 'Available',
324
336
  'provider.resets.in': 'Resets in',
325
337
  'provider.rate.limit.resets': 'Rate limit resets in',
326
- 'provider.instructions': '↑/↓ move selection j/k reorder e enable d disable Space toggle Enter save/select Esc cancel',
338
+ 'provider.instructions': '↑/↓ move selection j/k reorder e enable d disable i install Space toggle Enter save/select Esc cancel',
327
339
  'provider.status.available': 'Available',
328
340
  'provider.status.quota.infinite': 'Quota: Infinite',
329
341
  'provider.status.available.resets': 'available • resets in',
@@ -303,6 +303,18 @@ module.exports = {
303
303
  'interactive.sync.now': 'Sincronizar Ahora',
304
304
  'interactive.logout': 'Cerrar Sesión',
305
305
  'interactive.exit': 'Salir',
306
+ 'interactive.feedback': 'Comentarios',
307
+ 'interactive.feedback.title': 'Comentarios de Vibe Coding Machine',
308
+ 'interactive.feedback.instructions': '¡Tus comentarios nos ayudan a mejorar Vibe Coding Machine! Por favor, dinos qué piensas.',
309
+ 'interactive.feedback.email': 'Correo electrónico (opcional):',
310
+ 'interactive.feedback.comment': 'Comentario:',
311
+ 'interactive.feedback.comment.instructions': 'Ingresa tus comentarios a continuación (presiona Enter dos veces en una línea vacía para terminar):',
312
+ 'interactive.feedback.comment.required': 'El comentario es obligatorio',
313
+ 'interactive.feedback.submitting': 'Enviando comentarios',
314
+ 'interactive.feedback.success': '¡Comentarios enviados con éxito!',
315
+ 'interactive.feedback.error': 'No se pudieron enviar los comentarios',
316
+ 'interactive.feedback.error.network': 'Error de red: por favor, verifica tu conexión a internet o inténtalo de nuevo más tarde.',
317
+ 'interactive.feedback.cancelled': 'Comentarios cancelados',
306
318
  'interactive.initialize': 'Inicializar repositorio (.vibecodingmachine)',
307
319
  'interactive.goodbye': '¡Adiós! ¡Sigue soñando sobre qué requisitos agregar!',
308
320
  'interactive.confirm.exit': '¿Estás seguro de que quieres salir? (x/y/N)',
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Admin Utilities for VibeCodingMachine
3
+ */
4
+
5
+ const ADMIN_EMAILS = [
6
+ 'jesse.d.olsen@gmail.com'
7
+ ];
8
+
9
+ /**
10
+ * Check if an email belongs to an administrator
11
+ * @param {string} email - The email to check
12
+ * @returns {boolean} True if the email is an admin email
13
+ */
14
+ function isAdmin(email) {
15
+ if (!email) return false;
16
+ return ADMIN_EMAILS.includes(email.toLowerCase().trim());
17
+ }
18
+
19
+ /**
20
+ * Check if the application should automatically start implementation
21
+ * based on the user's email (admin feedback)
22
+ * @param {string} email - The email of the feedback sender
23
+ * @returns {boolean} True if auto-start should be triggered
24
+ */
25
+ function shouldAutoStart(email) {
26
+ return isAdmin(email);
27
+ }
28
+
29
+ module.exports = {
30
+ isAdmin,
31
+ shouldAutoStart,
32
+ ADMIN_EMAILS
33
+ };
@@ -5,19 +5,27 @@
5
5
  * This helps the admin know about issues and use VibeCodingMachine to fix itself.
6
6
  */
7
7
 
8
- const UserDatabase = require('../database/user-schema');
9
-
10
8
  class ErrorReporter {
11
9
  constructor() {
12
- this.db = new UserDatabase();
13
10
  this.enabled = true;
11
+ this.db = null;
12
+
13
+ try {
14
+ const UserDatabase = require('../database/user-schema');
15
+ this.db = new UserDatabase();
16
+ } catch (error) {
17
+ console.warn('ErrorReporter: Failed to initialize UserDatabase:', error.message);
18
+ this.enabled = false;
19
+ }
14
20
  }
15
21
 
16
22
  /**
17
23
  * Set authentication token for error reporting
18
24
  */
19
25
  setAuthToken(token) {
20
- this.db.setAuthToken(token);
26
+ if (this.db) {
27
+ this.db.setAuthToken(token);
28
+ }
21
29
  }
22
30
 
23
31
  /**
@@ -556,6 +556,8 @@ async function getProjectRequirementStats(repoPath) {
556
556
  let todoCount = 0;
557
557
  let toVerifyCount = 0;
558
558
  let verifiedCount = 0;
559
+ let clarificationCount = 0;
560
+ let recycledCount = 0;
559
561
 
560
562
  if (reqPath && await fs.pathExists(reqPath)) {
561
563
  const content = await fs.readFile(reqPath, 'utf8');
@@ -571,15 +573,31 @@ async function getProjectRequirementStats(repoPath) {
571
573
  'Verified by AI screenshot'
572
574
  ];
573
575
 
576
+ const clarificationVariants = [
577
+ 'NEEDING CLARIFICATION',
578
+ 'Requirements that need information',
579
+ 'need information'
580
+ ];
581
+
582
+ const recycledVariants = [
583
+ '♻️ Recycled',
584
+ 'Recycled'
585
+ ];
586
+
574
587
  for (const line of lines) {
575
588
  const trimmed = line.trim();
576
589
 
577
590
  if (trimmed.startsWith('###')) {
578
591
  if (currentSection) {
579
- const requirementText = trimmed.replace(/^###\s*/, '').trim();
580
- if (requirementText) {
592
+ // Remove ALL leading ### markers including spaces between them (handles "###", "### ###", "#### ####", etc.)
593
+ const requirementText = trimmed.replace(/^(#{1,}\s*)+/, '').trim();
594
+ // Filter out empty titles and package names
595
+ const packageNames = ['cli', 'core', 'electron-app', 'web', 'mobile', 'vscode-extension', 'sync-server'];
596
+ if (requirementText && requirementText.length > 0 && !packageNames.includes(requirementText.toLowerCase())) {
581
597
  if (currentSection === 'todo') todoCount++;
582
598
  else if (currentSection === 'toverify') toVerifyCount++;
599
+ else if (currentSection === 'clarification') clarificationCount++;
600
+ else if (currentSection === 'recycled') recycledCount++;
583
601
  }
584
602
  }
585
603
  } else if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
@@ -588,6 +606,10 @@ async function getProjectRequirementStats(repoPath) {
588
606
  currentSection = 'todo';
589
607
  } else if (verifySectionVariants.some(v => trimmed.includes(v))) {
590
608
  currentSection = 'toverify';
609
+ } else if (clarificationVariants.some(v => trimmed.includes(v))) {
610
+ currentSection = 'clarification';
611
+ } else if (recycledVariants.some(v => trimmed.includes(v))) {
612
+ currentSection = 'recycled';
591
613
  } else {
592
614
  currentSection = '';
593
615
  }
@@ -599,22 +621,30 @@ async function getProjectRequirementStats(repoPath) {
599
621
  const verifiedReqs = await loadVerifiedFromChangelog(repoPath);
600
622
  verifiedCount = verifiedReqs.length;
601
623
 
602
- const total = todoCount + toVerifyCount + verifiedCount;
624
+ const total = todoCount + toVerifyCount + verifiedCount + clarificationCount + recycledCount;
603
625
  const todoPercent = total > 0 ? Math.round((todoCount / total) * 100) : 0;
604
626
  const toVerifyPercent = total > 0 ? Math.round((toVerifyCount / total) * 100) : 0;
605
627
  const verifiedPercent = total > 0 ? Math.round((verifiedCount / total) * 100) : 0;
628
+ const clarificationPercent = total > 0 ? Math.round((clarificationCount / total) * 100) : 0;
629
+ const recycledPercent = total > 0 ? Math.round((recycledCount / total) * 100) : 0;
606
630
 
607
631
  return {
608
632
  todoCount,
609
633
  toVerifyCount,
610
634
  verifiedCount,
635
+ clarificationCount,
636
+ recycledCount,
611
637
  total,
612
638
  todoPercent,
613
639
  toVerifyPercent,
614
640
  verifiedPercent,
641
+ clarificationPercent,
642
+ recycledPercent,
615
643
  todoLabel: `TODO (${todoCount} - ${todoPercent}%)`,
616
644
  toVerifyLabel: `TO VERIFY (${toVerifyCount} - ${toVerifyPercent}%)`,
617
- verifiedLabel: `VERIFIED (${verifiedCount} - ${verifiedPercent}%)`
645
+ verifiedLabel: `VERIFIED (${verifiedCount} - ${verifiedPercent}%)`,
646
+ clarificationLabel: `CLARIFICATION (${clarificationCount} - ${clarificationPercent}%)`,
647
+ recycledLabel: `RECYCLED (${recycledCount} - ${recycledPercent}%)`
618
648
  };
619
649
  } catch (error) {
620
650
  logger.error('❌ Error getting project requirement stats:', error);
@@ -113,7 +113,7 @@ function parseRequirementsFile(content) {
113
113
 
114
114
  const details = [];
115
115
  const questions = [];
116
- let package = null;
116
+ let pkg = null;
117
117
  let options = null;
118
118
  let optionsType = null;
119
119
  let currentQuestion = null;
@@ -129,7 +129,7 @@ function parseRequirementsFile(content) {
129
129
 
130
130
  // Check for PACKAGE line
131
131
  if (nextLine.startsWith('PACKAGE:')) {
132
- package = nextLine.replace(/^PACKAGE:\s*/, '').trim();
132
+ pkg = nextLine.replace(/^PACKAGE:\s*/, '').trim();
133
133
  }
134
134
  // Check for OPTIONS line (for need information requirements)
135
135
  else if (nextLine.startsWith('OPTIONS:')) {
@@ -173,7 +173,7 @@ function parseRequirementsFile(content) {
173
173
  const requirement = {
174
174
  title,
175
175
  description: details.join('\n'),
176
- package,
176
+ package: pkg,
177
177
  options,
178
178
  optionsType,
179
179
  questions: currentSection === 'needInformation' ? questions : undefined,
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Unit Tests for HealthReporter
3
+ *
4
+ * Tests for formatting health data for CLI and Electron UI display.
5
+ */
6
+
7
+ const { HealthReporter } = require('../../../src/health-tracking/health-reporter');
8
+
9
+ describe('HealthReporter', () => {
10
+ let reporter;
11
+ let mockMetrics;
12
+
13
+ beforeEach(() => {
14
+ reporter = new HealthReporter();
15
+
16
+ mockMetrics = {
17
+ cursor: {
18
+ successCount: 42,
19
+ failureCount: 3,
20
+ successRate: 0.933,
21
+ averageResponseTime: 121500,
22
+ currentTimeout: 170100,
23
+ consecutiveFailures: 0,
24
+ lastSuccess: '2026-01-21T09:15:00.000Z',
25
+ lastFailure: '2026-01-20T23:45:00.000Z',
26
+ totalInteractions: 45,
27
+ recentInteractions: [
28
+ {
29
+ timestamp: '2026-01-21T09:15:00.000Z',
30
+ outcome: 'success',
31
+ responseTime: 120000,
32
+ timeoutUsed: 180000,
33
+ continuationPromptsDetected: 1,
34
+ requirementId: 'req-042',
35
+ errorMessage: null
36
+ }
37
+ ]
38
+ },
39
+ windsurf: {
40
+ successCount: 28,
41
+ failureCount: 15,
42
+ successRate: 0.651,
43
+ averageResponseTime: 1490000,
44
+ currentTimeout: 2086000,
45
+ consecutiveFailures: 2,
46
+ lastSuccess: '2026-01-21T08:30:00.000Z',
47
+ lastFailure: '2026-01-21T07:15:00.000Z',
48
+ totalInteractions: 43,
49
+ recentInteractions: [
50
+ {
51
+ timestamp: '2026-01-21T08:30:00.000Z',
52
+ outcome: 'failure',
53
+ responseTime: null,
54
+ timeoutUsed: 1800000,
55
+ continuationPromptsDetected: 0,
56
+ requirementId: 'req-041',
57
+ errorMessage: 'Timeout exceeded'
58
+ }
59
+ ]
60
+ }
61
+ };
62
+ });
63
+
64
+ describe('formatForCLI', () => {
65
+ it('should format health metrics for CLI display', () => {
66
+ const formatted = reporter.formatForCLI(mockMetrics);
67
+
68
+ expect(formatted).toContain('🖥️ Cursor');
69
+ expect(formatted).toContain('+42 -3');
70
+ expect(formatted).toContain('93.3%');
71
+ expect(formatted).toContain('2.0m');
72
+ expect(formatted).toContain('✅ Healthy');
73
+
74
+ expect(formatted).toContain('🌊 Windsurf');
75
+ expect(formatted).toContain('+28 -15');
76
+ expect(formatted).toContain('65.1%');
77
+ expect(formatted).toContain('24.9m');
78
+ expect(formatted).toContain('⚠️ 2 consecutive failures');
79
+ });
80
+
81
+ it('should handle empty metrics', () => {
82
+ const formatted = reporter.formatForCLI({});
83
+
84
+ expect(formatted).toContain('No IDE health data available');
85
+ });
86
+
87
+ it('should show warning for IDEs with consecutive failures', () => {
88
+ const metricsWithFailures = {
89
+ cursor: {
90
+ ...mockMetrics.cursor,
91
+ consecutiveFailures: 3
92
+ }
93
+ };
94
+
95
+ const formatted = reporter.formatForCLI(metricsWithFailures);
96
+
97
+ expect(formatted).toContain('⚠️ 3 consecutive failures');
98
+ expect(formatted).not.toContain('✅ Healthy');
99
+ });
100
+
101
+ it('should format timestamps relative to now', () => {
102
+ const now = new Date('2026-01-21T10:00:00.000Z');
103
+ const formatted = reporter.formatForCLI(mockMetrics, now);
104
+
105
+ expect(formatted).toContain('Last success: 45m ago');
106
+ expect(formatted).toContain('Last failure: 10h ago');
107
+ });
108
+
109
+ it('should show "Never" for null timestamps', () => {
110
+ const metricsWithNulls = {
111
+ cursor: {
112
+ ...mockMetrics.cursor,
113
+ lastSuccess: null,
114
+ lastFailure: null
115
+ }
116
+ };
117
+
118
+ const formatted = reporter.formatForCLI(metricsWithNulls);
119
+
120
+ expect(formatted).toContain('Last success: Never');
121
+ expect(formatted).toContain('Last failure: Never');
122
+ });
123
+ });
124
+
125
+ describe('formatForElectron', () => {
126
+ it('should format health metrics for Electron UI', () => {
127
+ const formatted = reporter.formatForElectron(mockMetrics);
128
+
129
+ expect(formatted).toHaveProperty('cursor');
130
+ expect(formatted).toHaveProperty('windsurf');
131
+
132
+ const cursorData = formatted.cursor;
133
+ expect(cursorData.ide).toBe('Cursor');
134
+ expect(cursorData.successCount).toBe(42);
135
+ expect(cursorData.failureCount).toBe(3);
136
+ expect(cursorData.successRate).toBe(93.3);
137
+ expect(cursorData.averageResponseTime).toBe('2.0m');
138
+ expect(cursorData.currentTimeout).toBe('2.8m');
139
+ expect(cursorData.status).toBe('healthy');
140
+ expect(cursorData.lastSuccess).toBe('45m ago');
141
+ expect(cursorData.lastFailure).toBe('10h ago');
142
+ });
143
+
144
+ it('should include recent interactions in Electron format', () => {
145
+ const formatted = reporter.formatForElectron(mockMetrics);
146
+
147
+ const cursorData = formatted.cursor;
148
+ expect(cursorData.recentInteractions).toHaveLength(1);
149
+
150
+ const interaction = cursorData.recentInteractions[0];
151
+ expect(interaction.outcome).toBe('success');
152
+ expect(interaction.displayTime).toBe('2.0m');
153
+ expect(interaction.requirementId).toBe('req-042');
154
+ });
155
+
156
+ it('should handle IDEs with no interactions', () => {
157
+ const metricsWithNoInteractions = {
158
+ vscode: {
159
+ successCount: 0,
160
+ failureCount: 0,
161
+ successRate: 0,
162
+ averageResponseTime: 0,
163
+ currentTimeout: 1800000,
164
+ consecutiveFailures: 0,
165
+ lastSuccess: null,
166
+ lastFailure: null,
167
+ totalInteractions: 0,
168
+ recentInteractions: []
169
+ }
170
+ };
171
+
172
+ const formatted = reporter.formatForElectron(metricsWithNoInteractions);
173
+
174
+ const vscodeData = formatted.vscode;
175
+ expect(vscodeData.status).toBe('no-data');
176
+ expect(vscodeData.successRate).toBe(0);
177
+ expect(vscodeData.averageResponseTime).toBe('N/A');
178
+ });
179
+
180
+ it('should format response times in human-readable units', () => {
181
+ const formatted = reporter.formatForElectron(mockMetrics);
182
+
183
+ expect(formatted.cursor.averageResponseTime).toBe('2.0m'); // 121.5s
184
+ expect(formatted.windsurf.averageResponseTime).toBe('24.9m'); // 1490s
185
+ expect(formatted.cursor.currentTimeout).toBe('2.8m'); // 170.1s
186
+ expect(formatted.windsurf.currentTimeout).toBe('34.8m'); // 2086s
187
+ });
188
+ });
189
+
190
+ describe('generateSummary', () => {
191
+ it('should generate overall health summary', () => {
192
+ const summary = reporter.generateSummary(mockMetrics);
193
+
194
+ expect(summary.totalIDEs).toBe(2);
195
+ expect(summary.healthyIDEs).toBe(1);
196
+ expect(summary.problematicIDEs).toBe(1);
197
+ expect(summary.totalInteractions).toBe(88);
198
+ expect(summary.overallSuccessRate).toBe(79.5);
199
+ expect(summary.averageResponseTime).toBe(805750); // Weighted average
200
+ expect(summary.recommendedIDE).toBe('cursor');
201
+ });
202
+
203
+ it('should handle empty metrics in summary', () => {
204
+ const summary = reporter.generateSummary({});
205
+
206
+ expect(summary.totalIDEs).toBe(0);
207
+ expect(summary.healthyIDEs).toBe(0);
208
+ expect(summary.problematicIDEs).toBe(0);
209
+ expect(summary.totalInteractions).toBe(0);
210
+ expect(summary.overallSuccessRate).toBe(0);
211
+ expect(summary.averageResponseTime).toBe(0);
212
+ expect(summary.recommendedIDE).toBeNull();
213
+ });
214
+
215
+ it('should calculate weighted average response time', () => {
216
+ const summary = reporter.generateSummary(mockMetrics);
217
+
218
+ // Weighted average: (45 * 121500 + 43 * 1490000) / 88
219
+ const expected = (45 * 121500 + 43 * 1490000) / 88;
220
+ expect(summary.averageResponseTime).toBeCloseTo(expected, 0);
221
+ });
222
+
223
+ it('should identify recommended IDE based on success rate', () => {
224
+ const summary = reporter.generateSummary(mockMetrics);
225
+
226
+ expect(summary.recommendedIDE).toBe('cursor'); // 93.3% vs 65.1%
227
+ });
228
+
229
+ it('should return null for recommendation when no IDE has sufficient data', () => {
230
+ const metricsWithInsufficientData = {
231
+ cursor: {
232
+ successCount: 1,
233
+ failureCount: 0,
234
+ successRate: 1.0,
235
+ totalInteractions: 1
236
+ }
237
+ };
238
+
239
+ const summary = reporter.generateSummary(metricsWithInsufficientData);
240
+
241
+ expect(summary.recommendedIDE).toBeNull();
242
+ });
243
+ });
244
+
245
+ describe('formatResponseTime', () => {
246
+ it('should format milliseconds to human readable format', () => {
247
+ expect(reporter.formatResponseTime(500)).toBe('500ms');
248
+ expect(reporter.formatResponseTime(1500)).toBe('1.5s');
249
+ expect(reporter.formatResponseTime(60000)).toBe('1.0m');
250
+ expect(reporter.formatResponseTime(121500)).toBe('2.0m');
251
+ expect(reporter.formatResponseTime(3600000)).toBe('1.0h');
252
+ expect(reporter.formatResponseTime(7200000)).toBe('2.0h');
253
+ });
254
+
255
+ it('should handle zero and null values', () => {
256
+ expect(reporter.formatResponseTime(0)).toBe('0ms');
257
+ expect(reporter.formatResponseTime(null)).toBe('N/A');
258
+ expect(reporter.formatResponseTime(undefined)).toBe('N/A');
259
+ });
260
+ });
261
+
262
+ describe('formatTimestamp', () => {
263
+ it('should format timestamps relative to now', () => {
264
+ const now = new Date('2026-01-21T10:00:00.000Z');
265
+
266
+ expect(reporter.formatTimestamp('2026-01-21T09:15:00.000Z', now))
267
+ .toBe('45m ago');
268
+
269
+ expect(reporter.formatTimestamp('2026-01-21T08:00:00.000Z', now))
270
+ .toBe('2h ago');
271
+
272
+ expect(reporter.formatTimestamp('2026-01-20T10:00:00.000Z', now))
273
+ .toBe('24h ago');
274
+
275
+ expect(reporter.formatTimestamp('2026-01-19T10:00:00.000Z', now))
276
+ .toBe('2d ago');
277
+ });
278
+
279
+ it('should handle null timestamps', () => {
280
+ expect(reporter.formatTimestamp(null)).toBe('Never');
281
+ expect(reporter.formatTimestamp(undefined)).toBe('Never');
282
+ });
283
+
284
+ it('should use current time when no reference provided', () => {
285
+ const timestamp = new Date(Date.now() - 5 * 60 * 1000).toISOString(); // 5 minutes ago
286
+ const formatted = reporter.formatTimestamp(timestamp);
287
+
288
+ expect(formatted).toBe('5m ago');
289
+ });
290
+ });
291
+
292
+ describe('getHealthStatus', () => {
293
+ it('should return healthy for IDEs with no consecutive failures', () => {
294
+ const status = reporter.getHealthStatus({
295
+ consecutiveFailures: 0,
296
+ totalInteractions: 10
297
+ });
298
+
299
+ expect(status).toBe('healthy');
300
+ });
301
+
302
+ it('should return warning for IDEs with consecutive failures', () => {
303
+ const status = reporter.getHealthStatus({
304
+ consecutiveFailures: 3,
305
+ totalInteractions: 10
306
+ });
307
+
308
+ expect(status).toBe('warning');
309
+ });
310
+
311
+ it('should return critical for IDEs with many consecutive failures', () => {
312
+ const status = reporter.getHealthStatus({
313
+ consecutiveFailures: 6,
314
+ totalInteractions: 10
315
+ });
316
+
317
+ expect(status).toBe('critical');
318
+ });
319
+
320
+ it('should return no-data for IDEs with no interactions', () => {
321
+ const status = reporter.getHealthStatus({
322
+ consecutiveFailures: 0,
323
+ totalInteractions: 0
324
+ });
325
+
326
+ expect(status).toBe('no-data');
327
+ });
328
+ });
329
+ });