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
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
|
-
|
|
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
|
|
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
|
-
|
|
580
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
});
|