@qatonic_innovations/qaios 0.3.2 → 0.4.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/dist/index.js +2539 -201
- package/dist/migrations/0003_xray_capture.sql +75 -0
- package/dist/xray/scripts/xray-step-reporter.mjs +62 -0
- package/package.json +2 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
-- 0003 — xray network capture (v0.4).
|
|
2
|
+
-- Source of truth: docs/internal/qaios-v0.4-xray-spec.md §7.
|
|
3
|
+
-- Forward-only; no down-migrations in v0.x.
|
|
4
|
+
--
|
|
5
|
+
-- These tables hold the per-test network capture that `qaios run --xray` /
|
|
6
|
+
-- `qaios explore` produce: one net_runs row per (test attempt, browser), one
|
|
7
|
+
-- net_requests row per captured request (attributed to a step by the
|
|
8
|
+
-- CausalityEngine and normalized into a stable identity key), content-addressed
|
|
9
|
+
-- bodies in net_bodies (gzip, deduped by hash), and learned per-field volatility
|
|
10
|
+
-- in net_volatility. WAL is already enabled (0001_init.sql) so the parallel
|
|
11
|
+
-- Playwright workers can each append safely.
|
|
12
|
+
|
|
13
|
+
-- ── net_runs (one per test attempt × browser) ─────────────────────────
|
|
14
|
+
CREATE TABLE net_runs (
|
|
15
|
+
run_id TEXT PRIMARY KEY, -- ULID
|
|
16
|
+
workflow_id TEXT NOT NULL,
|
|
17
|
+
test_id TEXT NOT NULL, -- stable per (file, title)
|
|
18
|
+
browser TEXT NOT NULL, -- chromium|firefox|webkit
|
|
19
|
+
tier TEXT NOT NULL CHECK (tier IN ('A','B')),
|
|
20
|
+
capture_level TEXT NOT NULL, -- headers|summary|full
|
|
21
|
+
started_at TEXT NOT NULL,
|
|
22
|
+
is_green INTEGER NOT NULL DEFAULT 0, -- 0/1 — test passed
|
|
23
|
+
is_baseline INTEGER NOT NULL DEFAULT 0, -- 0/1 — pinned baseline
|
|
24
|
+
FOREIGN KEY (workflow_id) REFERENCES workflows(id)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE INDEX idx_netruns_workflow ON net_runs(workflow_id);
|
|
28
|
+
CREATE INDEX idx_netruns_test ON net_runs(test_id, browser);
|
|
29
|
+
|
|
30
|
+
-- ── net_requests (one per captured request) ───────────────────────────
|
|
31
|
+
CREATE TABLE net_requests (
|
|
32
|
+
id TEXT PRIMARY KEY, -- ULID
|
|
33
|
+
run_id TEXT NOT NULL REFERENCES net_runs(run_id),
|
|
34
|
+
step_id TEXT, -- NULL => setup/background
|
|
35
|
+
attribution TEXT NOT NULL, -- initiator|temporal|temporal_ambiguous|background
|
|
36
|
+
target_type TEXT NOT NULL, -- page|iframe|service_worker
|
|
37
|
+
method TEXT NOT NULL,
|
|
38
|
+
url_raw TEXT NOT NULL,
|
|
39
|
+
url_template TEXT NOT NULL,
|
|
40
|
+
gql_operation TEXT,
|
|
41
|
+
identity_key TEXT NOT NULL, -- method|host|url_template|gql_operation
|
|
42
|
+
status INTEGER,
|
|
43
|
+
error_text TEXT, -- net::ERR_*, CORS, etc.
|
|
44
|
+
mime TEXT,
|
|
45
|
+
req_hash TEXT,
|
|
46
|
+
resp_hash TEXT,
|
|
47
|
+
shape_hash TEXT,
|
|
48
|
+
req_body_ref TEXT, -- net_bodies.hash, nullable
|
|
49
|
+
resp_body_ref TEXT, -- net_bodies.hash, nullable
|
|
50
|
+
body_status TEXT, -- stored|evicted|truncated|streaming|skipped
|
|
51
|
+
redirects_json TEXT,
|
|
52
|
+
timings_json TEXT, -- dns/connect/ttfb/total (ms)
|
|
53
|
+
initiator_json TEXT, -- trimmed stack (top 5 frames)
|
|
54
|
+
wall_start TEXT NOT NULL
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE INDEX idx_netreq_identity ON net_requests(identity_key, run_id);
|
|
58
|
+
CREATE INDEX idx_netreq_run_step ON net_requests(run_id, step_id);
|
|
59
|
+
|
|
60
|
+
-- ── net_bodies (content-addressed, deduped, gzip-compressed) ──────────
|
|
61
|
+
CREATE TABLE net_bodies (
|
|
62
|
+
hash TEXT PRIMARY KEY, -- sha256 of canonical form
|
|
63
|
+
compressed BLOB NOT NULL, -- node:zlib gzip
|
|
64
|
+
size_raw INTEGER NOT NULL
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
-- ── net_volatility (learned per-field volatility) ─────────────────────
|
|
68
|
+
CREATE TABLE net_volatility (
|
|
69
|
+
test_id TEXT NOT NULL,
|
|
70
|
+
identity_key TEXT NOT NULL,
|
|
71
|
+
json_path TEXT NOT NULL, -- '' => presence-level volatility
|
|
72
|
+
kind TEXT NOT NULL CHECK (kind IN ('value','presence')),
|
|
73
|
+
learned_at TEXT NOT NULL,
|
|
74
|
+
PRIMARY KEY (test_id, identity_key, json_path)
|
|
75
|
+
);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// QAIOS xray STEP reporter — runs inside the Playwright subprocess.
|
|
2
|
+
//
|
|
3
|
+
// Playwright's built-in `json` reporter does NOT serialize per-step timings, so
|
|
4
|
+
// QAIOS can't recover the `pw:api` step windows it needs to attribute network
|
|
5
|
+
// requests to the test action that caused them. This tiny reporter fills that
|
|
6
|
+
// gap: it records every step's (testId, title, category, start, end) wall-clock
|
|
7
|
+
// window to an NDJSON sidecar. It captures NO network — that's recordHar's job —
|
|
8
|
+
// so it's a pure, side-effect-free observer that coexists with `--reporter=json`.
|
|
9
|
+
//
|
|
10
|
+
// The sidecar path comes from env (QAIOS_XRAY_STEPS_PATH). One NDJSON line per
|
|
11
|
+
// step: {testId, title, category, startWall, endWall}. The runtime's
|
|
12
|
+
// step-windows parser reads this instead of the (step-less) JSON report.
|
|
13
|
+
|
|
14
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
|
+
class XrayStepReporter {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.out = process.env.QAIOS_XRAY_STEPS_PATH ?? '';
|
|
20
|
+
if (this.out) {
|
|
21
|
+
try {
|
|
22
|
+
mkdirSync(path.dirname(this.out), { recursive: true });
|
|
23
|
+
} catch {
|
|
24
|
+
/* best-effort */
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Stable test id matching the report parser's convention exactly:
|
|
30
|
+
// `<basename>::<title>`. The report parser uses path.basename for the file
|
|
31
|
+
// part, so we must too, or the sidecar-window merge won't match by testId.
|
|
32
|
+
_testId(test) {
|
|
33
|
+
const file = test.location?.file ?? '';
|
|
34
|
+
const base = file ? path.basename(file) : '';
|
|
35
|
+
const title = test.titlePath ? test.titlePath().slice(3).join(' › ') : test.title;
|
|
36
|
+
return `${base}::${title}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onStepEnd(test, _result, step) {
|
|
40
|
+
if (!this.out) return;
|
|
41
|
+
// Only `pw:api` steps (click/fill/goto/…) define a network-causing window.
|
|
42
|
+
if (step.category !== 'pw:api') return;
|
|
43
|
+
const start =
|
|
44
|
+
step.startTime instanceof Date ? step.startTime.getTime() : Date.parse(step.startTime);
|
|
45
|
+
if (!Number.isFinite(start)) return;
|
|
46
|
+
const dur = typeof step.duration === 'number' && step.duration >= 0 ? step.duration : 0;
|
|
47
|
+
const line = {
|
|
48
|
+
testId: this._testId(test),
|
|
49
|
+
title: step.title ?? '(step)',
|
|
50
|
+
category: step.category,
|
|
51
|
+
startWall: start,
|
|
52
|
+
endWall: start + dur,
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
appendFileSync(this.out, JSON.stringify(line) + '\n', 'utf-8');
|
|
56
|
+
} catch {
|
|
57
|
+
/* never let a reporting write failure affect the run */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default XrayStepReporter;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qatonic_innovations/qaios",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI QA engineer in your terminal — designs, writes, runs, heals, and explores tests for web UI and APIs with audit-grade traceability.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"commander": "^12.1.0",
|
|
50
50
|
"openai": "^4.77.0",
|
|
51
51
|
"ink": "^5.2.1",
|
|
52
|
+
"es-toolkit": "^1.47.1",
|
|
52
53
|
"pino": "^9.5.0",
|
|
53
54
|
"pino-pretty": "^11.3.0",
|
|
54
55
|
"pixelmatch": "^7.2.0",
|