deflake 1.0.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.
- package/README.md +34 -0
- package/cli.js +468 -0
- package/client.js +75 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# DeFlake (JavaScript Client) āļø
|
|
2
|
+
|
|
3
|
+
> **Stop Flaky Tests. Start Auto-Healing.**
|
|
4
|
+
> Official Node.js client for DeFlake.
|
|
5
|
+
|
|
6
|
+
## š Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install deflake --save-dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## š§ Usage
|
|
13
|
+
|
|
14
|
+
### Wrapper Mode (Zero Config)
|
|
15
|
+
Simply prepend `npx deflake` to your test run command:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Playwright
|
|
19
|
+
export DEFLAKE_API_KEY="your-api-key"
|
|
20
|
+
npx deflake npx playwright test
|
|
21
|
+
|
|
22
|
+
# Cypress
|
|
23
|
+
npx deflake npx cypress run
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Manual Mode
|
|
27
|
+
Analyze an existing error log and HTML snapshot:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx deflake --log error.log --html playwright-report/index.html
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## š License
|
|
34
|
+
ISC
|
package/cli.js
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const yargs = require('yargs/yargs');
|
|
3
|
+
const { hideBin } = require('yargs/helpers');
|
|
4
|
+
const DeFlakeClient = require('./client');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const argv = yargs(hideBin(process.argv))
|
|
10
|
+
.option('log', {
|
|
11
|
+
alias: 'l',
|
|
12
|
+
type: 'string',
|
|
13
|
+
description: 'Path to the error log file',
|
|
14
|
+
demandOption: false // No longer strictly required if running in wrapper mode
|
|
15
|
+
})
|
|
16
|
+
.option('html', {
|
|
17
|
+
alias: 'h',
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Path to the HTML snapshot',
|
|
20
|
+
demandOption: false // No longer strictly required if auto-detected
|
|
21
|
+
})
|
|
22
|
+
.option('api-url', {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description: 'Override Default API URL',
|
|
25
|
+
})
|
|
26
|
+
.help()
|
|
27
|
+
.argv;
|
|
28
|
+
|
|
29
|
+
// Helper to auto-detect artifacts (Batch Mode)
|
|
30
|
+
function detectAllArtifacts(providedLog, providedHtml) {
|
|
31
|
+
// If user provided explicit paths, use them as a single item list
|
|
32
|
+
if (providedHtml || providedLog) {
|
|
33
|
+
return [{
|
|
34
|
+
logPath: providedLog,
|
|
35
|
+
htmlPath: providedHtml,
|
|
36
|
+
id: 'manual-input',
|
|
37
|
+
name: 'Manual Input'
|
|
38
|
+
}];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const detected = [];
|
|
42
|
+
|
|
43
|
+
// 1. Scan test-results
|
|
44
|
+
if (fs.existsSync('test-results')) {
|
|
45
|
+
function findFiles(dir) {
|
|
46
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
const res = path.resolve(dir, entry.name);
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
findFiles(res);
|
|
51
|
+
} else {
|
|
52
|
+
// Unique ID for this test failure is the parent folder name
|
|
53
|
+
const folderName = path.basename(path.dirname(res));
|
|
54
|
+
|
|
55
|
+
// We only want ONE artifact per test folder.
|
|
56
|
+
// Priority: error-context.md > index.html > trace.zip
|
|
57
|
+
|
|
58
|
+
// But we can't easily dedup here without a map.
|
|
59
|
+
// Let's just collect all candidates and filter later.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Better approach: Iterate folders in test-results
|
|
65
|
+
const testFolders = fs.readdirSync('test-results', { withFileTypes: true })
|
|
66
|
+
.filter(dirent => dirent.isDirectory())
|
|
67
|
+
.map(dirent => path.resolve('test-results', dirent.name));
|
|
68
|
+
|
|
69
|
+
for (const folder of testFolders) {
|
|
70
|
+
let artifact = null;
|
|
71
|
+
let type = '';
|
|
72
|
+
|
|
73
|
+
const mdPath = path.join(folder, 'error-context.md');
|
|
74
|
+
const htmlPath = path.join(folder, 'trace.html'); // Some reporters use this
|
|
75
|
+
|
|
76
|
+
// Check for error-context.md (High Quality)
|
|
77
|
+
if (fs.existsSync(mdPath)) {
|
|
78
|
+
artifact = mdPath;
|
|
79
|
+
type = 'md';
|
|
80
|
+
}
|
|
81
|
+
// Else check for any HTML that isn't trace viewer boilerplate
|
|
82
|
+
else {
|
|
83
|
+
// simple fallback
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (artifact) {
|
|
87
|
+
detected.push({
|
|
88
|
+
logPath: null, // Log is usually embedded or passed via stdout
|
|
89
|
+
htmlPath: artifact,
|
|
90
|
+
id: path.basename(folder),
|
|
91
|
+
name: path.basename(folder)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Fallback: If no granular results, check global report
|
|
98
|
+
if (detected.length === 0) {
|
|
99
|
+
if (fs.existsSync('playwright-report/index.html')) {
|
|
100
|
+
detected.push({
|
|
101
|
+
logPath: providedLog,
|
|
102
|
+
htmlPath: 'playwright-report/index.html',
|
|
103
|
+
id: 'global-report',
|
|
104
|
+
name: 'Global Report'
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return detected;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function stripAnsi(str) {
|
|
113
|
+
return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parsePlaywrightLogs(fullLog) {
|
|
117
|
+
// Strip ANSI codes to reliable regex matching
|
|
118
|
+
const cleanLog = stripAnsi(fullLog);
|
|
119
|
+
const errorBlocks = [];
|
|
120
|
+
|
|
121
|
+
// Regex to find start of a test failure: " 1) [chromium] āŗ ..."
|
|
122
|
+
const errorHeaderRegex = /^\s*(\d+)\)\s+\[(.*?)\]\s+/gm;
|
|
123
|
+
let match;
|
|
124
|
+
|
|
125
|
+
// Find all start indices
|
|
126
|
+
const indices = [];
|
|
127
|
+
while ((match = errorHeaderRegex.exec(cleanLog)) !== null) {
|
|
128
|
+
indices.push({ index: match.index, browser: match[2] }); // Group 2 is browser
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Slice log into blocks
|
|
132
|
+
for (let i = 0; i < indices.length; i++) {
|
|
133
|
+
const start = indices[i].index;
|
|
134
|
+
const end = (i + 1 < indices.length) ? indices[i + 1].index : cleanLog.length;
|
|
135
|
+
const content = cleanLog.slice(start, end);
|
|
136
|
+
errorBlocks.push({
|
|
137
|
+
browser: indices[i].browser, // e.g. 'chromium'
|
|
138
|
+
content: content
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return errorBlocks;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function runHealer(logContent, htmlPath, apiUrl, testName) {
|
|
146
|
+
// Check file size limit (Basic check to avoid 429s on huge files)
|
|
147
|
+
if (htmlPath && fs.existsSync(htmlPath)) {
|
|
148
|
+
const stats = fs.statSync(htmlPath);
|
|
149
|
+
if (stats.size > 5 * 1024 * 1024) { // 5MB limit warning
|
|
150
|
+
console.warn(`ā ļø Warning: Artifact for ${testName} is large (` + (stats.size / 1024 / 1024).toFixed(2) + "MB).");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const client = new DeFlakeClient(apiUrl);
|
|
155
|
+
|
|
156
|
+
// Pass log CONTENT (string) directly if it came from wrapper, or file path if from args
|
|
157
|
+
let finalLogPath = argv.log;
|
|
158
|
+
let effectiveLogContent = logContent;
|
|
159
|
+
|
|
160
|
+
// Robustness: If logContent is empty but we have an MD artifact, read it
|
|
161
|
+
if (!effectiveLogContent && htmlPath && htmlPath.endsWith('.md') && fs.existsSync(htmlPath)) {
|
|
162
|
+
try {
|
|
163
|
+
effectiveLogContent = fs.readFileSync(htmlPath, 'utf8');
|
|
164
|
+
} catch (e) { }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (effectiveLogContent) {
|
|
168
|
+
// We have content (from stdio or artifact), write to temp file
|
|
169
|
+
finalLogPath = '.deflake-error.log';
|
|
170
|
+
fs.writeFileSync(finalLogPath, effectiveLogContent);
|
|
171
|
+
} else if (!finalLogPath && fs.existsSync('error.log')) {
|
|
172
|
+
// Fallback to auto-detected error.log if nothing else
|
|
173
|
+
finalLogPath = 'error.log';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Validation before calling client
|
|
177
|
+
if (!finalLogPath) {
|
|
178
|
+
// Create a dummy log if we have HTML but absolutely no log, to prevent client crash
|
|
179
|
+
if (htmlPath) {
|
|
180
|
+
console.log(" (Creating temporary log from available context)");
|
|
181
|
+
finalLogPath = '.deflake-error.log';
|
|
182
|
+
fs.writeFileSync(finalLogPath, "Log missing. Refer to HTML/MD snapshot.");
|
|
183
|
+
effectiveLogContent = "Log missing.";
|
|
184
|
+
} else {
|
|
185
|
+
console.error(`ā [${testName}] Error: No log file or content available.`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// š§ SMART CONTEXT: Extract the failing source code if possible
|
|
191
|
+
const failureLoc = extractFailureLocation(effectiveLogContent || fs.readFileSync(finalLogPath, 'utf8'));
|
|
192
|
+
|
|
193
|
+
let sourceCodeContent = null;
|
|
194
|
+
if (failureLoc && failureLoc.fullRootPath) {
|
|
195
|
+
try {
|
|
196
|
+
if (fs.existsSync(failureLoc.fullRootPath)) {
|
|
197
|
+
sourceCodeContent = fs.readFileSync(failureLoc.fullRootPath, 'utf8');
|
|
198
|
+
}
|
|
199
|
+
} catch (e) { }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!failureLoc) {
|
|
203
|
+
// Silent warning for batch mode - don't clutter output
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const result = await client.heal(finalLogPath, htmlPath, failureLoc, sourceCodeContent);
|
|
208
|
+
|
|
209
|
+
if (result && result.status === 'success') {
|
|
210
|
+
// Return structured object for grouping
|
|
211
|
+
return {
|
|
212
|
+
testName,
|
|
213
|
+
location: failureLoc,
|
|
214
|
+
sourceCode: sourceCodeContent,
|
|
215
|
+
fix: result.fix,
|
|
216
|
+
status: 'success'
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// Return original result so main() can check for QUOTA_EXCEEDED
|
|
220
|
+
return result || { status: 'error', detail: 'UNKNOWN' };
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return { status: 'error', detail: error.message };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ... (extractFailureLocation and printDetailedFix remain mostly same, just slight tweaks for robustness) ...
|
|
227
|
+
// Actually, I need to include them in the replace or user 'multi_replace' but this is 'replace_file' so I must provide full content or precise chunks.
|
|
228
|
+
// To avoid massive token usage, I will use the existing helper functions but just update Main.
|
|
229
|
+
|
|
230
|
+
// WAIT, I must provide the implementations of extractFailureLocation and printDetailedFix if I replace the whole file or large chunks.
|
|
231
|
+
// The previous tool call view_file shows I have the whole content. I will rewrite the whole file to be safe and clean.
|
|
232
|
+
|
|
233
|
+
function extractFailureLocation(logText) {
|
|
234
|
+
if (!logText) return null;
|
|
235
|
+
const loc = {
|
|
236
|
+
specFile: null,
|
|
237
|
+
testLine: null,
|
|
238
|
+
rootFile: null,
|
|
239
|
+
fullRootPath: null,
|
|
240
|
+
rootLine: null,
|
|
241
|
+
stepLine: null
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const testMatch = logText.match(/^\s*\d+\)\s+\[.*?\]\s+āŗ\s+(.*?.spec.ts):(\d+):\d+\s+āŗ/m);
|
|
245
|
+
if (testMatch) {
|
|
246
|
+
loc.specFile = testMatch[1];
|
|
247
|
+
loc.testLine = testMatch[2];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const stackRegex = /at\s+(?:.*? \()?((?:[a-zA-Z]:\\|[\/~]|\.?\.\/|[\w_\-]+\/).*?):(\d+):(\d+)\)?/g;
|
|
251
|
+
let match;
|
|
252
|
+
let foundRoot = false;
|
|
253
|
+
|
|
254
|
+
while ((match = stackRegex.exec(logText)) !== null) {
|
|
255
|
+
const file = match[1];
|
|
256
|
+
const line = match[2];
|
|
257
|
+
if (!foundRoot && !file.includes('node_modules')) {
|
|
258
|
+
loc.rootFile = file.split('/').pop();
|
|
259
|
+
loc.fullRootPath = path.resolve(process.cwd(), file);
|
|
260
|
+
loc.rootLine = line;
|
|
261
|
+
foundRoot = true;
|
|
262
|
+
}
|
|
263
|
+
if (loc.specFile && file.endsWith(loc.specFile)) {
|
|
264
|
+
loc.stepLine = line;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Fallback: If header regex failed but we found a root file that looks like a test
|
|
269
|
+
if (!loc.specFile && loc.rootFile && (loc.rootFile.includes('.spec.') || loc.rootFile.includes('.test.'))) {
|
|
270
|
+
loc.specFile = loc.rootFile;
|
|
271
|
+
loc.testLine = loc.rootLine;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (loc.specFile || loc.rootFile) return loc;
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function printDetailedFix(fixText, location, sourceCode = null) {
|
|
279
|
+
const C = {
|
|
280
|
+
RESET: "\x1b[0m",
|
|
281
|
+
BRIGHT: "\x1b[1m",
|
|
282
|
+
RED: "\x1b[31m",
|
|
283
|
+
GREEN: "\x1b[32m",
|
|
284
|
+
YELLOW: "\x1b[33m",
|
|
285
|
+
CYAN: "\x1b[36m",
|
|
286
|
+
BLUE: "\x1b[34m",
|
|
287
|
+
GRAY: "\x1b[90m",
|
|
288
|
+
WHITE: "\x1b[37m"
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
let fixCode = fixText;
|
|
292
|
+
let explanation = null;
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const parsed = JSON.parse(fixText);
|
|
296
|
+
if (parsed.code) {
|
|
297
|
+
fixCode = parsed.code;
|
|
298
|
+
explanation = parsed.reason;
|
|
299
|
+
}
|
|
300
|
+
} catch (e) { }
|
|
301
|
+
|
|
302
|
+
console.log("\n" + C.GRAY + "ā".repeat(50) + C.RESET);
|
|
303
|
+
if (location) {
|
|
304
|
+
// Label is default color, Value is colored
|
|
305
|
+
if (location.rootFile && location.rootLine) {
|
|
306
|
+
console.log(`š„ Runtime Error: ${C.RED}${location.rootFile}:${location.rootLine}${C.RESET}`);
|
|
307
|
+
}
|
|
308
|
+
if (location.specFile) {
|
|
309
|
+
// Calculate a nice label for the target
|
|
310
|
+
let targetLabel = location.specFile;
|
|
311
|
+
if (location.testLine) targetLabel += `:${location.testLine}`;
|
|
312
|
+
console.log(`šÆ Fix Target: ${C.CYAN}${targetLabel} (Definition)${C.RESET}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
console.log(C.GRAY + "ā".repeat(50) + C.RESET);
|
|
316
|
+
|
|
317
|
+
console.log(`${C.GREEN}${C.BRIGHT}⨠DEFLAKE SUGGESTION:${C.RESET}`);
|
|
318
|
+
if (explanation) console.log(`${C.GRAY}// ${explanation}${C.RESET}`);
|
|
319
|
+
|
|
320
|
+
// Print code with simple coloring
|
|
321
|
+
fixCode.split('\n').forEach(line => {
|
|
322
|
+
let colored = line
|
|
323
|
+
.replace(/(\/\/.*)/g, `${C.GRAY}$1${C.RESET}`)
|
|
324
|
+
.replace(/\b(const|let|var|await|async|function|return)\b/g, `${C.YELLOW}$1${C.RESET}`)
|
|
325
|
+
.replace(/('.*?')|(".*?")|(`.*?`)/g, `${C.GREEN}$1${C.RESET}`)
|
|
326
|
+
.replace(/(\.click|\.fill|\.locator)/g, `${C.CYAN}$1${C.RESET}`);
|
|
327
|
+
console.log(" " + colored);
|
|
328
|
+
});
|
|
329
|
+
console.log(C.GRAY + "ā".repeat(50) + C.RESET);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Core analysis engine for batch mode.
|
|
334
|
+
* Enforces tier limits and calculates deduplicated results.
|
|
335
|
+
*/
|
|
336
|
+
async function analyzeFailures(artifacts, fullLog, client) {
|
|
337
|
+
const C = {
|
|
338
|
+
RESET: "\x1b[0m",
|
|
339
|
+
YELLOW: "\x1b[33m",
|
|
340
|
+
RED: "\x1b[31m",
|
|
341
|
+
GRAY: "\x1b[90m"
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
if (artifacts.length === 0) {
|
|
345
|
+
console.log("ā ļø No error artifacts found.");
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 1. Check Quota / Tier
|
|
350
|
+
const usage = await client.getUsage();
|
|
351
|
+
let limit = 100; // Default safety cap
|
|
352
|
+
let tier = 'unknown';
|
|
353
|
+
|
|
354
|
+
if (usage) {
|
|
355
|
+
tier = usage.tier || 'free';
|
|
356
|
+
if (tier === 'free') limit = 5;
|
|
357
|
+
else if (tier === 'pro') limit = 50;
|
|
358
|
+
else if (tier === 'master' || tier === 'byok') limit = 100;
|
|
359
|
+
|
|
360
|
+
console.log(`š« Subscription: ${tier.toUpperCase()} | Quota: ${usage.usage}/${usage.limit}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (artifacts.length > limit) {
|
|
364
|
+
console.log(`${C.YELLOW}ā ļø Detected ${artifacts.length} failures, but your ${tier} plan limits batch analysis to ${limit} unique fixes.${C.RESET}`);
|
|
365
|
+
console.log(` (Processing the first ${limit}...)\n`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const errorBlocks = parsePlaywrightLogs(fullLog || "");
|
|
369
|
+
const results = [];
|
|
370
|
+
const processLimit = Math.min(artifacts.length, limit);
|
|
371
|
+
const batchArtifacts = artifacts.slice(0, processLimit);
|
|
372
|
+
|
|
373
|
+
console.log(`š Analyzing ${batchArtifacts.length} failure(s)...`);
|
|
374
|
+
|
|
375
|
+
for (const art of batchArtifacts) {
|
|
376
|
+
let specificLog = fullLog;
|
|
377
|
+
if (art.htmlPath && art.htmlPath.endsWith('.md') && errorBlocks.length > 0) {
|
|
378
|
+
let bestMatch = null;
|
|
379
|
+
let bestScore = -1;
|
|
380
|
+
const artifactTokens = art.name.toLowerCase().replace(/[-_]/g, ' ').split(' ').filter(w => w.length > 3 && w !== 'chromium' && w !== 'firefox' && w !== 'webkit');
|
|
381
|
+
const browser = art.name.toLowerCase().includes('firefox') ? 'firefox' : art.name.toLowerCase().includes('webkit') ? 'webkit' : 'chromium';
|
|
382
|
+
const candidates = errorBlocks.filter(b => b.browser.toLowerCase().includes(browser));
|
|
383
|
+
|
|
384
|
+
for (const block of candidates) {
|
|
385
|
+
let score = 0;
|
|
386
|
+
const blockLower = block.content.toLowerCase();
|
|
387
|
+
for (const token of artifactTokens) { if (blockLower.includes(token)) score++; }
|
|
388
|
+
if (score > bestScore) { bestScore = score; bestMatch = block; }
|
|
389
|
+
}
|
|
390
|
+
if (bestMatch && bestScore > 0) {
|
|
391
|
+
specificLog = bestMatch.content;
|
|
392
|
+
const idx = errorBlocks.indexOf(bestMatch);
|
|
393
|
+
if (idx > -1) errorBlocks.splice(idx, 1);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
process.stdout.write(`\rā³ Analyzing ${art.name}... `);
|
|
398
|
+
const result = await runHealer(specificLog, art.htmlPath, argv.apiUrl, art.name);
|
|
399
|
+
|
|
400
|
+
if (result && result.status === 'error' && result.detail === 'QUOTA_EXCEEDED') {
|
|
401
|
+
console.log(`\n${C.RED}š Quota exceeded! Stopping.${C.RESET}`);
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (result && result.status === 'success') {
|
|
406
|
+
results.push(result);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
console.log("\rā
Analysis complete. \n");
|
|
410
|
+
|
|
411
|
+
// GROUPING & PRINTING
|
|
412
|
+
const groups = {};
|
|
413
|
+
for (const res of results) {
|
|
414
|
+
if (!res.location) continue;
|
|
415
|
+
const key = `${res.location.rootFile}:${res.location.rootLine}|${JSON.stringify(res.fix)}`;
|
|
416
|
+
if (!groups[key]) {
|
|
417
|
+
groups[key] = { location: res.location, sourceCode: res.sourceCode, fix: res.fix, tests: [] };
|
|
418
|
+
}
|
|
419
|
+
groups[key].tests.push(res.testName);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const sortedKeys = Object.keys(groups).sort((a, b) => {
|
|
423
|
+
const lineA = parseInt(groups[a].location?.rootLine) || 0;
|
|
424
|
+
const lineB = parseInt(groups[b].location?.rootLine) || 0;
|
|
425
|
+
return lineA - lineB;
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
for (const key of sortedKeys) {
|
|
429
|
+
printDetailedFix(groups[key].fix, groups[key].location, groups[key].sourceCode);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function main() {
|
|
434
|
+
console.log("š DeFlake JS Client (Batch Mode)");
|
|
435
|
+
const client = new DeFlakeClient(argv.apiUrl);
|
|
436
|
+
const command = argv._;
|
|
437
|
+
|
|
438
|
+
if (command.length > 0) {
|
|
439
|
+
const cmd = command[0];
|
|
440
|
+
const args = command.slice(1);
|
|
441
|
+
console.log(`š Running command: ${cmd} ${args.join(' ')}`);
|
|
442
|
+
|
|
443
|
+
const child = spawn(cmd, args, { shell: true, stdio: 'pipe' });
|
|
444
|
+
let stdout = '';
|
|
445
|
+
let stderr = '';
|
|
446
|
+
|
|
447
|
+
child.stdout.on('data', (data) => { process.stdout.write(data); stdout += data.toString(); });
|
|
448
|
+
child.stderr.on('data', (data) => { process.stderr.write(data); stderr += data.toString(); });
|
|
449
|
+
|
|
450
|
+
child.on('close', async (code) => {
|
|
451
|
+
if (code !== 0) {
|
|
452
|
+
console.log(`\nš“ Command failed with code ${code}. Activating DeFlake...`);
|
|
453
|
+
const artifacts = detectAllArtifacts(null, argv.html);
|
|
454
|
+
await analyzeFailures(artifacts, stdout + "\n" + stderr, client);
|
|
455
|
+
process.exit(code);
|
|
456
|
+
} else {
|
|
457
|
+
console.log("\nš¢ Command passed successfully.");
|
|
458
|
+
process.exit(0);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
} else {
|
|
462
|
+
const artifacts = detectAllArtifacts(argv.log, argv.html);
|
|
463
|
+
const fullLog = argv.log && fs.existsSync(argv.log) ? fs.readFileSync(argv.log, 'utf8') : null;
|
|
464
|
+
await analyzeFailures(artifacts, fullLog, client);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
main();
|
package/client.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
require('dotenv').config();
|
|
5
|
+
|
|
6
|
+
class DeFlakeClient {
|
|
7
|
+
constructor(apiUrl, apiKey) {
|
|
8
|
+
// Use environment variable first, then fallback to a placeholder production URL
|
|
9
|
+
// User can still pass a custom URL for local development/B2B setups
|
|
10
|
+
this.productionUrl = 'https://deflake-api.up.railway.app/api/deflake';
|
|
11
|
+
this.apiUrl = apiUrl || process.env.DEFLAKE_API_URL || this.productionUrl;
|
|
12
|
+
this.apiKey = apiKey || process.env.DEFLAKE_API_KEY;
|
|
13
|
+
|
|
14
|
+
if (!this.apiKey) {
|
|
15
|
+
console.error("ā DeFlake Error: DEFLAKE_API_KEY is missing.");
|
|
16
|
+
console.error(" To get a free key, visit: http://deflake.com/register");
|
|
17
|
+
console.error(" Or set it in your environment: export DEFLAKE_API_KEY=your_key");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async getUsage() {
|
|
23
|
+
try {
|
|
24
|
+
const usageUrl = this.apiUrl.replace('/deflake', '/user/usage');
|
|
25
|
+
const response = await axios.get(usageUrl, {
|
|
26
|
+
headers: { 'X-API-KEY': this.apiKey }
|
|
27
|
+
});
|
|
28
|
+
return response.data;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return null; // Silent failure for usage check
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async heal(logPath, htmlPath, failureLocation = null, sourceCode = null) {
|
|
35
|
+
try {
|
|
36
|
+
// ... (rest of the check logic) ...
|
|
37
|
+
if (!fs.existsSync(logPath)) throw new Error(`Log file not found: ${logPath}`);
|
|
38
|
+
if (!fs.existsSync(htmlPath)) throw new Error(`HTML file not found: ${htmlPath}`);
|
|
39
|
+
|
|
40
|
+
const logContent = fs.readFileSync(logPath, 'utf8');
|
|
41
|
+
const htmlContent = fs.readFileSync(htmlPath, 'utf8');
|
|
42
|
+
|
|
43
|
+
const payload = {
|
|
44
|
+
error_log: logContent,
|
|
45
|
+
html_snapshot: htmlContent,
|
|
46
|
+
failing_line: failureLocation ? `Line ${failureLocation.rootLine}` : null,
|
|
47
|
+
source_code: sourceCode
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const response = await axios.post(this.apiUrl, payload, {
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
'X-API-KEY': this.apiKey
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return response.data;
|
|
58
|
+
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.response) {
|
|
61
|
+
if (error.response.status === 402) {
|
|
62
|
+
return { status: 'error', detail: 'QUOTA_EXCEEDED' };
|
|
63
|
+
}
|
|
64
|
+
let detail = error.response.data.detail || error.response.statusText;
|
|
65
|
+
if (typeof detail === 'object') detail = JSON.stringify(detail);
|
|
66
|
+
console.error(`ā API Error: ${error.response.status} - ${detail}`);
|
|
67
|
+
} else {
|
|
68
|
+
console.error(`ā Client Error: ${error.message}`);
|
|
69
|
+
}
|
|
70
|
+
return null; // Return null instead of exiting process
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = DeFlakeClient;
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "deflake",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-powered self-healing tool for Playwright, Cypress, and WebdriverIO tests.",
|
|
5
|
+
"main": "client.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"deflake": "cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"playwright",
|
|
14
|
+
"cypress",
|
|
15
|
+
"webdriverio",
|
|
16
|
+
"ai",
|
|
17
|
+
"self-healing",
|
|
18
|
+
"automation",
|
|
19
|
+
"testing"
|
|
20
|
+
],
|
|
21
|
+
"author": "VicQA Automation Cloud",
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"type": "commonjs",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/vicqaautomation-cloud/DeFlake.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/vicqaautomation-cloud/DeFlake#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/vicqaautomation-cloud/DeFlake/issues"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"axios": "^1.13.2",
|
|
34
|
+
"dotenv": "^17.2.3",
|
|
35
|
+
"yargs": "^18.0.0"
|
|
36
|
+
}
|
|
37
|
+
}
|