deflake 1.2.12 → 1.2.15

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 +144 -89
  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
  /**
@@ -703,6 +681,43 @@ async function runDoctor(argv) {
703
681
  } catch (error) {
704
682
  process.stdout.write(`\r ❌ ${C.RED}API Connectivity Error: ${error.message}${C.RESET}\n`);
705
683
  }
684
+ // 6. Artifact & Permission Check
685
+ console.log(`${C.BRIGHT}Checking Artifact Permissions:${C.RESET}`);
686
+ const criticalDirs = [
687
+ { path: 'test-results', label: 'Playwright Failures' },
688
+ { path: 'playwright-report', label: 'Playwright Report' },
689
+ { path: 'cypress/screenshots', label: 'Cypress Screenshots' },
690
+ { path: 'cypress/videos', label: 'Cypress Videos' }
691
+ ];
692
+
693
+ let permsIssue = false;
694
+ for (const dir of criticalDirs) {
695
+ if (fs.existsSync(dir.path)) {
696
+ try {
697
+ fs.accessSync(dir.path, fs.constants.R_OK | fs.constants.W_OK);
698
+ // Deep check: try readdir
699
+ fs.readdirSync(dir.path);
700
+ console.log(` ✅ ${dir.label.padEnd(20)}: ${C.GREEN}Accessible${C.RESET}`);
701
+ } catch (e) {
702
+ console.log(` ❌ ${dir.label.padEnd(20)}: ${C.RED}Access Denied${C.RESET}`);
703
+ console.log(` ${C.GRAY}Path: ${dir.path}${C.RESET}`);
704
+ permsIssue = true;
705
+ }
706
+ }
707
+ }
708
+
709
+ if (permsIssue) {
710
+ console.log(`\n ${C.YELLOW}⚠️ Permission bottleneck detected!${C.RESET}`);
711
+ if (process.platform === 'win32') {
712
+ console.log(` ${C.BRIGHT}Windows Fix:${C.RESET} Run this in PowerShell as Admin:`);
713
+ console.log(` ${C.CYAN}icacls . /grant \${env:USERNAME}:(OI)(CI)F /T${C.RESET}`);
714
+ } else {
715
+ console.log(` ${C.BRIGHT}macOS/Linux Fix:${C.RESET} Run this command:`);
716
+ console.log(` ${C.CYAN}sudo chown -R $(whoami) .${C.RESET}`);
717
+ }
718
+ } else {
719
+ console.log(` ✅ All critical directories are accessible`);
720
+ }
706
721
  console.log("");
707
722
 
708
723
  console.log(`${C.BRIGHT}Summary:${C.RESET}`);
@@ -749,34 +764,74 @@ async function applySelfHealing(result) {
749
764
  }];
750
765
  }
751
766
 
752
- for (const patch of patches) {
753
- const pFile = patch.file || filePath;
754
- const pLine = parseInt(patch.line || targetLine);
755
- const pAction = patch.action || 'REPLACE';
756
- let pNew = patch.new_line;
767
+ const backups = new Map();
768
+
769
+ try {
770
+ for (const patch of patches) {
771
+ const pFile = patch.file || filePath;
772
+ const pLine = parseInt(patch.line || targetLine);
773
+ const pAction = patch.action || 'REPLACE';
774
+ let pNew = patch.new_line;
757
775
 
758
- if (!fs.existsSync(pFile)) continue;
776
+ if (!fs.existsSync(pFile)) continue;
759
777
 
760
- const lines = fs.readFileSync(pFile, 'utf8').split('\n');
761
- const originalLineIndex = pLine - 1;
778
+ // Create backup if not already done
779
+ if (!backups.has(pFile)) {
780
+ backups.set(pFile, fs.readFileSync(pFile, 'utf8'));
781
+ }
762
782
 
763
- if (originalLineIndex < 0 || originalLineIndex >= lines.length) continue;
783
+ const currentLines = fs.readFileSync(pFile, 'utf8').split('\n');
784
+ const originalLineIndex = pLine - 1;
764
785
 
765
- const originalLine = lines[originalLineIndex];
766
- const indentation = originalLine.match(/^\s*/)[0];
786
+ if (originalLineIndex < 0 || originalLineIndex >= currentLines.length) continue;
767
787
 
768
- if (pAction === 'INSERT_AFTER') {
769
- lines.splice(pLine, 0, indentation + pNew.trim());
770
- fs.writeFileSync(pFile, lines.join('\n'));
771
- console.log(` ✅ ${C.GREEN}[Self-Healing] Successfully inserted at:${C.RESET} ${path.basename(pFile)}:${pLine}`);
772
- } else {
773
- lines[originalLineIndex] = indentation + pNew.trim();
774
- fs.writeFileSync(pFile, lines.join('\n'));
775
- console.log(` ✅ ${C.GREEN}[Self-Healing] Successfully patched:${C.RESET} ${path.basename(pFile)}:${pLine}`);
788
+ const originalLine = currentLines[originalLineIndex];
789
+ const indentation = originalLine.match(/^\s*/)[0];
790
+
791
+ // CRITICAL: Prevent injecting JSON metadata into the code
792
+ if (pNew.trim().startsWith('{') && pNew.includes('"patches"') && pNew.includes(':')) {
793
+ console.log(` ❌ ${C.RED}[Self-Healing] Detected attempted JSON injection. Aborting patch for ${path.basename(pFile)}.${C.RESET}`);
794
+ continue;
795
+ }
796
+
797
+ const finalNewLine = indentation + pNew.trim();
798
+
799
+ // DUPLICATE PROTECTION: Don't insert/replace if code already exists
800
+ if (currentLines.some(l => l.trim() === pNew.trim())) {
801
+ console.log(` ℹ️ ${C.GRAY}[Self-Healing] Skipping duplicate patch for:${C.RESET} ${path.basename(pFile)}`);
802
+ continue;
803
+ }
804
+
805
+ if (pAction === 'INSERT_AFTER') {
806
+ currentLines.splice(pLine, 0, finalNewLine);
807
+ fs.writeFileSync(pFile, currentLines.join('\n'));
808
+ console.log(` ✅ ${C.GREEN}[Self-Healing] Successfully inserted at:${C.RESET} ${path.basename(pFile)}:${pLine}`);
809
+ } else {
810
+ currentLines[originalLineIndex] = finalNewLine;
811
+ fs.writeFileSync(pFile, currentLines.join('\n'));
812
+ console.log(` ✅ ${C.GREEN}[Self-Healing] Successfully patched:${C.RESET} ${path.basename(pFile)}:${pLine}`);
813
+ }
814
+
815
+ // SYNTAX VALIDATION: Rollback if syntax is broken
816
+ if (pFile.endsWith('.ts') || pFile.endsWith('.js')) {
817
+ try {
818
+ const { execSync } = require('child_process');
819
+ execSync(`node --check "${pFile}"`, { stdio: 'ignore' });
820
+ } catch (err) {
821
+ console.log(` ⚠️ ${C.YELLOW}[Self-Healing] Patch broke syntax in ${path.basename(pFile)}. Rolling back...${C.RESET}`);
822
+ fs.writeFileSync(pFile, backups.get(pFile));
823
+ }
824
+ }
776
825
  }
826
+ } catch (patchError) {
827
+ console.error(` ❌ ${C.RED}[Self-Healing] Error applying patches: ${patchError.message}${C.RESET}`);
828
+ // Emergency rollback for all involved files
829
+ for (const [file, content] of backups) {
830
+ fs.writeFileSync(file, content);
831
+ }
777
832
  }
778
833
  } catch (error) {
779
- console.error(` ❌ ${C.RED}[Self-Healing] Error patching file:${C.RESET} ${error.message}`);
834
+ console.error(` ❌ ${C.RED}[Self-Healing] Error in self-healing logic:${C.RESET} ${error.message}`);
780
835
  }
781
836
  }
782
837
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deflake",
3
- "version": "1.2.12",
3
+ "version": "1.2.15",
4
4
  "description": "AI-powered self-healing tool for Playwright, Cypress, and WebdriverIO tests.",
5
5
  "main": "client.js",
6
6
  "bin": {