flakeiq 1.0.11
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/LICENSE +21 -0
- package/README.md +423 -0
- package/bin/flakeiq.js +173 -0
- package/lib/classify.js +38 -0
- package/lib/config.js +51 -0
- package/lib/seed.js +30 -0
- package/lib/serve.js +59 -0
- package/lib/setup.js +79 -0
- package/lib/utils.js +40 -0
- package/package.json +55 -0
- package/python/classify.py +191 -0
- package/python/dashboard.py +321 -0
- package/python/requirements.txt +2 -0
- package/python/seed.py +256 -0
- package/python/web/static/app.js +296 -0
- package/python/web/static/index.html +43 -0
- package/python/web/static/style.css +46 -0
- package/reporter/flake-reporter.js +93 -0
- package/reporter/index.js +1 -0
- package/scripts/postinstall.js +27 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FlakeIQ Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
# FlakeIQ
|
|
2
|
+
|
|
3
|
+
Test flake tracking + LLM failure classification for E2E tests. Works with **Playwright**, **Detox**, **Appium**, **Selenium**, and any framework that can output test results as JSON.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
| Requirement | Required For | Install |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| **Node.js 18+** | Reporter, CLI | [nodejs.org](https://nodejs.org/) |
|
|
10
|
+
| **Python 3.11+** | Dashboard, Classifier | [python.org](https://www.python.org/downloads/) |
|
|
11
|
+
| **Ollama** | AI failure classification | See below |
|
|
12
|
+
|
|
13
|
+
### Install Python
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Check if installed
|
|
17
|
+
python --version
|
|
18
|
+
|
|
19
|
+
# If not installed, download from:
|
|
20
|
+
# https://www.python.org/downloads/
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Windows users:** Check "Add Python to PATH" during installation.
|
|
24
|
+
|
|
25
|
+
### Install Ollama
|
|
26
|
+
|
|
27
|
+
Ollama runs a local LLM to classify test failures as real bugs vs flakes.
|
|
28
|
+
|
|
29
|
+
**macOS / Linux:**
|
|
30
|
+
```bash
|
|
31
|
+
curl -fsSL https://ollama.com/install.sh | sh
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Windows:**
|
|
35
|
+
```bash
|
|
36
|
+
# Download from https://ollama.com/download
|
|
37
|
+
# Or use winget:
|
|
38
|
+
winget install Ollama.Ollama
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**After install, pull the model and start the server:**
|
|
42
|
+
```bash
|
|
43
|
+
ollama pull llama3.2
|
|
44
|
+
ollama serve
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Verify it's running:**
|
|
48
|
+
```bash
|
|
49
|
+
curl http://localhost:11434/api/tags
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> **Note:** If Ollama is not running, FlakeIQ still works but failures are stored as "unclassified". Install Ollama to get AI-powered classification.
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
**Playwright** has a built-in reporter — zero config required. **All other frameworks** (Detox, Appium, Selenium, etc.) need a small custom reporter to output test results as JSONL. Once you have the JSONL file, `npx flakeiq classify` and `npx flakeiq serve` work identically regardless of framework.
|
|
57
|
+
|
|
58
|
+
### Playwright (Built-in Reporter)
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm install flakeiq --save-dev
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Add to your `playwright.config.ts`:
|
|
65
|
+
```js
|
|
66
|
+
reporter: [
|
|
67
|
+
['html'],
|
|
68
|
+
['flakeiq/reporter', { outputFile: 'flake-results.jsonl' }],
|
|
69
|
+
],
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Run your tests:
|
|
73
|
+
```bash
|
|
74
|
+
npx playwright test
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
View dashboard:
|
|
78
|
+
```bash
|
|
79
|
+
npx flakeiq serve
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Detox (React Native)
|
|
83
|
+
|
|
84
|
+
Output test results as JSONL in this format:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"test_file": "src/__tests__/login.test.js",
|
|
89
|
+
"test_name": "login with valid credentials",
|
|
90
|
+
"platform": "ios",
|
|
91
|
+
"device_id": "iPhone 15",
|
|
92
|
+
"duration_ms": 4500,
|
|
93
|
+
"result": "passed",
|
|
94
|
+
"error_message": "",
|
|
95
|
+
"last_actions": ["tap login button", "enter email", "enter password", "tap submit"],
|
|
96
|
+
"classification": null,
|
|
97
|
+
"classification_reason": null,
|
|
98
|
+
"screen_name": "login",
|
|
99
|
+
"session_id": "detox_run_123",
|
|
100
|
+
"run_at": "2026-01-15T10:30:00Z"
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Detox reporter example:**
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
// detox.config.js
|
|
108
|
+
module.exports = {
|
|
109
|
+
// ... your config
|
|
110
|
+
reporters: [
|
|
111
|
+
['jest-html-reporter', { outputPath: 'test-results.html' }],
|
|
112
|
+
// Custom JSONL reporter
|
|
113
|
+
['custom', {
|
|
114
|
+
onComplete: (results) => {
|
|
115
|
+
const fs = require('fs');
|
|
116
|
+
const lines = results.testResults.map(test => ({
|
|
117
|
+
test_file: test.testFilePath,
|
|
118
|
+
test_name: test.testTitle,
|
|
119
|
+
platform: process.platform,
|
|
120
|
+
device_id: process.env.DEVICE_ID || 'unknown',
|
|
121
|
+
duration_ms: test.duration,
|
|
122
|
+
result: test.status,
|
|
123
|
+
error_message: test.failureMessages.join('\n'),
|
|
124
|
+
last_actions: [],
|
|
125
|
+
classification: null,
|
|
126
|
+
classification_reason: null,
|
|
127
|
+
screen_name: test.testFilePath.split('/').pop().replace('.test.js', ''),
|
|
128
|
+
session_id: `detox_${Date.now()}`,
|
|
129
|
+
run_at: new Date().toISOString()
|
|
130
|
+
}));
|
|
131
|
+
fs.writeFileSync('flake-results.jsonl', lines.map(l => JSON.stringify(l)).join('\n'));
|
|
132
|
+
}
|
|
133
|
+
}]
|
|
134
|
+
]
|
|
135
|
+
};
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Appium
|
|
139
|
+
|
|
140
|
+
Output test results as JSONL:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"test_file": "tests/android/login.js",
|
|
145
|
+
"test_name": "Android login flow",
|
|
146
|
+
"platform": "android",
|
|
147
|
+
"device_id": "emulator-5554",
|
|
148
|
+
"duration_ms": 8200,
|
|
149
|
+
"result": "timedOut",
|
|
150
|
+
"error_message": "An unknown server-side error occurred while processing the command",
|
|
151
|
+
"last_actions": ["findElement", "click", "sendKeys", "waitForElement"],
|
|
152
|
+
"classification": null,
|
|
153
|
+
"classification_reason": null,
|
|
154
|
+
"screen_name": "login",
|
|
155
|
+
"session_id": "appium_session_abc123",
|
|
156
|
+
"run_at": "2026-01-15T11:00:00Z"
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Appium integration example:**
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
// wdio.conf.js
|
|
164
|
+
exports.config = {
|
|
165
|
+
// ... your config
|
|
166
|
+
onComplete: function(results) {
|
|
167
|
+
const fs = require('fs');
|
|
168
|
+
const lines = results.tests.map(test => ({
|
|
169
|
+
test_file: test.file,
|
|
170
|
+
test_name: test.title,
|
|
171
|
+
platform: this.capabilities.platformName,
|
|
172
|
+
device_id: this.capabilities.deviceName,
|
|
173
|
+
duration_ms: test.duration,
|
|
174
|
+
result: test.passed ? 'passed' : 'failed',
|
|
175
|
+
error_message: test.error || '',
|
|
176
|
+
last_actions: test.commands || [],
|
|
177
|
+
classification: null,
|
|
178
|
+
classification_reason: null,
|
|
179
|
+
screen_name: test.file.split('/').pop().replace('.js', ''),
|
|
180
|
+
session_id: `appium_${Date.now()}`,
|
|
181
|
+
run_at: new Date().toISOString()
|
|
182
|
+
}));
|
|
183
|
+
fs.writeFileSync('flake-results.jsonl', lines.map(l => JSON.stringify(l)).join('\n'));
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Selenium
|
|
189
|
+
|
|
190
|
+
Output test results as JSONL:
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"test_file": "tests/selenium/login_test.py",
|
|
195
|
+
"test_name": "Selenium login test",
|
|
196
|
+
"platform": "chrome",
|
|
197
|
+
"device_id": "Chrome 120",
|
|
198
|
+
"duration_ms": 3400,
|
|
199
|
+
"result": "passed",
|
|
200
|
+
"error_message": "",
|
|
201
|
+
"last_actions": ["find_element", "send_keys", "click", "assert"],
|
|
202
|
+
"classification": null,
|
|
203
|
+
"classification_reason": null,
|
|
204
|
+
"screen_name": "login",
|
|
205
|
+
"session_id": "selenium_run_456",
|
|
206
|
+
"run_at": "2026-01-15T12:00:00Z"
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Python Selenium integration:**
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
# conftest.py (pytest)
|
|
214
|
+
import json
|
|
215
|
+
import os
|
|
216
|
+
from datetime import datetime
|
|
217
|
+
|
|
218
|
+
def pytest_runtest_makereport(item, call):
|
|
219
|
+
if call.when == 'call':
|
|
220
|
+
result = {
|
|
221
|
+
'test_file': str(item.fspath),
|
|
222
|
+
'test_name': item.name,
|
|
223
|
+
'platform': 'selenium',
|
|
224
|
+
'device_id': os.environ.get('BROWSER', 'chrome'),
|
|
225
|
+
'duration_ms': int(call.duration * 1000),
|
|
226
|
+
'result': 'passed' if call.excinfo is None else 'failed',
|
|
227
|
+
'error_message': str(call.excinfo.value) if call.excinfo else '',
|
|
228
|
+
'last_actions': [],
|
|
229
|
+
'classification': None,
|
|
230
|
+
'classification_reason': None,
|
|
231
|
+
'screen_name': item.name,
|
|
232
|
+
'session_id': f'selenium_{int(datetime.now().timestamp())}',
|
|
233
|
+
'run_at': datetime.now().isoformat() + 'Z'
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
with open('flake-results.jsonl', 'a') as f:
|
|
237
|
+
f.write(json.dumps(result) + '\n')
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Any Framework
|
|
241
|
+
|
|
242
|
+
For any testing framework, output results as JSONL with this schema:
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"test_file": "string",
|
|
247
|
+
"test_name": "string",
|
|
248
|
+
"platform": "string",
|
|
249
|
+
"device_id": "string",
|
|
250
|
+
"duration_ms": 0,
|
|
251
|
+
"result": "passed|failed|timedOut|skipped",
|
|
252
|
+
"error_message": "string",
|
|
253
|
+
"last_actions": ["string"],
|
|
254
|
+
"classification": null,
|
|
255
|
+
"classification_reason": null,
|
|
256
|
+
"screen_name": "string",
|
|
257
|
+
"session_id": "string",
|
|
258
|
+
"run_at": "ISO8601"
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Then run:
|
|
263
|
+
```bash
|
|
264
|
+
npx flakeiq classify flake-results.jsonl
|
|
265
|
+
npx flakeiq serve
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Every Test Run (QA / CI / Local)
|
|
269
|
+
|
|
270
|
+
### Local / QA
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# 1. Run your Playwright tests (reporter auto-captures results)
|
|
274
|
+
npx playwright test
|
|
275
|
+
|
|
276
|
+
# 2. Import results into FlakeIQ database
|
|
277
|
+
npx flakeiq classify flake-results.jsonl
|
|
278
|
+
|
|
279
|
+
# 3. View dashboard to see flake analysis
|
|
280
|
+
npx flakeiq serve
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### CI (GitHub Actions)
|
|
284
|
+
|
|
285
|
+
```yaml
|
|
286
|
+
# .github/workflows/test.yml
|
|
287
|
+
name: E2E Tests
|
|
288
|
+
on: [push, pull_request]
|
|
289
|
+
jobs:
|
|
290
|
+
test:
|
|
291
|
+
runs-on: ubuntu-latest
|
|
292
|
+
steps:
|
|
293
|
+
- uses: actions/checkout@v4
|
|
294
|
+
- uses: actions/setup-node@v4
|
|
295
|
+
with:
|
|
296
|
+
node-version: 20
|
|
297
|
+
- uses: actions/setup-python@v5
|
|
298
|
+
with:
|
|
299
|
+
python-version: '3.13'
|
|
300
|
+
- run: npm ci
|
|
301
|
+
- run: npx playwright install --with-deps chromium
|
|
302
|
+
- run: npx playwright test
|
|
303
|
+
# Import results into FlakeIQ
|
|
304
|
+
- run: npx flakeiq classify flake-results.jsonl
|
|
305
|
+
# Upload results as artifact
|
|
306
|
+
- uses: actions/upload-artifact@v4
|
|
307
|
+
if: always()
|
|
308
|
+
with:
|
|
309
|
+
name: flakeiq-results
|
|
310
|
+
path: |
|
|
311
|
+
flake-results.jsonl
|
|
312
|
+
flakeiq-data/flake.db
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Weekly Team Review
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
# View accumulated flake data over time
|
|
319
|
+
npx flakeiq serve
|
|
320
|
+
|
|
321
|
+
# Check flake rate trends
|
|
322
|
+
# Check top flaky tests
|
|
323
|
+
# Check device health
|
|
324
|
+
# Make decisions on what to fix vs ignore
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## How It Works
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
your-repo/ FlakeIQ
|
|
331
|
+
========== =======
|
|
332
|
+
|
|
333
|
+
Your tests (Playwright/Detox/Appium/Selenium/any framework)
|
|
334
|
+
|
|
|
335
|
+
v (Reporter captures test results)
|
|
336
|
+
flake-results.jsonl ---------------> classify.py --> Ollama llama3.2 (on failures)
|
|
337
|
+
|
|
|
338
|
+
v
|
|
339
|
+
flake.db
|
|
340
|
+
|
|
|
341
|
+
v
|
|
342
|
+
dashboard.py --> browser (Chart.js)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Commands
|
|
346
|
+
|
|
347
|
+
| Command | Requires | Description |
|
|
348
|
+
|---|---|---|
|
|
349
|
+
| `npx flakeiq serve` | Python | Start the dashboard server |
|
|
350
|
+
| `npx flakeiq serve --port 3000` | Python | Start on a custom port |
|
|
351
|
+
| `npx flakeiq serve --seed` | Python | Dashboard with demo data |
|
|
352
|
+
| `npx flakeiq serve --open` | Python | Auto-open browser |
|
|
353
|
+
| `npx flakeiq classify results.jsonl` | Python + Ollama | Classify failures from JSONL |
|
|
354
|
+
| `npx flakeiq seed` | Python | Generate synthetic seed data |
|
|
355
|
+
| `npx flakeiq status` | — | Show environment status |
|
|
356
|
+
| `npx flakeiq reporter` | — | Show reporter setup instructions |
|
|
357
|
+
|
|
358
|
+
## Dashboard Options
|
|
359
|
+
|
|
360
|
+
| Flag | Description |
|
|
361
|
+
|---|---|
|
|
362
|
+
| `--db PATH` | SQLite database path (default: `flakeiq-data/flake.db`) |
|
|
363
|
+
| `--port NUM` | HTTP port (default: `8080`) |
|
|
364
|
+
| `--host ADDR` | Bind address (default: `127.0.0.1`) |
|
|
365
|
+
| `--seed` | Generate and use demo data |
|
|
366
|
+
| `--open` | Auto-launch browser on start |
|
|
367
|
+
|
|
368
|
+
## API Endpoints
|
|
369
|
+
|
|
370
|
+
All endpoints return JSON. Used by the dashboard's Chart.js frontend.
|
|
371
|
+
|
|
372
|
+
| Endpoint | Description |
|
|
373
|
+
|---|---|
|
|
374
|
+
| `/api/stats` | Total runs, failures, flake rate, avg duration, date range |
|
|
375
|
+
| `/api/sessions` | All test sessions with pass/fail counts, avg duration, time range |
|
|
376
|
+
| `/api/latest-session` | Latest session + per-test results |
|
|
377
|
+
| `/api/flake-rate` | Daily flake rate trend (last 60 days) |
|
|
378
|
+
| `/api/breakdown` | Classification category counts for failed tests |
|
|
379
|
+
| `/api/by-action` | Flake rate grouped by last action type |
|
|
380
|
+
| `/api/by-platform` | Pass/fail counts per platform |
|
|
381
|
+
| `/api/volume` | Daily pass/fail test volume |
|
|
382
|
+
| `/api/duration-dist` | Failure duration bucketed histogram |
|
|
383
|
+
| `/api/classification-trend` | Classification counts per day over time |
|
|
384
|
+
| `/api/heatmap` | Screen x day flake rate grid |
|
|
385
|
+
| `/api/top-flakes` | Flakiest tests ranked by fail rate |
|
|
386
|
+
| `/api/devices` | Device-level pass/fail stats |
|
|
387
|
+
|
|
388
|
+
## Seed Data
|
|
389
|
+
|
|
390
|
+
For demo without real test runs:
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
npx flakeiq seed
|
|
394
|
+
npx flakeiq serve --seed
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Environment Variables
|
|
398
|
+
|
|
399
|
+
| Variable | Default | Description |
|
|
400
|
+
|---|---|---|
|
|
401
|
+
| `FLAKE_DB` | `./flakeiq-data/flake.db` | SQLite database path |
|
|
402
|
+
| `FLAKE_PORT` | `8080` | Dashboard port |
|
|
403
|
+
| `FLAKE_HOST` | `127.0.0.1` | Dashboard bind address |
|
|
404
|
+
| `OLLAMA_URL` | `http://localhost:11434/api/generate` | Ollama API URL |
|
|
405
|
+
| `OLLAMA_MODEL` | `llama3.2` | Ollama model name |
|
|
406
|
+
| `FLAKE_DATA_DIR` | `./flakeiq-data` | Data directory |
|
|
407
|
+
|
|
408
|
+
## Files
|
|
409
|
+
|
|
410
|
+
| File | Purpose |
|
|
411
|
+
|---|---|
|
|
412
|
+
| `reporter/flake-reporter.js` | Playwright reporter — captures last 10 `pw:api` steps, writes JSONL |
|
|
413
|
+
| `reporter/index.js` | Re-export for `flakeiq/reporter` import |
|
|
414
|
+
| `python/classify.py` | Reads JSONL, calls Ollama, upserts SQLite |
|
|
415
|
+
| `python/dashboard.py` | HTTP server with Chart.js dashboard |
|
|
416
|
+
| `python/seed.py` | Generates 5100 synthetic records for demo |
|
|
417
|
+
| `python/web/static/` | Dashboard HTML/CSS/JS assets |
|
|
418
|
+
| `lib/*.js` | CLI commands (serve, classify, seed, status, reporter, setup) |
|
|
419
|
+
| `bin/flakeiq.js` | CLI entry point |
|
|
420
|
+
|
|
421
|
+
## License
|
|
422
|
+
|
|
423
|
+
MIT
|
package/bin/flakeiq.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { getConfig, isVenvReady } = require('../lib/config');
|
|
4
|
+
const { setup } = require('../lib/setup');
|
|
5
|
+
const { serve } = require('../lib/serve');
|
|
6
|
+
const { classify } = require('../lib/classify');
|
|
7
|
+
const { seed } = require('../lib/seed');
|
|
8
|
+
const { getPythonVersion } = require('../lib/utils');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const VERSION = require('../package.json').version;
|
|
12
|
+
|
|
13
|
+
const HELP = `
|
|
14
|
+
FlakeIQ v${VERSION} — Test flake tracking + LLM failure classification
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
npx flakeiq <command> [options]
|
|
18
|
+
|
|
19
|
+
Commands:
|
|
20
|
+
serve Start the dashboard server
|
|
21
|
+
classify Classify test failures from a JSONL file
|
|
22
|
+
seed Generate synthetic seed data
|
|
23
|
+
setup (Re)run Python environment setup
|
|
24
|
+
status Show FlakeIQ status and configuration
|
|
25
|
+
reporter Show reporter setup instructions
|
|
26
|
+
help Show this help message
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
npx flakeiq serve Start dashboard at localhost:8080
|
|
30
|
+
npx flakeiq serve --port 3000 Start on port 3000
|
|
31
|
+
npx flakeiq serve --seed Dashboard with demo data
|
|
32
|
+
npx flakeiq serve --open Auto-open browser
|
|
33
|
+
npx flakeiq classify results.jsonl Classify failures from JSONL
|
|
34
|
+
npx flakeiq seed Create seed database
|
|
35
|
+
npx flakeiq status Check environment
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
function parseArgs(args) {
|
|
39
|
+
const parsed = { _: [] };
|
|
40
|
+
for (let i = 0; i < args.length; i++) {
|
|
41
|
+
const arg = args[i];
|
|
42
|
+
if (arg.startsWith('--')) {
|
|
43
|
+
const key = arg.slice(2);
|
|
44
|
+
const next = args[i + 1];
|
|
45
|
+
if (next && !next.startsWith('--')) {
|
|
46
|
+
parsed[key] = next;
|
|
47
|
+
i++;
|
|
48
|
+
} else {
|
|
49
|
+
parsed[key] = true;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
parsed._.push(arg);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return parsed;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ensurePython() {
|
|
59
|
+
if (!isVenvReady()) {
|
|
60
|
+
console.log('Python environment not set up. Running setup...');
|
|
61
|
+
setup();
|
|
62
|
+
if (!isVenvReady()) {
|
|
63
|
+
console.error('\nCannot continue without Python. Install Python 3.11+ and retry.');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function showStatus() {
|
|
70
|
+
const config = getConfig();
|
|
71
|
+
const pythonVer = getPythonVersion(config.venvPython);
|
|
72
|
+
const sysPython = getPythonVersion('python3') || getPythonVersion('python');
|
|
73
|
+
|
|
74
|
+
console.log('');
|
|
75
|
+
console.log(` FlakeIQ v${VERSION}`);
|
|
76
|
+
console.log(' ─────────────────────────────────');
|
|
77
|
+
console.log(` Node.js: ${process.version}`);
|
|
78
|
+
console.log(` Python (system): ${sysPython ? sysPython.raw : 'NOT FOUND'}`);
|
|
79
|
+
console.log(` Python (venv): ${pythonVer ? pythonVer.raw : 'NOT SET UP'}`);
|
|
80
|
+
console.log(` Venv ready: ${isVenvReady() ? 'Yes' : 'No'}`);
|
|
81
|
+
console.log(` Database: ${config.dbPath}`);
|
|
82
|
+
console.log(` Data dir: ${config.dataDir}`);
|
|
83
|
+
console.log(` Ollama URL: ${config.ollamaUrl}`);
|
|
84
|
+
console.log(` Ollama model: ${config.ollamaModel}`);
|
|
85
|
+
console.log(` Default port: ${config.port}`);
|
|
86
|
+
console.log(` Reporter: ${config.reporterPath}`);
|
|
87
|
+
console.log('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function showReporterSetup() {
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(' Reporter Setup');
|
|
93
|
+
console.log(' ─────────────────────────────────');
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log(' 1. Add to your playwright.config.ts:');
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log(' export default {');
|
|
98
|
+
console.log(" reporter: [['html'], ['flakeiq/reporter']],");
|
|
99
|
+
console.log(' // ...');
|
|
100
|
+
console.log(' }');
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log(' 2. Run your tests:');
|
|
103
|
+
console.log(' npx playwright test');
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(' 3. View results:');
|
|
106
|
+
console.log(' npx flakeiq serve');
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log(' Output: flake-results.jsonl (auto-generated)');
|
|
109
|
+
console.log('');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function main() {
|
|
113
|
+
const args = parseArgs(process.argv.slice(2));
|
|
114
|
+
const command = args._[0] || 'help';
|
|
115
|
+
|
|
116
|
+
switch (command) {
|
|
117
|
+
case 'serve':
|
|
118
|
+
ensurePython();
|
|
119
|
+
await serve({
|
|
120
|
+
port: args.port ? parseInt(args.port) : undefined,
|
|
121
|
+
host: args.host,
|
|
122
|
+
seed: args.seed || false,
|
|
123
|
+
open: args.open !== undefined ? args.open : true,
|
|
124
|
+
db: args.db,
|
|
125
|
+
});
|
|
126
|
+
break;
|
|
127
|
+
|
|
128
|
+
case 'classify':
|
|
129
|
+
ensurePython();
|
|
130
|
+
classify(args._[1], {
|
|
131
|
+
db: args.db,
|
|
132
|
+
ollamaUrl: args['ollama-url'],
|
|
133
|
+
model: args.model,
|
|
134
|
+
});
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'seed':
|
|
138
|
+
ensurePython();
|
|
139
|
+
seed(args._[1]);
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
case 'setup':
|
|
143
|
+
setup();
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case 'status':
|
|
147
|
+
showStatus();
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
case 'reporter':
|
|
151
|
+
case 'init':
|
|
152
|
+
showReporterSetup();
|
|
153
|
+
break;
|
|
154
|
+
|
|
155
|
+
case 'help':
|
|
156
|
+
case '--help':
|
|
157
|
+
case '-h':
|
|
158
|
+
console.log(HELP);
|
|
159
|
+
break;
|
|
160
|
+
|
|
161
|
+
case '--version':
|
|
162
|
+
case '-v':
|
|
163
|
+
console.log(`flakeiq v${VERSION}`);
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
default:
|
|
167
|
+
console.error(` Unknown command: ${command}`);
|
|
168
|
+
console.log(' Run "npx flakeiq help" for usage.');
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
main();
|
package/lib/classify.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { getConfig } = require('./config');
|
|
4
|
+
|
|
5
|
+
function classify(jsonlPath, options = {}) {
|
|
6
|
+
const config = getConfig();
|
|
7
|
+
const python = config.venvPython;
|
|
8
|
+
const classifyScript = path.join(config.pythonDir, 'classify.py');
|
|
9
|
+
|
|
10
|
+
const args = [classifyScript];
|
|
11
|
+
if (jsonlPath) args.push(jsonlPath);
|
|
12
|
+
if (options.db) args.push('--db', options.db);
|
|
13
|
+
if (options.ollamaUrl) args.push('--ollama-url', options.ollamaUrl);
|
|
14
|
+
if (options.model) args.push('--model', options.model);
|
|
15
|
+
|
|
16
|
+
const child = spawn(python, args, {
|
|
17
|
+
stdio: 'inherit',
|
|
18
|
+
env: {
|
|
19
|
+
...process.env,
|
|
20
|
+
FLAKE_DB: options.db || config.dbPath,
|
|
21
|
+
OLLAMA_URL: options.ollamaUrl || config.ollamaUrl,
|
|
22
|
+
OLLAMA_MODEL: options.model || config.ollamaModel,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
child.on('error', (err) => {
|
|
27
|
+
if (err.code === 'ENOENT') {
|
|
28
|
+
console.error('Python not found. Run "npx flakeiq setup" or install Python 3.11+.');
|
|
29
|
+
} else {
|
|
30
|
+
console.error('Classify error:', err.message);
|
|
31
|
+
}
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
child.on('exit', (code) => { process.exit(code || 0); });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { classify };
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
const PACKAGE_DIR = path.resolve(__dirname, '..');
|
|
5
|
+
const PYTHON_DIR = path.join(PACKAGE_DIR, 'python');
|
|
6
|
+
const REPORTER_PATH = path.join(PACKAGE_DIR, 'reporter', 'flake-reporter.js');
|
|
7
|
+
|
|
8
|
+
function getVenvDir() {
|
|
9
|
+
return path.join(PACKAGE_DIR, '.flakeiq');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getVenvPython() {
|
|
13
|
+
const venvDir = getVenvDir();
|
|
14
|
+
if (process.platform === 'win32') {
|
|
15
|
+
return path.join(venvDir, 'Scripts', 'python.exe');
|
|
16
|
+
}
|
|
17
|
+
return path.join(venvDir, 'bin', 'python3');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getDataDir() {
|
|
21
|
+
const dir = process.env.FLAKE_DATA_DIR || path.join(process.cwd(), 'flakeiq-data');
|
|
22
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getDbPath() {
|
|
27
|
+
if (process.env.FLAKE_DB) return process.env.FLAKE_DB;
|
|
28
|
+
return path.join(getDataDir(), 'flake.db');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getConfig() {
|
|
32
|
+
return {
|
|
33
|
+
packageDir: PACKAGE_DIR,
|
|
34
|
+
pythonDir: PYTHON_DIR,
|
|
35
|
+
reporterPath: REPORTER_PATH,
|
|
36
|
+
venvDir: getVenvDir(),
|
|
37
|
+
venvPython: getVenvPython(),
|
|
38
|
+
dataDir: getDataDir(),
|
|
39
|
+
dbPath: getDbPath(),
|
|
40
|
+
port: parseInt(process.env.FLAKE_PORT || '8080', 10),
|
|
41
|
+
host: process.env.FLAKE_HOST || '127.0.0.1',
|
|
42
|
+
ollamaUrl: process.env.OLLAMA_URL || 'http://localhost:11434/api/generate',
|
|
43
|
+
ollamaModel: process.env.OLLAMA_MODEL || 'llama3.2',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isVenvReady() {
|
|
48
|
+
return fs.existsSync(getVenvPython());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { getVenvDir, getVenvPython, getDataDir, getDbPath, getConfig, isVenvReady, PACKAGE_DIR, PYTHON_DIR, REPORTER_PATH };
|