@iservu-inc/adf-cli 0.17.1 → 0.18.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.
Files changed (45) hide show
  1. package/.project/chats/current/SESSION-STATUS.md +29 -27
  2. package/.project/docs/ROADMAP.md +74 -64
  3. package/CHANGELOG.md +78 -0
  4. package/CLAUDE.md +1 -1
  5. package/README.md +63 -27
  6. package/bin/adf.js +54 -0
  7. package/lib/analysis/dynamic-pipeline.js +26 -0
  8. package/lib/analysis/knowledge-graph.js +66 -0
  9. package/lib/commands/deploy.js +35 -0
  10. package/lib/commands/harness.js +345 -0
  11. package/lib/commands/init.js +135 -10
  12. package/lib/frameworks/interviewer.js +130 -0
  13. package/lib/frameworks/progress-tracker.js +30 -1
  14. package/lib/frameworks/session-manager.js +76 -0
  15. package/lib/harness/context-window-manager.js +255 -0
  16. package/lib/harness/event-logger.js +115 -0
  17. package/lib/harness/feature-manifest.js +175 -0
  18. package/lib/harness/headless-adapter.js +184 -0
  19. package/lib/harness/milestone-tracker.js +183 -0
  20. package/lib/harness/protocol.js +503 -0
  21. package/lib/harness/provider-bridge.js +226 -0
  22. package/lib/harness/run-manager.js +267 -0
  23. package/lib/templates/scripts/analyze-docs.js +12 -1
  24. package/lib/utils/context-extractor.js +48 -0
  25. package/lib/utils/framework-detector.js +10 -1
  26. package/lib/utils/project-detector.js +5 -1
  27. package/lib/utils/tool-detector.js +167 -0
  28. package/lib/utils/tool-feature-registry.js +82 -13
  29. package/lib/utils/tool-recommender.js +325 -0
  30. package/package.json +1 -1
  31. package/tests/context-extractor.test.js +45 -0
  32. package/tests/framework-detector.test.js +28 -0
  33. package/tests/harness-backward-compat.test.js +251 -0
  34. package/tests/harness-context-window.test.js +310 -0
  35. package/tests/harness-event-logger.test.js +148 -0
  36. package/tests/harness-feature-manifest.test.js +124 -0
  37. package/tests/harness-headless-adapter.test.js +196 -0
  38. package/tests/harness-integration.test.js +207 -0
  39. package/tests/harness-milestone-tracker.test.js +158 -0
  40. package/tests/harness-protocol.test.js +341 -0
  41. package/tests/harness-provider-bridge.test.js +180 -0
  42. package/tests/harness-provider-switch.test.js +204 -0
  43. package/tests/harness-run-manager.test.js +131 -0
  44. package/tests/tool-detector.test.js +152 -0
  45. package/tests/tool-recommender.test.js +218 -0
@@ -50,6 +50,51 @@ Add many cool features.
50
50
  expect(context.proposedChanges).toContain('Add many cool features');
51
51
  });
52
52
 
53
+ test('should extract context from Gemini CLI Conductor', async () => {
54
+ await fs.ensureDir(path.join(tempDir, 'conductor'));
55
+ await fs.writeFile(path.join(tempDir, 'conductor', 'product.md'), `# My Product
56
+
57
+ ## Overview
58
+ An AI-powered task management system.
59
+
60
+ ## Features
61
+ - Smart task prioritization
62
+ - Team collaboration
63
+ `);
64
+ await fs.writeFile(path.join(tempDir, 'conductor', 'tech-stack.md'), `# Tech Stack
65
+
66
+ - **Language**: TypeScript
67
+ - **Runtime**: Node.js
68
+ - **Framework**: Express
69
+ `);
70
+ await fs.writeFile(path.join(tempDir, 'conductor', 'workflow.md'), `# Development Workflow
71
+
72
+ ## Quality Gates
73
+ All PRs must pass linting, tests, and review.
74
+ `);
75
+
76
+ const context = await ContextExtractor.extract(tempDir, ['gemini-conductor']);
77
+ expect(context.name).toBe('My Product');
78
+ expect(context.overview).toContain('AI-powered task management');
79
+ expect(context.techStack).toContain('TypeScript');
80
+ expect(context.architecture).toContain('linting, tests, and review');
81
+ });
82
+
83
+ test('should extract Conductor context with partial files', async () => {
84
+ await fs.ensureDir(path.join(tempDir, 'conductor'));
85
+ await fs.writeFile(path.join(tempDir, 'conductor', 'product.md'), `# TaskFlow
86
+
87
+ ## Vision
88
+ A next-gen project management tool.
89
+ `);
90
+
91
+ const context = await ContextExtractor.extract(tempDir, ['gemini-conductor']);
92
+ expect(context.name).toBe('TaskFlow');
93
+ expect(context.overview).toContain('next-gen project management');
94
+ expect(context.techStack).toBe('');
95
+ expect(context.architecture).toBe('');
96
+ });
97
+
53
98
  test('should extract tech stack and architecture from specification-driven', async () => {
54
99
  const specContent = `# Specification: App Core
55
100
 
@@ -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([]);
@@ -0,0 +1,251 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const ProgressTracker = require('../lib/frameworks/progress-tracker');
4
+ const SessionManager = require('../lib/frameworks/session-manager');
5
+ const KnowledgeGraph = require('../lib/analysis/knowledge-graph');
6
+ const DynamicPipeline = require('../lib/analysis/dynamic-pipeline');
7
+
8
+ describe('Harness Backward Compatibility', () => {
9
+ const tempDir = path.join(__dirname, 'temp-backward-compat');
10
+
11
+ beforeEach(async () => {
12
+ await fs.ensureDir(tempDir);
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await fs.remove(tempDir);
17
+ });
18
+
19
+ describe('ProgressTracker', () => {
20
+ test('should work without harness options (backward compat)', async () => {
21
+ const sessionPath = path.join(tempDir, 'session-1');
22
+ await fs.ensureDir(sessionPath);
23
+
24
+ // Create tracker without harness options (old way)
25
+ const tracker = new ProgressTracker(sessionPath, 3, 'rapid');
26
+ await tracker.initialize();
27
+
28
+ await tracker.startBlock(1, 'Overview');
29
+ await tracker.answerQuestion('q1', 'What?', 'React app', {
30
+ wordCount: 2, qualityScore: 80, isComprehensive: true
31
+ });
32
+ await tracker.completeBlock(1, 'Overview', 1);
33
+
34
+ // Should work exactly as before
35
+ const progress = tracker.getProgress();
36
+ expect(progress.status).toBe('in-progress');
37
+ expect(progress.totalQuestionsAnswered).toBe(1);
38
+ expect(progress.answers.q1).toBeDefined();
39
+ });
40
+
41
+ test('should work with harness options (new way)', async () => {
42
+ const sessionPath = path.join(tempDir, 'session-2');
43
+ await fs.ensureDir(sessionPath);
44
+
45
+ const EventLogger = require('../lib/harness/event-logger');
46
+ const loggerPath = path.join(tempDir, '_events.jsonl');
47
+ const logger = new EventLogger(loggerPath);
48
+
49
+ // Create tracker with harness options (new way)
50
+ const tracker = new ProgressTracker(sessionPath, 3, 'balanced', null, {
51
+ eventLogger: logger,
52
+ contextWindowId: 'cw_test-1'
53
+ });
54
+ await tracker.initialize();
55
+
56
+ // Set runId so events pass validation
57
+ tracker.progress.runId = 'run_test-1';
58
+
59
+ await tracker.startBlock(1, 'Overview');
60
+ await tracker.answerQuestion('q1', 'What?', 'React app', {
61
+ wordCount: 2, qualityScore: 80, isComprehensive: true
62
+ });
63
+ await tracker.completeBlock(1, 'Overview', 1);
64
+
65
+ // Should emit events
66
+ const events = await logger.readAll();
67
+ expect(events.length).toBeGreaterThanOrEqual(3); // block_start + answer + block_complete
68
+
69
+ const answerEvent = events.find(e => e.type === 'answer');
70
+ expect(answerEvent.contextWindowId).toBe('cw_test-1');
71
+ });
72
+ });
73
+
74
+ describe('SessionManager', () => {
75
+ test('should work without harness (backward compat)', async () => {
76
+ const sessionsDir = path.join(tempDir, '.adf', 'sessions');
77
+ await fs.ensureDir(sessionsDir);
78
+
79
+ // Create a legacy session (no harness)
80
+ const sessionDir = path.join(sessionsDir, '2026-01-01T00-00-00_rapid');
81
+ await fs.ensureDir(sessionDir);
82
+ await fs.writeJson(path.join(sessionDir, '_progress.json'), {
83
+ sessionId: '2026-01-01T00-00-00_rapid',
84
+ framework: 'rapid',
85
+ status: 'in-progress',
86
+ canResume: true,
87
+ lastUpdated: '2026-01-01T00:00:00.000Z',
88
+ totalBlocks: 3,
89
+ currentBlock: 1,
90
+ completedBlocks: [],
91
+ skippedBlocks: [],
92
+ totalQuestionsAnswered: 0,
93
+ answers: {}
94
+ }, { spaces: 2 });
95
+
96
+ const manager = new SessionManager(tempDir);
97
+ const sessions = await manager.listSessions();
98
+ expect(sessions.length).toBe(1);
99
+
100
+ const resumable = await manager.getResumableSessions();
101
+ expect(resumable.length).toBe(1);
102
+ });
103
+
104
+ test('getCurrentSession should fallback to mtime when no harness', async () => {
105
+ const sessionsDir = path.join(tempDir, '.adf', 'sessions');
106
+ await fs.ensureDir(sessionsDir);
107
+
108
+ // Create two sessions
109
+ const session1Dir = path.join(sessionsDir, '2026-01-01T00-00-00_rapid');
110
+ await fs.ensureDir(session1Dir);
111
+ await fs.writeJson(path.join(session1Dir, '_progress.json'), {
112
+ sessionId: '2026-01-01T00-00-00_rapid',
113
+ status: 'completed',
114
+ lastUpdated: '2026-01-01T00:00:00.000Z',
115
+ completedBlocks: [],
116
+ answers: {}
117
+ });
118
+
119
+ const session2Dir = path.join(sessionsDir, '2026-02-01T00-00-00_balanced');
120
+ await fs.ensureDir(session2Dir);
121
+ await fs.writeJson(path.join(session2Dir, '_progress.json'), {
122
+ sessionId: '2026-02-01T00-00-00_balanced',
123
+ status: 'in-progress',
124
+ lastUpdated: '2026-02-01T00:00:00.000Z',
125
+ completedBlocks: [],
126
+ answers: {}
127
+ });
128
+
129
+ const manager = new SessionManager(tempDir);
130
+ const current = await manager.getCurrentSession();
131
+ // Should return most recent by lastUpdated
132
+ expect(current.sessionId).toBe('2026-02-01T00-00-00_balanced');
133
+ });
134
+
135
+ test('forkSession should copy progress and knowledge graph', async () => {
136
+ const sessionsDir = path.join(tempDir, '.adf', 'sessions');
137
+ await fs.ensureDir(sessionsDir);
138
+
139
+ // Create source session
140
+ const sourceDir = path.join(sessionsDir, 'source-session');
141
+ await fs.ensureDir(sourceDir);
142
+ await fs.writeJson(path.join(sourceDir, '_progress.json'), {
143
+ sessionId: 'source-session',
144
+ framework: 'balanced',
145
+ status: 'completed',
146
+ answers: { q1: { text: 'React', quality: { qualityScore: 85 } } }
147
+ });
148
+ await fs.writeJson(path.join(sourceDir, '_knowledge_graph.json'), {
149
+ savedAt: '2026-01-01T00:00:00.000Z',
150
+ knowledge: [{ type: 'tech_stack', items: [{ content: 'React', confidence: 90, sources: ['q1'] }] }]
151
+ });
152
+
153
+ const manager = new SessionManager(tempDir);
154
+ const forked = await manager.forkSession('source-session');
155
+
156
+ expect(forked.sessionId).toContain('forked');
157
+ expect(await fs.pathExists(path.join(forked.sessionPath, '_progress.json'))).toBe(true);
158
+ expect(await fs.pathExists(path.join(forked.sessionPath, '_knowledge_graph.json'))).toBe(true);
159
+
160
+ // Verify forked progress
161
+ const progress = await fs.readJson(path.join(forked.sessionPath, '_progress.json'));
162
+ expect(progress.status).toBe('in-progress');
163
+ expect(progress.canResume).toBe(true);
164
+ expect(progress.answers.q1.text).toBe('React');
165
+ });
166
+ });
167
+
168
+ describe('KnowledgeGraph', () => {
169
+ test('existing methods still work unchanged', () => {
170
+ const kg = new KnowledgeGraph(tempDir);
171
+
172
+ // All existing methods should still work
173
+ kg.add([
174
+ { type: 'tech_stack', content: 'React', confidence: 90, source: 'q1' },
175
+ { type: 'architecture', content: 'Monolith', confidence: 70, source: 'q2' }
176
+ ]);
177
+
178
+ expect(kg.has('tech_stack', 90)).toBe(true);
179
+ expect(kg.get('tech_stack').length).toBe(1);
180
+ expect(kg.getConfidence('tech_stack')).toBe(90);
181
+ expect(kg.getSummary().tech_stack.count).toBe(1);
182
+ expect(kg.getDisplaySummary().length).toBeGreaterThan(0);
183
+ expect(kg.getStats().totalItems).toBe(2);
184
+ });
185
+
186
+ test('new handoff methods do not break existing data', () => {
187
+ const kg = new KnowledgeGraph(tempDir);
188
+ kg.add([
189
+ { type: 'tech_stack', content: 'React', confidence: 90, source: 'q1' }
190
+ ]);
191
+
192
+ // Export to handoff
193
+ const handoff = kg.toHandoffFormat();
194
+ expect(handoff.highConfidenceItems.length).toBe(1);
195
+
196
+ // Existing data untouched
197
+ expect(kg.has('tech_stack', 90)).toBe(true);
198
+ expect(kg.getStats().totalItems).toBe(1);
199
+
200
+ // Import from handoff into new graph
201
+ const kg2 = new KnowledgeGraph(tempDir);
202
+ kg2.fromHandoffFormat(handoff);
203
+ expect(kg2.has('tech_stack', 90)).toBe(true);
204
+ });
205
+ });
206
+
207
+ describe('DynamicPipeline', () => {
208
+ test('existing methods still work unchanged', () => {
209
+ const pipeline = new DynamicPipeline(tempDir, null, { enabled: true });
210
+
211
+ // Existing methods
212
+ expect(pipeline.isEnabled()).toBe(true);
213
+ pipeline.setEnabled(false);
214
+ expect(pipeline.isEnabled()).toBe(false);
215
+ pipeline.setEnabled(true);
216
+
217
+ const stats = pipeline.getStats();
218
+ expect(stats.questionsAsked).toBe(0);
219
+ });
220
+
221
+ test('new serialization methods do not break existing functionality', () => {
222
+ const pipeline = new DynamicPipeline(tempDir, null, { enabled: true });
223
+ pipeline.stats.questionsAsked = 5;
224
+
225
+ // New methods
226
+ const serialized = pipeline.serializeForHandoff();
227
+ expect(serialized.stats.questionsAsked).toBe(5);
228
+
229
+ // Existing state unchanged
230
+ expect(pipeline.stats.questionsAsked).toBe(5);
231
+ expect(pipeline.isEnabled()).toBe(true);
232
+
233
+ // Deserialize into new pipeline
234
+ const pipeline2 = new DynamicPipeline(tempDir, null, { enabled: true });
235
+ pipeline2.deserializeFromHandoff(serialized);
236
+ expect(pipeline2.stats.questionsAsked).toBe(5);
237
+ });
238
+ });
239
+
240
+ describe('No harness directory when not using --harness', () => {
241
+ test('should not create harness dir for normal init flow', async () => {
242
+ // Simulate normal flow - just create .adf directory
243
+ const adfDir = path.join(tempDir, '.adf');
244
+ await fs.ensureDir(adfDir);
245
+ await fs.ensureDir(path.join(adfDir, 'sessions'));
246
+
247
+ // Harness dir should not exist
248
+ expect(await fs.pathExists(path.join(adfDir, 'harness'))).toBe(false);
249
+ });
250
+ });
251
+ });
@@ -0,0 +1,310 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const ContextWindowManager = require('../lib/harness/context-window-manager');
4
+ const KnowledgeGraph = require('../lib/analysis/knowledge-graph');
5
+ const DynamicPipeline = require('../lib/analysis/dynamic-pipeline');
6
+
7
+ describe('ContextWindowManager', () => {
8
+ const tempDir = path.join(__dirname, 'temp-context-window-test');
9
+ let manager;
10
+
11
+ beforeEach(async () => {
12
+ await fs.ensureDir(tempDir);
13
+ await fs.ensureDir(path.join(tempDir, 'context-windows'));
14
+ manager = new ContextWindowManager(tempDir, 'run_test');
15
+ });
16
+
17
+ afterEach(async () => {
18
+ await fs.remove(tempDir);
19
+ });
20
+
21
+ test('should open a new context window', async () => {
22
+ const window = await manager.openWindow();
23
+ expect(window.id).toMatch(/^cw_/);
24
+ expect(window.runId).toBe('run_test');
25
+ expect(window.sequence).toBe(1);
26
+ expect(window.status).toBe('active');
27
+ expect(window.previousWindowId).toBeNull();
28
+ });
29
+
30
+ test('should chain windows with sequence numbers', async () => {
31
+ const w1 = await manager.openWindow();
32
+ await manager.closeWindow();
33
+
34
+ const w2 = await manager.openWindow();
35
+ expect(w2.sequence).toBe(2);
36
+ expect(w2.previousWindowId).toBe(w1.id);
37
+ });
38
+
39
+ test('should close current window', async () => {
40
+ await manager.openWindow();
41
+ const closed = await manager.closeWindow();
42
+ expect(closed.status).toBe('completed');
43
+ expect(closed.closedAt).toBeDefined();
44
+ expect(manager.getCurrentWindow()).toBeNull();
45
+ });
46
+
47
+ test('should auto-close previous window on open', async () => {
48
+ const w1 = await manager.openWindow();
49
+ const w2 = await manager.openWindow();
50
+
51
+ // w1 should have been closed
52
+ const windows = await manager.listWindows();
53
+ const closedW1 = windows.find(w => w.id === w1.id);
54
+ expect(closedW1.status).toBe('completed');
55
+ expect(w2.status).toBe('active');
56
+ });
57
+
58
+ test('should generate handoff package', async () => {
59
+ await manager.openWindow();
60
+
61
+ const handoff = await manager.generateHandoffPackage({
62
+ progressSummary: 'Completed 3 of 5 blocks',
63
+ completedWork: ['Block 1: Overview', 'Block 2: Architecture', 'Block 3: Features'],
64
+ nextSteps: ['Block 4: Deployment'],
65
+ currentMilestone: { index: 3, title: 'Deployment', percentage: 0 },
66
+ knowledgeGraph: { highConfidenceItems: [{ type: 'tech_stack', content: 'React' }], totalItems: 5, typesSeen: ['tech_stack'] },
67
+ criticalAnswers: [{ questionId: 'q1', summary: 'React SPA', quality: 90 }],
68
+ position: {
69
+ currentBlock: 4,
70
+ totalBlocks: 5,
71
+ answeredIds: ['q1', 'q2', 'q3'],
72
+ remainingIds: ['q4', 'q5']
73
+ }
74
+ });
75
+
76
+ expect(handoff.progressSummary).toBe('Completed 3 of 5 blocks');
77
+ expect(handoff.completedWork.length).toBe(3);
78
+ expect(handoff.position.answeredIds.length).toBe(3);
79
+ expect(handoff.sourceWindowId).toBeDefined();
80
+ });
81
+
82
+ test('should save handoff to file', async () => {
83
+ const window = await manager.openWindow();
84
+ await manager.generateHandoffPackage({
85
+ progressSummary: 'Test handoff'
86
+ });
87
+
88
+ const handoffFile = path.join(tempDir, 'context-windows', `${window.id}_handoff.json`);
89
+ expect(await fs.pathExists(handoffFile)).toBe(true);
90
+ });
91
+
92
+ test('should consume handoff package', async () => {
93
+ // Window 1: generate handoff
94
+ const w1 = await manager.openWindow();
95
+ await manager.generateHandoffPackage({
96
+ progressSummary: 'Half done',
97
+ position: { currentBlock: 3, totalBlocks: 6, answeredIds: ['q1', 'q2'], remainingIds: ['q3'] }
98
+ });
99
+ await manager.closeWindow();
100
+
101
+ // Window 2: consume handoff
102
+ await manager.openWindow();
103
+ const handoff = await manager.consumeHandoffPackage(w1.id);
104
+ expect(handoff).not.toBeNull();
105
+ expect(handoff.progressSummary).toBe('Half done');
106
+ expect(handoff.position.answeredIds).toEqual(['q1', 'q2']);
107
+ });
108
+
109
+ test('should consume latest handoff', async () => {
110
+ // Generate two handoffs
111
+ await manager.openWindow();
112
+ await manager.generateHandoffPackage({ progressSummary: 'First' });
113
+ await manager.closeWindow();
114
+
115
+ await manager.openWindow();
116
+ await manager.generateHandoffPackage({ progressSummary: 'Second' });
117
+ await manager.closeWindow();
118
+
119
+ // Consume latest
120
+ await manager.openWindow();
121
+ const handoff = await manager.consumeHandoffPackage('latest');
122
+ expect(handoff.progressSummary).toBe('Second');
123
+ });
124
+
125
+ test('should return null when no handoff available', async () => {
126
+ const handoff = await manager.consumeHandoffPackage('cw_nonexistent');
127
+ expect(handoff).toBeNull();
128
+ });
129
+
130
+ test('should record questions and blocks', async () => {
131
+ await manager.openWindow();
132
+
133
+ manager.recordQuestionAnswered('q1');
134
+ manager.recordQuestionAnswered('q2');
135
+ manager.recordBlockCompleted(1);
136
+
137
+ const window = manager.getCurrentWindow();
138
+ expect(window.questionsAnswered).toEqual(['q1', 'q2']);
139
+ expect(window.blocksCompleted).toEqual([1]);
140
+ });
141
+
142
+ test('should record knowledge types (no duplicates)', async () => {
143
+ await manager.openWindow();
144
+
145
+ manager.recordKnowledgeAdded('tech_stack');
146
+ manager.recordKnowledgeAdded('architecture');
147
+ manager.recordKnowledgeAdded('tech_stack'); // duplicate
148
+
149
+ const window = manager.getCurrentWindow();
150
+ expect(window.knowledgeAdded).toEqual(['tech_stack', 'architecture']);
151
+ });
152
+
153
+ test('should track token consumption', async () => {
154
+ await manager.openWindow({ tokenLimit: 100000 });
155
+
156
+ await manager.updateTokens(5000);
157
+ await manager.updateTokens(3000);
158
+
159
+ const window = manager.getCurrentWindow();
160
+ expect(window.tokensConsumed).toBe(8000);
161
+ });
162
+
163
+ test('should detect approaching token limit', async () => {
164
+ await manager.openWindow({ tokenLimit: 10000 });
165
+
166
+ expect(manager.isApproachingLimit()).toBe(false);
167
+
168
+ await manager.updateTokens(8600);
169
+ expect(manager.isApproachingLimit()).toBe(true);
170
+ });
171
+
172
+ test('should list all windows', async () => {
173
+ await manager.openWindow();
174
+ await manager.closeWindow();
175
+ await manager.openWindow();
176
+ await manager.closeWindow();
177
+ await manager.openWindow();
178
+
179
+ const windows = await manager.listWindows();
180
+ expect(windows.length).toBe(3);
181
+ expect(windows[0].sequence).toBe(1);
182
+ expect(windows[2].sequence).toBe(3);
183
+ });
184
+ });
185
+
186
+ describe('KnowledgeGraph Handoff Methods', () => {
187
+ const tempDir = path.join(__dirname, 'temp-kg-handoff-test');
188
+
189
+ beforeEach(async () => {
190
+ await fs.ensureDir(tempDir);
191
+ });
192
+
193
+ afterEach(async () => {
194
+ await fs.remove(tempDir);
195
+ });
196
+
197
+ test('should export to handoff format', () => {
198
+ const kg = new KnowledgeGraph(tempDir);
199
+ kg.add([
200
+ { type: 'tech_stack', content: 'React with TypeScript for the frontend', confidence: 90, source: 'q1' },
201
+ { type: 'architecture', content: 'Microservices with API gateway', confidence: 85, source: 'q2' },
202
+ { type: 'timeline', content: 'Some low confidence item', confidence: 50, source: 'q3' }
203
+ ]);
204
+
205
+ const handoff = kg.toHandoffFormat(70);
206
+ expect(handoff.highConfidenceItems.length).toBe(2);
207
+ expect(handoff.totalItems).toBe(3);
208
+ expect(handoff.typesSeen).toContain('tech_stack');
209
+ expect(handoff.typesSeen).toContain('architecture');
210
+ expect(handoff.typesSeen).toContain('timeline');
211
+ });
212
+
213
+ test('should truncate long content in handoff', () => {
214
+ const kg = new KnowledgeGraph(tempDir);
215
+ const longContent = 'A'.repeat(300);
216
+ kg.add([{ type: 'tech_stack', content: longContent, confidence: 90, source: 'q1' }]);
217
+
218
+ const handoff = kg.toHandoffFormat(70, 200);
219
+ expect(handoff.highConfidenceItems[0].content.length).toBeLessThanOrEqual(203); // 200 + '...'
220
+ });
221
+
222
+ test('should restore from handoff format', () => {
223
+ const kg = new KnowledgeGraph(tempDir);
224
+ kg.fromHandoffFormat({
225
+ highConfidenceItems: [
226
+ { type: 'tech_stack', content: 'React', confidence: 90, sources: ['q1'] },
227
+ { type: 'architecture', content: 'REST API', confidence: 85, sources: ['q2'] }
228
+ ],
229
+ totalItems: 2,
230
+ typesSeen: ['tech_stack', 'architecture']
231
+ });
232
+
233
+ expect(kg.has('tech_stack', 90)).toBe(true);
234
+ expect(kg.has('architecture', 85)).toBe(true);
235
+ expect(kg.getStats().totalItems).toBe(2);
236
+ });
237
+
238
+ test('should handle empty handoff data', () => {
239
+ const kg = new KnowledgeGraph(tempDir);
240
+ kg.fromHandoffFormat(null);
241
+ expect(kg.getStats().totalItems).toBe(0);
242
+
243
+ kg.fromHandoffFormat({});
244
+ expect(kg.getStats().totalItems).toBe(0);
245
+ });
246
+
247
+ test('should get high confidence snapshot', () => {
248
+ const kg = new KnowledgeGraph(tempDir);
249
+ kg.add([
250
+ { type: 'tech_stack', content: 'React', confidence: 95, source: 'q1' },
251
+ { type: 'tech_stack', content: 'Node.js', confidence: 70, source: 'q2' },
252
+ { type: 'architecture', content: 'Monolith', confidence: 85, source: 'q3' }
253
+ ]);
254
+
255
+ const snapshot = kg.getHighConfidenceSnapshot();
256
+ expect(snapshot.tech_stack.length).toBe(1); // Only 95 confidence one
257
+ expect(snapshot.architecture.length).toBe(1);
258
+ });
259
+ });
260
+
261
+ describe('DynamicPipeline Serialization', () => {
262
+ const tempDir = path.join(__dirname, 'temp-pipeline-handoff-test');
263
+
264
+ beforeEach(async () => {
265
+ await fs.ensureDir(tempDir);
266
+ });
267
+
268
+ afterEach(async () => {
269
+ await fs.remove(tempDir);
270
+ });
271
+
272
+ test('should serialize pipeline state', () => {
273
+ const pipeline = new DynamicPipeline(tempDir, null, { enabled: true });
274
+ pipeline.stats.questionsAsked = 5;
275
+ pipeline.stats.questionsSkipped = 2;
276
+
277
+ // Add some knowledge
278
+ pipeline.knowledgeGraph.add([
279
+ { type: 'tech_stack', content: 'React', confidence: 90, source: 'q1' }
280
+ ]);
281
+
282
+ const serialized = pipeline.serializeForHandoff();
283
+ expect(serialized.stats.questionsAsked).toBe(5);
284
+ expect(serialized.knowledgeGraph.highConfidenceItems.length).toBe(1);
285
+ });
286
+
287
+ test('should deserialize pipeline state', () => {
288
+ const pipeline = new DynamicPipeline(tempDir, null, { enabled: true });
289
+
290
+ pipeline.deserializeFromHandoff({
291
+ knowledgeGraph: {
292
+ highConfidenceItems: [
293
+ { type: 'tech_stack', content: 'Vue.js', confidence: 88, sources: ['q5'] }
294
+ ],
295
+ totalItems: 1,
296
+ typesSeen: ['tech_stack']
297
+ },
298
+ stats: { questionsAsked: 10, questionsSkipped: 3 }
299
+ });
300
+
301
+ expect(pipeline.knowledgeGraph.has('tech_stack', 88)).toBe(true);
302
+ expect(pipeline.stats.questionsAsked).toBe(10);
303
+ });
304
+
305
+ test('should handle null deserialization', () => {
306
+ const pipeline = new DynamicPipeline(tempDir, null, { enabled: true });
307
+ pipeline.deserializeFromHandoff(null);
308
+ expect(pipeline.stats.questionsAsked).toBe(0);
309
+ });
310
+ });