@qatonic_innovations/qaios 0.5.2 → 0.6.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 +48 -42
- package/dist/GateReview-MDXNA542.js +127 -0
- package/dist/SnapshotReview-7TPWX62T.js +137 -0
- package/dist/a11y/scripts/axe-runner.mjs +8 -1
- package/dist/a11y-X3GULPJY.js +355 -0
- package/dist/chunk-3SEEYC6R.js +1803 -0
- package/dist/chunk-6MCKYCGA.js +63 -0
- package/dist/chunk-AW4GQLOM.js +67 -0
- package/dist/chunk-EAMUH7ZB.js +23 -0
- package/dist/chunk-ICKV4DDG.js +231 -0
- package/dist/chunk-IFKXORJV.js +298 -0
- package/dist/chunk-KAXYG56C.js +185 -0
- package/dist/chunk-MABXT3RT.js +514 -0
- package/dist/chunk-PGTYCHBV.js +18 -0
- package/dist/chunk-RDZE7TZG.js +17 -0
- package/dist/chunk-RQP7ALWK.js +2068 -0
- package/dist/chunk-SKSVFAPZ.js +240 -0
- package/dist/chunk-VSSX6F42.js +27 -0
- package/dist/chunk-WUHORHPY.js +42 -0
- package/dist/chunk-XKBSTVSC.js +14116 -0
- package/dist/chunk-YIZRJPII.js +27 -0
- package/dist/config-3JZDCGLC.js +648 -0
- package/dist/config-schema.d.ts +20 -0
- package/dist/config-schema.js +3 -0
- package/dist/dist-P3PQVEIR.js +2 -0
- package/dist/doctor-AKOPTL62.js +904 -0
- package/dist/evidence-6RUORR7P.js +224 -0
- package/dist/explore-CMTPFLIB.js +423 -0
- package/dist/fix-OUIERLSI.js +796 -0
- package/dist/healing/scripts/capture-page.mjs +19 -2
- package/dist/history-QPKCL64Y.js +495 -0
- package/dist/index.d.ts +45 -1
- package/dist/index.js +765 -14786
- package/dist/init-5DG222SD.js +199 -0
- package/dist/manifest-seal-QXGIQ4LI.js +5 -0
- package/dist/mcp-NQQEZNQW.js +261 -0
- package/dist/migrations/0004_review_records.sql +56 -0
- package/dist/review-AWRKQMIE.js +499 -0
- package/dist/run-XXK4SY7X.js +303 -0
- package/dist/schemas/config-layer-v1.schema.json +381 -0
- package/dist/schemas/config-v1.schema.json +384 -0
- package/dist/snapshot-NWJXLNZC.js +442 -0
- package/dist/snapshot-review-LRRPNAYH.js +7 -0
- package/dist/test-W5BFE5IS.js +10 -0
- package/dist/visual/scripts/capture-screenshot.mjs +3 -0
- package/dist/xray-BON5QPYR.js +674 -0
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# QAIOS
|
|
2
2
|
|
|
3
|
-
> **AI QA engineer in your terminal.** Designs, writes, runs, heals, and explores tests for web UI and APIs — with
|
|
3
|
+
> **AI QA engineer in your terminal.** Designs, writes, runs, heals, and explores tests for web UI and APIs — with tamper-evident local history.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@qatonic_innovations/qaios)
|
|
6
6
|
[](https://github.com/qatonic/qaios/blob/main/LICENSE)
|
|
@@ -10,7 +10,7 @@ QAIOS acts like a QA engineer on your team. Point it at a feature description or
|
|
|
10
10
|
|
|
11
11
|
**Mental model: Claude Code, but for QA work instead of feature coding.**
|
|
12
12
|
|
|
13
|
-
> **Status:
|
|
13
|
+
> **Status: pre-1.0** ([changelog](https://github.com/qatonic/qaios/blob/main/CHANGELOG.md)). Core flows — `init`, `doctor`, test design
|
|
14
14
|
> & generation, Playwright execution, self-healing, accessibility, and the
|
|
15
15
|
> audit log — work today. Expect rough edges and please
|
|
16
16
|
> [report issues](https://github.com/qatonic/qaios/issues). After installing,
|
|
@@ -35,10 +35,11 @@ The published package is `@qatonic_innovations/qaios`; the installed **command i
|
|
|
35
35
|
|
|
36
36
|
### Requirements
|
|
37
37
|
|
|
38
|
-
- **Node.js 22
|
|
39
|
-
(`node:sqlite
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
- **Node.js 22.13 or newer.** QAIOS uses Node's **built-in** SQLite
|
|
39
|
+
(`node:sqlite`) for its local audit log — **no native module to compile**,
|
|
40
|
+
so `npm install` is just a download. `node:sqlite` loads without a flag from
|
|
41
|
+
Node 22.13; before that it isn't available, so upgrade Node. Node 22 still
|
|
42
|
+
labels it experimental; QAIOS hides that one warning.
|
|
42
43
|
- An **LLM provider API key.** QAIOS uses **Anthropic** by default — get a key
|
|
43
44
|
at [console.anthropic.com](https://console.anthropic.com/settings/keys) and
|
|
44
45
|
export it:
|
|
@@ -63,7 +64,7 @@ The published package is `@qatonic_innovations/qaios`; the installed **command i
|
|
|
63
64
|
|
|
64
65
|
QAIOS has **no native dependencies** — it uses Node's built-in SQLite — so
|
|
65
66
|
install is friction-free. If `qaios init` reports a SQLite/`node:sqlite` error,
|
|
66
|
-
your Node is too old: run `node -v` and upgrade to **Node 22
|
|
67
|
+
your Node is too old: run `node -v` and upgrade to **Node 22.13 or newer**.
|
|
67
68
|
|
|
68
69
|
`qaios doctor` will tell you exactly which check failed and what to run.
|
|
69
70
|
|
|
@@ -95,7 +96,7 @@ Generated:
|
|
|
95
96
|
tests/auth/LoginPage.po.ts
|
|
96
97
|
.qaios/specs/01JTZ1…/DesignSpec.json
|
|
97
98
|
|
|
98
|
-
Cost: 11,403 tokens · $0.06 (3 of
|
|
99
|
+
Cost: 11,403 tokens · $0.06 (3 of 20 LLM calls used)
|
|
99
100
|
```
|
|
100
101
|
|
|
101
102
|
> **Gates:** on HIGH/CRITICAL-risk features QAIOS pauses for review. Approve interactively with `qaios review`, or pass `--yes` to auto-approve for a non-interactive run.
|
|
@@ -107,17 +108,17 @@ Cost: 11,403 tokens · $0.06 (3 of 15 LLM calls used)
|
|
|
107
108
|
| Command | What it does |
|
|
108
109
|
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
109
110
|
| `qaios init [path]` | Bootstrap a project: create `.qaios/`, `QAIOS.md`, run migrations. |
|
|
110
|
-
| `qaios doctor` | Health-check your environment
|
|
111
|
+
| `qaios doctor` | Health-check your environment. Use `--deep` for read-only Chromium/MCP probes and `--json` for stable CI diagnostics. |
|
|
111
112
|
| `qaios test <description>` | Design + generate tests from natural language. `--type api` for OpenAPI. `--run` to execute. `--yes` to auto-approve gates. |
|
|
112
113
|
| `qaios run [pattern]` | Execute Playwright tests, classify failures (real / flaky / locator drift), auto-heal locator breaks, file defects. |
|
|
113
114
|
| `qaios fix [target]` | Self-heal a broken locator. `--last-failure` to target the most recent failure, `--auto` to apply, `--dry-run` to preview. |
|
|
114
|
-
| `qaios explore <url>` |
|
|
115
|
+
| `qaios explore <url>` | Charter + unverified hypotheses from one page snapshot (no browser session). `--focus`, `--file-hypotheses`. |
|
|
115
116
|
| `qaios a11y <url>` | Deterministic axe-core accessibility scan + AI triage. `--wcag-level A\|AA\|AAA`. Exits 1 on real violations (CI-gateable). |
|
|
116
117
|
| `qaios snapshot capture\|check\|review` | Visual regression: capture baselines, diff, and triage AI-classified diffs. |
|
|
117
118
|
| `qaios review` | Walk pending human-review gates and approve/reject; resumes the workflow. |
|
|
118
119
|
| `qaios history [--verify]` | List workflows, inspect the audit log, and verify the hash chain. |
|
|
119
120
|
| `qaios config get\|set\|show\|edit` | Read/write `.qaios/config.yaml` (schema-validated). |
|
|
120
|
-
| `qaios mcp list\|add\|remove\|enable\|disable\|test` | Manage MCP servers (GitHub, Jira,
|
|
121
|
+
| `qaios mcp list\|add\|remove\|enable\|disable\|test` | Manage stdio MCP servers (GitHub, Jira, custom). |
|
|
121
122
|
|
|
122
123
|
Run `qaios <command> --help` for the full option list of any command.
|
|
123
124
|
|
|
@@ -136,7 +137,7 @@ qaios fix tests/auth/login.spec.ts --last-failure --dry-run
|
|
|
136
137
|
# Accessibility scan, gate CI on real WCAG violations
|
|
137
138
|
qaios a11y https://staging.myapp.com --wcag-level AA
|
|
138
139
|
|
|
139
|
-
#
|
|
140
|
+
# Exploration charter + unverified hypotheses (no browser session)
|
|
140
141
|
qaios explore https://staging.myapp.com --duration 900 --focus "checkout flow"
|
|
141
142
|
|
|
142
143
|
# Visual regression
|
|
@@ -165,11 +166,13 @@ testing:
|
|
|
165
166
|
framework: playwright
|
|
166
167
|
testDir: tests
|
|
167
168
|
testIdAttribute: data-testid # e.g. set to 'data-test' for some apps
|
|
169
|
+
# fixtures: tests/fixtures.ts # optional: your `base.extend(...)` test; generated specs use it
|
|
168
170
|
locatorPreference: [data-testid, role, text]
|
|
169
171
|
gates:
|
|
170
172
|
autoExpireOnTimeout: abort # abort | auto_approve | auto_reject
|
|
171
173
|
defects:
|
|
172
174
|
target: stdout # stdout | github | jira
|
|
175
|
+
reproduceBeforeFiling: true # GitHub/Jira only get failures that fail again on a re-run
|
|
173
176
|
```
|
|
174
177
|
|
|
175
178
|
`qaios config show` prints the resolved config; `qaios config set <key> <value>` validates against the schema before writing. **API keys are never written to config** — they come from environment variables.
|
|
@@ -189,18 +192,20 @@ llm:
|
|
|
189
192
|
# OpenAI
|
|
190
193
|
llm:
|
|
191
194
|
provider: openai # reads OPENAI_API_KEY
|
|
192
|
-
model: gpt-
|
|
195
|
+
model: gpt-6.1-sol # optional; the default (try gpt-6-luna for lower cost)
|
|
193
196
|
```
|
|
194
197
|
|
|
195
198
|
```bash
|
|
196
199
|
export OPENAI_API_KEY=sk-...
|
|
197
|
-
qaios doctor # confirms the
|
|
200
|
+
qaios doctor # confirms the provider accepts your key (add --offline to skip the call)
|
|
198
201
|
```
|
|
199
202
|
|
|
200
203
|
The rest of QAIOS is provider-blind — every command (`test`, `run`, `fix`,
|
|
201
204
|
`explore`, `a11y`, …) works identically on either. Structured output uses each
|
|
202
|
-
provider's native
|
|
203
|
-
|
|
205
|
+
provider's native structured-output mode (a `submit` tool on Anthropic; strict
|
|
206
|
+
function calling on OpenAI), and every answer is validated against the skill's
|
|
207
|
+
schema, so generated artifacts stay schema-valid. GPT-5 and later models are
|
|
208
|
+
called through OpenAI's Responses API; GPT-4 models through Chat Completions. You can
|
|
204
209
|
override the key's env-var name with `llm.apiKeyEnv` if needed.
|
|
205
210
|
|
|
206
211
|
#### Picking a model
|
|
@@ -209,21 +214,22 @@ override the key's env-var name with `llm.apiKeyEnv` if needed.
|
|
|
209
214
|
that provider's API accepts works**, not just the defaults. When `llm.model` is
|
|
210
215
|
omitted, QAIOS uses a sensible default per provider:
|
|
211
216
|
|
|
212
|
-
| Provider | Default
|
|
213
|
-
| ----------- |
|
|
214
|
-
| `anthropic` | `claude-sonnet-
|
|
215
|
-
| `openai` | `gpt-
|
|
217
|
+
| Provider | Default ($ per 1M tokens in / out) | Other examples you can set |
|
|
218
|
+
| ----------- | ---------------------------------- | --------------------------------------------------------------- |
|
|
219
|
+
| `anthropic` | `claude-sonnet-5-5` ($2 / $10) | `claude-opus-5-5` ($4 / $20), `claude-haiku-5-5` (cheapest), … |
|
|
220
|
+
| `openai` | `gpt-6.1-sol` ($2 / $10) | `gpt-6-astra` ($10 / $50), `gpt-6-luna` (cheapest), `gpt-4o`, … |
|
|
216
221
|
|
|
217
222
|
```yaml
|
|
218
223
|
llm:
|
|
219
224
|
provider: openai
|
|
220
|
-
model: gpt-
|
|
225
|
+
model: gpt-6-luna # any OpenAI model id
|
|
221
226
|
```
|
|
222
227
|
|
|
223
228
|
Two things to know about non-default models:
|
|
224
229
|
|
|
225
230
|
- **Cost tracking.** QAIOS knows exact pricing for a built-in set of models
|
|
226
|
-
(Sonnet/Opus/Haiku
|
|
231
|
+
(Claude Sonnet/Opus/Haiku/Fable 4.x-5.x; GPT-6 Sol/Astra/Luna and
|
|
232
|
+
gpt-4o/4o-mini/4.1/4.1-mini). A model **outside** that
|
|
227
233
|
set still runs fine, but the USD figure in the audit log is approximate — it
|
|
228
234
|
bills at a default rate and prints a one-time `no pricing for model …`
|
|
229
235
|
warning. The per-workflow call/cost **cap still applies** regardless.
|
|
@@ -250,7 +256,7 @@ qaios config set mode TRUST
|
|
|
250
256
|
|
|
251
257
|
## Cost & privacy
|
|
252
258
|
|
|
253
|
-
- Skills
|
|
259
|
+
- Skills use Anthropic or OpenAI per `llm.provider`. Each workflow is capped at `min(20 calls, $1.00)` by default (configurable) and aborts if exceeded.
|
|
254
260
|
- **Local-first.** No telemetry, no phone-home. The only outbound traffic is (a) LLM calls to your configured provider (Anthropic or OpenAI) with the prompts QAIOS builds, and (b) any MCP servers you configure. You bring your own API key; QAIOS does not proxy your requests. See [SECURITY.md](https://github.com/qatonic/qaios/blob/main/SECURITY.md) for exactly what's sent.
|
|
255
261
|
|
|
256
262
|
---
|
|
@@ -259,18 +265,18 @@ qaios config set mode TRUST
|
|
|
259
265
|
|
|
260
266
|
Every command exits with a stable, documented code so you can gate pipelines:
|
|
261
267
|
|
|
262
|
-
| Code | Meaning | Example
|
|
263
|
-
| ----- | ------------------------------- |
|
|
264
|
-
| `0` | Success | tests generated; `doctor` all-green; `
|
|
265
|
-
| `1` | User error / actionable failure | bad flag; `
|
|
266
|
-
| `2` | Gate blocked (informational) |
|
|
267
|
-
| `3` | Tool/dependency error | Playwright or axe not installed; SQLite binding missing
|
|
268
|
-
| `4` | LLM error | rate limit, timeout, or invalid API key
|
|
269
|
-
| `5` | Internal error | unexpected crash (re-run with `--debug` for a stack trace)
|
|
270
|
-
| `130` | Cancelled | you pressed Ctrl+C
|
|
268
|
+
| Code | Meaning | Example |
|
|
269
|
+
| ----- | ------------------------------- | ------------------------------------------------------------------------ |
|
|
270
|
+
| `0` | Success | tests generated; `doctor` all-green; `fix --dry-run` found a valid patch |
|
|
271
|
+
| `1` | User error / actionable failure | bad flag; `run` had failing tests; `fix` found no valid fix |
|
|
272
|
+
| `2` | Gate blocked (informational) | paused for human review — run `qaios review`, or `fix` without `--auto` |
|
|
273
|
+
| `3` | Tool/dependency error | Playwright or axe not installed; SQLite binding missing |
|
|
274
|
+
| `4` | LLM error | rate limit, timeout, or invalid API key |
|
|
275
|
+
| `5` | Internal error | unexpected crash (re-run with `--debug` for a stack trace) |
|
|
276
|
+
| `130` | Cancelled | you pressed Ctrl+C |
|
|
271
277
|
|
|
272
|
-
|
|
273
|
-
|
|
278
|
+
Every command supports protocol-v1 machine output: `--json` emits one terminal object,
|
|
279
|
+
while `--jsonl` emits ordered events followed by one terminal object.
|
|
274
280
|
|
|
275
281
|
---
|
|
276
282
|
|
|
@@ -280,20 +286,20 @@ output for CI consumption.
|
|
|
280
286
|
- [Commands Reference](https://github.com/qatonic/qaios/blob/main/docs/commands.md) — every CLI command
|
|
281
287
|
- [Skills Reference](https://github.com/qatonic/qaios/blob/main/docs/skills-reference.md) — what each skill does
|
|
282
288
|
- [MCP Servers](https://github.com/qatonic/qaios/blob/main/docs/mcp-servers.md) — adding integrations
|
|
289
|
+
- [JSON Output](https://github.com/qatonic/qaios/blob/main/docs/json-output.md) — automation compatibility
|
|
290
|
+
- [Compatibility Policy](https://github.com/qatonic/qaios/blob/main/docs/compatibility.md) — stability and deprecation
|
|
283
291
|
- A runnable end-to-end example lives in [`examples/live-saucedemo`](https://github.com/qatonic/qaios/tree/main/examples/live-saucedemo).
|
|
284
292
|
|
|
285
293
|
---
|
|
286
294
|
|
|
287
295
|
## Status & roadmap
|
|
288
296
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
-
|
|
293
|
-
|
|
294
|
-
-
|
|
295
|
-
|
|
296
|
-
The roadmap is gated on real-world validation, not a calendar.
|
|
297
|
+
Releases and their changes are listed in the
|
|
298
|
+
[changelog](https://github.com/qatonic/qaios/blob/main/CHANGELOG.md). Current work focuses on
|
|
299
|
+
truth and consistency before the trustworthy-alpha, agent-runtime, quality-intelligence,
|
|
300
|
+
ecosystem, and stable-OS phases.
|
|
301
|
+
The [active roadmap](https://github.com/qatonic/qaios/blob/main/QAIOS_PRODUCT_AND_ENGINEERING_ROADMAP.md)
|
|
302
|
+
is validation-gated, not calendar-driven.
|
|
297
303
|
|
|
298
304
|
---
|
|
299
305
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { useApp, useInput, Box, Text } from 'ink';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
function dispatchGateKeypress(input, ctrl = false) {
|
|
7
|
+
const key = input.toLowerCase();
|
|
8
|
+
if (key === "q" || ctrl && key === "c") return { kind: "quit" };
|
|
9
|
+
if (key === "a") return { kind: "decide", action: "approve" };
|
|
10
|
+
if (key === "r") return { kind: "decide", action: "reject" };
|
|
11
|
+
if (key === "s") return { kind: "decide", action: "skip" };
|
|
12
|
+
return { kind: "ignore" };
|
|
13
|
+
}
|
|
14
|
+
function severityColor(level) {
|
|
15
|
+
switch (level) {
|
|
16
|
+
case "CRITICAL":
|
|
17
|
+
return "red";
|
|
18
|
+
case "HIGH":
|
|
19
|
+
return "yellow";
|
|
20
|
+
case "MEDIUM":
|
|
21
|
+
return "cyan";
|
|
22
|
+
default:
|
|
23
|
+
return "gray";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function GatePreview({ gate }) {
|
|
27
|
+
const payload = gate.payload;
|
|
28
|
+
const summaryLines = [];
|
|
29
|
+
if (gate.gateType === "design_review") {
|
|
30
|
+
if (payload["featureName"]) summaryLines.push(`feature: ${String(payload["featureName"])}`);
|
|
31
|
+
if (payload["suiteName"]) summaryLines.push(`suite: ${String(payload["suiteName"])}`);
|
|
32
|
+
if (payload["scenarioCount"])
|
|
33
|
+
summaryLines.push(`scenarios: ${String(payload["scenarioCount"])}`);
|
|
34
|
+
} else if (gate.gateType === "writing_review") {
|
|
35
|
+
if (payload["fileCount"]) summaryLines.push(`files: ${String(payload["fileCount"])}`);
|
|
36
|
+
if (payload["qaiosMdSuggestionCount"]) {
|
|
37
|
+
summaryLines.push(`review notes: ${String(payload["qaiosMdSuggestionCount"])}`);
|
|
38
|
+
}
|
|
39
|
+
} else if (gate.gateType === "healing_review") {
|
|
40
|
+
if (payload["file"]) summaryLines.push(`file: ${String(payload["file"])}`);
|
|
41
|
+
if (payload["oldLocator"]) summaryLines.push(`- ${String(payload["oldLocator"])}`);
|
|
42
|
+
if (payload["newLocator"]) summaryLines.push(`+ ${String(payload["newLocator"])}`);
|
|
43
|
+
} else if (gate.gateType === "visual_review") {
|
|
44
|
+
if (payload["target"]) summaryLines.push(`screenshot: ${String(payload["target"])}`);
|
|
45
|
+
if (payload["changed"]) summaryLines.push(`changed: ${String(payload["changed"])}`);
|
|
46
|
+
if (payload["verdict"]) summaryLines.push(`classifier: ${String(payload["verdict"])}`);
|
|
47
|
+
if (payload["currentImage"]) summaryLines.push(`new image: ${String(payload["currentImage"])}`);
|
|
48
|
+
if (payload["diffImage"]) summaryLines.push(`diff image: ${String(payload["diffImage"])}`);
|
|
49
|
+
}
|
|
50
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [
|
|
51
|
+
summaryLines.map((l) => /* @__PURE__ */ jsx(Text, { color: "white", children: l }, l)),
|
|
52
|
+
payload["reasoning"] !== void 0 && payload["reasoning"] !== null && String(payload["reasoning"]).length > 0 && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
53
|
+
String(payload["reasoning"]).slice(0, 240),
|
|
54
|
+
String(payload["reasoning"]).length > 240 ? "\u2026" : ""
|
|
55
|
+
] }) })
|
|
56
|
+
] });
|
|
57
|
+
}
|
|
58
|
+
function GateReview(props) {
|
|
59
|
+
const { gates, onDecide, onExit } = props;
|
|
60
|
+
const [index, setIndex] = useState(0);
|
|
61
|
+
const { exit } = useApp();
|
|
62
|
+
const current = gates[index];
|
|
63
|
+
useInput((input, key) => {
|
|
64
|
+
if (current === void 0) return;
|
|
65
|
+
const intent = dispatchGateKeypress(input, key.ctrl);
|
|
66
|
+
if (intent.kind === "quit") {
|
|
67
|
+
onExit("quit");
|
|
68
|
+
exit();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (intent.kind === "decide") {
|
|
72
|
+
onDecide(current, intent.action);
|
|
73
|
+
next();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
function next() {
|
|
77
|
+
if (index >= gates.length - 1) {
|
|
78
|
+
onExit("completed");
|
|
79
|
+
exit();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
setIndex(index + 1);
|
|
83
|
+
}
|
|
84
|
+
if (gates.length === 0) {
|
|
85
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2713 No pending gates." }) });
|
|
86
|
+
}
|
|
87
|
+
if (current === void 0) {
|
|
88
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
89
|
+
"Reviewing gates\u2026 (",
|
|
90
|
+
index + 1,
|
|
91
|
+
" of ",
|
|
92
|
+
gates.length,
|
|
93
|
+
")"
|
|
94
|
+
] }) });
|
|
95
|
+
}
|
|
96
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
97
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
98
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "\u26A0 Review: " }),
|
|
99
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: severityColor(current.riskLevel), children: current.gateType }),
|
|
100
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
101
|
+
" ",
|
|
102
|
+
"(",
|
|
103
|
+
index + 1,
|
|
104
|
+
"/",
|
|
105
|
+
gates.length,
|
|
106
|
+
")"
|
|
107
|
+
] })
|
|
108
|
+
] }),
|
|
109
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
110
|
+
"Skill: ",
|
|
111
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: current.skillId }),
|
|
112
|
+
" \xB7 Risk:",
|
|
113
|
+
" ",
|
|
114
|
+
/* @__PURE__ */ jsx(Text, { color: severityColor(current.riskLevel), children: current.riskLevel }),
|
|
115
|
+
" \xB7 Confidence:",
|
|
116
|
+
" ",
|
|
117
|
+
current.confidence.toFixed(2),
|
|
118
|
+
" \xB7 Action: ",
|
|
119
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: current.requiredAction })
|
|
120
|
+
] }) }),
|
|
121
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { children: "Preview:" }) }),
|
|
122
|
+
/* @__PURE__ */ jsx(GatePreview, { gate: current }),
|
|
123
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[a]pprove \xB7 [r]eject \xB7 [s]kip \xB7 [q]uit" }) })
|
|
124
|
+
] });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export { GateReview, dispatchGateKeypress };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { useApp, useInput, Box, Text } from 'ink';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
function dispatchKeypress(input) {
|
|
7
|
+
switch (input.toLowerCase()) {
|
|
8
|
+
case "a":
|
|
9
|
+
return { kind: "decide", action: "approve" };
|
|
10
|
+
case "r":
|
|
11
|
+
return { kind: "decide", action: "reject" };
|
|
12
|
+
case "s":
|
|
13
|
+
return { kind: "decide", action: "skip" };
|
|
14
|
+
case "q":
|
|
15
|
+
return { kind: "quit" };
|
|
16
|
+
default:
|
|
17
|
+
return { kind: "ignore" };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function formatPct(p) {
|
|
21
|
+
if (p >= 1) return `${p.toFixed(2)}%`;
|
|
22
|
+
if (p > 0) return `${p.toFixed(3)}%`;
|
|
23
|
+
return `0%`;
|
|
24
|
+
}
|
|
25
|
+
function ClassificationView({
|
|
26
|
+
classification
|
|
27
|
+
}) {
|
|
28
|
+
if (classification === null) {
|
|
29
|
+
return /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "(unclassified \u2014 visual.diff was skipped or failed)" });
|
|
30
|
+
}
|
|
31
|
+
const color = classification.verdict === "regression" ? "red" : classification.verdict === "intentional" ? "green" : classification.verdict === "noise" ? "gray" : "yellow";
|
|
32
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
33
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
34
|
+
"Verdict:",
|
|
35
|
+
" ",
|
|
36
|
+
/* @__PURE__ */ jsx(Text, { color, bold: true, children: classification.verdict }),
|
|
37
|
+
" ",
|
|
38
|
+
"(confidence ",
|
|
39
|
+
classification.confidence.toFixed(2),
|
|
40
|
+
")"
|
|
41
|
+
] }),
|
|
42
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
43
|
+
"Suggested: ",
|
|
44
|
+
classification.suggestedAction
|
|
45
|
+
] }),
|
|
46
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: classification.reasoning }) }),
|
|
47
|
+
classification.affectedAreas.length > 0 ? /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
48
|
+
"Affected: ",
|
|
49
|
+
classification.affectedAreas.join(", ")
|
|
50
|
+
] }) }) : null
|
|
51
|
+
] });
|
|
52
|
+
}
|
|
53
|
+
function SnapshotReview({
|
|
54
|
+
items,
|
|
55
|
+
onDecide,
|
|
56
|
+
onExit
|
|
57
|
+
}) {
|
|
58
|
+
const { exit } = useApp();
|
|
59
|
+
const [index, setIndex] = useState(0);
|
|
60
|
+
const [exited, setExited] = useState(false);
|
|
61
|
+
const finish = (reason) => {
|
|
62
|
+
if (exited) return;
|
|
63
|
+
setExited(true);
|
|
64
|
+
onExit(reason);
|
|
65
|
+
exit();
|
|
66
|
+
};
|
|
67
|
+
useInput((input) => {
|
|
68
|
+
if (exited || items.length === 0 || index >= items.length) return;
|
|
69
|
+
const intent = dispatchKeypress(input);
|
|
70
|
+
if (intent.kind === "ignore") return;
|
|
71
|
+
if (intent.kind === "quit") {
|
|
72
|
+
finish("quit");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
onDecide(items[index], intent.action);
|
|
76
|
+
const next = index + 1;
|
|
77
|
+
if (next >= items.length) finish("completed");
|
|
78
|
+
else setIndex(next);
|
|
79
|
+
});
|
|
80
|
+
if (items.length === 0) {
|
|
81
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsx(Text, { children: "(no pending visual diffs)" }) });
|
|
82
|
+
}
|
|
83
|
+
if (exited || index >= items.length) {
|
|
84
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsx(Text, { children: "Review complete." }) });
|
|
85
|
+
}
|
|
86
|
+
const current = items[index];
|
|
87
|
+
const total = items.length;
|
|
88
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
89
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
90
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "QAIOS visual review" }),
|
|
91
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` (${index + 1} / ${total})` })
|
|
92
|
+
] }),
|
|
93
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
94
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
95
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: current.baseline.name }),
|
|
96
|
+
/* @__PURE__ */ jsx(Text, { children: `@${current.baseline.viewport}` })
|
|
97
|
+
] }),
|
|
98
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
99
|
+
"route: ",
|
|
100
|
+
current.baseline.route
|
|
101
|
+
] }),
|
|
102
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
103
|
+
formatPct(current.diff.percentageChanged),
|
|
104
|
+
" pixels changed (",
|
|
105
|
+
current.diff.pixelsChanged,
|
|
106
|
+
" ",
|
|
107
|
+
"px)"
|
|
108
|
+
] }),
|
|
109
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
110
|
+
"baseline: ",
|
|
111
|
+
current.baseline.imagePath
|
|
112
|
+
] }),
|
|
113
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
114
|
+
"current: ",
|
|
115
|
+
current.diff.currentImagePath
|
|
116
|
+
] }),
|
|
117
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
118
|
+
"diff: ",
|
|
119
|
+
current.diff.diffImagePath
|
|
120
|
+
] })
|
|
121
|
+
] }),
|
|
122
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx(ClassificationView, { classification: current.diff.classification ?? null }) }),
|
|
123
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
124
|
+
"[",
|
|
125
|
+
/* @__PURE__ */ jsx(Text, { color: "green", children: "a" }),
|
|
126
|
+
"]pprove \u2192 update baseline \xB7 [",
|
|
127
|
+
/* @__PURE__ */ jsx(Text, { color: "red", children: "r" }),
|
|
128
|
+
"]eject \u2192 keep baseline \xB7 [",
|
|
129
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", children: "s" }),
|
|
130
|
+
"]kip \xB7 [",
|
|
131
|
+
/* @__PURE__ */ jsx(Text, { children: "q" }),
|
|
132
|
+
"]uit"
|
|
133
|
+
] }) })
|
|
134
|
+
] });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { SnapshotReview, dispatchKeypress };
|
|
@@ -78,6 +78,12 @@ try {
|
|
|
78
78
|
const context = await browser.newContext();
|
|
79
79
|
const page = await context.newPage();
|
|
80
80
|
await page.goto(url, { timeout: timeoutMs, waitUntil });
|
|
81
|
+
// Single-page apps render after DOMContentLoaded (live test: an Angular shop
|
|
82
|
+
// scanned as an empty shell, 0 violations vs 27). Let the network settle,
|
|
83
|
+
// best effort and bounded.
|
|
84
|
+
if (waitUntil !== 'networkidle') {
|
|
85
|
+
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
|
86
|
+
}
|
|
81
87
|
|
|
82
88
|
const builder = new AxeBuilder({ page });
|
|
83
89
|
if (tags.length > 0) builder.withTags(tags);
|
|
@@ -99,7 +105,8 @@ try {
|
|
|
99
105
|
...tag(results.incomplete, 'incomplete'),
|
|
100
106
|
];
|
|
101
107
|
|
|
102
|
-
|
|
108
|
+
// finalUrl: where navigation ended (a login redirect is reported, not hidden).
|
|
109
|
+
writeFileSync(out, JSON.stringify({ url, finalUrl: page.url(), findings }), 'utf-8');
|
|
103
110
|
} catch (err) {
|
|
104
111
|
process.stderr.write(`axe-runner: ${err?.message ?? err}\n`);
|
|
105
112
|
process.exit(4);
|