popeye-cli 1.5.0 → 1.7.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/CHANGELOG.md +54 -0
- package/README.md +184 -31
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +54 -4
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts +29 -0
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +90 -7
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts +4 -1
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +36 -316
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +18 -3
- package/dist/generators/doc-parser.d.ts.map +1 -1
- package/dist/generators/doc-parser.js +81 -10
- package/dist/generators/doc-parser.js.map +1 -1
- package/dist/generators/frontend-design-analyzer.d.ts +30 -0
- package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
- package/dist/generators/frontend-design-analyzer.js +208 -0
- package/dist/generators/frontend-design-analyzer.js.map +1 -0
- package/dist/generators/shared-packages.d.ts +45 -0
- package/dist/generators/shared-packages.d.ts.map +1 -0
- package/dist/generators/shared-packages.js +456 -0
- package/dist/generators/shared-packages.js.map +1 -0
- package/dist/generators/templates/index.d.ts +4 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +4 -0
- package/dist/generators/templates/index.js.map +1 -1
- package/dist/generators/templates/website-components.d.ts.map +1 -1
- package/dist/generators/templates/website-components.js +36 -11
- package/dist/generators/templates/website-components.js.map +1 -1
- package/dist/generators/templates/website-config.d.ts +15 -1
- package/dist/generators/templates/website-config.d.ts.map +1 -1
- package/dist/generators/templates/website-config.js +155 -13
- package/dist/generators/templates/website-config.js.map +1 -1
- package/dist/generators/templates/website-landing.d.ts +24 -0
- package/dist/generators/templates/website-landing.d.ts.map +1 -0
- package/dist/generators/templates/website-landing.js +276 -0
- package/dist/generators/templates/website-landing.js.map +1 -0
- package/dist/generators/templates/website-layout.d.ts +42 -0
- package/dist/generators/templates/website-layout.d.ts.map +1 -0
- package/dist/generators/templates/website-layout.js +408 -0
- package/dist/generators/templates/website-layout.js.map +1 -0
- package/dist/generators/templates/website-pricing.d.ts +11 -0
- package/dist/generators/templates/website-pricing.d.ts.map +1 -0
- package/dist/generators/templates/website-pricing.js +313 -0
- package/dist/generators/templates/website-pricing.js.map +1 -0
- package/dist/generators/templates/website-sections.d.ts +102 -0
- package/dist/generators/templates/website-sections.d.ts.map +1 -0
- package/dist/generators/templates/website-sections.js +444 -0
- package/dist/generators/templates/website-sections.js.map +1 -0
- package/dist/generators/templates/website.d.ts +10 -50
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +12 -788
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-content-scanner.d.ts +37 -0
- package/dist/generators/website-content-scanner.d.ts.map +1 -0
- package/dist/generators/website-content-scanner.js +165 -0
- package/dist/generators/website-content-scanner.js.map +1 -0
- package/dist/generators/website-context.d.ts +38 -2
- package/dist/generators/website-context.d.ts.map +1 -1
- package/dist/generators/website-context.js +179 -19
- package/dist/generators/website-context.js.map +1 -1
- package/dist/generators/website-debug.d.ts +68 -0
- package/dist/generators/website-debug.d.ts.map +1 -0
- package/dist/generators/website-debug.js +93 -0
- package/dist/generators/website-debug.js.map +1 -0
- package/dist/generators/website.d.ts +2 -0
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +66 -4
- package/dist/generators/website.js.map +1 -1
- package/dist/generators/workspace-root.d.ts +27 -0
- package/dist/generators/workspace-root.d.ts.map +1 -0
- package/dist/generators/workspace-root.js +100 -0
- package/dist/generators/workspace-root.js.map +1 -0
- package/dist/state/index.d.ts +8 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +11 -0
- package/dist/state/index.js.map +1 -1
- package/dist/types/consensus.d.ts +3 -0
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +1 -0
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/tester.d.ts +138 -0
- package/dist/types/tester.d.ts.map +1 -0
- package/dist/types/tester.js +110 -0
- package/dist/types/tester.js.map +1 -0
- package/dist/types/workflow.d.ts +151 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +14 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/handlers.d.ts +15 -0
- package/dist/upgrade/handlers.d.ts.map +1 -1
- package/dist/upgrade/handlers.js +52 -0
- package/dist/upgrade/handlers.js.map +1 -1
- package/dist/workflow/auto-fix-bundler.d.ts +37 -0
- package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
- package/dist/workflow/auto-fix-bundler.js +320 -0
- package/dist/workflow/auto-fix-bundler.js.map +1 -0
- package/dist/workflow/auto-fix.d.ts.map +1 -1
- package/dist/workflow/auto-fix.js +10 -3
- package/dist/workflow/auto-fix.js.map +1 -1
- package/dist/workflow/execution-mode.js +2 -2
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +2 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +13 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/overview.d.ts.map +1 -1
- package/dist/workflow/overview.js +4 -0
- package/dist/workflow/overview.js.map +1 -1
- package/dist/workflow/plan-mode.d.ts +4 -3
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +69 -5
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/task-workflow.d.ts +5 -0
- package/dist/workflow/task-workflow.d.ts.map +1 -1
- package/dist/workflow/task-workflow.js +172 -6
- package/dist/workflow/task-workflow.js.map +1 -1
- package/dist/workflow/tester.d.ts +120 -0
- package/dist/workflow/tester.d.ts.map +1 -0
- package/dist/workflow/tester.js +589 -0
- package/dist/workflow/tester.js.map +1 -0
- package/dist/workflow/website-strategy.d.ts +9 -0
- package/dist/workflow/website-strategy.d.ts.map +1 -1
- package/dist/workflow/website-strategy.js +73 -1
- package/dist/workflow/website-strategy.js.map +1 -1
- package/dist/workflow/website-updater.d.ts.map +1 -1
- package/dist/workflow/website-updater.js +15 -4
- package/dist/workflow/website-updater.js.map +1 -1
- package/dist/workflow/workflow-logger.d.ts +1 -1
- package/dist/workflow/workflow-logger.d.ts.map +1 -1
- package/dist/workflow/workflow-logger.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/create.ts +58 -4
- package/src/cli/interactive.ts +96 -7
- package/src/generators/all.ts +44 -332
- package/src/generators/doc-parser.ts +87 -10
- package/src/generators/frontend-design-analyzer.ts +261 -0
- package/src/generators/shared-packages.ts +500 -0
- package/src/generators/templates/index.ts +4 -0
- package/src/generators/templates/website-components.ts +36 -11
- package/src/generators/templates/website-config.ts +166 -13
- package/src/generators/templates/website-landing.ts +331 -0
- package/src/generators/templates/website-layout.ts +443 -0
- package/src/generators/templates/website-pricing.ts +330 -0
- package/src/generators/templates/website-sections.ts +541 -0
- package/src/generators/templates/website.ts +38 -851
- package/src/generators/website-content-scanner.ts +208 -0
- package/src/generators/website-context.ts +248 -20
- package/src/generators/website-debug.ts +130 -0
- package/src/generators/website.ts +71 -3
- package/src/generators/workspace-root.ts +113 -0
- package/src/state/index.ts +15 -0
- package/src/types/consensus.ts +3 -0
- package/src/types/index.ts +21 -0
- package/src/types/tester.ts +136 -0
- package/src/types/workflow.ts +32 -0
- package/src/upgrade/handlers.ts +65 -0
- package/src/workflow/auto-fix-bundler.ts +392 -0
- package/src/workflow/auto-fix.ts +11 -3
- package/src/workflow/execution-mode.ts +2 -2
- package/src/workflow/index.ts +13 -0
- package/src/workflow/overview.ts +6 -0
- package/src/workflow/plan-mode.ts +81 -7
- package/src/workflow/task-workflow.ts +227 -5
- package/src/workflow/tester.ts +723 -0
- package/src/workflow/website-strategy.ts +75 -1
- package/src/workflow/website-updater.ts +17 -6
- package/src/workflow/workflow-logger.ts +2 -0
- package/tests/cli/project-naming.test.ts +136 -0
- package/tests/generators/doc-parser.test.ts +121 -0
- package/tests/generators/frontend-design-analyzer.test.ts +90 -0
- package/tests/generators/quality-gate.test.ts +183 -0
- package/tests/generators/shared-packages.test.ts +83 -0
- package/tests/generators/website-components.test.ts +1 -1
- package/tests/generators/website-config.test.ts +84 -0
- package/tests/generators/website-content-scanner.test.ts +181 -0
- package/tests/generators/website-context.test.ts +109 -0
- package/tests/generators/website-debug.test.ts +77 -0
- package/tests/generators/website-landing.test.ts +188 -0
- package/tests/generators/website-pricing.test.ts +98 -0
- package/tests/generators/website-sections.test.ts +245 -0
- package/tests/generators/workspace-root.test.ts +105 -0
- package/tests/types/tester.test.ts +174 -0
- package/tests/upgrade/handlers.test.ts +162 -0
- package/tests/workflow/auto-fix-bundler.test.ts +242 -0
- package/tests/workflow/plan-mode.test.ts +111 -1
- package/tests/workflow/tester.test.ts +401 -0
- package/tests/workflow/website-strategy.test.ts +55 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import { parsePlanMilestones } from '../../src/workflow/plan-mode.js';
|
|
6
|
+
import { parsePlanMilestones, parseTaskTag, tagToAppTarget } from '../../src/workflow/plan-mode.js';
|
|
7
7
|
|
|
8
8
|
describe('parsePlanMilestones', () => {
|
|
9
9
|
describe('with explicit task markers', () => {
|
|
@@ -195,6 +195,76 @@ Description: Build the main API endpoints
|
|
|
195
195
|
});
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
+
describe('app tag handling', () => {
|
|
199
|
+
it('should extract tasks with [WEB] tags', () => {
|
|
200
|
+
const plan = `
|
|
201
|
+
## Milestone 1: Website Branding
|
|
202
|
+
|
|
203
|
+
### Task 1.1 [WEB]: Update root layout with Gateco branding
|
|
204
|
+
- **Description**: Replace default branding with Gateco colors and logo
|
|
205
|
+
|
|
206
|
+
### Task 1.2 [WEB]: Create hero section component
|
|
207
|
+
- **Description**: Build the landing page hero with CTA
|
|
208
|
+
`;
|
|
209
|
+
|
|
210
|
+
const milestones = parsePlanMilestones(plan);
|
|
211
|
+
const allTasks = milestones.flatMap(m => m.tasks);
|
|
212
|
+
|
|
213
|
+
expect(allTasks.length).toBeGreaterThanOrEqual(2);
|
|
214
|
+
const taskNames = allTasks.map(t => t.name.toLowerCase());
|
|
215
|
+
expect(taskNames.some(n => n.includes('update root layout'))).toBe(true);
|
|
216
|
+
expect(taskNames.some(n => n.includes('create hero section'))).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should extract tasks with [INT] tags', () => {
|
|
220
|
+
const plan = `
|
|
221
|
+
## Milestone 3: Integration
|
|
222
|
+
|
|
223
|
+
### Task 3.1 [INT]: Wire frontend auth to backend API
|
|
224
|
+
- **Description**: Connect the frontend login form to the backend auth endpoint
|
|
225
|
+
|
|
226
|
+
### Task 3.2 [INT]: Set up end-to-end test suite
|
|
227
|
+
- **Description**: Create E2E tests covering the full auth flow
|
|
228
|
+
`;
|
|
229
|
+
|
|
230
|
+
const milestones = parsePlanMilestones(plan);
|
|
231
|
+
const allTasks = milestones.flatMap(m => m.tasks);
|
|
232
|
+
|
|
233
|
+
expect(allTasks.length).toBeGreaterThanOrEqual(2);
|
|
234
|
+
const taskNames = allTasks.map(t => t.name.toLowerCase());
|
|
235
|
+
expect(taskNames.some(n => n.includes('wire frontend auth'))).toBe(true);
|
|
236
|
+
expect(taskNames.some(n => n.includes('set up end-to-end'))).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should extract tasks with mixed app tags across milestones', () => {
|
|
240
|
+
const plan = `
|
|
241
|
+
## Milestone 1: Setup
|
|
242
|
+
|
|
243
|
+
### Task 1.1 [FE]: Create React component library
|
|
244
|
+
- **Description**: Scaffold shared components
|
|
245
|
+
|
|
246
|
+
### Task 1.2 [BE]: Implement REST API endpoints
|
|
247
|
+
- **Description**: Build core API
|
|
248
|
+
|
|
249
|
+
### Task 1.3 [WEB]: Build marketing landing page
|
|
250
|
+
- **Description**: Create the public-facing website
|
|
251
|
+
|
|
252
|
+
### Task 1.4 [INT]: Configure CI/CD pipeline
|
|
253
|
+
- **Description**: Set up automated deployment
|
|
254
|
+
`;
|
|
255
|
+
|
|
256
|
+
const milestones = parsePlanMilestones(plan);
|
|
257
|
+
const allTasks = milestones.flatMap(m => m.tasks);
|
|
258
|
+
|
|
259
|
+
expect(allTasks.length).toBeGreaterThanOrEqual(4);
|
|
260
|
+
const taskNames = allTasks.map(t => t.name.toLowerCase());
|
|
261
|
+
expect(taskNames.some(n => n.includes('create react component'))).toBe(true);
|
|
262
|
+
expect(taskNames.some(n => n.includes('implement rest api'))).toBe(true);
|
|
263
|
+
expect(taskNames.some(n => n.includes('build marketing landing'))).toBe(true);
|
|
264
|
+
expect(taskNames.some(n => n.includes('configure ci/cd'))).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
198
268
|
describe('fallback behavior', () => {
|
|
199
269
|
it('should create a default milestone when no tasks found', () => {
|
|
200
270
|
const plan = `
|
|
@@ -211,3 +281,43 @@ The project will do various things but no specific implementation steps are list
|
|
|
211
281
|
});
|
|
212
282
|
});
|
|
213
283
|
});
|
|
284
|
+
|
|
285
|
+
describe('parseTaskTag', () => {
|
|
286
|
+
it('should return WEB for [WEB] tagged tasks', () => {
|
|
287
|
+
expect(parseTaskTag('[WEB]: Update root layout')).toBe('WEB');
|
|
288
|
+
expect(parseTaskTag('[web]: Update root layout')).toBe('WEB');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should return FE, BE, INT for their respective tags', () => {
|
|
292
|
+
expect(parseTaskTag('[FE]: Create component')).toBe('FE');
|
|
293
|
+
expect(parseTaskTag('[BE]: Build API')).toBe('BE');
|
|
294
|
+
expect(parseTaskTag('[INT]: Wire frontend to backend')).toBe('INT');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should return undefined for untagged tasks', () => {
|
|
298
|
+
expect(parseTaskTag('Create component')).toBeUndefined();
|
|
299
|
+
expect(parseTaskTag('Build API endpoints')).toBeUndefined();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should return undefined for unknown tags', () => {
|
|
303
|
+
expect(parseTaskTag('[UNKNOWN]: Some task')).toBeUndefined();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('tagToAppTarget', () => {
|
|
308
|
+
it('should map WEB to website', () => {
|
|
309
|
+
expect(tagToAppTarget('WEB')).toBe('website');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should map FE to frontend', () => {
|
|
313
|
+
expect(tagToAppTarget('FE')).toBe('frontend');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should map BE to backend', () => {
|
|
317
|
+
expect(tagToAppTarget('BE')).toBe('backend');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should map INT to unified', () => {
|
|
321
|
+
expect(tagToAppTarget('INT')).toBe('unified');
|
|
322
|
+
});
|
|
323
|
+
});
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Tester (QA) workflow module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import { promises as fs } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import {
|
|
9
|
+
discoverTestCommands,
|
|
10
|
+
getComponentPlaybook,
|
|
11
|
+
buildTestPlanPrompt,
|
|
12
|
+
buildTestRunReviewPrompt,
|
|
13
|
+
buildTestFixPlanPrompt,
|
|
14
|
+
isQaEnabled,
|
|
15
|
+
} from '../../src/workflow/tester.js';
|
|
16
|
+
import type { ProjectState, Task, Milestone } from '../../src/types/workflow.js';
|
|
17
|
+
import type { TestRunReview } from '../../src/types/tester.js';
|
|
18
|
+
import type { TestResult } from '../../src/workflow/test-runner.js';
|
|
19
|
+
|
|
20
|
+
// Mock fs for discoverTestCommands
|
|
21
|
+
vi.mock('node:fs', async () => {
|
|
22
|
+
const actual = await vi.importActual('node:fs');
|
|
23
|
+
return {
|
|
24
|
+
...actual,
|
|
25
|
+
promises: {
|
|
26
|
+
...(actual as Record<string, unknown>).promises,
|
|
27
|
+
readFile: vi.fn(),
|
|
28
|
+
mkdir: vi.fn(),
|
|
29
|
+
writeFile: vi.fn(),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const mockReadFile = vi.mocked(fs.readFile);
|
|
35
|
+
|
|
36
|
+
function makeState(overrides?: Partial<ProjectState>): ProjectState {
|
|
37
|
+
return {
|
|
38
|
+
id: 'test-project',
|
|
39
|
+
name: 'Test Project',
|
|
40
|
+
idea: 'Test idea',
|
|
41
|
+
language: 'typescript',
|
|
42
|
+
openaiModel: 'gpt-4o',
|
|
43
|
+
phase: 'execution',
|
|
44
|
+
status: 'in-progress',
|
|
45
|
+
milestones: [],
|
|
46
|
+
currentMilestone: null,
|
|
47
|
+
currentTask: null,
|
|
48
|
+
consensusHistory: [],
|
|
49
|
+
createdAt: new Date().toISOString(),
|
|
50
|
+
updatedAt: new Date().toISOString(),
|
|
51
|
+
...overrides,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makeTask(overrides?: Partial<Task>): Task {
|
|
56
|
+
return {
|
|
57
|
+
id: 'milestone-1-task-1',
|
|
58
|
+
name: 'Implement login endpoint',
|
|
59
|
+
description: 'Create POST /auth/login with JWT token generation',
|
|
60
|
+
status: 'in-progress',
|
|
61
|
+
...overrides,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function makeMilestone(overrides?: Partial<Milestone>): Milestone {
|
|
66
|
+
return {
|
|
67
|
+
id: 'milestone-1',
|
|
68
|
+
name: 'Authentication',
|
|
69
|
+
description: 'Implement auth system',
|
|
70
|
+
status: 'in-progress',
|
|
71
|
+
tasks: [makeTask()],
|
|
72
|
+
...overrides,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function makeTestResult(overrides?: Partial<TestResult>): TestResult {
|
|
77
|
+
return {
|
|
78
|
+
success: true,
|
|
79
|
+
total: 10,
|
|
80
|
+
passed: 10,
|
|
81
|
+
failed: 0,
|
|
82
|
+
output: 'All 10 tests passed',
|
|
83
|
+
...overrides,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe('discoverTestCommands', () => {
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
vi.clearAllMocks();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should discover commands from package.json', async () => {
|
|
93
|
+
mockReadFile.mockImplementation(async (filePath: unknown) => {
|
|
94
|
+
if (String(filePath).endsWith('package.json')) {
|
|
95
|
+
return JSON.stringify({
|
|
96
|
+
scripts: { test: 'vitest run', lint: 'eslint .', build: 'tsc', typecheck: 'tsc --noEmit' },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
throw new Error('ENOENT');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const result = await discoverTestCommands('/project', 'typescript');
|
|
103
|
+
expect(result.testCmd).toBe('npm test');
|
|
104
|
+
expect(result.lintCmd).toBe('npm run lint');
|
|
105
|
+
expect(result.buildCmd).toBe('npm run build');
|
|
106
|
+
expect(result.typecheckCmd).toBe('npm run typecheck');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should discover commands from pyproject.toml', async () => {
|
|
110
|
+
mockReadFile.mockImplementation(async (filePath: unknown) => {
|
|
111
|
+
if (String(filePath).endsWith('pyproject.toml')) {
|
|
112
|
+
return '[tool.pytest]\n[tool.ruff]\n[tool.mypy]\n';
|
|
113
|
+
}
|
|
114
|
+
throw new Error('ENOENT');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const result = await discoverTestCommands('/project', 'python');
|
|
118
|
+
expect(result.testCmd).toBe('pytest');
|
|
119
|
+
expect(result.lintCmd).toBe('ruff check .');
|
|
120
|
+
expect(result.typecheckCmd).toBe('mypy .');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should discover commands from Makefile', async () => {
|
|
124
|
+
mockReadFile.mockImplementation(async (filePath: unknown) => {
|
|
125
|
+
if (String(filePath).endsWith('Makefile')) {
|
|
126
|
+
return 'test:\n\tpytest\nlint:\n\truff\nbuild:\n\tdocker build .\n';
|
|
127
|
+
}
|
|
128
|
+
throw new Error('ENOENT');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const result = await discoverTestCommands('/project', 'python');
|
|
132
|
+
expect(result.testCmd).toBe('make test');
|
|
133
|
+
expect(result.lintCmd).toBe('make lint');
|
|
134
|
+
expect(result.buildCmd).toBe('make build');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should use fallback defaults when no config files exist', async () => {
|
|
138
|
+
mockReadFile.mockRejectedValue(new Error('ENOENT'));
|
|
139
|
+
|
|
140
|
+
const tsResult = await discoverTestCommands('/project', 'typescript');
|
|
141
|
+
expect(tsResult.testCmd).toBe('npx vitest run');
|
|
142
|
+
expect(tsResult.lintCmd).toBe('npx eslint .');
|
|
143
|
+
expect(tsResult.buildCmd).toBe('npm run build');
|
|
144
|
+
|
|
145
|
+
const pyResult = await discoverTestCommands('/project', 'python');
|
|
146
|
+
expect(pyResult.testCmd).toBe('pytest');
|
|
147
|
+
expect(pyResult.lintCmd).toBe('ruff check .');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should handle missing files gracefully', async () => {
|
|
151
|
+
mockReadFile.mockRejectedValue(new Error('ENOENT'));
|
|
152
|
+
const result = await discoverTestCommands('/project', 'website');
|
|
153
|
+
// Should not throw
|
|
154
|
+
expect(result).toBeDefined();
|
|
155
|
+
expect(result.testCmd).toBeDefined(); // Falls back to defaults
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('getComponentPlaybook', () => {
|
|
160
|
+
it('should return Python-specific guidance for python language', () => {
|
|
161
|
+
const playbook = getComponentPlaybook('python');
|
|
162
|
+
expect(playbook).toContain('pytest');
|
|
163
|
+
expect(playbook).toContain('FastAPI TestClient');
|
|
164
|
+
expect(playbook).not.toContain('Vitest');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should return TypeScript-specific guidance for typescript language', () => {
|
|
168
|
+
const playbook = getComponentPlaybook('typescript');
|
|
169
|
+
expect(playbook).toContain('Vitest');
|
|
170
|
+
expect(playbook).toContain('React Testing Library');
|
|
171
|
+
expect(playbook).not.toContain('pytest');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should return combined guidance for website language', () => {
|
|
175
|
+
const playbook = getComponentPlaybook('website');
|
|
176
|
+
expect(playbook).toContain('Vitest');
|
|
177
|
+
expect(playbook).toContain('axe-core');
|
|
178
|
+
expect(playbook).toContain('SEO meta tags');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should return comprehensive playbook for fullstack/all', () => {
|
|
182
|
+
const playbook = getComponentPlaybook('fullstack');
|
|
183
|
+
expect(playbook).toContain('pytest');
|
|
184
|
+
expect(playbook).toContain('Vitest');
|
|
185
|
+
expect(playbook).toContain('API Contract');
|
|
186
|
+
|
|
187
|
+
const allPlaybook = getComponentPlaybook('all');
|
|
188
|
+
expect(allPlaybook).toContain('pytest');
|
|
189
|
+
expect(allPlaybook).toContain('Vitest');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should return non-empty content for every language', () => {
|
|
193
|
+
const languages = ['python', 'typescript', 'website', 'fullstack', 'all'] as const;
|
|
194
|
+
for (const lang of languages) {
|
|
195
|
+
const playbook = getComponentPlaybook(lang);
|
|
196
|
+
expect(playbook.length).toBeGreaterThan(50);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('buildTestPlanPrompt', () => {
|
|
202
|
+
it('should include approved code plan in the prompt', () => {
|
|
203
|
+
const prompt = buildTestPlanPrompt(
|
|
204
|
+
makeTask(), makeMilestone(), makeState(),
|
|
205
|
+
'Implement login with bcrypt hashing',
|
|
206
|
+
{ testCmd: 'npm test', lintCmd: null, buildCmd: null, typecheckCmd: null },
|
|
207
|
+
);
|
|
208
|
+
expect(prompt).toContain('Implement login with bcrypt hashing');
|
|
209
|
+
expect(prompt).toContain('Approved Code Plan');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should include language-specific playbook', () => {
|
|
213
|
+
const prompt = buildTestPlanPrompt(
|
|
214
|
+
makeTask(), makeMilestone(), makeState({ language: 'python' }),
|
|
215
|
+
'code plan',
|
|
216
|
+
{ testCmd: 'pytest', lintCmd: null, buildCmd: null, typecheckCmd: null },
|
|
217
|
+
);
|
|
218
|
+
expect(prompt).toContain('pytest');
|
|
219
|
+
expect(prompt).toContain('Python Testing Playbook');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should include discovered commands', () => {
|
|
223
|
+
const prompt = buildTestPlanPrompt(
|
|
224
|
+
makeTask(), makeMilestone(), makeState(),
|
|
225
|
+
'plan',
|
|
226
|
+
{ testCmd: 'vitest run', lintCmd: 'eslint .', buildCmd: 'tsc', typecheckCmd: null },
|
|
227
|
+
);
|
|
228
|
+
expect(prompt).toContain('Test: vitest run');
|
|
229
|
+
expect(prompt).toContain('Lint: eslint .');
|
|
230
|
+
expect(prompt).toContain('Build: tsc');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should include task context', () => {
|
|
234
|
+
const prompt = buildTestPlanPrompt(
|
|
235
|
+
makeTask({ name: 'Implement OAuth' }),
|
|
236
|
+
makeMilestone(),
|
|
237
|
+
makeState({ name: 'MyApp' }),
|
|
238
|
+
'plan',
|
|
239
|
+
{ testCmd: null, lintCmd: null, buildCmd: null, typecheckCmd: null },
|
|
240
|
+
);
|
|
241
|
+
expect(prompt).toContain('Implement OAuth');
|
|
242
|
+
expect(prompt).toContain('MyApp');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should refer to "the Tester", not "Claude"', () => {
|
|
246
|
+
const prompt = buildTestPlanPrompt(
|
|
247
|
+
makeTask(), makeMilestone(), makeState(), 'plan',
|
|
248
|
+
{ testCmd: null, lintCmd: null, buildCmd: null, typecheckCmd: null },
|
|
249
|
+
);
|
|
250
|
+
expect(prompt).toContain('the Tester');
|
|
251
|
+
expect(prompt.toLowerCase()).not.toContain('claude');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should include completed tasks for context', () => {
|
|
255
|
+
const milestone = makeMilestone({
|
|
256
|
+
tasks: [
|
|
257
|
+
makeTask({ id: 't1', name: 'Setup DB', status: 'complete' }),
|
|
258
|
+
makeTask({ id: 't2', name: 'Current task', status: 'in-progress' }),
|
|
259
|
+
],
|
|
260
|
+
});
|
|
261
|
+
const prompt = buildTestPlanPrompt(
|
|
262
|
+
makeTask(), milestone, makeState(), 'plan',
|
|
263
|
+
{ testCmd: null, lintCmd: null, buildCmd: null, typecheckCmd: null },
|
|
264
|
+
);
|
|
265
|
+
expect(prompt).toContain('Setup DB');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should handle empty task description', () => {
|
|
269
|
+
const task = makeTask({ description: '' });
|
|
270
|
+
const prompt = buildTestPlanPrompt(
|
|
271
|
+
task, makeMilestone(), makeState(), 'plan',
|
|
272
|
+
{ testCmd: null, lintCmd: null, buildCmd: null, typecheckCmd: null },
|
|
273
|
+
);
|
|
274
|
+
// Should not throw and should still contain the task name
|
|
275
|
+
expect(prompt).toContain(task.name);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('buildTestRunReviewPrompt', () => {
|
|
280
|
+
it('should include test plan criteria and actual output', () => {
|
|
281
|
+
const prompt = buildTestRunReviewPrompt(
|
|
282
|
+
makeTask(),
|
|
283
|
+
'Test plan with acceptance criteria',
|
|
284
|
+
makeTestResult({ output: 'PASS: login test', passed: 5, total: 5 }),
|
|
285
|
+
makeState(),
|
|
286
|
+
);
|
|
287
|
+
expect(prompt).toContain('Test plan with acceptance criteria');
|
|
288
|
+
expect(prompt).toContain('PASS: login test');
|
|
289
|
+
expect(prompt).toContain('Passed: 5');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should include failed test names', () => {
|
|
293
|
+
const prompt = buildTestRunReviewPrompt(
|
|
294
|
+
makeTask(),
|
|
295
|
+
'test plan',
|
|
296
|
+
makeTestResult({
|
|
297
|
+
success: false,
|
|
298
|
+
failed: 2,
|
|
299
|
+
passed: 3,
|
|
300
|
+
total: 5,
|
|
301
|
+
failedTests: ['test_login_invalid', 'test_login_expired'],
|
|
302
|
+
}),
|
|
303
|
+
makeState(),
|
|
304
|
+
);
|
|
305
|
+
expect(prompt).toContain('test_login_invalid');
|
|
306
|
+
expect(prompt).toContain('test_login_expired');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should handle empty test output', () => {
|
|
310
|
+
const prompt = buildTestRunReviewPrompt(
|
|
311
|
+
makeTask(),
|
|
312
|
+
'test plan',
|
|
313
|
+
makeTestResult({ output: '' }),
|
|
314
|
+
makeState(),
|
|
315
|
+
);
|
|
316
|
+
expect(prompt).toBeDefined();
|
|
317
|
+
expect(prompt).toContain('the Tester');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should truncate long output to 5000 chars', () => {
|
|
321
|
+
const longOutput = 'x'.repeat(10000);
|
|
322
|
+
const prompt = buildTestRunReviewPrompt(
|
|
323
|
+
makeTask(), 'plan',
|
|
324
|
+
makeTestResult({ output: longOutput }),
|
|
325
|
+
makeState(),
|
|
326
|
+
);
|
|
327
|
+
// The prompt should not contain all 10000 chars of output
|
|
328
|
+
expect(prompt.length).toBeLessThan(longOutput.length);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('buildTestFixPlanPrompt', () => {
|
|
333
|
+
const review: TestRunReview = {
|
|
334
|
+
verdict: 'FAIL',
|
|
335
|
+
summary: 'Login test failed due to missing hash',
|
|
336
|
+
evidenceReviewed: ['test output'],
|
|
337
|
+
failures: ['test_login_valid: AssertionError'],
|
|
338
|
+
gaps: [],
|
|
339
|
+
recommendations: ['Add bcrypt import'],
|
|
340
|
+
requiresConsensus: true,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
it('should include root cause from review', () => {
|
|
344
|
+
const prompt = buildTestFixPlanPrompt(
|
|
345
|
+
makeTask(), 'test plan',
|
|
346
|
+
makeTestResult({ success: false, failed: 1 }),
|
|
347
|
+
review,
|
|
348
|
+
makeState(),
|
|
349
|
+
);
|
|
350
|
+
expect(prompt).toContain('Login test failed due to missing hash');
|
|
351
|
+
expect(prompt).toContain('test_login_valid: AssertionError');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('should detect test runner crash', () => {
|
|
355
|
+
const crashResult = makeTestResult({
|
|
356
|
+
success: false, passed: 0, failed: 50, output: 'ImportError: cannot import bcrypt',
|
|
357
|
+
});
|
|
358
|
+
const prompt = buildTestFixPlanPrompt(
|
|
359
|
+
makeTask(), 'test plan', crashResult, review, makeState(),
|
|
360
|
+
);
|
|
361
|
+
expect(prompt).toContain('test runner crash');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should refer to "the Tester", not "Claude"', () => {
|
|
365
|
+
const prompt = buildTestFixPlanPrompt(
|
|
366
|
+
makeTask(), 'plan',
|
|
367
|
+
makeTestResult({ success: false, failed: 1 }),
|
|
368
|
+
review, makeState(),
|
|
369
|
+
);
|
|
370
|
+
expect(prompt).toContain('the Tester');
|
|
371
|
+
expect(prompt.toLowerCase()).not.toContain('claude');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should handle no tests needed scenario', () => {
|
|
375
|
+
const emptyReview: TestRunReview = {
|
|
376
|
+
...review,
|
|
377
|
+
failures: [],
|
|
378
|
+
summary: 'No test failures to fix',
|
|
379
|
+
};
|
|
380
|
+
const prompt = buildTestFixPlanPrompt(
|
|
381
|
+
makeTask(), 'plan',
|
|
382
|
+
makeTestResult({ success: true, failed: 0 }),
|
|
383
|
+
emptyReview, makeState(),
|
|
384
|
+
);
|
|
385
|
+
expect(prompt).toContain('No test failures to fix');
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe('isQaEnabled', () => {
|
|
390
|
+
it('should return true when qaEnabled is true', () => {
|
|
391
|
+
expect(isQaEnabled(makeState({ qaEnabled: true }))).toBe(true);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should return false when qaEnabled is false', () => {
|
|
395
|
+
expect(isQaEnabled(makeState({ qaEnabled: false }))).toBe(false);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should return false when qaEnabled is undefined (existing projects)', () => {
|
|
399
|
+
expect(isQaEnabled(makeState())).toBe(false);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
loadWebsiteStrategy,
|
|
12
12
|
formatStrategyForPlanContext,
|
|
13
13
|
isStrategyStale,
|
|
14
|
+
packProductContext,
|
|
14
15
|
} from '../../src/workflow/website-strategy.js';
|
|
15
16
|
import type {
|
|
16
17
|
WebsiteStrategyDocument,
|
|
@@ -189,3 +190,57 @@ describe('isStrategyStale', () => {
|
|
|
189
190
|
expect(loaded!.strategy.messaging.headline).toBe('Ship Code 10x Faster');
|
|
190
191
|
});
|
|
191
192
|
});
|
|
193
|
+
|
|
194
|
+
describe('packProductContext', () => {
|
|
195
|
+
it('preserves high-priority docs (spec, pricing) within budget', () => {
|
|
196
|
+
const context = [
|
|
197
|
+
'--- random-notes.md ---',
|
|
198
|
+
'Some random notes about the project.',
|
|
199
|
+
'',
|
|
200
|
+
'--- product-spec.md ---',
|
|
201
|
+
'# Product Specification\nThis is the product specification.',
|
|
202
|
+
'',
|
|
203
|
+
'--- pricing.md ---',
|
|
204
|
+
'# Pricing\nFree, Pro, Enterprise tiers.',
|
|
205
|
+
'',
|
|
206
|
+
'--- color-scheme.md ---',
|
|
207
|
+
'# Colors\nPrimary: #2563EB',
|
|
208
|
+
].join('\n');
|
|
209
|
+
|
|
210
|
+
const packed = packProductContext(context, 500);
|
|
211
|
+
|
|
212
|
+
// Spec should come first (priority 1)
|
|
213
|
+
const specIndex = packed.indexOf('product-spec.md');
|
|
214
|
+
const pricingIndex = packed.indexOf('pricing.md');
|
|
215
|
+
const randomIndex = packed.indexOf('random-notes.md');
|
|
216
|
+
|
|
217
|
+
expect(specIndex).toBeGreaterThanOrEqual(0);
|
|
218
|
+
expect(pricingIndex).toBeGreaterThanOrEqual(0);
|
|
219
|
+
// Spec should appear before random notes (or random notes may be cut)
|
|
220
|
+
if (randomIndex >= 0) {
|
|
221
|
+
expect(specIndex).toBeLessThan(randomIndex);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('handles context without headers gracefully', () => {
|
|
226
|
+
const context = 'Just raw text without any headers';
|
|
227
|
+
|
|
228
|
+
const packed = packProductContext(context, 100);
|
|
229
|
+
|
|
230
|
+
expect(packed).toBe(context);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('respects budget limit', () => {
|
|
234
|
+
const largeContext = [
|
|
235
|
+
'--- spec.md ---',
|
|
236
|
+
'x'.repeat(5000),
|
|
237
|
+
'',
|
|
238
|
+
'--- pricing.md ---',
|
|
239
|
+
'y'.repeat(5000),
|
|
240
|
+
].join('\n');
|
|
241
|
+
|
|
242
|
+
const packed = packProductContext(largeContext, 1000);
|
|
243
|
+
|
|
244
|
+
expect(packed.length).toBeLessThanOrEqual(1100); // Some tolerance for headers
|
|
245
|
+
});
|
|
246
|
+
});
|