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.
- package/cli.js +138 -90
- 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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
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) {
|
|
483
|
+
if (explanation) {
|
|
458
484
|
console.log(`\n${C.BRIGHT}ℹ️ Reason:${C.RESET} ${explanation}`);
|
|
459
485
|
}
|
|
460
486
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
496
|
-
if (
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
|
707
|
+
let patches = [];
|
|
731
708
|
try {
|
|
732
709
|
const parsed = JSON.parse(result.fix);
|
|
733
|
-
if (parsed.
|
|
734
|
-
|
|
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
|
-
|
|
737
|
-
const originalLineIndex = targetLine - 1;
|
|
739
|
+
if (!fs.existsSync(pFile)) continue;
|
|
738
740
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
|
|
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
|
-
|
|
751
|
-
|
|
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
|
|
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
|
-
|
|
1061
|
+
finalExitCode = outcomeCode;
|
|
1015
1062
|
} else {
|
|
1016
1063
|
console.log("\n🟢 Command passed successfully.");
|
|
1017
|
-
|
|
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
|