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.
Files changed (195) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +184 -31
  3. package/dist/cli/commands/create.d.ts.map +1 -1
  4. package/dist/cli/commands/create.js +54 -4
  5. package/dist/cli/commands/create.js.map +1 -1
  6. package/dist/cli/interactive.d.ts +29 -0
  7. package/dist/cli/interactive.d.ts.map +1 -1
  8. package/dist/cli/interactive.js +90 -7
  9. package/dist/cli/interactive.js.map +1 -1
  10. package/dist/generators/all.d.ts +4 -1
  11. package/dist/generators/all.d.ts.map +1 -1
  12. package/dist/generators/all.js +36 -316
  13. package/dist/generators/all.js.map +1 -1
  14. package/dist/generators/doc-parser.d.ts +18 -3
  15. package/dist/generators/doc-parser.d.ts.map +1 -1
  16. package/dist/generators/doc-parser.js +81 -10
  17. package/dist/generators/doc-parser.js.map +1 -1
  18. package/dist/generators/frontend-design-analyzer.d.ts +30 -0
  19. package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
  20. package/dist/generators/frontend-design-analyzer.js +208 -0
  21. package/dist/generators/frontend-design-analyzer.js.map +1 -0
  22. package/dist/generators/shared-packages.d.ts +45 -0
  23. package/dist/generators/shared-packages.d.ts.map +1 -0
  24. package/dist/generators/shared-packages.js +456 -0
  25. package/dist/generators/shared-packages.js.map +1 -0
  26. package/dist/generators/templates/index.d.ts +4 -0
  27. package/dist/generators/templates/index.d.ts.map +1 -1
  28. package/dist/generators/templates/index.js +4 -0
  29. package/dist/generators/templates/index.js.map +1 -1
  30. package/dist/generators/templates/website-components.d.ts.map +1 -1
  31. package/dist/generators/templates/website-components.js +36 -11
  32. package/dist/generators/templates/website-components.js.map +1 -1
  33. package/dist/generators/templates/website-config.d.ts +15 -1
  34. package/dist/generators/templates/website-config.d.ts.map +1 -1
  35. package/dist/generators/templates/website-config.js +155 -13
  36. package/dist/generators/templates/website-config.js.map +1 -1
  37. package/dist/generators/templates/website-landing.d.ts +24 -0
  38. package/dist/generators/templates/website-landing.d.ts.map +1 -0
  39. package/dist/generators/templates/website-landing.js +276 -0
  40. package/dist/generators/templates/website-landing.js.map +1 -0
  41. package/dist/generators/templates/website-layout.d.ts +42 -0
  42. package/dist/generators/templates/website-layout.d.ts.map +1 -0
  43. package/dist/generators/templates/website-layout.js +408 -0
  44. package/dist/generators/templates/website-layout.js.map +1 -0
  45. package/dist/generators/templates/website-pricing.d.ts +11 -0
  46. package/dist/generators/templates/website-pricing.d.ts.map +1 -0
  47. package/dist/generators/templates/website-pricing.js +313 -0
  48. package/dist/generators/templates/website-pricing.js.map +1 -0
  49. package/dist/generators/templates/website-sections.d.ts +102 -0
  50. package/dist/generators/templates/website-sections.d.ts.map +1 -0
  51. package/dist/generators/templates/website-sections.js +444 -0
  52. package/dist/generators/templates/website-sections.js.map +1 -0
  53. package/dist/generators/templates/website.d.ts +10 -50
  54. package/dist/generators/templates/website.d.ts.map +1 -1
  55. package/dist/generators/templates/website.js +12 -788
  56. package/dist/generators/templates/website.js.map +1 -1
  57. package/dist/generators/website-content-scanner.d.ts +37 -0
  58. package/dist/generators/website-content-scanner.d.ts.map +1 -0
  59. package/dist/generators/website-content-scanner.js +165 -0
  60. package/dist/generators/website-content-scanner.js.map +1 -0
  61. package/dist/generators/website-context.d.ts +38 -2
  62. package/dist/generators/website-context.d.ts.map +1 -1
  63. package/dist/generators/website-context.js +179 -19
  64. package/dist/generators/website-context.js.map +1 -1
  65. package/dist/generators/website-debug.d.ts +68 -0
  66. package/dist/generators/website-debug.d.ts.map +1 -0
  67. package/dist/generators/website-debug.js +93 -0
  68. package/dist/generators/website-debug.js.map +1 -0
  69. package/dist/generators/website.d.ts +2 -0
  70. package/dist/generators/website.d.ts.map +1 -1
  71. package/dist/generators/website.js +66 -4
  72. package/dist/generators/website.js.map +1 -1
  73. package/dist/generators/workspace-root.d.ts +27 -0
  74. package/dist/generators/workspace-root.d.ts.map +1 -0
  75. package/dist/generators/workspace-root.js +100 -0
  76. package/dist/generators/workspace-root.js.map +1 -0
  77. package/dist/state/index.d.ts +8 -0
  78. package/dist/state/index.d.ts.map +1 -1
  79. package/dist/state/index.js +11 -0
  80. package/dist/state/index.js.map +1 -1
  81. package/dist/types/consensus.d.ts +3 -0
  82. package/dist/types/consensus.d.ts.map +1 -1
  83. package/dist/types/consensus.js +1 -0
  84. package/dist/types/consensus.js.map +1 -1
  85. package/dist/types/index.d.ts +1 -0
  86. package/dist/types/index.d.ts.map +1 -1
  87. package/dist/types/index.js +2 -0
  88. package/dist/types/index.js.map +1 -1
  89. package/dist/types/tester.d.ts +138 -0
  90. package/dist/types/tester.d.ts.map +1 -0
  91. package/dist/types/tester.js +110 -0
  92. package/dist/types/tester.js.map +1 -0
  93. package/dist/types/workflow.d.ts +151 -0
  94. package/dist/types/workflow.d.ts.map +1 -1
  95. package/dist/types/workflow.js +14 -0
  96. package/dist/types/workflow.js.map +1 -1
  97. package/dist/upgrade/handlers.d.ts +15 -0
  98. package/dist/upgrade/handlers.d.ts.map +1 -1
  99. package/dist/upgrade/handlers.js +52 -0
  100. package/dist/upgrade/handlers.js.map +1 -1
  101. package/dist/workflow/auto-fix-bundler.d.ts +37 -0
  102. package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
  103. package/dist/workflow/auto-fix-bundler.js +320 -0
  104. package/dist/workflow/auto-fix-bundler.js.map +1 -0
  105. package/dist/workflow/auto-fix.d.ts.map +1 -1
  106. package/dist/workflow/auto-fix.js +10 -3
  107. package/dist/workflow/auto-fix.js.map +1 -1
  108. package/dist/workflow/execution-mode.js +2 -2
  109. package/dist/workflow/execution-mode.js.map +1 -1
  110. package/dist/workflow/index.d.ts +2 -0
  111. package/dist/workflow/index.d.ts.map +1 -1
  112. package/dist/workflow/index.js +13 -0
  113. package/dist/workflow/index.js.map +1 -1
  114. package/dist/workflow/overview.d.ts.map +1 -1
  115. package/dist/workflow/overview.js +4 -0
  116. package/dist/workflow/overview.js.map +1 -1
  117. package/dist/workflow/plan-mode.d.ts +4 -3
  118. package/dist/workflow/plan-mode.d.ts.map +1 -1
  119. package/dist/workflow/plan-mode.js +69 -5
  120. package/dist/workflow/plan-mode.js.map +1 -1
  121. package/dist/workflow/task-workflow.d.ts +5 -0
  122. package/dist/workflow/task-workflow.d.ts.map +1 -1
  123. package/dist/workflow/task-workflow.js +172 -6
  124. package/dist/workflow/task-workflow.js.map +1 -1
  125. package/dist/workflow/tester.d.ts +120 -0
  126. package/dist/workflow/tester.d.ts.map +1 -0
  127. package/dist/workflow/tester.js +589 -0
  128. package/dist/workflow/tester.js.map +1 -0
  129. package/dist/workflow/website-strategy.d.ts +9 -0
  130. package/dist/workflow/website-strategy.d.ts.map +1 -1
  131. package/dist/workflow/website-strategy.js +73 -1
  132. package/dist/workflow/website-strategy.js.map +1 -1
  133. package/dist/workflow/website-updater.d.ts.map +1 -1
  134. package/dist/workflow/website-updater.js +15 -4
  135. package/dist/workflow/website-updater.js.map +1 -1
  136. package/dist/workflow/workflow-logger.d.ts +1 -1
  137. package/dist/workflow/workflow-logger.d.ts.map +1 -1
  138. package/dist/workflow/workflow-logger.js.map +1 -1
  139. package/package.json +1 -1
  140. package/src/cli/commands/create.ts +58 -4
  141. package/src/cli/interactive.ts +96 -7
  142. package/src/generators/all.ts +44 -332
  143. package/src/generators/doc-parser.ts +87 -10
  144. package/src/generators/frontend-design-analyzer.ts +261 -0
  145. package/src/generators/shared-packages.ts +500 -0
  146. package/src/generators/templates/index.ts +4 -0
  147. package/src/generators/templates/website-components.ts +36 -11
  148. package/src/generators/templates/website-config.ts +166 -13
  149. package/src/generators/templates/website-landing.ts +331 -0
  150. package/src/generators/templates/website-layout.ts +443 -0
  151. package/src/generators/templates/website-pricing.ts +330 -0
  152. package/src/generators/templates/website-sections.ts +541 -0
  153. package/src/generators/templates/website.ts +38 -851
  154. package/src/generators/website-content-scanner.ts +208 -0
  155. package/src/generators/website-context.ts +248 -20
  156. package/src/generators/website-debug.ts +130 -0
  157. package/src/generators/website.ts +71 -3
  158. package/src/generators/workspace-root.ts +113 -0
  159. package/src/state/index.ts +15 -0
  160. package/src/types/consensus.ts +3 -0
  161. package/src/types/index.ts +21 -0
  162. package/src/types/tester.ts +136 -0
  163. package/src/types/workflow.ts +32 -0
  164. package/src/upgrade/handlers.ts +65 -0
  165. package/src/workflow/auto-fix-bundler.ts +392 -0
  166. package/src/workflow/auto-fix.ts +11 -3
  167. package/src/workflow/execution-mode.ts +2 -2
  168. package/src/workflow/index.ts +13 -0
  169. package/src/workflow/overview.ts +6 -0
  170. package/src/workflow/plan-mode.ts +81 -7
  171. package/src/workflow/task-workflow.ts +227 -5
  172. package/src/workflow/tester.ts +723 -0
  173. package/src/workflow/website-strategy.ts +75 -1
  174. package/src/workflow/website-updater.ts +17 -6
  175. package/src/workflow/workflow-logger.ts +2 -0
  176. package/tests/cli/project-naming.test.ts +136 -0
  177. package/tests/generators/doc-parser.test.ts +121 -0
  178. package/tests/generators/frontend-design-analyzer.test.ts +90 -0
  179. package/tests/generators/quality-gate.test.ts +183 -0
  180. package/tests/generators/shared-packages.test.ts +83 -0
  181. package/tests/generators/website-components.test.ts +1 -1
  182. package/tests/generators/website-config.test.ts +84 -0
  183. package/tests/generators/website-content-scanner.test.ts +181 -0
  184. package/tests/generators/website-context.test.ts +109 -0
  185. package/tests/generators/website-debug.test.ts +77 -0
  186. package/tests/generators/website-landing.test.ts +188 -0
  187. package/tests/generators/website-pricing.test.ts +98 -0
  188. package/tests/generators/website-sections.test.ts +245 -0
  189. package/tests/generators/workspace-root.test.ts +105 -0
  190. package/tests/types/tester.test.ts +174 -0
  191. package/tests/upgrade/handlers.test.ts +162 -0
  192. package/tests/workflow/auto-fix-bundler.test.ts +242 -0
  193. package/tests/workflow/plan-mode.test.ts +111 -1
  194. package/tests/workflow/tester.test.ts +401 -0
  195. 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
+ });