deflake 1.2.37 → 1.2.39

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 (2) hide show
  1. package/cli.js +100 -13
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -46,15 +46,63 @@ async function main() {
46
46
  console.log(`šŸ” Detected Framework: ${C.GREEN}${fw.toUpperCase()}${C.RESET}`);
47
47
 
48
48
  if (commandToRun) {
49
- const { code, output } = await runNative(commandToRun);
50
- if (code !== 0) {
49
+ const MAX_ATTEMPTS = 5;
50
+ let { code, output } = await runNative(commandToRun);
51
+
52
+ if (code !== 0 && argv.fix) {
51
53
  console.log(`\nšŸ”“ Command failed with code ${code}. Activating DeFlake...`);
52
- const artifacts = detectFailures();
53
- const applied = await analyzeAndFix(artifacts, client, argv, output);
54
- if (applied > 0 && argv.fix) {
55
- console.log(`\n${C.BRIGHT}šŸ’‰ Fixes applied. Re-running tests to verify...${C.RESET}`);
56
- await runNative(commandToRun);
54
+
55
+ // Save backups of all source files before any modifications
56
+ const backups = new Map(); // path -> original content
57
+ let prevFailCount = Infinity;
58
+
59
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
60
+ console.log(`\n${C.BRIGHT}${C.CYAN}šŸ”„ Fix attempt ${attempt}/${MAX_ATTEMPTS}${C.RESET}`);
61
+
62
+ const artifacts = detectFailures();
63
+ const currentFailCount = artifacts.length;
64
+
65
+ if (currentFailCount === 0) {
66
+ console.log(`${C.GREEN}šŸŽ‰ All tests passing! No failures detected.${C.RESET}`);
67
+ break;
68
+ }
69
+
70
+ // If failures INCREASED since last attempt, rollback all changes
71
+ if (attempt > 1 && currentFailCount > prevFailCount) {
72
+ console.log(`${C.RED}āš ļø Failures increased (${prevFailCount} → ${currentFailCount}). Rolling back all changes...${C.RESET}`);
73
+ for (const [filePath, originalContent] of backups) {
74
+ fs.writeFileSync(filePath, originalContent);
75
+ console.log(` ${C.YELLOW}ā†©ļø Restored: ${path.basename(filePath)}${C.RESET}`);
76
+ }
77
+ break;
78
+ }
79
+ prevFailCount = currentFailCount;
80
+
81
+ const applied = await analyzeAndFix(artifacts, client, argv, output, backups);
82
+
83
+ if (applied === 0) {
84
+ console.log(`${C.YELLOW}āš ļø No fixes could be applied. Stopping retry loop.${C.RESET}`);
85
+ break;
86
+ }
87
+
88
+
89
+ console.log(`\n${C.BRIGHT}šŸ’‰ ${applied} fix(es) applied. Re-running tests to verify (attempt ${attempt})...${C.RESET}`);
90
+ const rerun = await runNative(commandToRun);
91
+ output = rerun.output; // Use new output for next iteration's AI context
92
+
93
+ if (rerun.code === 0) {
94
+ console.log(`\n${C.GREEN}${C.BRIGHT}šŸŽ‰ All tests passing after attempt ${attempt}!${C.RESET}`);
95
+ break;
96
+ }
97
+
98
+ if (attempt === MAX_ATTEMPTS) {
99
+ console.log(`\n${C.YELLOW}āš ļø Max attempts (${MAX_ATTEMPTS}) reached. Some failures may persist.${C.RESET}`);
100
+ }
57
101
  }
102
+ } else if (code !== 0) {
103
+ console.log(`\nšŸ”“ Command failed with code ${code}. Activating DeFlake...`);
104
+ const artifacts = detectFailures();
105
+ await analyzeAndFix(artifacts, client, argv, output, new Map());
58
106
  }
59
107
  }
60
108
  if (argv.report) showReport();
@@ -110,7 +158,7 @@ function detectFailures() {
110
158
  return results;
111
159
  }
112
160
 
113
- async function analyzeAndFix(artifacts, client, argv, capturedOutput = '') {
161
+ async function analyzeAndFix(artifacts, client, argv, capturedOutput = '', backups = new Map()) {
114
162
  if (artifacts.length === 0) {
115
163
  console.log(` ${C.YELLOW}āš ļø No failure artifacts detected. Run 'npx deflake doctor' to check permissions.${C.RESET}`);
116
164
  return 0;
@@ -186,7 +234,7 @@ async function analyzeAndFix(artifacts, client, argv, capturedOutput = '') {
186
234
  }
187
235
  if (fixLoc) {
188
236
  console.log(` ${C.BRIGHT}šŸ’‰ Applying patches...${C.RESET}`);
189
- if (await applyFix(res, fixLoc)) {
237
+ if (await applyFix(res, fixLoc, backups)) {
190
238
  count++;
191
239
  console.log(` ${C.GREEN}${C.BRIGHT}āœ… Fix applied to ${path.basename(fixLoc.path)}:${fixLoc.line}${C.RESET}`);
192
240
  } else {
@@ -239,7 +287,7 @@ function checkBalance(code) {
239
287
  return { balanced: true, detail: 'ok' };
240
288
  }
241
289
 
242
- async function applyFix(res, loc) {
290
+ async function applyFix(res, loc, backups = new Map()) {
243
291
  if (!loc?.path) {
244
292
  console.log(` ${C.YELLOW}āš ļø Cannot apply fix: failure location not detected.${C.RESET}`);
245
293
  return false;
@@ -251,6 +299,10 @@ async function applyFix(res, loc) {
251
299
  return false;
252
300
  }
253
301
  const original = fs.readFileSync(loc.path, 'utf8');
302
+ // Save backup before first modification
303
+ if (!backups.has(loc.path)) {
304
+ backups.set(loc.path, original);
305
+ }
254
306
  let content = original;
255
307
  let appliedCount = 0;
256
308
  for (const p of patches) {
@@ -281,9 +333,44 @@ async function applyFix(res, loc) {
281
333
  lines.splice(p.line, 0, cleanLine);
282
334
  console.log(` ${C.GREEN}+ Line ${p.line + 1}: ${cleanLine.trim()}${C.RESET}`);
283
335
  } else {
284
- lines[idx] = cleanLine;
285
- console.log(` ${C.RED}- Line ${p.line}: ${originalLine.trim()}${C.RESET}`);
286
- console.log(` ${C.GREEN}+ Line ${p.line}: ${cleanLine.trim()}${C.RESET}`);
336
+ // REPLACE action
337
+ // SMART: Handle multi-line expression replacement
338
+ // If the original line starts a multi-line expression (has unclosed parens)
339
+ // and the replacement is a complete statement, remove continuation lines
340
+ const origOpen = (originalLine.match(/\(/g) || []).length;
341
+ const origClose = (originalLine.match(/\)/g) || []).length;
342
+ const replOpen = (cleanLine.match(/\(/g) || []).length;
343
+ const replClose = (cleanLine.match(/\)/g) || []).length;
344
+
345
+ if (origOpen > origClose && replOpen <= replClose) {
346
+ // Original has unclosed parens, replacement is complete
347
+ // Find the closing of the multi-line expression
348
+ let depth = origOpen - origClose;
349
+ let endIdx = idx;
350
+ for (let j = idx + 1; j < lines.length && depth > 0; j++) {
351
+ const lo = (lines[j].match(/\(/g) || []).length;
352
+ const lc = (lines[j].match(/\)/g) || []).length;
353
+ depth += lo - lc;
354
+ endIdx = j;
355
+ }
356
+ if (endIdx > idx) {
357
+ const removedLines = endIdx - idx;
358
+ console.log(` ${C.GRAY}šŸ”§ Expanding REPLACE to cover ${removedLines + 1}-line expression (lines ${p.line}-${endIdx + 1})${C.RESET}`);
359
+ for (let j = idx; j <= endIdx; j++) {
360
+ console.log(` ${C.RED}- Line ${j + 1}: ${lines[j].trim()}${C.RESET}`);
361
+ }
362
+ lines.splice(idx, endIdx - idx + 1, cleanLine);
363
+ console.log(` ${C.GREEN}+ Line ${p.line}: ${cleanLine.trim()}${C.RESET}`);
364
+ } else {
365
+ lines[idx] = cleanLine;
366
+ console.log(` ${C.RED}- Line ${p.line}: ${originalLine.trim()}${C.RESET}`);
367
+ console.log(` ${C.GREEN}+ Line ${p.line}: ${cleanLine.trim()}${C.RESET}`);
368
+ }
369
+ } else {
370
+ lines[idx] = cleanLine;
371
+ console.log(` ${C.RED}- Line ${p.line}: ${originalLine.trim()}${C.RESET}`);
372
+ console.log(` ${C.GREEN}+ Line ${p.line}: ${cleanLine.trim()}${C.RESET}`);
373
+ }
287
374
  }
288
375
  content = lines.join('\n');
289
376
  appliedCount++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deflake",
3
- "version": "1.2.37",
3
+ "version": "1.2.39",
4
4
  "description": "AI-powered self-healing tool for Playwright, Cypress, and WebdriverIO tests.",
5
5
  "main": "client.js",
6
6
  "bin": {