qa-engineer 0.9.2 → 0.11.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/COMPATIBILITY.md +26 -12
- package/README.md +182 -84
- package/package.json +11 -20
- package/packages/engine/bin/qa-engine.mjs +560 -0
- package/packages/engine/lib/analysis/branding.mjs +187 -0
- package/packages/engine/lib/analysis/context.mjs +294 -0
- package/packages/engine/lib/analysis/contracts.mjs +149 -0
- package/packages/engine/lib/analysis/diff-guard.mjs +425 -0
- package/packages/engine/lib/analysis/discovery.mjs +220 -0
- package/packages/engine/lib/analysis/evidence.mjs +116 -0
- package/packages/engine/lib/analysis/har.mjs +124 -0
- package/packages/engine/lib/analysis/junit.mjs +126 -0
- package/packages/engine/lib/analysis/network.mjs +237 -0
- package/packages/engine/lib/analysis/redaction.mjs +127 -0
- package/packages/engine/lib/analysis/report-html.mjs +76 -0
- package/packages/engine/lib/analysis/taxonomy.mjs +109 -0
- package/packages/engine/lib/analysis/xml.mjs +153 -0
- package/packages/engine/lib/analysis/zip.mjs +107 -0
- package/packages/engine/lib/artifacts/manager.mjs +453 -0
- package/packages/engine/lib/artifacts/mime.mjs +109 -0
- package/packages/engine/lib/artifacts/zip-write.mjs +143 -0
- package/packages/engine/lib/diagnostics/engine.mjs +165 -0
- package/packages/engine/lib/diagnostics/internal-contracts.mjs +50 -0
- package/packages/engine/lib/diagnostics/prioritization.mjs +99 -0
- package/packages/engine/lib/diagnostics/repair.mjs +73 -0
- package/packages/engine/lib/diagnostics/root-cause.mjs +92 -0
- package/packages/engine/lib/diagnostics/timeline.mjs +101 -0
- package/packages/engine/lib/frameworks/junit-frameworks.mjs +48 -0
- package/packages/engine/lib/frameworks/playwright.mjs +158 -0
- package/packages/engine/lib/report/components/charts.mjs +424 -0
- package/packages/engine/lib/report/components/evidence.mjs +207 -0
- package/packages/engine/lib/report/components/findings.mjs +258 -0
- package/packages/engine/lib/report/components/nav.mjs +99 -0
- package/packages/engine/lib/report/components/primitives.mjs +246 -0
- package/packages/engine/lib/report/components/runtime.mjs +246 -0
- package/packages/engine/lib/report/components/timeline.mjs +65 -0
- package/packages/engine/lib/report/core/model.mjs +270 -0
- package/packages/engine/lib/report/core/normalize.mjs +226 -0
- package/packages/engine/lib/report/core/sections.mjs +978 -0
- package/packages/engine/lib/report/export/bundle.mjs +293 -0
- package/packages/engine/lib/report/export/html.mjs +183 -0
- package/packages/engine/lib/report/export/machine.mjs +290 -0
- package/packages/engine/lib/report/export/markdown.mjs +323 -0
- package/packages/engine/lib/report/schemas/qa-report.schema.json +555 -0
- package/packages/engine/lib/report/theme/css.mjs +529 -0
- package/packages/engine/lib/report/theme/tokens.mjs +137 -0
- package/packages/engine/lib/report/version.mjs +78 -0
- package/packages/engine/package.json +14 -0
- package/packages/installer/lib/agents/targets.mjs +90 -0
- package/packages/installer/lib/agents/user-level.mjs +80 -0
- package/packages/installer/lib/cli/flags.mjs +20 -2
- package/packages/installer/lib/commands/doctor.mjs +13 -20
- package/packages/installer/lib/commands/install.mjs +160 -91
- package/packages/installer/lib/commands/repair.mjs +10 -6
- package/packages/installer/lib/commands/self-test.mjs +4 -3
- package/packages/installer/lib/commands/uninstall.mjs +14 -8
- package/packages/installer/lib/commands/update.mjs +9 -4
- package/packages/installer/lib/commands/verify.mjs +13 -12
- package/packages/installer/lib/constants.mjs +13 -0
- package/packages/installer/lib/core/bundle.mjs +69 -92
- package/packages/installer/lib/core/conflict.mjs +5 -4
- package/packages/installer/lib/core/fs-safe.mjs +146 -6
- package/packages/installer/lib/core/integrity.mjs +59 -0
- package/packages/installer/lib/core/lockfile.mjs +19 -3
- package/packages/installer/lib/core/manifest.mjs +48 -57
- package/packages/installer/lib/core/plan.mjs +213 -0
- package/packages/installer/lib/core/qa-home.mjs +145 -0
- package/packages/installer/lib/core/scope.mjs +274 -0
- package/packages/installer/lib/core/validate-install.mjs +49 -31
- package/packages/installer/package.json +1 -1
- package/packages/installer/schemas/qa-lock.schema.json +119 -21
- package/shared/tooling/qa-tool.mjs +161 -0
- package/skills/qa-api/SKILL.md +5 -5
- package/skills/qa-api/references/deterministic-tooling.md +34 -32
- package/skills/qa-api/references/evidence-and-reporting.md +5 -5
- package/skills/qa-api/scripts/qa-tool.mjs +162 -0
- package/skills/qa-audit/SKILL.md +5 -5
- package/skills/qa-audit/references/deterministic-tooling.md +34 -32
- package/skills/qa-audit/references/evidence-and-reporting.md +5 -5
- package/skills/qa-audit/scripts/qa-tool.mjs +162 -0
- package/skills/qa-debug/SKILL.md +6 -6
- package/skills/qa-debug/references/deterministic-tooling.md +34 -32
- package/skills/qa-debug/references/diagnostic-engine.md +3 -3
- package/skills/qa-debug/references/evidence-and-reporting.md +5 -5
- package/skills/qa-debug/scripts/qa-tool.mjs +162 -0
- package/skills/qa-explore/SKILL.md +31 -15
- package/skills/qa-explore/contracts/explore-result.schema.json +517 -11
- package/skills/qa-explore/references/api-replay.md +40 -1
- package/skills/qa-explore/references/deterministic-tooling.md +34 -32
- package/skills/qa-explore/references/evidence-and-reporting.md +5 -5
- package/skills/qa-explore/references/report-pipeline.md +266 -96
- package/skills/qa-explore/scripts/qa-tool.mjs +162 -0
- package/skills/qa-fix/SKILL.md +4 -4
- package/skills/qa-fix/references/deterministic-tooling.md +34 -32
- package/skills/qa-fix/references/diagnostic-engine.md +3 -3
- package/skills/qa-fix/references/evidence-and-reporting.md +5 -5
- package/skills/qa-fix/scripts/qa-tool.mjs +162 -0
- package/skills/qa-flaky/SKILL.md +4 -4
- package/skills/qa-flaky/references/deterministic-tooling.md +34 -32
- package/skills/qa-flaky/references/evidence-and-reporting.md +5 -5
- package/skills/qa-flaky/scripts/qa-tool.mjs +162 -0
- package/skills/qa-generate/references/evidence-and-reporting.md +5 -5
- package/skills/qa-init/SKILL.md +4 -4
- package/skills/qa-init/references/deterministic-tooling.md +34 -32
- package/skills/qa-init/references/evidence-and-reporting.md +5 -5
- package/skills/qa-init/scripts/qa-tool.mjs +162 -0
- package/skills/qa-report/SKILL.md +7 -7
- package/skills/qa-report/references/deterministic-tooling.md +34 -32
- package/skills/qa-report/references/diagnostic-engine.md +3 -3
- package/skills/qa-report/references/evidence-and-reporting.md +5 -5
- package/skills/qa-report/scripts/qa-tool.mjs +162 -0
- package/skills/qa-review/references/evidence-and-reporting.md +5 -5
- package/skills/qa-run/SKILL.md +6 -6
- package/skills/qa-run/references/deterministic-tooling.md +34 -32
- package/skills/qa-run/references/evidence-and-reporting.md +5 -5
- package/skills/qa-run/scripts/qa-tool.mjs +162 -0
- package/shared/analysis/lib/qa_analysis/__init__.py +0 -11
- package/shared/analysis/lib/qa_analysis/branding.py +0 -175
- package/shared/analysis/lib/qa_analysis/cli.py +0 -144
- package/shared/analysis/lib/qa_analysis/context.py +0 -233
- package/shared/analysis/lib/qa_analysis/contracts.py +0 -158
- package/shared/analysis/lib/qa_analysis/diff_guard.py +0 -327
- package/shared/analysis/lib/qa_analysis/discovery.py +0 -113
- package/shared/analysis/lib/qa_analysis/evidence.py +0 -128
- package/shared/analysis/lib/qa_analysis/har.py +0 -87
- package/shared/analysis/lib/qa_analysis/junit.py +0 -104
- package/shared/analysis/lib/qa_analysis/redaction.py +0 -107
- package/shared/analysis/lib/qa_analysis/report_html.py +0 -781
- package/shared/analysis/lib/qa_analysis/taxonomy.py +0 -98
- package/shared/diagnostics/lib/qa_diagnostics/__init__.py +0 -13
- package/shared/diagnostics/lib/qa_diagnostics/cli.py +0 -124
- package/shared/diagnostics/lib/qa_diagnostics/engine.py +0 -143
- package/shared/diagnostics/lib/qa_diagnostics/internal_contracts.py +0 -75
- package/shared/diagnostics/lib/qa_diagnostics/prioritization.py +0 -101
- package/shared/diagnostics/lib/qa_diagnostics/repair.py +0 -72
- package/shared/diagnostics/lib/qa_diagnostics/root_cause.py +0 -89
- package/shared/diagnostics/lib/qa_diagnostics/timeline.py +0 -71
- package/shared/frameworks/cypress/lib/cypress_analysis.py +0 -27
- package/shared/frameworks/playwright/lib/playwright_analysis.py +0 -167
- package/shared/frameworks/selenium/lib/selenium_analysis.py +0 -28
- package/shared/frameworks/webdriverio/lib/webdriverio_analysis.py +0 -24
- package/shared/tooling/qa_tool.py +0 -127
- /package/{shared/analysis/lib/qa_analysis → packages/engine/lib/analysis}/branding.json +0 -0
- /package/{shared → packages/engine/lib}/analysis/schemas/context.schema.json +0 -0
- /package/{shared → packages/engine/lib}/diagnostics/schemas/internal/analysis-result.schema.json +0 -0
- /package/{shared → packages/engine/lib}/diagnostics/schemas/internal/diagnosis.schema.json +0 -0
- /package/{shared → packages/engine/lib}/diagnostics/schemas/internal/execution-result-min.schema.json +0 -0
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
"""JUnit XML parser.
|
|
2
|
-
|
|
3
|
-
Framework-agnostic: Playwright, Selenium, Cypress, WebdriverIO, and most unit
|
|
4
|
-
runners emit JUnit XML, so this one parser normalizes them all into the pack's
|
|
5
|
-
per-test result shape. This is the concrete proof that different frameworks
|
|
6
|
-
share one contract — only where the file lives differs, not how it is read.
|
|
7
|
-
|
|
8
|
-
Parses deterministically; a malformed document raises rather than guessing.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import xml.etree.ElementTree as ET
|
|
12
|
-
|
|
13
|
-
from .redaction import redact_text
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class MalformedArtifact(ValueError):
|
|
17
|
-
"""Raised when an artifact cannot be parsed. Never swallowed into a guess."""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _int(value, default=0):
|
|
21
|
-
try:
|
|
22
|
-
return int(value)
|
|
23
|
-
except (TypeError, ValueError):
|
|
24
|
-
return default
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _duration_ms(raw, path):
|
|
28
|
-
"""`time` in seconds as whole milliseconds.
|
|
29
|
-
|
|
30
|
-
An absent or empty attribute is zero — plenty of runners omit it. A value
|
|
31
|
-
that is present but not a finite number means the document is not what it
|
|
32
|
-
claims to be, so it raises: found by the Node port's parity corpus, where
|
|
33
|
-
`time="not-a-number"` escaped as a bare ValueError and `time="nan"` as
|
|
34
|
-
"cannot convert float NaN to integer". Both reached the caller as a traceback
|
|
35
|
-
and exit 1, while the CLI's documented failure mode is exit 2 with
|
|
36
|
-
{error, detail}.
|
|
37
|
-
"""
|
|
38
|
-
if raw is None or raw == "":
|
|
39
|
-
return 0
|
|
40
|
-
try:
|
|
41
|
-
seconds = float(raw)
|
|
42
|
-
except (TypeError, ValueError):
|
|
43
|
-
raise MalformedArtifact(
|
|
44
|
-
f"testcase time={raw!r} is not a number at {path}"
|
|
45
|
-
) from None
|
|
46
|
-
if seconds != seconds or seconds in (float("inf"), float("-inf")):
|
|
47
|
-
raise MalformedArtifact(f"testcase time={raw!r} is not a finite number at {path}")
|
|
48
|
-
return int(round(seconds * 1000))
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def parse_junit(path):
|
|
52
|
-
"""Parse a JUnit XML file into a normalized result.
|
|
53
|
-
|
|
54
|
-
Returns a dict: {tests: {total, passed, failed, skipped}, executed: [...]}.
|
|
55
|
-
Each executed entry: {title, file, status, durationMs, message?}.
|
|
56
|
-
Raises MalformedArtifact if the XML is unreadable or not JUnit-shaped.
|
|
57
|
-
"""
|
|
58
|
-
try:
|
|
59
|
-
tree = ET.parse(path)
|
|
60
|
-
except (ET.ParseError, OSError) as exc:
|
|
61
|
-
raise MalformedArtifact(f"could not parse JUnit XML at {path}: {exc}") from exc
|
|
62
|
-
|
|
63
|
-
root = tree.getroot()
|
|
64
|
-
# Accept either a <testsuites> root or a single <testsuite> root.
|
|
65
|
-
if root.tag == "testsuites":
|
|
66
|
-
suites = root.findall("testsuite")
|
|
67
|
-
elif root.tag == "testsuite":
|
|
68
|
-
suites = [root]
|
|
69
|
-
else:
|
|
70
|
-
raise MalformedArtifact(f"not a JUnit document (root <{root.tag}>) at {path}")
|
|
71
|
-
|
|
72
|
-
executed = []
|
|
73
|
-
for suite in suites:
|
|
74
|
-
for case in suite.findall("testcase"):
|
|
75
|
-
failure = case.find("failure")
|
|
76
|
-
error = case.find("error")
|
|
77
|
-
skipped = case.find("skipped")
|
|
78
|
-
if failure is not None or error is not None:
|
|
79
|
-
status = "failed"
|
|
80
|
-
node = failure if failure is not None else error
|
|
81
|
-
message = redact_text((node.get("message") or node.text or "").strip())
|
|
82
|
-
elif skipped is not None:
|
|
83
|
-
status = "skipped"
|
|
84
|
-
message = redact_text((skipped.get("message") or "").strip())
|
|
85
|
-
else:
|
|
86
|
-
status = "passed"
|
|
87
|
-
message = ""
|
|
88
|
-
entry = {
|
|
89
|
-
"title": case.get("name", ""),
|
|
90
|
-
"file": case.get("classname", ""),
|
|
91
|
-
"status": status,
|
|
92
|
-
"durationMs": _duration_ms(case.get("time"), path),
|
|
93
|
-
}
|
|
94
|
-
if message:
|
|
95
|
-
entry["message"] = message
|
|
96
|
-
executed.append(entry)
|
|
97
|
-
|
|
98
|
-
counts = {
|
|
99
|
-
"total": len(executed),
|
|
100
|
-
"passed": sum(1 for e in executed if e["status"] == "passed"),
|
|
101
|
-
"failed": sum(1 for e in executed if e["status"] == "failed"),
|
|
102
|
-
"skipped": sum(1 for e in executed if e["status"] == "skipped"),
|
|
103
|
-
}
|
|
104
|
-
return {"tests": counts, "executed": executed}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
"""Credential, secret, token, and PII redaction.
|
|
2
|
-
|
|
3
|
-
Every analyzer runs artifact text through redaction before it appears in a
|
|
4
|
-
finding, a report, or stdout. Redaction happens as evidence is captured, not
|
|
5
|
-
after — a secret must never reach a model's context or a log. Standard library
|
|
6
|
-
only; deterministic.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import re
|
|
10
|
-
|
|
11
|
-
# Ordered (name, pattern, replacement). Order matters: match structured,
|
|
12
|
-
# high-signal secrets (JWTs, provider keys) before generic fallbacks.
|
|
13
|
-
_RULES = [
|
|
14
|
-
("jwt", re.compile(r"eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}")),
|
|
15
|
-
("aws-access-key", re.compile(r"AKIA[0-9A-Z]{16}")),
|
|
16
|
-
("github-token", re.compile(r"gh[pousr]_[A-Za-z0-9]{20,}")),
|
|
17
|
-
("slack-token", re.compile(r"xox[baprs]-[A-Za-z0-9-]{10,}")),
|
|
18
|
-
("openai-key", re.compile(r"sk-[A-Za-z0-9]{20,}")),
|
|
19
|
-
("bearer", re.compile(r"(?i)\bBearer\s+[A-Za-z0-9._~+/-]+=*")),
|
|
20
|
-
# Sensitive header lines: keep the header name, mask the value.
|
|
21
|
-
#
|
|
22
|
-
# The value is `[^\r\n]+` rather than `.+$`, and the indent is `[ \t]*` rather
|
|
23
|
-
# than `\s*`, for one reason: on CRLF text, `.+` swallows the carriage return
|
|
24
|
-
# and `$` matches before the newline, so redacting a header silently rewrote
|
|
25
|
-
# the line ending to LF and corrupted the rest of the document's endings.
|
|
26
|
-
# Horizontal whitespace is also what a header indent actually is. Found by the
|
|
27
|
-
# Node port's parity corpus, where the two languages disagreed here and
|
|
28
|
-
# JavaScript was right.
|
|
29
|
-
("auth-header", re.compile(r"(?im)^([ \t]*(?:authorization|proxy-authorization)[ \t]*[:=][ \t]*)[^\r\n]+")),
|
|
30
|
-
("cookie-header", re.compile(r"(?im)^([ \t]*(?:set-cookie|cookie)[ \t]*[:=][ \t]*)[^\r\n]+")),
|
|
31
|
-
# Secret-like assignments: key=value / "key": "value".
|
|
32
|
-
("assigned-secret", re.compile(
|
|
33
|
-
r'(?i)(\b(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|client[_-]?secret)\b\s*[:=]\s*["\']?)'
|
|
34
|
-
r'([^\s"\'&]{4,})')),
|
|
35
|
-
# Credentials in URLs and query strings.
|
|
36
|
-
("url-credential", re.compile(r"(?i)(://[^:/@\s]+:)([^@/\s]+)(@)")),
|
|
37
|
-
("query-secret", re.compile(
|
|
38
|
-
r"(?i)([?&](?:password|passwd|pwd|token|secret|api[_-]?key|access[_-]?key)=)([^&\s#]+)")),
|
|
39
|
-
# PII: email addresses.
|
|
40
|
-
("email", re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b")),
|
|
41
|
-
]
|
|
42
|
-
|
|
43
|
-
# Rules whose replacement preserves a leading captured group (the label/prefix).
|
|
44
|
-
_PREFIX_PRESERVING = {
|
|
45
|
-
"auth-header": r"\1[REDACTED:auth-header]",
|
|
46
|
-
"cookie-header": r"\1[REDACTED:cookie-header]",
|
|
47
|
-
"assigned-secret": r"\1[REDACTED:secret]",
|
|
48
|
-
"url-credential": r"\1[REDACTED:credential]\3",
|
|
49
|
-
"query-secret": r"\1[REDACTED:secret]",
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def redact_text(text):
|
|
54
|
-
"""Return text with every recognized secret or PII value masked."""
|
|
55
|
-
if not text:
|
|
56
|
-
return text
|
|
57
|
-
result = text
|
|
58
|
-
for name, pattern in _RULES:
|
|
59
|
-
if name in _PREFIX_PRESERVING:
|
|
60
|
-
result = pattern.sub(_PREFIX_PRESERVING[name], result)
|
|
61
|
-
else:
|
|
62
|
-
result = pattern.sub(f"[REDACTED:{name}]", result)
|
|
63
|
-
return result
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def detect_secrets(text):
|
|
67
|
-
"""Return a list of {type, start, end} for secrets found — never the values.
|
|
68
|
-
|
|
69
|
-
Used to decide whether an artifact is safe to expose, without surfacing the
|
|
70
|
-
secret itself.
|
|
71
|
-
"""
|
|
72
|
-
findings = []
|
|
73
|
-
if not text:
|
|
74
|
-
return findings
|
|
75
|
-
for name, pattern in _RULES:
|
|
76
|
-
for match in pattern.finditer(text):
|
|
77
|
-
findings.append({"type": name, "start": match.start(), "end": match.end()})
|
|
78
|
-
findings.sort(key=lambda f: f["start"])
|
|
79
|
-
return findings
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
_SENSITIVE_HEADERS = {
|
|
83
|
-
"authorization", "proxy-authorization", "cookie", "set-cookie",
|
|
84
|
-
"x-api-key", "x-auth-token", "api-key", "x-csrf-token",
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def redact_headers(headers):
|
|
89
|
-
"""Mask the values of sensitive HTTP headers.
|
|
90
|
-
|
|
91
|
-
Accepts a dict, or a list of {"name","value"} entries (HAR shape), and
|
|
92
|
-
returns the same shape with sensitive values replaced.
|
|
93
|
-
"""
|
|
94
|
-
def mask(name, value):
|
|
95
|
-
return "[REDACTED:header]" if name.strip().lower() in _SENSITIVE_HEADERS else redact_text(value)
|
|
96
|
-
|
|
97
|
-
if isinstance(headers, dict):
|
|
98
|
-
return {k: mask(k, str(v)) for k, v in headers.items()}
|
|
99
|
-
if isinstance(headers, list):
|
|
100
|
-
out = []
|
|
101
|
-
for entry in headers:
|
|
102
|
-
if isinstance(entry, dict) and "name" in entry:
|
|
103
|
-
out.append({**entry, "value": mask(str(entry["name"]), str(entry.get("value", "")))})
|
|
104
|
-
else:
|
|
105
|
-
out.append(entry)
|
|
106
|
-
return out
|
|
107
|
-
return headers
|