qa-intelligence 1.0.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 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,19 +10,28 @@ 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
 
28
+ npm automatically installs peer dependencies (`typescript`, `@types/node`) — no separate dev-deps step needed.
29
+
21
30
  ---
22
31
 
23
- ## Quick setup (existing project)
32
+ ## Setup
33
+
34
+ All paths below are relative to `playwright/` when using the recommended subfolder layout.
24
35
 
25
36
  ### 1. Environment (`.env`)
26
37
 
@@ -31,7 +42,26 @@ PW_WORKERS=2
31
42
  PW_RETRIES=1
32
43
  ```
33
44
 
34
- ### 2. `playwright.config.ts`
45
+ ### 2. `tsconfig.json` (TypeScript projects)
46
+
47
+ ```json
48
+ {
49
+ "compilerOptions": {
50
+ "target": "ES2022",
51
+ "module": "Node16",
52
+ "moduleResolution": "Node16",
53
+ "strict": true,
54
+ "esModuleInterop": true,
55
+ "types": ["node"],
56
+ "skipLibCheck": true
57
+ },
58
+ "include": ["tests/**/*", "playwright.config.ts"]
59
+ }
60
+ ```
61
+
62
+ > `module` and `moduleResolution` must both be `"Node16"` so TypeScript resolves the package `exports` map.
63
+
64
+ ### 3. `playwright.config.ts`
35
65
 
36
66
  ```ts
37
67
  import { defineConfig } from "@playwright/test";
@@ -53,7 +83,7 @@ export default defineConfig({
53
83
  });
54
84
  ```
55
85
 
56
- ### 3. Write tests
86
+ ### 4. Write tests
57
87
 
58
88
  Always import `test` from the package — **not** directly from Playwright:
59
89
 
@@ -73,13 +103,14 @@ import { step } from "qa-intelligence/playwright/steps";
73
103
  import { BasePage } from "qa-intelligence/playwright/basePage";
74
104
  ```
75
105
 
76
- ### 4. What you provide
106
+ ### 5. What you provide
77
107
 
78
108
  | You write | Package provides |
79
109
  |-----------|------------------|
80
- | `tests/` | Test hooks, artifact capture |
81
- | `.env` | Env validation |
82
- | `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)) |
83
114
  | `.github/workflows/ci.yml` | `qa-intelligence-diff`, `qa-intelligence-history`, `qa-intelligence-comment` |
84
115
  | Page objects (optional) | `BasePage`, `step()` |
85
116
 
@@ -87,11 +118,11 @@ import { BasePage } from "qa-intelligence/playwright/basePage";
87
118
 
88
119
  ## CI setup (GitHub Actions)
89
120
 
90
- ### 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.
91
122
 
92
- 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.).
93
124
 
94
- It includes everything wired together:
125
+ The workflow includes:
95
126
 
96
127
  - Docker test execution
97
128
  - Artifact upload on every run
@@ -104,28 +135,6 @@ Then update:
104
135
  - `BASE_URL` in the `docker run` step
105
136
  - GitHub secrets (see below)
106
137
 
107
- ### Option B: Add to your existing workflow
108
-
109
- If you already run Playwright in CI, add these steps **after** tests run and `artifacts/` are uploaded:
110
-
111
- ```bash
112
- npm install qa-intelligence
113
- npx qa-intelligence-diff --baseline baseline-artifacts --current artifacts
114
- npx qa-intelligence-history
115
- npx qa-intelligence-comment \
116
- --diff failure-diff.json \
117
- --repo owner/repo \
118
- --pr 123 \
119
- --token $GITHUB_TOKEN
120
- ```
121
-
122
- You must also handle on your own:
123
-
124
- - Uploading `artifacts/` after each run
125
- - Downloading `baseline-artifacts/` from the target branch on PRs
126
-
127
- See the [framework CI workflow](https://github.com/ardithaqi/qa-intelligence-framework/blob/master/.github/workflows/ci.yml) for reference.
128
-
129
138
  ### Required GitHub secrets
130
139
 
131
140
  - `OPENAI_API_KEY` — enables AI failure analysis in teardown
@@ -179,6 +188,12 @@ The template uses this package under the hood.
179
188
 
180
189
  ---
181
190
 
191
+ ## Author
192
+
193
+ Built as the intelligence engine behind QA automation — Playwright hooks, AI failure analysis, and PR-aware CI. If you use this package, consider leaving a star or linking back—contributions are welcome.
194
+
195
+ ---
196
+
182
197
  ## License
183
198
 
184
199
  MIT
@@ -0,0 +1,2 @@
1
+ export declare function analyzeFailureFile(jsonFilePath: string): Promise<string | null>;
2
+ export declare function analyzeLatestFailure(): Promise<string | null>;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Enriches failure-diff with recurrence: first_seen, occurrence_count.
4
+ * Persists run history in a cache file for the next run.
5
+ */
6
+ export {};
@@ -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))
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,7 @@
1
+ export declare const env: {
2
+ ENV: "dev" | "staging" | "prod";
3
+ BASE_URL: string;
4
+ HEADLESS: boolean;
5
+ PW_WORKERS: number;
6
+ PW_RETRIES: number;
7
+ };
@@ -0,0 +1,14 @@
1
+ export interface Failure {
2
+ file: string;
3
+ line: number;
4
+ failure_type: string;
5
+ severity: string;
6
+ confidence: number;
7
+ is_flaky_suspected?: boolean;
8
+ }
9
+ export declare function computeDiff(baselineDir: string, currentDir: string): {
10
+ newFailures: Failure[];
11
+ unchangedFailures: Failure[];
12
+ fixedFailures: Failure[];
13
+ blockingFailures: Failure[];
14
+ };
@@ -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
+ }
@@ -0,0 +1,10 @@
1
+ import { AiFailure, FailureKey } from "./types";
2
+ export type ReadFailuresOptions = {
3
+ rootDir: string;
4
+ };
5
+ export declare function failureKey(f: AiFailure): FailureKey;
6
+ export declare function readFailures(options: ReadFailuresOptions): {
7
+ failures: AiFailure[];
8
+ byKey: Map<FailureKey, AiFailure>;
9
+ sourceFiles: string[];
10
+ };
@@ -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;
@@ -0,0 +1,3 @@
1
+ import { DiffResult } from "./types";
2
+ export declare function formatDiffComment(diff: DiffResult): string;
3
+ export declare function hasFailureChanges(diff: DiffResult): boolean;
@@ -0,0 +1,21 @@
1
+ export type Severity = "low" | "medium" | "high";
2
+ export type FailureType = "assertion_mismatch" | "selector_not_found" | "timeout" | "navigation_failure" | "environment_error" | "unknown";
3
+ export type AiFailure = {
4
+ file: string;
5
+ line: number;
6
+ failure_type: FailureType | string;
7
+ expected?: number | string;
8
+ received?: number | string;
9
+ is_flaky_suspected?: boolean;
10
+ severity: Severity | string;
11
+ confidence: number;
12
+ first_seen?: string;
13
+ occurrence_count?: number;
14
+ };
15
+ export type FailureKey = string;
16
+ export type DiffResult = {
17
+ newFailures: AiFailure[];
18
+ unchangedFailures: AiFailure[];
19
+ fixedFailures: AiFailure[];
20
+ blockingFailures?: AiFailure[];
21
+ };
@@ -0,0 +1,6 @@
1
+ import { Page } from "@playwright/test";
2
+ export declare class BasePage {
3
+ protected page: Page;
4
+ constructor(page: Page);
5
+ protected navigate(path: string): Promise<void>;
6
+ }
@@ -0,0 +1,5 @@
1
+ import { expect } from "@playwright/test";
2
+ import { env } from "../config/env";
3
+ import "./testHooks";
4
+ export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
5
+ export { expect, env };
@@ -0,0 +1 @@
1
+ export default function globalSetup(): Promise<void>;
@@ -0,0 +1 @@
1
+ export default function globalTeardown(): Promise<void>;
@@ -0,0 +1 @@
1
+ export declare function step<T>(name: string, fn: () => Promise<T>): Promise<T>;
@@ -0,0 +1 @@
1
+ export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
@@ -0,0 +1,7 @@
1
+ export declare const logger: {
2
+ info: (msg: string) => void;
3
+ warn: (msg: string) => void;
4
+ error: (msg: string) => void;
5
+ debug: (msg: string) => void;
6
+ };
7
+ export declare function getLogFilePath(): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qa-intelligence",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "engines": {
5
5
  "node": ">=18"
6
6
  },
@@ -25,15 +25,35 @@
25
25
  "type": "commonjs",
26
26
  "main": "dist/index.js",
27
27
  "exports": {
28
- "./playwright": "./dist/playwright/baseTest.js",
29
- "./playwright/globalSetup": "./dist/playwright/globalSetup.js",
30
- "./playwright/globalTeardown": "./dist/playwright/globalTeardown.js",
31
- "./playwright/basePage": "./dist/playwright/basePage.js",
32
- "./playwright/steps": "./dist/playwright/steps.js",
33
- "./config/env": "./dist/config/env.js"
28
+ "./playwright": {
29
+ "types": "./dist/playwright/baseTest.d.ts",
30
+ "default": "./dist/playwright/baseTest.js"
31
+ },
32
+ "./playwright/globalSetup": {
33
+ "types": "./dist/playwright/globalSetup.d.ts",
34
+ "default": "./dist/playwright/globalSetup.js"
35
+ },
36
+ "./playwright/globalTeardown": {
37
+ "types": "./dist/playwright/globalTeardown.d.ts",
38
+ "default": "./dist/playwright/globalTeardown.js"
39
+ },
40
+ "./playwright/basePage": {
41
+ "types": "./dist/playwright/basePage.d.ts",
42
+ "default": "./dist/playwright/basePage.js"
43
+ },
44
+ "./playwright/steps": {
45
+ "types": "./dist/playwright/steps.d.ts",
46
+ "default": "./dist/playwright/steps.js"
47
+ },
48
+ "./config/env": {
49
+ "types": "./dist/config/env.d.ts",
50
+ "default": "./dist/config/env.js"
51
+ }
34
52
  },
35
53
  "peerDependencies": {
36
- "@playwright/test": ">=1.40.0"
54
+ "@playwright/test": ">=1.40.0",
55
+ "typescript": ">=5.0.0",
56
+ "@types/node": ">=18.0.0"
37
57
  },
38
58
  "bin": {
39
59
  "qa-intelligence": "dist/cli/index.js",