@iservu-inc/adf-cli 0.4.36 → 0.5.1
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/.project/chats/current/2025-10-05_INTELLIGENT-ANSWER-ANALYSIS.md +649 -0
- package/.project/chats/current/2025-10-05_MULTI-IDE-IMPROVEMENTS.md +415 -0
- package/.project/chats/current/SESSION-STATUS.md +166 -71
- package/.project/docs/ROADMAP.md +150 -17
- package/CHANGELOG.md +243 -0
- package/lib/analysis/answer-analyzer.js +304 -0
- package/lib/analysis/dynamic-pipeline.js +262 -0
- package/lib/analysis/knowledge-graph.js +227 -0
- package/lib/analysis/question-mapper.js +293 -0
- package/lib/commands/init.js +137 -8
- package/lib/frameworks/interviewer.js +59 -0
- package/package.json +1 -1
- package/tests/answer-analyzer.test.js +262 -0
- package/tests/dynamic-pipeline.test.js +332 -0
- package/tests/knowledge-graph.test.js +322 -0
- package/tests/question-mapper.test.js +342 -0
- /package/.project/chats/{current → complete}/2025-10-04_CRITICAL-MODEL-FETCHING-BUG.md +0 -0
- /package/.project/chats/{current → complete}/2025-10-04_PHASE-4-2-COMPLETION-AND-ROADMAP.md +0 -0
- /package/.project/chats/{current → complete}/2025-10-04_PHASE-4-2-LEARNING-SYSTEM.md +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
const AnswerAnalyzer = require('../lib/analysis/answer-analyzer');
|
|
2
|
+
|
|
3
|
+
describe('AnswerAnalyzer', () => {
|
|
4
|
+
let analyzer;
|
|
5
|
+
let mockAIClient;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockAIClient = {
|
|
9
|
+
sendMessage: jest.fn()
|
|
10
|
+
};
|
|
11
|
+
analyzer = new AnswerAnalyzer(mockAIClient);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('heuristicExtraction', () => {
|
|
15
|
+
it('should extract tech stack from answer', () => {
|
|
16
|
+
const answer = 'I am building a React frontend with PostgreSQL database and Node.js backend';
|
|
17
|
+
const questionId = 'what-building';
|
|
18
|
+
|
|
19
|
+
const extracted = analyzer.heuristicExtraction(answer, questionId);
|
|
20
|
+
|
|
21
|
+
const techStack = extracted.find(e => e.type === 'tech_stack');
|
|
22
|
+
expect(techStack).toBeDefined();
|
|
23
|
+
expect(techStack.extractedTerms).toContain('react');
|
|
24
|
+
expect(techStack.extractedTerms).toContain('postgresql');
|
|
25
|
+
expect(techStack.extractedTerms).toContain('node');
|
|
26
|
+
expect(techStack.confidence).toBeGreaterThan(60);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should extract architecture patterns', () => {
|
|
30
|
+
const answer = 'I will build a frontend with a separate backend API using microservices';
|
|
31
|
+
const questionId = 'architecture';
|
|
32
|
+
|
|
33
|
+
const extracted = analyzer.heuristicExtraction(answer, questionId);
|
|
34
|
+
|
|
35
|
+
const frontendBackend = extracted.find(e =>
|
|
36
|
+
e.type === 'architecture' && e.pattern === 'frontend-backend-separation'
|
|
37
|
+
);
|
|
38
|
+
const microservices = extracted.find(e =>
|
|
39
|
+
e.type === 'architecture' && e.pattern === 'microservices'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(frontendBackend).toBeDefined();
|
|
43
|
+
expect(microservices).toBeDefined();
|
|
44
|
+
expect(frontendBackend.confidence).toBeGreaterThan(70);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should extract platform information', () => {
|
|
48
|
+
const answer = 'This is a mobile app for iOS and Android using React Native';
|
|
49
|
+
const questionId = 'platform';
|
|
50
|
+
|
|
51
|
+
const extracted = analyzer.heuristicExtraction(answer, questionId);
|
|
52
|
+
|
|
53
|
+
const platform = extracted.find(e => e.type === 'platform');
|
|
54
|
+
expect(platform).toBeDefined();
|
|
55
|
+
expect(platform.platform).toBe('mobile');
|
|
56
|
+
expect(platform.confidence).toBeGreaterThan(70);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should extract project goal from goal questions', () => {
|
|
60
|
+
const answer = 'Create a social media platform for developers to share code snippets';
|
|
61
|
+
const questionId = 'what-is-goal';
|
|
62
|
+
|
|
63
|
+
const extracted = analyzer.heuristicExtraction(answer, questionId);
|
|
64
|
+
|
|
65
|
+
const goal = extracted.find(e => e.type === 'project_goal');
|
|
66
|
+
expect(goal).toBeDefined();
|
|
67
|
+
expect(goal.confidence).toBe(95);
|
|
68
|
+
expect(goal.content).toBe(answer);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should extract timeline information', () => {
|
|
72
|
+
const answer = 'I need this completed in 3 weeks for a product launch';
|
|
73
|
+
const questionId = 'timeline';
|
|
74
|
+
|
|
75
|
+
const extracted = analyzer.heuristicExtraction(answer, questionId);
|
|
76
|
+
|
|
77
|
+
const timeline = extracted.find(e => e.type === 'timeline');
|
|
78
|
+
expect(timeline).toBeDefined();
|
|
79
|
+
expect(timeline.confidence).toBeGreaterThan(70);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should extract target users', () => {
|
|
83
|
+
const answer = 'The target audience is small businesses and their customers';
|
|
84
|
+
const questionId = 'users';
|
|
85
|
+
|
|
86
|
+
const extracted = analyzer.heuristicExtraction(answer, questionId);
|
|
87
|
+
|
|
88
|
+
const users = extracted.find(e => e.type === 'target_users');
|
|
89
|
+
expect(users).toBeDefined();
|
|
90
|
+
expect(users.confidence).toBe(70);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should return empty array for uninformative answers', () => {
|
|
94
|
+
const answer = 'yes';
|
|
95
|
+
const questionId = 'question';
|
|
96
|
+
|
|
97
|
+
const extracted = analyzer.heuristicExtraction(answer, questionId);
|
|
98
|
+
|
|
99
|
+
expect(extracted).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should handle multiple tech mentions with higher confidence', () => {
|
|
103
|
+
const answer = 'Using React, Next.js, TypeScript, PostgreSQL, Redis, and AWS';
|
|
104
|
+
const questionId = 'tech';
|
|
105
|
+
|
|
106
|
+
const extracted = analyzer.heuristicExtraction(answer, questionId);
|
|
107
|
+
|
|
108
|
+
const techStack = extracted.find(e => e.type === 'tech_stack');
|
|
109
|
+
expect(techStack).toBeDefined();
|
|
110
|
+
expect(techStack.extractedTerms.length).toBeGreaterThanOrEqual(5);
|
|
111
|
+
expect(techStack.confidence).toBeGreaterThan(80);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('aiExtraction', () => {
|
|
116
|
+
it('should extract information using AI when available', async () => {
|
|
117
|
+
const mockAIResponse = JSON.stringify([
|
|
118
|
+
{
|
|
119
|
+
type: 'tech_stack',
|
|
120
|
+
content: 'React and PostgreSQL',
|
|
121
|
+
confidence: 95,
|
|
122
|
+
reasoning: 'Explicitly mentioned'
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
type: 'architecture',
|
|
126
|
+
content: 'separate backend API',
|
|
127
|
+
confidence: 85,
|
|
128
|
+
reasoning: 'Implies frontend-backend separation'
|
|
129
|
+
}
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
mockAIClient.sendMessage.mockResolvedValue(mockAIResponse);
|
|
133
|
+
|
|
134
|
+
const extracted = await analyzer.aiExtraction(
|
|
135
|
+
'What tech stack?',
|
|
136
|
+
'React frontend with separate backend API using PostgreSQL',
|
|
137
|
+
'tech-stack'
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(mockAIClient.sendMessage).toHaveBeenCalled();
|
|
141
|
+
expect(extracted).toHaveLength(2);
|
|
142
|
+
expect(extracted[0].type).toBe('tech_stack');
|
|
143
|
+
expect(extracted[0].method).toBe('ai');
|
|
144
|
+
expect(extracted[0].source).toBe('tech-stack');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should handle AI extraction errors gracefully', async () => {
|
|
148
|
+
mockAIClient.sendMessage.mockRejectedValue(new Error('AI error'));
|
|
149
|
+
|
|
150
|
+
const extracted = await analyzer.aiExtraction(
|
|
151
|
+
'What are you building?',
|
|
152
|
+
'A web app',
|
|
153
|
+
'goal'
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(extracted).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should handle invalid JSON from AI', async () => {
|
|
160
|
+
mockAIClient.sendMessage.mockResolvedValue('This is not JSON');
|
|
161
|
+
|
|
162
|
+
const extracted = await analyzer.aiExtraction(
|
|
163
|
+
'What tech?',
|
|
164
|
+
'React and Node',
|
|
165
|
+
'tech'
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(extracted).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('analyzeAnswer', () => {
|
|
173
|
+
it('should combine heuristic and AI results', async () => {
|
|
174
|
+
const mockAIResponse = JSON.stringify([
|
|
175
|
+
{
|
|
176
|
+
type: 'tech_stack',
|
|
177
|
+
content: 'React and Node.js',
|
|
178
|
+
confidence: 90,
|
|
179
|
+
reasoning: 'Explicitly stated'
|
|
180
|
+
}
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
mockAIClient.sendMessage.mockResolvedValue(mockAIResponse);
|
|
184
|
+
|
|
185
|
+
const extracted = await analyzer.analyzeAnswer(
|
|
186
|
+
'What tech stack?',
|
|
187
|
+
'I will use React for frontend and Node.js for backend',
|
|
188
|
+
'tech-stack'
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(extracted.length).toBeGreaterThan(0);
|
|
192
|
+
// Should have at least tech_stack from both methods
|
|
193
|
+
const techStackItems = extracted.filter(e => e.type === 'tech_stack');
|
|
194
|
+
expect(techStackItems.length).toBeGreaterThanOrEqual(1);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should use AI result if confidence is higher', async () => {
|
|
198
|
+
const mockAIResponse = JSON.stringify([
|
|
199
|
+
{
|
|
200
|
+
type: 'tech_stack',
|
|
201
|
+
content: 'Full tech stack analysis',
|
|
202
|
+
confidence: 95,
|
|
203
|
+
reasoning: 'Comprehensive analysis'
|
|
204
|
+
}
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
mockAIClient.sendMessage.mockResolvedValue(mockAIResponse);
|
|
208
|
+
|
|
209
|
+
const extracted = await analyzer.analyzeAnswer(
|
|
210
|
+
'What tech?',
|
|
211
|
+
'React and Node',
|
|
212
|
+
'tech'
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const techStack = extracted.find(e => e.type === 'tech_stack');
|
|
216
|
+
expect(techStack.confidence).toBeGreaterThanOrEqual(85);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should work without AI client', async () => {
|
|
220
|
+
const analyzerWithoutAI = new AnswerAnalyzer(null);
|
|
221
|
+
|
|
222
|
+
const extracted = await analyzerWithoutAI.analyzeAnswer(
|
|
223
|
+
'What are you building?',
|
|
224
|
+
'A React web application with PostgreSQL database',
|
|
225
|
+
'goal'
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
expect(extracted.length).toBeGreaterThan(0);
|
|
229
|
+
const techStack = extracted.find(e => e.type === 'tech_stack');
|
|
230
|
+
expect(techStack).toBeDefined();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('getSummary', () => {
|
|
235
|
+
it('should generate human-readable summary', () => {
|
|
236
|
+
const extractedInfo = [
|
|
237
|
+
{ type: 'tech_stack', confidence: 90 },
|
|
238
|
+
{ type: 'architecture', confidence: 85 },
|
|
239
|
+
{ type: 'project_goal', confidence: 95 }
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
const summary = analyzer.getSummary(extractedInfo);
|
|
243
|
+
|
|
244
|
+
expect(summary).toContain('TECH STACK: 90% confidence');
|
|
245
|
+
expect(summary).toContain('ARCHITECTURE: 85% confidence');
|
|
246
|
+
expect(summary).toContain('PROJECT GOAL: 95% confidence');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should handle multiple items of same type', () => {
|
|
250
|
+
const extractedInfo = [
|
|
251
|
+
{ type: 'tech_stack', confidence: 90 },
|
|
252
|
+
{ type: 'tech_stack', confidence: 80 },
|
|
253
|
+
{ type: 'architecture', confidence: 85 }
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
const summary = analyzer.getSummary(extractedInfo);
|
|
257
|
+
|
|
258
|
+
// Should show highest confidence
|
|
259
|
+
expect(summary).toContain('TECH STACK: 90% confidence');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -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
|
+
});
|