qa-intelligence 1.1.0 → 1.1.1
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 +20 -32
- package/dist/cli/history.js +2 -1
- package/dist/lib/computeDiff.js +51 -18
- package/dist/lib/failureIdentity.d.ts +7 -0
- package/dist/lib/failureIdentity.js +36 -0
- package/dist/lib/failureReader.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# QA Intelligence
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**npm:** [`qa-intelligence`](https://www.npmjs.com/package/qa-intelligence)
|
|
4
|
+
|
|
5
|
+
The intelligence engine for Playwright CI pipelines — install it into any project or use the full [QA Intelligence Framework](https://github.com/ardithaqi/qa-intelligence-framework) template.
|
|
4
6
|
|
|
5
7
|
- AI-powered failure analysis
|
|
6
8
|
- PR failure diff (new, flaky, still failing, fixed)
|
|
@@ -8,13 +10,18 @@ CI intelligence engine for Playwright test pipelines.
|
|
|
8
10
|
- PR blocking on new non-flaky failures
|
|
9
11
|
- Recurrence tracking ("3rd time since...")
|
|
10
12
|
|
|
11
|
-
Use it **without the framework template** — install the package into
|
|
13
|
+
Use it **without the framework template** — install the package into your existing app repo.
|
|
14
|
+
|
|
15
|
+
**Recommended layout:** create a `playwright/` subfolder at the repo root and keep all E2E tooling there, separate from your main app code. Full step-by-step guide (folder layout, `Dockerfile`, CI path changes): **[qa-intelligence-framework README](https://github.com/ardithaqi/qa-intelligence-framework#add-to-an-existing-project-npm-package)**.
|
|
12
16
|
|
|
13
17
|
---
|
|
14
18
|
|
|
15
19
|
## Install
|
|
16
20
|
|
|
21
|
+
Run inside your `playwright/` folder (or your Playwright project root if tests already live there):
|
|
22
|
+
|
|
17
23
|
```bash
|
|
24
|
+
cd playwright
|
|
18
25
|
npm install qa-intelligence @playwright/test
|
|
19
26
|
```
|
|
20
27
|
|
|
@@ -22,7 +29,9 @@ npm automatically installs peer dependencies (`typescript`, `@types/node`) — n
|
|
|
22
29
|
|
|
23
30
|
---
|
|
24
31
|
|
|
25
|
-
## Setup
|
|
32
|
+
## Setup
|
|
33
|
+
|
|
34
|
+
All paths below are relative to `playwright/` when using the recommended subfolder layout.
|
|
26
35
|
|
|
27
36
|
### 1. Environment (`.env`)
|
|
28
37
|
|
|
@@ -98,9 +107,10 @@ import { BasePage } from "qa-intelligence/playwright/basePage";
|
|
|
98
107
|
|
|
99
108
|
| You write | Package provides |
|
|
100
109
|
|-----------|------------------|
|
|
101
|
-
| `tests/` | Test hooks, artifact capture |
|
|
102
|
-
|
|
|
103
|
-
| `playwright.config.ts` | `globalSetup`, `globalTeardown`, AI teardown |
|
|
110
|
+
| `playwright/tests/` | Test hooks, artifact capture |
|
|
111
|
+
| `playwright/.env` | Env validation |
|
|
112
|
+
| `playwright/playwright.config.ts` | `globalSetup`, `globalTeardown`, AI teardown |
|
|
113
|
+
| `playwright/Dockerfile` | — (copy from [framework](https://github.com/ardithaqi/qa-intelligence-framework/blob/master/Dockerfile)) |
|
|
104
114
|
| `.github/workflows/ci.yml` | `qa-intelligence-diff`, `qa-intelligence-history`, `qa-intelligence-comment` |
|
|
105
115
|
| Page objects (optional) | `BasePage`, `step()` |
|
|
106
116
|
|
|
@@ -108,11 +118,11 @@ import { BasePage } from "qa-intelligence/playwright/basePage";
|
|
|
108
118
|
|
|
109
119
|
## CI setup (GitHub Actions)
|
|
110
120
|
|
|
111
|
-
|
|
121
|
+
Copy [`.github/workflows/ci.yml`](https://github.com/ardithaqi/qa-intelligence-framework/blob/master/.github/workflows/ci.yml) and [`Dockerfile`](https://github.com/ardithaqi/qa-intelligence-framework/blob/master/Dockerfile) from the framework repo.
|
|
112
122
|
|
|
113
|
-
|
|
123
|
+
When using the `playwright/` subfolder layout, apply the CI path changes documented in the [framework adoption guide](https://github.com/ardithaqi/qa-intelligence-framework#add-to-an-existing-project-npm-package) (`working-directory: playwright`, artifact paths, etc.).
|
|
114
124
|
|
|
115
|
-
|
|
125
|
+
The workflow includes:
|
|
116
126
|
|
|
117
127
|
- Docker test execution
|
|
118
128
|
- Artifact upload on every run
|
|
@@ -125,28 +135,6 @@ Then update:
|
|
|
125
135
|
- `BASE_URL` in the `docker run` step
|
|
126
136
|
- GitHub secrets (see below)
|
|
127
137
|
|
|
128
|
-
### Option B: Add to your existing workflow
|
|
129
|
-
|
|
130
|
-
If you already run Playwright in CI, add these steps **after** tests run and `artifacts/` are uploaded:
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
npm install qa-intelligence
|
|
134
|
-
npx qa-intelligence-diff --baseline baseline-artifacts --current artifacts
|
|
135
|
-
npx qa-intelligence-history
|
|
136
|
-
npx qa-intelligence-comment \
|
|
137
|
-
--diff failure-diff.json \
|
|
138
|
-
--repo owner/repo \
|
|
139
|
-
--pr 123 \
|
|
140
|
-
--token $GITHUB_TOKEN
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
You must also handle on your own:
|
|
144
|
-
|
|
145
|
-
- Uploading `artifacts/` after each run
|
|
146
|
-
- Downloading `baseline-artifacts/` from the target branch on PRs
|
|
147
|
-
|
|
148
|
-
See the [framework CI workflow](https://github.com/ardithaqi/qa-intelligence-framework/blob/master/.github/workflows/ci.yml) for reference.
|
|
149
|
-
|
|
150
138
|
### Required GitHub secrets
|
|
151
139
|
|
|
152
140
|
- `OPENAI_API_KEY` — enables AI failure analysis in teardown
|
package/dist/cli/history.js
CHANGED
|
@@ -10,10 +10,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
12
|
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const failureIdentity_1 = require("../lib/failureIdentity");
|
|
13
14
|
const HISTORY_PATH = ".cache/failure-history.json";
|
|
14
15
|
const MAX_RUNS = 20;
|
|
15
16
|
function failureKey(f) {
|
|
16
|
-
return
|
|
17
|
+
return (0, failureIdentity_1.failureDiffKey)(f.file, f.failure_type);
|
|
17
18
|
}
|
|
18
19
|
function loadHistory() {
|
|
19
20
|
if (!fs_1.default.existsSync(HISTORY_PATH))
|
package/dist/lib/computeDiff.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.computeDiff = computeDiff;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const failureIdentity_1 = require("./failureIdentity");
|
|
9
10
|
function extractTrailingJson(content) {
|
|
10
11
|
const trimmed = content.trimEnd();
|
|
11
12
|
for (let i = trimmed.lastIndexOf("{"); i >= 0; i = trimmed.lastIndexOf("{", i - 1)) {
|
|
@@ -16,39 +17,71 @@ function extractTrailingJson(content) {
|
|
|
16
17
|
}
|
|
17
18
|
return null;
|
|
18
19
|
}
|
|
20
|
+
function readMetaJson(aiTxtPath) {
|
|
21
|
+
const metaPath = path_1.default.join(path_1.default.dirname(aiTxtPath), "meta.json");
|
|
22
|
+
if (!fs_1.default.existsSync(metaPath))
|
|
23
|
+
return null;
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(fs_1.default.readFileSync(metaPath, "utf8"));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function resolveFailure(aiTxtPath) {
|
|
32
|
+
const content = fs_1.default.readFileSync(aiTxtPath, "utf8");
|
|
33
|
+
const ai = extractTrailingJson(content);
|
|
34
|
+
if (!ai?.failure_type || typeof ai.severity !== "string")
|
|
35
|
+
return null;
|
|
36
|
+
if (typeof ai.confidence !== "number")
|
|
37
|
+
return null;
|
|
38
|
+
const meta = readMetaJson(aiTxtPath);
|
|
39
|
+
const fromStack = (0, failureIdentity_1.parseLocationFromStack)(meta?.stack);
|
|
40
|
+
const file = (0, failureIdentity_1.normalizeTestPath)(fromStack?.file ?? (typeof ai.file === "string" ? ai.file : ""));
|
|
41
|
+
const line = fromStack?.line ??
|
|
42
|
+
(typeof ai.line === "number" ? ai.line : Number(ai.line) || 0);
|
|
43
|
+
if (!file)
|
|
44
|
+
return null;
|
|
45
|
+
return {
|
|
46
|
+
file,
|
|
47
|
+
line,
|
|
48
|
+
failure_type: ai.failure_type,
|
|
49
|
+
severity: ai.severity,
|
|
50
|
+
confidence: ai.confidence,
|
|
51
|
+
is_flaky_suspected: ai.is_flaky_suspected ?? meta?.is_flaky_suspected ?? false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
19
54
|
function collectFailures(root) {
|
|
20
|
-
const failures = [];
|
|
21
55
|
if (!fs_1.default.existsSync(root))
|
|
22
|
-
return
|
|
56
|
+
return [];
|
|
57
|
+
const bestByKey = new Map();
|
|
23
58
|
function walk(dir) {
|
|
24
59
|
for (const file of fs_1.default.readdirSync(dir)) {
|
|
25
60
|
const full = path_1.default.join(dir, file);
|
|
26
61
|
if (fs_1.default.statSync(full).isDirectory()) {
|
|
27
62
|
walk(full);
|
|
63
|
+
continue;
|
|
28
64
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
confidence: data.confidence,
|
|
40
|
-
is_flaky_suspected: data.is_flaky_suspected ?? false
|
|
41
|
-
});
|
|
65
|
+
if (file !== "ai.txt")
|
|
66
|
+
continue;
|
|
67
|
+
const failure = resolveFailure(full);
|
|
68
|
+
if (!failure)
|
|
69
|
+
continue;
|
|
70
|
+
const key = (0, failureIdentity_1.failureDiffKey)(failure.file, failure.failure_type);
|
|
71
|
+
const attempt = (0, failureIdentity_1.parseAttemptFromAiPath)(full);
|
|
72
|
+
const existing = bestByKey.get(key);
|
|
73
|
+
if (!existing || attempt >= existing.attempt) {
|
|
74
|
+
bestByKey.set(key, { failure, attempt });
|
|
42
75
|
}
|
|
43
76
|
}
|
|
44
77
|
}
|
|
45
78
|
walk(root);
|
|
46
|
-
return
|
|
79
|
+
return [...bestByKey.values()].map((entry) => entry.failure);
|
|
47
80
|
}
|
|
48
81
|
function toMap(arr) {
|
|
49
82
|
const map = new Map();
|
|
50
83
|
for (const item of arr) {
|
|
51
|
-
const key =
|
|
84
|
+
const key = (0, failureIdentity_1.failureDiffKey)(item.file, item.failure_type);
|
|
52
85
|
map.set(key, item);
|
|
53
86
|
}
|
|
54
87
|
return map;
|
|
@@ -76,6 +109,6 @@ function computeDiff(baselineDir, currentDir) {
|
|
|
76
109
|
newFailures,
|
|
77
110
|
unchangedFailures,
|
|
78
111
|
fixedFailures,
|
|
79
|
-
blockingFailures
|
|
112
|
+
blockingFailures,
|
|
80
113
|
};
|
|
81
114
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function normalizeTestPath(file: string): string;
|
|
2
|
+
export declare function parseLocationFromStack(stack?: string): {
|
|
3
|
+
file: string;
|
|
4
|
+
line: number;
|
|
5
|
+
} | null;
|
|
6
|
+
export declare function failureDiffKey(file: string, failure_type: string): string;
|
|
7
|
+
export declare function parseAttemptFromAiPath(aiTxtPath: string): number;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeTestPath = normalizeTestPath;
|
|
4
|
+
exports.parseLocationFromStack = parseLocationFromStack;
|
|
5
|
+
exports.failureDiffKey = failureDiffKey;
|
|
6
|
+
exports.parseAttemptFromAiPath = parseAttemptFromAiPath;
|
|
7
|
+
function normalizeTestPath(file) {
|
|
8
|
+
const p = file.replace(/\\/g, "/").trim();
|
|
9
|
+
const testsIdx = p.indexOf("tests/");
|
|
10
|
+
if (testsIdx >= 0)
|
|
11
|
+
return p.slice(testsIdx);
|
|
12
|
+
return `tests/${p.replace(/^\//, "")}`;
|
|
13
|
+
}
|
|
14
|
+
function parseLocationFromStack(stack) {
|
|
15
|
+
if (!stack)
|
|
16
|
+
return null;
|
|
17
|
+
const normalized = stack.replace(/\\/g, "/");
|
|
18
|
+
const match = normalized.match(/(tests\/[^\s:)]+?\.ts):(\d+)/);
|
|
19
|
+
if (!match)
|
|
20
|
+
return null;
|
|
21
|
+
return {
|
|
22
|
+
file: match[1],
|
|
23
|
+
line: Number(match[2]),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function failureDiffKey(file, failure_type) {
|
|
27
|
+
return `${normalizeTestPath(file)}:${failure_type}`;
|
|
28
|
+
}
|
|
29
|
+
function parseAttemptFromAiPath(aiTxtPath) {
|
|
30
|
+
const normalized = aiTxtPath.replace(/\\/g, "/");
|
|
31
|
+
const match = normalized.match(/\/attempt-(\d+)\/ai\.txt$/);
|
|
32
|
+
if (!match)
|
|
33
|
+
return 0;
|
|
34
|
+
const attempt = Number(match[1]);
|
|
35
|
+
return Number.isFinite(attempt) ? attempt : 0;
|
|
36
|
+
}
|
|
@@ -7,6 +7,7 @@ exports.failureKey = failureKey;
|
|
|
7
7
|
exports.readFailures = readFailures;
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const failureIdentity_1 = require("./failureIdentity");
|
|
10
11
|
function isDirectory(p) {
|
|
11
12
|
return fs_1.default.existsSync(p) && fs_1.default.statSync(p).isDirectory();
|
|
12
13
|
}
|
|
@@ -68,7 +69,7 @@ function parseAiTxt(filePath) {
|
|
|
68
69
|
}
|
|
69
70
|
const lineNum = typeof parsed.line === "string" ? Number(parsed.line) : parsed.line;
|
|
70
71
|
const failure = {
|
|
71
|
-
file: parsed.file,
|
|
72
|
+
file: (0, failureIdentity_1.normalizeTestPath)(parsed.file),
|
|
72
73
|
line: Number.isFinite(lineNum) ? lineNum : 0,
|
|
73
74
|
failure_type: parsed.failure_type,
|
|
74
75
|
expected: parsed.expected,
|
|
@@ -84,7 +85,7 @@ function parseAiTxt(filePath) {
|
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
function failureKey(f) {
|
|
87
|
-
return
|
|
88
|
+
return (0, failureIdentity_1.failureDiffKey)(f.file, f.failure_type);
|
|
88
89
|
}
|
|
89
90
|
function readFailures(options) {
|
|
90
91
|
const root = options.rootDir;
|