deflake 1.2.9 → 1.2.13

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 +138 -90
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -427,99 +427,77 @@ function extractFailureLocation(logText) {
427
427
  // --- COLORS Moved to top ---
428
428
 
429
429
  function printDetailedFix(fixText, location, sourceCode = null, isApplied = false) {
430
-
431
- let fixCode = fixText;
430
+ let patches = [];
432
431
  let explanation = null;
433
432
 
434
433
  try {
435
- const parsed = JSON.parse(fixText);
436
- if (parsed.code) {
437
- fixCode = parsed.code;
438
- explanation = parsed.reason;
434
+ // Strip markdown if present
435
+ const cleaned = fixText.replace(/```json\n?/, '').replace(/```/, '').trim();
436
+ const parsed = JSON.parse(cleaned);
437
+ explanation = parsed.explanation || parsed.reason;
438
+ if (parsed.patches && Array.isArray(parsed.patches)) {
439
+ patches = parsed.patches;
440
+ } else if (parsed.code) {
441
+ patches = [{
442
+ file: location ? (location.specFile || location.rootFile) : 'unknown',
443
+ line: location ? (location.testLine || location.rootLine) : 0,
444
+ action: 'REPLACE',
445
+ new_line: parsed.code
446
+ }];
447
+ }
448
+ } catch (e) {
449
+ // If it looks like JSON but couldn't be parsed, try harder or fallback
450
+ if (fixText.trim().startsWith('{')) {
451
+ try {
452
+ const match = fixText.match(/\{[\s\S]*\}/);
453
+ if (match) {
454
+ const parsed = JSON.parse(match[0]);
455
+ explanation = parsed.explanation || parsed.reason;
456
+ patches = parsed.patches || (parsed.code ? [{ file: 'target', line: 0, action: 'REPLACE', new_line: parsed.code }] : []);
457
+ }
458
+ } catch (e2) {}
439
459
  }
440
- } catch (e) { }
460
+
461
+ if (patches.length === 0) {
462
+ patches = [{
463
+ file: location ? (location.specFile || location.rootFile) : 'unknown',
464
+ line: location ? (location.testLine || location.rootLine) : 0,
465
+ action: 'REPLACE',
466
+ new_line: fixText
467
+ }];
468
+ }
469
+ }
441
470
 
442
471
  console.log("\n" + C.GRAY + "─".repeat(50) + C.RESET);
443
472
 
444
473
  if (isApplied) {
445
- // --- APPLIED VIEW ---
446
474
  console.log(`${C.GREEN}${C.BRIGHT}✅ FIX APPLIED:${C.RESET}`);
447
475
  if (location) {
448
- // Label is default color, Value is colored
449
476
  const fileLabel = location.specFile || location.rootFile;
450
477
  let lineLabel = location.testLine || location.rootLine;
451
-
452
478
  if (fileLabel) {
453
479
  console.log(`${C.BRIGHT}📄 File:${C.RESET} ${fileLabel}:${lineLabel}`);
454
480
  }
455
481
  }
456
482
 
457
- if (explanation) { // Highlight the reason as requested by user
483
+ if (explanation) {
458
484
  console.log(`\n${C.BRIGHT}ℹ️ Reason:${C.RESET} ${explanation}`);
459
485
  }
460
486
 
461
- console.log(C.GRAY + "─".repeat(20) + C.RESET);
462
-
463
- // --- OLD CODE (Context) ---
464
- if (sourceCode && location && location.rootLine) {
465
- console.log(`${C.RED}🔴 OLD:${C.RESET}`);
466
- try {
467
- const lines = sourceCode.split('\n');
468
- const centerIdx = parseInt(location.rootLine) - 1;
469
- // Show a small window around the error
470
- const start = Math.max(0, centerIdx - 2);
471
- const end = Math.min(lines.length - 1, centerIdx + 2);
472
-
473
- for (let i = start; i <= end; i++) {
474
- let prefix = (i === centerIdx) ? "> " : " ";
475
- let line = lines[i];
476
- if (i === centerIdx) line = `${C.RED}${line}${C.RESET}`;
477
- else line = `${C.GRAY}${line}${C.RESET}`;
478
- console.log(prefix + line);
479
- }
480
- } catch (e) { }
487
+ for (const patch of patches) {
488
+ console.log(C.GRAY + "─".repeat(20) + C.RESET);
489
+ console.log(`${C.BRIGHT}📄 File:${C.RESET} ${path.basename(patch.file)}:${patch.line} [${patch.action}]`);
490
+ console.log(`\n${C.GREEN}${C.BRIGHT}🟢 NEW:${C.RESET}\n ${C.GREEN}${patch.new_line.trim()}${C.RESET}`);
481
491
  }
482
-
483
- // --- NEW CODE ---
484
- console.log(`\n${C.GREEN}🟢 NEW:${C.RESET}`);
485
- fixCode.split('\n').forEach(line => {
486
- let colored = line
487
- .replace(/(\/\/.*)/g, `${C.GRAY}$1${C.RESET}`)
488
- .replace(/\b(const|let|var|await|async|function|return)\b/g, `${C.YELLOW}$1${C.RESET}`)
489
- .replace(/('.*?')|(".*?")|(`.*?`)/g, `${C.GREEN}$1${C.RESET}`)
490
- .replace(/(\.click|\.fill|\.locator)/g, `${C.CYAN}$1${C.RESET}`);
491
- console.log(" " + colored);
492
- });
493
-
494
492
  } else {
495
- // --- SUGGESTION VIEW (Default) ---
496
- if (location) {
497
- if (location.rootFile && location.rootLine) {
498
- console.log(`💥 Runtime Error: ${C.RED}${location.rootFile}:${location.rootLine}${C.RESET}`);
499
- }
500
- if (location.specFile) {
501
- let targetLabel = location.specFile;
502
- if (location.testLine) targetLabel += `:${location.testLine}`;
503
- console.log(`🎯 Fix Target: ${C.CYAN}${targetLabel} (Definition)${C.RESET}`);
504
- }
493
+ console.log(`${C.CYAN}${C.BRIGHT}💡 SUGGESTED FIX:${C.RESET}`);
494
+ if (explanation) {
495
+ console.log(`\n${C.BRIGHT}ℹ️ Reason:${C.RESET} ${explanation}`);
496
+ }
497
+ for (const patch of patches) {
498
+ console.log(` - ${C.CYAN}${path.basename(patch.file)}:${patch.line}${C.RESET}: ${patch.new_line.trim()}`);
505
499
  }
506
- console.log(C.GRAY + "─".repeat(50) + C.RESET);
507
-
508
- console.log(`${C.GREEN}${C.BRIGHT}✨ DEFLAKE SUGGESTION:${C.RESET}`);
509
- if (explanation) console.log(`${C.GRAY}// ${explanation}${C.RESET}`);
510
-
511
- // Print code with simple coloring
512
- fixCode.split('\n').forEach(line => {
513
- let colored = line
514
- .replace(/(\/\/.*)/g, `${C.GRAY}$1${C.RESET}`)
515
- .replace(/\b(const|let|var|await|async|function|return)\b/g, `${C.YELLOW}$1${C.RESET}`)
516
- .replace(/('.*?')|(".*?")|(`.*?`)/g, `${C.GREEN}$1${C.RESET}`)
517
- .replace(/(\.click|\.fill|\.locator)/g, `${C.CYAN}$1${C.RESET}`);
518
- console.log(" " + colored);
519
- });
520
500
  }
521
-
522
- console.log(C.GRAY + "─".repeat(50) + C.RESET);
523
501
  }
524
502
 
525
503
  /**
@@ -715,7 +693,6 @@ async function runDoctor(argv) {
715
693
  }
716
694
 
717
695
  async function applySelfHealing(result) {
718
-
719
696
  if (!result.location || !result.location.fullRootPath || !result.fix) return;
720
697
 
721
698
  try {
@@ -727,30 +704,97 @@ async function applySelfHealing(result) {
727
704
  return;
728
705
  }
729
706
 
730
- let fixCode = result.fix;
707
+ let patches = [];
731
708
  try {
732
709
  const parsed = JSON.parse(result.fix);
733
- if (parsed.code) fixCode = parsed.code;
734
- } catch (e) { }
710
+ if (parsed.patches && Array.isArray(parsed.patches)) {
711
+ patches = parsed.patches;
712
+ } else if (parsed.code) {
713
+ patches = [{
714
+ file: filePath,
715
+ line: targetLine,
716
+ action: 'REPLACE',
717
+ new_line: parsed.code
718
+ }];
719
+ }
720
+ } catch (e) {
721
+ // Fallback for raw string fixes
722
+ patches = [{
723
+ file: filePath,
724
+ line: targetLine,
725
+ action: 'REPLACE',
726
+ new_line: result.fix
727
+ }];
728
+ }
729
+
730
+ const backups = new Map();
731
+
732
+ try {
733
+ for (const patch of patches) {
734
+ const pFile = patch.file || filePath;
735
+ const pLine = parseInt(patch.line || targetLine);
736
+ const pAction = patch.action || 'REPLACE';
737
+ let pNew = patch.new_line;
735
738
 
736
- const lines = fs.readFileSync(filePath, 'utf8').split('\n');
737
- const originalLineIndex = targetLine - 1;
739
+ if (!fs.existsSync(pFile)) continue;
738
740
 
739
- if (originalLineIndex < 0 || originalLineIndex >= lines.length) {
740
- console.error(` ❌ [Self-Healing] Line ${targetLine} out of bounds in ${filePath}`);
741
- return;
742
- }
741
+ // Create backup if not already done
742
+ if (!backups.has(pFile)) {
743
+ backups.set(pFile, fs.readFileSync(pFile, 'utf8'));
744
+ }
745
+
746
+ const currentLines = fs.readFileSync(pFile, 'utf8').split('\n');
747
+ const originalLineIndex = pLine - 1;
748
+
749
+ if (originalLineIndex < 0 || originalLineIndex >= currentLines.length) continue;
750
+
751
+ const originalLine = currentLines[originalLineIndex];
752
+ const indentation = originalLine.match(/^\s*/)[0];
753
+
754
+ // CRITICAL: Prevent injecting JSON metadata into the code
755
+ if (pNew.trim().startsWith('{') && pNew.includes('"patches"') && pNew.includes(':')) {
756
+ console.log(` ❌ ${C.RED}[Self-Healing] Detected attempted JSON injection. Aborting patch for ${path.basename(pFile)}.${C.RESET}`);
757
+ continue;
758
+ }
743
759
 
744
- // Apply fix: Replace the entire line or the specific locator
745
- // For robustness, we replace the whole line but keep indentation
746
- const originalLine = lines[originalLineIndex];
747
- const indentation = originalLine.match(/^\s*/)[0];
748
- lines[originalLineIndex] = indentation + fixCode.trim();
760
+ const finalNewLine = indentation + pNew.trim();
749
761
 
750
- fs.writeFileSync(filePath, lines.join('\n'));
751
- console.log(` ✅ ${C.GREEN}[Self-Healing] Successfully patched:${C.RESET} ${path.basename(filePath)}:${targetLine}`);
762
+ // DUPLICATE PROTECTION: Don't insert/replace if code already exists
763
+ if (currentLines.some(l => l.trim() === pNew.trim())) {
764
+ console.log(` ℹ️ ${C.GRAY}[Self-Healing] Skipping duplicate patch for:${C.RESET} ${path.basename(pFile)}`);
765
+ continue;
766
+ }
767
+
768
+ if (pAction === 'INSERT_AFTER') {
769
+ currentLines.splice(pLine, 0, finalNewLine);
770
+ fs.writeFileSync(pFile, currentLines.join('\n'));
771
+ console.log(` ✅ ${C.GREEN}[Self-Healing] Successfully inserted at:${C.RESET} ${path.basename(pFile)}:${pLine}`);
772
+ } else {
773
+ currentLines[originalLineIndex] = finalNewLine;
774
+ fs.writeFileSync(pFile, currentLines.join('\n'));
775
+ console.log(` ✅ ${C.GREEN}[Self-Healing] Successfully patched:${C.RESET} ${path.basename(pFile)}:${pLine}`);
776
+ }
777
+
778
+ // SYNTAX VALIDATION: Rollback if syntax is broken
779
+ if (pFile.endsWith('.ts') || pFile.endsWith('.js')) {
780
+ try {
781
+ const { execSync } = require('child_process');
782
+ execSync(`node --check "${pFile}"`, { stdio: 'ignore' });
783
+ } catch (err) {
784
+ console.log(` ⚠️ ${C.YELLOW}[Self-Healing] Patch broke syntax in ${path.basename(pFile)}. Rolling back...${C.RESET}`);
785
+ fs.writeFileSync(pFile, backups.get(pFile));
786
+ }
787
+ }
788
+ }
789
+ } catch (patchError) {
790
+ console.error(` ❌ ${C.RED}[Self-Healing] Error applying patches: ${patchError.message}${C.RESET}`);
791
+ // Emergency rollback for all involved files
792
+ for (const [file, content] of backups) {
793
+ fs.writeFileSync(file, content);
794
+ }
795
+ }
752
796
  } catch (error) {
753
- console.error(` ❌ ${C.RED}[Self-Healing] Error patching file:${C.RESET} ${error.message}`);
797
+ console.error(` ❌ ${C.RED}[Self-Healing] Error in self-healing logic:${C.RESET} ${error.message}`);
754
798
  }
755
799
  }
756
800
 
@@ -981,6 +1025,7 @@ async function runCommand(cmd, args) {
981
1025
 
982
1026
  async function main() {
983
1027
  const command = argv._ || [];
1028
+ let finalExitCode = 0;
984
1029
 
985
1030
  // If 'doctor' or 'migrate' was called, don't proceed to wrapper logic
986
1031
  if (command.includes('doctor') || command.includes('migrate')) return;
@@ -1001,20 +1046,22 @@ async function main() {
1001
1046
  const artifacts = detectAllArtifacts(null, argv.html);
1002
1047
  const fixesApplied = await analyzeFailures(artifacts, output, client);
1003
1048
 
1049
+ let outcomeCode = code;
1004
1050
  // AUTO-VERIFICATION
1005
1051
  if (fixesApplied > 0 && argv.fix) {
1006
1052
  console.log(`\n${C.BRIGHT}💉 Fixes applied. Re-running tests to verify...${C.RESET}`);
1007
1053
  const secondRun = await runCommand(cmd, args);
1054
+ outcomeCode = secondRun.code;
1008
1055
  if (secondRun.code === 0) {
1009
1056
  console.log(`\n${C.GREEN}${C.BRIGHT}✅ All tests passed after DeFlake healing!${C.RESET}`);
1010
1057
  } else {
1011
1058
  console.log(`\n${C.YELLOW}⚠️ Some tests still failing after fixes. Check the report for details.${C.RESET}`);
1012
1059
  }
1013
1060
  }
1014
- process.exit(code);
1061
+ finalExitCode = outcomeCode;
1015
1062
  } else {
1016
1063
  console.log("\n🟢 Command passed successfully.");
1017
- process.exit(0);
1064
+ finalExitCode = 0;
1018
1065
  }
1019
1066
  } else {
1020
1067
  const artifacts = detectAllArtifacts(argv.log, argv.html);
@@ -1023,10 +1070,11 @@ async function main() {
1023
1070
  }
1024
1071
 
1025
1072
  // FINAL REPORT TRIGGER
1026
- // We wait until the very end so verification runs (if any) are reflected
1027
1073
  if (argv.report) {
1028
1074
  showFrameworkReport();
1029
1075
  }
1076
+
1077
+ process.exit(finalExitCode);
1030
1078
  }
1031
1079
 
1032
1080
  // main() call is now handled by the check above
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deflake",
3
- "version": "1.2.9",
3
+ "version": "1.2.13",
4
4
  "description": "AI-powered self-healing tool for Playwright, Cypress, and WebdriverIO tests.",
5
5
  "main": "client.js",
6
6
  "bin": {