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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Writing a ZIP, with no dependencies.
|
|
2
|
+
//
|
|
3
|
+
// `lib/analysis/zip.mjs` reads them (Playwright traces); this writes them, so a report
|
|
4
|
+
// folder can leave as one file a stakeholder can actually receive. Every mail system,
|
|
5
|
+
// ticket tracker, and chat tool accepts a `.zip`; none of them accept a directory.
|
|
6
|
+
//
|
|
7
|
+
// ## Scope
|
|
8
|
+
//
|
|
9
|
+
// The minimum format that every extractor understands: local headers, deflate or
|
|
10
|
+
// store, a central directory, and an end-of-central-directory record. No ZIP64, no
|
|
11
|
+
// encryption, no multi-disk — a QA report is megabytes, not gigabytes, and the
|
|
12
|
+
// simplest file that opens everywhere is the right one.
|
|
13
|
+
//
|
|
14
|
+
// ## Reproducibility
|
|
15
|
+
//
|
|
16
|
+
// Timestamps come from the caller, defaulting to the report's own `generatedAt`.
|
|
17
|
+
// Re-zipping the same report therefore produces the same bytes, so a bundle can be
|
|
18
|
+
// checksummed and a diff between two runs is a diff of the content.
|
|
19
|
+
|
|
20
|
+
import zlib from 'node:zlib';
|
|
21
|
+
|
|
22
|
+
const LOCAL_SIGNATURE = 0x04034b50;
|
|
23
|
+
const CENTRAL_SIGNATURE = 0x02014b50;
|
|
24
|
+
const EOCD_SIGNATURE = 0x06054b50;
|
|
25
|
+
|
|
26
|
+
// Deflate is only worth its CPU on compressible bytes. A PNG or an MP4 is already
|
|
27
|
+
// compressed, and re-deflating it costs time to make the file marginally larger.
|
|
28
|
+
const ALREADY_COMPRESSED = new Set([
|
|
29
|
+
'.png', '.jpg', '.jpeg', '.webp', '.gif', '.avif', '.mp4', '.webm', '.ogv', '.zip', '.gz',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const CRC_TABLE = (() => {
|
|
33
|
+
const table = new Uint32Array(256);
|
|
34
|
+
for (let n = 0; n < 256; n += 1) {
|
|
35
|
+
let c = n;
|
|
36
|
+
for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
37
|
+
table[n] = c >>> 0;
|
|
38
|
+
}
|
|
39
|
+
return table;
|
|
40
|
+
})();
|
|
41
|
+
|
|
42
|
+
export function crc32(buffer) {
|
|
43
|
+
let crc = 0xffffffff;
|
|
44
|
+
for (const byte of buffer) crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
45
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** MS-DOS date and time, the only clock a ZIP header understands. */
|
|
49
|
+
function dosStamp(date) {
|
|
50
|
+
// The format cannot represent anything before 1980; clamp rather than wrap, so a
|
|
51
|
+
// missing or nonsense timestamp produces a valid archive.
|
|
52
|
+
const year = Math.max(1980, date.getFullYear());
|
|
53
|
+
const time =
|
|
54
|
+
(date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2);
|
|
55
|
+
const day = ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate();
|
|
56
|
+
return { time: time & 0xffff, date: day & 0xffff };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function shouldStore(name) {
|
|
60
|
+
const dot = name.lastIndexOf('.');
|
|
61
|
+
return dot > 0 && ALREADY_COMPRESSED.has(name.slice(dot).toLowerCase());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build a ZIP from `[{ name, data }]`.
|
|
66
|
+
*
|
|
67
|
+
* `name` is the path inside the archive and always uses forward slashes — a backslash
|
|
68
|
+
* here produces an archive that extracts into one long filename on Unix.
|
|
69
|
+
*/
|
|
70
|
+
export function createZip(entries, { modifiedAt = new Date(0) } = {}) {
|
|
71
|
+
const stamp = dosStamp(modifiedAt.getFullYear() >= 1980 ? modifiedAt : new Date('1980-01-01T00:00:00Z'));
|
|
72
|
+
const locals = [];
|
|
73
|
+
const centrals = [];
|
|
74
|
+
let offset = 0;
|
|
75
|
+
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const name = Buffer.from(String(entry.name).split('\\').join('/'), 'utf8');
|
|
78
|
+
const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data, 'utf8');
|
|
79
|
+
const store = shouldStore(entry.name);
|
|
80
|
+
const compressed = store ? data : zlib.deflateRawSync(data, { level: 9 });
|
|
81
|
+
// Deflate can inflate small or incompressible input; storing it is then both
|
|
82
|
+
// smaller and faster to read back.
|
|
83
|
+
const useStore = store || compressed.length >= data.length;
|
|
84
|
+
const body = useStore ? data : compressed;
|
|
85
|
+
const method = useStore ? 0 : 8;
|
|
86
|
+
const checksum = crc32(data);
|
|
87
|
+
|
|
88
|
+
const local = Buffer.alloc(30);
|
|
89
|
+
local.writeUInt32LE(LOCAL_SIGNATURE, 0);
|
|
90
|
+
local.writeUInt16LE(20, 4);
|
|
91
|
+
local.writeUInt16LE(0, 6);
|
|
92
|
+
local.writeUInt16LE(method, 8);
|
|
93
|
+
local.writeUInt16LE(stamp.time, 10);
|
|
94
|
+
local.writeUInt16LE(stamp.date, 12);
|
|
95
|
+
local.writeUInt32LE(checksum, 14);
|
|
96
|
+
local.writeUInt32LE(body.length, 18);
|
|
97
|
+
local.writeUInt32LE(data.length, 22);
|
|
98
|
+
local.writeUInt16LE(name.length, 26);
|
|
99
|
+
local.writeUInt16LE(0, 28);
|
|
100
|
+
locals.push(local, name, body);
|
|
101
|
+
|
|
102
|
+
const central = Buffer.alloc(46);
|
|
103
|
+
central.writeUInt32LE(CENTRAL_SIGNATURE, 0);
|
|
104
|
+
central.writeUInt16LE(20, 4);
|
|
105
|
+
central.writeUInt16LE(20, 6);
|
|
106
|
+
central.writeUInt16LE(0, 8);
|
|
107
|
+
central.writeUInt16LE(method, 10);
|
|
108
|
+
central.writeUInt16LE(stamp.time, 12);
|
|
109
|
+
central.writeUInt16LE(stamp.date, 14);
|
|
110
|
+
central.writeUInt32LE(checksum, 16);
|
|
111
|
+
central.writeUInt32LE(body.length, 20);
|
|
112
|
+
central.writeUInt32LE(data.length, 24);
|
|
113
|
+
central.writeUInt16LE(name.length, 28);
|
|
114
|
+
central.writeUInt16LE(0, 30);
|
|
115
|
+
central.writeUInt16LE(0, 32);
|
|
116
|
+
central.writeUInt16LE(0, 34);
|
|
117
|
+
central.writeUInt16LE(0, 36);
|
|
118
|
+
// 0o644 in the high 16 bits, so extraction on Unix produces readable files rather
|
|
119
|
+
// than mode 000.
|
|
120
|
+
central.writeUInt32LE((0o100644 << 16) >>> 0, 38);
|
|
121
|
+
central.writeUInt32LE(offset, 42);
|
|
122
|
+
centrals.push(central, name);
|
|
123
|
+
|
|
124
|
+
offset += local.length + name.length + body.length;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const centralBuffer = Buffer.concat(centrals);
|
|
128
|
+
// End of central directory. The field offsets are 4/6/8/10/12/16/20 — writing the
|
|
129
|
+
// directory size and its offset two bytes late produces a file that every extractor
|
|
130
|
+
// reads as truncated by roughly a gigabyte, because it takes the tail of one field
|
|
131
|
+
// and the head of the next as a length.
|
|
132
|
+
const eocd = Buffer.alloc(22);
|
|
133
|
+
eocd.writeUInt32LE(EOCD_SIGNATURE, 0);
|
|
134
|
+
eocd.writeUInt16LE(0, 4);
|
|
135
|
+
eocd.writeUInt16LE(0, 6);
|
|
136
|
+
eocd.writeUInt16LE(entries.length, 8);
|
|
137
|
+
eocd.writeUInt16LE(entries.length, 10);
|
|
138
|
+
eocd.writeUInt32LE(centralBuffer.length, 12);
|
|
139
|
+
eocd.writeUInt32LE(offset, 16);
|
|
140
|
+
eocd.writeUInt16LE(0, 20);
|
|
141
|
+
|
|
142
|
+
return Buffer.concat([...locals, centralBuffer, eocd]);
|
|
143
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// The diagnostic engine — the one place failure reasoning happens.
|
|
2
|
+
//
|
|
3
|
+
// Orchestrates the analysis platform and the diagnostics modules into a single
|
|
4
|
+
// diagnosis: per-failure root cause, prioritization, a reconstructed timeline, and
|
|
5
|
+
// ranked recommendations. qa-debug presents the diagnosis; qa-fix turns it into
|
|
6
|
+
// repair plans; qa-report aggregates diagnoses. None of them re-implements this.
|
|
7
|
+
|
|
8
|
+
import * as taxonomy from '../analysis/taxonomy.mjs';
|
|
9
|
+
|
|
10
|
+
import { analyze } from './root-cause.mjs';
|
|
11
|
+
import { prioritize } from './prioritization.mjs';
|
|
12
|
+
import { planRepair } from './repair.mjs';
|
|
13
|
+
import { buildTimeline } from './timeline.mjs';
|
|
14
|
+
import { validateDiagnosis } from './internal-contracts.mjs';
|
|
15
|
+
|
|
16
|
+
const PRIORITY_RANK = { P1: 3, P2: 2, P3: 1 };
|
|
17
|
+
|
|
18
|
+
// Classifications that block a release when they are the cause of a failure.
|
|
19
|
+
const RELEASE_BLOCKING = new Set([
|
|
20
|
+
taxonomy.APPLICATION_BUG, taxonomy.NETWORK, taxonomy.INFRASTRUCTURE,
|
|
21
|
+
taxonomy.AUTH, taxonomy.AUTHORIZATION,
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Produce a full diagnosis from the available results.
|
|
26
|
+
*
|
|
27
|
+
* `{entries, timeline, recommendations}`, where each entry combines a root cause
|
|
28
|
+
* with its prioritization and affected tests.
|
|
29
|
+
*/
|
|
30
|
+
export function diagnose(executionResult, { analysisResult = null } = {}) {
|
|
31
|
+
const signals = deriveSignals(executionResult, analysisResult);
|
|
32
|
+
const findings = (analysisResult ?? {}).findings ?? [];
|
|
33
|
+
|
|
34
|
+
const entries = signals.map((signal) => {
|
|
35
|
+
const rootCause = analyze(signal);
|
|
36
|
+
const blocking = RELEASE_BLOCKING.has(rootCause.classification);
|
|
37
|
+
return {
|
|
38
|
+
rootCause,
|
|
39
|
+
priority: prioritize(rootCause, { blocking }),
|
|
40
|
+
affectedTests: signal.affectedTests ?? [],
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Highest priority first, then highest confidence. Python's `sort(reverse=True)`
|
|
45
|
+
// on a tuple key is a stable descending sort, so equal keys keep insertion order
|
|
46
|
+
// — reproduced by comparing in reverse and falling back to the original index.
|
|
47
|
+
const decorated = entries.map((entry, index) => ({ entry, index }));
|
|
48
|
+
decorated.sort((a, b) => {
|
|
49
|
+
const rank = (PRIORITY_RANK[b.entry.priority.priority] ?? 0) - (PRIORITY_RANK[a.entry.priority.priority] ?? 0);
|
|
50
|
+
if (rank !== 0) return rank;
|
|
51
|
+
const confidence = b.entry.rootCause.confidence - a.entry.rootCause.confidence;
|
|
52
|
+
if (confidence !== 0) return confidence;
|
|
53
|
+
// Insertion order for ties. Python's `sort(reverse=True)` is stable, so equal
|
|
54
|
+
// keys keep the order they arrived in — reversing the tie-break too would put
|
|
55
|
+
// two equally-ranked locator failures back to front.
|
|
56
|
+
return a.index - b.index;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const diagnosis = {
|
|
60
|
+
entries: decorated.map(({ entry }) => entry),
|
|
61
|
+
timeline: buildTimeline(executionResult, findings),
|
|
62
|
+
recommendations: recommendations(decorated.map(({ entry }) => entry)),
|
|
63
|
+
};
|
|
64
|
+
// Mechanical seam enforcement: a diagnosis must match the internal contract.
|
|
65
|
+
return validateDiagnosis(diagnosis);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Turn a diagnosis into repair plans (for qa-fix).
|
|
70
|
+
*
|
|
71
|
+
* One plan per entry; non-repairable causes yield an escalation plan. Never
|
|
72
|
+
* produces code.
|
|
73
|
+
*/
|
|
74
|
+
export function planRepairs(diagnosis) {
|
|
75
|
+
return diagnosis.entries.map((entry) => ({
|
|
76
|
+
...planRepair(entry.rootCause, entry.affectedTests),
|
|
77
|
+
priority: entry.priority.priority,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Aggregate a diagnosis for qa-report: totals, breakdown by classification, the
|
|
83
|
+
* top-priority findings, and a deterministic release-readiness call.
|
|
84
|
+
*/
|
|
85
|
+
export function summarize(executionResult, diagnosis) {
|
|
86
|
+
const tests = (executionResult ?? {}).tests ?? {};
|
|
87
|
+
const byClassification = {};
|
|
88
|
+
for (const entry of diagnosis.entries) {
|
|
89
|
+
const cls = entry.rootCause.classification;
|
|
90
|
+
byClassification[cls] = (byClassification[cls] ?? 0) + 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
totals: tests,
|
|
95
|
+
byClassification,
|
|
96
|
+
topPriority: diagnosis.entries.filter((entry) => entry.priority.priority === 'P1'),
|
|
97
|
+
releaseReadiness: releaseReadiness(executionResult, diagnosis),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Derive failure signals, preferring the analysis platform's findings. */
|
|
102
|
+
function deriveSignals(executionResult, analysisResult) {
|
|
103
|
+
const findings = (analysisResult ?? {}).findings ?? [];
|
|
104
|
+
if (findings.length > 0) {
|
|
105
|
+
return findings.map((finding) => ({
|
|
106
|
+
message: finding.reason ?? '',
|
|
107
|
+
classification: finding.classification,
|
|
108
|
+
confidence: finding.confidence,
|
|
109
|
+
reason: finding.reason,
|
|
110
|
+
httpStatus: finding.httpStatus,
|
|
111
|
+
retries: finding.retries ?? 0,
|
|
112
|
+
finalStatus: finding.finalStatus,
|
|
113
|
+
evidence: finding.evidence ?? [],
|
|
114
|
+
affectedTests: finding.affectedTests ?? [],
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const signals = [];
|
|
119
|
+
for (const test of (executionResult ?? {}).executed ?? []) {
|
|
120
|
+
if (test.status === 'failed' || test.status === 'flaky') {
|
|
121
|
+
signals.push({
|
|
122
|
+
message: test.message ?? '',
|
|
123
|
+
retries: test.retries ?? 0,
|
|
124
|
+
finalStatus: test.status,
|
|
125
|
+
affectedTests: [test.title ?? ''],
|
|
126
|
+
evidence: [{
|
|
127
|
+
type: 'junit',
|
|
128
|
+
description: `${test.status}: ${test.title ?? ''}`,
|
|
129
|
+
source: test.file ?? 'execution-result',
|
|
130
|
+
}],
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return signals;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Ranked, de-duplicated recommendations — highest priority first. */
|
|
138
|
+
function recommendations(entries) {
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
const ranked = [];
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
const action = entry.rootCause.recommendation;
|
|
143
|
+
if (seen.has(action)) continue;
|
|
144
|
+
seen.add(action);
|
|
145
|
+
ranked.push({
|
|
146
|
+
action,
|
|
147
|
+
priority: entry.priority.priority,
|
|
148
|
+
owner: entry.rootCause.ownership,
|
|
149
|
+
classification: entry.rootCause.classification,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return ranked;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function releaseReadiness(executionResult, diagnosis) {
|
|
156
|
+
const failed = ((executionResult ?? {}).tests ?? {}).failed ?? 0;
|
|
157
|
+
if (failed === 0 && diagnosis.entries.length === 0) return 'ready';
|
|
158
|
+
const classes = new Set(diagnosis.entries.map((entry) => entry.rootCause.classification));
|
|
159
|
+
for (const blocking of RELEASE_BLOCKING) {
|
|
160
|
+
if (classes.has(blocking)) return 'not-ready';
|
|
161
|
+
}
|
|
162
|
+
// `classes <= {UNKNOWN}` in Python: every class is unknown (or there are none).
|
|
163
|
+
if ([...classes].every((cls) => cls === taxonomy.UNKNOWN)) return 'insufficient-data';
|
|
164
|
+
return 'ready-with-risks';
|
|
165
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Validate internal Analysis → Diagnostics seam payloads.
|
|
2
|
+
//
|
|
3
|
+
// Uses the dependency-free contract validator against the schemas under
|
|
4
|
+
// shared/diagnostics/schemas/internal/.
|
|
5
|
+
//
|
|
6
|
+
// The schemas must be reachable in both layouts this code runs in: beside the
|
|
7
|
+
// engine when it ships inside the package, and at the canonical repository path
|
|
8
|
+
// during development. Missing schemas raise InternalContractError naming the
|
|
9
|
+
// locations tried, rather than a bare ENOENT from deep inside a diagnosis.
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
import { validate } from '../analysis/contracts.mjs';
|
|
16
|
+
|
|
17
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
const SCHEMA_DIRS = [path.join(here, 'schemas', 'internal')];
|
|
20
|
+
|
|
21
|
+
export class InternalContractError extends Error {}
|
|
22
|
+
|
|
23
|
+
/** The directory holding the internal schemas, whichever layout applies. */
|
|
24
|
+
export function schemaDir() {
|
|
25
|
+
for (const candidate of SCHEMA_DIRS) {
|
|
26
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) return candidate;
|
|
27
|
+
}
|
|
28
|
+
throw new InternalContractError(
|
|
29
|
+
`internal schemas not found; tried: ${SCHEMA_DIRS.join(', ')}. ` +
|
|
30
|
+
'A package that ships the engine must carry diagnostics/schemas/internal/.',
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function load(name) {
|
|
35
|
+
const file = path.join(schemaDir(), name);
|
|
36
|
+
if (!fs.existsSync(file)) throw new InternalContractError(`internal schema missing: ${file}`);
|
|
37
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function require_(payload, schemaName) {
|
|
41
|
+
const errors = validate(payload, load(schemaName));
|
|
42
|
+
if (errors.length > 0) {
|
|
43
|
+
throw new InternalContractError(`${schemaName}: ${errors.join('; ')}`);
|
|
44
|
+
}
|
|
45
|
+
return payload;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const validateAnalysisResult = (payload) => require_(payload, 'analysis-result.schema.json');
|
|
49
|
+
export const validateExecutionResultMin = (payload) => require_(payload, 'execution-result-min.schema.json');
|
|
50
|
+
export const validateDiagnosis = (payload) => require_(payload, 'diagnosis.schema.json');
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Deterministic finding prioritization.
|
|
2
|
+
//
|
|
3
|
+
// Assigns every finding a severity, priority, the three impacts, an owner, and an
|
|
4
|
+
// estimated effort — by a fixed algorithm, so the same finding always ranks the
|
|
5
|
+
// same way. Priority is not a feeling; it is a function of severity, confidence,
|
|
6
|
+
// and business impact.
|
|
7
|
+
|
|
8
|
+
import * as taxonomy from '../analysis/taxonomy.mjs';
|
|
9
|
+
|
|
10
|
+
// Base severity per classification (before confidence adjustment).
|
|
11
|
+
const SEVERITY = {
|
|
12
|
+
[taxonomy.APPLICATION_BUG]: 'high',
|
|
13
|
+
[taxonomy.INFRASTRUCTURE]: 'high',
|
|
14
|
+
[taxonomy.NETWORK]: 'high',
|
|
15
|
+
[taxonomy.AUTH]: 'high',
|
|
16
|
+
[taxonomy.AUTHORIZATION]: 'high',
|
|
17
|
+
[taxonomy.ASSERTION]: 'medium',
|
|
18
|
+
[taxonomy.LOCATOR]: 'medium',
|
|
19
|
+
[taxonomy.TIMEOUT]: 'medium',
|
|
20
|
+
[taxonomy.TEST_DATA]: 'medium',
|
|
21
|
+
[taxonomy.CONFIGURATION]: 'medium',
|
|
22
|
+
[taxonomy.ENVIRONMENT]: 'medium',
|
|
23
|
+
[taxonomy.FRAMEWORK]: 'medium',
|
|
24
|
+
[taxonomy.FLAKY]: 'medium',
|
|
25
|
+
[taxonomy.UNKNOWN]: 'low',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Where the impact predominantly lands, per classification.
|
|
29
|
+
const IMPACT = {
|
|
30
|
+
[taxonomy.APPLICATION_BUG]: { business: 'high', technical: 'high', testing: 'low' },
|
|
31
|
+
[taxonomy.NETWORK]: { business: 'high', technical: 'high', testing: 'medium' },
|
|
32
|
+
[taxonomy.AUTH]: { business: 'high', technical: 'medium', testing: 'medium' },
|
|
33
|
+
[taxonomy.AUTHORIZATION]: { business: 'high', technical: 'medium', testing: 'medium' },
|
|
34
|
+
[taxonomy.INFRASTRUCTURE]: { business: 'medium', technical: 'high', testing: 'high' },
|
|
35
|
+
[taxonomy.LOCATOR]: { business: 'low', technical: 'low', testing: 'high' },
|
|
36
|
+
[taxonomy.ASSERTION]: { business: 'medium', technical: 'medium', testing: 'medium' },
|
|
37
|
+
[taxonomy.TIMEOUT]: { business: 'low', technical: 'medium', testing: 'high' },
|
|
38
|
+
[taxonomy.TEST_DATA]: { business: 'low', technical: 'low', testing: 'high' },
|
|
39
|
+
[taxonomy.CONFIGURATION]: { business: 'low', technical: 'medium', testing: 'high' },
|
|
40
|
+
[taxonomy.ENVIRONMENT]: { business: 'low', technical: 'medium', testing: 'high' },
|
|
41
|
+
[taxonomy.FRAMEWORK]: { business: 'low', technical: 'medium', testing: 'high' },
|
|
42
|
+
[taxonomy.FLAKY]: { business: 'low', technical: 'low', testing: 'high' },
|
|
43
|
+
[taxonomy.UNKNOWN]: { business: 'low', technical: 'low', testing: 'medium' },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Rough effort to resolve, per classification.
|
|
47
|
+
const EFFORT = {
|
|
48
|
+
[taxonomy.LOCATOR]: 'low',
|
|
49
|
+
[taxonomy.ASSERTION]: 'low',
|
|
50
|
+
[taxonomy.CONFIGURATION]: 'low',
|
|
51
|
+
[taxonomy.ENVIRONMENT]: 'low',
|
|
52
|
+
[taxonomy.TEST_DATA]: 'medium',
|
|
53
|
+
[taxonomy.TIMEOUT]: 'medium',
|
|
54
|
+
[taxonomy.FLAKY]: 'medium',
|
|
55
|
+
[taxonomy.AUTH]: 'medium',
|
|
56
|
+
[taxonomy.AUTHORIZATION]: 'medium',
|
|
57
|
+
[taxonomy.FRAMEWORK]: 'medium',
|
|
58
|
+
[taxonomy.NETWORK]: 'high',
|
|
59
|
+
[taxonomy.INFRASTRUCTURE]: 'high',
|
|
60
|
+
[taxonomy.APPLICATION_BUG]: 'external',
|
|
61
|
+
[taxonomy.UNKNOWN]: 'unknown',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const RANK = { low: 1, medium: 2, high: 3 };
|
|
65
|
+
const PRIORITY = { 1: 'P3', 2: 'P2', 3: 'P1', 4: 'P1' };
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Prioritize a root cause.
|
|
69
|
+
*
|
|
70
|
+
* Priority derives from severity, business impact, and confidence, and is escalated
|
|
71
|
+
* one step when the failure blocks a release.
|
|
72
|
+
*/
|
|
73
|
+
export function prioritize(rootCause, { blocking = false } = {}) {
|
|
74
|
+
const classification = rootCause.classification;
|
|
75
|
+
const confidence = 'confidence' in rootCause && rootCause.confidence !== undefined
|
|
76
|
+
? rootCause.confidence
|
|
77
|
+
: 0.5;
|
|
78
|
+
const severity = SEVERITY[classification] ?? 'low';
|
|
79
|
+
const impact = IMPACT[classification] ?? IMPACT[taxonomy.UNKNOWN];
|
|
80
|
+
|
|
81
|
+
// Priority score: severity and business impact drive it; low confidence holds it
|
|
82
|
+
// back (an uncertain finding should not top the queue).
|
|
83
|
+
let score = RANK[severity];
|
|
84
|
+
if (impact.business === 'high') score += 1;
|
|
85
|
+
if (confidence < 0.5) score -= 1;
|
|
86
|
+
if (blocking) score += 1;
|
|
87
|
+
score = Math.max(1, Math.min(4, score));
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
severity,
|
|
91
|
+
priority: PRIORITY[score],
|
|
92
|
+
businessImpact: impact.business,
|
|
93
|
+
technicalImpact: impact.technical,
|
|
94
|
+
testingImpact: impact.testing,
|
|
95
|
+
confidence,
|
|
96
|
+
owner: rootCause.ownership ?? 'needs-triage',
|
|
97
|
+
estimatedEffort: EFFORT[classification] ?? 'unknown',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Deterministic repair planning.
|
|
2
|
+
//
|
|
3
|
+
// Turns a root cause into a repair *plan* — never code. It decides whether the
|
|
4
|
+
// failure is test-side repairable at all, and if so proposes an abstract change,
|
|
5
|
+
// the candidate type, the risk, and a rollback. qa-fix consumes these plans; the
|
|
6
|
+
// plan is always gated by the diff guard and always requires permission before any
|
|
7
|
+
// edit is applied.
|
|
8
|
+
|
|
9
|
+
import * as taxonomy from '../analysis/taxonomy.mjs';
|
|
10
|
+
|
|
11
|
+
// classification -> [repairable, candidate type, abstract change, risk].
|
|
12
|
+
// Only test-side causes are repairable; product, network, infra, authorization and
|
|
13
|
+
// environment failures are escalations, not repairs.
|
|
14
|
+
const PLANS = {
|
|
15
|
+
[taxonomy.LOCATOR]: [true, 'locator-update',
|
|
16
|
+
'Update the failing locator to target the same element in the current DOM.', 'low'],
|
|
17
|
+
[taxonomy.ASSERTION]: [true, 'assertion-improvement',
|
|
18
|
+
'Correct the assertion to match the intended behavior, or confirm a product bug first.', 'medium'],
|
|
19
|
+
[taxonomy.TIMEOUT]: [true, 'wait-strategy',
|
|
20
|
+
'Replace a fixed or missing wait with a web-first wait on the awaited condition.', 'medium'],
|
|
21
|
+
[taxonomy.FLAKY]: [true, 'synchronization',
|
|
22
|
+
'Remove the race by awaiting the real condition; add a tracked quarantine only if needed.', 'medium'],
|
|
23
|
+
[taxonomy.TEST_DATA]: [true, 'test-data',
|
|
24
|
+
'Repair or reseed the test data the scenario depends on.', 'medium'],
|
|
25
|
+
[taxonomy.CONFIGURATION]: [true, 'configuration',
|
|
26
|
+
'Correct the test configuration the run depends on.', 'low'],
|
|
27
|
+
[taxonomy.ENVIRONMENT]: [false, 'environment',
|
|
28
|
+
'Fix the environment (base URL, service availability); not a test-side repair.', 'n/a'],
|
|
29
|
+
[taxonomy.AUTH]: [true, 'authentication',
|
|
30
|
+
"Repair the test's credentials or auth setup; do not weaken the check.", 'medium'],
|
|
31
|
+
[taxonomy.AUTHORIZATION]: [false, 'authorization',
|
|
32
|
+
'Grant the test account permission or use an authorized role; not a code repair.', 'n/a'],
|
|
33
|
+
[taxonomy.NETWORK]: [false, 'network',
|
|
34
|
+
'Investigate the upstream service; not a test-side repair.', 'n/a'],
|
|
35
|
+
[taxonomy.INFRASTRUCTURE]: [false, 'infrastructure',
|
|
36
|
+
'Escalate to CI/infra; not a test-side repair.', 'n/a'],
|
|
37
|
+
[taxonomy.APPLICATION_BUG]: [false, 'application-bug',
|
|
38
|
+
'File a product bug; the test correctly caught a real defect.', 'n/a'],
|
|
39
|
+
[taxonomy.FRAMEWORK]: [false, 'framework',
|
|
40
|
+
'Update or pin the framework/driver; not a test-side repair.', 'n/a'],
|
|
41
|
+
[taxonomy.UNKNOWN]: [false, 'unknown',
|
|
42
|
+
'Investigate further before any repair.', 'n/a'],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Produce a repair plan for a root cause.
|
|
47
|
+
*
|
|
48
|
+
* `{repairable, candidateType, proposedChanges, affectedFiles, risk,
|
|
49
|
+
* permissionRequired, rollbackStrategy, safetyReview}`. Never contains code.
|
|
50
|
+
*/
|
|
51
|
+
export function planRepair(rootCause, affectedFiles = null) {
|
|
52
|
+
const [repairable, candidate, change, risk] =
|
|
53
|
+
PLANS[rootCause.classification] ?? PLANS[taxonomy.UNKNOWN];
|
|
54
|
+
|
|
55
|
+
const safety = repairable
|
|
56
|
+
? 'Any edit will be checked by the diff guard before it is proposed as complete; ' +
|
|
57
|
+
'the guard rejects removed assertions, added skips, forced passes, and timeout inflation.'
|
|
58
|
+
: 'No test-side edit is appropriate; this is an escalation, not a repair.';
|
|
59
|
+
const rollback = repairable
|
|
60
|
+
? 'No source is changed without approval; revert the proposed edits to roll back.'
|
|
61
|
+
: 'Not applicable — no change is proposed.';
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
repairable,
|
|
65
|
+
candidateType: candidate,
|
|
66
|
+
proposedChanges: repairable ? [change] : [],
|
|
67
|
+
affectedFiles: [...(affectedFiles ?? [])],
|
|
68
|
+
risk,
|
|
69
|
+
permissionRequired: true,
|
|
70
|
+
rollbackStrategy: rollback,
|
|
71
|
+
safetyReview: safety,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Deterministic root-cause analysis.
|
|
2
|
+
//
|
|
3
|
+
// Turns a failure signal into a classified root cause with the four things every
|
|
4
|
+
// classification must carry: a taxonomy class (with confidence and evidence-backed
|
|
5
|
+
// reason), a recommended action, and an owner. It reuses the analysis platform's
|
|
6
|
+
// failure taxonomy; it adds the ownership and recommendation mappings and the
|
|
7
|
+
// metadata-driven classes (flaky) the taxonomy cannot infer from a message alone.
|
|
8
|
+
//
|
|
9
|
+
// No unsupported conclusions: a signal that matches no rule is `unknown`.
|
|
10
|
+
|
|
11
|
+
import * as taxonomy from '../analysis/taxonomy.mjs';
|
|
12
|
+
|
|
13
|
+
/** classification -> the party that typically owns the fix. */
|
|
14
|
+
export const OWNERSHIP = {
|
|
15
|
+
[taxonomy.ASSERTION]: 'test-author-or-product',
|
|
16
|
+
[taxonomy.LOCATOR]: 'test-author',
|
|
17
|
+
[taxonomy.TIMEOUT]: 'test-author-or-environment',
|
|
18
|
+
[taxonomy.NETWORK]: 'backend-or-infrastructure',
|
|
19
|
+
[taxonomy.AUTH]: 'auth-or-test-setup',
|
|
20
|
+
[taxonomy.AUTHORIZATION]: 'permissions-or-test-account',
|
|
21
|
+
[taxonomy.ENVIRONMENT]: 'environment-owner',
|
|
22
|
+
[taxonomy.CONFIGURATION]: 'config-owner',
|
|
23
|
+
[taxonomy.INFRASTRUCTURE]: 'ci-or-infrastructure',
|
|
24
|
+
[taxonomy.TEST_DATA]: 'test-data-owner',
|
|
25
|
+
[taxonomy.APPLICATION_BUG]: 'product',
|
|
26
|
+
[taxonomy.FRAMEWORK]: 'framework-or-driver',
|
|
27
|
+
[taxonomy.FLAKY]: 'test-author',
|
|
28
|
+
[taxonomy.UNKNOWN]: 'needs-triage',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// classification -> the safe recommended action (implements the analysis
|
|
32
|
+
// platform's recommendation-guidelines; never recommends forcing a pass).
|
|
33
|
+
export const RECOMMENDATION = {
|
|
34
|
+
[taxonomy.ASSERTION]: 'Confirm whether the app or the expectation is wrong; fix whichever is genuinely incorrect.',
|
|
35
|
+
[taxonomy.LOCATOR]: 'Inspect the current DOM and update the locator to target the same element.',
|
|
36
|
+
[taxonomy.TIMEOUT]: 'Investigate the slowness; raise a wait only if the operation is legitimately slower.',
|
|
37
|
+
[taxonomy.NETWORK]: 'Check the upstream service and the request; retry only if the failure is genuinely transient.',
|
|
38
|
+
[taxonomy.AUTH]: 'Fix the credentials or auth setup; do not weaken the authentication check.',
|
|
39
|
+
[taxonomy.AUTHORIZATION]: 'Grant the test account the needed permission or use an authorized role; do not bypass the check.',
|
|
40
|
+
[taxonomy.ENVIRONMENT]: 'Fix the environment (base URL, service availability); the test is likely fine.',
|
|
41
|
+
[taxonomy.CONFIGURATION]: 'Correct the configuration; do not work around it in the test.',
|
|
42
|
+
[taxonomy.INFRASTRUCTURE]: 'Escalate to CI or infrastructure owners; add resources, do not shrink the suite.',
|
|
43
|
+
[taxonomy.TEST_DATA]: 'Repair or reseed the data; do not delete the assertion that caught the gap.',
|
|
44
|
+
[taxonomy.APPLICATION_BUG]: 'File a bug against the product; do NOT modify the test to pass.',
|
|
45
|
+
[taxonomy.FRAMEWORK]: 'Update or pin the framework/driver; report upstream if it is a genuine defect.',
|
|
46
|
+
[taxonomy.FLAKY]: 'Stabilize the test (fix the race or synchronization); quarantine only with a tracking issue.',
|
|
47
|
+
[taxonomy.UNKNOWN]: 'Investigate further; the evidence was insufficient to classify.',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Classify a failure signal into a root cause.
|
|
52
|
+
*
|
|
53
|
+
* `signal` may carry: message, httpStatus, retries, finalStatus, classification,
|
|
54
|
+
* confidence, reason, evidence. Returns
|
|
55
|
+
* `{classification, confidence, reason, ownership, recommendation, evidence}`.
|
|
56
|
+
*/
|
|
57
|
+
export function analyze(signal) {
|
|
58
|
+
const message = signal.message ?? '';
|
|
59
|
+
const httpStatus = signal.httpStatus;
|
|
60
|
+
const retries = signal.retries || 0;
|
|
61
|
+
const finalStatus = signal.finalStatus;
|
|
62
|
+
const provided = signal.classification;
|
|
63
|
+
|
|
64
|
+
let classification;
|
|
65
|
+
let confidence;
|
|
66
|
+
let reason;
|
|
67
|
+
|
|
68
|
+
// Flakiness is a metadata signal, not a message pattern: a test that needed a
|
|
69
|
+
// retry to pass, or is explicitly flagged flaky, is nondeterministic.
|
|
70
|
+
if (finalStatus === 'flaky' || (retries > 0 && ['passed', 'flaky'].includes(finalStatus))) {
|
|
71
|
+
classification = taxonomy.FLAKY;
|
|
72
|
+
confidence = 0.8;
|
|
73
|
+
reason = 'The test passed only after a retry, indicating nondeterministic behavior.';
|
|
74
|
+
} else if (taxonomy.CLASSES.has(provided)) {
|
|
75
|
+
// The analysis platform already classified this deterministically; trust it
|
|
76
|
+
// rather than re-deriving from the message.
|
|
77
|
+
classification = provided;
|
|
78
|
+
confidence = 'confidence' in signal ? signal.confidence : 0.8;
|
|
79
|
+
reason = signal.reason || `Classified ${provided} by the analysis platform.`;
|
|
80
|
+
} else {
|
|
81
|
+
({ classification, confidence, reason } = taxonomy.classify(message, httpStatus ?? null));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
classification,
|
|
86
|
+
confidence,
|
|
87
|
+
reason,
|
|
88
|
+
ownership: OWNERSHIP[classification] ?? 'needs-triage',
|
|
89
|
+
recommendation: RECOMMENDATION[classification] ?? RECOMMENDATION[taxonomy.UNKNOWN],
|
|
90
|
+
evidence: signal.evidence ?? [],
|
|
91
|
+
};
|
|
92
|
+
}
|