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.
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +6 -0
- package/dist/adapters/claude.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +82 -5
- package/dist/cli/interactive.js.map +1 -1
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +3 -2
- package/dist/state/index.js.map +1 -1
- package/dist/types/workflow.d.ts +12 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +4 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/workflow/auto-fix.d.ts +17 -1
- package/dist/workflow/auto-fix.d.ts.map +1 -1
- package/dist/workflow/auto-fix.js +97 -16
- package/dist/workflow/auto-fix.js.map +1 -1
- package/dist/workflow/execution-mode.d.ts +24 -0
- package/dist/workflow/execution-mode.d.ts.map +1 -1
- package/dist/workflow/execution-mode.js +293 -19
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +1 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/milestone-workflow.d.ts.map +1 -1
- package/dist/workflow/milestone-workflow.js +63 -3
- package/dist/workflow/milestone-workflow.js.map +1 -1
- package/dist/workflow/project-structure.d.ts +29 -0
- package/dist/workflow/project-structure.d.ts.map +1 -0
- package/dist/workflow/project-structure.js +193 -0
- package/dist/workflow/project-structure.js.map +1 -0
- package/dist/workflow/project-verification.d.ts +24 -1
- package/dist/workflow/project-verification.d.ts.map +1 -1
- package/dist/workflow/project-verification.js +105 -22
- package/dist/workflow/project-verification.js.map +1 -1
- package/dist/workflow/remediation.d.ts +124 -0
- package/dist/workflow/remediation.d.ts.map +1 -0
- package/dist/workflow/remediation.js +510 -0
- package/dist/workflow/remediation.js.map +1 -0
- package/dist/workflow/task-workflow.d.ts.map +1 -1
- package/dist/workflow/task-workflow.js +202 -4
- package/dist/workflow/task-workflow.js.map +1 -1
- package/dist/workflow/ui-setup.d.ts +1 -0
- package/dist/workflow/ui-setup.d.ts.map +1 -1
- package/dist/workflow/ui-setup.js +1 -1
- package/dist/workflow/ui-setup.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/claude.ts +6 -0
- package/src/cli/interactive.ts +85 -5
- package/src/state/index.ts +3 -2
- package/src/types/workflow.ts +9 -0
- package/src/workflow/auto-fix.ts +123 -18
- package/src/workflow/execution-mode.ts +365 -20
- package/src/workflow/index.ts +1 -0
- package/src/workflow/milestone-workflow.ts +80 -3
- package/src/workflow/project-structure.ts +233 -0
- package/src/workflow/project-verification.ts +128 -21
- package/src/workflow/remediation.ts +711 -0
- package/src/workflow/task-workflow.ts +237 -4
- package/src/workflow/ui-setup.ts +2 -1
- package/tests/workflow/auto-fix-enhanced.test.ts +351 -0
- package/tests/workflow/project-structure.test.ts +131 -0
- package/tests/workflow/project-verification.test.ts +130 -0
- 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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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) {
|
package/src/workflow/ui-setup.ts
CHANGED
|
@@ -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
|
+
});
|