deflake 1.1.6 → 1.2.0

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 (3) hide show
  1. package/cli.js +49 -26
  2. package/client.js +3 -2
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -177,35 +177,50 @@ function stripAnsi(str) {
177
177
  }
178
178
 
179
179
  function parsePlaywrightLogs(fullLog) {
180
- // Strip ANSI codes to reliable regex matching
181
180
  const cleanLog = stripAnsi(fullLog);
182
181
  const errorBlocks = [];
183
-
184
- // Regex to find start of a test failure: " 1) [chromium] › ..."
185
182
  const errorHeaderRegex = /^\s*(\d+)\)\s+\[(.*?)\]\s+/gm;
186
183
  let match;
187
184
 
188
- // Find all start indices
189
185
  const indices = [];
190
186
  while ((match = errorHeaderRegex.exec(cleanLog)) !== null) {
191
- indices.push({ index: match.index, browser: match[2] }); // Group 2 is browser
187
+ indices.push({ index: match.index, name: match[0].trim() });
192
188
  }
193
189
 
194
- // Slice log into blocks
195
190
  for (let i = 0; i < indices.length; i++) {
196
191
  const start = indices[i].index;
197
192
  const end = (i + 1 < indices.length) ? indices[i + 1].index : cleanLog.length;
198
- const content = cleanLog.slice(start, end);
199
193
  errorBlocks.push({
200
- browser: indices[i].browser, // e.g. 'chromium'
201
- content: content
194
+ name: indices[i].name,
195
+ content: cleanLog.slice(start, end)
202
196
  });
203
197
  }
204
-
205
198
  return errorBlocks;
206
199
  }
207
200
 
208
- async function runHealer(logContent, htmlPath, apiUrl, testName) {
201
+ function parseCypressLogs(fullLog) {
202
+ const cleanLog = stripAnsi(fullLog);
203
+ const specBlocks = [];
204
+ const specHeaderRegex = /Running:\s+([^\s]+)\s+\(\d+\s+of\s+\d+\)/g;
205
+ let match;
206
+
207
+ const indices = [];
208
+ while ((match = specHeaderRegex.exec(cleanLog)) !== null) {
209
+ indices.push({ index: match.index, spec: match[1] });
210
+ }
211
+
212
+ for (let i = 0; i < indices.length; i++) {
213
+ const start = indices[i].index;
214
+ const end = (i + 1 < indices.length) ? indices[i + 1].index : cleanLog.length;
215
+ specBlocks.push({
216
+ spec: indices[i].spec,
217
+ content: cleanLog.slice(start, end)
218
+ });
219
+ }
220
+ return specBlocks;
221
+ }
222
+
223
+ async function runHealer(logContent, htmlPath, apiUrl, testName, applyFix = false) {
209
224
  // Check file size limit (Basic check to avoid 429s on huge files)
210
225
  if (htmlPath && fs.existsSync(htmlPath)) {
211
226
  const stats = fs.statSync(htmlPath);
@@ -267,7 +282,7 @@ async function runHealer(logContent, htmlPath, apiUrl, testName) {
267
282
  }
268
283
 
269
284
  try {
270
- const result = await client.heal(finalLogPath, htmlPath, failureLoc, sourceCodeContent);
285
+ const result = await client.heal(finalLogPath, htmlPath, failureLoc, sourceCodeContent, applyFix);
271
286
 
272
287
  if (result && result.status === 'success') {
273
288
  // Return structured object for grouping
@@ -625,7 +640,10 @@ async function runDoctor(argv) {
625
640
  if (result.status === 'success') {
626
641
  const usage = result.data;
627
642
  process.stdout.write(`\r ✅ API Connected! (Tier: ${C.GREEN}${usage.tier.toUpperCase()}${C.RESET}) \n`);
628
- console.log(` Quota: ${usage.usage}/${usage.limit}`);
643
+ const u = usage;
644
+ const remaining = u.limit - u.usage;
645
+ console.log(` Fix Quota: ${C.GREEN}${remaining}/${u.limit} remaining${C.RESET}`);
646
+ console.log(` Analysis: ${C.GREEN}FREE${C.RESET} (Pay-per-fix model enabled)`);
629
647
  } else {
630
648
  process.stdout.write(`\r ❌ ${C.RED}API Connectivity Failed${C.RESET} \n`);
631
649
  if (result.code === 401 || result.code === 403) {
@@ -715,23 +733,30 @@ async function analyzeFailures(artifacts, fullLog, client) {
715
733
  console.log(` (Processing the first ${limit}...)\n`);
716
734
  }
717
735
 
718
- const errorBlocks = parsePlaywrightLogs(fullLog || "");
719
- const results = [];
720
- const processLimit = Math.min(artifacts.length, limit);
721
- const batchArtifacts = artifacts.slice(0, processLimit);
736
+ const framework = artifacts.length > 0 && artifacts[0].htmlPath?.includes('cypress') ? 'Cypress' : 'Playwright';
722
737
 
723
738
  console.log(`🔍 Analyzing ${batchArtifacts.length} failure(s)...`);
724
739
 
740
+ const playwrightBlocks = (framework === 'Playwright') ? parsePlaywrightLogs(fullLog || "") : [];
741
+ const cypressBlocks = (framework === 'Cypress') ? parseCypressLogs(fullLog || "") : [];
742
+
725
743
  for (const art of batchArtifacts) {
726
744
  let specificLog = fullLog;
727
- if (art.htmlPath && art.htmlPath.endsWith('.md') && errorBlocks.length > 0) {
745
+
746
+ if (framework === 'Cypress' && cypressBlocks.length > 0) {
747
+ // Match screenshot path to spec log
748
+ // Screenshot typically: cypress/screenshots/folder/file.cy.ts/Test Name (failed).png
749
+ const match = cypressBlocks.find(b => art.htmlPath && art.htmlPath.includes(b.spec));
750
+ if (match) {
751
+ specificLog = match.content;
752
+ }
753
+ } else if (framework === 'Playwright' && playwrightBlocks.length > 0) {
754
+ // Heuristic matching for Playwright error blocks
728
755
  let bestMatch = null;
729
756
  let bestScore = -1;
730
- const artifactTokens = art.name.toLowerCase().replace(/[-_]/g, ' ').split(' ').filter(w => w.length > 3 && w !== 'chromium' && w !== 'firefox' && w !== 'webkit');
731
- const browser = art.name.toLowerCase().includes('firefox') ? 'firefox' : art.name.toLowerCase().includes('webkit') ? 'webkit' : 'chromium';
732
- const candidates = errorBlocks.filter(b => b.browser.toLowerCase().includes(browser));
757
+ const artifactTokens = art.name.toLowerCase().replace(/[-_]/g, ' ').split(' ').filter(w => w.length > 3);
733
758
 
734
- for (const block of candidates) {
759
+ for (const block of playwrightBlocks) {
735
760
  let score = 0;
736
761
  const blockLower = block.content.toLowerCase();
737
762
  for (const token of artifactTokens) { if (blockLower.includes(token)) score++; }
@@ -739,8 +764,6 @@ async function analyzeFailures(artifacts, fullLog, client) {
739
764
  }
740
765
  if (bestMatch && bestScore > 0) {
741
766
  specificLog = bestMatch.content;
742
- const idx = errorBlocks.indexOf(bestMatch);
743
- if (idx > -1) errorBlocks.splice(idx, 1);
744
767
  }
745
768
  }
746
769
 
@@ -748,7 +771,7 @@ async function analyzeFailures(artifacts, fullLog, client) {
748
771
  process.stdout.write(`\r⏳ Analyzing ${displayName}... `);
749
772
 
750
773
  try {
751
- const result = await runHealer(specificLog, art.htmlPath, argv.apiUrl, art.name);
774
+ const result = await runHealer(specificLog, art.htmlPath, argv.apiUrl, art.name, argv.fix);
752
775
 
753
776
  if (result && result.status === 'success') {
754
777
  results.push(result);
@@ -804,7 +827,7 @@ async function analyzeFailures(artifacts, fullLog, client) {
804
827
  if (updatedUsage.status === 'success') {
805
828
  const u = updatedUsage.data;
806
829
  const remaining = u.limit - u.usage;
807
- console.log(` - Monthly Quota: ${C.GREEN}${remaining}/${u.limit} remaining${C.RESET}`);
830
+ console.log(` - Fix Quota: ${C.GREEN}${remaining}/${u.limit} remaining (Analysis is FREE)${C.RESET}`);
808
831
  }
809
832
  } catch (e) {
810
833
  // usage fetch failed is non-critical
package/client.js CHANGED
@@ -94,7 +94,7 @@ class DeFlakeClient {
94
94
  }
95
95
  }
96
96
 
97
- async heal(logPath, htmlPath, failureLocation = null, sourceCode = null) {
97
+ async heal(logPath, htmlPath, failureLocation = null, sourceCode = null, applyFix = false) {
98
98
  try {
99
99
  // ... (rest of the check logic) ...
100
100
  if (!fs.existsSync(logPath)) throw new Error(`Log file not found: ${logPath}`);
@@ -119,7 +119,8 @@ class DeFlakeClient {
119
119
  html_snapshot: htmlContent || "",
120
120
  failing_line: failureLocation ? `Line ${failureLocation.rootLine}` : "",
121
121
  source_code: sourceCode || "",
122
- framework: this.framework
122
+ framework: this.framework,
123
+ apply_fix: applyFix
123
124
  };
124
125
 
125
126
  const response = await axios.post(this.apiUrl, payload, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deflake",
3
- "version": "1.1.6",
3
+ "version": "1.2.0",
4
4
  "description": "AI-powered self-healing tool for Playwright, Cypress, and WebdriverIO tests.",
5
5
  "main": "client.js",
6
6
  "bin": {