qa-intelligence 1.1.0 → 1.1.2

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 CHANGED
@@ -1,6 +1,8 @@
1
- # qa-intelligence
1
+ # QA Intelligence
2
2
 
3
- CI intelligence engine for Playwright test pipelines.
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 any existing Playwright project.
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 (existing Playwright project)
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
- | `.env` | Env validation |
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
- ### Option A: Copy the full workflow (recommended)
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
- Copy `.github/workflows/ci.yml` from the [framework template](https://github.com/ardithaqi/qa-intelligence-framework/blob/master/.github/workflows/ci.yml) into your repo.
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
- It includes everything wired together:
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
@@ -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 `${f.file}:${f.line}:${f.failure_type}`;
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))
@@ -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 failures;
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
- else if (file === "ai.txt") {
30
- const content = fs_1.default.readFileSync(full, "utf8");
31
- const data = extractTrailingJson(content);
32
- if (!data)
33
- continue;
34
- failures.push({
35
- file: data.file,
36
- line: data.line,
37
- failure_type: data.failure_type,
38
- severity: data.severity,
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 failures;
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 = `${item.file}:${item.line}:${item.failure_type}`;
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 `${f.file}:${f.line}:${f.failure_type}`;
88
+ return (0, failureIdentity_1.failureDiffKey)(f.file, f.failure_type);
88
89
  }
89
90
  function readFailures(options) {
90
91
  const root = options.rootDir;
@@ -46,7 +46,7 @@ function formatDiffComment(diff) {
46
46
  body += "\n";
47
47
  if (unchangedFailures.length > 0) {
48
48
  body +=
49
- "⚠️ **Existing issues** from the base branch are still failing on this branch. This PR was not blocked by them. Fix these on this branch or on the base branch to get to green.\n\n";
49
+ "⚠️ **Pre-existing failures** from the base branch are still failing. These are shown for visibility only **they do not block this PR**. Only **New Issues** block merge.\n\n";
50
50
  }
51
51
  body += formatSection("New Issues", realNewFailures);
52
52
  body += formatSection("Flaky", flaky, false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qa-intelligence",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "engines": {
5
5
  "node": ">=18"
6
6
  },