popeye-cli 1.6.0 → 1.8.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 (161) hide show
  1. package/README.md +240 -32
  2. package/cheatsheet.md +407 -0
  3. package/dist/cli/commands/db.d.ts +10 -0
  4. package/dist/cli/commands/db.d.ts.map +1 -0
  5. package/dist/cli/commands/db.js +240 -0
  6. package/dist/cli/commands/db.js.map +1 -0
  7. package/dist/cli/commands/doctor.d.ts +18 -0
  8. package/dist/cli/commands/doctor.d.ts.map +1 -0
  9. package/dist/cli/commands/doctor.js +255 -0
  10. package/dist/cli/commands/doctor.js.map +1 -0
  11. package/dist/cli/commands/index.d.ts +2 -0
  12. package/dist/cli/commands/index.d.ts.map +1 -1
  13. package/dist/cli/commands/index.js +2 -0
  14. package/dist/cli/commands/index.js.map +1 -1
  15. package/dist/cli/index.d.ts.map +1 -1
  16. package/dist/cli/index.js +3 -1
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/interactive.d.ts.map +1 -1
  19. package/dist/cli/interactive.js +96 -0
  20. package/dist/cli/interactive.js.map +1 -1
  21. package/dist/generators/admin-wizard.d.ts +25 -0
  22. package/dist/generators/admin-wizard.d.ts.map +1 -0
  23. package/dist/generators/admin-wizard.js +123 -0
  24. package/dist/generators/admin-wizard.js.map +1 -0
  25. package/dist/generators/all.d.ts.map +1 -1
  26. package/dist/generators/all.js +10 -3
  27. package/dist/generators/all.js.map +1 -1
  28. package/dist/generators/database.d.ts +58 -0
  29. package/dist/generators/database.d.ts.map +1 -0
  30. package/dist/generators/database.js +229 -0
  31. package/dist/generators/database.js.map +1 -0
  32. package/dist/generators/fullstack.d.ts.map +1 -1
  33. package/dist/generators/fullstack.js +23 -7
  34. package/dist/generators/fullstack.js.map +1 -1
  35. package/dist/generators/index.d.ts +2 -0
  36. package/dist/generators/index.d.ts.map +1 -1
  37. package/dist/generators/index.js +2 -0
  38. package/dist/generators/index.js.map +1 -1
  39. package/dist/generators/templates/admin-wizard-python.d.ts +32 -0
  40. package/dist/generators/templates/admin-wizard-python.d.ts.map +1 -0
  41. package/dist/generators/templates/admin-wizard-python.js +425 -0
  42. package/dist/generators/templates/admin-wizard-python.js.map +1 -0
  43. package/dist/generators/templates/admin-wizard-react.d.ts +48 -0
  44. package/dist/generators/templates/admin-wizard-react.d.ts.map +1 -0
  45. package/dist/generators/templates/admin-wizard-react.js +554 -0
  46. package/dist/generators/templates/admin-wizard-react.js.map +1 -0
  47. package/dist/generators/templates/database-docker.d.ts +23 -0
  48. package/dist/generators/templates/database-docker.d.ts.map +1 -0
  49. package/dist/generators/templates/database-docker.js +221 -0
  50. package/dist/generators/templates/database-docker.js.map +1 -0
  51. package/dist/generators/templates/database-python.d.ts +54 -0
  52. package/dist/generators/templates/database-python.d.ts.map +1 -0
  53. package/dist/generators/templates/database-python.js +723 -0
  54. package/dist/generators/templates/database-python.js.map +1 -0
  55. package/dist/generators/templates/database-typescript.d.ts +34 -0
  56. package/dist/generators/templates/database-typescript.d.ts.map +1 -0
  57. package/dist/generators/templates/database-typescript.js +232 -0
  58. package/dist/generators/templates/database-typescript.js.map +1 -0
  59. package/dist/generators/templates/fullstack.d.ts.map +1 -1
  60. package/dist/generators/templates/fullstack.js +29 -0
  61. package/dist/generators/templates/fullstack.js.map +1 -1
  62. package/dist/generators/templates/index.d.ts +5 -0
  63. package/dist/generators/templates/index.d.ts.map +1 -1
  64. package/dist/generators/templates/index.js +5 -0
  65. package/dist/generators/templates/index.js.map +1 -1
  66. package/dist/state/index.d.ts +10 -0
  67. package/dist/state/index.d.ts.map +1 -1
  68. package/dist/state/index.js +22 -0
  69. package/dist/state/index.js.map +1 -1
  70. package/dist/types/consensus.d.ts +3 -0
  71. package/dist/types/consensus.d.ts.map +1 -1
  72. package/dist/types/consensus.js +1 -0
  73. package/dist/types/consensus.js.map +1 -1
  74. package/dist/types/database-runtime.d.ts +86 -0
  75. package/dist/types/database-runtime.d.ts.map +1 -0
  76. package/dist/types/database-runtime.js +61 -0
  77. package/dist/types/database-runtime.js.map +1 -0
  78. package/dist/types/database.d.ts +85 -0
  79. package/dist/types/database.d.ts.map +1 -0
  80. package/dist/types/database.js +71 -0
  81. package/dist/types/database.js.map +1 -0
  82. package/dist/types/index.d.ts +3 -0
  83. package/dist/types/index.d.ts.map +1 -1
  84. package/dist/types/index.js +6 -0
  85. package/dist/types/index.js.map +1 -1
  86. package/dist/types/tester.d.ts +138 -0
  87. package/dist/types/tester.d.ts.map +1 -0
  88. package/dist/types/tester.js +110 -0
  89. package/dist/types/tester.js.map +1 -0
  90. package/dist/types/workflow.d.ts +166 -0
  91. package/dist/types/workflow.d.ts.map +1 -1
  92. package/dist/types/workflow.js +14 -0
  93. package/dist/types/workflow.js.map +1 -1
  94. package/dist/workflow/db-setup-runner.d.ts +63 -0
  95. package/dist/workflow/db-setup-runner.d.ts.map +1 -0
  96. package/dist/workflow/db-setup-runner.js +336 -0
  97. package/dist/workflow/db-setup-runner.js.map +1 -0
  98. package/dist/workflow/db-state-machine.d.ts +30 -0
  99. package/dist/workflow/db-state-machine.d.ts.map +1 -0
  100. package/dist/workflow/db-state-machine.js +51 -0
  101. package/dist/workflow/db-state-machine.js.map +1 -0
  102. package/dist/workflow/execution-mode.js +2 -2
  103. package/dist/workflow/execution-mode.js.map +1 -1
  104. package/dist/workflow/index.d.ts +3 -0
  105. package/dist/workflow/index.d.ts.map +1 -1
  106. package/dist/workflow/index.js +3 -0
  107. package/dist/workflow/index.js.map +1 -1
  108. package/dist/workflow/task-workflow.d.ts +5 -0
  109. package/dist/workflow/task-workflow.d.ts.map +1 -1
  110. package/dist/workflow/task-workflow.js +172 -6
  111. package/dist/workflow/task-workflow.js.map +1 -1
  112. package/dist/workflow/tester.d.ts +120 -0
  113. package/dist/workflow/tester.d.ts.map +1 -0
  114. package/dist/workflow/tester.js +589 -0
  115. package/dist/workflow/tester.js.map +1 -0
  116. package/dist/workflow/workflow-logger.d.ts +1 -1
  117. package/dist/workflow/workflow-logger.d.ts.map +1 -1
  118. package/dist/workflow/workflow-logger.js.map +1 -1
  119. package/package.json +1 -1
  120. package/src/cli/commands/db.ts +281 -0
  121. package/src/cli/commands/doctor.ts +273 -0
  122. package/src/cli/commands/index.ts +2 -0
  123. package/src/cli/index.ts +4 -0
  124. package/src/cli/interactive.ts +102 -0
  125. package/src/generators/admin-wizard.ts +146 -0
  126. package/src/generators/all.ts +10 -3
  127. package/src/generators/database.ts +286 -0
  128. package/src/generators/fullstack.ts +26 -9
  129. package/src/generators/index.ts +12 -0
  130. package/src/generators/templates/admin-wizard-python.ts +431 -0
  131. package/src/generators/templates/admin-wizard-react.ts +560 -0
  132. package/src/generators/templates/database-docker.ts +227 -0
  133. package/src/generators/templates/database-python.ts +734 -0
  134. package/src/generators/templates/database-typescript.ts +238 -0
  135. package/src/generators/templates/fullstack.ts +29 -0
  136. package/src/generators/templates/index.ts +5 -0
  137. package/src/state/index.ts +29 -0
  138. package/src/types/consensus.ts +3 -0
  139. package/src/types/database-runtime.ts +69 -0
  140. package/src/types/database.ts +84 -0
  141. package/src/types/index.ts +50 -0
  142. package/src/types/tester.ts +136 -0
  143. package/src/types/workflow.ts +31 -0
  144. package/src/workflow/db-setup-runner.ts +391 -0
  145. package/src/workflow/db-state-machine.ts +58 -0
  146. package/src/workflow/execution-mode.ts +2 -2
  147. package/src/workflow/index.ts +3 -0
  148. package/src/workflow/task-workflow.ts +227 -5
  149. package/src/workflow/tester.ts +723 -0
  150. package/src/workflow/workflow-logger.ts +2 -0
  151. package/tests/generators/admin-wizard-orchestrator.test.ts +64 -0
  152. package/tests/generators/admin-wizard-templates.test.ts +366 -0
  153. package/tests/generators/cross-phase-integration.test.ts +383 -0
  154. package/tests/generators/database.test.ts +456 -0
  155. package/tests/generators/fe-be-db-integration.test.ts +613 -0
  156. package/tests/types/database-runtime.test.ts +158 -0
  157. package/tests/types/database.test.ts +187 -0
  158. package/tests/types/tester.test.ts +174 -0
  159. package/tests/workflow/db-setup-runner.test.ts +211 -0
  160. package/tests/workflow/db-state-machine.test.ts +117 -0
  161. package/tests/workflow/tester.test.ts +401 -0
@@ -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
+ });