deflake 1.2.27 ā 1.2.29
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 +102 -17
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -46,11 +46,11 @@ async function main() {
|
|
|
46
46
|
console.log(`š Detected Framework: ${C.GREEN}${fw.toUpperCase()}${C.RESET}`);
|
|
47
47
|
|
|
48
48
|
if (commandToRun) {
|
|
49
|
-
const { code } = await runNative(commandToRun);
|
|
49
|
+
const { code, output } = await runNative(commandToRun);
|
|
50
50
|
if (code !== 0) {
|
|
51
51
|
console.log(`\nš“ Command failed with code ${code}. Activating DeFlake...`);
|
|
52
52
|
const artifacts = detectFailures();
|
|
53
|
-
const applied = await analyzeAndFix(artifacts, client, argv);
|
|
53
|
+
const applied = await analyzeAndFix(artifacts, client, argv, output);
|
|
54
54
|
if (applied > 0 && argv.fix) {
|
|
55
55
|
console.log(`\n${C.BRIGHT}š Fixes applied. Re-running tests to verify...${C.RESET}`);
|
|
56
56
|
await runNative(commandToRun);
|
|
@@ -63,12 +63,23 @@ async function main() {
|
|
|
63
63
|
async function runNative(fullCommand) {
|
|
64
64
|
return new Promise((resolve) => {
|
|
65
65
|
console.log(`š Running command: ${C.CYAN}${fullCommand}${C.RESET}`);
|
|
66
|
+
let capturedOutput = '';
|
|
66
67
|
const child = spawn(fullCommand, {
|
|
67
68
|
shell: true,
|
|
68
|
-
stdio: 'inherit',
|
|
69
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
69
70
|
env: { ...process.env, PLAYWRIGHT_HTML_OPEN: 'never' }
|
|
70
71
|
});
|
|
71
|
-
child.on('
|
|
72
|
+
child.stdout.on('data', (data) => {
|
|
73
|
+
const text = data.toString();
|
|
74
|
+
process.stdout.write(text);
|
|
75
|
+
capturedOutput += text;
|
|
76
|
+
});
|
|
77
|
+
child.stderr.on('data', (data) => {
|
|
78
|
+
const text = data.toString();
|
|
79
|
+
process.stderr.write(text);
|
|
80
|
+
capturedOutput += text;
|
|
81
|
+
});
|
|
82
|
+
child.on('close', (code) => resolve({ code, output: capturedOutput }));
|
|
72
83
|
});
|
|
73
84
|
}
|
|
74
85
|
|
|
@@ -99,7 +110,7 @@ function detectFailures() {
|
|
|
99
110
|
return results;
|
|
100
111
|
}
|
|
101
112
|
|
|
102
|
-
async function analyzeAndFix(artifacts, client, argv) {
|
|
113
|
+
async function analyzeAndFix(artifacts, client, argv, capturedOutput = '') {
|
|
103
114
|
if (artifacts.length === 0) {
|
|
104
115
|
console.log(` ${C.YELLOW}ā ļø No failure artifacts detected. Run 'npx deflake doctor' to check permissions.${C.RESET}`);
|
|
105
116
|
return 0;
|
|
@@ -111,20 +122,30 @@ async function analyzeAndFix(artifacts, client, argv) {
|
|
|
111
122
|
console.log(`${C.CYAN}āāā [${i+1}/${artifacts.length}] ${art.name} āāā${C.RESET}`);
|
|
112
123
|
try {
|
|
113
124
|
const content = fs.readFileSync(art.htmlPath, 'utf8');
|
|
114
|
-
|
|
125
|
+
|
|
126
|
+
// SMART Location Extraction: try multiple sources
|
|
127
|
+
let loc = extractLoc(content);
|
|
128
|
+
let locSource = 'error-context.md';
|
|
129
|
+
|
|
130
|
+
if (!loc && capturedOutput) {
|
|
131
|
+
// Extract the relevant section from captured output for this artifact
|
|
132
|
+
const relevantOutput = extractRelevantOutput(capturedOutput, art.name);
|
|
133
|
+
loc = extractLoc(relevantOutput || capturedOutput);
|
|
134
|
+
locSource = 'console output';
|
|
135
|
+
}
|
|
115
136
|
|
|
116
137
|
// Step 1: Location extraction
|
|
117
138
|
if (loc) {
|
|
118
|
-
console.log(` ${C.GRAY}š Location:${C.RESET} ${path.basename(loc.path)}:${loc.line}`);
|
|
139
|
+
console.log(` ${C.GRAY}š Location:${C.RESET} ${path.basename(loc.path)}:${loc.line} ${C.GRAY}(from ${locSource})${C.RESET}`);
|
|
119
140
|
} else {
|
|
120
|
-
console.log(` ${C.YELLOW}š Location: Could not extract
|
|
141
|
+
console.log(` ${C.YELLOW}š Location: Could not extract from any source${C.RESET}`);
|
|
121
142
|
}
|
|
122
143
|
|
|
123
144
|
// Step 2: Source code reading
|
|
124
145
|
const source = loc && fs.existsSync(loc.path) ? fs.readFileSync(loc.path, 'utf8') : null;
|
|
125
146
|
console.log(` ${C.GRAY}š Source:${C.RESET} ${source ? 'Loaded (' + source.split('\n').length + ' lines)' : C.YELLOW + 'Not available' + C.RESET}`);
|
|
126
147
|
|
|
127
|
-
// Step 3: API call
|
|
148
|
+
// Step 3: API call ā send BOTH error-context AND console output for richer diagnosis
|
|
128
149
|
console.log(` ${C.GRAY}š Calling DeFlake API...${C.RESET}`);
|
|
129
150
|
const res = await client.heal(null, art.htmlPath, loc, source, argv.fix);
|
|
130
151
|
|
|
@@ -141,12 +162,25 @@ async function analyzeAndFix(artifacts, client, argv) {
|
|
|
141
162
|
console.log(` ${C.GRAY}ā ${p.action} at line ${p.line}: ${p.new_line?.substring(0, 70)}${C.RESET}`);
|
|
142
163
|
}
|
|
143
164
|
if (argv.fix) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
165
|
+
// Use loc from patches if our extraction failed
|
|
166
|
+
let fixLoc = loc;
|
|
167
|
+
if (!fixLoc && parsed.patches[0]?.file) {
|
|
168
|
+
const patchFile = path.resolve(parsed.patches[0].file);
|
|
169
|
+
if (fs.existsSync(patchFile)) {
|
|
170
|
+
fixLoc = { path: patchFile, line: parsed.patches[0].line };
|
|
171
|
+
console.log(` ${C.GRAY}š Using patch target:${C.RESET} ${path.basename(patchFile)}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (fixLoc) {
|
|
175
|
+
console.log(` ${C.BRIGHT}š Applying patches...${C.RESET}`);
|
|
176
|
+
if (await applyFix(res, fixLoc)) {
|
|
177
|
+
count++;
|
|
178
|
+
console.log(` ${C.GREEN}${C.BRIGHT}ā
Fix applied to ${path.basename(fixLoc.path)}:${fixLoc.line}${C.RESET}`);
|
|
179
|
+
} else {
|
|
180
|
+
console.log(` ${C.YELLOW}ā ļø Patches could not be applied (see details above)${C.RESET}`);
|
|
181
|
+
}
|
|
148
182
|
} else {
|
|
149
|
-
|
|
183
|
+
console.log(` ${C.YELLOW}ā ļø Cannot apply: no target file detected${C.RESET}`);
|
|
150
184
|
}
|
|
151
185
|
} else {
|
|
152
186
|
console.log(` ${C.YELLOW}ā¹ļø Use --fix flag to auto-apply these patches${C.RESET}`);
|
|
@@ -221,10 +255,61 @@ async function applyFix(res, loc) {
|
|
|
221
255
|
}
|
|
222
256
|
}
|
|
223
257
|
|
|
258
|
+
function extractRelevantOutput(fullOutput, artifactName) {
|
|
259
|
+
// Extract error sections from captured console output
|
|
260
|
+
// Covers: Playwright, Cypress, WebdriverIO stack traces
|
|
261
|
+
const lines = fullOutput.split('\n');
|
|
262
|
+
let relevant = [];
|
|
263
|
+
let capturing = false;
|
|
264
|
+
|
|
265
|
+
for (const line of lines) {
|
|
266
|
+
if (line.includes('Error:') || line.includes('at ') || line.includes('Failing:')) {
|
|
267
|
+
capturing = true;
|
|
268
|
+
}
|
|
269
|
+
if (capturing) {
|
|
270
|
+
relevant.push(line);
|
|
271
|
+
if (line.includes('Error Context:') || (relevant.length > 30 && line.trim() === '')) {
|
|
272
|
+
capturing = false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return relevant.join('\n');
|
|
277
|
+
}
|
|
278
|
+
|
|
224
279
|
function extractLoc(text) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
280
|
+
if (!text) return null;
|
|
281
|
+
|
|
282
|
+
// === MULTI-FRAMEWORK LOCATION EXTRACTION ===
|
|
283
|
+
// Supports: Playwright, Cypress, WebdriverIO, Jest, Mocha
|
|
284
|
+
|
|
285
|
+
// Strategy 1: Absolute path inside parentheses (most precise)
|
|
286
|
+
// Format: at FunctionName (/absolute/path/file.ts:75:35)
|
|
287
|
+
let m = text.match(/\(([^()]*?[\\/][^()]+\.(?:ts|js|tsx|jsx|mjs|cjs)):(\d+)(?::(\d+))?\)/);
|
|
288
|
+
if (m) return { path: path.resolve(m[1]), line: parseInt(m[2]) };
|
|
289
|
+
|
|
290
|
+
// Strategy 2: Absolute path after "at" keyword (no parens)
|
|
291
|
+
// Format: at /absolute/path/file.ts:75:35
|
|
292
|
+
m = text.match(/at\s+(\/[^\s]+\.(?:ts|js|tsx|jsx)):(\d+)(?::(\d+))?/);
|
|
293
|
+
if (m) return { path: path.resolve(m[1]), line: parseInt(m[2]) };
|
|
294
|
+
|
|
295
|
+
// Strategy 3: Relative path after "at" keyword
|
|
296
|
+
// Format: at ../pages/file.ts:75
|
|
297
|
+
m = text.match(/at\s+(\.\.?\/[^\s]+\.(?:ts|js|tsx|jsx)):(\d+)(?::(\d+))?/);
|
|
298
|
+
if (m) return { path: path.resolve(m[1]), line: parseInt(m[2]) };
|
|
299
|
+
|
|
300
|
+
// Strategy 4: Playwright "at file:line" in stack (single line match)
|
|
301
|
+
m = text.match(/^\s+at\s+(.*?\.(?:ts|js)):(\d+)$/m);
|
|
302
|
+
if (m) return { path: path.resolve(m[1]), line: parseInt(m[2]) };
|
|
303
|
+
|
|
304
|
+
// Strategy 5: Cypress ā at Context.<anonymous> (path)
|
|
305
|
+
m = text.match(/Context\.<anonymous>\s*\(([^)]+\.(?:ts|js|cy\.ts|cy\.js)):(\d+):(\d+)\)/);
|
|
306
|
+
if (m) return { path: path.resolve(m[1]), line: parseInt(m[2]) };
|
|
307
|
+
|
|
308
|
+
// Strategy 6: Generic ā any path-like string with /dir/file.ts:line
|
|
309
|
+
m = text.match(/((?:[\w@.-]+\/)+[\w.-]+\.(?:ts|js|tsx|jsx)):(\d+)/);
|
|
310
|
+
if (m) return { path: path.resolve(m[1]), line: parseInt(m[2]) };
|
|
311
|
+
|
|
312
|
+
return null;
|
|
228
313
|
}
|
|
229
314
|
|
|
230
315
|
async function runDoctor() {
|