vibecodingmachine-core 2025.12.25-25 → 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/ERROR_REPORTING_API.md +212 -0
- package/ERROR_REPORTING_USAGE.md +380 -0
- package/__tests__/provider-manager-fallback.test.js +43 -0
- package/__tests__/provider-manager-rate-limit.test.js +61 -0
- package/__tests__/utils/git-branch-manager.test.js +61 -0
- package/package.json +1 -1
- package/src/beta-request.js +160 -0
- package/src/compliance/compliance-manager.js +5 -2
- package/src/database/migrations.js +135 -12
- package/src/database/user-database-client.js +127 -8
- package/src/database/user-schema.js +28 -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 +1087 -9
- package/src/ide-integration/applescript-manager.js +565 -15
- package/src/ide-integration/applescript-utils.js +26 -18
- 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 +36 -1
- package/src/index.js +20 -0
- package/src/localization/translations/en.js +15 -1
- package/src/localization/translations/es.js +14 -0
- package/src/requirement-numbering.js +164 -0
- package/src/sync/aws-setup.js +4 -4
- package/src/utils/admin-utils.js +33 -0
- package/src/utils/error-reporter.js +117 -0
- package/src/utils/git-branch-manager.js +278 -0
- package/src/utils/requirement-helpers.js +44 -5
- package/src/utils/requirements-parser.js +28 -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
|
@@ -514,8 +514,17 @@ async function loadVerifiedFromChangelog(repoPath) {
|
|
|
514
514
|
for (const line of lines) {
|
|
515
515
|
const trimmed = line.trim();
|
|
516
516
|
|
|
517
|
+
if (trimmed.includes('## Verified Requirements')) {
|
|
518
|
+
inVerifiedSection = true;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (inVerifiedSection && trimmed.startsWith('##') && !trimmed.includes('Verified Requirements')) {
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
|
|
517
526
|
// Collect items starting with "- " as verified requirements
|
|
518
|
-
if (trimmed.startsWith('- ') && trimmed.length > 5) {
|
|
527
|
+
if (inVerifiedSection && trimmed.startsWith('- ') && trimmed.length > 5) {
|
|
519
528
|
let reqText = trimmed.substring(2);
|
|
520
529
|
// Extract title part (before timestamp in parentheses if present)
|
|
521
530
|
const titleMatch = reqText.match(/^(.+?)\s*\([\d-]+\)$/);
|
|
@@ -547,6 +556,8 @@ async function getProjectRequirementStats(repoPath) {
|
|
|
547
556
|
let todoCount = 0;
|
|
548
557
|
let toVerifyCount = 0;
|
|
549
558
|
let verifiedCount = 0;
|
|
559
|
+
let clarificationCount = 0;
|
|
560
|
+
let recycledCount = 0;
|
|
550
561
|
|
|
551
562
|
if (reqPath && await fs.pathExists(reqPath)) {
|
|
552
563
|
const content = await fs.readFile(reqPath, 'utf8');
|
|
@@ -562,15 +573,31 @@ async function getProjectRequirementStats(repoPath) {
|
|
|
562
573
|
'Verified by AI screenshot'
|
|
563
574
|
];
|
|
564
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
|
+
|
|
565
587
|
for (const line of lines) {
|
|
566
588
|
const trimmed = line.trim();
|
|
567
589
|
|
|
568
590
|
if (trimmed.startsWith('###')) {
|
|
569
591
|
if (currentSection) {
|
|
570
|
-
|
|
571
|
-
|
|
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())) {
|
|
572
597
|
if (currentSection === 'todo') todoCount++;
|
|
573
598
|
else if (currentSection === 'toverify') toVerifyCount++;
|
|
599
|
+
else if (currentSection === 'clarification') clarificationCount++;
|
|
600
|
+
else if (currentSection === 'recycled') recycledCount++;
|
|
574
601
|
}
|
|
575
602
|
}
|
|
576
603
|
} else if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
|
|
@@ -579,6 +606,10 @@ async function getProjectRequirementStats(repoPath) {
|
|
|
579
606
|
currentSection = 'todo';
|
|
580
607
|
} else if (verifySectionVariants.some(v => trimmed.includes(v))) {
|
|
581
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';
|
|
582
613
|
} else {
|
|
583
614
|
currentSection = '';
|
|
584
615
|
}
|
|
@@ -590,22 +621,30 @@ async function getProjectRequirementStats(repoPath) {
|
|
|
590
621
|
const verifiedReqs = await loadVerifiedFromChangelog(repoPath);
|
|
591
622
|
verifiedCount = verifiedReqs.length;
|
|
592
623
|
|
|
593
|
-
const total = todoCount + toVerifyCount + verifiedCount;
|
|
624
|
+
const total = todoCount + toVerifyCount + verifiedCount + clarificationCount + recycledCount;
|
|
594
625
|
const todoPercent = total > 0 ? Math.round((todoCount / total) * 100) : 0;
|
|
595
626
|
const toVerifyPercent = total > 0 ? Math.round((toVerifyCount / total) * 100) : 0;
|
|
596
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;
|
|
597
630
|
|
|
598
631
|
return {
|
|
599
632
|
todoCount,
|
|
600
633
|
toVerifyCount,
|
|
601
634
|
verifiedCount,
|
|
635
|
+
clarificationCount,
|
|
636
|
+
recycledCount,
|
|
602
637
|
total,
|
|
603
638
|
todoPercent,
|
|
604
639
|
toVerifyPercent,
|
|
605
640
|
verifiedPercent,
|
|
641
|
+
clarificationPercent,
|
|
642
|
+
recycledPercent,
|
|
606
643
|
todoLabel: `TODO (${todoCount} - ${todoPercent}%)`,
|
|
607
644
|
toVerifyLabel: `TO VERIFY (${toVerifyCount} - ${toVerifyPercent}%)`,
|
|
608
|
-
verifiedLabel: `VERIFIED (${verifiedCount} - ${verifiedPercent}%)
|
|
645
|
+
verifiedLabel: `VERIFIED (${verifiedCount} - ${verifiedPercent}%)`,
|
|
646
|
+
clarificationLabel: `CLARIFICATION (${clarificationCount} - ${clarificationPercent}%)`,
|
|
647
|
+
recycledLabel: `RECYCLED (${recycledCount} - ${recycledPercent}%)`
|
|
609
648
|
};
|
|
610
649
|
} catch (error) {
|
|
611
650
|
logger.error('❌ Error getting project requirement stats:', error);
|
|
@@ -112,9 +112,11 @@ function parseRequirementsFile(content) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
const details = [];
|
|
115
|
-
|
|
115
|
+
const questions = [];
|
|
116
|
+
let pkg = null;
|
|
116
117
|
let options = null;
|
|
117
118
|
let optionsType = null;
|
|
119
|
+
let currentQuestion = null;
|
|
118
120
|
|
|
119
121
|
// Read package, description, and options
|
|
120
122
|
for (let j = i + 1; j < lines.length; j++) {
|
|
@@ -127,7 +129,7 @@ function parseRequirementsFile(content) {
|
|
|
127
129
|
|
|
128
130
|
// Check for PACKAGE line
|
|
129
131
|
if (nextLine.startsWith('PACKAGE:')) {
|
|
130
|
-
|
|
132
|
+
pkg = nextLine.replace(/^PACKAGE:\s*/, '').trim();
|
|
131
133
|
}
|
|
132
134
|
// Check for OPTIONS line (for need information requirements)
|
|
133
135
|
else if (nextLine.startsWith('OPTIONS:')) {
|
|
@@ -140,18 +142,41 @@ function parseRequirementsFile(content) {
|
|
|
140
142
|
options = [optionsText];
|
|
141
143
|
}
|
|
142
144
|
}
|
|
145
|
+
// Check for QUESTIONS line (pipe-separated)
|
|
146
|
+
else if (nextLine.startsWith('QUESTIONS:')) {
|
|
147
|
+
const questionsText = nextLine.replace(/^QUESTIONS:\s*/, '').trim();
|
|
148
|
+
const parsedQuestions = questionsText.split('|').map(q => q.trim()).filter(Boolean);
|
|
149
|
+
for (const q of parsedQuestions) {
|
|
150
|
+
questions.push({ question: q, response: null });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Parse numbered questions/responses in needInformation section (CLI-style)
|
|
154
|
+
else if (currentSection === 'needInformation' && nextLine.match(/^\d+\./)) {
|
|
155
|
+
if (currentQuestion) {
|
|
156
|
+
questions.push(currentQuestion);
|
|
157
|
+
}
|
|
158
|
+
currentQuestion = { question: nextLine, response: null };
|
|
159
|
+
}
|
|
160
|
+
else if (currentSection === 'needInformation' && currentQuestion && nextLine.startsWith('Response:')) {
|
|
161
|
+
currentQuestion.response = nextLine.replace(/^Response:\s*/, '').trim();
|
|
162
|
+
}
|
|
143
163
|
// Description line (include all lines including empty ones to preserve formatting)
|
|
144
164
|
else if (nextLine !== undefined) {
|
|
145
165
|
details.push(nextLine);
|
|
146
166
|
}
|
|
147
167
|
}
|
|
148
168
|
|
|
169
|
+
if (currentQuestion) {
|
|
170
|
+
questions.push(currentQuestion);
|
|
171
|
+
}
|
|
172
|
+
|
|
149
173
|
const requirement = {
|
|
150
174
|
title,
|
|
151
175
|
description: details.join('\n'),
|
|
152
|
-
package,
|
|
176
|
+
package: pkg,
|
|
153
177
|
options,
|
|
154
178
|
optionsType,
|
|
179
|
+
questions: currentSection === 'needInformation' ? questions : undefined,
|
|
155
180
|
lineIndex: i
|
|
156
181
|
};
|
|
157
182
|
|
|
@@ -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
|
+
});
|