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.
Files changed (2) hide show
  1. package/cli.js +116 -20
  2. 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('close', (code) => resolve({ code }));
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
- const loc = extractLoc(content);
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 (regex mismatch)${C.RESET}`);
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
- console.log(` ${C.BRIGHT}šŸ’‰ Applying patches...${C.RESET}`);
145
- if (await applyFix(res, loc)) {
146
- count++;
147
- console.log(` ${C.GREEN}${C.BRIGHT}āœ… Fix applied to ${path.basename(loc.path)}:${loc.line}${C.RESET}`);
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
- console.log(` ${C.YELLOW}āš ļø Patches could not be applied (see details above)${C.RESET}`);
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
- // Strategy 1: Absolute path inside parentheses — at Function (/absolute/path/file.ts:75:35)
226
- let m = text.match(/\(([^()]+\.(?:ts|js)):(\d+)(?::(\d+))?\)/);
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 2: Absolute or relative path after "at" — at /path/file.ts:75:35 or at ../pages/file.ts:75
230
- m = text.match(/at\s+([^\s(]+\.(?:ts|js)):(\d+)(?::(\d+))?/);
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 3: Any path ending in .ts/.js with a line number (broadest match)
234
- m = text.match(/([\w.\/\\-]+\.(?:ts|js)):(\d+)(?::(\d+))?/);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deflake",
3
- "version": "1.2.28",
3
+ "version": "1.2.30",
4
4
  "description": "AI-powered self-healing tool for Playwright, Cypress, and WebdriverIO tests.",
5
5
  "main": "client.js",
6
6
  "bin": {