@yasserkhanorg/e2e-agents 0.5.15 → 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 -559
- 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 -556
- 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
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
-
// See LICENSE.txt for license information.
|
|
3
|
-
import { normalizePath } from './utils.js';
|
|
4
|
-
const INTERACTIVE_TAGS = ['button', 'input', 'select', 'textarea', 'form', 'a'];
|
|
5
|
-
const INTERACTIVE_HINT = /(onClick|onSubmit|onChange|type=['"]submit['"]|role=['"]button['"])/;
|
|
6
|
-
export function findDataTestIdSuggestions(relativePath, content, flowId) {
|
|
7
|
-
if (!content) {
|
|
8
|
-
return [];
|
|
9
|
-
}
|
|
10
|
-
const suggestions = [];
|
|
11
|
-
const lines = content.split('\n');
|
|
12
|
-
let counter = 1;
|
|
13
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
14
|
-
const line = lines[i];
|
|
15
|
-
const trimmed = line.trim();
|
|
16
|
-
if (!trimmed.startsWith('<')) {
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
if (trimmed.includes('data-testid')) {
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
const tagMatch = trimmed.match(/^<([a-z][a-z0-9-]*)\b/);
|
|
23
|
-
if (!tagMatch) {
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
const tag = tagMatch[1];
|
|
27
|
-
if (!INTERACTIVE_TAGS.includes(tag)) {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (!INTERACTIVE_HINT.test(trimmed)) {
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
const testId = `${flowId}-${tag}-${counter}`;
|
|
34
|
-
counter += 1;
|
|
35
|
-
suggestions.push({
|
|
36
|
-
file: normalizePath(relativePath),
|
|
37
|
-
line: i + 1,
|
|
38
|
-
tag,
|
|
39
|
-
testId,
|
|
40
|
-
snippet: trimmed.slice(0, 200),
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
return suggestions;
|
|
44
|
-
}
|
|
45
|
-
export function applyDataTestIdSuggestions(content, suggestions) {
|
|
46
|
-
if (suggestions.length === 0) {
|
|
47
|
-
return content;
|
|
48
|
-
}
|
|
49
|
-
const lines = content.split('\n');
|
|
50
|
-
const suggestionsByLine = new Map();
|
|
51
|
-
for (const suggestion of suggestions) {
|
|
52
|
-
const bucket = suggestionsByLine.get(suggestion.line) || [];
|
|
53
|
-
bucket.push(suggestion);
|
|
54
|
-
suggestionsByLine.set(suggestion.line, bucket);
|
|
55
|
-
}
|
|
56
|
-
for (const [lineNumber, lineSuggestions] of suggestionsByLine.entries()) {
|
|
57
|
-
const index = lineNumber - 1;
|
|
58
|
-
if (index < 0 || index >= lines.length) {
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
let line = lines[index];
|
|
62
|
-
for (const suggestion of lineSuggestions) {
|
|
63
|
-
const pattern = new RegExp(`<${suggestion.tag}(\\s|>)`);
|
|
64
|
-
if (pattern.test(line) && !line.includes('data-testid')) {
|
|
65
|
-
line = line.replace(pattern, `<${suggestion.tag} data-testid="${suggestion.testId}"$1`);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
lines[index] = line;
|
|
69
|
-
}
|
|
70
|
-
return lines.join('\n');
|
|
71
|
-
}
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
-
// See LICENSE.txt for license information.
|
|
3
|
-
import { existsSync, readFileSync } from 'fs';
|
|
4
|
-
import { matchGlob, normalizePath } from './utils.js';
|
|
5
|
-
const PRIORITY_RANK = {
|
|
6
|
-
P0: 0,
|
|
7
|
-
P1: 1,
|
|
8
|
-
P2: 2,
|
|
9
|
-
};
|
|
10
|
-
function coerceNumber(value) {
|
|
11
|
-
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
12
|
-
return value;
|
|
13
|
-
}
|
|
14
|
-
if (typeof value === 'string') {
|
|
15
|
-
const parsed = Number(value);
|
|
16
|
-
if (Number.isFinite(parsed)) {
|
|
17
|
-
return parsed;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return undefined;
|
|
21
|
-
}
|
|
22
|
-
function normalizePriority(value) {
|
|
23
|
-
if (value === 'P0' || value === 'P1' || value === 'P2') {
|
|
24
|
-
return value;
|
|
25
|
-
}
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
function parseStringArray(value) {
|
|
29
|
-
if (!Array.isArray(value)) {
|
|
30
|
-
return [];
|
|
31
|
-
}
|
|
32
|
-
return value
|
|
33
|
-
.filter((item) => typeof item === 'string')
|
|
34
|
-
.map((item) => item.trim())
|
|
35
|
-
.filter(Boolean);
|
|
36
|
-
}
|
|
37
|
-
function parsePathArray(value) {
|
|
38
|
-
return parseStringArray(value).map((item) => normalizePath(item));
|
|
39
|
-
}
|
|
40
|
-
function parseKeywords(value) {
|
|
41
|
-
if (!Array.isArray(value)) {
|
|
42
|
-
return [];
|
|
43
|
-
}
|
|
44
|
-
return Array.from(new Set(value
|
|
45
|
-
.filter((item) => typeof item === 'string')
|
|
46
|
-
.map((item) => item.trim().toLowerCase())
|
|
47
|
-
.filter(Boolean)));
|
|
48
|
-
}
|
|
49
|
-
function parseRules(rawRules, warnings) {
|
|
50
|
-
if (!Array.isArray(rawRules)) {
|
|
51
|
-
warnings.push('Subsystem risk map has no "rules" array.');
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
54
|
-
const parsed = [];
|
|
55
|
-
for (let i = 0; i < rawRules.length; i += 1) {
|
|
56
|
-
const rawRule = rawRules[i];
|
|
57
|
-
if (!rawRule || typeof rawRule !== 'object') {
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
const patterns = parsePathArray(rawRule.patterns);
|
|
61
|
-
if (patterns.length === 0) {
|
|
62
|
-
warnings.push(`Subsystem risk rule at index ${i} has no valid patterns and was skipped.`);
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
const id = typeof rawRule.id === 'string' && rawRule.id.trim()
|
|
66
|
-
? rawRule.id.trim()
|
|
67
|
-
: `rule-${i + 1}`;
|
|
68
|
-
const description = typeof rawRule.description === 'string' ? rawRule.description.trim() : undefined;
|
|
69
|
-
const reasons = parseStringArray(rawRule.reasons);
|
|
70
|
-
const keywords = parseKeywords(rawRule.keywords);
|
|
71
|
-
const scoreDelta = coerceNumber(rawRule.scoreDelta) ?? 0;
|
|
72
|
-
const priorityFloor = normalizePriority(rawRule.priorityFloor);
|
|
73
|
-
const normalizedReasons = reasons.length > 0
|
|
74
|
-
? reasons
|
|
75
|
-
: (description ? [description] : []);
|
|
76
|
-
parsed.push({
|
|
77
|
-
id,
|
|
78
|
-
description,
|
|
79
|
-
patterns,
|
|
80
|
-
scoreDelta,
|
|
81
|
-
priorityFloor,
|
|
82
|
-
reasons: normalizedReasons,
|
|
83
|
-
keywords,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
return parsed;
|
|
87
|
-
}
|
|
88
|
-
function comparePriority(a, b) {
|
|
89
|
-
return PRIORITY_RANK[a] <= PRIORITY_RANK[b] ? a : b;
|
|
90
|
-
}
|
|
91
|
-
export function loadSubsystemRiskResolver(config) {
|
|
92
|
-
const mapPath = normalizePath(config.mapPath);
|
|
93
|
-
const warnings = [];
|
|
94
|
-
if (!config.enabled) {
|
|
95
|
-
return {
|
|
96
|
-
info: {
|
|
97
|
-
source: 'map',
|
|
98
|
-
enabled: false,
|
|
99
|
-
mapPath,
|
|
100
|
-
mapFound: false,
|
|
101
|
-
rulesLoaded: 0,
|
|
102
|
-
},
|
|
103
|
-
warnings,
|
|
104
|
-
matchFile: () => [],
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
if (!existsSync(config.mapPath)) {
|
|
108
|
-
warnings.push(`Subsystem risk map file not found: ${config.mapPath}`);
|
|
109
|
-
return {
|
|
110
|
-
info: {
|
|
111
|
-
source: 'map',
|
|
112
|
-
enabled: true,
|
|
113
|
-
mapPath,
|
|
114
|
-
mapFound: false,
|
|
115
|
-
rulesLoaded: 0,
|
|
116
|
-
},
|
|
117
|
-
warnings,
|
|
118
|
-
matchFile: () => [],
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
let raw;
|
|
122
|
-
try {
|
|
123
|
-
raw = JSON.parse(readFileSync(config.mapPath, 'utf-8'));
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
warnings.push(`Subsystem risk map is invalid JSON: ${config.mapPath}`);
|
|
127
|
-
return {
|
|
128
|
-
info: {
|
|
129
|
-
source: 'map',
|
|
130
|
-
enabled: true,
|
|
131
|
-
mapPath,
|
|
132
|
-
mapFound: true,
|
|
133
|
-
rulesLoaded: 0,
|
|
134
|
-
},
|
|
135
|
-
warnings,
|
|
136
|
-
matchFile: () => [],
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
const rules = parseRules(raw.rules, warnings);
|
|
140
|
-
if (rules.length === 0) {
|
|
141
|
-
warnings.push(`Subsystem risk map loaded but no valid rules were found: ${config.mapPath}`);
|
|
142
|
-
}
|
|
143
|
-
const maxRules = Math.max(1, Math.round(config.maxRulesPerFile));
|
|
144
|
-
return {
|
|
145
|
-
info: {
|
|
146
|
-
source: 'map',
|
|
147
|
-
enabled: true,
|
|
148
|
-
mapPath,
|
|
149
|
-
mapFound: true,
|
|
150
|
-
rulesLoaded: rules.length,
|
|
151
|
-
},
|
|
152
|
-
warnings,
|
|
153
|
-
matchFile: (relativePath) => {
|
|
154
|
-
const normalizedPath = normalizePath(relativePath);
|
|
155
|
-
const matches = [];
|
|
156
|
-
for (const rule of rules) {
|
|
157
|
-
const matched = rule.patterns.some((pattern) => matchGlob(normalizedPath, pattern));
|
|
158
|
-
if (!matched) {
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
const reasons = rule.reasons.length > 0
|
|
162
|
-
? rule.reasons
|
|
163
|
-
: [`Subsystem risk rule matched: ${rule.id}`];
|
|
164
|
-
matches.push({
|
|
165
|
-
ruleId: rule.id,
|
|
166
|
-
scoreDelta: rule.scoreDelta,
|
|
167
|
-
priorityFloor: rule.priorityFloor,
|
|
168
|
-
reasons,
|
|
169
|
-
keywords: rule.keywords,
|
|
170
|
-
priorityRank: rule.priorityFloor ? PRIORITY_RANK[rule.priorityFloor] : 99,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
if (matches.length <= maxRules) {
|
|
174
|
-
return matches.map(({ priorityRank, ...rest }) => rest);
|
|
175
|
-
}
|
|
176
|
-
matches.sort((a, b) => {
|
|
177
|
-
const deltaDiff = Math.abs(b.scoreDelta) - Math.abs(a.scoreDelta);
|
|
178
|
-
if (deltaDiff !== 0) {
|
|
179
|
-
return deltaDiff;
|
|
180
|
-
}
|
|
181
|
-
const priorityDiff = a.priorityRank - b.priorityRank;
|
|
182
|
-
if (priorityDiff !== 0) {
|
|
183
|
-
return priorityDiff;
|
|
184
|
-
}
|
|
185
|
-
return a.ruleId.localeCompare(b.ruleId);
|
|
186
|
-
});
|
|
187
|
-
const capped = matches.slice(0, maxRules).map(({ priorityRank, ...rest }) => rest);
|
|
188
|
-
let floor;
|
|
189
|
-
for (const match of capped) {
|
|
190
|
-
if (match.priorityFloor) {
|
|
191
|
-
floor = floor ? comparePriority(floor, match.priorityFloor) : match.priorityFloor;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
if (floor) {
|
|
195
|
-
for (const match of capped) {
|
|
196
|
-
if (!match.priorityFloor) {
|
|
197
|
-
match.priorityFloor = floor;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return capped;
|
|
202
|
-
},
|
|
203
|
-
};
|
|
204
|
-
}
|
package/dist/esm/agent/tests.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
-
// See LICENSE.txt for license information.
|
|
3
|
-
import { existsSync } from 'fs';
|
|
4
|
-
import { globSync } from 'glob';
|
|
5
|
-
import { join } from 'path';
|
|
6
|
-
import { normalizePath, safeReadTextFile, tokenize, uniqueTokens } from './utils.js';
|
|
7
|
-
export function discoverTests(appRoot, patterns) {
|
|
8
|
-
const files = new Set();
|
|
9
|
-
for (const pattern of patterns) {
|
|
10
|
-
const matches = globSync(pattern, {
|
|
11
|
-
cwd: appRoot,
|
|
12
|
-
ignore: ['**/node_modules/**', '**/.git/**'],
|
|
13
|
-
nodir: true,
|
|
14
|
-
});
|
|
15
|
-
for (const match of matches) {
|
|
16
|
-
files.add(normalizePath(match));
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return Array.from(files).map((relativePath) => {
|
|
20
|
-
const fullPath = join(appRoot, relativePath);
|
|
21
|
-
const content = safeReadTextFile(fullPath);
|
|
22
|
-
return { path: relativePath, content };
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
function buildFlowKeywords(flow) {
|
|
26
|
-
const tokens = [];
|
|
27
|
-
tokens.push(...tokenize(flow.id));
|
|
28
|
-
tokens.push(...tokenize(flow.name));
|
|
29
|
-
tokens.push(...flow.keywords);
|
|
30
|
-
return uniqueTokens(tokens);
|
|
31
|
-
}
|
|
32
|
-
export function mapTestsToFlows(flows, tests) {
|
|
33
|
-
const coverage = [];
|
|
34
|
-
for (const flow of flows) {
|
|
35
|
-
const keywords = buildFlowKeywords(flow);
|
|
36
|
-
const matched = [];
|
|
37
|
-
let score = 0;
|
|
38
|
-
for (const test of tests) {
|
|
39
|
-
const haystack = `${test.path} ${test.content || ''}`.toLowerCase();
|
|
40
|
-
let localScore = 0;
|
|
41
|
-
for (const keyword of keywords) {
|
|
42
|
-
if (keyword && haystack.includes(keyword.toLowerCase())) {
|
|
43
|
-
localScore += 1;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (localScore > 0) {
|
|
47
|
-
matched.push(test.path);
|
|
48
|
-
score += localScore;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
coverage.push({
|
|
52
|
-
flowId: flow.id,
|
|
53
|
-
flowName: flow.name,
|
|
54
|
-
priority: flow.priority,
|
|
55
|
-
coveredBy: matched,
|
|
56
|
-
score,
|
|
57
|
-
source: 'heuristic',
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
return coverage;
|
|
61
|
-
}
|
|
62
|
-
function resolveExpectedTests(testsRoot, expectedTests) {
|
|
63
|
-
const resolved = [];
|
|
64
|
-
for (const entry of expectedTests) {
|
|
65
|
-
if (!entry) {
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
const normalized = normalizePath(entry).replace(/^\.\//, '');
|
|
69
|
-
if (normalized.startsWith('e2e-tests/playwright/')) {
|
|
70
|
-
resolved.push(normalized.slice('e2e-tests/playwright/'.length));
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
const specsIndex = normalized.indexOf('specs/');
|
|
74
|
-
if (specsIndex >= 0) {
|
|
75
|
-
resolved.push(normalized.slice(specsIndex));
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
resolved.push(normalized);
|
|
79
|
-
}
|
|
80
|
-
return Array.from(new Set(resolved));
|
|
81
|
-
}
|
|
82
|
-
export function mapCatalogTestsToFlows(flows, testsRoot, testsByFlow) {
|
|
83
|
-
return flows.map((flow) => {
|
|
84
|
-
const expectedTests = resolveExpectedTests(testsRoot, testsByFlow.get(flow.id) || []);
|
|
85
|
-
const coveredBy = [];
|
|
86
|
-
for (const expected of expectedTests) {
|
|
87
|
-
const isAbsolute = expected.startsWith('/');
|
|
88
|
-
const globTarget = isAbsolute ? expected : expected;
|
|
89
|
-
if (expected.includes('*') || expected.includes('?') || expected.includes('{')) {
|
|
90
|
-
const matches = globSync(globTarget, { cwd: isAbsolute ? undefined : testsRoot, nodir: true });
|
|
91
|
-
if (matches.length > 0) {
|
|
92
|
-
coveredBy.push(...matches.map((match) => normalizePath(match)));
|
|
93
|
-
}
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
const fullPath = isAbsolute ? expected : join(testsRoot, expected);
|
|
97
|
-
if (existsSync(fullPath)) {
|
|
98
|
-
coveredBy.push(isAbsolute ? normalizePath(expected) : expected);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
flowId: flow.id,
|
|
103
|
-
flowName: flow.name,
|
|
104
|
-
priority: flow.priority,
|
|
105
|
-
coveredBy: Array.from(new Set(coveredBy)),
|
|
106
|
-
score: coveredBy.length,
|
|
107
|
-
expectedTests,
|
|
108
|
-
source: 'catalog',
|
|
109
|
-
};
|
|
110
|
-
});
|
|
111
|
-
}
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
-
// See LICENSE.txt for license information.
|
|
3
|
-
import { existsSync, readFileSync } from 'fs';
|
|
4
|
-
import { isAbsolute, join } from 'path';
|
|
5
|
-
import { normalizePath } from './utils.js';
|
|
6
|
-
function ratio(numerator, denominator) {
|
|
7
|
-
if (denominator <= 0) {
|
|
8
|
-
return 0;
|
|
9
|
-
}
|
|
10
|
-
return Number((numerator / denominator).toFixed(4));
|
|
11
|
-
}
|
|
12
|
-
function resolveManifestPath(root, configuredPath) {
|
|
13
|
-
if (isAbsolute(configuredPath)) {
|
|
14
|
-
return configuredPath;
|
|
15
|
-
}
|
|
16
|
-
return join(root, configuredPath);
|
|
17
|
-
}
|
|
18
|
-
function safeReadManifest(path) {
|
|
19
|
-
if (!existsSync(path)) {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
try {
|
|
23
|
-
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function normalizeFiles(files) {
|
|
30
|
-
if (!files) {
|
|
31
|
-
return [];
|
|
32
|
-
}
|
|
33
|
-
return files.map((file) => normalizePath(file)).filter(Boolean);
|
|
34
|
-
}
|
|
35
|
-
function normalizeTests(tests) {
|
|
36
|
-
if (!tests) {
|
|
37
|
-
return [];
|
|
38
|
-
}
|
|
39
|
-
return tests.map((test) => normalizePath(test)).filter(Boolean);
|
|
40
|
-
}
|
|
41
|
-
function buildFileToTestsMap(manifest) {
|
|
42
|
-
const fileToTests = new Map();
|
|
43
|
-
const setMapping = (file, test) => {
|
|
44
|
-
if (!file || !test) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
const key = normalizePath(file);
|
|
48
|
-
const value = normalizePath(test);
|
|
49
|
-
if (!fileToTests.has(key)) {
|
|
50
|
-
fileToTests.set(key, new Set());
|
|
51
|
-
}
|
|
52
|
-
fileToTests.get(key)?.add(value);
|
|
53
|
-
};
|
|
54
|
-
if (manifest.tests) {
|
|
55
|
-
for (const entry of manifest.tests) {
|
|
56
|
-
const files = normalizeFiles(entry.touchedFiles);
|
|
57
|
-
for (const file of files) {
|
|
58
|
-
setMapping(file, entry.test);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (manifest.fileToTests) {
|
|
63
|
-
for (const [file, tests] of Object.entries(manifest.fileToTests)) {
|
|
64
|
-
const normalizedTests = normalizeTests(tests);
|
|
65
|
-
for (const test of normalizedTests) {
|
|
66
|
-
setMapping(file, test);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (manifest.mappings) {
|
|
71
|
-
for (const entry of manifest.mappings) {
|
|
72
|
-
const normalizedTests = normalizeTests(entry.tests);
|
|
73
|
-
for (const test of normalizedTests) {
|
|
74
|
-
setMapping(entry.file, test);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return fileToTests;
|
|
79
|
-
}
|
|
80
|
-
export function mapTraceabilityToFlows(appRoot, config, flows) {
|
|
81
|
-
const manifestPath = resolveManifestPath(appRoot, config.manifestPath);
|
|
82
|
-
const warnings = [];
|
|
83
|
-
const fallbackStats = {
|
|
84
|
-
source: 'manifest',
|
|
85
|
-
enabled: config.enabled,
|
|
86
|
-
manifestPath,
|
|
87
|
-
manifestFound: false,
|
|
88
|
-
manifestTests: 0,
|
|
89
|
-
manifestEdges: 0,
|
|
90
|
-
matchedFlows: 0,
|
|
91
|
-
totalFlows: flows.length,
|
|
92
|
-
matchedTests: 0,
|
|
93
|
-
coverageRatio: 0,
|
|
94
|
-
};
|
|
95
|
-
if (!config.enabled) {
|
|
96
|
-
return {
|
|
97
|
-
coverage: [],
|
|
98
|
-
warnings,
|
|
99
|
-
stats: fallbackStats,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
const manifest = safeReadManifest(manifestPath);
|
|
103
|
-
if (!manifest) {
|
|
104
|
-
warnings.push(`Traceability manifest not found or invalid: ${manifestPath}`);
|
|
105
|
-
return {
|
|
106
|
-
coverage: [],
|
|
107
|
-
warnings,
|
|
108
|
-
stats: fallbackStats,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
const fileToTests = buildFileToTestsMap(manifest);
|
|
112
|
-
let manifestEdges = 0;
|
|
113
|
-
for (const tests of fileToTests.values()) {
|
|
114
|
-
manifestEdges += tests.size;
|
|
115
|
-
}
|
|
116
|
-
const manifestTests = new Set();
|
|
117
|
-
const coverage = [];
|
|
118
|
-
const matchedTests = new Set();
|
|
119
|
-
let matchedFlows = 0;
|
|
120
|
-
for (const tests of fileToTests.values()) {
|
|
121
|
-
for (const test of tests) {
|
|
122
|
-
manifestTests.add(test);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
for (const flow of flows) {
|
|
126
|
-
const signalCounts = new Map();
|
|
127
|
-
const files = flow.files.map((file) => normalizePath(file));
|
|
128
|
-
for (const file of files) {
|
|
129
|
-
const tests = fileToTests.get(file);
|
|
130
|
-
if (!tests) {
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
for (const test of tests) {
|
|
134
|
-
signalCounts.set(test, (signalCounts.get(test) || 0) + 1);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
const coveredBy = Array.from(signalCounts.entries())
|
|
138
|
-
.filter(([, count]) => count >= Math.max(1, config.minSignalsPerTest))
|
|
139
|
-
.map(([test]) => test)
|
|
140
|
-
.sort();
|
|
141
|
-
const score = Array.from(signalCounts.values()).reduce((acc, value) => acc + value, 0);
|
|
142
|
-
if (coveredBy.length > 0) {
|
|
143
|
-
matchedFlows += 1;
|
|
144
|
-
for (const test of coveredBy) {
|
|
145
|
-
matchedTests.add(test);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
coverage.push({
|
|
149
|
-
flowId: flow.id,
|
|
150
|
-
flowName: flow.name,
|
|
151
|
-
priority: flow.priority,
|
|
152
|
-
coveredBy,
|
|
153
|
-
score,
|
|
154
|
-
source: 'traceability',
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
const stats = {
|
|
158
|
-
source: 'manifest',
|
|
159
|
-
enabled: config.enabled,
|
|
160
|
-
manifestPath,
|
|
161
|
-
manifestFound: true,
|
|
162
|
-
manifestTests: manifestTests.size,
|
|
163
|
-
manifestEdges,
|
|
164
|
-
matchedFlows,
|
|
165
|
-
totalFlows: flows.length,
|
|
166
|
-
matchedTests: matchedTests.size,
|
|
167
|
-
coverageRatio: ratio(matchedFlows, flows.length),
|
|
168
|
-
};
|
|
169
|
-
if (manifestEdges === 0) {
|
|
170
|
-
warnings.push(`Traceability manifest has no file-to-test mappings: ${manifestPath}`);
|
|
171
|
-
}
|
|
172
|
-
else if (stats.coverageRatio < 0.4) {
|
|
173
|
-
warnings.push(`Traceability coverage is low (${stats.matchedFlows}/${stats.totalFlows} flows mapped). Recommendations may require heuristic fallback.`);
|
|
174
|
-
}
|
|
175
|
-
return {
|
|
176
|
-
coverage,
|
|
177
|
-
warnings,
|
|
178
|
-
stats,
|
|
179
|
-
};
|
|
180
|
-
}
|