popeye-cli 1.4.1 → 1.4.3

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 (65) hide show
  1. package/dist/adapters/claude.d.ts.map +1 -1
  2. package/dist/adapters/claude.js +6 -0
  3. package/dist/adapters/claude.js.map +1 -1
  4. package/dist/cli/interactive.d.ts.map +1 -1
  5. package/dist/cli/interactive.js +82 -5
  6. package/dist/cli/interactive.js.map +1 -1
  7. package/dist/state/index.d.ts.map +1 -1
  8. package/dist/state/index.js +3 -2
  9. package/dist/state/index.js.map +1 -1
  10. package/dist/types/workflow.d.ts +12 -0
  11. package/dist/types/workflow.d.ts.map +1 -1
  12. package/dist/types/workflow.js +4 -0
  13. package/dist/types/workflow.js.map +1 -1
  14. package/dist/workflow/auto-fix.d.ts +17 -1
  15. package/dist/workflow/auto-fix.d.ts.map +1 -1
  16. package/dist/workflow/auto-fix.js +97 -16
  17. package/dist/workflow/auto-fix.js.map +1 -1
  18. package/dist/workflow/execution-mode.d.ts +24 -0
  19. package/dist/workflow/execution-mode.d.ts.map +1 -1
  20. package/dist/workflow/execution-mode.js +293 -19
  21. package/dist/workflow/execution-mode.js.map +1 -1
  22. package/dist/workflow/index.d.ts +1 -0
  23. package/dist/workflow/index.d.ts.map +1 -1
  24. package/dist/workflow/index.js +1 -0
  25. package/dist/workflow/index.js.map +1 -1
  26. package/dist/workflow/milestone-workflow.d.ts.map +1 -1
  27. package/dist/workflow/milestone-workflow.js +63 -3
  28. package/dist/workflow/milestone-workflow.js.map +1 -1
  29. package/dist/workflow/project-structure.d.ts +29 -0
  30. package/dist/workflow/project-structure.d.ts.map +1 -0
  31. package/dist/workflow/project-structure.js +193 -0
  32. package/dist/workflow/project-structure.js.map +1 -0
  33. package/dist/workflow/project-verification.d.ts +24 -1
  34. package/dist/workflow/project-verification.d.ts.map +1 -1
  35. package/dist/workflow/project-verification.js +105 -22
  36. package/dist/workflow/project-verification.js.map +1 -1
  37. package/dist/workflow/remediation.d.ts +124 -0
  38. package/dist/workflow/remediation.d.ts.map +1 -0
  39. package/dist/workflow/remediation.js +510 -0
  40. package/dist/workflow/remediation.js.map +1 -0
  41. package/dist/workflow/task-workflow.d.ts.map +1 -1
  42. package/dist/workflow/task-workflow.js +202 -4
  43. package/dist/workflow/task-workflow.js.map +1 -1
  44. package/dist/workflow/ui-setup.d.ts +1 -0
  45. package/dist/workflow/ui-setup.d.ts.map +1 -1
  46. package/dist/workflow/ui-setup.js +1 -1
  47. package/dist/workflow/ui-setup.js.map +1 -1
  48. package/package.json +1 -1
  49. package/src/adapters/claude.ts +6 -0
  50. package/src/cli/interactive.ts +85 -5
  51. package/src/state/index.ts +3 -2
  52. package/src/types/workflow.ts +9 -0
  53. package/src/workflow/auto-fix.ts +123 -18
  54. package/src/workflow/execution-mode.ts +365 -20
  55. package/src/workflow/index.ts +1 -0
  56. package/src/workflow/milestone-workflow.ts +80 -3
  57. package/src/workflow/project-structure.ts +233 -0
  58. package/src/workflow/project-verification.ts +128 -21
  59. package/src/workflow/remediation.ts +711 -0
  60. package/src/workflow/task-workflow.ts +237 -4
  61. package/src/workflow/ui-setup.ts +2 -1
  62. package/tests/workflow/auto-fix-enhanced.test.ts +351 -0
  63. package/tests/workflow/project-structure.test.ts +131 -0
  64. package/tests/workflow/project-verification.test.ts +130 -0
  65. package/tests/workflow/remediation.test.ts +238 -0
@@ -14,7 +14,7 @@ import {
14
14
  updateState,
15
15
  } from '../state/index.js';
16
16
  import { iterateUntilConsensus, runOptimizedConsensusProcess, type ConsensusProcessResult } from './consensus.js';
17
- import { executeTask as executeTaskCode } from './execution-mode.js';
17
+ import { executeTask as executeTaskCode, handleTestFailure } from './execution-mode.js';
18
18
  import { runTests, testsExist, getTestSummary, type TestResult } from './test-runner.js';
19
19
 
20
20
  /**
@@ -224,6 +224,119 @@ async function updateTaskInState(
224
224
  return updateState(projectDir, { milestones: updatedMilestones });
225
225
  }
226
226
 
227
+ /**
228
+ * Detect if the failure is a test runner crash (0 passed, many failed)
229
+ * vs actual individual test failures
230
+ */
231
+ function isTestRunnerCrash(testResult: TestResult): boolean {
232
+ return testResult.passed === 0 && testResult.failed > 20;
233
+ }
234
+
235
+ /**
236
+ * Detect which app a task targets from its name (e.g., "[BE]", "[FE]", "[WEB]")
237
+ */
238
+ function detectTaskApp(taskName: string): string | null {
239
+ if (taskName.includes('[BE]') || taskName.toLowerCase().includes('backend')) return 'backend';
240
+ if (taskName.includes('[FE]') || taskName.toLowerCase().includes('frontend')) return 'frontend';
241
+ if (taskName.includes('[WEB]') || taskName.toLowerCase().includes('website')) return 'website';
242
+ return null;
243
+ }
244
+
245
+ /**
246
+ * Build a fix plan for test failures that can be reviewed by consensus
247
+ *
248
+ * @param task - The task whose tests failed
249
+ * @param testResult - The test result with failure details
250
+ * @param state - Current project state
251
+ * @returns A plan string describing the proposed fix
252
+ */
253
+ function buildTestFixPlan(
254
+ task: Task,
255
+ testResult: TestResult,
256
+ state: ProjectState,
257
+ ): string {
258
+ const isCrash = isTestRunnerCrash(testResult);
259
+ const targetApp = detectTaskApp(task.name);
260
+
261
+ // For crashes, extract the first error (usually the root cause)
262
+ const outputSnippet = isCrash
263
+ ? testResult.output.slice(0, 3000)
264
+ : testResult.output.slice(0, 2000);
265
+
266
+ if (isCrash) {
267
+ return `## Test Runner Crash - Fix Plan
268
+
269
+ ### Task: ${task.name}
270
+ ### Language: ${state.language}
271
+ ${targetApp ? `### Target App: ${targetApp}` : ''}
272
+
273
+ ### CRITICAL: Test Runner Crashed
274
+ The test runner crashed with 0 passed out of ${testResult.failed} total tests.
275
+ This is NOT ${testResult.failed} individual failures - this is a **startup/import crash**.
276
+
277
+ ### Most Likely Root Causes (check in this order)
278
+ 1. **Import error** - A new file imports a module that doesn't exist or has a typo
279
+ 2. **Syntax error** - A new or modified file has invalid syntax
280
+ 3. **Missing dependency** - A package is used but not installed
281
+ 4. **Circular import** - New code creates a circular dependency
282
+ 5. **Config error** - Test config (vitest.config, jest.config, pytest.ini) was broken
283
+
284
+ ### Test Output (look for the FIRST error)
285
+ \`\`\`
286
+ ${outputSnippet}
287
+ \`\`\`
288
+
289
+ ### Fix Approach
290
+ 1. Find the FIRST error in the output above - that's the root cause
291
+ 2. Fix that single error (likely a broken import or syntax issue)
292
+ 3. Do NOT try to fix ${testResult.failed} individual tests - they all fail because of the one root cause
293
+ ${targetApp ? `4. Focus ONLY on files in the apps/${targetApp}/ directory` : ''}
294
+
295
+ ### Review Checklist
296
+ - [ ] Identified the single root cause (import/syntax/config error)
297
+ - [ ] Fix targets the root cause, not symptoms
298
+ - [ ] No unrelated code changes
299
+ `;
300
+ }
301
+
302
+ // Normal case: individual test failures
303
+ const failedTests = testResult.failedTests?.map(t => `- ${t}`).join('\n') || '(see output)';
304
+
305
+ return `## Test Failure Fix Plan
306
+
307
+ ### Task: ${task.name}
308
+ ### Language: ${state.language}
309
+ ${targetApp ? `### Target App: ${targetApp}` : ''}
310
+
311
+ ### Test Results
312
+ - Passed: ${testResult.passed}
313
+ - Failed: ${testResult.failed}
314
+ - Total: ${testResult.total}
315
+
316
+ ### Failed Tests
317
+ ${failedTests}
318
+
319
+ ### Test Output (truncated)
320
+ \`\`\`
321
+ ${outputSnippet}
322
+ \`\`\`
323
+
324
+ ### Proposed Fix Approach
325
+ 1. Analyze the root cause of each test failure from the output above
326
+ 2. Identify whether the issue is in the implementation code (most likely) or the test expectations
327
+ 3. Fix the implementation code to satisfy the test assertions
328
+ 4. Do NOT modify the tests unless they contain clear bugs
329
+ 5. Ensure fixes don't break other passing tests
330
+ ${targetApp ? `6. Focus ONLY on files in the apps/${targetApp}/ directory` : ''}
331
+
332
+ ### Review Checklist
333
+ - [ ] Root cause correctly identified for each failure
334
+ - [ ] Fix addresses the actual problem, not just the symptom
335
+ - [ ] No regressions introduced to passing tests
336
+ - [ ] Code changes are minimal and focused
337
+ `;
338
+ }
339
+
227
340
  /**
228
341
  * Run the complete task workflow: Plan → Consensus → Implement → Test
229
342
  *
@@ -486,11 +599,131 @@ Task: ${task.name}
486
599
  break;
487
600
  }
488
601
 
602
+ // Build failure reason for visibility
603
+ const isCrash = isTestRunnerCrash(testResult);
604
+ let failureReason: string;
605
+
606
+ if (isCrash) {
607
+ // Extract first meaningful error line from output
608
+ const errorLines = testResult.output.split('\n')
609
+ .filter(l => /error|Error|ERROR|failed to|cannot find|SyntaxError|ImportError|ModuleNotFound/i.test(l))
610
+ .slice(0, 2);
611
+ const rootCause = errorLines.length > 0
612
+ ? errorLines[0].trim().slice(0, 120)
613
+ : 'test runner crashed';
614
+ failureReason = `TEST RUNNER CRASH (0/${testResult.failed} passed) - ${rootCause}`;
615
+ } else {
616
+ const failedNames = testResult.failedTests?.slice(0, 5).join(', ') || '';
617
+ failureReason = failedNames
618
+ ? `${testResult.failed} failed: ${failedNames}${(testResult.failedTests?.length || 0) > 5 ? '...' : ''}`
619
+ : testResult.error || getTestSummary(testResult);
620
+ }
621
+
622
+ // Tests failed - check if retries exhausted
623
+ if (retries >= maxRetries) {
624
+ onProgress?.('task-test', `Tests failed after ${retries} retries (${failureReason})`);
625
+ break;
626
+ }
627
+
489
628
  retries++;
490
- if (retries <= maxRetries) {
491
- onProgress?.('task-test', `Tests failed, retry ${retries}/${maxRetries}...`);
492
- // Could add fix attempt here
629
+ onProgress?.('task-test', `Tests failed (${failureReason}), planning fix ${retries}/${maxRetries}...`);
630
+
631
+ // Build a fix plan and get consensus before implementing
632
+ const fixPlan = buildTestFixPlan(task, testResult, state);
633
+ const fixContext = `
634
+ Project: ${state.name}
635
+ Language: ${state.language}
636
+ Milestone: ${milestone.name}
637
+ Task: ${task.name}
638
+ Phase: Test failure fix (attempt ${retries}/${maxRetries})
639
+ `.trim();
640
+
641
+ const useOptimized = consensusConfig?.useOptimizedConsensus !== false;
642
+ let fixConsensus: ConsensusProcessResult;
643
+
644
+ if (useOptimized) {
645
+ onProgress?.('task-test', `Getting consensus on fix plan (attempt ${retries}/${maxRetries})...`);
646
+ fixConsensus = await runOptimizedConsensusProcess(
647
+ fixPlan,
648
+ fixContext,
649
+ {
650
+ projectDir,
651
+ config: consensusConfig,
652
+ milestoneId: milestone.id,
653
+ milestoneName: milestone.name,
654
+ taskId: task.id,
655
+ taskName: `${task.name} - Test Fix ${retries}`,
656
+ parallelReviews: true,
657
+ isFullstack: isWorkspace(state.language),
658
+ onIteration: (iteration, result) => {
659
+ onProgress?.('task-test', `Fix consensus iteration ${iteration}: ${result.score}%`);
660
+ },
661
+ onProgress,
662
+ }
663
+ );
664
+ } else {
665
+ onProgress?.('task-test', `Getting consensus on fix plan (attempt ${retries}/${maxRetries})...`);
666
+ fixConsensus = await iterateUntilConsensus(
667
+ fixPlan,
668
+ fixContext,
669
+ {
670
+ projectDir,
671
+ config: consensusConfig,
672
+ isFullstack: isWorkspace(state.language),
673
+ language: state.language,
674
+ onIteration: (iteration, result) => {
675
+ onProgress?.('task-test', `Fix consensus iteration ${iteration}: ${result.score}%`);
676
+ },
677
+ onProgress,
678
+ }
679
+ );
493
680
  }
681
+
682
+ if (!fixConsensus.approved) {
683
+ onProgress?.('task-test', `Fix plan not approved (${fixConsensus.finalScore}%), skipping fix`);
684
+ break;
685
+ }
686
+
687
+ onProgress?.('task-test', `Fix plan approved (${fixConsensus.finalScore}%), implementing fix...`);
688
+
689
+ // Implement the consensus-approved fix
690
+ const fixResult = await handleTestFailure(
691
+ task,
692
+ testResult,
693
+ fixConsensus.bestPlan,
694
+ projectDir,
695
+ (msg) => onProgress?.('task-test', msg),
696
+ );
697
+
698
+ if (!fixResult.success) {
699
+ // Check if this is a rate limit pause (not a real failure)
700
+ if (fixResult.rateLimitPaused) {
701
+ const resetInfo = fixResult.rateLimitInfo;
702
+ const pauseMessage = resetInfo?.message || 'Rate limit reached';
703
+
704
+ state = await updateTaskInState(projectDir, task.id, {
705
+ status: 'paused',
706
+ error: `Rate limit during test fix: ${pauseMessage}. Run /resume to continue.`,
707
+ });
708
+
709
+ onProgress?.('task-test', `Test fix paused due to rate limit: ${pauseMessage}`);
710
+ onProgress?.('task-test', 'Your progress is saved. Run /resume to continue after the rate limit resets.');
711
+
712
+ return {
713
+ success: false,
714
+ task: { ...task, status: 'paused' },
715
+ consensusResult,
716
+ testResult,
717
+ rateLimitPaused: true,
718
+ error: `Rate limit: ${pauseMessage}`,
719
+ };
720
+ }
721
+
722
+ onProgress?.('task-test', `Fix attempt ${retries} failed: ${fixResult.error || 'unknown error'}`);
723
+ break;
724
+ }
725
+
726
+ onProgress?.('task-test', `Fix ${retries} applied, re-running tests...`);
494
727
  }
495
728
 
496
729
  if (testResult && !testResult.success) {
@@ -418,10 +418,11 @@ export async function setupUI(
418
418
  theme?: string;
419
419
  projectType?: string;
420
420
  idea?: string;
421
+ frontendDir?: string;
421
422
  } = {},
422
423
  onProgress?: (message: string) => void
423
424
  ): Promise<UISetupResult> {
424
- const frontendDir = path.join(projectDir, 'packages', 'frontend');
425
+ const frontendDir = options.frontendDir || path.join(projectDir, 'packages', 'frontend');
425
426
  const componentsInstalled: string[] = [];
426
427
 
427
428
  try {
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Tests for enhanced auto-fix: ENOENT detection, path parsing, structural issue heuristic
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { promises as fs } from 'node:fs';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import { parseErrorFilePaths, analyzeFileExistence } from '../../src/workflow/execution-mode.js';
10
+ import { parseTypeScriptErrors } from '../../src/workflow/auto-fix.js';
11
+
12
+ let tempDir: string;
13
+
14
+ beforeEach(async () => {
15
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-autofix-'));
16
+ });
17
+
18
+ afterEach(async () => {
19
+ await fs.rm(tempDir, { recursive: true, force: true });
20
+ });
21
+
22
+ describe('parseErrorFilePaths', () => {
23
+ it('should extract paths from TS format: path(line,col): error TS', () => {
24
+ const output = `src/index.ts(10,5): error TS2304: Cannot find name 'foo'
25
+ src/utils.ts(25,12): error TS2339: Property 'bar' does not exist`;
26
+
27
+ const paths = parseErrorFilePaths(output);
28
+
29
+ expect(paths).toContain('src/index.ts');
30
+ expect(paths).toContain('src/utils.ts');
31
+ expect(paths).toHaveLength(2);
32
+ });
33
+
34
+ it('should extract paths from generic format: path:line:col - error TS', () => {
35
+ const output = `src/app.tsx:15:3 - error TS2322: Type 'string' is not assignable
36
+ src/components/Button.tsx:8:1 - error TS2307: Cannot find module`;
37
+
38
+ const paths = parseErrorFilePaths(output);
39
+
40
+ expect(paths).toContain('src/app.tsx');
41
+ expect(paths).toContain('src/components/Button.tsx');
42
+ expect(paths).toHaveLength(2);
43
+ });
44
+
45
+ it('should normalize Windows backslashes to forward slashes', () => {
46
+ const output = `src\\components\\Header.tsx(5,10): error TS2304: Cannot find name 'x'`;
47
+
48
+ const paths = parseErrorFilePaths(output);
49
+
50
+ expect(paths).toContain('src/components/Header.tsx');
51
+ });
52
+
53
+ it('should strip ANSI color codes before parsing', () => {
54
+ const output = `\x1b[31msrc/index.ts\x1b[0m(10,5): error TS2304: Cannot find name 'foo'`;
55
+
56
+ const paths = parseErrorFilePaths(output);
57
+
58
+ expect(paths).toContain('src/index.ts');
59
+ });
60
+
61
+ it('should filter out virtual/non-project paths', () => {
62
+ const output = `src/index.ts(10,5): error TS2304: Cannot find name 'foo'
63
+ node_modules/@types/react/index.d.ts(100,1): error TS2300: Duplicate identifier
64
+ ../node_modules/vite/dist/node/index.d.ts(50,1): error TS2300: Duplicate`;
65
+
66
+ const paths = parseErrorFilePaths(output);
67
+
68
+ expect(paths).toContain('src/index.ts');
69
+ // node_modules and vite paths should be filtered
70
+ expect(paths).not.toContain('node_modules/@types/react/index.d.ts');
71
+ expect(paths).toHaveLength(1);
72
+ });
73
+
74
+ it('should de-duplicate paths after normalization', () => {
75
+ const output = `src/index.ts(10,5): error TS2304: Cannot find name 'foo'
76
+ src/index.ts(15,1): error TS2339: Property 'bar' does not exist
77
+ src/index.ts(20,3): error TS2322: Type mismatch`;
78
+
79
+ const paths = parseErrorFilePaths(output);
80
+
81
+ expect(paths).toContain('src/index.ts');
82
+ expect(paths).toHaveLength(1);
83
+ });
84
+
85
+ it('should handle ERROR in prefix wrappers', () => {
86
+ const output = `ERROR in src/app.ts(5,10): error TS2304: Cannot find name 'x'`;
87
+
88
+ const paths = parseErrorFilePaths(output);
89
+
90
+ expect(paths).toContain('src/app.ts');
91
+ });
92
+
93
+ it('should handle apps/ and packages/ prefixed paths', () => {
94
+ const output = `apps/frontend/src/App.tsx(10,5): error TS2304: Cannot find name 'foo'
95
+ packages/shared/src/types.ts(5,1): error TS2307: Cannot find module`;
96
+
97
+ const paths = parseErrorFilePaths(output);
98
+
99
+ expect(paths).toContain('apps/frontend/src/App.tsx');
100
+ expect(paths).toContain('packages/shared/src/types.ts');
101
+ expect(paths).toHaveLength(2);
102
+ });
103
+
104
+ it('should return empty array for non-TS error output', () => {
105
+ const output = 'npm ERR! code ELIFECYCLE\nnpm ERR! errno 1';
106
+
107
+ const paths = parseErrorFilePaths(output);
108
+
109
+ expect(paths).toHaveLength(0);
110
+ });
111
+ });
112
+
113
+ describe('analyzeFileExistence', () => {
114
+ it('should correctly identify existing and missing files', async () => {
115
+ // Create some files
116
+ await fs.mkdir(path.join(tempDir, 'src'), { recursive: true });
117
+ await fs.writeFile(path.join(tempDir, 'src', 'index.ts'), 'export {}');
118
+
119
+ const result = await analyzeFileExistence(tempDir, [
120
+ 'src/index.ts', // exists
121
+ 'src/missing.ts', // does not exist
122
+ ]);
123
+
124
+ expect(result.existing).toContain('src/index.ts');
125
+ expect(result.missing).toContain('src/missing.ts');
126
+ expect(result.existing).toHaveLength(1);
127
+ expect(result.missing).toHaveLength(1);
128
+ expect(result.summary).toContain('1/2 error files exist');
129
+ expect(result.summary).toContain('1/2 MISSING');
130
+ });
131
+
132
+ it('should handle absolute paths correctly', async () => {
133
+ await fs.mkdir(path.join(tempDir, 'src'), { recursive: true });
134
+ await fs.writeFile(path.join(tempDir, 'src', 'app.ts'), 'export {}');
135
+
136
+ const absolutePath = path.join(tempDir, 'src', 'app.ts');
137
+ const result = await analyzeFileExistence(tempDir, [absolutePath]);
138
+
139
+ expect(result.existing).toContain(absolutePath);
140
+ expect(result.missing).toHaveLength(0);
141
+ });
142
+
143
+ it('should handle empty file list', async () => {
144
+ const result = await analyzeFileExistence(tempDir, []);
145
+
146
+ expect(result.existing).toHaveLength(0);
147
+ expect(result.missing).toHaveLength(0);
148
+ expect(result.summary).toBe('No error files to check');
149
+ });
150
+
151
+ it('should report all missing when no files exist', async () => {
152
+ const result = await analyzeFileExistence(tempDir, [
153
+ 'src/a.ts',
154
+ 'src/b.ts',
155
+ 'src/c.ts',
156
+ ]);
157
+
158
+ expect(result.existing).toHaveLength(0);
159
+ expect(result.missing).toHaveLength(3);
160
+ expect(result.summary).toContain('0/3 error files exist');
161
+ expect(result.summary).toContain('3/3 MISSING');
162
+ });
163
+ });
164
+
165
+ describe('structural issue heuristic', () => {
166
+ it('should flag structural issue when >50% of files are missing', () => {
167
+ // Testing the heuristic logic directly
168
+ const missingFileCount = 6;
169
+ const totalErrorFiles = 10;
170
+
171
+ const isStructuralIssue = totalErrorFiles > 0 && (
172
+ (missingFileCount / totalErrorFiles >= 0.5) ||
173
+ (missingFileCount >= 25)
174
+ );
175
+
176
+ expect(isStructuralIssue).toBe(true);
177
+ });
178
+
179
+ it('should flag structural issue when >=25 missing even if <50%', () => {
180
+ const missingFileCount = 25;
181
+ const totalErrorFiles = 100;
182
+
183
+ const isStructuralIssue = totalErrorFiles > 0 && (
184
+ (missingFileCount / totalErrorFiles >= 0.5) ||
185
+ (missingFileCount >= 25)
186
+ );
187
+
188
+ expect(isStructuralIssue).toBe(true);
189
+ });
190
+
191
+ it('should NOT flag structural issue when all files are accessible', () => {
192
+ const missingFileCount = 0;
193
+ const totalErrorFiles = 50;
194
+
195
+ const isStructuralIssue = totalErrorFiles > 0 && (
196
+ (missingFileCount / totalErrorFiles >= 0.5) ||
197
+ (missingFileCount >= 25)
198
+ );
199
+
200
+ expect(isStructuralIssue).toBe(false);
201
+ });
202
+
203
+ it('should NOT flag structural issue when few files are missing (<50% and <25)', () => {
204
+ const missingFileCount = 3;
205
+ const totalErrorFiles = 20;
206
+
207
+ const isStructuralIssue = totalErrorFiles > 0 && (
208
+ (missingFileCount / totalErrorFiles >= 0.5) ||
209
+ (missingFileCount >= 25)
210
+ );
211
+
212
+ expect(isStructuralIssue).toBe(false);
213
+ });
214
+
215
+ it('should NOT flag structural issue when there are no error files', () => {
216
+ const missingFileCount = 0;
217
+ const totalErrorFiles = 0;
218
+
219
+ const isStructuralIssue = totalErrorFiles > 0 && (
220
+ (missingFileCount / totalErrorFiles >= 0.5) ||
221
+ (missingFileCount >= 25)
222
+ );
223
+
224
+ expect(isStructuralIssue).toBe(false);
225
+ });
226
+ });
227
+
228
+ describe('parseTypeScriptErrors', () => {
229
+ it('should parse tsc direct format: path(line,col): error TS', () => {
230
+ const output = `src/index.ts(10,5): error TS2304: Cannot find name 'foo'
231
+ src/utils.ts(25,12): error TS2339: Property 'bar' does not exist`;
232
+
233
+ const errors = parseTypeScriptErrors(output);
234
+
235
+ expect(errors).toHaveLength(2);
236
+ expect(errors[0]).toEqual({
237
+ file: 'src/index.ts',
238
+ line: 10,
239
+ column: 5,
240
+ code: 'TS2304',
241
+ message: "Cannot find name 'foo'",
242
+ });
243
+ expect(errors[1]).toEqual({
244
+ file: 'src/utils.ts',
245
+ line: 25,
246
+ column: 12,
247
+ code: 'TS2339',
248
+ message: "Property 'bar' does not exist",
249
+ });
250
+ });
251
+
252
+ it('should parse bundler format: path:line:col - error TS', () => {
253
+ const output = `src/App.tsx:5:3 - error TS2304: Cannot find name 'Component'
254
+ src/pages/Home.tsx:15:10 - error TS2322: Type 'string' is not assignable to type 'number'`;
255
+
256
+ const errors = parseTypeScriptErrors(output);
257
+
258
+ expect(errors).toHaveLength(2);
259
+ expect(errors[0]).toEqual({
260
+ file: 'src/App.tsx',
261
+ line: 5,
262
+ column: 3,
263
+ code: 'TS2304',
264
+ message: "Cannot find name 'Component'",
265
+ });
266
+ expect(errors[1]).toEqual({
267
+ file: 'src/pages/Home.tsx',
268
+ line: 15,
269
+ column: 10,
270
+ code: 'TS2322',
271
+ message: "Type 'string' is not assignable to type 'number'",
272
+ });
273
+ });
274
+
275
+ it('should parse mixed formats and de-duplicate', () => {
276
+ const output = `src/index.ts(10,5): error TS2304: Cannot find name 'foo'
277
+ src/index.ts:10:5 - error TS2304: Cannot find name 'foo'
278
+ src/utils.ts:3:1 - error TS2307: Cannot find module './missing'`;
279
+
280
+ const errors = parseTypeScriptErrors(output);
281
+
282
+ // First two are the same error in different formats - should be de-duped
283
+ expect(errors).toHaveLength(2);
284
+ expect(errors[0].file).toBe('src/index.ts');
285
+ expect(errors[1].file).toBe('src/utils.ts');
286
+ });
287
+
288
+ it('should strip ANSI color codes before parsing', () => {
289
+ const output = `\x1b[31msrc/index.ts\x1b[0m(10,5): error TS2304: Cannot find name 'foo'
290
+ \x1b[36msrc/App.tsx\x1b[0m:5:3 - error TS2322: Type mismatch`;
291
+
292
+ const errors = parseTypeScriptErrors(output);
293
+
294
+ expect(errors).toHaveLength(2);
295
+ expect(errors[0].file).toBe('src/index.ts');
296
+ expect(errors[0].code).toBe('TS2304');
297
+ expect(errors[1].file).toBe('src/App.tsx');
298
+ expect(errors[1].code).toBe('TS2322');
299
+ });
300
+
301
+ it('should return empty array for non-TS error output', () => {
302
+ const output = `npm ERR! code ELIFECYCLE
303
+ npm ERR! errno 1
304
+ npm ERR! project@1.0.0 build: vite build
305
+ npm ERR! Exit status 1`;
306
+
307
+ const errors = parseTypeScriptErrors(output);
308
+
309
+ expect(errors).toHaveLength(0);
310
+ });
311
+
312
+ it('should return empty array for empty output', () => {
313
+ expect(parseTypeScriptErrors('')).toHaveLength(0);
314
+ });
315
+ });
316
+
317
+ describe('autoFixTypeScriptErrors false success prevention', () => {
318
+ it('should return success: false when zero errors parsed on first attempt', () => {
319
+ // Simulating the logic from autoFixTypeScriptErrors
320
+ const attempts = 1;
321
+ const fixes: Array<{ file: string; description: string }> = [];
322
+
323
+ // Build failed but parseTypeScriptErrors returned 0 errors (unparseable format)
324
+ const errorsLength = 0;
325
+
326
+ // This is the logic under test
327
+ const noParsedOnFirstAttempt = attempts === 1 && fixes.length === 0;
328
+
329
+ if (errorsLength === 0 && noParsedOnFirstAttempt) {
330
+ // Should NOT return success: true
331
+ expect(noParsedOnFirstAttempt).toBe(true);
332
+ }
333
+ });
334
+
335
+ it('should return success: true when zero errors after prior fixes', () => {
336
+ // After fixing files, tsc --noEmit returns 0 errors = genuinely fixed
337
+ const attempts = 2;
338
+ const fixes = [{ file: 'src/index.ts', description: 'Fixed 1 error' }];
339
+
340
+ const errorsLength = 0;
341
+ const noParsedOnFirstAttempt = attempts === 1 && fixes.length === 0;
342
+
343
+ // Should NOT trigger the false-success guard
344
+ expect(noParsedOnFirstAttempt).toBe(false);
345
+
346
+ // So the function would return success: true (correct behavior)
347
+ if (errorsLength === 0 && !noParsedOnFirstAttempt) {
348
+ expect(true).toBe(true); // Reaches the success: true branch
349
+ }
350
+ });
351
+ });