deflake 1.2.28 ā 1.2.30
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 +116 -20
- 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,17 +255,79 @@ async function applyFix(res, loc) {
|
|
|
221
255
|
}
|
|
222
256
|
}
|
|
223
257
|
|
|
258
|
+
function extractRelevantOutput(fullOutput, artifactName) {
|
|
259
|
+
// SMART: Use Playwright's "Error Context:" line to find the EXACT error block for this artifact.
|
|
260
|
+
// Each Playwright failure ends with: "Error Context: test-results/<artifact-name>/error-context.md"
|
|
261
|
+
// We find that line, then walk backwards to capture the full error + stack trace.
|
|
262
|
+
|
|
263
|
+
const lines = fullOutput.split('\n');
|
|
264
|
+
|
|
265
|
+
// Find the Error Context line that matches this artifact
|
|
266
|
+
let anchorIdx = -1;
|
|
267
|
+
for (let i = 0; i < lines.length; i++) {
|
|
268
|
+
if (lines[i].includes('Error Context:') && lines[i].includes(artifactName)) {
|
|
269
|
+
anchorIdx = i;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (anchorIdx === -1) {
|
|
275
|
+
// Fallback: try partial match on artifact name parts
|
|
276
|
+
const nameParts = artifactName.split('-').filter(p => p.length > 3);
|
|
277
|
+
for (let i = 0; i < lines.length; i++) {
|
|
278
|
+
if (lines[i].includes('Error Context:') && nameParts.some(part => lines[i].includes(part))) {
|
|
279
|
+
anchorIdx = i;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (anchorIdx === -1) return '';
|
|
286
|
+
|
|
287
|
+
// Walk backwards from anchor to find the start of this error block
|
|
288
|
+
// Error blocks in Playwright start with "N) [chromium] āŗ tests/..."
|
|
289
|
+
let startIdx = anchorIdx;
|
|
290
|
+
for (let i = anchorIdx; i >= 0; i--) {
|
|
291
|
+
if (lines[i].match(/^\s*\d+\)\s+\[/)) {
|
|
292
|
+
startIdx = i;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return lines.slice(startIdx, anchorIdx + 1).join('\n');
|
|
298
|
+
}
|
|
299
|
+
|
|
224
300
|
function extractLoc(text) {
|
|
225
|
-
|
|
226
|
-
|
|
301
|
+
if (!text) return null;
|
|
302
|
+
|
|
303
|
+
// === MULTI-FRAMEWORK LOCATION EXTRACTION ===
|
|
304
|
+
// Supports: Playwright, Cypress, WebdriverIO, Jest, Mocha
|
|
305
|
+
|
|
306
|
+
// Strategy 1: Absolute path inside parentheses (most precise)
|
|
307
|
+
// Format: at FunctionName (/absolute/path/file.ts:75:35)
|
|
308
|
+
let m = text.match(/\(([^()]*?[\\/][^()]+\.(?:ts|js|tsx|jsx|mjs|cjs)):(\d+)(?::(\d+))?\)/);
|
|
309
|
+
if (m) return { path: path.resolve(m[1]), line: parseInt(m[2]) };
|
|
310
|
+
|
|
311
|
+
// Strategy 2: Absolute path after "at" keyword (no parens)
|
|
312
|
+
// Format: at /absolute/path/file.ts:75:35
|
|
313
|
+
m = text.match(/at\s+(\/[^\s]+\.(?:ts|js|tsx|jsx)):(\d+)(?::(\d+))?/);
|
|
227
314
|
if (m) return { path: path.resolve(m[1]), line: parseInt(m[2]) };
|
|
228
315
|
|
|
229
|
-
// Strategy
|
|
230
|
-
|
|
316
|
+
// Strategy 3: Relative path after "at" keyword
|
|
317
|
+
// Format: at ../pages/file.ts:75
|
|
318
|
+
m = text.match(/at\s+(\.\.?\/[^\s]+\.(?:ts|js|tsx|jsx)):(\d+)(?::(\d+))?/);
|
|
319
|
+
if (m) return { path: path.resolve(m[1]), line: parseInt(m[2]) };
|
|
320
|
+
|
|
321
|
+
// Strategy 4: Playwright "at file:line" in stack (single line match)
|
|
322
|
+
m = text.match(/^\s+at\s+(.*?\.(?:ts|js)):(\d+)$/m);
|
|
323
|
+
if (m) return { path: path.resolve(m[1]), line: parseInt(m[2]) };
|
|
324
|
+
|
|
325
|
+
// Strategy 5: Cypress ā at Context.<anonymous> (path)
|
|
326
|
+
m = text.match(/Context\.<anonymous>\s*\(([^)]+\.(?:ts|js|cy\.ts|cy\.js)):(\d+):(\d+)\)/);
|
|
231
327
|
if (m) return { path: path.resolve(m[1]), line: parseInt(m[2]) };
|
|
232
328
|
|
|
233
|
-
// Strategy
|
|
234
|
-
m = text.match(/([\w
|
|
329
|
+
// Strategy 6: Generic ā any path-like string with /dir/file.ts:line
|
|
330
|
+
m = text.match(/((?:[\w@.-]+\/)+[\w.-]+\.(?:ts|js|tsx|jsx)):(\d+)/);
|
|
235
331
|
if (m) return { path: path.resolve(m[1]), line: parseInt(m[2]) };
|
|
236
332
|
|
|
237
333
|
return null;
|