@iservu-inc/adf-cli 0.17.0 → 0.17.5
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/.context/memory/architecture.md +1 -1
- package/.context/memory/glossary.md +1 -1
- package/CLAUDE.md +5 -5
- package/README.md +1 -1
- package/lib/analysis/heuristic-gap-analyzer.js +4 -4
- package/lib/analysis/synthesis-engine.js +5 -5
- package/lib/commands/init.js +545 -467
- package/lib/frameworks/output-generators.js +54 -147
- package/lib/frameworks/progress-tracker.js +16 -0
- package/lib/frameworks/questions.js +156 -464
- package/lib/frameworks/session-manager.js +56 -0
- package/lib/learning/analytics-view.js +5 -5
- package/lib/learning/analytics.js +22 -6
- package/lib/templates/scripts/analyze-docs.js +23 -12
- package/lib/templates/scripts/build.js +1 -1
- package/lib/templates/scripts/check-framework-updates.js +1 -1
- package/lib/templates/scripts/init.js +1 -1
- package/lib/templates/shared/memory/constitution.md +2 -2
- package/lib/templates/shared/templates/README.md +2 -2
- package/lib/utils/context-extractor.js +51 -3
- package/lib/utils/framework-detector.js +11 -2
- package/package.json +1 -1
- package/tests/analytics-view.test.js +12 -10
- package/tests/context-extractor.test.js +47 -2
- package/tests/decay-manager.test.js +22 -19
- package/tests/deploy.test.js +7 -3
- package/tests/dynamic-question-generator.test.js +2 -2
- package/tests/framework-detector.test.js +31 -3
- package/tests/heuristic-gap-analyzer.test.js +5 -5
- package/tests/pattern-decay.test.js +34 -52
- package/tests/session-manager.test.js +125 -0
|
@@ -20,6 +20,9 @@ describe('DecayManager Integration Tests', () => {
|
|
|
20
20
|
await fs.remove(testProjectPath);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
// Helper: get a date N days ago
|
|
24
|
+
const daysAgo = (n) => new Date(Date.now() - n * 24 * 60 * 60 * 1000).toISOString();
|
|
25
|
+
|
|
23
26
|
const createTestPattern = (overrides = {}) => ({
|
|
24
27
|
id: `pattern_${Date.now()}`,
|
|
25
28
|
type: 'consistent_skip',
|
|
@@ -30,19 +33,19 @@ describe('DecayManager Integration Tests', () => {
|
|
|
30
33
|
skipCount: 9,
|
|
31
34
|
status: 'active',
|
|
32
35
|
userApproved: false,
|
|
33
|
-
createdAt:
|
|
34
|
-
lastSeen:
|
|
35
|
-
lastDecayCalculation:
|
|
36
|
+
createdAt: daysAgo(60),
|
|
37
|
+
lastSeen: daysAgo(10),
|
|
38
|
+
lastDecayCalculation: daysAgo(10),
|
|
36
39
|
timesRenewed: 0,
|
|
37
40
|
...overrides
|
|
38
41
|
});
|
|
39
42
|
|
|
40
43
|
describe('loadPatternsWithDecay', () => {
|
|
41
44
|
test('loads patterns and applies decay', async () => {
|
|
42
|
-
// Create patterns with
|
|
45
|
+
// Create patterns with lastSeen dates ~2 months ago (enough for measurable decay)
|
|
43
46
|
const patterns = [
|
|
44
|
-
createTestPattern({ id: 'p1', confidence: 90, lastSeen:
|
|
45
|
-
createTestPattern({ id: 'p2', confidence: 80, lastSeen:
|
|
47
|
+
createTestPattern({ id: 'p1', confidence: 90, lastSeen: daysAgo(60) }),
|
|
48
|
+
createTestPattern({ id: 'p2', confidence: 80, lastSeen: daysAgo(60) })
|
|
46
49
|
];
|
|
47
50
|
|
|
48
51
|
await storage.savePatterns(testProjectPath, {
|
|
@@ -58,17 +61,17 @@ describe('DecayManager Integration Tests', () => {
|
|
|
58
61
|
});
|
|
59
62
|
|
|
60
63
|
test('removes stale patterns automatically', async () => {
|
|
61
|
-
// Create patterns - one stale, one active
|
|
64
|
+
// Create patterns - one stale (below threshold), one active
|
|
62
65
|
const patterns = [
|
|
63
66
|
createTestPattern({
|
|
64
67
|
id: 'stale',
|
|
65
68
|
confidence: 35, // Below removeBelow threshold (40)
|
|
66
|
-
lastSeen:
|
|
69
|
+
lastSeen: daysAgo(10)
|
|
67
70
|
}),
|
|
68
71
|
createTestPattern({
|
|
69
72
|
id: 'active',
|
|
70
73
|
confidence: 80,
|
|
71
|
-
lastSeen:
|
|
74
|
+
lastSeen: daysAgo(10)
|
|
72
75
|
})
|
|
73
76
|
];
|
|
74
77
|
|
|
@@ -91,7 +94,7 @@ describe('DecayManager Integration Tests', () => {
|
|
|
91
94
|
await storage.saveLearningConfig(testProjectPath, config);
|
|
92
95
|
|
|
93
96
|
const patterns = [
|
|
94
|
-
createTestPattern({ confidence: 90, lastSeen:
|
|
97
|
+
createTestPattern({ confidence: 90, lastSeen: daysAgo(60) })
|
|
95
98
|
];
|
|
96
99
|
|
|
97
100
|
await storage.savePatterns(testProjectPath, {
|
|
@@ -269,12 +272,12 @@ describe('DecayManager Integration Tests', () => {
|
|
|
269
272
|
createTestPattern({
|
|
270
273
|
id: 'ancient',
|
|
271
274
|
confidence: 80,
|
|
272
|
-
lastSeen:
|
|
275
|
+
lastSeen: daysAgo(300) // ~10 months ago
|
|
273
276
|
}),
|
|
274
277
|
createTestPattern({
|
|
275
278
|
id: 'recent',
|
|
276
279
|
confidence: 80,
|
|
277
|
-
lastSeen:
|
|
280
|
+
lastSeen: daysAgo(10)
|
|
278
281
|
})
|
|
279
282
|
];
|
|
280
283
|
|
|
@@ -289,8 +292,8 @@ describe('DecayManager Integration Tests', () => {
|
|
|
289
292
|
|
|
290
293
|
test('keeps all patterns if none meet removal criteria', () => {
|
|
291
294
|
const patterns = [
|
|
292
|
-
createTestPattern({ confidence: 80, lastSeen:
|
|
293
|
-
createTestPattern({ confidence: 70, lastSeen:
|
|
295
|
+
createTestPattern({ confidence: 80, lastSeen: daysAgo(10) }),
|
|
296
|
+
createTestPattern({ confidence: 70, lastSeen: daysAgo(10) })
|
|
294
297
|
];
|
|
295
298
|
|
|
296
299
|
const { activePatterns, removedPatterns } = decayManager.cleanupStalePatterns(patterns);
|
|
@@ -303,8 +306,8 @@ describe('DecayManager Integration Tests', () => {
|
|
|
303
306
|
describe('triggerDecayCalculation', () => {
|
|
304
307
|
test('manually triggers decay and returns stats', async () => {
|
|
305
308
|
const patterns = [
|
|
306
|
-
createTestPattern({ confidence: 90, lastSeen:
|
|
307
|
-
createTestPattern({ confidence: 35, lastSeen:
|
|
309
|
+
createTestPattern({ confidence: 90, lastSeen: daysAgo(60) }),
|
|
310
|
+
createTestPattern({ confidence: 35, lastSeen: daysAgo(60) }) // Will be removed (below threshold)
|
|
308
311
|
];
|
|
309
312
|
|
|
310
313
|
await storage.savePatterns(testProjectPath, {
|
|
@@ -337,9 +340,9 @@ describe('DecayManager Integration Tests', () => {
|
|
|
337
340
|
describe('getDecayStats', () => {
|
|
338
341
|
test('calculates decay statistics', async () => {
|
|
339
342
|
const patterns = [
|
|
340
|
-
createTestPattern({ confidence: 85, createdAt:
|
|
341
|
-
createTestPattern({ confidence: 70, createdAt:
|
|
342
|
-
createTestPattern({ confidence: 55, createdAt:
|
|
343
|
+
createTestPattern({ confidence: 85, createdAt: daysAgo(30) }), // High
|
|
344
|
+
createTestPattern({ confidence: 70, createdAt: daysAgo(30) }), // Medium
|
|
345
|
+
createTestPattern({ confidence: 55, createdAt: daysAgo(30) }) // Low
|
|
343
346
|
];
|
|
344
347
|
|
|
345
348
|
await storage.savePatterns(testProjectPath, {
|
package/tests/deploy.test.js
CHANGED
|
@@ -11,9 +11,13 @@ jest.mock('../lib/generators', () => ({
|
|
|
11
11
|
generateVSCode: jest.fn(),
|
|
12
12
|
generateZed: jest.fn(),
|
|
13
13
|
generateAntigravity: jest.fn(),
|
|
14
|
-
generateOpenCode: jest.fn().mockResolvedValue({ config: 'opencode.json' }),
|
|
14
|
+
generateOpenCode: jest.fn().mockResolvedValue({ config: '.opencode.json' }),
|
|
15
15
|
generateGeminiCLI: jest.fn(),
|
|
16
|
-
generateDeepAgent: jest.fn()
|
|
16
|
+
generateDeepAgent: jest.fn(),
|
|
17
|
+
generateA2A: jest.fn(),
|
|
18
|
+
generateKiro: jest.fn(),
|
|
19
|
+
generateTrae: jest.fn(),
|
|
20
|
+
generateCodexCLI: jest.fn()
|
|
17
21
|
}));
|
|
18
22
|
|
|
19
23
|
const TEST_DIR = path.join(__dirname, 'test-deploy-opencode');
|
|
@@ -51,7 +55,7 @@ describe('Deploy Command - OpenCode', () => {
|
|
|
51
55
|
// However, deploy.js writes a fallback/wrapper config at the end.
|
|
52
56
|
|
|
53
57
|
// The key check here is that it didn't crash and tried to write to opencode.json
|
|
54
|
-
const configPath = path.join(TEST_DIR, 'opencode.json');
|
|
58
|
+
const configPath = path.join(TEST_DIR, '.opencode.json');
|
|
55
59
|
expect(await fs.pathExists(configPath)).toBe(true);
|
|
56
60
|
});
|
|
57
61
|
});
|
|
@@ -4,7 +4,7 @@ describe('Dynamic Question Generator', () => {
|
|
|
4
4
|
test('should generate questions from heuristic and AI gaps', () => {
|
|
5
5
|
const heuristicGaps = {
|
|
6
6
|
missingFields: ['architecture'],
|
|
7
|
-
missingQuestions: ['bal-
|
|
7
|
+
missingQuestions: ['bal-19']
|
|
8
8
|
};
|
|
9
9
|
const aiGaps = [
|
|
10
10
|
'Authentication flow is mentioned but not defined',
|
|
@@ -14,7 +14,7 @@ describe('Dynamic Question Generator', () => {
|
|
|
14
14
|
const questions = DynamicQuestionGenerator.generate(heuristicGaps, aiGaps);
|
|
15
15
|
|
|
16
16
|
// Heuristic questions (existing ones)
|
|
17
|
-
expect(questions.some(q => q.id === 'bal-
|
|
17
|
+
expect(questions.some(q => q.id === 'bal-19')).toBe(true);
|
|
18
18
|
|
|
19
19
|
// Dynamic AI questions (new ones)
|
|
20
20
|
expect(questions.some(q => q.text.includes('Authentication flow'))).toBe(true);
|
|
@@ -23,7 +23,7 @@ describe('Framework Detector', () => {
|
|
|
23
23
|
await fs.ensureDir(path.join(tempDir, 'docs/prd'));
|
|
24
24
|
await fs.ensureDir(path.join(tempDir, 'docs/architecture'));
|
|
25
25
|
const detected = await FrameworkDetector.detect(tempDir);
|
|
26
|
-
expect(detected).toContain('
|
|
26
|
+
expect(detected).toContain('agent-native');
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
test('should detect OpenSpec project', async () => {
|
|
@@ -33,11 +33,11 @@ describe('Framework Detector', () => {
|
|
|
33
33
|
expect(detected).toContain('openspec');
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
test('should detect
|
|
36
|
+
test('should detect specification-driven project', async () => {
|
|
37
37
|
await fs.writeFile(path.join(tempDir, 'constitution.md'), '# Constitution');
|
|
38
38
|
await fs.writeFile(path.join(tempDir, 'specification.md'), '# Specification');
|
|
39
39
|
const detected = await FrameworkDetector.detect(tempDir);
|
|
40
|
-
expect(detected).toContain('
|
|
40
|
+
expect(detected).toContain('specification-driven');
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
test('should detect multiple frameworks', async () => {
|
|
@@ -48,6 +48,34 @@ describe('Framework Detector', () => {
|
|
|
48
48
|
expect(detected).toContain('openspec');
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
test('should detect Gemini CLI Conductor project', async () => {
|
|
52
|
+
await fs.ensureDir(path.join(tempDir, 'conductor'));
|
|
53
|
+
await fs.writeFile(path.join(tempDir, 'conductor', 'product.md'), '# My Product');
|
|
54
|
+
await fs.writeFile(path.join(tempDir, 'conductor', 'workflow.md'), '# Workflow');
|
|
55
|
+
const detected = await FrameworkDetector.detect(tempDir);
|
|
56
|
+
expect(detected).toContain('gemini-conductor');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('should detect Conductor with only product.md', async () => {
|
|
60
|
+
await fs.ensureDir(path.join(tempDir, 'conductor'));
|
|
61
|
+
await fs.writeFile(path.join(tempDir, 'conductor', 'product.md'), '# My Product');
|
|
62
|
+
const detected = await FrameworkDetector.detect(tempDir);
|
|
63
|
+
expect(detected).toContain('gemini-conductor');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('should detect Conductor with only workflow.md', async () => {
|
|
67
|
+
await fs.ensureDir(path.join(tempDir, 'conductor'));
|
|
68
|
+
await fs.writeFile(path.join(tempDir, 'conductor', 'workflow.md'), '# Workflow');
|
|
69
|
+
const detected = await FrameworkDetector.detect(tempDir);
|
|
70
|
+
expect(detected).toContain('gemini-conductor');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('should not detect Conductor for empty conductor directory', async () => {
|
|
74
|
+
await fs.ensureDir(path.join(tempDir, 'conductor'));
|
|
75
|
+
const detected = await FrameworkDetector.detect(tempDir);
|
|
76
|
+
expect(detected).not.toContain('gemini-conductor');
|
|
77
|
+
});
|
|
78
|
+
|
|
51
79
|
test('should return empty array for no frameworks', async () => {
|
|
52
80
|
const detected = await FrameworkDetector.detect(tempDir);
|
|
53
81
|
expect(detected).toEqual([]);
|
|
@@ -11,9 +11,9 @@ describe('Heuristic Gap Analyzer', () => {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
const gaps = HeuristicGapAnalyzer.analyze(context, 'balanced');
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
expect(gaps.missingFields).toContain('architecture');
|
|
16
|
-
expect(gaps.missingQuestions).toContain('bal-
|
|
16
|
+
expect(gaps.missingQuestions).toContain('bal-19');
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
test('should identify missing tech stack in rapid framework', () => {
|
|
@@ -24,9 +24,9 @@ describe('Heuristic Gap Analyzer', () => {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const gaps = HeuristicGapAnalyzer.analyze(context, 'rapid');
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
expect(gaps.missingFields).toContain('techStack');
|
|
29
|
-
expect(gaps.missingQuestions).toContain('prp-
|
|
29
|
+
expect(gaps.missingQuestions).toContain('prp-5');
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
test('should find no gaps when context is complete', () => {
|
|
@@ -39,7 +39,7 @@ describe('Heuristic Gap Analyzer', () => {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
const gaps = HeuristicGapAnalyzer.analyze(context, 'balanced');
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
expect(gaps.missingFields.length).toBe(0);
|
|
44
44
|
expect(gaps.missingQuestions.length).toBe(0);
|
|
45
45
|
});
|
|
@@ -40,11 +40,15 @@ describe('Pattern Decay Algorithm', () => {
|
|
|
40
40
|
|
|
41
41
|
test('low confidence (<75) decays at 1.5x rate', () => {
|
|
42
42
|
const lowRate = getDecayRate(65, 0.15);
|
|
43
|
-
expect(lowRate).
|
|
43
|
+
expect(lowRate).toBeCloseTo(0.225); // 22.5%
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
describe('calculateDecay', () => {
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
jest.useRealTimers();
|
|
50
|
+
});
|
|
51
|
+
|
|
48
52
|
test('decays confidence over time', () => {
|
|
49
53
|
const pattern = createPattern({
|
|
50
54
|
confidence: 90,
|
|
@@ -52,16 +56,14 @@ describe('Pattern Decay Algorithm', () => {
|
|
|
52
56
|
});
|
|
53
57
|
|
|
54
58
|
// Mock current date to 2 months later
|
|
55
|
-
|
|
56
|
-
jest.
|
|
59
|
+
jest.useFakeTimers();
|
|
60
|
+
jest.setSystemTime(new Date('2025-10-01'));
|
|
57
61
|
|
|
58
62
|
const decayed = calculateDecay(pattern, config);
|
|
59
63
|
|
|
60
64
|
// Should decay over 2 months
|
|
61
65
|
expect(decayed.confidence).toBeLessThan(90);
|
|
62
|
-
expect(decayed.lastDecayCalculation).toBe(
|
|
63
|
-
|
|
64
|
-
jest.restoreAllMocks();
|
|
66
|
+
expect(decayed.lastDecayCalculation).toBe(new Date('2025-10-01').toISOString());
|
|
65
67
|
});
|
|
66
68
|
|
|
67
69
|
test('high confidence patterns decay slower than low confidence', () => {
|
|
@@ -75,8 +77,8 @@ describe('Pattern Decay Algorithm', () => {
|
|
|
75
77
|
lastSeen: new Date('2025-08-01').toISOString()
|
|
76
78
|
});
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
jest.
|
|
80
|
+
jest.useFakeTimers();
|
|
81
|
+
jest.setSystemTime(new Date('2025-10-01'));
|
|
80
82
|
|
|
81
83
|
const highDecayed = calculateDecay(highPattern, config);
|
|
82
84
|
const lowDecayed = calculateDecay(lowPattern, config);
|
|
@@ -86,8 +88,6 @@ describe('Pattern Decay Algorithm', () => {
|
|
|
86
88
|
const lowDecayPct = (lowPattern.confidence - lowDecayed.confidence) / lowPattern.confidence;
|
|
87
89
|
|
|
88
90
|
expect(highDecayPct).toBeLessThan(lowDecayPct);
|
|
89
|
-
|
|
90
|
-
jest.restoreAllMocks();
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
test('skips decay if disabled', () => {
|
|
@@ -107,16 +107,14 @@ describe('Pattern Decay Algorithm', () => {
|
|
|
107
107
|
|
|
108
108
|
const protectConfig = { ...config, protectApproved: true };
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
jest.
|
|
110
|
+
jest.useFakeTimers();
|
|
111
|
+
jest.setSystemTime(new Date('2025-10-01'));
|
|
112
112
|
|
|
113
113
|
const decayed = calculateDecay(approvedPattern, protectConfig);
|
|
114
114
|
const normalDecayed = calculateDecay(createPattern({ userApproved: false, lastSeen: new Date('2025-06-01').toISOString() }), config);
|
|
115
115
|
|
|
116
116
|
// User-approved should decay slower
|
|
117
117
|
expect(decayed.confidence).toBeGreaterThan(normalDecayed.confidence);
|
|
118
|
-
|
|
119
|
-
jest.restoreAllMocks();
|
|
120
118
|
});
|
|
121
119
|
|
|
122
120
|
test('skips decay if recently seen (< 1 week)', () => {
|
|
@@ -124,14 +122,12 @@ describe('Pattern Decay Algorithm', () => {
|
|
|
124
122
|
lastSeen: new Date('2025-09-28').toISOString()
|
|
125
123
|
});
|
|
126
124
|
|
|
127
|
-
|
|
128
|
-
jest.
|
|
125
|
+
jest.useFakeTimers();
|
|
126
|
+
jest.setSystemTime(new Date('2025-10-01'));
|
|
129
127
|
|
|
130
128
|
const result = calculateDecay(recentPattern, config);
|
|
131
129
|
|
|
132
130
|
expect(result.confidence).toBe(recentPattern.confidence);
|
|
133
|
-
|
|
134
|
-
jest.restoreAllMocks();
|
|
135
131
|
});
|
|
136
132
|
|
|
137
133
|
test('confidence never goes below 0', () => {
|
|
@@ -140,14 +136,12 @@ describe('Pattern Decay Algorithm', () => {
|
|
|
140
136
|
lastSeen: new Date('2024-01-01').toISOString()
|
|
141
137
|
});
|
|
142
138
|
|
|
143
|
-
|
|
144
|
-
jest.
|
|
139
|
+
jest.useFakeTimers();
|
|
140
|
+
jest.setSystemTime(new Date('2025-10-01'));
|
|
145
141
|
|
|
146
142
|
const decayed = calculateDecay(lowPattern, config);
|
|
147
143
|
|
|
148
144
|
expect(decayed.confidence).toBeGreaterThanOrEqual(0);
|
|
149
|
-
|
|
150
|
-
jest.restoreAllMocks();
|
|
151
145
|
});
|
|
152
146
|
});
|
|
153
147
|
|
|
@@ -155,16 +149,11 @@ describe('Pattern Decay Algorithm', () => {
|
|
|
155
149
|
test('increases confidence by renewal boost', () => {
|
|
156
150
|
const pattern = createPattern({ confidence: 60 });
|
|
157
151
|
|
|
158
|
-
const mockDate = new Date('2025-10-01');
|
|
159
|
-
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
|
160
|
-
|
|
161
152
|
const renewed = renewPattern(pattern, config);
|
|
162
153
|
|
|
163
154
|
expect(renewed.confidence).toBe(70); // 60 + 10 (renewalBoost)
|
|
164
155
|
expect(renewed.timesRenewed).toBe(1);
|
|
165
|
-
expect(renewed.lastSeen).
|
|
166
|
-
|
|
167
|
-
jest.restoreAllMocks();
|
|
156
|
+
expect(renewed.lastSeen).toBeTruthy();
|
|
168
157
|
});
|
|
169
158
|
|
|
170
159
|
test('caps at initialConfidence + 5', () => {
|
|
@@ -192,19 +181,19 @@ describe('Pattern Decay Algorithm', () => {
|
|
|
192
181
|
test('updates lastSeen and prevents immediate decay', () => {
|
|
193
182
|
const pattern = createPattern();
|
|
194
183
|
|
|
195
|
-
const mockDate = new Date('2025-10-01');
|
|
196
|
-
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
|
197
|
-
|
|
198
184
|
const renewed = renewPattern(pattern, config);
|
|
199
185
|
|
|
200
|
-
expect(renewed.lastSeen).
|
|
201
|
-
expect(renewed.lastDecayCalculation).
|
|
202
|
-
|
|
203
|
-
jest.restoreAllMocks();
|
|
186
|
+
expect(renewed.lastSeen).toBeTruthy();
|
|
187
|
+
expect(renewed.lastDecayCalculation).toBeTruthy();
|
|
188
|
+
expect(renewed.lastSeen).toBe(renewed.lastDecayCalculation);
|
|
204
189
|
});
|
|
205
190
|
});
|
|
206
191
|
|
|
207
192
|
describe('applyDecayToStoredPatterns', () => {
|
|
193
|
+
afterEach(() => {
|
|
194
|
+
jest.useRealTimers();
|
|
195
|
+
});
|
|
196
|
+
|
|
208
197
|
test('applies decay to all patterns', () => {
|
|
209
198
|
const patterns = [
|
|
210
199
|
createPattern({ id: 'p1', confidence: 90, lastSeen: new Date('2025-08-01').toISOString() }),
|
|
@@ -212,8 +201,8 @@ describe('Pattern Decay Algorithm', () => {
|
|
|
212
201
|
createPattern({ id: 'p3', confidence: 70, lastSeen: new Date('2025-08-01').toISOString() })
|
|
213
202
|
];
|
|
214
203
|
|
|
215
|
-
|
|
216
|
-
jest.
|
|
204
|
+
jest.useFakeTimers();
|
|
205
|
+
jest.setSystemTime(new Date('2025-10-01'));
|
|
217
206
|
|
|
218
207
|
const decayed = applyDecayToStoredPatterns(patterns, config);
|
|
219
208
|
|
|
@@ -221,8 +210,6 @@ describe('Pattern Decay Algorithm', () => {
|
|
|
221
210
|
expect(decayed[0].confidence).toBeLessThan(90);
|
|
222
211
|
expect(decayed[1].confidence).toBeLessThan(80);
|
|
223
212
|
expect(decayed[2].confidence).toBeLessThan(70);
|
|
224
|
-
|
|
225
|
-
jest.restoreAllMocks();
|
|
226
213
|
});
|
|
227
214
|
|
|
228
215
|
test('handles empty pattern array', () => {
|
|
@@ -237,34 +224,31 @@ describe('Pattern Decay Algorithm', () => {
|
|
|
237
224
|
test('renews a single pattern', () => {
|
|
238
225
|
const pattern = createPattern({ confidence: 65 });
|
|
239
226
|
|
|
240
|
-
const mockDate = new Date('2025-10-01');
|
|
241
|
-
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
|
242
|
-
|
|
243
227
|
const renewed = renewStoredPattern(pattern, config);
|
|
244
228
|
|
|
245
229
|
expect(renewed.confidence).toBe(75);
|
|
246
230
|
expect(renewed.timesRenewed).toBe(1);
|
|
247
|
-
|
|
248
|
-
jest.restoreAllMocks();
|
|
249
231
|
});
|
|
250
232
|
});
|
|
251
233
|
|
|
252
234
|
describe('Edge Cases', () => {
|
|
235
|
+
afterEach(() => {
|
|
236
|
+
jest.useRealTimers();
|
|
237
|
+
});
|
|
238
|
+
|
|
253
239
|
test('handles very old patterns (6+ months)', () => {
|
|
254
240
|
const oldPattern = createPattern({
|
|
255
241
|
confidence: 80,
|
|
256
242
|
lastSeen: new Date('2024-12-01').toISOString()
|
|
257
243
|
});
|
|
258
244
|
|
|
259
|
-
|
|
260
|
-
jest.
|
|
245
|
+
jest.useFakeTimers();
|
|
246
|
+
jest.setSystemTime(new Date('2025-10-01'));
|
|
261
247
|
|
|
262
248
|
const decayed = calculateDecay(oldPattern, config);
|
|
263
249
|
|
|
264
250
|
// Should have significantly decayed after 10 months
|
|
265
251
|
expect(decayed.confidence).toBeLessThan(40);
|
|
266
|
-
|
|
267
|
-
jest.restoreAllMocks();
|
|
268
252
|
});
|
|
269
253
|
|
|
270
254
|
test('handles pattern oscillation (multiple renewals)', () => {
|
|
@@ -306,15 +290,13 @@ describe('Pattern Decay Algorithm', () => {
|
|
|
306
290
|
lastSeen: new Date('2025-08-01').toISOString()
|
|
307
291
|
});
|
|
308
292
|
|
|
309
|
-
|
|
310
|
-
jest.
|
|
293
|
+
jest.useFakeTimers();
|
|
294
|
+
jest.setSystemTime(new Date('2025-10-01'));
|
|
311
295
|
|
|
312
296
|
const decayed = calculateDecay(maxPattern, config);
|
|
313
297
|
|
|
314
298
|
expect(decayed.confidence).toBeLessThan(100);
|
|
315
299
|
expect(decayed.confidence).toBeGreaterThan(0);
|
|
316
|
-
|
|
317
|
-
jest.restoreAllMocks();
|
|
318
300
|
});
|
|
319
301
|
});
|
|
320
302
|
|
|
@@ -132,6 +132,131 @@ describe('SessionManager', () => {
|
|
|
132
132
|
});
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
+
describe('getSessionsWithDetails', () => {
|
|
136
|
+
it('should return empty array when no sessions exist', async () => {
|
|
137
|
+
const manager = new SessionManager(TEST_PROJECT_PATH);
|
|
138
|
+
const sessions = await manager.getSessionsWithDetails();
|
|
139
|
+
|
|
140
|
+
expect(sessions).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should count answered and unanswered questions correctly', async () => {
|
|
144
|
+
const manager = new SessionManager(TEST_PROJECT_PATH);
|
|
145
|
+
|
|
146
|
+
const sessionPath = path.join(TEST_SESSIONS_DIR, 'session-details');
|
|
147
|
+
await fs.ensureDir(sessionPath);
|
|
148
|
+
|
|
149
|
+
const tracker = new ProgressTracker(sessionPath, 5, 'rapid');
|
|
150
|
+
await tracker.initialize();
|
|
151
|
+
|
|
152
|
+
// Simulate 2 answered questions (rapid has 10 questions starting with prp-1, prp-2, etc.)
|
|
153
|
+
tracker.progress.answers['prp-1'] = { text: 'Answer 1', quality: { qualityScore: 80, wordCount: 10, isComprehensive: true }, timestamp: new Date().toISOString() };
|
|
154
|
+
tracker.progress.answers['prp-2'] = { text: 'Answer 2', quality: { qualityScore: 75, wordCount: 8, isComprehensive: false }, timestamp: new Date().toISOString() };
|
|
155
|
+
await tracker.save();
|
|
156
|
+
|
|
157
|
+
const sessions = await manager.getSessionsWithDetails();
|
|
158
|
+
|
|
159
|
+
expect(sessions.length).toBe(1);
|
|
160
|
+
expect(sessions[0].totalQuestions).toBe(10); // rapid/prp has 10 questions
|
|
161
|
+
expect(sessions[0].answeredCount).toBe(2);
|
|
162
|
+
expect(sessions[0].unansweredCount).toBe(8);
|
|
163
|
+
expect(sessions[0].hasGaps).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should report hasGaps false when all questions answered', async () => {
|
|
167
|
+
const manager = new SessionManager(TEST_PROJECT_PATH);
|
|
168
|
+
|
|
169
|
+
const sessionPath = path.join(TEST_SESSIONS_DIR, 'session-full');
|
|
170
|
+
await fs.ensureDir(sessionPath);
|
|
171
|
+
|
|
172
|
+
const tracker = new ProgressTracker(sessionPath, 5, 'rapid');
|
|
173
|
+
await tracker.initialize();
|
|
174
|
+
|
|
175
|
+
// Simulate all 10 PRP questions answered
|
|
176
|
+
for (let i = 1; i <= 10; i++) {
|
|
177
|
+
tracker.progress.answers[`prp-${i}`] = {
|
|
178
|
+
text: `Answer ${i}`,
|
|
179
|
+
quality: { qualityScore: 80, wordCount: 10, isComprehensive: true },
|
|
180
|
+
timestamp: new Date().toISOString()
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
await tracker.save();
|
|
184
|
+
|
|
185
|
+
const sessions = await manager.getSessionsWithDetails();
|
|
186
|
+
|
|
187
|
+
expect(sessions.length).toBe(1);
|
|
188
|
+
expect(sessions[0].answeredCount).toBe(10);
|
|
189
|
+
expect(sessions[0].unansweredCount).toBe(0);
|
|
190
|
+
expect(sessions[0].hasGaps).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should ignore answer keys that do not match framework questions', async () => {
|
|
194
|
+
const manager = new SessionManager(TEST_PROJECT_PATH);
|
|
195
|
+
|
|
196
|
+
const sessionPath = path.join(TEST_SESSIONS_DIR, 'session-stale');
|
|
197
|
+
await fs.ensureDir(sessionPath);
|
|
198
|
+
|
|
199
|
+
const tracker = new ProgressTracker(sessionPath, 5, 'rapid');
|
|
200
|
+
await tracker.initialize();
|
|
201
|
+
|
|
202
|
+
// Add a valid answer and a stale/invalid key
|
|
203
|
+
tracker.progress.answers['prp-1'] = { text: 'Answer 1', quality: { qualityScore: 80, wordCount: 10, isComprehensive: true }, timestamp: new Date().toISOString() };
|
|
204
|
+
tracker.progress.answers['old-removed-question'] = { text: 'Stale', quality: { qualityScore: 50, wordCount: 3, isComprehensive: false }, timestamp: new Date().toISOString() };
|
|
205
|
+
await tracker.save();
|
|
206
|
+
|
|
207
|
+
const sessions = await manager.getSessionsWithDetails();
|
|
208
|
+
|
|
209
|
+
expect(sessions[0].answeredCount).toBe(1); // only prp-1 counts
|
|
210
|
+
expect(sessions[0].unansweredCount).toBe(9);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('reopenSession', () => {
|
|
215
|
+
it('should reset counters but preserve answers', async () => {
|
|
216
|
+
const manager = new SessionManager(TEST_PROJECT_PATH);
|
|
217
|
+
|
|
218
|
+
const sessionPath = path.join(TEST_SESSIONS_DIR, 'session-reopen');
|
|
219
|
+
await fs.ensureDir(sessionPath);
|
|
220
|
+
|
|
221
|
+
const tracker = new ProgressTracker(sessionPath, 5, 'rapid');
|
|
222
|
+
await tracker.initialize();
|
|
223
|
+
|
|
224
|
+
// Simulate some progress and complete
|
|
225
|
+
tracker.progress.answers['prp-1'] = { text: 'Answer 1', quality: { qualityScore: 80, wordCount: 10, isComprehensive: true }, timestamp: new Date().toISOString() };
|
|
226
|
+
tracker.progress.completedBlocks = [{ number: 1, title: 'Block 1', questionsAnswered: 1, completedAt: new Date().toISOString() }];
|
|
227
|
+
tracker.progress.totalQuestionsAnswered = 1;
|
|
228
|
+
tracker.progress.currentBlock = 1;
|
|
229
|
+
await tracker.complete();
|
|
230
|
+
|
|
231
|
+
// Verify it's completed
|
|
232
|
+
const beforeReopen = await fs.readJson(path.join(sessionPath, '_progress.json'));
|
|
233
|
+
expect(beforeReopen.status).toBe('completed');
|
|
234
|
+
expect(beforeReopen.canResume).toBe(false);
|
|
235
|
+
|
|
236
|
+
// Reopen it
|
|
237
|
+
const result = await manager.reopenSession('session-reopen');
|
|
238
|
+
|
|
239
|
+
expect(result.progress.status).toBe('in-progress');
|
|
240
|
+
expect(result.progress.canResume).toBe(true);
|
|
241
|
+
expect(result.progress.completedAt).toBeNull();
|
|
242
|
+
expect(result.progress.completedBlocks).toEqual([]);
|
|
243
|
+
expect(result.progress.skippedBlocks).toEqual([]);
|
|
244
|
+
expect(result.progress.totalQuestionsAnswered).toBe(0);
|
|
245
|
+
expect(result.progress.currentBlock).toBe(0);
|
|
246
|
+
|
|
247
|
+
// Answers should be preserved
|
|
248
|
+
expect(result.progress.answers['prp-1']).toBeDefined();
|
|
249
|
+
expect(result.progress.answers['prp-1'].text).toBe('Answer 1');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should throw error for non-existent session', async () => {
|
|
253
|
+
const manager = new SessionManager(TEST_PROJECT_PATH);
|
|
254
|
+
|
|
255
|
+
await expect(manager.reopenSession('non-existent'))
|
|
256
|
+
.rejects.toThrow('Session not found: non-existent');
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
135
260
|
describe('deleteAllSessions', () => {
|
|
136
261
|
it('should delete all sessions and recreate directory', async () => {
|
|
137
262
|
const manager = new SessionManager(TEST_PROJECT_PATH);
|