@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.
- package/.project/chats/current/SESSION-STATUS.md +29 -27
- package/.project/docs/ROADMAP.md +74 -64
- package/CHANGELOG.md +78 -0
- package/CLAUDE.md +1 -1
- package/README.md +63 -27
- package/bin/adf.js +54 -0
- package/lib/analysis/dynamic-pipeline.js +26 -0
- package/lib/analysis/knowledge-graph.js +66 -0
- package/lib/commands/deploy.js +35 -0
- package/lib/commands/harness.js +345 -0
- package/lib/commands/init.js +135 -10
- package/lib/frameworks/interviewer.js +130 -0
- package/lib/frameworks/progress-tracker.js +30 -1
- package/lib/frameworks/session-manager.js +76 -0
- package/lib/harness/context-window-manager.js +255 -0
- package/lib/harness/event-logger.js +115 -0
- package/lib/harness/feature-manifest.js +175 -0
- package/lib/harness/headless-adapter.js +184 -0
- package/lib/harness/milestone-tracker.js +183 -0
- package/lib/harness/protocol.js +503 -0
- package/lib/harness/provider-bridge.js +226 -0
- package/lib/harness/run-manager.js +267 -0
- package/lib/templates/scripts/analyze-docs.js +12 -1
- package/lib/utils/context-extractor.js +48 -0
- package/lib/utils/framework-detector.js +10 -1
- package/lib/utils/project-detector.js +5 -1
- package/lib/utils/tool-detector.js +167 -0
- package/lib/utils/tool-feature-registry.js +82 -13
- package/lib/utils/tool-recommender.js +325 -0
- package/package.json +1 -1
- package/tests/context-extractor.test.js +45 -0
- package/tests/framework-detector.test.js +28 -0
- package/tests/harness-backward-compat.test.js +251 -0
- package/tests/harness-context-window.test.js +310 -0
- package/tests/harness-event-logger.test.js +148 -0
- package/tests/harness-feature-manifest.test.js +124 -0
- package/tests/harness-headless-adapter.test.js +196 -0
- package/tests/harness-integration.test.js +207 -0
- package/tests/harness-milestone-tracker.test.js +158 -0
- package/tests/harness-protocol.test.js +341 -0
- package/tests/harness-provider-bridge.test.js +180 -0
- package/tests/harness-provider-switch.test.js +204 -0
- package/tests/harness-run-manager.test.js +131 -0
- package/tests/tool-detector.test.js +152 -0
- 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
|
+
});
|