@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.
@@ -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: new Date('2025-01-01').toISOString(),
34
- lastSeen: new Date('2025-09-01').toISOString(),
35
- lastDecayCalculation: new Date('2025-09-01').toISOString(),
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 old lastSeen dates
45
+ // Create patterns with lastSeen dates ~2 months ago (enough for measurable decay)
43
46
  const patterns = [
44
- createTestPattern({ id: 'p1', confidence: 90, lastSeen: new Date('2025-07-01').toISOString() }),
45
- createTestPattern({ id: 'p2', confidence: 80, lastSeen: new Date('2025-07-01').toISOString() })
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: new Date('2025-09-01').toISOString()
69
+ lastSeen: daysAgo(10)
67
70
  }),
68
71
  createTestPattern({
69
72
  id: 'active',
70
73
  confidence: 80,
71
- lastSeen: new Date('2025-09-01').toISOString()
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: new Date('2025-06-01').toISOString() })
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: new Date('2024-12-01').toISOString() // 10 months ago
275
+ lastSeen: daysAgo(300) // ~10 months ago
273
276
  }),
274
277
  createTestPattern({
275
278
  id: 'recent',
276
279
  confidence: 80,
277
- lastSeen: new Date('2025-09-01').toISOString()
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: new Date('2025-09-01').toISOString() }),
293
- createTestPattern({ confidence: 70, lastSeen: new Date('2025-09-01').toISOString() })
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: new Date('2025-07-01').toISOString() }),
307
- createTestPattern({ confidence: 35, lastSeen: new Date('2025-07-01').toISOString() }) // Will be removed
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: new Date('2025-08-01').toISOString() }), // High
341
- createTestPattern({ confidence: 70, createdAt: new Date('2025-08-01').toISOString() }), // Medium
342
- createTestPattern({ confidence: 55, createdAt: new Date('2025-08-01').toISOString() }) // Low
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, {
@@ -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-37']
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-37')).toBe(true);
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('bmad');
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 Spec-Kit project', async () => {
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('spec-kit');
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-37');
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-8');
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).toBe(0.225); // 22.5%
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
- const mockDate = new Date('2025-10-01');
56
- jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
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(mockDate.toISOString());
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
- const mockDate = new Date('2025-10-01');
79
- jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
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
- const mockDate = new Date('2025-10-01');
111
- jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
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
- const mockDate = new Date('2025-10-01');
128
- jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
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
- const mockDate = new Date('2025-10-01');
144
- jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
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).toBe(mockDate.toISOString());
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).toBe(mockDate.toISOString());
201
- expect(renewed.lastDecayCalculation).toBe(mockDate.toISOString());
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
- const mockDate = new Date('2025-10-01');
216
- jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
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
- const mockDate = new Date('2025-10-01');
260
- jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
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
- const mockDate = new Date('2025-10-01');
310
- jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
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);