@yasserkhanorg/e2e-agents 0.3.2

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.
Files changed (221) hide show
  1. package/LICENSE +168 -0
  2. package/README.md +620 -0
  3. package/dist/agent/analysis.d.ts +62 -0
  4. package/dist/agent/analysis.d.ts.map +1 -0
  5. package/dist/agent/analysis.js +292 -0
  6. package/dist/agent/blast_radius.d.ts +4 -0
  7. package/dist/agent/blast_radius.d.ts.map +1 -0
  8. package/dist/agent/blast_radius.js +37 -0
  9. package/dist/agent/cache_utils.d.ts +38 -0
  10. package/dist/agent/cache_utils.d.ts.map +1 -0
  11. package/dist/agent/cache_utils.js +67 -0
  12. package/dist/agent/config.d.ts +148 -0
  13. package/dist/agent/config.d.ts.map +1 -0
  14. package/dist/agent/config.js +640 -0
  15. package/dist/agent/dependency_graph.d.ts +14 -0
  16. package/dist/agent/dependency_graph.d.ts.map +1 -0
  17. package/dist/agent/dependency_graph.js +227 -0
  18. package/dist/agent/feedback.d.ts +55 -0
  19. package/dist/agent/feedback.d.ts.map +1 -0
  20. package/dist/agent/feedback.js +257 -0
  21. package/dist/agent/flags.d.ts +23 -0
  22. package/dist/agent/flags.d.ts.map +1 -0
  23. package/dist/agent/flags.js +171 -0
  24. package/dist/agent/flow_catalog.d.ts +25 -0
  25. package/dist/agent/flow_catalog.d.ts.map +1 -0
  26. package/dist/agent/flow_catalog.js +106 -0
  27. package/dist/agent/flow_mapping.d.ts +10 -0
  28. package/dist/agent/flow_mapping.d.ts.map +1 -0
  29. package/dist/agent/flow_mapping.js +84 -0
  30. package/dist/agent/framework.d.ts +13 -0
  31. package/dist/agent/framework.d.ts.map +1 -0
  32. package/dist/agent/framework.js +149 -0
  33. package/dist/agent/gap_suggestions.d.ts +14 -0
  34. package/dist/agent/gap_suggestions.d.ts.map +1 -0
  35. package/dist/agent/gap_suggestions.js +101 -0
  36. package/dist/agent/generator.d.ts +10 -0
  37. package/dist/agent/generator.d.ts.map +1 -0
  38. package/dist/agent/generator.js +115 -0
  39. package/dist/agent/git.d.ts +11 -0
  40. package/dist/agent/git.d.ts.map +1 -0
  41. package/dist/agent/git.js +90 -0
  42. package/dist/agent/handoff.d.ts +22 -0
  43. package/dist/agent/handoff.d.ts.map +1 -0
  44. package/dist/agent/handoff.js +180 -0
  45. package/dist/agent/impact-analyzer.d.ts +114 -0
  46. package/dist/agent/impact-analyzer.d.ts.map +1 -0
  47. package/dist/agent/impact-analyzer.js +557 -0
  48. package/dist/agent/index.d.ts +21 -0
  49. package/dist/agent/index.d.ts.map +1 -0
  50. package/dist/agent/index.js +38 -0
  51. package/dist/agent/model-router.d.ts +57 -0
  52. package/dist/agent/model-router.d.ts.map +1 -0
  53. package/dist/agent/model-router.js +154 -0
  54. package/dist/agent/operational_insights.d.ts +41 -0
  55. package/dist/agent/operational_insights.d.ts.map +1 -0
  56. package/dist/agent/operational_insights.js +126 -0
  57. package/dist/agent/pipeline.d.ts +23 -0
  58. package/dist/agent/pipeline.d.ts.map +1 -0
  59. package/dist/agent/pipeline.js +609 -0
  60. package/dist/agent/plan.d.ts +91 -0
  61. package/dist/agent/plan.d.ts.map +1 -0
  62. package/dist/agent/plan.js +331 -0
  63. package/dist/agent/playwright_report.d.ts +8 -0
  64. package/dist/agent/playwright_report.d.ts.map +1 -0
  65. package/dist/agent/playwright_report.js +126 -0
  66. package/dist/agent/report-generator.d.ts +24 -0
  67. package/dist/agent/report-generator.d.ts.map +1 -0
  68. package/dist/agent/report-generator.js +250 -0
  69. package/dist/agent/report.d.ts +81 -0
  70. package/dist/agent/report.d.ts.map +1 -0
  71. package/dist/agent/report.js +147 -0
  72. package/dist/agent/runner.d.ts +7 -0
  73. package/dist/agent/runner.d.ts.map +1 -0
  74. package/dist/agent/runner.js +576 -0
  75. package/dist/agent/selectors.d.ts +10 -0
  76. package/dist/agent/selectors.d.ts.map +1 -0
  77. package/dist/agent/selectors.js +75 -0
  78. package/dist/agent/spec-bridge.d.ts +101 -0
  79. package/dist/agent/spec-bridge.d.ts.map +1 -0
  80. package/dist/agent/spec-bridge.js +273 -0
  81. package/dist/agent/spec-builder.d.ts +102 -0
  82. package/dist/agent/spec-builder.d.ts.map +1 -0
  83. package/dist/agent/spec-builder.js +273 -0
  84. package/dist/agent/subsystem_risk.d.ts +23 -0
  85. package/dist/agent/subsystem_risk.d.ts.map +1 -0
  86. package/dist/agent/subsystem_risk.js +207 -0
  87. package/dist/agent/telemetry.d.ts +84 -0
  88. package/dist/agent/telemetry.d.ts.map +1 -0
  89. package/dist/agent/telemetry.js +220 -0
  90. package/dist/agent/test_path.d.ts +2 -0
  91. package/dist/agent/test_path.d.ts.map +1 -0
  92. package/dist/agent/test_path.js +23 -0
  93. package/dist/agent/tests.d.ts +18 -0
  94. package/dist/agent/tests.d.ts.map +1 -0
  95. package/dist/agent/tests.js +106 -0
  96. package/dist/agent/traceability.d.ts +22 -0
  97. package/dist/agent/traceability.d.ts.map +1 -0
  98. package/dist/agent/traceability.js +183 -0
  99. package/dist/agent/traceability_capture.d.ts +18 -0
  100. package/dist/agent/traceability_capture.d.ts.map +1 -0
  101. package/dist/agent/traceability_capture.js +313 -0
  102. package/dist/agent/traceability_ingest.d.ts +21 -0
  103. package/dist/agent/traceability_ingest.d.ts.map +1 -0
  104. package/dist/agent/traceability_ingest.js +237 -0
  105. package/dist/agent/utils.d.ts +13 -0
  106. package/dist/agent/utils.d.ts.map +1 -0
  107. package/dist/agent/utils.js +152 -0
  108. package/dist/agent/validators/selector-validator.d.ts +74 -0
  109. package/dist/agent/validators/selector-validator.d.ts.map +1 -0
  110. package/dist/agent/validators/selector-validator.js +165 -0
  111. package/dist/anthropic_provider.d.ts +65 -0
  112. package/dist/anthropic_provider.d.ts.map +1 -0
  113. package/dist/anthropic_provider.js +332 -0
  114. package/dist/api.d.ts +48 -0
  115. package/dist/api.d.ts.map +1 -0
  116. package/dist/api.js +113 -0
  117. package/dist/base_provider.d.ts +53 -0
  118. package/dist/base_provider.d.ts.map +1 -0
  119. package/dist/base_provider.js +81 -0
  120. package/dist/cli.d.ts +3 -0
  121. package/dist/cli.d.ts.map +1 -0
  122. package/dist/cli.js +843 -0
  123. package/dist/custom_provider.d.ts +20 -0
  124. package/dist/custom_provider.d.ts.map +1 -0
  125. package/dist/custom_provider.js +276 -0
  126. package/dist/e2e-test-gen/index.d.ts +51 -0
  127. package/dist/e2e-test-gen/index.d.ts.map +1 -0
  128. package/dist/e2e-test-gen/index.js +57 -0
  129. package/dist/e2e-test-gen/spec_parser.d.ts +142 -0
  130. package/dist/e2e-test-gen/spec_parser.d.ts.map +1 -0
  131. package/dist/e2e-test-gen/spec_parser.js +786 -0
  132. package/dist/e2e-test-gen/types.d.ts +185 -0
  133. package/dist/e2e-test-gen/types.d.ts.map +1 -0
  134. package/dist/e2e-test-gen/types.js +4 -0
  135. package/dist/esm/agent/analysis.js +287 -0
  136. package/dist/esm/agent/blast_radius.js +34 -0
  137. package/dist/esm/agent/cache_utils.js +63 -0
  138. package/dist/esm/agent/config.js +637 -0
  139. package/dist/esm/agent/dependency_graph.js +224 -0
  140. package/dist/esm/agent/feedback.js +253 -0
  141. package/dist/esm/agent/flags.js +160 -0
  142. package/dist/esm/agent/flow_catalog.js +103 -0
  143. package/dist/esm/agent/flow_mapping.js +81 -0
  144. package/dist/esm/agent/framework.js +145 -0
  145. package/dist/esm/agent/gap_suggestions.js +98 -0
  146. package/dist/esm/agent/generator.js +112 -0
  147. package/dist/esm/agent/git.js +87 -0
  148. package/dist/esm/agent/handoff.js +177 -0
  149. package/dist/esm/agent/impact-analyzer.js +548 -0
  150. package/dist/esm/agent/index.js +22 -0
  151. package/dist/esm/agent/model-router.js +150 -0
  152. package/dist/esm/agent/operational_insights.js +123 -0
  153. package/dist/esm/agent/pipeline.js +605 -0
  154. package/dist/esm/agent/plan.js +324 -0
  155. package/dist/esm/agent/playwright_report.js +123 -0
  156. package/dist/esm/agent/report-generator.js +247 -0
  157. package/dist/esm/agent/report.js +144 -0
  158. package/dist/esm/agent/runner.js +572 -0
  159. package/dist/esm/agent/selectors.js +71 -0
  160. package/dist/esm/agent/spec-bridge.js +267 -0
  161. package/dist/esm/agent/spec-builder.js +267 -0
  162. package/dist/esm/agent/subsystem_risk.js +204 -0
  163. package/dist/esm/agent/telemetry.js +216 -0
  164. package/dist/esm/agent/test_path.js +20 -0
  165. package/dist/esm/agent/tests.js +101 -0
  166. package/dist/esm/agent/traceability.js +180 -0
  167. package/dist/esm/agent/traceability_capture.js +310 -0
  168. package/dist/esm/agent/traceability_ingest.js +234 -0
  169. package/dist/esm/agent/utils.js +138 -0
  170. package/dist/esm/agent/validators/selector-validator.js +160 -0
  171. package/dist/esm/anthropic_provider.js +324 -0
  172. package/dist/esm/api.js +105 -0
  173. package/dist/esm/base_provider.js +77 -0
  174. package/dist/esm/cli.js +841 -0
  175. package/dist/esm/custom_provider.js +272 -0
  176. package/dist/esm/e2e-test-gen/index.js +50 -0
  177. package/dist/esm/e2e-test-gen/spec_parser.js +782 -0
  178. package/dist/esm/e2e-test-gen/types.js +3 -0
  179. package/dist/esm/index.js +16 -0
  180. package/dist/esm/logger.js +89 -0
  181. package/dist/esm/mcp-server.js +465 -0
  182. package/dist/esm/ollama_provider.js +300 -0
  183. package/dist/esm/openai_provider.js +242 -0
  184. package/dist/esm/package.json +3 -0
  185. package/dist/esm/plan-and-test-constants.js +126 -0
  186. package/dist/esm/provider_factory.js +336 -0
  187. package/dist/esm/provider_interface.js +23 -0
  188. package/dist/esm/provider_utils.js +96 -0
  189. package/dist/index.d.ts +31 -0
  190. package/dist/index.d.ts.map +1 -0
  191. package/dist/index.js +41 -0
  192. package/dist/logger.d.ts +23 -0
  193. package/dist/logger.d.ts.map +1 -0
  194. package/dist/logger.js +93 -0
  195. package/dist/mcp-server.d.ts +35 -0
  196. package/dist/mcp-server.d.ts.map +1 -0
  197. package/dist/mcp-server.js +469 -0
  198. package/dist/ollama_provider.d.ts +65 -0
  199. package/dist/ollama_provider.d.ts.map +1 -0
  200. package/dist/ollama_provider.js +308 -0
  201. package/dist/openai_provider.d.ts +23 -0
  202. package/dist/openai_provider.d.ts.map +1 -0
  203. package/dist/openai_provider.js +250 -0
  204. package/dist/plan-and-test-constants.d.ts +110 -0
  205. package/dist/plan-and-test-constants.d.ts.map +1 -0
  206. package/dist/plan-and-test-constants.js +132 -0
  207. package/dist/provider_factory.d.ts +99 -0
  208. package/dist/provider_factory.d.ts.map +1 -0
  209. package/dist/provider_factory.js +341 -0
  210. package/dist/provider_interface.d.ts +358 -0
  211. package/dist/provider_interface.d.ts.map +1 -0
  212. package/dist/provider_interface.js +28 -0
  213. package/dist/provider_utils.d.ts +39 -0
  214. package/dist/provider_utils.d.ts.map +1 -0
  215. package/dist/provider_utils.js +103 -0
  216. package/package.json +101 -0
  217. package/schemas/gap.schema.json +18 -0
  218. package/schemas/impact.schema.json +418 -0
  219. package/schemas/plan.schema.json +285 -0
  220. package/schemas/subsystem-risk-map.schema.json +62 -0
  221. package/schemas/traceability-input.schema.json +122 -0
@@ -0,0 +1,224 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { globSync } from 'glob';
4
+ import { dirname, join, normalize } from 'path';
5
+ import { normalizePath, safeReadTextFile } from './utils.js';
6
+ const IMPORT_REGEXES = [
7
+ /import\s+[\s\S]*?\s+from\s+['"]([^'"]+)['"]/g,
8
+ /import\s+['"]([^'"]+)['"]/g,
9
+ /export\s+[\s\S]*?\s+from\s+['"]([^'"]+)['"]/g,
10
+ /require\(\s*['"]([^'"]+)['"]\s*\)/g,
11
+ /import\(\s*['"]([^'"]+)['"]\s*\)/g,
12
+ ];
13
+ const RESOLVABLE_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs'];
14
+ function extractImportSpecifiers(content) {
15
+ const imports = [];
16
+ for (const regex of IMPORT_REGEXES) {
17
+ regex.lastIndex = 0;
18
+ let match = regex.exec(content);
19
+ while (match) {
20
+ const specifier = match[1];
21
+ if (specifier) {
22
+ imports.push(specifier);
23
+ }
24
+ match = regex.exec(content);
25
+ }
26
+ }
27
+ return Array.from(new Set(imports));
28
+ }
29
+ function listCandidateFiles(appRoot, cfg) {
30
+ const files = new Set();
31
+ for (const pattern of cfg.filePatterns) {
32
+ const matches = globSync(pattern, {
33
+ cwd: appRoot,
34
+ nodir: true,
35
+ ignore: cfg.excludePatterns,
36
+ });
37
+ for (const match of matches) {
38
+ files.add(normalizePath(match));
39
+ }
40
+ }
41
+ return Array.from(files);
42
+ }
43
+ function resolveWithCandidates(candidateBase, fileSet) {
44
+ if (fileSet.has(candidateBase)) {
45
+ return candidateBase;
46
+ }
47
+ for (const ext of RESOLVABLE_EXTENSIONS) {
48
+ const withExt = `${candidateBase}.${ext}`;
49
+ if (fileSet.has(withExt)) {
50
+ return withExt;
51
+ }
52
+ }
53
+ for (const ext of RESOLVABLE_EXTENSIONS) {
54
+ const indexPath = `${candidateBase}/index.${ext}`;
55
+ if (fileSet.has(indexPath)) {
56
+ return indexPath;
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+ function expandAliasTarget(pattern, target, specifier) {
62
+ const normalizedPattern = normalizePath(pattern);
63
+ const normalizedTarget = normalizePath(target);
64
+ const normalizedSpecifier = normalizePath(specifier);
65
+ if (normalizedPattern.endsWith('/*')) {
66
+ const prefix = normalizedPattern.slice(0, -2);
67
+ if (normalizedSpecifier === prefix || normalizedSpecifier.startsWith(`${prefix}/`)) {
68
+ const suffix = normalizedSpecifier === prefix ? '' : normalizedSpecifier.slice(prefix.length + 1);
69
+ if (normalizedTarget.endsWith('/*')) {
70
+ const targetPrefix = normalizedTarget.slice(0, -2);
71
+ return suffix ? normalizePath(`${targetPrefix}/${suffix}`) : normalizePath(targetPrefix);
72
+ }
73
+ return normalizePath(normalizedTarget);
74
+ }
75
+ return null;
76
+ }
77
+ if (normalizedPattern === normalizedSpecifier) {
78
+ if (normalizedTarget.endsWith('/*')) {
79
+ return normalizePath(normalizedTarget.slice(0, -2));
80
+ }
81
+ return normalizePath(normalizedTarget);
82
+ }
83
+ return null;
84
+ }
85
+ function resolvePathAliasImport(specifier, fileSet, cfg) {
86
+ for (const [pattern, targets] of Object.entries(cfg.pathAliases)) {
87
+ for (const target of targets) {
88
+ const aliasPath = expandAliasTarget(pattern, target, specifier);
89
+ if (!aliasPath) {
90
+ continue;
91
+ }
92
+ const resolved = resolveWithCandidates(aliasPath, fileSet);
93
+ if (resolved) {
94
+ return resolved;
95
+ }
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+ function resolveImport(fromFile, specifier, fileSet, cfg) {
101
+ if (specifier.startsWith('.')) {
102
+ const fromDir = dirname(fromFile);
103
+ const relativeCandidate = normalizePath(normalize(join(fromDir, specifier)));
104
+ return resolveWithCandidates(relativeCandidate, fileSet);
105
+ }
106
+ const aliasResolved = resolvePathAliasImport(specifier, fileSet, cfg);
107
+ if (aliasResolved) {
108
+ return aliasResolved;
109
+ }
110
+ for (const root of cfg.aliasRoots) {
111
+ const rootedCandidate = normalizePath(normalize(join(root, specifier)));
112
+ const resolved = resolveWithCandidates(rootedCandidate, fileSet);
113
+ if (resolved) {
114
+ return resolved;
115
+ }
116
+ }
117
+ const directCandidate = normalizePath(specifier);
118
+ return resolveWithCandidates(directCandidate, fileSet);
119
+ }
120
+ export function expandByDependencyGraph(appRoot, changedFiles, cfg) {
121
+ const warnings = [];
122
+ if (!cfg.enabled) {
123
+ return {
124
+ source: 'static-dependency-graph',
125
+ seedFiles: [],
126
+ impactedFiles: [],
127
+ expandedFiles: [],
128
+ analyzedFiles: 0,
129
+ analyzedEdges: 0,
130
+ maxDepth: 0,
131
+ truncated: false,
132
+ warnings,
133
+ };
134
+ }
135
+ const candidates = listCandidateFiles(appRoot, cfg);
136
+ const fileSet = new Set(candidates);
137
+ if (candidates.length === 0) {
138
+ warnings.push('Dependency graph found no candidate source files.');
139
+ }
140
+ const reverse = new Map();
141
+ let analyzedEdges = 0;
142
+ for (const file of candidates) {
143
+ const fullPath = join(appRoot, file);
144
+ const content = safeReadTextFile(fullPath);
145
+ if (!content) {
146
+ continue;
147
+ }
148
+ const imports = extractImportSpecifiers(content);
149
+ for (const specifier of imports) {
150
+ const resolved = resolveImport(file, specifier, fileSet, cfg);
151
+ if (!resolved) {
152
+ continue;
153
+ }
154
+ if (!reverse.has(resolved)) {
155
+ reverse.set(resolved, new Set());
156
+ }
157
+ reverse.get(resolved)?.add(file);
158
+ analyzedEdges += 1;
159
+ }
160
+ }
161
+ const seeds = Array.from(new Set(changedFiles
162
+ .map((file) => normalizePath(file))
163
+ .filter((file) => fileSet.has(file))));
164
+ if (seeds.length === 0) {
165
+ warnings.push('No changed files were found in dependency graph candidates.');
166
+ return {
167
+ source: 'static-dependency-graph',
168
+ seedFiles: [],
169
+ impactedFiles: [],
170
+ expandedFiles: [],
171
+ analyzedFiles: candidates.length,
172
+ analyzedEdges,
173
+ maxDepth: cfg.maxDepth,
174
+ truncated: false,
175
+ warnings,
176
+ };
177
+ }
178
+ const impacted = new Set(seeds);
179
+ const queue = seeds.map((file) => ({ file, depth: 0 }));
180
+ let truncated = false;
181
+ while (queue.length > 0) {
182
+ const next = queue.shift();
183
+ if (!next) {
184
+ continue;
185
+ }
186
+ if (next.depth >= cfg.maxDepth) {
187
+ continue;
188
+ }
189
+ const dependents = reverse.get(next.file);
190
+ if (!dependents) {
191
+ continue;
192
+ }
193
+ for (const dependent of dependents) {
194
+ if (impacted.has(dependent)) {
195
+ continue;
196
+ }
197
+ if (impacted.size - seeds.length >= cfg.maxExpandedFiles) {
198
+ truncated = true;
199
+ break;
200
+ }
201
+ impacted.add(dependent);
202
+ queue.push({ file: dependent, depth: next.depth + 1 });
203
+ }
204
+ if (truncated) {
205
+ break;
206
+ }
207
+ }
208
+ const impactedFiles = Array.from(impacted);
209
+ const expandedFiles = impactedFiles.filter((file) => !seeds.includes(file));
210
+ if (truncated) {
211
+ warnings.push(`Dependency expansion was truncated at maxExpandedFiles=${cfg.maxExpandedFiles}.`);
212
+ }
213
+ return {
214
+ source: 'static-dependency-graph',
215
+ seedFiles: seeds,
216
+ impactedFiles,
217
+ expandedFiles,
218
+ analyzedFiles: candidates.length,
219
+ analyzedEdges,
220
+ maxDepth: cfg.maxDepth,
221
+ truncated,
222
+ warnings,
223
+ };
224
+ }
@@ -0,0 +1,253 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { inferSubsystemFromTestPath } from './test_path.js';
6
+ function readJson(path) {
7
+ if (!existsSync(path)) {
8
+ return null;
9
+ }
10
+ try {
11
+ return JSON.parse(readFileSync(path, 'utf-8'));
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ function normalizeTestName(test) {
18
+ return test.replace(/ \(flags:.*\)$/, '').trim();
19
+ }
20
+ function asSet(values) {
21
+ return new Set(values.map(normalizeTestName).filter(Boolean));
22
+ }
23
+ function ratio(numerator, denominator) {
24
+ if (denominator <= 0) {
25
+ return 0;
26
+ }
27
+ return Number((numerator / denominator).toFixed(4));
28
+ }
29
+ function subsystemForTest(test) {
30
+ return inferSubsystemFromTestPath(test);
31
+ }
32
+ function parseTimestamp(value) {
33
+ const time = Date.parse(value);
34
+ if (Number.isNaN(time)) {
35
+ return null;
36
+ }
37
+ return time;
38
+ }
39
+ function filterRecent(entries, days) {
40
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
41
+ return entries.filter((entry) => {
42
+ const time = parseTimestamp(entry.timestamp);
43
+ return time !== null && time >= cutoff;
44
+ });
45
+ }
46
+ function aggregateMetrics(entries) {
47
+ let truePositives = 0;
48
+ let recommendedTotal = 0;
49
+ let failuresTotal = 0;
50
+ let escapedTotal = 0;
51
+ for (const entry of entries) {
52
+ const recommended = asSet(entry.recommendedTests || []);
53
+ const failed = asSet(entry.failedTests || []);
54
+ const escaped = asSet(entry.escapedFailures || []);
55
+ const tp = Array.from(recommended).filter((test) => failed.has(test)).length;
56
+ truePositives += tp;
57
+ recommendedTotal += recommended.size;
58
+ failuresTotal += failed.size;
59
+ escapedTotal += escaped.size;
60
+ }
61
+ return {
62
+ precision: ratio(truePositives, recommendedTotal),
63
+ recall: ratio(truePositives, failuresTotal),
64
+ falseNegativeRate: ratio(escapedTotal, failuresTotal + escapedTotal),
65
+ samples: entries.length,
66
+ };
67
+ }
68
+ function aggregate(entries) {
69
+ const subsystemAcc = new Map();
70
+ for (const entry of entries) {
71
+ const recommended = asSet(entry.recommendedTests || []);
72
+ const failed = asSet(entry.failedTests || []);
73
+ const escaped = asSet(entry.escapedFailures || []);
74
+ const allSubsystems = new Set([
75
+ ...Array.from(recommended).map(subsystemForTest),
76
+ ...Array.from(failed).map(subsystemForTest),
77
+ ...Array.from(escaped).map(subsystemForTest),
78
+ ]);
79
+ for (const subsystem of allSubsystems) {
80
+ const bucket = subsystemAcc.get(subsystem) || { entries: [] };
81
+ bucket.entries.push({
82
+ ...entry,
83
+ recommendedTests: Array.from(recommended).filter((test) => subsystemForTest(test) === subsystem),
84
+ executedTests: (entry.executedTests || []).filter((test) => subsystemForTest(test) === subsystem),
85
+ failedTests: Array.from(failed).filter((test) => subsystemForTest(test) === subsystem),
86
+ escapedFailures: Array.from(escaped).filter((test) => subsystemForTest(test) === subsystem),
87
+ });
88
+ subsystemAcc.set(subsystem, bucket);
89
+ }
90
+ }
91
+ const bySubsystem = {};
92
+ for (const [subsystem, bucket] of subsystemAcc.entries()) {
93
+ const all = aggregateMetrics(bucket.entries);
94
+ const recent7d = aggregateMetrics(filterRecent(bucket.entries, 7));
95
+ const recent30d = aggregateMetrics(filterRecent(bucket.entries, 30));
96
+ bySubsystem[subsystem] = {
97
+ precision: all.precision,
98
+ recall: all.recall,
99
+ falseNegativeRate: all.falseNegativeRate,
100
+ samples: all.samples,
101
+ recent7d,
102
+ recent30d,
103
+ };
104
+ }
105
+ const overall = aggregateMetrics(entries);
106
+ const recent7d = aggregateMetrics(filterRecent(entries, 7));
107
+ const recent30d = aggregateMetrics(filterRecent(entries, 30));
108
+ return {
109
+ schemaVersion: '1.1.0',
110
+ generatedAt: new Date().toISOString(),
111
+ samples: entries.length,
112
+ overall,
113
+ recent7d,
114
+ recent30d,
115
+ bySubsystem,
116
+ };
117
+ }
118
+ function rateFor(runs, failed) {
119
+ return runs > 0 ? Number((failed / runs).toFixed(4)) : 0;
120
+ }
121
+ function trendFor(rate7d, rate30d) {
122
+ if (rate7d - rate30d >= 0.08) {
123
+ return 'up';
124
+ }
125
+ if (rate30d - rate7d >= 0.08) {
126
+ return 'down';
127
+ }
128
+ return 'stable';
129
+ }
130
+ function loadOwners(appRoot) {
131
+ const path = join(appRoot, '.e2e-ai-agents', 'subsystem-owners.json');
132
+ const manifest = readJson(path);
133
+ if (!manifest) {
134
+ return {};
135
+ }
136
+ if (manifest.ownersBySubsystem) {
137
+ return manifest.ownersBySubsystem;
138
+ }
139
+ if (manifest.subsystems) {
140
+ return manifest.subsystems;
141
+ }
142
+ return {};
143
+ }
144
+ function daysSince(value) {
145
+ if (!value) {
146
+ return null;
147
+ }
148
+ const time = parseTimestamp(value);
149
+ if (time === null) {
150
+ return null;
151
+ }
152
+ return (Date.now() - time) / (24 * 60 * 60 * 1000);
153
+ }
154
+ function aggregateFlaky(entries, appRoot) {
155
+ const acc = new Map();
156
+ const cutoff7 = Date.now() - 7 * 24 * 60 * 60 * 1000;
157
+ const cutoff30 = Date.now() - 30 * 24 * 60 * 60 * 1000;
158
+ const ownersBySubsystem = loadOwners(appRoot);
159
+ for (const entry of entries) {
160
+ const executed = asSet(entry.executedTests || []);
161
+ const failed = asSet(entry.failedTests || []);
162
+ const time = parseTimestamp(entry.timestamp);
163
+ for (const test of executed) {
164
+ const bucket = acc.get(test) || {
165
+ runs: 0,
166
+ failed: 0,
167
+ runs7d: 0,
168
+ failed7d: 0,
169
+ runs30d: 0,
170
+ failed30d: 0,
171
+ lastFailureAt: undefined,
172
+ };
173
+ bucket.runs += 1;
174
+ if (time !== null && time >= cutoff7) {
175
+ bucket.runs7d += 1;
176
+ }
177
+ if (time !== null && time >= cutoff30) {
178
+ bucket.runs30d += 1;
179
+ }
180
+ if (failed.has(test)) {
181
+ bucket.failed += 1;
182
+ bucket.lastFailureAt = entry.timestamp;
183
+ if (time !== null && time >= cutoff7) {
184
+ bucket.failed7d += 1;
185
+ }
186
+ if (time !== null && time >= cutoff30) {
187
+ bucket.failed30d += 1;
188
+ }
189
+ }
190
+ acc.set(test, bucket);
191
+ }
192
+ }
193
+ const tests = Array.from(acc.entries())
194
+ .map(([test, bucket]) => {
195
+ const flakeRate = rateFor(bucket.runs, bucket.failed);
196
+ const flakeRate7d = rateFor(bucket.runs7d, bucket.failed7d);
197
+ const flakeRate30d = rateFor(bucket.runs30d, bucket.failed30d);
198
+ const trend = trendFor(flakeRate7d, flakeRate30d);
199
+ const quarantine = (bucket.runs30d >= 5 && flakeRate30d >= 0.35) || (bucket.runs >= 8 && flakeRate >= 0.4);
200
+ const daysFromLastFailure = daysSince(bucket.lastFailureAt);
201
+ const quarantineState = quarantine
202
+ ? (daysFromLastFailure !== null && daysFromLastFailure >= 14 && flakeRate7d <= 0.05 ? 'retire-candidate' : 'active')
203
+ : 'none';
204
+ const subsystem = subsystemForTest(test);
205
+ const owners = ownersBySubsystem[subsystem] || [];
206
+ return {
207
+ test,
208
+ subsystem,
209
+ owners,
210
+ flakeRate,
211
+ flakeRate7d,
212
+ flakeRate30d,
213
+ trend,
214
+ quarantine,
215
+ quarantineState,
216
+ lastFailureAt: bucket.lastFailureAt,
217
+ samples: bucket.runs,
218
+ samples7d: bucket.runs7d,
219
+ samples30d: bucket.runs30d,
220
+ };
221
+ })
222
+ .filter((entry) => entry.flakeRate > 0)
223
+ .sort((a, b) => (b.flakeRate30d || b.flakeRate) - (a.flakeRate30d || a.flakeRate));
224
+ return {
225
+ schemaVersion: '1.1.0',
226
+ generatedAt: new Date().toISOString(),
227
+ tests,
228
+ };
229
+ }
230
+ export function appendFeedbackAndRecompute(appRoot, input) {
231
+ const baseDir = join(appRoot, '.e2e-ai-agents');
232
+ mkdirSync(baseDir, { recursive: true });
233
+ const feedbackPath = join(baseDir, 'feedback.json');
234
+ const existing = readJson(feedbackPath) || { schemaVersion: '1.0.0', entries: [] };
235
+ existing.entries.push({
236
+ ...input,
237
+ recommendedTests: input.recommendedTests || [],
238
+ executedTests: input.executedTests || [],
239
+ failedTests: input.failedTests || [],
240
+ escapedFailures: input.escapedFailures || [],
241
+ });
242
+ writeFileSync(feedbackPath, JSON.stringify(existing, null, 2), 'utf-8');
243
+ const calibration = aggregate(existing.entries);
244
+ const calibrationPath = join(baseDir, 'calibration.json');
245
+ writeFileSync(calibrationPath, JSON.stringify(calibration, null, 2), 'utf-8');
246
+ const flaky = aggregateFlaky(existing.entries, appRoot);
247
+ const flakyPath = join(baseDir, 'flaky-tests.json');
248
+ writeFileSync(flakyPath, JSON.stringify(flaky, null, 2), 'utf-8');
249
+ return { feedbackPath, calibrationPath, calibration };
250
+ }
251
+ export function readCalibration(appRoot) {
252
+ return readJson(join(appRoot, '.e2e-ai-agents', 'calibration.json'));
253
+ }
@@ -0,0 +1,160 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ const ROLE_ORDER = [
4
+ 'system_admin',
5
+ 'team_admin',
6
+ 'channel_admin',
7
+ 'member',
8
+ 'guest',
9
+ 'deactivated',
10
+ ];
11
+ const FEATURE_FLAG_REGEX = /\bFeatureFlags?\.(\w+)\b/g;
12
+ const FEATURE_FLAG_STRING_REGEX = /\b(?:isFeatureEnabled|getFeatureFlag|featureFlag)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
13
+ const SERVICE_SETTINGS_REGEX = /\bServiceSettings\.(\w+)\b/g;
14
+ const TEST_GATE_REGEX = /\bskipIfFeatureFlagNotSet\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
15
+ const ROLE_ALIASES = {
16
+ 'system admin': 'system_admin',
17
+ 'system_admin': 'system_admin',
18
+ sysadmin: 'system_admin',
19
+ 'team admin': 'team_admin',
20
+ 'team_admin': 'team_admin',
21
+ 'channel admin': 'channel_admin',
22
+ 'channel_admin': 'channel_admin',
23
+ member: 'member',
24
+ members: 'member',
25
+ guest: 'guest',
26
+ guests: 'guest',
27
+ deactivated: 'deactivated',
28
+ inactive: 'deactivated',
29
+ disabled: 'deactivated',
30
+ };
31
+ export function normalizeRole(role) {
32
+ const key = role.trim().toLowerCase();
33
+ return ROLE_ALIASES[key] ?? null;
34
+ }
35
+ export function normalizeRoles(roles, fallback) {
36
+ const normalized = roles
37
+ .map((role) => normalizeRole(role))
38
+ .filter((role) => Boolean(role));
39
+ const combined = normalized.length > 0 ? normalized : fallback;
40
+ const unique = new Set(combined);
41
+ return ROLE_ORDER.filter((role) => unique.has(role));
42
+ }
43
+ export function normalizeFlagSource(source) {
44
+ if (!source) {
45
+ return 'featureFlag';
46
+ }
47
+ const value = source.trim().toLowerCase();
48
+ if (['feature', 'featureflag', 'feature_flag', 'flag'].includes(value)) {
49
+ return 'featureFlag';
50
+ }
51
+ if (['config', 'service', 'servicesettings', 'server'].includes(value)) {
52
+ return 'configFlag';
53
+ }
54
+ if (['test', 'gate', 'testgate'].includes(value)) {
55
+ return 'testGate';
56
+ }
57
+ return 'featureFlag';
58
+ }
59
+ export function normalizeFlagState(value, fallback) {
60
+ if (!value) {
61
+ return fallback;
62
+ }
63
+ const lowered = value.trim().toLowerCase();
64
+ if (lowered === 'on' || lowered === 'off' || lowered === 'unknown') {
65
+ return lowered;
66
+ }
67
+ return fallback;
68
+ }
69
+ export function mergeFlags(flags, defaultState) {
70
+ const map = new Map();
71
+ for (const flag of flags) {
72
+ const key = `${flag.source}:${flag.name.toLowerCase()}`;
73
+ if (!map.has(key)) {
74
+ map.set(key, {
75
+ ...flag,
76
+ defaultState: flag.defaultState ?? defaultState,
77
+ });
78
+ }
79
+ }
80
+ return Array.from(map.values());
81
+ }
82
+ export function extractFlagHits(content, config) {
83
+ if (!content) {
84
+ return [];
85
+ }
86
+ const hits = [];
87
+ const defaultState = config.flags.defaultState;
88
+ for (const match of content.matchAll(FEATURE_FLAG_REGEX)) {
89
+ if (match[1]) {
90
+ hits.push({ name: match[1], source: 'featureFlag', defaultState });
91
+ }
92
+ }
93
+ for (const match of content.matchAll(FEATURE_FLAG_STRING_REGEX)) {
94
+ if (match[1]) {
95
+ hits.push({ name: match[1], source: 'featureFlag', defaultState });
96
+ }
97
+ }
98
+ for (const match of content.matchAll(SERVICE_SETTINGS_REGEX)) {
99
+ if (match[1]) {
100
+ hits.push({ name: match[1], source: 'configFlag', defaultState });
101
+ }
102
+ }
103
+ for (const match of content.matchAll(TEST_GATE_REGEX)) {
104
+ if (match[1]) {
105
+ hits.push({ name: match[1], source: 'testGate', defaultState });
106
+ }
107
+ }
108
+ return mergeFlags(hits, defaultState);
109
+ }
110
+ export function inferAudienceFromPath(relativePath, config) {
111
+ const normalized = relativePath.toLowerCase();
112
+ if (normalized.includes('admin_console') || normalized.includes('system_console')) {
113
+ return normalizeRoles(['system_admin'], config.audience.defaultRoles);
114
+ }
115
+ if (normalized.includes('team') && normalized.includes('admin')) {
116
+ return normalizeRoles(['team_admin'], config.audience.defaultRoles);
117
+ }
118
+ if (normalized.includes('channel') && normalized.includes('admin')) {
119
+ return normalizeRoles(['channel_admin'], config.audience.defaultRoles);
120
+ }
121
+ return normalizeRoles(config.audience.defaultRoles, config.audience.defaultRoles);
122
+ }
123
+ export function formatFlags(flags) {
124
+ if (flags.length === 0) {
125
+ return 'none';
126
+ }
127
+ return flags.map((flag) => `${flag.name} (${flag.defaultState})`).join(', ');
128
+ }
129
+ export function computeBlastRadius(audience, flags, config) {
130
+ const normalizedAudience = normalizeRoles(audience, config.audience.defaultRoles);
131
+ const normalizedFlags = mergeFlags(flags, config.flags.defaultState);
132
+ const hasMember = normalizedAudience.includes('member');
133
+ const hasGuest = normalizedAudience.includes('guest');
134
+ const hasAdmin = normalizedAudience.some((role) => role === 'system_admin' || role === 'team_admin' || role === 'channel_admin');
135
+ const scope = hasMember || hasGuest ? 'broad' : hasAdmin ? 'admin-only' : 'unknown';
136
+ const flagState = normalizedFlags.length === 0
137
+ ? 'unflagged'
138
+ : normalizedFlags.some((flag) => flag.defaultState === 'off')
139
+ ? 'flagged-off'
140
+ : 'flagged-on';
141
+ let scoreDelta = 0;
142
+ if (hasMember) {
143
+ scoreDelta += config.blastRadius.memberBonus;
144
+ }
145
+ if (hasGuest) {
146
+ scoreDelta += config.blastRadius.guestBonus;
147
+ }
148
+ if (!hasMember && !hasGuest) {
149
+ scoreDelta += config.blastRadius.adminOnlyPenalty;
150
+ }
151
+ if (normalizedFlags.some((flag) => flag.defaultState === 'off')) {
152
+ scoreDelta += config.blastRadius.flagOffPenalty;
153
+ }
154
+ return {
155
+ audience: normalizedAudience,
156
+ flags: normalizedFlags,
157
+ summary: `${scope}; ${flagState}`,
158
+ scoreDelta,
159
+ };
160
+ }