popeye-cli 1.6.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 (50) hide show
  1. package/README.md +139 -28
  2. package/dist/state/index.d.ts.map +1 -1
  3. package/dist/state/index.js +1 -0
  4. package/dist/state/index.js.map +1 -1
  5. package/dist/types/consensus.d.ts +3 -0
  6. package/dist/types/consensus.d.ts.map +1 -1
  7. package/dist/types/consensus.js +1 -0
  8. package/dist/types/consensus.js.map +1 -1
  9. package/dist/types/index.d.ts +1 -0
  10. package/dist/types/index.d.ts.map +1 -1
  11. package/dist/types/index.js +2 -0
  12. package/dist/types/index.js.map +1 -1
  13. package/dist/types/tester.d.ts +138 -0
  14. package/dist/types/tester.d.ts.map +1 -0
  15. package/dist/types/tester.js +110 -0
  16. package/dist/types/tester.js.map +1 -0
  17. package/dist/types/workflow.d.ts +145 -0
  18. package/dist/types/workflow.d.ts.map +1 -1
  19. package/dist/types/workflow.js +12 -0
  20. package/dist/types/workflow.js.map +1 -1
  21. package/dist/workflow/execution-mode.js +2 -2
  22. package/dist/workflow/execution-mode.js.map +1 -1
  23. package/dist/workflow/index.d.ts +1 -0
  24. package/dist/workflow/index.d.ts.map +1 -1
  25. package/dist/workflow/index.js +1 -0
  26. package/dist/workflow/index.js.map +1 -1
  27. package/dist/workflow/task-workflow.d.ts +5 -0
  28. package/dist/workflow/task-workflow.d.ts.map +1 -1
  29. package/dist/workflow/task-workflow.js +172 -6
  30. package/dist/workflow/task-workflow.js.map +1 -1
  31. package/dist/workflow/tester.d.ts +120 -0
  32. package/dist/workflow/tester.d.ts.map +1 -0
  33. package/dist/workflow/tester.js +589 -0
  34. package/dist/workflow/tester.js.map +1 -0
  35. package/dist/workflow/workflow-logger.d.ts +1 -1
  36. package/dist/workflow/workflow-logger.d.ts.map +1 -1
  37. package/dist/workflow/workflow-logger.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/state/index.ts +1 -0
  40. package/src/types/consensus.ts +3 -0
  41. package/src/types/index.ts +21 -0
  42. package/src/types/tester.ts +136 -0
  43. package/src/types/workflow.ts +26 -0
  44. package/src/workflow/execution-mode.ts +2 -2
  45. package/src/workflow/index.ts +1 -0
  46. package/src/workflow/task-workflow.ts +227 -5
  47. package/src/workflow/tester.ts +723 -0
  48. package/src/workflow/workflow-logger.ts +2 -0
  49. package/tests/types/tester.test.ts +174 -0
  50. package/tests/workflow/tester.test.ts +401 -0
@@ -40,6 +40,8 @@ export type WorkflowStage =
40
40
  | 'ui-design'
41
41
  | 'ui-setup'
42
42
  | 'website-strategy'
43
+ | 'test-planning'
44
+ | 'test-review'
43
45
  | 'completion';
44
46
 
45
47
  /**
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Tests for Tester (QA) type schemas
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ TestVerdictSchema,
8
+ TestCommandSchema,
9
+ TestCaseSchema,
10
+ TestPlanOutputSchema,
11
+ TestRunReviewSchema,
12
+ FixStepSchema,
13
+ TestFixPlanSchema,
14
+ } from '../../src/types/tester.js';
15
+
16
+ describe('TestVerdictSchema', () => {
17
+ it('should accept valid verdicts', () => {
18
+ expect(TestVerdictSchema.parse('PASS')).toBe('PASS');
19
+ expect(TestVerdictSchema.parse('PASS_WITH_NOTES')).toBe('PASS_WITH_NOTES');
20
+ expect(TestVerdictSchema.parse('FAIL')).toBe('FAIL');
21
+ });
22
+
23
+ it('should reject invalid verdict strings', () => {
24
+ expect(() => TestVerdictSchema.parse('pass')).toThrow();
25
+ expect(() => TestVerdictSchema.parse('UNKNOWN')).toThrow();
26
+ expect(() => TestVerdictSchema.parse('')).toThrow();
27
+ });
28
+ });
29
+
30
+ describe('TestCommandSchema', () => {
31
+ it('should accept a valid command', () => {
32
+ const cmd = { command: 'npm test', purpose: 'Run unit tests', required: true };
33
+ expect(TestCommandSchema.parse(cmd)).toEqual(cmd);
34
+ });
35
+
36
+ it('should accept command with optional cwd', () => {
37
+ const cmd = { command: 'pytest', cwd: 'backend/', purpose: 'Run backend tests', required: false };
38
+ expect(TestCommandSchema.parse(cmd)).toEqual(cmd);
39
+ });
40
+
41
+ it('should reject empty command string', () => {
42
+ expect(() => TestCommandSchema.parse({ command: '', purpose: 'test', required: true })).toThrow();
43
+ });
44
+
45
+ it('should reject missing required fields', () => {
46
+ expect(() => TestCommandSchema.parse({ command: 'npm test' })).toThrow();
47
+ expect(() => TestCommandSchema.parse({ purpose: 'test', required: true })).toThrow();
48
+ });
49
+ });
50
+
51
+ describe('TestCaseSchema', () => {
52
+ const validCase = {
53
+ id: 'TC-1',
54
+ category: 'unit',
55
+ description: 'Test user login',
56
+ acceptanceCriteria: 'Returns 200 with valid credentials',
57
+ evidenceRequired: 'Test output showing assertion passed',
58
+ priority: 'critical' as const,
59
+ };
60
+
61
+ it('should accept a valid test case', () => {
62
+ expect(TestCaseSchema.parse(validCase)).toEqual(validCase);
63
+ });
64
+
65
+ it('should reject invalid priority values', () => {
66
+ expect(() => TestCaseSchema.parse({ ...validCase, priority: 'urgent' })).toThrow();
67
+ });
68
+
69
+ it('should reject empty id', () => {
70
+ expect(() => TestCaseSchema.parse({ ...validCase, id: '' })).toThrow();
71
+ });
72
+ });
73
+
74
+ describe('TestPlanOutputSchema', () => {
75
+ const validPlan = {
76
+ summary: 'Tests login feature risks',
77
+ scope: ['backend'] as const,
78
+ testMatrix: [{
79
+ id: 'TC-1', category: 'unit', description: 'Login test',
80
+ acceptanceCriteria: 'passes', evidenceRequired: 'output', priority: 'high' as const,
81
+ }],
82
+ commands: [{ command: 'pytest', purpose: 'Run tests', required: true }],
83
+ riskFocus: ['Authentication bypass'],
84
+ evidenceRequired: ['test output'],
85
+ minimumVerification: ['build check'],
86
+ };
87
+
88
+ it('should accept a valid test plan', () => {
89
+ const result = TestPlanOutputSchema.parse(validPlan);
90
+ expect(result.summary).toBe('Tests login feature risks');
91
+ expect(result.commands).toHaveLength(1);
92
+ });
93
+
94
+ it('should accept plan with noTestsRationale', () => {
95
+ const plan = { ...validPlan, noTestsRationale: 'Config-only change, no code logic' };
96
+ expect(TestPlanOutputSchema.parse(plan).noTestsRationale).toBe('Config-only change, no code logic');
97
+ });
98
+
99
+ it('should reject empty commands array', () => {
100
+ expect(() => TestPlanOutputSchema.parse({ ...validPlan, commands: [] })).toThrow();
101
+ });
102
+
103
+ it('should reject empty scope array', () => {
104
+ expect(() => TestPlanOutputSchema.parse({ ...validPlan, scope: [] })).toThrow();
105
+ });
106
+ });
107
+
108
+ describe('TestRunReviewSchema', () => {
109
+ const validReview = {
110
+ verdict: 'PASS' as const,
111
+ summary: 'All tests passed',
112
+ evidenceReviewed: ['test output'],
113
+ failures: [],
114
+ gaps: [],
115
+ recommendations: [],
116
+ requiresConsensus: false,
117
+ };
118
+
119
+ it('should accept a valid PASS review', () => {
120
+ expect(TestRunReviewSchema.parse(validReview).verdict).toBe('PASS');
121
+ });
122
+
123
+ it('should accept a FAIL review with failures', () => {
124
+ const review = {
125
+ ...validReview,
126
+ verdict: 'FAIL' as const,
127
+ failures: ['Login test failed'],
128
+ requiresConsensus: true,
129
+ };
130
+ expect(TestRunReviewSchema.parse(review).requiresConsensus).toBe(true);
131
+ });
132
+
133
+ it('should reject missing verdict', () => {
134
+ const { verdict, ...rest } = validReview;
135
+ expect(() => TestRunReviewSchema.parse(rest)).toThrow();
136
+ });
137
+
138
+ it('should reject empty evidenceReviewed', () => {
139
+ expect(() => TestRunReviewSchema.parse({ ...validReview, evidenceReviewed: [] })).toThrow();
140
+ });
141
+ });
142
+
143
+ describe('TestFixPlanSchema', () => {
144
+ const validFix = {
145
+ failedCriteria: ['Login returns 200'],
146
+ rootCauseAnalysis: 'Password hashing function is not async',
147
+ fixSteps: [{ file: 'src/auth.ts', change: 'Add await', reason: 'Async hash' }],
148
+ regressionRisks: ['May affect session handling'],
149
+ retestStrategy: 'Re-run login test suite',
150
+ };
151
+
152
+ it('should accept a valid fix plan', () => {
153
+ expect(TestFixPlanSchema.parse(validFix).rootCauseAnalysis).toContain('async');
154
+ });
155
+
156
+ it('should reject empty fixSteps', () => {
157
+ expect(() => TestFixPlanSchema.parse({ ...validFix, fixSteps: [] })).toThrow();
158
+ });
159
+
160
+ it('should reject empty failedCriteria', () => {
161
+ expect(() => TestFixPlanSchema.parse({ ...validFix, failedCriteria: [] })).toThrow();
162
+ });
163
+ });
164
+
165
+ describe('FixStepSchema', () => {
166
+ it('should accept valid fix step', () => {
167
+ const step = { file: 'src/index.ts', change: 'Fix import', reason: 'Missing module' };
168
+ expect(FixStepSchema.parse(step)).toEqual(step);
169
+ });
170
+
171
+ it('should reject empty file', () => {
172
+ expect(() => FixStepSchema.parse({ file: '', change: 'Fix', reason: 'Bug' })).toThrow();
173
+ });
174
+ });
@@ -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
+ });