@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,477 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const { exportAnalytics } = require('../lib/learning/analytics-exporter');
4
+
5
+ describe('Analytics Exporter', () => {
6
+ const tempDir = path.join(__dirname, 'temp-exporter-test');
7
+
8
+ const sampleAnalyticsData = {
9
+ version: '1.0',
10
+ exportedAt: new Date().toISOString(),
11
+ overview: {
12
+ totalSessions: 10,
13
+ totalSkips: 50,
14
+ manualSkips: 30,
15
+ filteredSkips: 20,
16
+ totalAnswers: 100,
17
+ activePatterns: 5,
18
+ activeRules: 3,
19
+ learningAge: 45
20
+ },
21
+ skipTrends: [
22
+ {
23
+ week: '2025-W01',
24
+ weekNumber: 1,
25
+ manualSkips: 5,
26
+ filteredSkips: 3,
27
+ totalSkips: 8,
28
+ skipRate: 40
29
+ },
30
+ {
31
+ week: '2025-W02',
32
+ weekNumber: 2,
33
+ manualSkips: 4,
34
+ filteredSkips: 2,
35
+ totalSkips: 6,
36
+ skipRate: 35
37
+ }
38
+ ],
39
+ categoryPreferences: [
40
+ {
41
+ category: 'deployment',
42
+ skips: 20,
43
+ answers: 10,
44
+ total: 30,
45
+ skipRate: 66.7,
46
+ level: 'High Skip'
47
+ },
48
+ {
49
+ category: 'testing',
50
+ skips: 5,
51
+ answers: 20,
52
+ total: 25,
53
+ skipRate: 20,
54
+ level: 'Low Skip'
55
+ }
56
+ ],
57
+ patternDistribution: {
58
+ totalPatterns: 15,
59
+ distribution: {
60
+ high: 5,
61
+ medium: 6,
62
+ low: 3,
63
+ veryLow: 1
64
+ },
65
+ avgConfidence: 78,
66
+ atRiskCount: 2
67
+ },
68
+ decayStatus: {
69
+ healthy: 10,
70
+ warning: 3,
71
+ critical: 2,
72
+ recentlyRenewed: 4,
73
+ avgAge: 35
74
+ },
75
+ effectiveness: {
76
+ timeSavedMinutes: 120,
77
+ timeSavedHours: 2,
78
+ questionsFiltered: 60,
79
+ falsePositives: 3,
80
+ falsePositiveRate: 5,
81
+ patternAccuracy: 87.5,
82
+ totalRuleApplications: 45,
83
+ avgApplicationsPerRule: 15,
84
+ overallEffectiveness: 85
85
+ }
86
+ };
87
+
88
+ beforeEach(async () => {
89
+ await fs.ensureDir(tempDir);
90
+ });
91
+
92
+ afterEach(async () => {
93
+ await fs.remove(tempDir);
94
+ });
95
+
96
+ describe('JSON Export', () => {
97
+ test('should export analytics as JSON file', async () => {
98
+ const result = await exportAnalytics(sampleAnalyticsData, tempDir, 'json');
99
+
100
+ expect(result.format).toBe('json');
101
+ expect(result.file).toBeTruthy();
102
+ expect(result.size).toBeTruthy();
103
+
104
+ // Verify file exists
105
+ const fileExists = await fs.pathExists(result.file);
106
+ expect(fileExists).toBe(true);
107
+
108
+ // Verify file contains valid JSON
109
+ const content = await fs.readJSON(result.file);
110
+ expect(content.version).toBe('1.0');
111
+ expect(content.overview.totalSessions).toBe(10);
112
+ expect(content.skipTrends.length).toBe(2);
113
+ });
114
+
115
+ test('should create exports directory if it does not exist', async () => {
116
+ const result = await exportAnalytics(sampleAnalyticsData, tempDir, 'json');
117
+
118
+ const exportsPath = path.join(tempDir, '.adf', 'learning', 'exports');
119
+ const dirExists = await fs.pathExists(exportsPath);
120
+
121
+ expect(dirExists).toBe(true);
122
+ expect(result.file).toContain('exports');
123
+ });
124
+
125
+ test('should include timestamp in filename', async () => {
126
+ const result = await exportAnalytics(sampleAnalyticsData, tempDir, 'json');
127
+
128
+ const filename = path.basename(result.file);
129
+ expect(filename).toMatch(/^analytics-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.json$/);
130
+ });
131
+
132
+ test('should report file size in KB', async () => {
133
+ const result = await exportAnalytics(sampleAnalyticsData, tempDir, 'json');
134
+
135
+ expect(result.size).toMatch(/^\d+\.\d{2} KB$/);
136
+ });
137
+
138
+ test('should format JSON with proper indentation', async () => {
139
+ const result = await exportAnalytics(sampleAnalyticsData, tempDir, 'json');
140
+
141
+ const content = await fs.readFile(result.file, 'utf8');
142
+
143
+ // Should be pretty-printed with 2-space indentation
144
+ expect(content).toContain(' "version"');
145
+ expect(content).toContain(' "overview"');
146
+ });
147
+
148
+ test('should preserve all data fields', async () => {
149
+ const result = await exportAnalytics(sampleAnalyticsData, tempDir, 'json');
150
+ const content = await fs.readJSON(result.file);
151
+
152
+ expect(content).toHaveProperty('version');
153
+ expect(content).toHaveProperty('exportedAt');
154
+ expect(content).toHaveProperty('overview');
155
+ expect(content).toHaveProperty('skipTrends');
156
+ expect(content).toHaveProperty('categoryPreferences');
157
+ expect(content).toHaveProperty('patternDistribution');
158
+ expect(content).toHaveProperty('decayStatus');
159
+ expect(content).toHaveProperty('effectiveness');
160
+ });
161
+ });
162
+
163
+ describe('CSV Export', () => {
164
+ test('should export analytics as multiple CSV files', async () => {
165
+ const result = await exportAnalytics(sampleAnalyticsData, tempDir, 'csv');
166
+
167
+ expect(result.format).toBe('csv');
168
+ expect(result.files).toBeTruthy();
169
+ expect(result.files.length).toBe(5);
170
+
171
+ // Verify all files exist
172
+ for (const file of result.files) {
173
+ const fileExists = await fs.pathExists(file);
174
+ expect(fileExists).toBe(true);
175
+ }
176
+ });
177
+
178
+ test('should create overview CSV with correct headers', async () => {
179
+ const result = await exportAnalytics(sampleAnalyticsData, tempDir, 'csv');
180
+
181
+ const overviewFile = result.files.find(f => f.includes('overview'));
182
+ const content = await fs.readFile(overviewFile, 'utf8');
183
+
184
+ expect(content).toContain('Metric,Value,Unit');
185
+ expect(content).toContain('Total Sessions,10,sessions');
186
+ expect(content).toContain('Total Skips,50,questions');
187
+ expect(content).toContain('Manual Skips,30,questions');
188
+ expect(content).toContain('Filtered Skips,20,questions');
189
+ expect(content).toContain('Total Answers,100,questions');
190
+ expect(content).toContain('Active Patterns,5,patterns');
191
+ expect(content).toContain('Active Rules,3,rules');
192
+ expect(content).toContain('Learning Age,45,days');
193
+ });
194
+
195
+ test('should create skip trends CSV with correct data', async () => {
196
+ const result = await exportAnalytics(sampleAnalyticsData, tempDir, 'csv');
197
+
198
+ const trendsFile = result.files.find(f => f.includes('skip-trends'));
199
+ const content = await fs.readFile(trendsFile, 'utf8');
200
+
201
+ expect(content).toContain('Week,Week Number,Manual Skips,Filtered Skips,Total Skips,Skip Rate (%)');
202
+ expect(content).toContain('2025-W01,1,5,3,8,40');
203
+ expect(content).toContain('2025-W02,2,4,2,6,35');
204
+ });
205
+
206
+ test('should create categories CSV with correct data', async () => {
207
+ const result = await exportAnalytics(sampleAnalyticsData, tempDir, 'csv');
208
+
209
+ const categoriesFile = result.files.find(f => f.includes('categories'));
210
+ const content = await fs.readFile(categoriesFile, 'utf8');
211
+
212
+ expect(content).toContain('Category,Skips,Answers,Total,Skip Rate (%),Level');
213
+ expect(content).toContain('deployment,20,10,30,66.7,High Skip');
214
+ expect(content).toContain('testing,5,20,25,20,Low Skip');
215
+ });
216
+
217
+ test('should create patterns CSV with distribution and decay data', async () => {
218
+ const result = await exportAnalytics(sampleAnalyticsData, tempDir, 'csv');
219
+
220
+ const patternsFile = result.files.find(f => f.includes('patterns'));
221
+ const content = await fs.readFile(patternsFile, 'utf8');
222
+
223
+ expect(content).toContain('Metric,Value,Description');
224
+ expect(content).toContain('Total Patterns,15,All patterns');
225
+ expect(content).toContain('High Confidence (90%+),5,Very strong patterns');
226
+ expect(content).toContain('Medium Confidence (75-89%),6,Strong patterns');
227
+ expect(content).toContain('Low Confidence (50-74%),3,Weak patterns');
228
+ expect(content).toContain('Very Low Confidence (<50%),1,Very weak patterns');
229
+ expect(content).toContain('Average Confidence,78%,Mean confidence score');
230
+ expect(content).toContain('At Risk,2,Patterns at risk of removal');
231
+ expect(content).toContain('Decay Status');
232
+ expect(content).toContain('Healthy Patterns,10,Active and strong');
233
+ expect(content).toContain('Warning Patterns,3,Needs attention');
234
+ expect(content).toContain('Critical Patterns,2,May be removed soon');
235
+ expect(content).toContain('Recently Renewed,4,Last 30 days');
236
+ expect(content).toContain('Average Age,35 days,Mean pattern age');
237
+ });
238
+
239
+ test('should create effectiveness CSV with all metrics', async () => {
240
+ const result = await exportAnalytics(sampleAnalyticsData, tempDir, 'csv');
241
+
242
+ const effectivenessFile = result.files.find(f => f.includes('effectiveness'));
243
+ const content = await fs.readFile(effectivenessFile, 'utf8');
244
+
245
+ expect(content).toContain('Metric,Value,Unit');
246
+ expect(content).toContain('Time Saved,120,minutes');
247
+ expect(content).toContain('Time Saved (Hours),2,hours');
248
+ expect(content).toContain('Questions Filtered,60,questions');
249
+ expect(content).toContain('False Positives,3,questions');
250
+ expect(content).toContain('False Positive Rate,5,%');
251
+ expect(content).toContain('Pattern Accuracy,87.5,%');
252
+ expect(content).toContain('Total Rule Applications,45,applications');
253
+ expect(content).toContain('Avg Applications Per Rule,15,applications');
254
+ expect(content).toContain('Overall Effectiveness,85,%');
255
+ });
256
+
257
+ test('should include timestamp in CSV filenames', async () => {
258
+ const result = await exportAnalytics(sampleAnalyticsData, tempDir, 'csv');
259
+
260
+ for (const file of result.files) {
261
+ const filename = path.basename(file);
262
+ expect(filename).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}/);
263
+ }
264
+ });
265
+
266
+ test('should escape CSV values with special characters', async () => {
267
+ const dataWithSpecialChars = {
268
+ ...sampleAnalyticsData,
269
+ categoryPreferences: [
270
+ {
271
+ category: 'test,with,commas',
272
+ skips: 5,
273
+ answers: 10,
274
+ total: 15,
275
+ skipRate: 33.3,
276
+ level: 'Low Skip'
277
+ },
278
+ {
279
+ category: 'test"with"quotes',
280
+ skips: 3,
281
+ answers: 7,
282
+ total: 10,
283
+ skipRate: 30,
284
+ level: 'Low Skip'
285
+ }
286
+ ]
287
+ };
288
+
289
+ const result = await exportAnalytics(dataWithSpecialChars, tempDir, 'csv');
290
+
291
+ const categoriesFile = result.files.find(f => f.includes('categories'));
292
+ const content = await fs.readFile(categoriesFile, 'utf8');
293
+
294
+ expect(content).toContain('"test,with,commas"');
295
+ expect(content).toContain('"test""with""quotes"');
296
+ });
297
+ });
298
+
299
+ describe('Error Handling', () => {
300
+ test('should throw error for unsupported format', async () => {
301
+ await expect(
302
+ exportAnalytics(sampleAnalyticsData, tempDir, 'xml')
303
+ ).rejects.toThrow('Unsupported export format: xml');
304
+ });
305
+
306
+ test('should default to JSON format if not specified', async () => {
307
+ const result = await exportAnalytics(sampleAnalyticsData, tempDir);
308
+
309
+ expect(result.format).toBe('json');
310
+ expect(result.file).toBeTruthy();
311
+ });
312
+
313
+ test('should handle empty analytics data gracefully', async () => {
314
+ const emptyData = {
315
+ version: '1.0',
316
+ exportedAt: new Date().toISOString(),
317
+ overview: {},
318
+ skipTrends: [],
319
+ categoryPreferences: [],
320
+ patternDistribution: {},
321
+ decayStatus: {},
322
+ effectiveness: {}
323
+ };
324
+
325
+ const result = await exportAnalytics(emptyData, tempDir, 'json');
326
+
327
+ expect(result.format).toBe('json');
328
+ expect(result.file).toBeTruthy();
329
+
330
+ const content = await fs.readJSON(result.file);
331
+ expect(content.version).toBe('1.0');
332
+ });
333
+
334
+ test('should handle missing optional fields', async () => {
335
+ const minimalData = {
336
+ version: '1.0',
337
+ exportedAt: new Date().toISOString(),
338
+ overview: {
339
+ totalSessions: 0,
340
+ totalSkips: 0,
341
+ totalAnswers: 0
342
+ },
343
+ skipTrends: [],
344
+ categoryPreferences: [],
345
+ patternDistribution: { totalPatterns: 0 },
346
+ decayStatus: {},
347
+ effectiveness: {}
348
+ };
349
+
350
+ const result = await exportAnalytics(minimalData, tempDir, 'csv');
351
+
352
+ expect(result.format).toBe('csv');
353
+ expect(result.files.length).toBe(5);
354
+
355
+ // All files should be created even with minimal data
356
+ for (const file of result.files) {
357
+ const fileExists = await fs.pathExists(file);
358
+ expect(fileExists).toBe(true);
359
+ }
360
+ });
361
+ });
362
+
363
+ describe('File Management', () => {
364
+ test('should create exports in .adf/learning/exports directory', async () => {
365
+ const result = await exportAnalytics(sampleAnalyticsData, tempDir, 'json');
366
+
367
+ expect(result.file).toContain(path.join('.adf', 'learning', 'exports'));
368
+ });
369
+
370
+ test('should allow multiple exports without overwriting', async () => {
371
+ const result1 = await exportAnalytics(sampleAnalyticsData, tempDir, 'json');
372
+
373
+ // Wait 1 second to ensure different timestamp (timestamp is truncated to seconds)
374
+ await new Promise(resolve => setTimeout(resolve, 1100));
375
+
376
+ const result2 = await exportAnalytics(sampleAnalyticsData, tempDir, 'json');
377
+
378
+ expect(result1.file).not.toBe(result2.file);
379
+
380
+ const file1Exists = await fs.pathExists(result1.file);
381
+ const file2Exists = await fs.pathExists(result2.file);
382
+
383
+ expect(file1Exists).toBe(true);
384
+ expect(file2Exists).toBe(true);
385
+ });
386
+
387
+ test('should create valid paths on Windows', async () => {
388
+ // This test ensures path.join is used correctly for cross-platform compatibility
389
+ const result = await exportAnalytics(sampleAnalyticsData, tempDir, 'json');
390
+
391
+ expect(result.file).not.toContain('//');
392
+ expect(result.file).not.toContain('\\\\');
393
+
394
+ const fileExists = await fs.pathExists(result.file);
395
+ expect(fileExists).toBe(true);
396
+ });
397
+ });
398
+
399
+ describe('Data Integrity', () => {
400
+ test('should preserve numeric precision in JSON export', async () => {
401
+ const preciseData = {
402
+ ...sampleAnalyticsData,
403
+ categoryPreferences: [
404
+ {
405
+ category: 'test',
406
+ skips: 1,
407
+ answers: 3,
408
+ total: 4,
409
+ skipRate: 33.333333333,
410
+ level: 'Low Skip'
411
+ }
412
+ ]
413
+ };
414
+
415
+ const result = await exportAnalytics(preciseData, tempDir, 'json');
416
+ const content = await fs.readJSON(result.file);
417
+
418
+ expect(content.categoryPreferences[0].skipRate).toBe(33.333333333);
419
+ });
420
+
421
+ test('should preserve unicode characters in CSV export', async () => {
422
+ const unicodeData = {
423
+ ...sampleAnalyticsData,
424
+ categoryPreferences: [
425
+ {
426
+ category: '测试分类',
427
+ skips: 5,
428
+ answers: 10,
429
+ total: 15,
430
+ skipRate: 33.3,
431
+ level: 'Low Skip'
432
+ }
433
+ ]
434
+ };
435
+
436
+ const result = await exportAnalytics(unicodeData, tempDir, 'csv');
437
+
438
+ const categoriesFile = result.files.find(f => f.includes('categories'));
439
+ const content = await fs.readFile(categoriesFile, 'utf8');
440
+
441
+ expect(content).toContain('测试分类');
442
+ });
443
+
444
+ test('should handle very large datasets efficiently', async () => {
445
+ const largeData = {
446
+ ...sampleAnalyticsData,
447
+ skipTrends: Array.from({ length: 1000 }, (_, i) => ({
448
+ week: `2025-W${i + 1}`,
449
+ weekNumber: i + 1,
450
+ manualSkips: Math.floor(Math.random() * 10),
451
+ filteredSkips: Math.floor(Math.random() * 10),
452
+ totalSkips: Math.floor(Math.random() * 20),
453
+ skipRate: Math.random() * 100
454
+ })),
455
+ categoryPreferences: Array.from({ length: 100 }, (_, i) => ({
456
+ category: `category-${i}`,
457
+ skips: Math.floor(Math.random() * 50),
458
+ answers: Math.floor(Math.random() * 50),
459
+ total: Math.floor(Math.random() * 100),
460
+ skipRate: Math.random() * 100,
461
+ level: 'Medium Skip'
462
+ }))
463
+ };
464
+
465
+ const startTime = Date.now();
466
+ const result = await exportAnalytics(largeData, tempDir, 'json');
467
+ const endTime = Date.now();
468
+
469
+ // Should complete in reasonable time (< 2 seconds)
470
+ expect(endTime - startTime).toBeLessThan(2000);
471
+ expect(result.file).toBeTruthy();
472
+
473
+ const fileExists = await fs.pathExists(result.file);
474
+ expect(fileExists).toBe(true);
475
+ });
476
+ });
477
+ });