@yasserkhanorg/e2e-agents 0.5.16 → 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/dist/agent/pipeline.d.ts +1 -1
- package/dist/agent/pipeline.d.ts.map +1 -1
- package/dist/agent/plan.d.ts +0 -12
- package/dist/agent/plan.d.ts.map +1 -1
- package/dist/agent/plan.js +0 -365
- package/dist/agent/types.d.ts +42 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +4 -0
- package/dist/api.d.ts +10 -14
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +29 -59
- package/dist/cli.js +41 -174
- package/dist/engine/impact_engine.d.ts +36 -0
- package/dist/engine/impact_engine.d.ts.map +1 -0
- package/dist/engine/impact_engine.js +196 -0
- package/dist/engine/plan_builder.d.ts +9 -0
- package/dist/engine/plan_builder.d.ts.map +1 -0
- package/dist/engine/plan_builder.js +329 -0
- package/dist/esm/agent/plan.js +1 -360
- package/dist/esm/agent/types.js +3 -0
- package/dist/esm/api.js +27 -56
- package/dist/esm/cli.js +40 -173
- package/dist/esm/engine/impact_engine.js +191 -0
- package/dist/esm/engine/plan_builder.js +323 -0
- package/dist/esm/index.js +6 -3
- package/dist/esm/knowledge/route_families.js +57 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -5
- package/dist/knowledge/route_families.d.ts +19 -0
- package/dist/knowledge/route_families.d.ts.map +1 -1
- package/dist/knowledge/route_families.js +60 -0
- package/package.json +1 -1
- package/dist/agent/ai_flow_analysis.d.ts +0 -13
- package/dist/agent/ai_flow_analysis.d.ts.map +0 -1
- package/dist/agent/ai_flow_analysis.js +0 -334
- package/dist/agent/ai_mapping.d.ts +0 -14
- package/dist/agent/ai_mapping.d.ts.map +0 -1
- package/dist/agent/ai_mapping.js +0 -560
- package/dist/agent/analysis.d.ts +0 -64
- package/dist/agent/analysis.d.ts.map +0 -1
- package/dist/agent/analysis.js +0 -292
- package/dist/agent/blast_radius.d.ts +0 -4
- package/dist/agent/blast_radius.d.ts.map +0 -1
- package/dist/agent/blast_radius.js +0 -37
- package/dist/agent/dependency_graph.d.ts +0 -14
- package/dist/agent/dependency_graph.d.ts.map +0 -1
- package/dist/agent/dependency_graph.js +0 -227
- package/dist/agent/flags.d.ts +0 -23
- package/dist/agent/flags.d.ts.map +0 -1
- package/dist/agent/flags.js +0 -171
- package/dist/agent/flow_catalog.d.ts +0 -25
- package/dist/agent/flow_catalog.d.ts.map +0 -1
- package/dist/agent/flow_catalog.js +0 -115
- package/dist/agent/flow_mapping.d.ts +0 -10
- package/dist/agent/flow_mapping.d.ts.map +0 -1
- package/dist/agent/flow_mapping.js +0 -84
- package/dist/agent/framework.d.ts +0 -13
- package/dist/agent/framework.d.ts.map +0 -1
- package/dist/agent/framework.js +0 -149
- package/dist/agent/gap_suggestions.d.ts +0 -14
- package/dist/agent/gap_suggestions.d.ts.map +0 -1
- package/dist/agent/gap_suggestions.js +0 -101
- package/dist/agent/generator.d.ts +0 -10
- package/dist/agent/generator.d.ts.map +0 -1
- package/dist/agent/generator.js +0 -115
- package/dist/agent/operational_insights.d.ts +0 -41
- package/dist/agent/operational_insights.d.ts.map +0 -1
- package/dist/agent/operational_insights.js +0 -127
- package/dist/agent/report.d.ts +0 -97
- package/dist/agent/report.d.ts.map +0 -1
- package/dist/agent/report.js +0 -159
- package/dist/agent/runner.d.ts +0 -7
- package/dist/agent/runner.d.ts.map +0 -1
- package/dist/agent/runner.js +0 -898
- package/dist/agent/selectors.d.ts +0 -10
- package/dist/agent/selectors.d.ts.map +0 -1
- package/dist/agent/selectors.js +0 -75
- package/dist/agent/subsystem_risk.d.ts +0 -23
- package/dist/agent/subsystem_risk.d.ts.map +0 -1
- package/dist/agent/subsystem_risk.js +0 -207
- package/dist/agent/tests.d.ts +0 -19
- package/dist/agent/tests.d.ts.map +0 -1
- package/dist/agent/tests.js +0 -116
- package/dist/agent/traceability.d.ts +0 -22
- package/dist/agent/traceability.d.ts.map +0 -1
- package/dist/agent/traceability.js +0 -183
- package/dist/esm/agent/ai_flow_analysis.js +0 -331
- package/dist/esm/agent/ai_mapping.js +0 -557
- package/dist/esm/agent/analysis.js +0 -287
- package/dist/esm/agent/blast_radius.js +0 -34
- package/dist/esm/agent/dependency_graph.js +0 -224
- package/dist/esm/agent/flags.js +0 -160
- package/dist/esm/agent/flow_catalog.js +0 -112
- package/dist/esm/agent/flow_mapping.js +0 -81
- package/dist/esm/agent/framework.js +0 -145
- package/dist/esm/agent/gap_suggestions.js +0 -98
- package/dist/esm/agent/generator.js +0 -112
- package/dist/esm/agent/operational_insights.js +0 -124
- package/dist/esm/agent/report.js +0 -156
- package/dist/esm/agent/runner.js +0 -894
- package/dist/esm/agent/selectors.js +0 -71
- package/dist/esm/agent/subsystem_risk.js +0 -204
- package/dist/esm/agent/tests.js +0 -111
- package/dist/esm/agent/traceability.js +0 -180
package/dist/agent/runner.js
DELETED
|
@@ -1,898 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
3
|
-
// See LICENSE.txt for license information.
|
|
4
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
-
exports.runImpact = runImpact;
|
|
6
|
-
exports.runGap = runGap;
|
|
7
|
-
const fs_1 = require("fs");
|
|
8
|
-
const path_1 = require("path");
|
|
9
|
-
const analysis_js_1 = require("./analysis.js");
|
|
10
|
-
const blast_radius_js_1 = require("./blast_radius.js");
|
|
11
|
-
const framework_js_1 = require("./framework.js");
|
|
12
|
-
const git_js_1 = require("./git.js");
|
|
13
|
-
const report_js_1 = require("./report.js");
|
|
14
|
-
const flags_js_1 = require("./flags.js");
|
|
15
|
-
const selectors_js_1 = require("./selectors.js");
|
|
16
|
-
const tests_js_1 = require("./tests.js");
|
|
17
|
-
const generator_js_1 = require("./generator.js");
|
|
18
|
-
const flow_catalog_js_1 = require("./flow_catalog.js");
|
|
19
|
-
const flow_mapping_js_1 = require("./flow_mapping.js");
|
|
20
|
-
const pipeline_js_1 = require("./pipeline.js");
|
|
21
|
-
const gap_suggestions_js_1 = require("./gap_suggestions.js");
|
|
22
|
-
const dependency_graph_js_1 = require("./dependency_graph.js");
|
|
23
|
-
const traceability_js_1 = require("./traceability.js");
|
|
24
|
-
const utils_js_1 = require("./utils.js");
|
|
25
|
-
const ai_mapping_js_1 = require("./ai_mapping.js");
|
|
26
|
-
const ai_flow_analysis_js_1 = require("./ai_flow_analysis.js");
|
|
27
|
-
const PRIORITY_RANK = {
|
|
28
|
-
P0: 0,
|
|
29
|
-
P1: 1,
|
|
30
|
-
P2: 2,
|
|
31
|
-
};
|
|
32
|
-
function ensureAppRoot(path) {
|
|
33
|
-
if (!(0, fs_1.existsSync)(path)) {
|
|
34
|
-
throw new Error(`App path does not exist: ${path}`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function computeGaps(flows, coverageMap, coverage) {
|
|
38
|
-
const coverageByFlowId = new Map((coverage || []).map((c) => [c.flowId, c]));
|
|
39
|
-
return flows
|
|
40
|
-
.filter((flow) => {
|
|
41
|
-
if (flow.priority !== 'P0' && flow.priority !== 'P1') {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
const coveredBy = coverageMap.get(flow.id) || [];
|
|
45
|
-
if (coveredBy.length === 0) {
|
|
46
|
-
return true; // no tests at all
|
|
47
|
-
}
|
|
48
|
-
// Flows with 3+ mapped tests are considered comprehensively covered — the AI
|
|
49
|
-
// maps 3 tests only when each has specific behavioral evidence for the flow.
|
|
50
|
-
// missingScenarios from the AI are informational for such flows, not blocking.
|
|
51
|
-
if (coveredBy.length >= 3) {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
// For flows with 1-3 tests, flag as a gap if AI identified missing scenarios.
|
|
55
|
-
const flowCoverage = coverageByFlowId.get(flow.id);
|
|
56
|
-
return (flowCoverage?.missingScenarios || []).length > 0;
|
|
57
|
-
})
|
|
58
|
-
.map((flow) => {
|
|
59
|
-
const coveredBy = coverageMap.get(flow.id) || [];
|
|
60
|
-
const flowCoverage = coverageByFlowId.get(flow.id);
|
|
61
|
-
return {
|
|
62
|
-
...flow,
|
|
63
|
-
existingTests: coveredBy.length > 0 ? coveredBy : undefined,
|
|
64
|
-
missingScenarios: flowCoverage?.missingScenarios?.length ? flowCoverage.missingScenarios : undefined,
|
|
65
|
-
};
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
function normalizeChangedFiles(appRoot, files) {
|
|
69
|
-
const normalizedRoot = appRoot.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
70
|
-
const baseName = normalizedRoot.split('/').pop() || '';
|
|
71
|
-
return files
|
|
72
|
-
.map((file) => file.replace(/\\/g, '/'))
|
|
73
|
-
.map((file) => {
|
|
74
|
-
if (baseName && file.startsWith(`${baseName}/`)) {
|
|
75
|
-
return file.slice(baseName.length + 1);
|
|
76
|
-
}
|
|
77
|
-
return file;
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
function sortFlows(flows) {
|
|
81
|
-
const priorityRank = { P0: 0, P1: 1, P2: 2 };
|
|
82
|
-
return [...flows].sort((a, b) => {
|
|
83
|
-
const rankDiff = (priorityRank[a.priority] ?? 3) - (priorityRank[b.priority] ?? 3);
|
|
84
|
-
if (rankDiff !== 0) {
|
|
85
|
-
return rankDiff;
|
|
86
|
-
}
|
|
87
|
-
return b.score - a.score;
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
function applyPriorityThresholds(flows, config) {
|
|
91
|
-
return flows.map((flow) => {
|
|
92
|
-
const priority = flow.score >= config.risk.p0Threshold
|
|
93
|
-
? 'P0'
|
|
94
|
-
: flow.score >= config.risk.p1Threshold
|
|
95
|
-
? 'P1'
|
|
96
|
-
: 'P2';
|
|
97
|
-
const boundedPriority = flow.priorityFloor && PRIORITY_RANK[flow.priorityFloor] < PRIORITY_RANK[priority]
|
|
98
|
-
? flow.priorityFloor
|
|
99
|
-
: priority;
|
|
100
|
-
return { ...flow, priority: boundedPriority };
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
function buildRecommendedTestsWithFlags(flows, testsByFlow) {
|
|
104
|
-
const testNotes = new Map();
|
|
105
|
-
for (const flow of flows) {
|
|
106
|
-
if (flow.priority !== 'P0' && flow.priority !== 'P1') {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
const tests = testsByFlow.get(flow.id) || [];
|
|
110
|
-
const flagSummary = (0, flags_js_1.formatFlags)(flow.flags || []);
|
|
111
|
-
for (const test of tests) {
|
|
112
|
-
const normalizedTest = (0, utils_js_1.normalizePath)(test)
|
|
113
|
-
.replace(/^\.\//, '')
|
|
114
|
-
.replace(/^e2e-tests\/playwright\//, '');
|
|
115
|
-
if (!testNotes.has(normalizedTest)) {
|
|
116
|
-
testNotes.set(normalizedTest, new Set());
|
|
117
|
-
}
|
|
118
|
-
if (flagSummary !== 'none') {
|
|
119
|
-
testNotes.get(normalizedTest)?.add(flagSummary);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return Array.from(testNotes.entries())
|
|
124
|
-
.map(([test, notes]) => {
|
|
125
|
-
if (notes.size === 0) {
|
|
126
|
-
return test;
|
|
127
|
-
}
|
|
128
|
-
return `${test} (flags: ${Array.from(notes).join(', ')})`;
|
|
129
|
-
})
|
|
130
|
-
.sort();
|
|
131
|
-
}
|
|
132
|
-
function buildRecommendedTestsFromCoverage(flows, coverage) {
|
|
133
|
-
const flowMap = new Map();
|
|
134
|
-
for (const flow of flows) {
|
|
135
|
-
flowMap.set(flow.id, flow);
|
|
136
|
-
}
|
|
137
|
-
const testNotes = new Map();
|
|
138
|
-
for (const entry of coverage) {
|
|
139
|
-
if (entry.priority !== 'P0' && entry.priority !== 'P1') {
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
const flow = flowMap.get(entry.flowId);
|
|
143
|
-
const flagSummary = (0, flags_js_1.formatFlags)(flow?.flags || []);
|
|
144
|
-
for (const test of entry.coveredBy) {
|
|
145
|
-
const normalizedTest = (0, utils_js_1.normalizePath)(test)
|
|
146
|
-
.replace(/^\.\//, '')
|
|
147
|
-
.replace(/^e2e-tests\/playwright\//, '');
|
|
148
|
-
if (!testNotes.has(normalizedTest)) {
|
|
149
|
-
testNotes.set(normalizedTest, new Set());
|
|
150
|
-
}
|
|
151
|
-
if (flagSummary !== 'none') {
|
|
152
|
-
testNotes.get(normalizedTest)?.add(flagSummary);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return Array.from(testNotes.entries())
|
|
157
|
-
.map(([test, notes]) => {
|
|
158
|
-
if (notes.size === 0) {
|
|
159
|
-
return test;
|
|
160
|
-
}
|
|
161
|
-
return `${test} (flags: ${Array.from(notes).join(', ')})`;
|
|
162
|
-
})
|
|
163
|
-
.sort();
|
|
164
|
-
}
|
|
165
|
-
function uniquePaths(paths) {
|
|
166
|
-
return Array.from(new Set(paths.map((value) => value.replace(/\\/g, '/')).filter(Boolean)));
|
|
167
|
-
}
|
|
168
|
-
function mergeCoverageWithHeuristicFallback(traceability, heuristic) {
|
|
169
|
-
const byFlow = new Map();
|
|
170
|
-
for (const entry of traceability) {
|
|
171
|
-
byFlow.set(entry.flowId, entry);
|
|
172
|
-
}
|
|
173
|
-
for (const entry of heuristic) {
|
|
174
|
-
const existing = byFlow.get(entry.flowId);
|
|
175
|
-
if (!existing) {
|
|
176
|
-
byFlow.set(entry.flowId, entry);
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
if (existing.coveredBy.length === 0 && entry.coveredBy.length > 0) {
|
|
180
|
-
byFlow.set(entry.flowId, entry);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return Array.from(byFlow.values());
|
|
184
|
-
}
|
|
185
|
-
function buildMattermostFailClosedWarning(reason) {
|
|
186
|
-
return `${reason} Mattermost strict mode will emit uncovered flows as must-add-tests without heuristic fallback.`;
|
|
187
|
-
}
|
|
188
|
-
function applyMattermostEvidencePolicy(config, state) {
|
|
189
|
-
if (config.profile !== 'mattermost') {
|
|
190
|
-
return {
|
|
191
|
-
coverage: state.coverage,
|
|
192
|
-
recommendedTests: state.recommendedTests,
|
|
193
|
-
testMappingSource: state.testMappingSource,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
if (state.testMappingSource === 'heuristic') {
|
|
197
|
-
throw new Error('Mattermost profile requires AI or catalog evidence for test selection. Heuristic-only mapping is not allowed.');
|
|
198
|
-
}
|
|
199
|
-
if (state.failClosedTargeting) {
|
|
200
|
-
return {
|
|
201
|
-
coverage: state.coverage,
|
|
202
|
-
recommendedTests: state.recommendedTests,
|
|
203
|
-
testMappingSource: state.testMappingSource,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
if (state.testMappingSource !== 'catalog' && state.testMappingSource !== 'ai' && !state.traceabilityStats?.manifestFound) {
|
|
207
|
-
throw new Error('Mattermost profile requires traceability evidence or AI mapping. Generate or refresh traceability manifest, or enable impact.aiMapping in config.');
|
|
208
|
-
}
|
|
209
|
-
const traceabilityCoverageRatio = state.traceabilityStats?.coverageRatio ?? 0;
|
|
210
|
-
if (state.testMappingSource === 'traceability' && traceabilityCoverageRatio < 0.6) {
|
|
211
|
-
throw new Error(`Mattermost profile requires stronger traceability coverage. Current ratio is ${traceabilityCoverageRatio.toFixed(2)}; AI mapping is required to close evidence gaps.`);
|
|
212
|
-
}
|
|
213
|
-
const output = {
|
|
214
|
-
coverage: state.coverage,
|
|
215
|
-
recommendedTests: state.recommendedTests,
|
|
216
|
-
testMappingSource: state.testMappingSource,
|
|
217
|
-
};
|
|
218
|
-
if (state.testMappingSource === 'ai') {
|
|
219
|
-
const traceabilityWarningPrefix = 'Traceability manifest not found or invalid:';
|
|
220
|
-
for (let i = state.warnings.length - 1; i >= 0; i -= 1) {
|
|
221
|
-
if (state.warnings[i].startsWith(traceabilityWarningPrefix)) {
|
|
222
|
-
state.warnings.splice(i, 1);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return output;
|
|
227
|
-
}
|
|
228
|
-
function classifyImpactModelConfidence(flowMapping, testMapping, dependencyGraph, traceability, warnings) {
|
|
229
|
-
let score = 0;
|
|
230
|
-
if (flowMapping === 'catalog') {
|
|
231
|
-
score += 2;
|
|
232
|
-
}
|
|
233
|
-
else if (flowMapping === 'ai') {
|
|
234
|
-
score += 2;
|
|
235
|
-
}
|
|
236
|
-
if (testMapping === 'catalog') {
|
|
237
|
-
score += 2;
|
|
238
|
-
}
|
|
239
|
-
else if (testMapping === 'traceability') {
|
|
240
|
-
score += 3;
|
|
241
|
-
}
|
|
242
|
-
else if (testMapping === 'ai') {
|
|
243
|
-
score += 2;
|
|
244
|
-
}
|
|
245
|
-
if (traceability) {
|
|
246
|
-
if (!traceability.manifestFound) {
|
|
247
|
-
score -= 1;
|
|
248
|
-
}
|
|
249
|
-
else if (traceability.coverageRatio >= 0.7) {
|
|
250
|
-
score += 1;
|
|
251
|
-
}
|
|
252
|
-
else if (traceability.coverageRatio < 0.4) {
|
|
253
|
-
score -= 1;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
if (dependencyGraph && dependencyGraph.expandedFiles.length > 0) {
|
|
257
|
-
score += 1;
|
|
258
|
-
}
|
|
259
|
-
if (dependencyGraph && dependencyGraph.truncated) {
|
|
260
|
-
score -= 1;
|
|
261
|
-
}
|
|
262
|
-
if (warnings.length > 0) {
|
|
263
|
-
score -= 1;
|
|
264
|
-
}
|
|
265
|
-
if (score >= 5) {
|
|
266
|
-
return 'high';
|
|
267
|
-
}
|
|
268
|
-
if (score >= 3) {
|
|
269
|
-
return 'medium';
|
|
270
|
-
}
|
|
271
|
-
return 'low';
|
|
272
|
-
}
|
|
273
|
-
function createRunId(mode) {
|
|
274
|
-
const ciRunId = process.env.GITHUB_RUN_ID;
|
|
275
|
-
const entropy = Math.random().toString(36).slice(2, 8);
|
|
276
|
-
const ts = Date.now().toString(36);
|
|
277
|
-
if (ciRunId) {
|
|
278
|
-
return `${mode}-gh-${ciRunId}-${ts}-${entropy}`;
|
|
279
|
-
}
|
|
280
|
-
return `${mode}-local-${ts}-${entropy}`;
|
|
281
|
-
}
|
|
282
|
-
async function runImpact(_config, _options) {
|
|
283
|
-
ensureAppRoot(_config.path);
|
|
284
|
-
if (_config.testsRoot) {
|
|
285
|
-
ensureAppRoot(_config.testsRoot);
|
|
286
|
-
}
|
|
287
|
-
const deadline = Date.now() + _config.timeLimitMinutes * 60 * 1000;
|
|
288
|
-
const runStartedAt = new Date().toISOString();
|
|
289
|
-
const runStartedTs = Date.now();
|
|
290
|
-
const runId = createRunId('impact');
|
|
291
|
-
const warnings = [];
|
|
292
|
-
const testsRoot = _config.testsRoot || _config.path;
|
|
293
|
-
const frameworkDetection = (0, framework_js_1.detectFramework)(testsRoot, _config.framework);
|
|
294
|
-
const testPatterns = (0, framework_js_1.resolveTestPatterns)(testsRoot, frameworkDetection, _config.testDiscovery.patterns);
|
|
295
|
-
if (frameworkDetection.framework === 'unknown' && testPatterns.patterns.length === 0) {
|
|
296
|
-
throw new Error('No framework config found. Provide testDiscovery.patterns in config or --patterns.');
|
|
297
|
-
}
|
|
298
|
-
const gitResult = (0, git_js_1.getChangedFiles)(_config.path, _config.git.since, {
|
|
299
|
-
includeUncommitted: _config.git.includeUncommitted,
|
|
300
|
-
});
|
|
301
|
-
const changedFiles = normalizeChangedFiles(_config.path, gitResult.files);
|
|
302
|
-
if (gitResult.error) {
|
|
303
|
-
warnings.push(`Git diff failed: ${gitResult.error}`);
|
|
304
|
-
}
|
|
305
|
-
if (changedFiles.length === 0 && !_config.impact.allowFallback) {
|
|
306
|
-
throw new Error('No changed files detected. Provide --since or use gap mode (or --allow-fallback).');
|
|
307
|
-
}
|
|
308
|
-
const changedAppFiles = changedFiles.filter((file) => !(0, analysis_js_1.isTestFilePath)(file));
|
|
309
|
-
let analysisTargets = [...changedAppFiles];
|
|
310
|
-
if (analysisTargets.length === 0 && _config.impact.allowFallback) {
|
|
311
|
-
warnings.push('No changed files detected. Falling back to repository scan for screens.');
|
|
312
|
-
analysisTargets = (0, analysis_js_1.scanRepositoryFlows)(_config.path, 250, _config.flowDiscovery.patterns, _config.flowDiscovery.exclude);
|
|
313
|
-
}
|
|
314
|
-
let dependencyGraph;
|
|
315
|
-
if (analysisTargets.length > 0 && _config.impact.dependencyGraph.enabled) {
|
|
316
|
-
dependencyGraph = (0, dependency_graph_js_1.expandByDependencyGraph)(_config.path, analysisTargets, _config.impact.dependencyGraph);
|
|
317
|
-
warnings.push(...dependencyGraph.warnings);
|
|
318
|
-
if (dependencyGraph.expandedFiles.length > 0) {
|
|
319
|
-
analysisTargets = uniquePaths([...analysisTargets, ...dependencyGraph.expandedFiles]);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
const analysis = (0, analysis_js_1.analyzeFiles)(_config.path, analysisTargets, _config);
|
|
323
|
-
warnings.push(...analysis.warnings);
|
|
324
|
-
if (Date.now() > deadline) {
|
|
325
|
-
warnings.push('Time limit exceeded after impact analysis. Skipping coverage and selector steps.');
|
|
326
|
-
}
|
|
327
|
-
let coverage = [];
|
|
328
|
-
let gaps = [];
|
|
329
|
-
let dataTestIds = [];
|
|
330
|
-
let flows = [];
|
|
331
|
-
let flowCatalogSource;
|
|
332
|
-
let recommendedTests = [];
|
|
333
|
-
let testsByFlow;
|
|
334
|
-
let testSuggestions = [];
|
|
335
|
-
const catalog = (0, flow_catalog_js_1.loadFlowCatalog)(_config);
|
|
336
|
-
let flowMappingSource = catalog ? 'catalog' : 'heuristic';
|
|
337
|
-
let testMappingSource = 'heuristic';
|
|
338
|
-
let traceabilityStats;
|
|
339
|
-
let mattermostFailClosedTargeting = false;
|
|
340
|
-
if (catalog) {
|
|
341
|
-
flowCatalogSource = catalog.source;
|
|
342
|
-
const mapping = (0, flow_mapping_js_1.mapChangesToCatalogFlows)(catalog, analysisTargets, 'impact', _config);
|
|
343
|
-
flows = mapping.flows;
|
|
344
|
-
testsByFlow = mapping.testsByFlow;
|
|
345
|
-
warnings.push(...mapping.warnings);
|
|
346
|
-
if (_config.profile === 'mattermost' && changedAppFiles.length > 0 && flows.length === 0) {
|
|
347
|
-
throw new Error('Mattermost profile catalog mapping returned no impacted flows. Refresh traceability or AI flow mapping before target selection.');
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
flows = analysis.flows;
|
|
352
|
-
if (changedAppFiles.length === 0) {
|
|
353
|
-
// No app files changed (e.g. only CI config or e2e files changed).
|
|
354
|
-
// Treat as zero app impact without calling AI — the gate should pass cleanly.
|
|
355
|
-
flows = [];
|
|
356
|
-
flowMappingSource = 'ai';
|
|
357
|
-
warnings.push('No app files changed; skipping AI flow analysis.');
|
|
358
|
-
}
|
|
359
|
-
else if (_config.impact.aiFlow.enabled) {
|
|
360
|
-
const aiFlow = await (0, ai_flow_analysis_js_1.mapAIFlowsFromFiles)(_config.path, testsRoot, _config.impact.aiFlow, analysis.files, changedAppFiles);
|
|
361
|
-
warnings.push(...aiFlow.warnings);
|
|
362
|
-
if (aiFlow.used) {
|
|
363
|
-
flows = aiFlow.flows;
|
|
364
|
-
flowMappingSource = 'ai';
|
|
365
|
-
}
|
|
366
|
-
else if (aiFlow.ran) {
|
|
367
|
-
// AI ran successfully but found no user-facing flows — treat as zero impact.
|
|
368
|
-
flows = [];
|
|
369
|
-
flowMappingSource = 'ai';
|
|
370
|
-
}
|
|
371
|
-
else if (_config.impact.aiFlow.strict || _config.profile === 'mattermost') {
|
|
372
|
-
throw new Error('AI flow analysis is required but unavailable. Check Anthropic/LLM provider configuration.');
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
if (_config.profile === 'mattermost' && flowMappingSource === 'heuristic') {
|
|
377
|
-
throw new Error('Mattermost profile requires AI or catalog flow mapping; heuristic flow mapping is disabled.');
|
|
378
|
-
}
|
|
379
|
-
flows = (0, blast_radius_js_1.applyBlastRadius)(flows, analysis.files, _config);
|
|
380
|
-
if (flowMappingSource === 'heuristic') {
|
|
381
|
-
flows = applyPriorityThresholds(flows, _config);
|
|
382
|
-
}
|
|
383
|
-
if (Date.now() <= deadline) {
|
|
384
|
-
if (catalog && testsByFlow) {
|
|
385
|
-
coverage = (0, tests_js_1.mapCatalogTestsToFlows)(flows, testsRoot, testsByFlow);
|
|
386
|
-
testMappingSource = 'catalog';
|
|
387
|
-
const coverageMap = new Map();
|
|
388
|
-
for (const entry of coverage) {
|
|
389
|
-
coverageMap.set(entry.flowId, entry.coveredBy);
|
|
390
|
-
}
|
|
391
|
-
gaps = computeGaps(flows, coverageMap);
|
|
392
|
-
recommendedTests = buildRecommendedTestsFromCoverage(flows, coverage);
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
const traceability = (0, traceability_js_1.mapTraceabilityToFlows)(testsRoot, _config.impact.traceability, flows);
|
|
396
|
-
warnings.push(...traceability.warnings);
|
|
397
|
-
traceabilityStats = traceability.stats;
|
|
398
|
-
if (traceability.stats.manifestFound && traceability.stats.matchedFlows > 0) {
|
|
399
|
-
coverage = traceability.coverage;
|
|
400
|
-
testMappingSource = 'traceability';
|
|
401
|
-
if (traceability.stats.coverageRatio < 0.8) {
|
|
402
|
-
const tests = (0, tests_js_1.discoverTests)(testsRoot, testPatterns.patterns);
|
|
403
|
-
if (_config.impact.aiMapping.enabled) {
|
|
404
|
-
const aiMapping = await (0, ai_mapping_js_1.mapAITestsToFlows)(_config.path, testsRoot, _config.impact.aiMapping, flows, tests);
|
|
405
|
-
warnings.push(...aiMapping.warnings);
|
|
406
|
-
if (aiMapping.used) {
|
|
407
|
-
coverage = mergeCoverageWithHeuristicFallback(coverage, aiMapping.coverage);
|
|
408
|
-
testMappingSource = 'ai';
|
|
409
|
-
}
|
|
410
|
-
else if (_config.profile === 'mattermost') {
|
|
411
|
-
warnings.push(buildMattermostFailClosedWarning('Mattermost profile requires AI mapping when traceability coverage is incomplete, but AI mapping did not produce target tests.'));
|
|
412
|
-
testMappingSource = 'ai';
|
|
413
|
-
mattermostFailClosedTargeting = true;
|
|
414
|
-
}
|
|
415
|
-
else {
|
|
416
|
-
const heuristicCoverage = (0, tests_js_1.mapTestsToFlows)(flows, tests);
|
|
417
|
-
coverage = mergeCoverageWithHeuristicFallback(coverage, heuristicCoverage);
|
|
418
|
-
warnings.push('Applied heuristic fallback for flows not covered by traceability mapping.');
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
else if (_config.profile === 'mattermost') {
|
|
422
|
-
warnings.push(buildMattermostFailClosedWarning('Mattermost profile requires AI mapping when traceability coverage is incomplete, but AI mapping is disabled.'));
|
|
423
|
-
mattermostFailClosedTargeting = true;
|
|
424
|
-
}
|
|
425
|
-
else {
|
|
426
|
-
const tests = (0, tests_js_1.discoverTests)(testsRoot, testPatterns.patterns);
|
|
427
|
-
const heuristicCoverage = (0, tests_js_1.mapTestsToFlows)(flows, tests);
|
|
428
|
-
coverage = mergeCoverageWithHeuristicFallback(coverage, heuristicCoverage);
|
|
429
|
-
warnings.push('Applied heuristic fallback for flows not covered by traceability mapping.');
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
else {
|
|
434
|
-
const tests = (0, tests_js_1.discoverTests)(testsRoot, testPatterns.patterns);
|
|
435
|
-
if (_config.impact.aiMapping.enabled) {
|
|
436
|
-
const aiMapping = await (0, ai_mapping_js_1.mapAITestsToFlows)(_config.path, testsRoot, _config.impact.aiMapping, flows, tests);
|
|
437
|
-
warnings.push(...aiMapping.warnings);
|
|
438
|
-
if (aiMapping.used) {
|
|
439
|
-
coverage = aiMapping.coverage;
|
|
440
|
-
testMappingSource = 'ai';
|
|
441
|
-
}
|
|
442
|
-
else if (_config.profile === 'mattermost') {
|
|
443
|
-
warnings.push(buildMattermostFailClosedWarning('Mattermost profile requires AI mapping because traceability evidence did not produce target tests, but AI mapping returned no valid matches.'));
|
|
444
|
-
testMappingSource = 'ai';
|
|
445
|
-
mattermostFailClosedTargeting = true;
|
|
446
|
-
}
|
|
447
|
-
else {
|
|
448
|
-
coverage = (0, tests_js_1.mapTestsToFlows)(flows, tests);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
else if (_config.profile === 'mattermost') {
|
|
452
|
-
warnings.push(buildMattermostFailClosedWarning('Mattermost profile requires traceability evidence or AI mapping to produce target tests, but AI mapping is disabled.'));
|
|
453
|
-
testMappingSource = 'traceability';
|
|
454
|
-
mattermostFailClosedTargeting = true;
|
|
455
|
-
}
|
|
456
|
-
else {
|
|
457
|
-
coverage = (0, tests_js_1.mapTestsToFlows)(flows, tests);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
const coverageMap = new Map();
|
|
461
|
-
for (const entry of coverage) {
|
|
462
|
-
coverageMap.set(entry.flowId, entry.coveredBy);
|
|
463
|
-
}
|
|
464
|
-
gaps = computeGaps(flows, coverageMap);
|
|
465
|
-
recommendedTests = buildRecommendedTestsFromCoverage(flows, coverage);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
const mattermostAdjusted = applyMattermostEvidencePolicy(_config, {
|
|
469
|
-
warnings,
|
|
470
|
-
flows,
|
|
471
|
-
coverage,
|
|
472
|
-
recommendedTests,
|
|
473
|
-
testMappingSource,
|
|
474
|
-
traceabilityStats,
|
|
475
|
-
failClosedTargeting: mattermostFailClosedTargeting,
|
|
476
|
-
});
|
|
477
|
-
coverage = mattermostAdjusted.coverage;
|
|
478
|
-
recommendedTests = mattermostAdjusted.recommendedTests;
|
|
479
|
-
testMappingSource = mattermostAdjusted.testMappingSource;
|
|
480
|
-
if (Date.now() <= deadline) {
|
|
481
|
-
const coverageMap = new Map();
|
|
482
|
-
for (const entry of coverage) {
|
|
483
|
-
coverageMap.set(entry.flowId, entry.coveredBy);
|
|
484
|
-
}
|
|
485
|
-
// Pass the full coverage array so partial gaps (tests exist but missing scenarios) are included.
|
|
486
|
-
gaps = computeGaps(flows, coverageMap, coverage);
|
|
487
|
-
}
|
|
488
|
-
if (Date.now() <= deadline) {
|
|
489
|
-
testSuggestions = (0, gap_suggestions_js_1.buildGapTestSuggestions)(testsRoot, gaps, frameworkDetection.framework, testPatterns.patterns);
|
|
490
|
-
}
|
|
491
|
-
if (Date.now() <= deadline) {
|
|
492
|
-
dataTestIds = analysis.files
|
|
493
|
-
.filter((file) => file.isUI && file.content)
|
|
494
|
-
.flatMap((file) => (0, selectors_js_1.findDataTestIdSuggestions)(file.relativePath, file.content, file.flowId));
|
|
495
|
-
}
|
|
496
|
-
if (_config.specPDF) {
|
|
497
|
-
warnings.push('Spec PDF provided but parsing is not implemented in v1.');
|
|
498
|
-
}
|
|
499
|
-
const applied = _options.apply && Date.now() <= deadline
|
|
500
|
-
? applyChanges(_config, analysis.files, dataTestIds, gaps, frameworkDetection.framework, testPatterns.patterns)
|
|
501
|
-
: undefined;
|
|
502
|
-
let pipelineSummary;
|
|
503
|
-
if (_config.pipeline.enabled && frameworkDetection.framework === 'playwright' && gaps.length > 0) {
|
|
504
|
-
pipelineSummary = (0, pipeline_js_1.runPlaywrightPipeline)(testsRoot, gaps, _config.pipeline);
|
|
505
|
-
if (pipelineSummary.warnings.length > 0) {
|
|
506
|
-
warnings.push(...pipelineSummary.warnings);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
const reportRoot = testsRoot;
|
|
510
|
-
const report = (0, report_js_1.writeReport)(reportRoot, _config, {
|
|
511
|
-
mode: 'impact',
|
|
512
|
-
runMetadata: {
|
|
513
|
-
runId,
|
|
514
|
-
startedAt: runStartedAt,
|
|
515
|
-
completedAt: new Date().toISOString(),
|
|
516
|
-
durationMs: Date.now() - runStartedTs,
|
|
517
|
-
sinceRef: _config.git.since,
|
|
518
|
-
appPath: _config.path,
|
|
519
|
-
testsRoot,
|
|
520
|
-
},
|
|
521
|
-
changedFiles,
|
|
522
|
-
flows: sortFlows(flows),
|
|
523
|
-
coverage,
|
|
524
|
-
gaps,
|
|
525
|
-
dataTestIds,
|
|
526
|
-
framework: frameworkDetection.framework,
|
|
527
|
-
testPatterns: testPatterns.patterns,
|
|
528
|
-
specPDF: _config.specPDF,
|
|
529
|
-
warnings,
|
|
530
|
-
flowCatalog: flowCatalogSource,
|
|
531
|
-
recommendedTests,
|
|
532
|
-
impactModel: {
|
|
533
|
-
schemaVersion: '1.0.0',
|
|
534
|
-
flowMapping: flowMappingSource,
|
|
535
|
-
testMapping: testMappingSource,
|
|
536
|
-
confidenceClass: classifyImpactModelConfidence(flowMappingSource, testMappingSource, dependencyGraph, traceabilityStats, warnings),
|
|
537
|
-
traceability: traceabilityStats,
|
|
538
|
-
dependencyGraph: dependencyGraph
|
|
539
|
-
? {
|
|
540
|
-
source: dependencyGraph.source,
|
|
541
|
-
enabled: _config.impact.dependencyGraph.enabled,
|
|
542
|
-
seedFiles: dependencyGraph.seedFiles.length,
|
|
543
|
-
expandedFiles: dependencyGraph.expandedFiles.length,
|
|
544
|
-
analyzedFiles: dependencyGraph.analyzedFiles,
|
|
545
|
-
analyzedEdges: dependencyGraph.analyzedEdges,
|
|
546
|
-
maxDepth: dependencyGraph.maxDepth,
|
|
547
|
-
truncated: dependencyGraph.truncated,
|
|
548
|
-
}
|
|
549
|
-
: undefined,
|
|
550
|
-
subsystemRisk: analysis.subsystemRisk.enabled ? analysis.subsystemRisk : undefined,
|
|
551
|
-
},
|
|
552
|
-
testSuggestions,
|
|
553
|
-
pipeline: pipelineSummary,
|
|
554
|
-
applied,
|
|
555
|
-
});
|
|
556
|
-
// eslint-disable-next-line no-console
|
|
557
|
-
console.log(`Impact report: ${report.markdownPath}`);
|
|
558
|
-
// eslint-disable-next-line no-console
|
|
559
|
-
console.log(`Impact data: ${report.jsonPath}`);
|
|
560
|
-
}
|
|
561
|
-
async function runGap(_config, _options) {
|
|
562
|
-
ensureAppRoot(_config.path);
|
|
563
|
-
if (_config.testsRoot) {
|
|
564
|
-
ensureAppRoot(_config.testsRoot);
|
|
565
|
-
}
|
|
566
|
-
const deadline = Date.now() + _config.timeLimitMinutes * 60 * 1000;
|
|
567
|
-
const runStartedAt = new Date().toISOString();
|
|
568
|
-
const runStartedTs = Date.now();
|
|
569
|
-
const runId = createRunId('gap');
|
|
570
|
-
const warnings = [];
|
|
571
|
-
const testsRoot = _config.testsRoot || _config.path;
|
|
572
|
-
const frameworkDetection = (0, framework_js_1.detectFramework)(testsRoot, _config.framework);
|
|
573
|
-
const testPatterns = (0, framework_js_1.resolveTestPatterns)(testsRoot, frameworkDetection, _config.testDiscovery.patterns);
|
|
574
|
-
if (frameworkDetection.framework === 'unknown' && testPatterns.patterns.length === 0) {
|
|
575
|
-
throw new Error('No framework config found. Provide testDiscovery.patterns in config or --patterns.');
|
|
576
|
-
}
|
|
577
|
-
const gitResult = (0, git_js_1.getChangedFiles)(_config.path, _config.git.since, {
|
|
578
|
-
includeUncommitted: _config.git.includeUncommitted,
|
|
579
|
-
});
|
|
580
|
-
const changedFiles = normalizeChangedFiles(_config.path, gitResult.files);
|
|
581
|
-
if (gitResult.error) {
|
|
582
|
-
warnings.push(`Git diff failed: ${gitResult.error}`);
|
|
583
|
-
}
|
|
584
|
-
const changedAppFiles = changedFiles.filter((file) => !(0, analysis_js_1.isTestFilePath)(file));
|
|
585
|
-
let analysisTargets = [...changedAppFiles];
|
|
586
|
-
if (analysisTargets.length === 0) {
|
|
587
|
-
analysisTargets = (0, analysis_js_1.scanRepositoryFlows)(_config.path, 250, _config.flowDiscovery.patterns, _config.flowDiscovery.exclude);
|
|
588
|
-
}
|
|
589
|
-
if (analysisTargets.length === 0) {
|
|
590
|
-
warnings.push('No flow candidates found. Ensure pages/screens exist or provide changed files.');
|
|
591
|
-
}
|
|
592
|
-
let dependencyGraph;
|
|
593
|
-
if (analysisTargets.length > 0 && _config.impact.dependencyGraph.enabled) {
|
|
594
|
-
dependencyGraph = (0, dependency_graph_js_1.expandByDependencyGraph)(_config.path, analysisTargets, _config.impact.dependencyGraph);
|
|
595
|
-
warnings.push(...dependencyGraph.warnings);
|
|
596
|
-
if (dependencyGraph.expandedFiles.length > 0) {
|
|
597
|
-
analysisTargets = uniquePaths([...analysisTargets, ...dependencyGraph.expandedFiles]);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
const analysis = (0, analysis_js_1.analyzeFiles)(_config.path, analysisTargets, _config);
|
|
601
|
-
warnings.push(...analysis.warnings);
|
|
602
|
-
if (Date.now() > deadline) {
|
|
603
|
-
warnings.push('Time limit exceeded after gap analysis. Skipping coverage and selector steps.');
|
|
604
|
-
}
|
|
605
|
-
let coverage = [];
|
|
606
|
-
let gaps = [];
|
|
607
|
-
let dataTestIds = [];
|
|
608
|
-
let flows = [];
|
|
609
|
-
let flowCatalogSource;
|
|
610
|
-
let recommendedTests = [];
|
|
611
|
-
let testsByFlow;
|
|
612
|
-
let testSuggestions = [];
|
|
613
|
-
const catalog = (0, flow_catalog_js_1.loadFlowCatalog)(_config);
|
|
614
|
-
let flowMappingSource = catalog ? 'catalog' : 'heuristic';
|
|
615
|
-
let testMappingSource = 'heuristic';
|
|
616
|
-
let traceabilityStats;
|
|
617
|
-
let mattermostFailClosedTargeting = false;
|
|
618
|
-
if (catalog) {
|
|
619
|
-
flowCatalogSource = catalog.source;
|
|
620
|
-
const catalogMode = changedAppFiles.length > 0 ? 'impact' : 'gap';
|
|
621
|
-
let mapping = (0, flow_mapping_js_1.mapChangesToCatalogFlows)(catalog, analysisTargets, catalogMode, _config);
|
|
622
|
-
if (catalogMode === 'impact' && mapping.flows.length === 0 && _config.impact.allowFallback) {
|
|
623
|
-
const fallbackMapping = (0, flow_mapping_js_1.mapChangesToCatalogFlows)(catalog, analysisTargets, 'gap', _config);
|
|
624
|
-
mapping = {
|
|
625
|
-
flows: fallbackMapping.flows,
|
|
626
|
-
testsByFlow: fallbackMapping.testsByFlow,
|
|
627
|
-
warnings: [
|
|
628
|
-
...mapping.warnings,
|
|
629
|
-
...fallbackMapping.warnings,
|
|
630
|
-
'No catalog flow matched changed files; applied full-catalog fallback because allowFallback=true.',
|
|
631
|
-
],
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
flows = mapping.flows;
|
|
635
|
-
testsByFlow = mapping.testsByFlow;
|
|
636
|
-
warnings.push(...mapping.warnings);
|
|
637
|
-
if (_config.profile === 'mattermost' && changedAppFiles.length > 0 && flows.length === 0) {
|
|
638
|
-
throw new Error('Mattermost profile catalog mapping returned no impacted flows. Refresh traceability or AI flow mapping before target selection.');
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
else {
|
|
642
|
-
flows = analysis.flows;
|
|
643
|
-
if (changedAppFiles.length === 0) {
|
|
644
|
-
// No app files changed (e.g. only CI config or e2e files changed).
|
|
645
|
-
// Treat as zero app impact without calling AI — the gate should pass cleanly.
|
|
646
|
-
flows = [];
|
|
647
|
-
flowMappingSource = 'ai';
|
|
648
|
-
warnings.push('No app files changed; skipping AI flow analysis.');
|
|
649
|
-
}
|
|
650
|
-
else if (_config.impact.aiFlow.enabled) {
|
|
651
|
-
const aiFlow = await (0, ai_flow_analysis_js_1.mapAIFlowsFromFiles)(_config.path, testsRoot, _config.impact.aiFlow, analysis.files, changedAppFiles);
|
|
652
|
-
warnings.push(...aiFlow.warnings);
|
|
653
|
-
if (aiFlow.used) {
|
|
654
|
-
flows = aiFlow.flows;
|
|
655
|
-
flowMappingSource = 'ai';
|
|
656
|
-
}
|
|
657
|
-
else if (aiFlow.ran) {
|
|
658
|
-
// AI ran successfully but found no user-facing flows — treat as zero impact.
|
|
659
|
-
flows = [];
|
|
660
|
-
flowMappingSource = 'ai';
|
|
661
|
-
}
|
|
662
|
-
else if (_config.impact.aiFlow.strict || _config.profile === 'mattermost') {
|
|
663
|
-
throw new Error('AI flow analysis is required but unavailable. Check Anthropic/LLM provider configuration.');
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
if (_config.profile === 'mattermost' && flowMappingSource === 'heuristic') {
|
|
668
|
-
throw new Error('Mattermost profile requires AI or catalog flow mapping; heuristic flow mapping is disabled.');
|
|
669
|
-
}
|
|
670
|
-
flows = (0, blast_radius_js_1.applyBlastRadius)(flows, analysis.files, _config);
|
|
671
|
-
if (flowMappingSource === 'heuristic') {
|
|
672
|
-
flows = applyPriorityThresholds(flows, _config);
|
|
673
|
-
}
|
|
674
|
-
if (Date.now() <= deadline) {
|
|
675
|
-
if (catalog && testsByFlow) {
|
|
676
|
-
coverage = (0, tests_js_1.mapCatalogTestsToFlows)(flows, testsRoot, testsByFlow);
|
|
677
|
-
testMappingSource = 'catalog';
|
|
678
|
-
const coverageMap = new Map();
|
|
679
|
-
for (const entry of coverage) {
|
|
680
|
-
coverageMap.set(entry.flowId, entry.coveredBy);
|
|
681
|
-
}
|
|
682
|
-
gaps = computeGaps(flows, coverageMap);
|
|
683
|
-
recommendedTests = buildRecommendedTestsFromCoverage(flows, coverage);
|
|
684
|
-
}
|
|
685
|
-
else {
|
|
686
|
-
const traceability = (0, traceability_js_1.mapTraceabilityToFlows)(testsRoot, _config.impact.traceability, flows);
|
|
687
|
-
warnings.push(...traceability.warnings);
|
|
688
|
-
traceabilityStats = traceability.stats;
|
|
689
|
-
if (traceability.stats.manifestFound && traceability.stats.matchedFlows > 0) {
|
|
690
|
-
coverage = traceability.coverage;
|
|
691
|
-
testMappingSource = 'traceability';
|
|
692
|
-
if (traceability.stats.coverageRatio < 0.8) {
|
|
693
|
-
const tests = (0, tests_js_1.discoverTests)(testsRoot, testPatterns.patterns);
|
|
694
|
-
if (_config.impact.aiMapping.enabled) {
|
|
695
|
-
const aiMapping = await (0, ai_mapping_js_1.mapAITestsToFlows)(_config.path, testsRoot, _config.impact.aiMapping, flows, tests);
|
|
696
|
-
warnings.push(...aiMapping.warnings);
|
|
697
|
-
if (aiMapping.used) {
|
|
698
|
-
coverage = mergeCoverageWithHeuristicFallback(coverage, aiMapping.coverage);
|
|
699
|
-
testMappingSource = 'ai';
|
|
700
|
-
}
|
|
701
|
-
else if (_config.profile === 'mattermost') {
|
|
702
|
-
warnings.push(buildMattermostFailClosedWarning('Mattermost profile requires AI mapping when traceability coverage is incomplete, but AI mapping did not produce target tests.'));
|
|
703
|
-
testMappingSource = 'ai';
|
|
704
|
-
mattermostFailClosedTargeting = true;
|
|
705
|
-
}
|
|
706
|
-
else {
|
|
707
|
-
const heuristicCoverage = (0, tests_js_1.mapTestsToFlows)(flows, tests);
|
|
708
|
-
coverage = mergeCoverageWithHeuristicFallback(coverage, heuristicCoverage);
|
|
709
|
-
warnings.push('Applied heuristic fallback for flows not covered by traceability mapping.');
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
else if (_config.profile === 'mattermost') {
|
|
713
|
-
warnings.push(buildMattermostFailClosedWarning('Mattermost profile requires AI mapping when traceability coverage is incomplete, but AI mapping is disabled.'));
|
|
714
|
-
mattermostFailClosedTargeting = true;
|
|
715
|
-
}
|
|
716
|
-
else {
|
|
717
|
-
const tests = (0, tests_js_1.discoverTests)(testsRoot, testPatterns.patterns);
|
|
718
|
-
const heuristicCoverage = (0, tests_js_1.mapTestsToFlows)(flows, tests);
|
|
719
|
-
coverage = mergeCoverageWithHeuristicFallback(coverage, heuristicCoverage);
|
|
720
|
-
warnings.push('Applied heuristic fallback for flows not covered by traceability mapping.');
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
else {
|
|
725
|
-
const tests = (0, tests_js_1.discoverTests)(testsRoot, testPatterns.patterns);
|
|
726
|
-
if (_config.impact.aiMapping.enabled) {
|
|
727
|
-
const aiMapping = await (0, ai_mapping_js_1.mapAITestsToFlows)(_config.path, testsRoot, _config.impact.aiMapping, flows, tests);
|
|
728
|
-
warnings.push(...aiMapping.warnings);
|
|
729
|
-
if (aiMapping.used) {
|
|
730
|
-
coverage = aiMapping.coverage;
|
|
731
|
-
testMappingSource = 'ai';
|
|
732
|
-
}
|
|
733
|
-
else if (_config.profile === 'mattermost') {
|
|
734
|
-
warnings.push(buildMattermostFailClosedWarning('Mattermost profile requires AI mapping because traceability evidence did not produce target tests, but AI mapping returned no valid matches.'));
|
|
735
|
-
testMappingSource = 'ai';
|
|
736
|
-
mattermostFailClosedTargeting = true;
|
|
737
|
-
}
|
|
738
|
-
else {
|
|
739
|
-
coverage = (0, tests_js_1.mapTestsToFlows)(flows, tests);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
else if (_config.profile === 'mattermost') {
|
|
743
|
-
warnings.push(buildMattermostFailClosedWarning('Mattermost profile requires traceability evidence or AI mapping to produce target tests, but AI mapping is disabled.'));
|
|
744
|
-
testMappingSource = 'traceability';
|
|
745
|
-
mattermostFailClosedTargeting = true;
|
|
746
|
-
}
|
|
747
|
-
else {
|
|
748
|
-
coverage = (0, tests_js_1.mapTestsToFlows)(flows, tests);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
const coverageMap = new Map();
|
|
752
|
-
for (const entry of coverage) {
|
|
753
|
-
coverageMap.set(entry.flowId, entry.coveredBy);
|
|
754
|
-
}
|
|
755
|
-
gaps = computeGaps(flows, coverageMap);
|
|
756
|
-
recommendedTests = buildRecommendedTestsFromCoverage(flows, coverage);
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
const mattermostAdjusted = applyMattermostEvidencePolicy(_config, {
|
|
760
|
-
warnings,
|
|
761
|
-
flows,
|
|
762
|
-
coverage,
|
|
763
|
-
recommendedTests,
|
|
764
|
-
testMappingSource,
|
|
765
|
-
traceabilityStats,
|
|
766
|
-
failClosedTargeting: mattermostFailClosedTargeting,
|
|
767
|
-
});
|
|
768
|
-
coverage = mattermostAdjusted.coverage;
|
|
769
|
-
recommendedTests = mattermostAdjusted.recommendedTests;
|
|
770
|
-
testMappingSource = mattermostAdjusted.testMappingSource;
|
|
771
|
-
if (Date.now() <= deadline) {
|
|
772
|
-
const coverageMap = new Map();
|
|
773
|
-
for (const entry of coverage) {
|
|
774
|
-
coverageMap.set(entry.flowId, entry.coveredBy);
|
|
775
|
-
}
|
|
776
|
-
// Pass the full coverage array so partial gaps (tests exist but missing scenarios) are included.
|
|
777
|
-
gaps = computeGaps(flows, coverageMap, coverage);
|
|
778
|
-
}
|
|
779
|
-
if (Date.now() <= deadline) {
|
|
780
|
-
testSuggestions = (0, gap_suggestions_js_1.buildGapTestSuggestions)(testsRoot, gaps, frameworkDetection.framework, testPatterns.patterns);
|
|
781
|
-
}
|
|
782
|
-
if (Date.now() <= deadline) {
|
|
783
|
-
dataTestIds = analysis.files
|
|
784
|
-
.filter((file) => file.isUI && file.content)
|
|
785
|
-
.flatMap((file) => (0, selectors_js_1.findDataTestIdSuggestions)(file.relativePath, file.content, file.flowId));
|
|
786
|
-
}
|
|
787
|
-
if (_config.specPDF) {
|
|
788
|
-
warnings.push('Spec PDF provided but parsing is not implemented in v1.');
|
|
789
|
-
}
|
|
790
|
-
const applied = _options.apply && Date.now() <= deadline
|
|
791
|
-
? applyChanges(_config, analysis.files, dataTestIds, gaps, frameworkDetection.framework, testPatterns.patterns)
|
|
792
|
-
: undefined;
|
|
793
|
-
let pipelineSummary;
|
|
794
|
-
if (_config.pipeline.enabled && frameworkDetection.framework === 'playwright' && gaps.length > 0) {
|
|
795
|
-
pipelineSummary = (0, pipeline_js_1.runPlaywrightPipeline)(testsRoot, gaps, _config.pipeline);
|
|
796
|
-
if (pipelineSummary.warnings.length > 0) {
|
|
797
|
-
warnings.push(...pipelineSummary.warnings);
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
const reportRoot = testsRoot;
|
|
801
|
-
const report = (0, report_js_1.writeReport)(reportRoot, _config, {
|
|
802
|
-
mode: 'gap',
|
|
803
|
-
runMetadata: {
|
|
804
|
-
runId,
|
|
805
|
-
startedAt: runStartedAt,
|
|
806
|
-
completedAt: new Date().toISOString(),
|
|
807
|
-
durationMs: Date.now() - runStartedTs,
|
|
808
|
-
sinceRef: _config.git.since,
|
|
809
|
-
appPath: _config.path,
|
|
810
|
-
testsRoot,
|
|
811
|
-
},
|
|
812
|
-
changedFiles,
|
|
813
|
-
flows: sortFlows(flows),
|
|
814
|
-
coverage,
|
|
815
|
-
gaps,
|
|
816
|
-
dataTestIds,
|
|
817
|
-
framework: frameworkDetection.framework,
|
|
818
|
-
testPatterns: testPatterns.patterns,
|
|
819
|
-
specPDF: _config.specPDF,
|
|
820
|
-
warnings,
|
|
821
|
-
flowCatalog: flowCatalogSource,
|
|
822
|
-
recommendedTests,
|
|
823
|
-
impactModel: {
|
|
824
|
-
schemaVersion: '1.0.0',
|
|
825
|
-
flowMapping: flowMappingSource,
|
|
826
|
-
testMapping: testMappingSource,
|
|
827
|
-
confidenceClass: classifyImpactModelConfidence(flowMappingSource, testMappingSource, dependencyGraph, traceabilityStats, warnings),
|
|
828
|
-
traceability: traceabilityStats,
|
|
829
|
-
dependencyGraph: dependencyGraph
|
|
830
|
-
? {
|
|
831
|
-
source: dependencyGraph.source,
|
|
832
|
-
enabled: _config.impact.dependencyGraph.enabled,
|
|
833
|
-
seedFiles: dependencyGraph.seedFiles.length,
|
|
834
|
-
expandedFiles: dependencyGraph.expandedFiles.length,
|
|
835
|
-
analyzedFiles: dependencyGraph.analyzedFiles,
|
|
836
|
-
analyzedEdges: dependencyGraph.analyzedEdges,
|
|
837
|
-
maxDepth: dependencyGraph.maxDepth,
|
|
838
|
-
truncated: dependencyGraph.truncated,
|
|
839
|
-
}
|
|
840
|
-
: undefined,
|
|
841
|
-
subsystemRisk: analysis.subsystemRisk.enabled ? analysis.subsystemRisk : undefined,
|
|
842
|
-
},
|
|
843
|
-
testSuggestions,
|
|
844
|
-
pipeline: pipelineSummary,
|
|
845
|
-
applied,
|
|
846
|
-
});
|
|
847
|
-
// eslint-disable-next-line no-console
|
|
848
|
-
console.log(`Gap report: ${report.markdownPath}`);
|
|
849
|
-
// eslint-disable-next-line no-console
|
|
850
|
-
console.log(`Gap data: ${report.jsonPath}`);
|
|
851
|
-
}
|
|
852
|
-
function applyChanges(config, files, dataTestIds, gaps, framework, testPatterns) {
|
|
853
|
-
const patchedFiles = [];
|
|
854
|
-
const suggestionsByFile = new Map();
|
|
855
|
-
for (const suggestion of dataTestIds) {
|
|
856
|
-
const bucket = suggestionsByFile.get(suggestion.file) || [];
|
|
857
|
-
bucket.push(suggestion);
|
|
858
|
-
suggestionsByFile.set(suggestion.file, bucket);
|
|
859
|
-
}
|
|
860
|
-
if (config.selectors.patchOnApply) {
|
|
861
|
-
for (const file of files) {
|
|
862
|
-
const suggestions = suggestionsByFile.get(file.relativePath);
|
|
863
|
-
if (!suggestions || !file.content) {
|
|
864
|
-
continue;
|
|
865
|
-
}
|
|
866
|
-
const updated = (0, selectors_js_1.applyDataTestIdSuggestions)(file.content, suggestions);
|
|
867
|
-
if (updated !== file.content) {
|
|
868
|
-
const fullPath = (0, path_1.join)(config.path, file.relativePath);
|
|
869
|
-
(0, fs_1.writeFileSync)(fullPath, updated, 'utf-8');
|
|
870
|
-
patchedFiles.push(file.relativePath);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
const fileToFlow = new Map();
|
|
875
|
-
for (const file of files) {
|
|
876
|
-
fileToFlow.set(file.relativePath, file.flowId);
|
|
877
|
-
}
|
|
878
|
-
const testIdsByFlow = new Map();
|
|
879
|
-
for (const suggestion of dataTestIds) {
|
|
880
|
-
const flowId = fileToFlow.get(suggestion.file);
|
|
881
|
-
if (!flowId) {
|
|
882
|
-
continue;
|
|
883
|
-
}
|
|
884
|
-
const bucket = testIdsByFlow.get(flowId) || [];
|
|
885
|
-
bucket.push(suggestion.testId);
|
|
886
|
-
testIdsByFlow.set(flowId, bucket);
|
|
887
|
-
}
|
|
888
|
-
let generatedTests = [];
|
|
889
|
-
let skippedTests = [];
|
|
890
|
-
if (!config.pipeline.enabled) {
|
|
891
|
-
const frameworkType = framework === 'playwright' || framework === 'cypress' || framework === 'selenium' ? framework : 'playwright';
|
|
892
|
-
const testsRoot = config.testsRoot || config.path;
|
|
893
|
-
const generated = (0, generator_js_1.generateTests)(testsRoot, gaps, frameworkType, testPatterns, testIdsByFlow);
|
|
894
|
-
generatedTests = generated.filter((entry) => entry.created).map((entry) => entry.path);
|
|
895
|
-
skippedTests = generated.filter((entry) => !entry.created).map((entry) => entry.path);
|
|
896
|
-
}
|
|
897
|
-
return { patchedFiles, generatedTests, skippedTests };
|
|
898
|
-
}
|