@iservu-inc/adf-cli 0.9.1 → 0.11.0

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.
@@ -0,0 +1,466 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const analytics = require('../lib/learning/analytics');
4
+ const storage = require('../lib/learning/storage');
5
+
6
+ // Note: analytics-view.js is primarily an interactive CLI component that
7
+ // requires user input via inquirer. These tests focus on integration testing
8
+ // by verifying the analytics-view can successfully load and display data
9
+ // without errors. Full UI testing would require mocking inquirer interactions.
10
+
11
+ describe('Analytics View Integration', () => {
12
+ const tempDir = path.join(__dirname, 'temp-analytics-view-test');
13
+
14
+ beforeEach(async () => {
15
+ await fs.ensureDir(tempDir);
16
+ });
17
+
18
+ afterEach(async () => {
19
+ await fs.remove(tempDir);
20
+ });
21
+
22
+ describe('Data Loading', () => {
23
+ test('should successfully generate analytics data for display', async () => {
24
+ // Set up minimal test data
25
+ const skipHistory = {
26
+ sessions: [
27
+ {
28
+ sessionId: 's1',
29
+ timestamp: new Date().toISOString(),
30
+ skips: [
31
+ { questionId: 'q1', text: 'Deploy?', category: 'deployment', reason: 'manual' }
32
+ ]
33
+ }
34
+ ]
35
+ };
36
+
37
+ const answerHistory = {
38
+ sessions: [
39
+ {
40
+ sessionId: 's1',
41
+ timestamp: new Date().toISOString(),
42
+ answers: [
43
+ { questionId: 'q2', text: 'Testing?', category: 'testing', answer: 'Jest' }
44
+ ]
45
+ }
46
+ ]
47
+ };
48
+
49
+ const patterns = {
50
+ patterns: [
51
+ {
52
+ id: 'p1',
53
+ questionId: 'q1',
54
+ confidence: 90,
55
+ timesApplied: 5,
56
+ createdAt: new Date().toISOString(),
57
+ lastSeen: new Date().toISOString()
58
+ }
59
+ ]
60
+ };
61
+
62
+ const rules = {
63
+ rules: [
64
+ { id: 'r1', enabled: true, timesApplied: 10 }
65
+ ]
66
+ };
67
+
68
+ await storage.writeLearningData(tempDir, 'skip-history.json', skipHistory);
69
+ await storage.writeLearningData(tempDir, 'answer-history.json', answerHistory);
70
+ await storage.writeLearningData(tempDir, 'patterns.json', patterns);
71
+ await storage.writeLearningData(tempDir, 'learned-rules.json', rules);
72
+
73
+ // Generate analytics data (this is what the view uses)
74
+ const analyticsData = await analytics.generateAnalytics(tempDir);
75
+
76
+ // Verify all required sections exist for view rendering
77
+ expect(analyticsData.overview).toBeTruthy();
78
+ expect(analyticsData.skipTrends).toBeTruthy();
79
+ expect(analyticsData.categoryPreferences).toBeTruthy();
80
+ expect(analyticsData.patternDistribution).toBeTruthy();
81
+ expect(analyticsData.decayStatus).toBeTruthy();
82
+ expect(analyticsData.effectiveness).toBeTruthy();
83
+ expect(analyticsData.sessionTimeline).toBeTruthy();
84
+ expect(analyticsData.impactfulPatterns).toBeTruthy();
85
+ expect(analyticsData.questionStats).toBeTruthy();
86
+ });
87
+
88
+ test('should handle empty learning data gracefully', async () => {
89
+ // No data files created, should return empty analytics
90
+ const analyticsData = await analytics.generateAnalytics(tempDir);
91
+
92
+ expect(analyticsData.overview.totalSessions).toBe(0);
93
+ expect(analyticsData.skipTrends).toEqual([]);
94
+ expect(analyticsData.categoryPreferences).toEqual([]);
95
+
96
+ // View should be able to handle this without errors
97
+ // (actual rendering would require mocking inquirer)
98
+ });
99
+
100
+ test('should provide complete data structure for all views', async () => {
101
+ // Set up comprehensive test data
102
+ const skipHistory = {
103
+ sessions: Array.from({ length: 10 }, (_, i) => ({
104
+ sessionId: `s${i}`,
105
+ timestamp: new Date(Date.now() - i * 7 * 24 * 60 * 60 * 1000).toISOString(),
106
+ skips: [
107
+ { questionId: `q${i}`, text: `Question ${i}`, category: 'deployment', reason: 'manual' }
108
+ ]
109
+ }))
110
+ };
111
+
112
+ const answerHistory = {
113
+ sessions: Array.from({ length: 10 }, (_, i) => ({
114
+ sessionId: `s${i}`,
115
+ timestamp: new Date(Date.now() - i * 7 * 24 * 60 * 60 * 1000).toISOString(),
116
+ answers: [
117
+ { questionId: `qa${i}`, text: `Answer ${i}`, category: 'testing', answer: `Answer ${i}`, wordCount: 10 }
118
+ ]
119
+ }))
120
+ };
121
+
122
+ const patterns = {
123
+ patterns: Array.from({ length: 5 }, (_, i) => ({
124
+ id: `p${i}`,
125
+ questionId: `q${i}`,
126
+ confidence: 70 + (i * 5),
127
+ timesApplied: 5 + i,
128
+ createdAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
129
+ lastSeen: new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString()
130
+ }))
131
+ };
132
+
133
+ await storage.writeLearningData(tempDir, 'skip-history.json', skipHistory);
134
+ await storage.writeLearningData(tempDir, 'answer-history.json', answerHistory);
135
+ await storage.writeLearningData(tempDir, 'patterns.json', patterns);
136
+
137
+ const analyticsData = await analytics.generateAnalytics(tempDir);
138
+
139
+ // Verify main dashboard data
140
+ expect(analyticsData.overview.totalSessions).toBeGreaterThan(0);
141
+ expect(analyticsData.overview.learningAge).toBeGreaterThanOrEqual(0);
142
+
143
+ // Verify skip trends data (for trends view)
144
+ expect(analyticsData.skipTrends.length).toBeGreaterThan(0);
145
+ expect(analyticsData.skipTrends[0]).toHaveProperty('week');
146
+ expect(analyticsData.skipTrends[0]).toHaveProperty('totalSkips');
147
+ expect(analyticsData.skipTrends[0]).toHaveProperty('skipRate');
148
+
149
+ // Verify category preferences data (for category view)
150
+ expect(analyticsData.categoryPreferences.length).toBeGreaterThan(0);
151
+ expect(analyticsData.categoryPreferences[0]).toHaveProperty('category');
152
+ expect(analyticsData.categoryPreferences[0]).toHaveProperty('skipRate');
153
+ expect(analyticsData.categoryPreferences[0]).toHaveProperty('level');
154
+
155
+ // Verify pattern distribution data (for pattern health view)
156
+ expect(analyticsData.patternDistribution).toHaveProperty('totalPatterns');
157
+ expect(analyticsData.patternDistribution).toHaveProperty('distribution');
158
+ expect(analyticsData.patternDistribution).toHaveProperty('avgConfidence');
159
+
160
+ // Verify decay status data (for pattern health view)
161
+ expect(analyticsData.decayStatus).toHaveProperty('healthy');
162
+ expect(analyticsData.decayStatus).toHaveProperty('warning');
163
+ expect(analyticsData.decayStatus).toHaveProperty('critical');
164
+
165
+ // Verify effectiveness data (for main dashboard)
166
+ expect(analyticsData.effectiveness).toHaveProperty('timeSavedMinutes');
167
+ expect(analyticsData.effectiveness).toHaveProperty('patternAccuracy');
168
+ expect(analyticsData.effectiveness).toHaveProperty('overallEffectiveness');
169
+
170
+ // Verify session timeline data (for timeline view)
171
+ expect(analyticsData.sessionTimeline).toHaveProperty('totalSessions');
172
+ expect(analyticsData.sessionTimeline).toHaveProperty('avgSessionsPerWeek');
173
+ expect(analyticsData.sessionTimeline.sessions).toBeTruthy();
174
+
175
+ // Verify impactful patterns data (for impact view)
176
+ expect(analyticsData.impactfulPatterns.byTimeSaved).toBeTruthy();
177
+ expect(analyticsData.impactfulPatterns.byApplications).toBeTruthy();
178
+
179
+ // Verify question stats data (for questions view)
180
+ expect(analyticsData.questionStats.mostAnswered).toBeTruthy();
181
+ expect(analyticsData.questionStats.mostSkipped).toBeTruthy();
182
+ expect(analyticsData.questionStats.totalUniqueQuestions).toBeGreaterThanOrEqual(0);
183
+ });
184
+ });
185
+
186
+ describe('Display Data Validation', () => {
187
+ test('should provide valid skip trend chart data', async () => {
188
+ const skipHistory = {
189
+ sessions: [
190
+ {
191
+ sessionId: 's1',
192
+ timestamp: new Date().toISOString(),
193
+ skips: [{ questionId: 'q1', reason: 'manual' }]
194
+ },
195
+ {
196
+ sessionId: 's2',
197
+ timestamp: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
198
+ skips: [{ questionId: 'q2', reason: 'filtered' }]
199
+ }
200
+ ]
201
+ };
202
+
203
+ await storage.writeLearningData(tempDir, 'skip-history.json', skipHistory);
204
+
205
+ const analyticsData = await analytics.generateAnalytics(tempDir);
206
+
207
+ // Validate skip trends can be rendered in ASCII chart
208
+ for (const trend of analyticsData.skipTrends) {
209
+ expect(trend.week).toMatch(/^\d{4}-W\d{2}$/);
210
+ expect(trend.totalSkips).toBeGreaterThanOrEqual(0);
211
+ expect(trend.skipRate).toBeGreaterThanOrEqual(0);
212
+ expect(trend.skipRate).toBeLessThanOrEqual(100);
213
+ }
214
+ });
215
+
216
+ test('should provide valid category distribution data', async () => {
217
+ const skipHistory = {
218
+ sessions: [
219
+ {
220
+ sessionId: 's1',
221
+ skips: [
222
+ { questionId: 'q1', category: 'deployment', reason: 'manual' },
223
+ { questionId: 'q2', category: 'testing', reason: 'manual' }
224
+ ]
225
+ }
226
+ ]
227
+ };
228
+
229
+ await storage.writeLearningData(tempDir, 'skip-history.json', skipHistory);
230
+
231
+ const analyticsData = await analytics.generateAnalytics(tempDir);
232
+
233
+ // Validate category preferences can be rendered in heatmap
234
+ for (const category of analyticsData.categoryPreferences) {
235
+ expect(category.category).toBeTruthy();
236
+ expect(category.total).toBeGreaterThan(0);
237
+ expect(category.skipRate).toBeGreaterThanOrEqual(0);
238
+ expect(category.skipRate).toBeLessThanOrEqual(100);
239
+ expect(['High Skip', 'Medium Skip', 'Low Skip']).toContain(category.level);
240
+ }
241
+ });
242
+
243
+ test('should provide valid pattern distribution percentages', async () => {
244
+ const patterns = {
245
+ patterns: [
246
+ { id: 'p1', confidence: 95, timesApplied: 10 },
247
+ { id: 'p2', confidence: 85, timesApplied: 5 },
248
+ { id: 'p3', confidence: 65, timesApplied: 2 },
249
+ { id: 'p4', confidence: 45, timesApplied: 1 }
250
+ ]
251
+ };
252
+
253
+ await storage.writeLearningData(tempDir, 'patterns.json', patterns);
254
+
255
+ const analyticsData = await analytics.generateAnalytics(tempDir);
256
+
257
+ const dist = analyticsData.patternDistribution.distribution;
258
+
259
+ // Validate distribution totals equal totalPatterns
260
+ const total = dist.high + dist.medium + dist.low + dist.veryLow;
261
+ expect(total).toBe(analyticsData.patternDistribution.totalPatterns);
262
+
263
+ // All counts should be non-negative
264
+ expect(dist.high).toBeGreaterThanOrEqual(0);
265
+ expect(dist.medium).toBeGreaterThanOrEqual(0);
266
+ expect(dist.low).toBeGreaterThanOrEqual(0);
267
+ expect(dist.veryLow).toBeGreaterThanOrEqual(0);
268
+ });
269
+
270
+ test('should provide valid effectiveness percentages', async () => {
271
+ const skipHistory = {
272
+ sessions: [
273
+ {
274
+ sessionId: 's1',
275
+ skips: [
276
+ { questionId: 'q1', reason: 'filtered' },
277
+ { questionId: 'q2', reason: 'manual' }
278
+ ]
279
+ }
280
+ ]
281
+ };
282
+
283
+ const patterns = {
284
+ patterns: [
285
+ { id: 'p1', confidence: 90, timesApplied: 10 }
286
+ ]
287
+ };
288
+
289
+ await storage.writeLearningData(tempDir, 'skip-history.json', skipHistory);
290
+ await storage.writeLearningData(tempDir, 'patterns.json', patterns);
291
+
292
+ const analyticsData = await analytics.generateAnalytics(tempDir);
293
+
294
+ const eff = analyticsData.effectiveness;
295
+
296
+ // All percentage fields should be 0-100
297
+ expect(eff.falsePositiveRate).toBeGreaterThanOrEqual(0);
298
+ expect(eff.falsePositiveRate).toBeLessThanOrEqual(100);
299
+ expect(eff.patternAccuracy).toBeGreaterThanOrEqual(0);
300
+ expect(eff.patternAccuracy).toBeLessThanOrEqual(100);
301
+ expect(eff.overallEffectiveness).toBeGreaterThanOrEqual(0);
302
+ expect(eff.overallEffectiveness).toBeLessThanOrEqual(100);
303
+
304
+ // Time saved should be positive
305
+ expect(eff.timeSavedMinutes).toBeGreaterThanOrEqual(0);
306
+ expect(eff.timeSavedHours).toBeGreaterThanOrEqual(0);
307
+ });
308
+ });
309
+
310
+ describe('View Requirements', () => {
311
+ test('should provide impactful patterns sorted by time saved', async () => {
312
+ const patterns = {
313
+ patterns: [
314
+ { id: 'p1', questionId: 'q1', confidence: 90, timesApplied: 20 },
315
+ { id: 'p2', questionId: 'q2', confidence: 85, timesApplied: 5 },
316
+ { id: 'p3', questionId: 'q3', confidence: 95, timesApplied: 15 }
317
+ ]
318
+ };
319
+
320
+ await storage.writeLearningData(tempDir, 'patterns.json', patterns);
321
+
322
+ const analyticsData = await analytics.generateAnalytics(tempDir);
323
+
324
+ // Top patterns by time saved should be sorted descending
325
+ const byTimeSaved = analyticsData.impactfulPatterns.byTimeSaved;
326
+ expect(byTimeSaved.length).toBeGreaterThan(0);
327
+
328
+ if (byTimeSaved.length > 1) {
329
+ for (let i = 0; i < byTimeSaved.length - 1; i++) {
330
+ expect(byTimeSaved[i].timeSaved).toBeGreaterThanOrEqual(byTimeSaved[i + 1].timeSaved);
331
+ }
332
+ }
333
+ });
334
+
335
+ test('should provide impactful patterns sorted by applications', async () => {
336
+ const patterns = {
337
+ patterns: [
338
+ { id: 'p1', questionId: 'q1', confidence: 90, timesApplied: 20 },
339
+ { id: 'p2', questionId: 'q2', confidence: 85, timesApplied: 5 },
340
+ { id: 'p3', questionId: 'q3', confidence: 95, timesApplied: 15 }
341
+ ]
342
+ };
343
+
344
+ await storage.writeLearningData(tempDir, 'patterns.json', patterns);
345
+
346
+ const analyticsData = await analytics.generateAnalytics(tempDir);
347
+
348
+ // Top patterns by applications should be sorted descending
349
+ const byApplications = analyticsData.impactfulPatterns.byApplications;
350
+ expect(byApplications.length).toBeGreaterThan(0);
351
+
352
+ if (byApplications.length > 1) {
353
+ for (let i = 0; i < byApplications.length - 1; i++) {
354
+ expect(byApplications[i].timesApplied).toBeGreaterThanOrEqual(byApplications[i + 1].timesApplied);
355
+ }
356
+ }
357
+ });
358
+
359
+ test('should provide question statistics with top answers and skips', async () => {
360
+ const skipHistory = {
361
+ sessions: [
362
+ {
363
+ sessionId: 's1',
364
+ skips: [
365
+ { questionId: 'q1', text: 'Deploy?', reason: 'manual' },
366
+ { questionId: 'q1', text: 'Deploy?', reason: 'manual' },
367
+ { questionId: 'q2', text: 'CI/CD?', reason: 'manual' }
368
+ ]
369
+ }
370
+ ]
371
+ };
372
+
373
+ const answerHistory = {
374
+ sessions: [
375
+ {
376
+ sessionId: 's1',
377
+ answers: [
378
+ { questionId: 'qa1', text: 'Testing?', answer: 'Jest' },
379
+ { questionId: 'qa1', text: 'Testing?', answer: 'Jest' },
380
+ { questionId: 'qa1', text: 'Testing?', answer: 'Jest' },
381
+ { questionId: 'qa2', text: 'Framework?', answer: 'React' }
382
+ ]
383
+ }
384
+ ]
385
+ };
386
+
387
+ await storage.writeLearningData(tempDir, 'skip-history.json', skipHistory);
388
+ await storage.writeLearningData(tempDir, 'answer-history.json', answerHistory);
389
+
390
+ const analyticsData = await analytics.generateAnalytics(tempDir);
391
+
392
+ // Most answered questions should be sorted by count
393
+ const mostAnswered = analyticsData.questionStats.mostAnswered;
394
+ expect(mostAnswered.length).toBeGreaterThan(0);
395
+ expect(mostAnswered[0].count).toBeGreaterThanOrEqual(mostAnswered[mostAnswered.length - 1]?.count || 0);
396
+
397
+ // Most skipped questions should be sorted by count
398
+ const mostSkipped = analyticsData.questionStats.mostSkipped;
399
+ expect(mostSkipped.length).toBeGreaterThan(0);
400
+ expect(mostSkipped[0].count).toBeGreaterThanOrEqual(mostSkipped[mostSkipped.length - 1]?.count || 0);
401
+
402
+ // Should have unique question count
403
+ expect(analyticsData.questionStats.totalUniqueQuestions).toBeGreaterThan(0);
404
+ });
405
+
406
+ test('should provide session timeline with frequency metrics', async () => {
407
+ const skipHistory = {
408
+ sessions: Array.from({ length: 10 }, (_, i) => ({
409
+ sessionId: `s${i}`,
410
+ timestamp: new Date(Date.now() - i * 7 * 24 * 60 * 60 * 1000).toISOString(),
411
+ skips: []
412
+ }))
413
+ };
414
+
415
+ await storage.writeLearningData(tempDir, 'skip-history.json', skipHistory);
416
+
417
+ const analyticsData = await analytics.generateAnalytics(tempDir);
418
+
419
+ const timeline = analyticsData.sessionTimeline;
420
+
421
+ expect(timeline.totalSessions).toBe(10);
422
+ expect(timeline.avgSessionsPerWeek).toBeGreaterThan(0);
423
+ expect(timeline.sessions.length).toBeGreaterThan(0);
424
+
425
+ // Sessions should be sorted by date (most recent first)
426
+ if (timeline.sessions.length > 1) {
427
+ const dates = timeline.sessions.map(s => new Date(s.date).getTime());
428
+ for (let i = 0; i < dates.length - 1; i++) {
429
+ expect(dates[i]).toBeGreaterThanOrEqual(dates[i + 1]);
430
+ }
431
+ }
432
+ });
433
+ });
434
+
435
+ describe('Export Integration', () => {
436
+ test('should provide data compatible with both JSON and CSV export', async () => {
437
+ const skipHistory = {
438
+ sessions: [
439
+ {
440
+ sessionId: 's1',
441
+ timestamp: new Date().toISOString(),
442
+ skips: [{ questionId: 'q1', category: 'deployment', reason: 'manual' }]
443
+ }
444
+ ]
445
+ };
446
+
447
+ await storage.writeLearningData(tempDir, 'skip-history.json', skipHistory);
448
+
449
+ const analyticsData = await analytics.generateAnalytics(tempDir);
450
+
451
+ // Data should be exportable (this validates data structure)
452
+ expect(analyticsData.version).toBe('1.0');
453
+ expect(analyticsData.exportedAt).toBeTruthy();
454
+ expect(analyticsData.overview).toBeTruthy();
455
+ expect(analyticsData.skipTrends).toBeTruthy();
456
+ expect(analyticsData.categoryPreferences).toBeTruthy();
457
+ expect(analyticsData.patternDistribution).toBeTruthy();
458
+ expect(analyticsData.decayStatus).toBeTruthy();
459
+ expect(analyticsData.effectiveness).toBeTruthy();
460
+
461
+ // All arrays should be valid for CSV export
462
+ expect(Array.isArray(analyticsData.skipTrends)).toBe(true);
463
+ expect(Array.isArray(analyticsData.categoryPreferences)).toBe(true);
464
+ });
465
+ });
466
+ });