@iservu-inc/adf-cli 0.1.6 → 0.3.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/complete/2025-10-03_ADF-CLI-QUALITY-BASED-PROGRESS-AND-RESUME.md +399 -0
- package/.project/chats/current/2025-10-03_AGENTS-MD-AND-TOOL-GENERATORS.md +699 -0
- package/.project/docs/architecture/SYSTEM-DESIGN.md +369 -0
- package/.project/docs/frameworks/FRAMEWORK-METHODOLOGIES.md +449 -0
- package/.project/docs/goals/PROJECT-VISION.md +112 -0
- package/.project/docs/tool-integrations/IDE-CUSTOMIZATIONS.md +578 -0
- package/.project/docs/tool-integrations/RESEARCH-FINDINGS.md +828 -0
- package/CHANGELOG.md +292 -0
- package/jest.config.js +20 -0
- package/lib/commands/deploy.js +122 -3
- package/lib/commands/init.js +41 -113
- package/lib/frameworks/answer-quality-analyzer.js +216 -0
- package/lib/frameworks/interviewer.js +447 -0
- package/lib/frameworks/output-generators.js +345 -0
- package/lib/frameworks/progress-tracker.js +239 -0
- package/lib/frameworks/questions.js +664 -0
- package/lib/frameworks/session-manager.js +100 -0
- package/lib/generators/agents-md-generator.js +388 -0
- package/lib/generators/cursor-generator.js +374 -0
- package/lib/generators/index.js +98 -0
- package/lib/generators/tool-config-generator.js +188 -0
- package/lib/generators/vscode-generator.js +403 -0
- package/lib/generators/windsurf-generator.js +596 -0
- package/package.json +10 -5
- package/tests/agents-md-generator.test.js +245 -0
- package/tests/answer-quality-analyzer.test.js +173 -0
- package/tests/cursor-generator.test.js +326 -0
- package/tests/progress-tracker.test.js +205 -0
- package/tests/session-manager.test.js +162 -0
- package/tests/vscode-generator.test.js +436 -0
- package/tests/windsurf-generator.test.js +320 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const CursorGenerator = require('../lib/generators/cursor-generator');
|
|
4
|
+
|
|
5
|
+
const TEST_PROJECT_PATH = path.join(__dirname, 'test-project-cursor');
|
|
6
|
+
const TEST_SESSION_PATH = path.join(TEST_PROJECT_PATH, '.adf', 'sessions', 'test-session');
|
|
7
|
+
|
|
8
|
+
describe('CursorGenerator', () => {
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
// Clean up test directories
|
|
11
|
+
await fs.remove(TEST_PROJECT_PATH);
|
|
12
|
+
await fs.ensureDir(TEST_PROJECT_PATH);
|
|
13
|
+
await fs.ensureDir(TEST_SESSION_PATH);
|
|
14
|
+
await fs.ensureDir(path.join(TEST_SESSION_PATH, 'outputs'));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
// Clean up after tests
|
|
19
|
+
await fs.remove(TEST_PROJECT_PATH);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('PRP Framework', () => {
|
|
23
|
+
it('should generate Cursor configurations from PRP output', async () => {
|
|
24
|
+
// Create mock PRP output
|
|
25
|
+
const prpContent = `# Product Requirement Prompt (PRP)
|
|
26
|
+
|
|
27
|
+
## 1. Goal Definition
|
|
28
|
+
Build a React dashboard that displays real-time analytics from PostgreSQL database.
|
|
29
|
+
|
|
30
|
+
## 2. Business Justification
|
|
31
|
+
This will help users make data-driven decisions and improve productivity.
|
|
32
|
+
|
|
33
|
+
## 3. Contextual Intelligence
|
|
34
|
+
### Technology Stack
|
|
35
|
+
- Frontend: React 18, TypeScript
|
|
36
|
+
- Backend: Node.js, Express
|
|
37
|
+
- Database: PostgreSQL
|
|
38
|
+
|
|
39
|
+
## 4. Implementation Blueprint
|
|
40
|
+
### File Structure
|
|
41
|
+
- src/components/Dashboard/
|
|
42
|
+
- src/api/analytics/
|
|
43
|
+
|
|
44
|
+
### Core Logic
|
|
45
|
+
1. Fetch data from analytics API
|
|
46
|
+
2. Process and aggregate
|
|
47
|
+
3. Render charts
|
|
48
|
+
|
|
49
|
+
## 5. Validation
|
|
50
|
+
### Success Criteria
|
|
51
|
+
- Dashboard loads in <2s
|
|
52
|
+
- All charts render correctly
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
await fs.writeFile(
|
|
56
|
+
path.join(TEST_SESSION_PATH, 'outputs', 'prp.md'),
|
|
57
|
+
prpContent,
|
|
58
|
+
'utf-8'
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Create metadata
|
|
62
|
+
await fs.writeJson(path.join(TEST_SESSION_PATH, '_metadata.json'), {
|
|
63
|
+
framework: 'rapid',
|
|
64
|
+
projectName: 'Test Analytics Dashboard'
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Generate Cursor configs
|
|
68
|
+
const generator = new CursorGenerator(TEST_SESSION_PATH, TEST_PROJECT_PATH, 'rapid');
|
|
69
|
+
const generated = await generator.generate();
|
|
70
|
+
|
|
71
|
+
// Verify .cursor/rules was created
|
|
72
|
+
const rulesPath = path.join(TEST_PROJECT_PATH, '.cursor', 'rules');
|
|
73
|
+
expect(await fs.pathExists(rulesPath)).toBe(true);
|
|
74
|
+
|
|
75
|
+
const rulesContent = await fs.readFile(rulesPath, 'utf-8');
|
|
76
|
+
expect(rulesContent).toContain('Test Analytics Dashboard');
|
|
77
|
+
expect(rulesContent).toContain('Cursor Rules');
|
|
78
|
+
expect(rulesContent).toContain('Project Goal');
|
|
79
|
+
expect(rulesContent).toContain('React dashboard');
|
|
80
|
+
expect(rulesContent).toContain('Tech Stack');
|
|
81
|
+
expect(rulesContent).toContain('Before Implementing Features');
|
|
82
|
+
expect(rulesContent).toContain('.adf/sessions/test-session/outputs/prp.md');
|
|
83
|
+
expect(rulesContent).toContain('Code Standards');
|
|
84
|
+
expect(rulesContent).toContain('What to Avoid');
|
|
85
|
+
|
|
86
|
+
// Verify .cursorrules deprecation notice was created
|
|
87
|
+
const legacyPath = path.join(TEST_PROJECT_PATH, '.cursorrules');
|
|
88
|
+
expect(await fs.pathExists(legacyPath)).toBe(true);
|
|
89
|
+
|
|
90
|
+
const legacyContent = await fs.readFile(legacyPath, 'utf-8');
|
|
91
|
+
expect(legacyContent).toContain('DEPRECATED');
|
|
92
|
+
expect(legacyContent).toContain('.cursor/rules');
|
|
93
|
+
expect(legacyContent).toContain('Migration');
|
|
94
|
+
|
|
95
|
+
// Verify generated structure
|
|
96
|
+
expect(generated.rules).toHaveLength(1);
|
|
97
|
+
expect(generated.legacy).toHaveLength(1);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('Balanced Framework', () => {
|
|
102
|
+
it('should generate Cursor configurations from Balanced outputs', async () => {
|
|
103
|
+
// Create mock outputs
|
|
104
|
+
const constitutionContent = `# Constitution
|
|
105
|
+
|
|
106
|
+
## Core Principles
|
|
107
|
+
1. User privacy is paramount
|
|
108
|
+
2. Performance over features
|
|
109
|
+
|
|
110
|
+
## Constraints
|
|
111
|
+
- No third-party analytics
|
|
112
|
+
- WCAG 2.1 AA compliance
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
const specificationContent = `# Specification
|
|
116
|
+
|
|
117
|
+
## Overview
|
|
118
|
+
A comprehensive user management system.
|
|
119
|
+
|
|
120
|
+
## Architecture
|
|
121
|
+
Microservices architecture with API gateway.
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
const planContent = `# Technical Plan
|
|
125
|
+
|
|
126
|
+
## Technology Stack
|
|
127
|
+
- React 18
|
|
128
|
+
- Node.js 20
|
|
129
|
+
- PostgreSQL 15
|
|
130
|
+
|
|
131
|
+
## Code Style
|
|
132
|
+
- Use TypeScript strict mode
|
|
133
|
+
- Follow Airbnb style guide
|
|
134
|
+
|
|
135
|
+
## Coding Standards
|
|
136
|
+
- Write tests first
|
|
137
|
+
- Document public APIs
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
await fs.writeFile(
|
|
141
|
+
path.join(TEST_SESSION_PATH, 'outputs', 'constitution.md'),
|
|
142
|
+
constitutionContent,
|
|
143
|
+
'utf-8'
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
await fs.writeFile(
|
|
147
|
+
path.join(TEST_SESSION_PATH, 'outputs', 'specification.md'),
|
|
148
|
+
specificationContent,
|
|
149
|
+
'utf-8'
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
await fs.writeFile(
|
|
153
|
+
path.join(TEST_SESSION_PATH, 'outputs', 'plan.md'),
|
|
154
|
+
planContent,
|
|
155
|
+
'utf-8'
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
await fs.writeJson(path.join(TEST_SESSION_PATH, '_metadata.json'), {
|
|
159
|
+
framework: 'balanced',
|
|
160
|
+
projectName: 'User Management System'
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Generate Cursor configs
|
|
164
|
+
const generator = new CursorGenerator(TEST_SESSION_PATH, TEST_PROJECT_PATH, 'balanced');
|
|
165
|
+
await generator.generate();
|
|
166
|
+
|
|
167
|
+
// Verify .cursor/rules
|
|
168
|
+
const rulesPath = path.join(TEST_PROJECT_PATH, '.cursor', 'rules');
|
|
169
|
+
const rulesContent = await fs.readFile(rulesPath, 'utf-8');
|
|
170
|
+
|
|
171
|
+
expect(rulesContent).toContain('User Management System');
|
|
172
|
+
expect(rulesContent).toContain('Core Principles');
|
|
173
|
+
expect(rulesContent).toContain('User privacy is paramount');
|
|
174
|
+
expect(rulesContent).toContain('Constraints (Non-Negotiable)');
|
|
175
|
+
expect(rulesContent).toContain('WCAG 2.1 AA compliance');
|
|
176
|
+
expect(rulesContent).toContain('Microservices architecture');
|
|
177
|
+
expect(rulesContent).toContain('constitution.md');
|
|
178
|
+
expect(rulesContent).toContain('specification.md');
|
|
179
|
+
expect(rulesContent).toContain('plan.md');
|
|
180
|
+
expect(rulesContent).toContain('What You MUST NOT Do');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('BMAD Framework', () => {
|
|
185
|
+
it('should generate Cursor configurations from BMAD outputs', async () => {
|
|
186
|
+
// Create mock outputs
|
|
187
|
+
const prdContent = `# Product Requirements Document
|
|
188
|
+
|
|
189
|
+
## Executive Summary
|
|
190
|
+
A complete e-commerce platform for small businesses.
|
|
191
|
+
|
|
192
|
+
## Goals and Objectives
|
|
193
|
+
- Enable online sales
|
|
194
|
+
- Provide analytics
|
|
195
|
+
|
|
196
|
+
## Technical Requirements
|
|
197
|
+
- RESTful API
|
|
198
|
+
- Payment gateway integration
|
|
199
|
+
- Secure checkout flow
|
|
200
|
+
`;
|
|
201
|
+
|
|
202
|
+
const architectureContent = `# System Architecture
|
|
203
|
+
|
|
204
|
+
## System Overview
|
|
205
|
+
Modular monolith architecture with separate domains.
|
|
206
|
+
|
|
207
|
+
## Architecture Overview
|
|
208
|
+
Clean architecture with domain-driven design.
|
|
209
|
+
|
|
210
|
+
## Components
|
|
211
|
+
- Order Management
|
|
212
|
+
- Inventory System
|
|
213
|
+
- Payment Processing
|
|
214
|
+
`;
|
|
215
|
+
|
|
216
|
+
await fs.writeFile(
|
|
217
|
+
path.join(TEST_SESSION_PATH, 'outputs', 'prd.md'),
|
|
218
|
+
prdContent,
|
|
219
|
+
'utf-8'
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
await fs.writeFile(
|
|
223
|
+
path.join(TEST_SESSION_PATH, 'outputs', 'architecture.md'),
|
|
224
|
+
architectureContent,
|
|
225
|
+
'utf-8'
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
await fs.writeJson(path.join(TEST_SESSION_PATH, '_metadata.json'), {
|
|
229
|
+
framework: 'comprehensive',
|
|
230
|
+
projectName: 'E-commerce Platform'
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Generate Cursor configs
|
|
234
|
+
const generator = new CursorGenerator(TEST_SESSION_PATH, TEST_PROJECT_PATH, 'comprehensive');
|
|
235
|
+
await generator.generate();
|
|
236
|
+
|
|
237
|
+
// Verify .cursor/rules
|
|
238
|
+
const rulesPath = path.join(TEST_PROJECT_PATH, '.cursor', 'rules');
|
|
239
|
+
const rulesContent = await fs.readFile(rulesPath, 'utf-8');
|
|
240
|
+
|
|
241
|
+
expect(rulesContent).toContain('E-commerce Platform');
|
|
242
|
+
expect(rulesContent).toContain('Product Overview');
|
|
243
|
+
expect(rulesContent).toContain('e-commerce platform');
|
|
244
|
+
expect(rulesContent).toContain('Goals and Objectives');
|
|
245
|
+
expect(rulesContent).toContain('Enable online sales');
|
|
246
|
+
expect(rulesContent).toContain('System Architecture');
|
|
247
|
+
expect(rulesContent).toContain('Code Quality Standards');
|
|
248
|
+
expect(rulesContent).toContain('Security');
|
|
249
|
+
expect(rulesContent).toContain('Performance');
|
|
250
|
+
expect(rulesContent).toContain('prd.md');
|
|
251
|
+
expect(rulesContent).toContain('architecture.md');
|
|
252
|
+
expect(rulesContent).toContain('stories.md');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('Modern vs Legacy', () => {
|
|
257
|
+
it('should create modern .cursor/rules as primary config', async () => {
|
|
258
|
+
const prpContent = '# PRP\n\n## 1. Goal Definition\nTest project';
|
|
259
|
+
|
|
260
|
+
await fs.writeFile(
|
|
261
|
+
path.join(TEST_SESSION_PATH, 'outputs', 'prp.md'),
|
|
262
|
+
prpContent,
|
|
263
|
+
'utf-8'
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const generator = new CursorGenerator(TEST_SESSION_PATH, TEST_PROJECT_PATH, 'rapid');
|
|
267
|
+
const generated = await generator.generate();
|
|
268
|
+
|
|
269
|
+
// Modern config should exist
|
|
270
|
+
const modernPath = path.join(TEST_PROJECT_PATH, '.cursor', 'rules');
|
|
271
|
+
expect(await fs.pathExists(modernPath)).toBe(true);
|
|
272
|
+
|
|
273
|
+
// Legacy should be deprecation notice
|
|
274
|
+
const legacyPath = path.join(TEST_PROJECT_PATH, '.cursorrules');
|
|
275
|
+
expect(await fs.pathExists(legacyPath)).toBe(true);
|
|
276
|
+
|
|
277
|
+
const legacyContent = await fs.readFile(legacyPath, 'utf-8');
|
|
278
|
+
expect(legacyContent).toContain('DEPRECATED');
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('Template Variables', () => {
|
|
283
|
+
it('should replace session ID in paths', async () => {
|
|
284
|
+
const prpContent = '# PRP\n\n## 1. Goal Definition\nTest project';
|
|
285
|
+
|
|
286
|
+
await fs.writeFile(
|
|
287
|
+
path.join(TEST_SESSION_PATH, 'outputs', 'prp.md'),
|
|
288
|
+
prpContent,
|
|
289
|
+
'utf-8'
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const generator = new CursorGenerator(TEST_SESSION_PATH, TEST_PROJECT_PATH, 'rapid');
|
|
293
|
+
await generator.generate();
|
|
294
|
+
|
|
295
|
+
const rulesContent = await fs.readFile(
|
|
296
|
+
path.join(TEST_PROJECT_PATH, '.cursor', 'rules'),
|
|
297
|
+
'utf-8'
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Should contain the actual session ID, not a template variable
|
|
301
|
+
expect(rulesContent).toContain('test-session');
|
|
302
|
+
expect(rulesContent).not.toContain('{SESSION_ID}');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should include ADF CLI version', async () => {
|
|
306
|
+
const prpContent = '# PRP\n\n## 1. Goal Definition\nTest';
|
|
307
|
+
|
|
308
|
+
await fs.writeFile(
|
|
309
|
+
path.join(TEST_SESSION_PATH, 'outputs', 'prp.md'),
|
|
310
|
+
prpContent,
|
|
311
|
+
'utf-8'
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const generator = new CursorGenerator(TEST_SESSION_PATH, TEST_PROJECT_PATH, 'rapid');
|
|
315
|
+
await generator.generate();
|
|
316
|
+
|
|
317
|
+
const rulesContent = await fs.readFile(
|
|
318
|
+
path.join(TEST_PROJECT_PATH, '.cursor', 'rules'),
|
|
319
|
+
'utf-8'
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
expect(rulesContent).toContain('Generated by');
|
|
323
|
+
expect(rulesContent).toContain('ADF CLI');
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const ProgressTracker = require('../lib/frameworks/progress-tracker');
|
|
4
|
+
|
|
5
|
+
const TEST_SESSION_PATH = path.join(__dirname, 'test-session');
|
|
6
|
+
|
|
7
|
+
describe('ProgressTracker', () => {
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
// Clean up test session directory
|
|
10
|
+
await fs.remove(TEST_SESSION_PATH);
|
|
11
|
+
await fs.ensureDir(TEST_SESSION_PATH);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
// Clean up after tests
|
|
16
|
+
await fs.remove(TEST_SESSION_PATH);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('initialize', () => {
|
|
20
|
+
it('should create new progress file for new session', async () => {
|
|
21
|
+
const tracker = new ProgressTracker(TEST_SESSION_PATH, 5, 'rapid');
|
|
22
|
+
const isResumable = await tracker.initialize();
|
|
23
|
+
|
|
24
|
+
expect(isResumable).toBe(false);
|
|
25
|
+
expect(await fs.pathExists(path.join(TEST_SESSION_PATH, '_progress.json'))).toBe(true);
|
|
26
|
+
expect(await fs.pathExists(path.join(TEST_SESSION_PATH, '_progress-log.md'))).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should load existing progress for resumable session', async () => {
|
|
30
|
+
// Create initial session
|
|
31
|
+
const tracker1 = new ProgressTracker(TEST_SESSION_PATH, 5, 'rapid');
|
|
32
|
+
await tracker1.initialize();
|
|
33
|
+
await tracker1.startBlock(1, 'Test Block');
|
|
34
|
+
|
|
35
|
+
// Resume session
|
|
36
|
+
const tracker2 = new ProgressTracker(TEST_SESSION_PATH, 5, 'rapid');
|
|
37
|
+
const isResumable = await tracker2.initialize();
|
|
38
|
+
|
|
39
|
+
expect(isResumable).toBe(true);
|
|
40
|
+
expect(tracker2.getProgress().currentBlock).toBe(1);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('answerQuestion', () => {
|
|
45
|
+
it('should save answer with quality metrics', async () => {
|
|
46
|
+
const tracker = new ProgressTracker(TEST_SESSION_PATH, 5, 'rapid');
|
|
47
|
+
await tracker.initialize();
|
|
48
|
+
|
|
49
|
+
const qualityMetrics = {
|
|
50
|
+
wordCount: 50,
|
|
51
|
+
qualityScore: 85,
|
|
52
|
+
isComprehensive: true,
|
|
53
|
+
hasKeywords: { matched: ['react', 'web'], count: 2 },
|
|
54
|
+
hasRequiredElements: { detected: ['platform'], count: 1 }
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
await tracker.answerQuestion('q1', 'What are you building?', 'A React web app', qualityMetrics);
|
|
58
|
+
|
|
59
|
+
const progress = tracker.getProgress();
|
|
60
|
+
expect(progress.answers['q1']).toBeDefined();
|
|
61
|
+
expect(progress.answers['q1'].text).toBe('A React web app');
|
|
62
|
+
expect(progress.answers['q1'].quality.qualityScore).toBe(85);
|
|
63
|
+
expect(progress.totalWordCount).toBe(50);
|
|
64
|
+
expect(progress.comprehensiveAnswers).toBe(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should calculate average answer quality correctly', async () => {
|
|
68
|
+
const tracker = new ProgressTracker(TEST_SESSION_PATH, 5, 'rapid');
|
|
69
|
+
await tracker.initialize();
|
|
70
|
+
|
|
71
|
+
await tracker.answerQuestion('q1', 'Q1', 'Answer 1', { qualityScore: 80, wordCount: 20, isComprehensive: true });
|
|
72
|
+
await tracker.answerQuestion('q2', 'Q2', 'Answer 2', { qualityScore: 90, wordCount: 30, isComprehensive: true });
|
|
73
|
+
await tracker.answerQuestion('q3', 'Q3', 'Answer 3', { qualityScore: 70, wordCount: 25, isComprehensive: true });
|
|
74
|
+
|
|
75
|
+
const progress = tracker.getProgress();
|
|
76
|
+
expect(progress.averageAnswerQuality).toBe(80); // (80+90+70)/3 = 80
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should calculate information richness based on quality and completion', async () => {
|
|
80
|
+
const tracker = new ProgressTracker(TEST_SESSION_PATH, 5, 'rapid');
|
|
81
|
+
await tracker.initialize();
|
|
82
|
+
|
|
83
|
+
// Complete 2 of 5 blocks (40% completion)
|
|
84
|
+
await tracker.completeBlock(1, 'Block 1', 3);
|
|
85
|
+
await tracker.completeBlock(2, 'Block 2', 2);
|
|
86
|
+
|
|
87
|
+
// Add high-quality answers (90% quality)
|
|
88
|
+
await tracker.answerQuestion('q1', 'Q1', 'A1', { qualityScore: 90, wordCount: 50, isComprehensive: true });
|
|
89
|
+
await tracker.answerQuestion('q2', 'Q2', 'A2', { qualityScore: 90, wordCount: 50, isComprehensive: true });
|
|
90
|
+
|
|
91
|
+
const progress = tracker.getProgress();
|
|
92
|
+
// informationRichness = (0.4 * completionFactor) + (0.6 * qualityFactor)
|
|
93
|
+
// = (0.4 * 0.4) + (0.6 * 0.9) = 0.16 + 0.54 = 0.70 = 70%
|
|
94
|
+
expect(progress.informationRichness).toBeCloseTo(70, 0);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('saveWithBackup', () => {
|
|
99
|
+
it('should create triple-redundant saves', async () => {
|
|
100
|
+
const tracker = new ProgressTracker(TEST_SESSION_PATH, 5, 'rapid');
|
|
101
|
+
await tracker.initialize();
|
|
102
|
+
|
|
103
|
+
await tracker.answerQuestion('q1', 'Q1', 'Answer', { qualityScore: 75, wordCount: 20, isComprehensive: true });
|
|
104
|
+
|
|
105
|
+
// Check all three save locations exist
|
|
106
|
+
expect(await fs.pathExists(path.join(TEST_SESSION_PATH, '_progress.json'))).toBe(true);
|
|
107
|
+
expect(await fs.pathExists(path.join(TEST_SESSION_PATH, '_progress.backup.json'))).toBe(true);
|
|
108
|
+
expect(await fs.pathExists(path.join(TEST_SESSION_PATH, '_progress-log.md'))).toBe(true);
|
|
109
|
+
|
|
110
|
+
// Verify main and backup have same content
|
|
111
|
+
const main = await fs.readJson(path.join(TEST_SESSION_PATH, '_progress.json'));
|
|
112
|
+
const backup = await fs.readJson(path.join(TEST_SESSION_PATH, '_progress.backup.json'));
|
|
113
|
+
expect(main).toEqual(backup);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should create emergency save on error', async () => {
|
|
117
|
+
const tracker = new ProgressTracker(TEST_SESSION_PATH, 5, 'rapid');
|
|
118
|
+
await tracker.initialize();
|
|
119
|
+
|
|
120
|
+
// Make main file read-only to simulate save error
|
|
121
|
+
const mainFile = path.join(TEST_SESSION_PATH, '_progress.json');
|
|
122
|
+
await fs.chmod(mainFile, 0o444);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await tracker.answerQuestion('q1', 'Q1', 'Answer', { qualityScore: 75, wordCount: 20, isComprehensive: true });
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// May throw, but should still create emergency file
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check for emergency file
|
|
131
|
+
const files = await fs.readdir(TEST_SESSION_PATH);
|
|
132
|
+
const emergencyFiles = files.filter(f => f.startsWith('_emergency-'));
|
|
133
|
+
|
|
134
|
+
// Restore permissions for cleanup
|
|
135
|
+
await fs.chmod(mainFile, 0o666);
|
|
136
|
+
|
|
137
|
+
// Emergency file should exist if save failed
|
|
138
|
+
expect(emergencyFiles.length).toBeGreaterThan(0);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('block tracking', () => {
|
|
143
|
+
it('should track block start, complete, and skip', async () => {
|
|
144
|
+
const tracker = new ProgressTracker(TEST_SESSION_PATH, 5, 'rapid');
|
|
145
|
+
await tracker.initialize();
|
|
146
|
+
|
|
147
|
+
await tracker.startBlock(1, 'Block 1');
|
|
148
|
+
await tracker.completeBlock(1, 'Block 1', 3);
|
|
149
|
+
await tracker.skipBlock(2, 'Block 2');
|
|
150
|
+
|
|
151
|
+
const progress = tracker.getProgress();
|
|
152
|
+
expect(progress.currentBlock).toBe(1);
|
|
153
|
+
expect(progress.completedBlocks.length).toBe(1);
|
|
154
|
+
expect(progress.completedBlocks[0].number).toBe(1);
|
|
155
|
+
expect(progress.completedBlocks[0].questionsAnswered).toBe(3);
|
|
156
|
+
expect(progress.skippedBlocks.length).toBe(1);
|
|
157
|
+
expect(progress.skippedBlocks[0].number).toBe(2);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('complete', () => {
|
|
162
|
+
it('should mark session as completed', async () => {
|
|
163
|
+
const tracker = new ProgressTracker(TEST_SESSION_PATH, 5, 'rapid');
|
|
164
|
+
await tracker.initialize();
|
|
165
|
+
|
|
166
|
+
await tracker.complete();
|
|
167
|
+
|
|
168
|
+
const progress = tracker.getProgress();
|
|
169
|
+
expect(progress.status).toBe('completed');
|
|
170
|
+
expect(progress.completedAt).toBeDefined();
|
|
171
|
+
expect(progress.canResume).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('canResume', () => {
|
|
176
|
+
it('should return true for in-progress sessions', async () => {
|
|
177
|
+
const tracker = new ProgressTracker(TEST_SESSION_PATH, 5, 'rapid');
|
|
178
|
+
await tracker.initialize();
|
|
179
|
+
|
|
180
|
+
expect(tracker.canResume()).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should return false for completed sessions', async () => {
|
|
184
|
+
const tracker = new ProgressTracker(TEST_SESSION_PATH, 5, 'rapid');
|
|
185
|
+
await tracker.initialize();
|
|
186
|
+
await tracker.complete();
|
|
187
|
+
|
|
188
|
+
expect(tracker.canResume()).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('getResumeInfo', () => {
|
|
193
|
+
it('should return resume information', async () => {
|
|
194
|
+
const tracker = new ProgressTracker(TEST_SESSION_PATH, 5, 'rapid');
|
|
195
|
+
await tracker.initialize();
|
|
196
|
+
await tracker.completeBlock(1, 'Block 1', 3); // This adds 3 to totalQuestionsAnswered
|
|
197
|
+
await tracker.answerQuestion('q1', 'Q1', 'A1', { qualityScore: 80, wordCount: 20, isComprehensive: true });
|
|
198
|
+
|
|
199
|
+
const info = tracker.getResumeInfo();
|
|
200
|
+
expect(info.completedBlocks).toBe(1);
|
|
201
|
+
expect(info.totalBlocks).toBe(5);
|
|
202
|
+
expect(info.totalQuestionsAnswered).toBe(3); // completeBlock added 3
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const SessionManager = require('../lib/frameworks/session-manager');
|
|
4
|
+
const ProgressTracker = require('../lib/frameworks/progress-tracker');
|
|
5
|
+
|
|
6
|
+
const TEST_PROJECT_PATH = path.join(__dirname, 'test-project');
|
|
7
|
+
const TEST_SESSIONS_DIR = path.join(TEST_PROJECT_PATH, '.adf', 'sessions');
|
|
8
|
+
|
|
9
|
+
describe('SessionManager', () => {
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
// Clean up test project directory
|
|
12
|
+
await fs.remove(TEST_PROJECT_PATH);
|
|
13
|
+
await fs.ensureDir(TEST_PROJECT_PATH);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
// Clean up after tests
|
|
18
|
+
await fs.remove(TEST_PROJECT_PATH);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('listSessions', () => {
|
|
22
|
+
it('should return empty array when no sessions exist', async () => {
|
|
23
|
+
const manager = new SessionManager(TEST_PROJECT_PATH);
|
|
24
|
+
const sessions = await manager.listSessions();
|
|
25
|
+
|
|
26
|
+
expect(sessions).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should list all sessions with progress files', async () => {
|
|
30
|
+
const manager = new SessionManager(TEST_PROJECT_PATH);
|
|
31
|
+
|
|
32
|
+
// Create test sessions
|
|
33
|
+
const session1Path = path.join(TEST_SESSIONS_DIR, 'session-1');
|
|
34
|
+
const session2Path = path.join(TEST_SESSIONS_DIR, 'session-2');
|
|
35
|
+
|
|
36
|
+
await fs.ensureDir(session1Path);
|
|
37
|
+
await fs.ensureDir(session2Path);
|
|
38
|
+
|
|
39
|
+
// Create progress trackers
|
|
40
|
+
const tracker1 = new ProgressTracker(session1Path, 5, 'rapid');
|
|
41
|
+
await tracker1.initialize();
|
|
42
|
+
|
|
43
|
+
const tracker2 = new ProgressTracker(session2Path, 10, 'balanced');
|
|
44
|
+
await tracker2.initialize();
|
|
45
|
+
|
|
46
|
+
const sessions = await manager.listSessions();
|
|
47
|
+
|
|
48
|
+
expect(sessions.length).toBe(2);
|
|
49
|
+
expect(sessions[0].sessionId).toBe('session-1');
|
|
50
|
+
expect(sessions[1].sessionId).toBe('session-2');
|
|
51
|
+
expect(sessions[0].progress.framework).toBe('rapid');
|
|
52
|
+
expect(sessions[1].progress.framework).toBe('balanced');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should skip directories without progress files', async () => {
|
|
56
|
+
const manager = new SessionManager(TEST_PROJECT_PATH);
|
|
57
|
+
|
|
58
|
+
// Create session with progress
|
|
59
|
+
const session1Path = path.join(TEST_SESSIONS_DIR, 'session-1');
|
|
60
|
+
await fs.ensureDir(session1Path);
|
|
61
|
+
const tracker1 = new ProgressTracker(session1Path, 5, 'rapid');
|
|
62
|
+
await tracker1.initialize();
|
|
63
|
+
|
|
64
|
+
// Create directory without progress file
|
|
65
|
+
const session2Path = path.join(TEST_SESSIONS_DIR, 'session-2');
|
|
66
|
+
await fs.ensureDir(session2Path);
|
|
67
|
+
|
|
68
|
+
const sessions = await manager.listSessions();
|
|
69
|
+
|
|
70
|
+
expect(sessions.length).toBe(1);
|
|
71
|
+
expect(sessions[0].sessionId).toBe('session-1');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('getResumableSessions', () => {
|
|
76
|
+
it('should return only in-progress resumable sessions', async () => {
|
|
77
|
+
const manager = new SessionManager(TEST_PROJECT_PATH);
|
|
78
|
+
|
|
79
|
+
// Create in-progress session
|
|
80
|
+
const session1Path = path.join(TEST_SESSIONS_DIR, 'session-1');
|
|
81
|
+
await fs.ensureDir(session1Path);
|
|
82
|
+
const tracker1 = new ProgressTracker(session1Path, 5, 'rapid');
|
|
83
|
+
await tracker1.initialize();
|
|
84
|
+
await tracker1.startBlock(1, 'Block 1');
|
|
85
|
+
|
|
86
|
+
// Create completed session
|
|
87
|
+
const session2Path = path.join(TEST_SESSIONS_DIR, 'session-2');
|
|
88
|
+
await fs.ensureDir(session2Path);
|
|
89
|
+
const tracker2 = new ProgressTracker(session2Path, 5, 'rapid');
|
|
90
|
+
await tracker2.initialize();
|
|
91
|
+
await tracker2.complete();
|
|
92
|
+
|
|
93
|
+
const resumable = await manager.getResumableSessions();
|
|
94
|
+
|
|
95
|
+
expect(resumable.length).toBe(1);
|
|
96
|
+
expect(resumable[0].sessionId).toBe('session-1');
|
|
97
|
+
expect(resumable[0].progress.status).toBe('in-progress');
|
|
98
|
+
expect(resumable[0].progress.canResume).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should return empty array when no resumable sessions', async () => {
|
|
102
|
+
const manager = new SessionManager(TEST_PROJECT_PATH);
|
|
103
|
+
|
|
104
|
+
// Create completed session
|
|
105
|
+
const sessionPath = path.join(TEST_SESSIONS_DIR, 'session-1');
|
|
106
|
+
await fs.ensureDir(sessionPath);
|
|
107
|
+
const tracker = new ProgressTracker(sessionPath, 5, 'rapid');
|
|
108
|
+
await tracker.initialize();
|
|
109
|
+
await tracker.complete();
|
|
110
|
+
|
|
111
|
+
const resumable = await manager.getResumableSessions();
|
|
112
|
+
|
|
113
|
+
expect(resumable).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('deleteSession', () => {
|
|
118
|
+
it('should delete session directory', async () => {
|
|
119
|
+
const manager = new SessionManager(TEST_PROJECT_PATH);
|
|
120
|
+
|
|
121
|
+
// Create session
|
|
122
|
+
const sessionPath = path.join(TEST_SESSIONS_DIR, 'session-1');
|
|
123
|
+
await fs.ensureDir(sessionPath);
|
|
124
|
+
const tracker = new ProgressTracker(sessionPath, 5, 'rapid');
|
|
125
|
+
await tracker.initialize();
|
|
126
|
+
|
|
127
|
+
expect(await fs.pathExists(sessionPath)).toBe(true);
|
|
128
|
+
|
|
129
|
+
await manager.deleteSession('session-1');
|
|
130
|
+
|
|
131
|
+
expect(await fs.pathExists(sessionPath)).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('deleteAllSessions', () => {
|
|
136
|
+
it('should delete all sessions and recreate directory', async () => {
|
|
137
|
+
const manager = new SessionManager(TEST_PROJECT_PATH);
|
|
138
|
+
|
|
139
|
+
// Create multiple sessions
|
|
140
|
+
const session1Path = path.join(TEST_SESSIONS_DIR, 'session-1');
|
|
141
|
+
const session2Path = path.join(TEST_SESSIONS_DIR, 'session-2');
|
|
142
|
+
|
|
143
|
+
await fs.ensureDir(session1Path);
|
|
144
|
+
await fs.ensureDir(session2Path);
|
|
145
|
+
|
|
146
|
+
const tracker1 = new ProgressTracker(session1Path, 5, 'rapid');
|
|
147
|
+
await tracker1.initialize();
|
|
148
|
+
|
|
149
|
+
const tracker2 = new ProgressTracker(session2Path, 5, 'rapid');
|
|
150
|
+
await tracker2.initialize();
|
|
151
|
+
|
|
152
|
+
expect(await fs.pathExists(session1Path)).toBe(true);
|
|
153
|
+
expect(await fs.pathExists(session2Path)).toBe(true);
|
|
154
|
+
|
|
155
|
+
await manager.deleteAllSessions();
|
|
156
|
+
|
|
157
|
+
expect(await fs.pathExists(session1Path)).toBe(false);
|
|
158
|
+
expect(await fs.pathExists(session2Path)).toBe(false);
|
|
159
|
+
expect(await fs.pathExists(TEST_SESSIONS_DIR)).toBe(true); // Directory should still exist
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|