lacuna-cli 0.1.5 → 0.1.7
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/README.md +83 -21
- package/dist/agent/context.d.ts +2 -0
- package/dist/agent/context.d.ts.map +1 -1
- package/dist/agent/context.js +314 -1
- package/dist/agent/context.js.map +1 -1
- package/dist/agent/fix-loop.d.ts +5 -0
- package/dist/agent/fix-loop.d.ts.map +1 -1
- package/dist/agent/fix-loop.js +376 -41
- package/dist/agent/fix-loop.js.map +1 -1
- package/dist/agent/generator.d.ts +8 -1
- package/dist/agent/generator.d.ts.map +1 -1
- package/dist/agent/generator.js +89 -11
- package/dist/agent/generator.js.map +1 -1
- package/dist/agent/loop.d.ts +8 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +108 -46
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/{prompts.d.ts → prompts/index.d.ts} +12 -2
- package/dist/agent/prompts/index.d.ts.map +1 -0
- package/dist/agent/prompts/index.js +668 -0
- package/dist/agent/prompts/index.js.map +1 -0
- package/dist/agent/prompts/nextjs.d.ts +12 -0
- package/dist/agent/prompts/nextjs.d.ts.map +1 -0
- package/dist/agent/prompts/nextjs.js +138 -0
- package/dist/agent/prompts/nextjs.js.map +1 -0
- package/dist/agent/prompts/react-native.d.ts +4 -0
- package/dist/agent/prompts/react-native.d.ts.map +1 -0
- package/dist/agent/prompts/react-native.js +82 -0
- package/dist/agent/prompts/react-native.js.map +1 -0
- package/dist/agent/prompts/react.d.ts +2 -0
- package/dist/agent/prompts/react.d.ts.map +1 -0
- package/dist/agent/prompts/react.js +48 -0
- package/dist/agent/prompts/react.js.map +1 -0
- package/dist/agent/prompts/runners/js-common.d.ts +2 -0
- package/dist/agent/prompts/runners/js-common.d.ts.map +1 -0
- package/dist/agent/prompts/runners/js-common.js +13 -0
- package/dist/agent/prompts/runners/js-common.js.map +1 -0
- package/dist/agent/prompts/runners/typescript.d.ts +2 -0
- package/dist/agent/prompts/runners/typescript.d.ts.map +1 -0
- package/dist/agent/prompts/runners/typescript.js +12 -0
- package/dist/agent/prompts/runners/typescript.js.map +1 -0
- package/dist/agent/prompts/runners/vitest.d.ts +2 -0
- package/dist/agent/prompts/runners/vitest.d.ts.map +1 -0
- package/dist/agent/prompts/runners/vitest.js +23 -0
- package/dist/agent/prompts/runners/vitest.js.map +1 -0
- package/dist/agent/prompts/vue.d.ts +3 -0
- package/dist/agent/prompts/vue.d.ts.map +1 -0
- package/dist/agent/prompts/vue.js +29 -0
- package/dist/agent/prompts/vue.js.map +1 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +43 -32
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/fix.d.ts +2 -0
- package/dist/commands/fix.d.ts.map +1 -1
- package/dist/commands/fix.js +32 -3
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +208 -32
- package/dist/commands/init.js.map +1 -1
- package/dist/lib/config.d.ts +3 -3
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +3 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/coverage/gaps.d.ts +2 -2
- package/dist/lib/coverage/gaps.d.ts.map +1 -1
- package/dist/lib/coverage/gaps.js +35 -5
- package/dist/lib/coverage/gaps.js.map +1 -1
- package/dist/lib/coverage/index.d.ts +1 -1
- package/dist/lib/coverage/index.d.ts.map +1 -1
- package/dist/lib/coverage/index.js +1 -1
- package/dist/lib/coverage/index.js.map +1 -1
- package/dist/lib/detector.d.ts +1 -0
- package/dist/lib/detector.d.ts.map +1 -1
- package/dist/lib/detector.js +41 -8
- package/dist/lib/detector.js.map +1 -1
- package/dist/lib/providers/anthropic.d.ts.map +1 -1
- package/dist/lib/providers/anthropic.js +46 -3
- package/dist/lib/providers/anthropic.js.map +1 -1
- package/dist/lib/providers/openai-compatible.d.ts +1 -1
- package/dist/lib/providers/openai-compatible.d.ts.map +1 -1
- package/dist/lib/providers/openai-compatible.js +43 -2
- package/dist/lib/providers/openai-compatible.js.map +1 -1
- package/dist/lib/providers/types.d.ts +4 -0
- package/dist/lib/providers/types.d.ts.map +1 -1
- package/dist/lib/providers/types.js +10 -0
- package/dist/lib/providers/types.js.map +1 -1
- package/dist/lib/skeleton.d.ts +4 -0
- package/dist/lib/skeleton.d.ts.map +1 -1
- package/dist/lib/skeleton.js +220 -0
- package/dist/lib/skeleton.js.map +1 -1
- package/dist/lib/validate.d.ts +1 -0
- package/dist/lib/validate.d.ts.map +1 -1
- package/dist/lib/validate.js +179 -15
- package/dist/lib/validate.js.map +1 -1
- package/dist/lib/worker-display.d.ts +10 -0
- package/dist/lib/worker-display.d.ts.map +1 -1
- package/dist/lib/worker-display.js +53 -5
- package/dist/lib/worker-display.js.map +1 -1
- package/oclif.manifest.json +16 -2
- package/package.json +1 -1
- package/dist/agent/prompts.d.ts.map +0 -1
- package/dist/agent/prompts.js +0 -632
- package/dist/agent/prompts.js.map +0 -1
- package/dist/lib/report-upload.d.ts +0 -3
- package/dist/lib/report-upload.d.ts.map +0 -1
- package/dist/lib/report-upload.js +0 -15
- package/dist/lib/report-upload.js.map +0 -1
package/dist/agent/fix-loop.d.ts
CHANGED
|
@@ -9,11 +9,16 @@ export interface FixOptions {
|
|
|
9
9
|
targetFile?: string;
|
|
10
10
|
workers?: number;
|
|
11
11
|
fresh?: boolean;
|
|
12
|
+
regenerateOnFailure?: boolean;
|
|
13
|
+
fixPolluters?: boolean;
|
|
12
14
|
log: (msg: string) => void;
|
|
13
15
|
}
|
|
14
16
|
export interface FixResult {
|
|
15
17
|
filesProcessed: number;
|
|
16
18
|
filesFixed: number;
|
|
19
|
+
filesAlreadyPassing: number;
|
|
20
|
+
pollutersFixed: number;
|
|
21
|
+
victimsRegenerated: number;
|
|
17
22
|
errors: string[];
|
|
18
23
|
}
|
|
19
24
|
export declare function runFixLoop(options: FixOptions): Promise<FixResult>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fix-loop.d.ts","sourceRoot":"","sources":["../../src/agent/fix-loop.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"fix-loop.d.ts","sourceRoot":"","sources":["../../src/agent/fix-loop.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAiB7D,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,YAAY,CAAA;IACpB,GAAG,EAAE,mBAAmB,CAAA;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,OAAO,CAAA;IACf,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAC3B;AAoCD,MAAM,WAAW,SAAS;IACxB,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,cAAc,EAAE,MAAM,CAAA;IACtB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,MAAM,EAAE,MAAM,EAAE,CAAA;CACjB;AAypBD,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAwJxE"}
|
package/dist/agent/fix-loop.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir, unlink } from 'fs/promises';
|
|
1
|
+
import { readFile, writeFile, mkdir, unlink, readdir } from 'fs/promises';
|
|
2
2
|
import { join, dirname, basename, extname } from 'path';
|
|
3
3
|
import { access, stat } from 'fs/promises';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
import { fileTestCommand } from '../lib/detector.js';
|
|
5
|
+
import { fileTestCommand, multiFileTestCommand } from '../lib/detector.js';
|
|
6
6
|
import { runCommand } from '../lib/runner.js';
|
|
7
7
|
import { startCoverageSpinner } from '../lib/coverage-spinner.js';
|
|
8
8
|
import { WorkerDisplay } from '../lib/worker-display.js';
|
|
9
9
|
import { buildFixFileContext, computeRelativeImport, collectTypeDefinitions, collectLocalImportPaths, detectReactMajorVersion } from './context.js';
|
|
10
|
-
import { TestGenerator, TruncatedOutputError, OscillationError, TRUNCATION_RETRY_MESSAGE } from './generator.js';
|
|
10
|
+
import { TestGenerator, TruncatedOutputError, OscillationError, ModelStallError, TRUNCATION_RETRY_MESSAGE, OSCILLATION_ESCAPE_MESSAGE } from './generator.js';
|
|
11
|
+
import { processGap } from './loop.js';
|
|
11
12
|
import { ProjectMemory } from './project-memory.js';
|
|
12
13
|
import { getActiveTips, createTipRotator, formatTip } from '../lib/tips.js';
|
|
13
14
|
import { typeCheckFile } from '../lib/typecheck.js';
|
|
14
|
-
import { hasTestFunctions, enrichNoTestsError, isZeroTestsOutput, parsePassCount, buildStructureBrokenMessage, buildRegressionMessage, sanitizeMocksContent, stripLeadingProse, mergeMocksContent } from '../lib/validate.js';
|
|
15
|
+
import { hasTestFunctions, enrichNoTestsError, isZeroTestsOutput, parsePassCount, buildStructureBrokenMessage, buildRegressionMessage, sanitizeMocksContent, stripLeadingProse, mergeMocksContent, deduplicateViMocks } from '../lib/validate.js';
|
|
15
16
|
import { extractTestFailure } from '../lib/extract-error.js';
|
|
16
17
|
import { StreamingFileViewer } from '../lib/streaming-viewer.js';
|
|
17
18
|
// ─── Failing-files cache ──────────────────────────────────────────────────────
|
|
@@ -46,42 +47,84 @@ async function clearFixCache(cwd) {
|
|
|
46
47
|
catch { /* already gone — fine */ }
|
|
47
48
|
}
|
|
48
49
|
// ─── Parse failing test files from runner output ──────────────────────────────
|
|
49
|
-
const TEST_FILE_RE = /[\w
|
|
50
|
+
const TEST_FILE_RE = /[\w./\\@\[\]()-]+\.(?:test|spec)\.(?:tsx|mts|ts|jsx|js)/;
|
|
51
|
+
function stripAnsi(s) {
|
|
52
|
+
// Strip all CSI sequences (ESC [ ... letter), OSC sequences, carriage returns
|
|
53
|
+
return s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '').replace(/\x1B\][^\x1B]*/g, '').replace(/\r/g, '');
|
|
54
|
+
}
|
|
50
55
|
function parseFailingTestFiles(output, runner) {
|
|
51
|
-
const files = new Set();
|
|
52
56
|
const lines = output.split('\n');
|
|
57
|
+
// Two separate sets — cross/tick pattern (per-file summary) vs FAIL pattern (per-test details)
|
|
58
|
+
const crossFiles = new Set();
|
|
59
|
+
const failFiles = new Set();
|
|
53
60
|
for (const line of lines) {
|
|
54
|
-
const clean = line
|
|
55
|
-
// Vitest: cross character followed by file path (covers multiple cross symbols across versions)
|
|
61
|
+
const clean = stripAnsi(line).trim();
|
|
56
62
|
if (runner === 'vitest' || runner === 'unknown') {
|
|
57
63
|
const m = clean.match(new RegExp(`^[×✗✕✖✘❌]\\s+(${TEST_FILE_RE.source})`));
|
|
58
64
|
if (m) {
|
|
59
|
-
|
|
65
|
+
crossFiles.add(m[1]);
|
|
60
66
|
continue;
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
|
-
// "FAIL <path>" — used by Jest and some Vitest reporters/configurations
|
|
64
69
|
if (runner === 'jest' || runner === 'vitest' || runner === 'unknown') {
|
|
65
70
|
const m = clean.match(new RegExp(`^FAIL\\s+(${TEST_FILE_RE.source})`));
|
|
66
71
|
if (m) {
|
|
67
|
-
|
|
68
|
-
continue;
|
|
72
|
+
failFiles.add(m[1]);
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
// Parse the expected failing file count from the runner summary line
|
|
77
|
+
let expectedCount = null;
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
const clean = stripAnsi(line).trim();
|
|
80
|
+
const mv = clean.match(/Test Files\s+(\d+)\s+failed/);
|
|
81
|
+
if (mv) {
|
|
82
|
+
expectedCount = parseInt(mv[1], 10);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
const mj = clean.match(/Test Suites:\s+(\d+)\s+failed/);
|
|
86
|
+
if (mj) {
|
|
87
|
+
expectedCount = parseInt(mj[1], 10);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const combined = new Set([...crossFiles, ...failFiles]);
|
|
92
|
+
if (expectedCount !== null && combined.size > expectedCount) {
|
|
93
|
+
// Over-detected: prune false positives by preferring files confirmed by both patterns,
|
|
94
|
+
// then FAIL-only (strong signal — comes from the detailed failures section),
|
|
95
|
+
// then cross-only last (more likely to include false positives).
|
|
96
|
+
const pruned = new Set();
|
|
97
|
+
for (const f of crossFiles) {
|
|
98
|
+
if (failFiles.has(f))
|
|
99
|
+
pruned.add(f);
|
|
100
|
+
}
|
|
101
|
+
for (const f of failFiles) {
|
|
102
|
+
if (pruned.size < expectedCount)
|
|
103
|
+
pruned.add(f);
|
|
104
|
+
}
|
|
105
|
+
for (const f of crossFiles) {
|
|
106
|
+
if (pruned.size < expectedCount)
|
|
107
|
+
pruned.add(f);
|
|
108
|
+
}
|
|
109
|
+
return [...pruned];
|
|
110
|
+
}
|
|
111
|
+
// Supplement with stack traces only when primary patterns under-detected
|
|
112
|
+
const needsSupplement = expectedCount !== null ? combined.size < expectedCount : combined.size === 0;
|
|
113
|
+
if (needsSupplement) {
|
|
114
|
+
let inTrace = false;
|
|
76
115
|
for (const line of lines) {
|
|
77
|
-
const clean = line
|
|
78
|
-
|
|
116
|
+
const clean = stripAnsi(line).trim();
|
|
117
|
+
if (!clean || clean.startsWith('●') || clean.startsWith('FAIL') || /^[×✗✕✖✘❌]/.test(clean)) {
|
|
118
|
+
inTrace = false;
|
|
119
|
+
}
|
|
79
120
|
const m = clean.match(new RegExp(`\\(?(${TEST_FILE_RE.source}):\\d+`));
|
|
80
|
-
if (m)
|
|
81
|
-
|
|
121
|
+
if (m && !combined.has(m[1]) && !inTrace) {
|
|
122
|
+
combined.add(m[1]);
|
|
123
|
+
inTrace = true;
|
|
124
|
+
}
|
|
82
125
|
}
|
|
83
126
|
}
|
|
84
|
-
return [...
|
|
127
|
+
return [...combined];
|
|
85
128
|
}
|
|
86
129
|
// ─── Find the source file that a test file is testing ────────────────────────
|
|
87
130
|
async function findSourceFile(testFilePath, cwd) {
|
|
@@ -94,7 +137,11 @@ async function findSourceFile(testFilePath, cwd) {
|
|
|
94
137
|
const sourceDir = basename(dir) === '__tests__' ? dirname(dir) : dir;
|
|
95
138
|
const exts = [ext, '.ts', '.tsx', '.js', '.jsx'];
|
|
96
139
|
for (const e of exts) {
|
|
97
|
-
|
|
140
|
+
// If sourceDir is absolute (testFilePath was absolute), use it directly.
|
|
141
|
+
// join(cwd, absoluteDir) doubles the path: /project/home/project/app/... which never exists.
|
|
142
|
+
const candidate = sourceDir.startsWith('/') || sourceDir.startsWith('\\')
|
|
143
|
+
? join(sourceDir, `${sourceBase}${e}`)
|
|
144
|
+
: join(cwd, sourceDir, `${sourceBase}${e}`);
|
|
98
145
|
try {
|
|
99
146
|
await access(candidate);
|
|
100
147
|
return candidate;
|
|
@@ -115,9 +162,9 @@ async function fixFile(testFilePath, options, generator, onStatus, projectMemory
|
|
|
115
162
|
const firstRun = await runCommand(fileTestCommand(env, absTestPath), cwd, 60_000);
|
|
116
163
|
if (firstRun.success) {
|
|
117
164
|
if (!onStatus)
|
|
118
|
-
log(chalk.
|
|
165
|
+
log(chalk.dim(' Already passing — skipping.'));
|
|
119
166
|
onStatus?.({ phase: 'passed', file: shortPath });
|
|
120
|
-
return { success: true };
|
|
167
|
+
return { success: true, skipped: true };
|
|
121
168
|
}
|
|
122
169
|
let errorOutput = extractTestFailure(firstRun.stdout + '\n' + firstRun.stderr);
|
|
123
170
|
const initialErrorOutput = errorOutput;
|
|
@@ -153,15 +200,25 @@ async function fixFile(testFilePath, options, generator, onStatus, projectMemory
|
|
|
153
200
|
]);
|
|
154
201
|
// Build mocks/setup context relative to the actual test file path
|
|
155
202
|
const ctx = await buildFixFileContext(absTestPath, cwd, config).catch(() => null);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
onStatus?.({ phase: 'generating', file: shortPath });
|
|
203
|
+
let stallRetries = 0;
|
|
204
|
+
const MAX_STALL_RETRIES = 2;
|
|
159
205
|
for (let attempt = 1; attempt <= config.maxIterations; attempt++) {
|
|
160
206
|
if (attempt > 1) {
|
|
161
207
|
if (!onStatus)
|
|
162
208
|
log(chalk.yellow(`\n Retry ${attempt}/${config.maxIterations}...`));
|
|
163
|
-
onStatus?.({ phase: 'retrying', file: shortPath, attempt, max: config.maxIterations });
|
|
164
209
|
}
|
|
210
|
+
// Show waiting phase before the model call; transition to generating/retrying on first token
|
|
211
|
+
onStatus?.({ phase: 'waiting', file: shortPath, since: Date.now() });
|
|
212
|
+
const currentAttempt = attempt;
|
|
213
|
+
generator.setFirstTokenCallback(() => {
|
|
214
|
+
onStatus?.({
|
|
215
|
+
phase: currentAttempt === 1 ? 'generating' : 'retrying',
|
|
216
|
+
file: shortPath,
|
|
217
|
+
...(currentAttempt > 1 ? { attempt: currentAttempt, max: config.maxIterations } : {}),
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
if (!onStatus)
|
|
221
|
+
log(chalk.dim(` ⌛ Waiting for model response...`));
|
|
165
222
|
let viewer;
|
|
166
223
|
if (verbose && !onStatus) {
|
|
167
224
|
viewer = new StreamingFileViewer(shortPath);
|
|
@@ -178,6 +235,7 @@ async function fixFile(testFilePath, options, generator, onStatus, projectMemory
|
|
|
178
235
|
sourceCode,
|
|
179
236
|
sourceImportPath,
|
|
180
237
|
errorOutput,
|
|
238
|
+
env,
|
|
181
239
|
mocksCode: ctx?.mocksCode ?? null,
|
|
182
240
|
mocksImportPath: ctx?.mocksImportPath ?? null,
|
|
183
241
|
setupFileCode: ctx?.setupFileCode ?? null,
|
|
@@ -193,6 +251,18 @@ async function fixFile(testFilePath, options, generator, onStatus, projectMemory
|
|
|
193
251
|
catch (err) {
|
|
194
252
|
viewer?.stop();
|
|
195
253
|
generator.setTokenCallback(undefined);
|
|
254
|
+
generator.setFirstTokenCallback(undefined);
|
|
255
|
+
if (err instanceof ModelStallError) {
|
|
256
|
+
if (stallRetries < MAX_STALL_RETRIES) {
|
|
257
|
+
stallRetries++;
|
|
258
|
+
if (!onStatus)
|
|
259
|
+
log(chalk.yellow(`\n ⌛ Model stalled — reconnecting (${stallRetries}/${MAX_STALL_RETRIES})...`));
|
|
260
|
+
onStatus?.({ phase: 'waiting', file: shortPath, since: Date.now() });
|
|
261
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
262
|
+
attempt--; // don't consume an AI iteration for a connection stall
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
196
266
|
if (err instanceof TruncatedOutputError) {
|
|
197
267
|
errorOutput = TRUNCATION_RETRY_MESSAGE;
|
|
198
268
|
if (!onStatus)
|
|
@@ -201,6 +271,16 @@ async function fixFile(testFilePath, options, generator, onStatus, projectMemory
|
|
|
201
271
|
continue;
|
|
202
272
|
}
|
|
203
273
|
if (err instanceof OscillationError) {
|
|
274
|
+
if (attempt < config.maxIterations) {
|
|
275
|
+
// Iterations remain — give one escape-hatch attempt with fresh oscillation state
|
|
276
|
+
// and an explicit "completely different approach" message instead of stopping.
|
|
277
|
+
if (!onStatus)
|
|
278
|
+
log(chalk.yellow(`\n ⚠ Agent loop detected — retrying with different strategy...`));
|
|
279
|
+
onStatus?.({ phase: 'retrying', file: shortPath, attempt, max: config.maxIterations });
|
|
280
|
+
generator.resetOscillationState();
|
|
281
|
+
errorOutput = OSCILLATION_ESCAPE_MESSAGE;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
204
284
|
if (!onStatus)
|
|
205
285
|
log(chalk.red(`\n ⚠ Agent loop detected — output identical to a previous attempt. Stopping early.`));
|
|
206
286
|
onStatus?.({ phase: 'failed', file: shortPath });
|
|
@@ -215,6 +295,7 @@ async function fixFile(testFilePath, options, generator, onStatus, projectMemory
|
|
|
215
295
|
}
|
|
216
296
|
viewer?.stop();
|
|
217
297
|
generator.setTokenCallback(undefined);
|
|
298
|
+
generator.setFirstTokenCallback(undefined);
|
|
218
299
|
if (dryRun) {
|
|
219
300
|
if (!onStatus) {
|
|
220
301
|
log(chalk.yellow('\n [dry-run] Would write:'));
|
|
@@ -255,6 +336,7 @@ async function fixFile(testFilePath, options, generator, onStatus, projectMemory
|
|
|
255
336
|
}
|
|
256
337
|
}
|
|
257
338
|
}
|
|
339
|
+
testFileContent = deduplicateViMocks(testFileContent);
|
|
258
340
|
// Catch empty test files before writing
|
|
259
341
|
if (!hasTestFunctions(testFileContent)) {
|
|
260
342
|
errorOutput =
|
|
@@ -288,11 +370,15 @@ async function fixFile(testFilePath, options, generator, onStatus, projectMemory
|
|
|
288
370
|
return { success: true };
|
|
289
371
|
}
|
|
290
372
|
const rawRunOutput = result.stdout + '\n' + result.stderr;
|
|
291
|
-
const
|
|
373
|
+
const rawExtracted = extractTestFailure(rawRunOutput);
|
|
292
374
|
const structureBroken = isZeroTestsOutput(rawRunOutput);
|
|
293
375
|
const currentPassCount = structureBroken ? 0 : parsePassCount(rawRunOutput);
|
|
376
|
+
// enrichNoTestsError adds guidance for genuinely missing test functions;
|
|
377
|
+
// in the structure-broken path the issue is always a broken import, so use
|
|
378
|
+
// rawExtracted there so the actual module error isn't buried in boilerplate.
|
|
379
|
+
const extracted = enrichNoTestsError(rawExtracted);
|
|
294
380
|
if (structureBroken) {
|
|
295
|
-
errorOutput = buildStructureBrokenMessage(initialErrorOutput,
|
|
381
|
+
errorOutput = buildStructureBrokenMessage(initialErrorOutput, rawExtracted);
|
|
296
382
|
if (!onStatus)
|
|
297
383
|
log(chalk.red(` Fix broke file structure — 0 tests collected (attempt ${attempt}/${config.maxIterations})`));
|
|
298
384
|
}
|
|
@@ -317,13 +403,210 @@ async function fixFile(testFilePath, options, generator, onStatus, projectMemory
|
|
|
317
403
|
error: `Still failing after ${config.maxIterations} attempts. Last error:\n${errorOutput.slice(0, 1500)}`,
|
|
318
404
|
};
|
|
319
405
|
}
|
|
406
|
+
// ─── Polluter detection ───────────────────────────────────────────────────────
|
|
407
|
+
function buildTestFileRegex(pattern) {
|
|
408
|
+
const filename = pattern.split('/').pop() ?? pattern;
|
|
409
|
+
const regexStr = filename
|
|
410
|
+
.replace(/\{([^}]+)\}/g, (_, g) => `(${g.split(',').map((s) => s.trim()).join('|')})`)
|
|
411
|
+
.replace(/\./g, '\\.')
|
|
412
|
+
.replace(/\*+/g, '[^/]+');
|
|
413
|
+
return new RegExp(regexStr + '$');
|
|
414
|
+
}
|
|
415
|
+
async function discoverTestFiles(cwd, env) {
|
|
416
|
+
const testRe = buildTestFileRegex(env.testFilePattern);
|
|
417
|
+
const files = [];
|
|
418
|
+
const skipDirs = new Set(['node_modules', 'dist', '.git', 'coverage', '.nyc_output', '.lacuna']);
|
|
419
|
+
async function walk(dir) {
|
|
420
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
421
|
+
for (const e of entries) {
|
|
422
|
+
if (e.isDirectory()) {
|
|
423
|
+
if (!skipDirs.has(e.name))
|
|
424
|
+
await walk(join(dir, e.name));
|
|
425
|
+
}
|
|
426
|
+
else if (testRe.test(e.name)) {
|
|
427
|
+
files.push(join(dir, e.name));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
await walk(cwd);
|
|
432
|
+
return files.sort();
|
|
433
|
+
}
|
|
434
|
+
function victimInFailing(victim, failing, cwd) {
|
|
435
|
+
const rel = (p) => (p.startsWith(cwd + '/') ? p.slice(cwd.length + 1) : p);
|
|
436
|
+
const shortVictim = rel(victim);
|
|
437
|
+
return failing.some(f => {
|
|
438
|
+
const shortF = rel(f);
|
|
439
|
+
return shortF === shortVictim || shortVictim.endsWith(shortF) || shortF.endsWith(shortVictim);
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
async function victimFailsWithSubset(victim, subset, env, cwd) {
|
|
443
|
+
if (subset.length === 0)
|
|
444
|
+
return false;
|
|
445
|
+
const result = await runCommand(multiFileTestCommand(env, [...subset, victim]), cwd, 120_000);
|
|
446
|
+
if (result.success)
|
|
447
|
+
return false;
|
|
448
|
+
const failing = parseFailingTestFiles(result.stdout + '\n' + result.stderr, env.testRunner);
|
|
449
|
+
return victimInFailing(victim, failing, cwd);
|
|
450
|
+
}
|
|
451
|
+
async function bisectPolluter(victim, candidates, env, cwd) {
|
|
452
|
+
if (candidates.length === 0)
|
|
453
|
+
return null;
|
|
454
|
+
if (candidates.length === 1) {
|
|
455
|
+
const fails = await victimFailsWithSubset(victim, candidates, env, cwd);
|
|
456
|
+
return fails ? candidates[0] : null;
|
|
457
|
+
}
|
|
458
|
+
const mid = Math.floor(candidates.length / 2);
|
|
459
|
+
const left = candidates.slice(0, mid);
|
|
460
|
+
const right = candidates.slice(mid);
|
|
461
|
+
if (await victimFailsWithSubset(victim, left, env, cwd))
|
|
462
|
+
return bisectPolluter(victim, left, env, cwd);
|
|
463
|
+
if (await victimFailsWithSubset(victim, right, env, cwd))
|
|
464
|
+
return bisectPolluter(victim, right, env, cwd);
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
async function findAndFixPolluters(victimFiles, options, projectMemory) {
|
|
468
|
+
const { config, env, cwd, log } = options;
|
|
469
|
+
const allTestFiles = await discoverTestFiles(cwd, env);
|
|
470
|
+
log(chalk.dim(` Discovered ${allTestFiles.length} test files to search.`));
|
|
471
|
+
const generator = new TestGenerator({ config, env });
|
|
472
|
+
let pollutersFixed = 0;
|
|
473
|
+
let victimsRegenerated = 0;
|
|
474
|
+
const seenPolluters = new Set();
|
|
475
|
+
const unresolvedVictims = [];
|
|
476
|
+
for (const victim of victimFiles) {
|
|
477
|
+
const shortVictim = victim.replace(cwd + '/', '');
|
|
478
|
+
log(chalk.dim(`\n Bisecting for: ${chalk.cyan(shortVictim)}`));
|
|
479
|
+
const candidates = allTestFiles.filter(f => f !== victim);
|
|
480
|
+
// Probe: verify the pollution reproduces before spending O(log N) bisect runs.
|
|
481
|
+
// If it doesn't reproduce here, the pollution requires vitest's default multi-worker
|
|
482
|
+
// config to manifest and can't be found by this approach.
|
|
483
|
+
log(chalk.dim(` Probing (${candidates.length} files + victim)...`));
|
|
484
|
+
const reproduced = await victimFailsWithSubset(victim, candidates, env, cwd);
|
|
485
|
+
if (!reproduced) {
|
|
486
|
+
log(chalk.yellow(` Pollution did not reproduce in sequential mode — this is concurrency-based globalThis contamination.`));
|
|
487
|
+
log(chalk.dim(` A vi.spyOn(global, ...) spy from another file is persisting in the shared worker thread.`));
|
|
488
|
+
log(chalk.dim(` Fix: add restoreMocks: true and clearMocks: true to the test: {} block in vitest.config.ts`));
|
|
489
|
+
log(chalk.dim(` Also add beforeEach(() => vi.restoreAllMocks()) to your test setup file.`));
|
|
490
|
+
unresolvedVictims.push(victim);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const polluter = await bisectPolluter(victim, candidates, env, cwd);
|
|
494
|
+
if (!polluter) {
|
|
495
|
+
log(chalk.yellow(` Could not isolate a polluter — file may have an internal spy lifecycle bug.`));
|
|
496
|
+
unresolvedVictims.push(victim);
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
const shortPolluter = polluter.replace(cwd + '/', '');
|
|
500
|
+
log(` Found polluter: ${chalk.cyan(shortPolluter)}`);
|
|
501
|
+
if (seenPolluters.has(polluter)) {
|
|
502
|
+
log(chalk.dim(` Already processed ${shortPolluter}.`));
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
seenPolluters.add(polluter);
|
|
506
|
+
// Capture the victim's failure output when run after the polluter
|
|
507
|
+
const errorRun = await runCommand(multiFileTestCommand(env, [polluter, victim]), cwd, 60_000);
|
|
508
|
+
const victimError = extractTestFailure(errorRun.stdout + '\n' + errorRun.stderr);
|
|
509
|
+
const pollutorCode = await readFile(polluter, 'utf-8').catch(() => null);
|
|
510
|
+
const victimCode = await readFile(victim, 'utf-8').catch(() => null);
|
|
511
|
+
if (!pollutorCode || !victimCode) {
|
|
512
|
+
log(chalk.red(` Could not read files — skipping ${shortPolluter}`));
|
|
513
|
+
unresolvedVictims.push(victim);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
log(chalk.dim(` Sending to ${config.model} for cleanup...`));
|
|
517
|
+
let fixed;
|
|
518
|
+
try {
|
|
519
|
+
fixed = await generator.fixPollution({
|
|
520
|
+
pollutorFile: shortPolluter,
|
|
521
|
+
pollutorCode,
|
|
522
|
+
victimFile: shortVictim,
|
|
523
|
+
victimCode,
|
|
524
|
+
victimError,
|
|
525
|
+
env,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
catch (err) {
|
|
529
|
+
log(chalk.red(` AI error: ${err instanceof Error ? err.message : String(err)}`));
|
|
530
|
+
unresolvedVictims.push(victim);
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
await writeFile(polluter, fixed, 'utf-8');
|
|
534
|
+
const verifyRun = await runCommand(multiFileTestCommand(env, [polluter, victim]), cwd, 60_000);
|
|
535
|
+
const verifyFailing = parseFailingTestFiles(verifyRun.stdout + '\n' + verifyRun.stderr, env.testRunner);
|
|
536
|
+
const victimResolved = !victimInFailing(victim, verifyFailing, cwd);
|
|
537
|
+
if (victimResolved) {
|
|
538
|
+
log(chalk.green(` Cleanup applied: ${shortPolluter}`));
|
|
539
|
+
pollutersFixed++;
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
log(chalk.red(` Cleanup did not resolve the victim — restoring ${shortPolluter}`));
|
|
543
|
+
await writeFile(polluter, pollutorCode, 'utf-8').catch(() => { });
|
|
544
|
+
unresolvedVictims.push(victim);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// Phase 2: regenerate victims that bisection couldn't resolve.
|
|
548
|
+
// These files pass alone but fail in the suite due to internal bugs
|
|
549
|
+
// (e.g. module-level vi.spyOn, wrong mock structure). A fresh generation
|
|
550
|
+
// produces properly-structured tests with spies inside beforeEach.
|
|
551
|
+
if (unresolvedVictims.length > 0 && options.regenerateOnFailure !== false) {
|
|
552
|
+
log(chalk.bold(`\n Regenerating ${unresolvedVictims.length} victim file(s) that couldn't be resolved by polluter cleanup...`));
|
|
553
|
+
for (const victim of unresolvedVictims) {
|
|
554
|
+
const shortVictim = victim.replace(cwd + '/', '');
|
|
555
|
+
log(chalk.dim(`\n Regenerating: ${chalk.cyan(shortVictim)}`));
|
|
556
|
+
const result = await regenerateFile(victim, options, undefined, projectMemory);
|
|
557
|
+
if (result.success) {
|
|
558
|
+
log(chalk.green(` Regenerated successfully.`));
|
|
559
|
+
victimsRegenerated++;
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
log(chalk.red(` Regeneration failed: ${result.error?.slice(0, 200) ?? 'unknown error'}`));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return { pollutersFixed, victimsRegenerated };
|
|
567
|
+
}
|
|
568
|
+
// ─── Regeneration fallback ────────────────────────────────────────────────────
|
|
569
|
+
async function regenerateFile(testFilePath, options, onStatus, projectMemory) {
|
|
570
|
+
const absTestFile = testFilePath.startsWith('/') ? testFilePath : join(options.cwd, testFilePath);
|
|
571
|
+
// Find the source file so processGap gets the right starting point.
|
|
572
|
+
// processGap expects gap.filePath to be the SOURCE file, not the test file.
|
|
573
|
+
const sourceFile = await findSourceFile(absTestFile, options.cwd);
|
|
574
|
+
if (!sourceFile) {
|
|
575
|
+
return { success: false, error: `Could not find source file for ${absTestFile}` };
|
|
576
|
+
}
|
|
577
|
+
// Delete the broken test file before regenerating. If it stays on disk,
|
|
578
|
+
// buildFileContext reads it as existingTestCode and the generate prompt says
|
|
579
|
+
// "preserve all existing tests" — locking the AI into the same broken structure.
|
|
580
|
+
await unlink(absTestFile).catch(() => { });
|
|
581
|
+
const gap = { filePath: sourceFile, uncoveredLines: [], uncoveredFunctions: [] };
|
|
582
|
+
const generator = new TestGenerator({ config: options.config, env: options.env });
|
|
583
|
+
// processGap uses gap.filePath (the source file) as its display identifier, but during
|
|
584
|
+
// regen the worker should stay in 'regenerating' for all intermediate phases and only
|
|
585
|
+
// flip to passed/failed at the end. This prevents the brief flash where 'regenerating'
|
|
586
|
+
// gets overwritten by 'generating' (<80ms) as soon as processGap starts.
|
|
587
|
+
const testShortPath = absTestFile.replace(options.cwd + '/', '');
|
|
588
|
+
const regenOnStatus = onStatus
|
|
589
|
+
? (state) => {
|
|
590
|
+
if (state.phase === 'passed' || state.phase === 'failed') {
|
|
591
|
+
onStatus('file' in state ? { ...state, file: testShortPath } : state);
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
onStatus({ phase: 'regenerating', file: testShortPath });
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
: undefined;
|
|
598
|
+
const result = await processGap(gap, options, generator, true, regenOnStatus, projectMemory);
|
|
599
|
+
return { success: result.success, error: result.error };
|
|
600
|
+
}
|
|
320
601
|
// ─── Worker pool ──────────────────────────────────────────────────────────────
|
|
321
602
|
async function runFixWorkers(testFiles, options, workerCount, projectMemory) {
|
|
322
603
|
const queue = [...testFiles];
|
|
323
604
|
let filesProcessed = 0;
|
|
324
605
|
let filesFixed = 0;
|
|
606
|
+
let filesAlreadyPassing = 0;
|
|
325
607
|
const errors = [];
|
|
326
608
|
const stillFailingFiles = [];
|
|
609
|
+
const victimFiles = [];
|
|
327
610
|
const tips = getActiveTips({
|
|
328
611
|
workers: workerCount,
|
|
329
612
|
targetFile: options.targetFile,
|
|
@@ -344,10 +627,32 @@ async function runFixWorkers(testFiles, options, workerCount, projectMemory) {
|
|
|
344
627
|
if (!file)
|
|
345
628
|
break;
|
|
346
629
|
const onStatus = (state) => display.update(wi, state);
|
|
347
|
-
const
|
|
630
|
+
const absFile = file.startsWith('/') ? file : join(options.cwd, file);
|
|
631
|
+
const workerOptions = { ...options, log: () => { }, verbose: false };
|
|
632
|
+
const result = await fixFile(absFile, workerOptions, generator, onStatus, projectMemory);
|
|
348
633
|
filesProcessed++;
|
|
349
|
-
if (result.success)
|
|
350
|
-
|
|
634
|
+
if (result.success) {
|
|
635
|
+
if (result.skipped) {
|
|
636
|
+
filesAlreadyPassing++;
|
|
637
|
+
victimFiles.push(absFile);
|
|
638
|
+
}
|
|
639
|
+
else
|
|
640
|
+
filesFixed++;
|
|
641
|
+
}
|
|
642
|
+
else if (options.regenerateOnFailure) {
|
|
643
|
+
// Signal 'regenerating' first — this undoes the 'failed' done-count from fixFile
|
|
644
|
+
// so the regen's final phase is the single counted outcome for this file.
|
|
645
|
+
onStatus?.({ phase: 'regenerating', file: absFile.replace(options.cwd + '/', '') });
|
|
646
|
+
const regenResult = await regenerateFile(absFile, workerOptions, onStatus, projectMemory);
|
|
647
|
+
if (regenResult.success) {
|
|
648
|
+
filesFixed++;
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
stillFailingFiles.push(file);
|
|
652
|
+
if (regenResult.error)
|
|
653
|
+
errors.push(regenResult.error);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
351
656
|
else {
|
|
352
657
|
stillFailingFiles.push(file);
|
|
353
658
|
if (result.error)
|
|
@@ -356,7 +661,7 @@ async function runFixWorkers(testFiles, options, workerCount, projectMemory) {
|
|
|
356
661
|
}
|
|
357
662
|
}));
|
|
358
663
|
display.finish();
|
|
359
|
-
return { filesProcessed, filesFixed, errors, stillFailingFiles };
|
|
664
|
+
return { filesProcessed, filesFixed, filesAlreadyPassing, errors, stillFailingFiles, victimFiles };
|
|
360
665
|
}
|
|
361
666
|
// ─── Main entry point ─────────────────────────────────────────────────────────
|
|
362
667
|
export async function runFixLoop(options) {
|
|
@@ -374,7 +679,7 @@ export async function runFixLoop(options) {
|
|
|
374
679
|
spinner.stop();
|
|
375
680
|
if (fileResult.success) {
|
|
376
681
|
log(chalk.green('\n All tests are passing — nothing to fix.'));
|
|
377
|
-
return { filesProcessed: 0, filesFixed: 0, errors: [] };
|
|
682
|
+
return { filesProcessed: 0, filesFixed: 0, filesAlreadyPassing: 0, pollutersFixed: 0, victimsRegenerated: 0, errors: [] };
|
|
378
683
|
}
|
|
379
684
|
failingFiles = [absTarget];
|
|
380
685
|
}
|
|
@@ -396,7 +701,7 @@ export async function runFixLoop(options) {
|
|
|
396
701
|
}
|
|
397
702
|
if (suiteResult.success) {
|
|
398
703
|
log(chalk.green('\n All tests are passing — nothing to fix.'));
|
|
399
|
-
return { filesProcessed: 0, filesFixed: 0, errors: [] };
|
|
704
|
+
return { filesProcessed: 0, filesFixed: 0, filesAlreadyPassing: 0, pollutersFixed: 0, victimsRegenerated: 0, errors: [] };
|
|
400
705
|
}
|
|
401
706
|
failingFiles = parseFailingTestFiles(suiteResult.stdout + suiteResult.stderr, env.testRunner);
|
|
402
707
|
failingFiles = failingFiles.filter((f) => {
|
|
@@ -413,7 +718,7 @@ export async function runFixLoop(options) {
|
|
|
413
718
|
.join('\n');
|
|
414
719
|
if (lastLines)
|
|
415
720
|
log(chalk.dim('\n Last output lines:\n' + lastLines.split('\n').map((l) => ` ${l}`).join('\n')));
|
|
416
|
-
return { filesProcessed: 0, filesFixed: 0, errors: [] };
|
|
721
|
+
return { filesProcessed: 0, filesFixed: 0, filesAlreadyPassing: 0, pollutersFixed: 0, victimsRegenerated: 0, errors: [] };
|
|
417
722
|
}
|
|
418
723
|
await saveFixCache(cwd, failingFiles);
|
|
419
724
|
}
|
|
@@ -429,17 +734,21 @@ export async function runFixLoop(options) {
|
|
|
429
734
|
const memorySnapshot = memory.toPromptSection();
|
|
430
735
|
let filesProcessed;
|
|
431
736
|
let filesFixed;
|
|
737
|
+
let filesAlreadyPassing;
|
|
432
738
|
let errors;
|
|
433
739
|
let stillFailingFiles;
|
|
740
|
+
let victimFiles;
|
|
434
741
|
if (parallel) {
|
|
435
742
|
;
|
|
436
|
-
({ filesProcessed, filesFixed, errors, stillFailingFiles } = await runFixWorkers(failingFiles, options, workerCount, memorySnapshot));
|
|
743
|
+
({ filesProcessed, filesFixed, filesAlreadyPassing, errors, stillFailingFiles, victimFiles } = await runFixWorkers(failingFiles, options, workerCount, memorySnapshot));
|
|
437
744
|
}
|
|
438
745
|
else {
|
|
439
746
|
filesProcessed = 0;
|
|
440
747
|
filesFixed = 0;
|
|
748
|
+
filesAlreadyPassing = 0;
|
|
441
749
|
errors = [];
|
|
442
750
|
stillFailingFiles = [];
|
|
751
|
+
victimFiles = [];
|
|
443
752
|
const generator = new TestGenerator({ config, env });
|
|
444
753
|
const tips = getActiveTips({
|
|
445
754
|
workers: 1,
|
|
@@ -460,8 +769,26 @@ export async function runFixLoop(options) {
|
|
|
460
769
|
const absFile = file.startsWith('/') ? file : join(cwd, file);
|
|
461
770
|
const result = await fixFile(absFile, options, generator, undefined, memory.toPromptSection());
|
|
462
771
|
filesProcessed++;
|
|
463
|
-
if (result.success)
|
|
464
|
-
|
|
772
|
+
if (result.success) {
|
|
773
|
+
if (result.skipped) {
|
|
774
|
+
filesAlreadyPassing++;
|
|
775
|
+
victimFiles.push(absFile);
|
|
776
|
+
}
|
|
777
|
+
else
|
|
778
|
+
filesFixed++;
|
|
779
|
+
}
|
|
780
|
+
else if (options.regenerateOnFailure) {
|
|
781
|
+
log(chalk.yellow(` Fix exhausted — falling back to full regeneration...`));
|
|
782
|
+
const regenResult = await regenerateFile(absFile, options, undefined, memory.toPromptSection());
|
|
783
|
+
if (regenResult.success) {
|
|
784
|
+
filesFixed++;
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
stillFailingFiles.push(file);
|
|
788
|
+
if (regenResult.error)
|
|
789
|
+
errors.push(regenResult.error);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
465
792
|
else {
|
|
466
793
|
stillFailingFiles.push(file);
|
|
467
794
|
if (result.error)
|
|
@@ -479,6 +806,14 @@ export async function runFixLoop(options) {
|
|
|
479
806
|
else
|
|
480
807
|
await clearFixCache(cwd);
|
|
481
808
|
}
|
|
482
|
-
|
|
809
|
+
let pollutersFixed = 0;
|
|
810
|
+
let victimsRegenerated = 0;
|
|
811
|
+
if (options.fixPolluters && victimFiles.length > 0) {
|
|
812
|
+
log(chalk.bold(`\n Scanning for test polluters (${victimFiles.length} victim file(s) pass alone but fail in suite)...`));
|
|
813
|
+
const polluterResult = await findAndFixPolluters(victimFiles, options, memorySnapshot);
|
|
814
|
+
pollutersFixed = polluterResult.pollutersFixed;
|
|
815
|
+
victimsRegenerated = polluterResult.victimsRegenerated;
|
|
816
|
+
}
|
|
817
|
+
return { filesProcessed, filesFixed, filesAlreadyPassing, pollutersFixed, victimsRegenerated, errors };
|
|
483
818
|
}
|
|
484
819
|
//# sourceMappingURL=fix-loop.js.map
|