@iservu-inc/adf-cli 0.4.36 → 0.5.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,332 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const DynamicPipeline = require('../lib/analysis/dynamic-pipeline');
5
+
6
+ describe('DynamicPipeline', () => {
7
+ let tempDir;
8
+ let pipeline;
9
+ let mockAIClient;
10
+
11
+ beforeEach(async () => {
12
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dp-test-'));
13
+
14
+ mockAIClient = {
15
+ sendMessage: jest.fn().mockResolvedValue(JSON.stringify([
16
+ {
17
+ type: 'tech_stack',
18
+ content: 'React and Node.js',
19
+ confidence: 90,
20
+ reasoning: 'Explicitly mentioned'
21
+ }
22
+ ]))
23
+ };
24
+
25
+ pipeline = new DynamicPipeline(tempDir, mockAIClient, {
26
+ enabled: true,
27
+ minSkipConfidence: 75,
28
+ showAnalysis: false,
29
+ verbose: false
30
+ });
31
+ });
32
+
33
+ afterEach(async () => {
34
+ await fs.remove(tempDir);
35
+ });
36
+
37
+ describe('initialize', () => {
38
+ it('should initialize without errors', async () => {
39
+ const loaded = await pipeline.initialize();
40
+ expect(loaded).toBe(false); // No existing data
41
+ });
42
+
43
+ it('should load existing knowledge graph', async () => {
44
+ // Create existing knowledge graph
45
+ const kgPath = path.join(tempDir, '_knowledge_graph.json');
46
+ await fs.writeJson(kgPath, {
47
+ savedAt: new Date().toISOString(),
48
+ knowledge: [
49
+ {
50
+ type: 'tech_stack',
51
+ items: [
52
+ {
53
+ type: 'tech_stack',
54
+ content: 'React',
55
+ confidence: 90,
56
+ sources: ['prev-q'],
57
+ addedAt: new Date().toISOString()
58
+ }
59
+ ]
60
+ }
61
+ ]
62
+ });
63
+
64
+ const loaded = await pipeline.initialize();
65
+ expect(loaded).toBe(true);
66
+ });
67
+ });
68
+
69
+ describe('processAnswer', () => {
70
+ it('should extract information from answer', async () => {
71
+ const result = await pipeline.processAnswer(
72
+ 'tech-stack',
73
+ 'What tech stack?',
74
+ 'I will use React for frontend and Node.js for backend'
75
+ );
76
+
77
+ expect(result.extracted.length).toBeGreaterThan(0);
78
+ expect(result.summary.length).toBeGreaterThan(0);
79
+ });
80
+
81
+ it('should update knowledge graph', async () => {
82
+ await pipeline.processAnswer(
83
+ 'tech-stack',
84
+ 'What tech stack?',
85
+ 'React and PostgreSQL'
86
+ );
87
+
88
+ const summary = pipeline.getKnowledgeSummary();
89
+ expect(summary.length).toBeGreaterThan(0);
90
+ });
91
+
92
+ it('should increment stats', async () => {
93
+ await pipeline.processAnswer(
94
+ 'q1',
95
+ 'Question 1?',
96
+ 'React and Node.js'
97
+ );
98
+
99
+ const stats = pipeline.getStats();
100
+ expect(stats.informationExtracted).toBeGreaterThan(0);
101
+ });
102
+
103
+ it('should handle errors gracefully', async () => {
104
+ mockAIClient.sendMessage.mockRejectedValue(new Error('AI error'));
105
+
106
+ const result = await pipeline.processAnswer(
107
+ 'q1',
108
+ 'Question?',
109
+ 'Answer'
110
+ );
111
+
112
+ // Should not throw, should return empty
113
+ expect(result.extracted).toBeDefined();
114
+ });
115
+
116
+ it('should work when disabled', async () => {
117
+ pipeline.setEnabled(false);
118
+
119
+ const result = await pipeline.processAnswer(
120
+ 'q1',
121
+ 'Question?',
122
+ 'Answer with React'
123
+ );
124
+
125
+ expect(result.extracted).toEqual([]);
126
+ expect(result.summary).toEqual([]);
127
+ });
128
+ });
129
+
130
+ describe('shouldSkipQuestion', () => {
131
+ it('should recommend skipping when knowledge exists', async () => {
132
+ // Add knowledge first
133
+ await pipeline.processAnswer(
134
+ 'tech-q',
135
+ 'What tech?',
136
+ 'React and Node.js and PostgreSQL'
137
+ );
138
+
139
+ // Check if we should skip tech stack question
140
+ const result = pipeline.shouldSkipQuestion({
141
+ id: 'tech-stack',
142
+ text: 'What tech stack will you use?'
143
+ });
144
+
145
+ expect(result.shouldSkip).toBe(true);
146
+ expect(result.reason).toBeTruthy();
147
+ expect(result.confidence).toBeGreaterThan(0);
148
+ });
149
+
150
+ it('should not skip when knowledge is missing', () => {
151
+ const result = pipeline.shouldSkipQuestion({
152
+ id: 'tech-stack',
153
+ text: 'What tech stack?'
154
+ });
155
+
156
+ expect(result.shouldSkip).toBe(false);
157
+ });
158
+
159
+ it('should increment stats correctly', async () => {
160
+ await pipeline.processAnswer('q1', 'Tech?', 'React');
161
+
162
+ pipeline.shouldSkipQuestion({ id: 'tech-stack', text: 'Tech stack?' }); // Skip
163
+ pipeline.shouldSkipQuestion({ id: 'timeline', text: 'Timeline?' }); // Don't skip
164
+
165
+ const stats = pipeline.getStats();
166
+ expect(stats.questionsSkipped).toBe(1);
167
+ expect(stats.questionsAsked).toBe(1);
168
+ expect(stats.timeSaved).toBeGreaterThan(0);
169
+ });
170
+
171
+ it('should work when disabled', () => {
172
+ pipeline.setEnabled(false);
173
+
174
+ const result = pipeline.shouldSkipQuestion({
175
+ id: 'q1',
176
+ text: 'Question?'
177
+ });
178
+
179
+ expect(result.shouldSkip).toBe(false);
180
+ expect(result.confidence).toBe(0);
181
+ });
182
+ });
183
+
184
+ describe('reorderQuestions', () => {
185
+ it('should reorder questions based on knowledge', async () => {
186
+ await pipeline.processAnswer('q1', 'Tech?', 'React and Node.js');
187
+
188
+ const questions = [
189
+ { id: 'tech-stack', text: 'What tech stack?' },
190
+ { id: 'timeline', text: 'Timeline?' },
191
+ { id: 'users', text: 'Who are users?' }
192
+ ];
193
+
194
+ const reordered = pipeline.reorderQuestions(questions);
195
+
196
+ expect(reordered).toHaveLength(3);
197
+ expect(reordered[0].question).toBeDefined();
198
+ expect(reordered[0].relevanceScore).toBeDefined();
199
+
200
+ // Tech stack should have lowest score (can skip)
201
+ const techItem = reordered.find(r => r.question.id === 'tech-stack');
202
+ expect(techItem.relevanceScore).toBeLessThan(50);
203
+ });
204
+
205
+ it('should work when disabled', () => {
206
+ pipeline.setEnabled(false);
207
+
208
+ const questions = [
209
+ { id: 'q1', text: 'Q1?' },
210
+ { id: 'q2', text: 'Q2?' }
211
+ ];
212
+
213
+ const reordered = pipeline.reorderQuestions(questions);
214
+
215
+ // Should return all with max score
216
+ expect(reordered.every(r => r.relevanceScore === 100)).toBe(true);
217
+ });
218
+ });
219
+
220
+ describe('getFilteringSummary', () => {
221
+ it('should return summary statistics', async () => {
222
+ await pipeline.processAnswer('q1', 'Tech?', 'React');
223
+
224
+ const questions = [
225
+ { id: 'tech-stack', text: 'Tech stack?' },
226
+ { id: 'timeline', text: 'Timeline?' }
227
+ ];
228
+
229
+ const summary = pipeline.getFilteringSummary(questions);
230
+
231
+ expect(summary.total).toBe(2);
232
+ expect(summary.canSkip).toBeGreaterThanOrEqual(0);
233
+ expect(summary.needed).toBeGreaterThanOrEqual(0);
234
+ expect(summary.estimatedTimeSaved).toBeGreaterThanOrEqual(0);
235
+ });
236
+
237
+ it('should work when disabled', () => {
238
+ pipeline.setEnabled(false);
239
+
240
+ const questions = [{ id: 'q1', text: 'Q1?' }];
241
+ const summary = pipeline.getFilteringSummary(questions);
242
+
243
+ expect(summary.total).toBe(1);
244
+ expect(summary.canSkip).toBe(0);
245
+ expect(summary.needed).toBe(1);
246
+ });
247
+ });
248
+
249
+ describe('getKnowledgeSummary', () => {
250
+ it('should return displayable knowledge summary', async () => {
251
+ await pipeline.processAnswer('q1', 'Tech?', 'React and Node.js');
252
+ await pipeline.processAnswer('q2', 'Platform?', 'Web application');
253
+
254
+ const summary = pipeline.getKnowledgeSummary();
255
+
256
+ expect(Array.isArray(summary)).toBe(true);
257
+ if (summary.length > 0) {
258
+ expect(summary[0]).toHaveProperty('type');
259
+ expect(summary[0]).toHaveProperty('icon');
260
+ expect(summary[0]).toHaveProperty('confidence');
261
+ }
262
+ });
263
+ });
264
+
265
+ describe('getStats', () => {
266
+ it('should return comprehensive statistics', async () => {
267
+ await pipeline.processAnswer('q1', 'Tech?', 'React');
268
+ pipeline.shouldSkipQuestion({ id: 'tech', text: 'Tech?' }); // Skip
269
+ pipeline.shouldSkipQuestion({ id: 'timeline', text: 'Timeline?' }); // Ask
270
+
271
+ const stats = pipeline.getStats();
272
+
273
+ expect(stats).toHaveProperty('questionsAsked');
274
+ expect(stats).toHaveProperty('questionsSkipped');
275
+ expect(stats).toHaveProperty('informationExtracted');
276
+ expect(stats).toHaveProperty('knowledgeItems');
277
+ expect(stats).toHaveProperty('timeSaved');
278
+
279
+ expect(stats.questionsSkipped).toBe(1);
280
+ expect(stats.questionsAsked).toBe(1);
281
+ });
282
+ });
283
+
284
+ describe('save', () => {
285
+ it('should save knowledge graph to disk', async () => {
286
+ await pipeline.processAnswer('q1', 'Tech?', 'React');
287
+ await pipeline.save();
288
+
289
+ const kgPath = path.join(tempDir, '_knowledge_graph.json');
290
+ expect(await fs.pathExists(kgPath)).toBe(true);
291
+ });
292
+ });
293
+
294
+ describe('enabled/disabled', () => {
295
+ it('should check if enabled', () => {
296
+ expect(pipeline.isEnabled()).toBe(true);
297
+
298
+ pipeline.setEnabled(false);
299
+ expect(pipeline.isEnabled()).toBe(false);
300
+ });
301
+
302
+ it('should respect enabled flag', async () => {
303
+ pipeline.setEnabled(false);
304
+
305
+ // All operations should be no-ops
306
+ const result = await pipeline.processAnswer('q1', 'Tech?', 'React');
307
+ expect(result.extracted).toEqual([]);
308
+
309
+ const skipResult = pipeline.shouldSkipQuestion({ id: 'q1', text: 'Q?' });
310
+ expect(skipResult.shouldSkip).toBe(false);
311
+ });
312
+ });
313
+
314
+ describe('without AI client', () => {
315
+ it('should work without AI client using heuristics', async () => {
316
+ const pipelineWithoutAI = new DynamicPipeline(tempDir, null, {
317
+ enabled: true
318
+ });
319
+
320
+ await pipelineWithoutAI.initialize();
321
+
322
+ const result = await pipelineWithoutAI.processAnswer(
323
+ 'tech',
324
+ 'What tech?',
325
+ 'React and Node.js and PostgreSQL'
326
+ );
327
+
328
+ // Should still extract information using heuristics
329
+ expect(result.extracted.length).toBeGreaterThan(0);
330
+ });
331
+ });
332
+ });
@@ -0,0 +1,322 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const KnowledgeGraph = require('../lib/analysis/knowledge-graph');
5
+
6
+ describe('KnowledgeGraph', () => {
7
+ let tempDir;
8
+ let kg;
9
+
10
+ beforeEach(async () => {
11
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'kg-test-'));
12
+ kg = new KnowledgeGraph(tempDir);
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await fs.remove(tempDir);
17
+ });
18
+
19
+ describe('add', () => {
20
+ it('should add new information', () => {
21
+ const extractedInfo = [
22
+ {
23
+ type: 'tech_stack',
24
+ content: 'React and Node.js',
25
+ confidence: 90,
26
+ source: 'tech-question'
27
+ }
28
+ ];
29
+
30
+ kg.add(extractedInfo);
31
+
32
+ expect(kg.has('tech_stack', 70)).toBe(true);
33
+ expect(kg.getConfidence('tech_stack')).toBe(90);
34
+ });
35
+
36
+ it('should update existing information with higher confidence', () => {
37
+ const info1 = [
38
+ { type: 'tech_stack', content: 'React for frontend development', confidence: 70, source: 'q1' }
39
+ ];
40
+ const info2 = [
41
+ { type: 'tech_stack', content: 'React for the frontend', confidence: 90, source: 'q2' }
42
+ ];
43
+
44
+ kg.add(info1);
45
+ kg.add(info2);
46
+
47
+ expect(kg.getConfidence('tech_stack')).toBe(90);
48
+ const items = kg.get('tech_stack');
49
+ // Should have merged or both sources tracked
50
+ const allSources = items.flatMap(i => i.sources);
51
+ expect(allSources).toContain('q1');
52
+ expect(allSources).toContain('q2');
53
+ });
54
+
55
+ it('should not replace existing information with lower confidence', () => {
56
+ const info1 = [
57
+ { type: 'tech_stack', content: 'React and Node', confidence: 90, source: 'q1' }
58
+ ];
59
+ const info2 = [
60
+ { type: 'tech_stack', content: 'React', confidence: 70, source: 'q2' }
61
+ ];
62
+
63
+ kg.add(info1);
64
+ kg.add(info2);
65
+
66
+ expect(kg.getConfidence('tech_stack')).toBe(90);
67
+ const items = kg.get('tech_stack');
68
+ expect(items[0].content).toContain('Node');
69
+ });
70
+
71
+ it('should add dissimilar information as separate items', () => {
72
+ const info1 = [
73
+ { type: 'features', content: 'User authentication', confidence: 85, source: 'q1' }
74
+ ];
75
+ const info2 = [
76
+ { type: 'features', content: 'Data export to CSV', confidence: 80, source: 'q2' }
77
+ ];
78
+
79
+ kg.add(info1);
80
+ kg.add(info2);
81
+
82
+ const items = kg.get('features');
83
+ expect(items).toHaveLength(2);
84
+ });
85
+
86
+ it('should merge similar information', () => {
87
+ const info1 = [
88
+ { type: 'tech_stack', content: 'React for frontend', confidence: 85, source: 'q1' }
89
+ ];
90
+ const info2 = [
91
+ { type: 'tech_stack', content: 'React for the frontend', confidence: 80, source: 'q2' }
92
+ ];
93
+
94
+ kg.add(info1);
95
+ kg.add(info2);
96
+
97
+ const items = kg.get('tech_stack');
98
+ // Should merge because very similar
99
+ expect(items.length).toBeLessThanOrEqual(2);
100
+ });
101
+ });
102
+
103
+ describe('has', () => {
104
+ it('should return true if information exists with sufficient confidence', () => {
105
+ const info = [
106
+ { type: 'project_goal', content: 'Build a CRM', confidence: 85, source: 'q1' }
107
+ ];
108
+
109
+ kg.add(info);
110
+
111
+ expect(kg.has('project_goal', 80)).toBe(true);
112
+ expect(kg.has('project_goal', 90)).toBe(false);
113
+ });
114
+
115
+ it('should return false for missing information types', () => {
116
+ expect(kg.has('tech_stack')).toBe(false);
117
+ });
118
+
119
+ it('should use default minConfidence of 70', () => {
120
+ const info = [
121
+ { type: 'platform', content: 'Web', confidence: 75, source: 'q1' }
122
+ ];
123
+
124
+ kg.add(info);
125
+
126
+ expect(kg.has('platform')).toBe(true);
127
+ });
128
+ });
129
+
130
+ describe('get', () => {
131
+ it('should return all items of a type', () => {
132
+ const info = [
133
+ { type: 'features', content: 'Auth', confidence: 85, source: 'q1' },
134
+ { type: 'features', content: 'Export', confidence: 80, source: 'q2' }
135
+ ];
136
+
137
+ kg.add(info);
138
+
139
+ const items = kg.get('features');
140
+ expect(items).toHaveLength(2);
141
+ });
142
+
143
+ it('should return empty array for non-existent type', () => {
144
+ const items = kg.get('nonexistent');
145
+ expect(items).toEqual([]);
146
+ });
147
+ });
148
+
149
+ describe('getConfidence', () => {
150
+ it('should return highest confidence for a type', () => {
151
+ const info = [
152
+ { type: 'tech_stack', content: 'React', confidence: 80, source: 'q1' },
153
+ { type: 'tech_stack', content: 'Node', confidence: 90, source: 'q2' }
154
+ ];
155
+
156
+ kg.add(info);
157
+
158
+ expect(kg.getConfidence('tech_stack')).toBe(90);
159
+ });
160
+
161
+ it('should return 0 for non-existent type', () => {
162
+ expect(kg.getConfidence('nonexistent')).toBe(0);
163
+ });
164
+ });
165
+
166
+ describe('getSummary', () => {
167
+ it('should return summary of all knowledge', () => {
168
+ const info = [
169
+ { type: 'tech_stack', content: 'React', confidence: 90, source: 'q1' },
170
+ { type: 'architecture', content: 'SPA', confidence: 85, source: 'q2' },
171
+ { type: 'platform', content: 'Web', confidence: 80, source: 'q3' }
172
+ ];
173
+
174
+ kg.add(info);
175
+
176
+ const summary = kg.getSummary();
177
+
178
+ expect(summary.tech_stack.maxConfidence).toBe(90);
179
+ expect(summary.architecture.maxConfidence).toBe(85);
180
+ expect(summary.platform.maxConfidence).toBe(80);
181
+ expect(summary.tech_stack.sources).toContain('q1');
182
+ });
183
+ });
184
+
185
+ describe('getDisplaySummary', () => {
186
+ it('should return displayable summary with icons', () => {
187
+ const info = [
188
+ { type: 'tech_stack', content: 'React', confidence: 90, source: 'q1' },
189
+ { type: 'project_goal', content: 'Build CRM', confidence: 95, source: 'q2' }
190
+ ];
191
+
192
+ kg.add(info);
193
+
194
+ const display = kg.getDisplaySummary();
195
+
196
+ expect(display.length).toBe(2);
197
+ expect(display[0].confidence).toBe(95); // Sorted by confidence
198
+ expect(display[0].icon).toBe('🎯');
199
+ expect(display[1].icon).toBe('🔧');
200
+ });
201
+
202
+ it('should filter out low confidence items', () => {
203
+ const info = [
204
+ { type: 'tech_stack', content: 'React', confidence: 90, source: 'q1' },
205
+ { type: 'timeline', content: 'Soon', confidence: 50, source: 'q2' }
206
+ ];
207
+
208
+ kg.add(info);
209
+
210
+ const display = kg.getDisplaySummary();
211
+
212
+ // Should only include tech_stack (90% > 60%)
213
+ expect(display).toHaveLength(1);
214
+ expect(display[0].type).toBe('tech stack');
215
+ });
216
+ });
217
+
218
+ describe('calculateSimilarity', () => {
219
+ it('should calculate high similarity for similar texts', () => {
220
+ const similarity = kg.calculateSimilarity(
221
+ 'React for frontend development',
222
+ 'React for the frontend'
223
+ );
224
+
225
+ expect(similarity).toBeGreaterThan(0.5);
226
+ });
227
+
228
+ it('should calculate low similarity for different texts', () => {
229
+ const similarity = kg.calculateSimilarity(
230
+ 'React for frontend',
231
+ 'PostgreSQL database'
232
+ );
233
+
234
+ expect(similarity).toBeLessThan(0.5);
235
+ });
236
+
237
+ it('should return 1 for identical texts', () => {
238
+ const similarity = kg.calculateSimilarity(
239
+ 'same text',
240
+ 'same text'
241
+ );
242
+
243
+ expect(similarity).toBe(1);
244
+ });
245
+ });
246
+
247
+ describe('save and load', () => {
248
+ it('should save knowledge graph to disk', async () => {
249
+ const info = [
250
+ { type: 'tech_stack', content: 'React', confidence: 90, source: 'q1' }
251
+ ];
252
+
253
+ kg.add(info);
254
+ await kg.save();
255
+
256
+ const filePath = path.join(tempDir, '_knowledge_graph.json');
257
+ expect(await fs.pathExists(filePath)).toBe(true);
258
+
259
+ const data = await fs.readJson(filePath);
260
+ expect(data.knowledge).toBeDefined();
261
+ expect(data.knowledge.length).toBeGreaterThan(0);
262
+ });
263
+
264
+ it('should load knowledge graph from disk', async () => {
265
+ const info = [
266
+ { type: 'tech_stack', content: 'React', confidence: 90, source: 'q1' },
267
+ { type: 'platform', content: 'Web', confidence: 85, source: 'q2' }
268
+ ];
269
+
270
+ kg.add(info);
271
+ await kg.save();
272
+
273
+ // Create new instance and load
274
+ const kg2 = new KnowledgeGraph(tempDir);
275
+ const loaded = await kg2.load();
276
+
277
+ expect(loaded).toBe(true);
278
+ expect(kg2.has('tech_stack')).toBe(true);
279
+ expect(kg2.has('platform')).toBe(true);
280
+ expect(kg2.getConfidence('tech_stack')).toBe(90);
281
+ });
282
+
283
+ it('should return false when loading from non-existent file', async () => {
284
+ const loaded = await kg.load();
285
+ expect(loaded).toBe(false);
286
+ });
287
+ });
288
+
289
+ describe('getStats', () => {
290
+ it('should return statistics about knowledge graph', () => {
291
+ const info = [
292
+ { type: 'tech_stack', content: 'React', confidence: 90, source: 'q1' },
293
+ { type: 'tech_stack', content: 'Node', confidence: 85, source: 'q2' },
294
+ { type: 'platform', content: 'Web', confidence: 75, source: 'q3' },
295
+ { type: 'architecture', content: 'SPA', confidence: 80, source: 'q4' }
296
+ ];
297
+
298
+ kg.add(info);
299
+
300
+ const stats = kg.getStats();
301
+
302
+ expect(stats.totalItems).toBe(4);
303
+ expect(stats.highConfidenceItems).toBe(3); // 90, 85, 80 are >= 80
304
+ expect(stats.types).toBe(3); // tech_stack, platform, architecture
305
+ expect(stats.typeList).toContain('tech_stack');
306
+ });
307
+ });
308
+
309
+ describe('clear', () => {
310
+ it('should clear all knowledge', () => {
311
+ const info = [
312
+ { type: 'tech_stack', content: 'React', confidence: 90, source: 'q1' }
313
+ ];
314
+
315
+ kg.add(info);
316
+ expect(kg.has('tech_stack')).toBe(true);
317
+
318
+ kg.clear();
319
+ expect(kg.has('tech_stack')).toBe(false);
320
+ });
321
+ });
322
+ });