@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,572 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { existsSync, writeFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { analyzeFiles, isTestFilePath, scanRepositoryFlows } from './analysis.js';
6
+ import { applyBlastRadius } from './blast_radius.js';
7
+ import { detectFramework, resolveTestPatterns } from './framework.js';
8
+ import { getChangedFiles } from './git.js';
9
+ import { writeReport } from './report.js';
10
+ import { formatFlags } from './flags.js';
11
+ import { applyDataTestIdSuggestions, findDataTestIdSuggestions } from './selectors.js';
12
+ import { discoverTests, mapCatalogTestsToFlows, mapTestsToFlows } from './tests.js';
13
+ import { generateTests } from './generator.js';
14
+ import { loadFlowCatalog } from './flow_catalog.js';
15
+ import { mapChangesToCatalogFlows } from './flow_mapping.js';
16
+ import { runPlaywrightPipeline } from './pipeline.js';
17
+ import { buildGapTestSuggestions } from './gap_suggestions.js';
18
+ import { expandByDependencyGraph } from './dependency_graph.js';
19
+ import { mapTraceabilityToFlows } from './traceability.js';
20
+ const PRIORITY_RANK = {
21
+ P0: 0,
22
+ P1: 1,
23
+ P2: 2,
24
+ };
25
+ function ensureAppRoot(path) {
26
+ if (!existsSync(path)) {
27
+ throw new Error(`App path does not exist: ${path}`);
28
+ }
29
+ }
30
+ function computeGaps(flows, coverageMap) {
31
+ return flows.filter((flow) => {
32
+ if (flow.priority !== 'P0' && flow.priority !== 'P1') {
33
+ return false;
34
+ }
35
+ const coveredBy = coverageMap.get(flow.id) || [];
36
+ return coveredBy.length === 0;
37
+ });
38
+ }
39
+ function normalizeChangedFiles(appRoot, files) {
40
+ const normalizedRoot = appRoot.replace(/\\/g, '/').replace(/\/+$/, '');
41
+ const baseName = normalizedRoot.split('/').pop() || '';
42
+ return files
43
+ .map((file) => file.replace(/\\/g, '/'))
44
+ .map((file) => {
45
+ if (baseName && file.startsWith(`${baseName}/`)) {
46
+ return file.slice(baseName.length + 1);
47
+ }
48
+ return file;
49
+ });
50
+ }
51
+ function sortFlows(flows) {
52
+ const priorityRank = { P0: 0, P1: 1, P2: 2 };
53
+ return [...flows].sort((a, b) => {
54
+ const rankDiff = (priorityRank[a.priority] ?? 3) - (priorityRank[b.priority] ?? 3);
55
+ if (rankDiff !== 0) {
56
+ return rankDiff;
57
+ }
58
+ return b.score - a.score;
59
+ });
60
+ }
61
+ function applyPriorityThresholds(flows, config) {
62
+ return flows.map((flow) => {
63
+ const priority = flow.score >= config.risk.p0Threshold
64
+ ? 'P0'
65
+ : flow.score >= config.risk.p1Threshold
66
+ ? 'P1'
67
+ : 'P2';
68
+ const boundedPriority = flow.priorityFloor && PRIORITY_RANK[flow.priorityFloor] < PRIORITY_RANK[priority]
69
+ ? flow.priorityFloor
70
+ : priority;
71
+ return { ...flow, priority: boundedPriority };
72
+ });
73
+ }
74
+ function buildRecommendedTestsWithFlags(flows, testsByFlow) {
75
+ const testNotes = new Map();
76
+ for (const flow of flows) {
77
+ if (flow.priority !== 'P0' && flow.priority !== 'P1') {
78
+ continue;
79
+ }
80
+ const tests = testsByFlow.get(flow.id) || [];
81
+ const flagSummary = formatFlags(flow.flags || []);
82
+ for (const test of tests) {
83
+ if (!testNotes.has(test)) {
84
+ testNotes.set(test, new Set());
85
+ }
86
+ if (flagSummary !== 'none') {
87
+ testNotes.get(test)?.add(flagSummary);
88
+ }
89
+ }
90
+ }
91
+ return Array.from(testNotes.entries())
92
+ .map(([test, notes]) => {
93
+ if (notes.size === 0) {
94
+ return test;
95
+ }
96
+ return `${test} (flags: ${Array.from(notes).join(', ')})`;
97
+ })
98
+ .sort();
99
+ }
100
+ function buildRecommendedTestsFromCoverage(flows, coverage) {
101
+ const flowMap = new Map();
102
+ for (const flow of flows) {
103
+ flowMap.set(flow.id, flow);
104
+ }
105
+ const testNotes = new Map();
106
+ for (const entry of coverage) {
107
+ if (entry.priority !== 'P0' && entry.priority !== 'P1') {
108
+ continue;
109
+ }
110
+ const flow = flowMap.get(entry.flowId);
111
+ const flagSummary = formatFlags(flow?.flags || []);
112
+ for (const test of entry.coveredBy) {
113
+ if (!testNotes.has(test)) {
114
+ testNotes.set(test, new Set());
115
+ }
116
+ if (flagSummary !== 'none') {
117
+ testNotes.get(test)?.add(flagSummary);
118
+ }
119
+ }
120
+ }
121
+ return Array.from(testNotes.entries())
122
+ .map(([test, notes]) => {
123
+ if (notes.size === 0) {
124
+ return test;
125
+ }
126
+ return `${test} (flags: ${Array.from(notes).join(', ')})`;
127
+ })
128
+ .sort();
129
+ }
130
+ function uniquePaths(paths) {
131
+ return Array.from(new Set(paths.map((value) => value.replace(/\\/g, '/')).filter(Boolean)));
132
+ }
133
+ function mergeCoverageWithHeuristicFallback(traceability, heuristic) {
134
+ const byFlow = new Map();
135
+ for (const entry of traceability) {
136
+ byFlow.set(entry.flowId, entry);
137
+ }
138
+ for (const entry of heuristic) {
139
+ const existing = byFlow.get(entry.flowId);
140
+ if (!existing) {
141
+ byFlow.set(entry.flowId, entry);
142
+ continue;
143
+ }
144
+ if (existing.coveredBy.length === 0 && entry.coveredBy.length > 0) {
145
+ byFlow.set(entry.flowId, entry);
146
+ }
147
+ }
148
+ return Array.from(byFlow.values());
149
+ }
150
+ function classifyImpactModelConfidence(flowMapping, testMapping, dependencyGraph, traceability, warnings) {
151
+ let score = 0;
152
+ if (flowMapping === 'catalog') {
153
+ score += 2;
154
+ }
155
+ if (testMapping === 'catalog') {
156
+ score += 2;
157
+ }
158
+ else if (testMapping === 'traceability') {
159
+ score += 3;
160
+ }
161
+ if (traceability) {
162
+ if (!traceability.manifestFound) {
163
+ score -= 1;
164
+ }
165
+ else if (traceability.coverageRatio >= 0.7) {
166
+ score += 1;
167
+ }
168
+ else if (traceability.coverageRatio < 0.4) {
169
+ score -= 1;
170
+ }
171
+ }
172
+ if (dependencyGraph && dependencyGraph.expandedFiles.length > 0) {
173
+ score += 1;
174
+ }
175
+ if (dependencyGraph && dependencyGraph.truncated) {
176
+ score -= 1;
177
+ }
178
+ if (warnings.length > 0) {
179
+ score -= 1;
180
+ }
181
+ if (score >= 5) {
182
+ return 'high';
183
+ }
184
+ if (score >= 3) {
185
+ return 'medium';
186
+ }
187
+ return 'low';
188
+ }
189
+ export async function runImpact(_config, _options) {
190
+ ensureAppRoot(_config.path);
191
+ if (_config.testsRoot) {
192
+ ensureAppRoot(_config.testsRoot);
193
+ }
194
+ const deadline = Date.now() + _config.timeLimitMinutes * 60 * 1000;
195
+ const warnings = [];
196
+ const testsRoot = _config.testsRoot || _config.path;
197
+ const frameworkDetection = detectFramework(testsRoot, _config.framework);
198
+ const testPatterns = resolveTestPatterns(testsRoot, frameworkDetection, _config.testDiscovery.patterns);
199
+ if (frameworkDetection.framework === 'unknown' && testPatterns.patterns.length === 0) {
200
+ throw new Error('No framework config found. Provide testDiscovery.patterns in config or --patterns.');
201
+ }
202
+ const gitResult = getChangedFiles(_config.path, _config.git.since, {
203
+ includeUncommitted: _config.git.includeUncommitted,
204
+ });
205
+ const changedFiles = normalizeChangedFiles(_config.path, gitResult.files);
206
+ if (gitResult.error) {
207
+ warnings.push(`Git diff failed: ${gitResult.error}`);
208
+ }
209
+ if (changedFiles.length === 0 && !_config.impact.allowFallback) {
210
+ throw new Error('No changed files detected. Provide --since or use gap mode (or --allow-fallback).');
211
+ }
212
+ let analysisTargets = changedFiles.filter((file) => !isTestFilePath(file));
213
+ if (analysisTargets.length === 0 && _config.impact.allowFallback) {
214
+ warnings.push('No changed files detected. Falling back to repository scan for screens.');
215
+ analysisTargets = scanRepositoryFlows(_config.path, 250, _config.flowDiscovery.patterns, _config.flowDiscovery.exclude);
216
+ }
217
+ let dependencyGraph;
218
+ if (analysisTargets.length > 0 && _config.impact.dependencyGraph.enabled) {
219
+ dependencyGraph = expandByDependencyGraph(_config.path, analysisTargets, _config.impact.dependencyGraph);
220
+ warnings.push(...dependencyGraph.warnings);
221
+ if (dependencyGraph.expandedFiles.length > 0) {
222
+ analysisTargets = uniquePaths([...analysisTargets, ...dependencyGraph.expandedFiles]);
223
+ warnings.push(`Dependency graph expanded impacted files by ${dependencyGraph.expandedFiles.length} (depth=${dependencyGraph.maxDepth}).`);
224
+ }
225
+ }
226
+ const analysis = analyzeFiles(_config.path, analysisTargets, _config);
227
+ warnings.push(...analysis.warnings);
228
+ if (Date.now() > deadline) {
229
+ warnings.push('Time limit exceeded after impact analysis. Skipping coverage and selector steps.');
230
+ }
231
+ let coverage = [];
232
+ let gaps = [];
233
+ let dataTestIds = [];
234
+ let flows = [];
235
+ let flowCatalogSource;
236
+ let recommendedTests = [];
237
+ let testsByFlow;
238
+ let testSuggestions = [];
239
+ const catalog = loadFlowCatalog(_config);
240
+ const flowMappingSource = catalog ? 'catalog' : 'heuristic';
241
+ let testMappingSource = 'heuristic';
242
+ let traceabilityStats;
243
+ if (catalog) {
244
+ flowCatalogSource = catalog.source;
245
+ const mapping = mapChangesToCatalogFlows(catalog, analysisTargets, 'impact', _config);
246
+ flows = mapping.flows;
247
+ testsByFlow = mapping.testsByFlow;
248
+ warnings.push(...mapping.warnings);
249
+ }
250
+ else {
251
+ flows = analysis.flows;
252
+ }
253
+ flows = applyBlastRadius(flows, analysis.files, _config);
254
+ if (!catalog) {
255
+ flows = applyPriorityThresholds(flows, _config);
256
+ }
257
+ if (Date.now() <= deadline) {
258
+ if (catalog && testsByFlow) {
259
+ coverage = mapCatalogTestsToFlows(flows, testsRoot, testsByFlow);
260
+ testMappingSource = 'catalog';
261
+ const coverageMap = new Map();
262
+ for (const entry of coverage) {
263
+ coverageMap.set(entry.flowId, entry.coveredBy);
264
+ }
265
+ gaps = computeGaps(flows, coverageMap);
266
+ recommendedTests = buildRecommendedTestsWithFlags(flows, testsByFlow);
267
+ }
268
+ else {
269
+ const traceability = mapTraceabilityToFlows(testsRoot, _config.impact.traceability, flows);
270
+ warnings.push(...traceability.warnings);
271
+ traceabilityStats = traceability.stats;
272
+ if (traceability.stats.manifestFound && traceability.stats.matchedFlows > 0) {
273
+ coverage = traceability.coverage;
274
+ testMappingSource = 'traceability';
275
+ if (traceability.stats.coverageRatio < 0.8) {
276
+ const tests = discoverTests(testsRoot, testPatterns.patterns);
277
+ const heuristicCoverage = mapTestsToFlows(flows, tests);
278
+ coverage = mergeCoverageWithHeuristicFallback(coverage, heuristicCoverage);
279
+ warnings.push('Applied heuristic fallback for flows not covered by traceability mapping.');
280
+ }
281
+ }
282
+ else {
283
+ const tests = discoverTests(testsRoot, testPatterns.patterns);
284
+ coverage = mapTestsToFlows(flows, tests);
285
+ }
286
+ const coverageMap = new Map();
287
+ for (const entry of coverage) {
288
+ coverageMap.set(entry.flowId, entry.coveredBy);
289
+ }
290
+ gaps = computeGaps(flows, coverageMap);
291
+ recommendedTests = buildRecommendedTestsFromCoverage(flows, coverage);
292
+ }
293
+ }
294
+ if (Date.now() <= deadline) {
295
+ testSuggestions = buildGapTestSuggestions(testsRoot, gaps, frameworkDetection.framework, testPatterns.patterns);
296
+ }
297
+ if (Date.now() <= deadline) {
298
+ dataTestIds = analysis.files
299
+ .filter((file) => file.isUI && file.content)
300
+ .flatMap((file) => findDataTestIdSuggestions(file.relativePath, file.content, file.flowId));
301
+ }
302
+ if (_config.specPDF) {
303
+ warnings.push('Spec PDF provided but parsing is not implemented in v1.');
304
+ }
305
+ const applied = _options.apply && Date.now() <= deadline
306
+ ? applyChanges(_config, analysis.files, dataTestIds, gaps, frameworkDetection.framework, testPatterns.patterns)
307
+ : undefined;
308
+ let pipelineSummary;
309
+ if (_config.pipeline.enabled && frameworkDetection.framework === 'playwright' && gaps.length > 0) {
310
+ pipelineSummary = runPlaywrightPipeline(testsRoot, gaps, _config.pipeline);
311
+ if (pipelineSummary.warnings.length > 0) {
312
+ warnings.push(...pipelineSummary.warnings);
313
+ }
314
+ }
315
+ const reportRoot = testsRoot;
316
+ const report = writeReport(reportRoot, _config, {
317
+ mode: 'impact',
318
+ changedFiles,
319
+ flows: sortFlows(flows),
320
+ coverage,
321
+ gaps,
322
+ dataTestIds,
323
+ framework: frameworkDetection.framework,
324
+ testPatterns: testPatterns.patterns,
325
+ specPDF: _config.specPDF,
326
+ warnings,
327
+ flowCatalog: flowCatalogSource,
328
+ recommendedTests,
329
+ impactModel: {
330
+ schemaVersion: '1.0.0',
331
+ flowMapping: flowMappingSource,
332
+ testMapping: testMappingSource,
333
+ confidenceClass: classifyImpactModelConfidence(flowMappingSource, testMappingSource, dependencyGraph, traceabilityStats, warnings),
334
+ traceability: traceabilityStats,
335
+ dependencyGraph: dependencyGraph
336
+ ? {
337
+ source: dependencyGraph.source,
338
+ enabled: _config.impact.dependencyGraph.enabled,
339
+ seedFiles: dependencyGraph.seedFiles.length,
340
+ expandedFiles: dependencyGraph.expandedFiles.length,
341
+ analyzedFiles: dependencyGraph.analyzedFiles,
342
+ analyzedEdges: dependencyGraph.analyzedEdges,
343
+ maxDepth: dependencyGraph.maxDepth,
344
+ truncated: dependencyGraph.truncated,
345
+ }
346
+ : undefined,
347
+ subsystemRisk: analysis.subsystemRisk.enabled ? analysis.subsystemRisk : undefined,
348
+ },
349
+ testSuggestions,
350
+ pipeline: pipelineSummary,
351
+ applied,
352
+ });
353
+ // eslint-disable-next-line no-console
354
+ console.log(`Impact report: ${report.markdownPath}`);
355
+ // eslint-disable-next-line no-console
356
+ console.log(`Impact data: ${report.jsonPath}`);
357
+ }
358
+ export async function runGap(_config, _options) {
359
+ ensureAppRoot(_config.path);
360
+ if (_config.testsRoot) {
361
+ ensureAppRoot(_config.testsRoot);
362
+ }
363
+ const deadline = Date.now() + _config.timeLimitMinutes * 60 * 1000;
364
+ const warnings = [];
365
+ const testsRoot = _config.testsRoot || _config.path;
366
+ const frameworkDetection = detectFramework(testsRoot, _config.framework);
367
+ const testPatterns = resolveTestPatterns(testsRoot, frameworkDetection, _config.testDiscovery.patterns);
368
+ if (frameworkDetection.framework === 'unknown' && testPatterns.patterns.length === 0) {
369
+ throw new Error('No framework config found. Provide testDiscovery.patterns in config or --patterns.');
370
+ }
371
+ const gitResult = getChangedFiles(_config.path, _config.git.since, {
372
+ includeUncommitted: _config.git.includeUncommitted,
373
+ });
374
+ const changedFiles = normalizeChangedFiles(_config.path, gitResult.files);
375
+ if (gitResult.error) {
376
+ warnings.push(`Git diff failed: ${gitResult.error}`);
377
+ }
378
+ let analysisTargets = changedFiles.filter((file) => !isTestFilePath(file));
379
+ if (analysisTargets.length === 0) {
380
+ analysisTargets = scanRepositoryFlows(_config.path, 250, _config.flowDiscovery.patterns, _config.flowDiscovery.exclude);
381
+ }
382
+ if (analysisTargets.length === 0) {
383
+ warnings.push('No flow candidates found. Ensure pages/screens exist or provide changed files.');
384
+ }
385
+ let dependencyGraph;
386
+ if (analysisTargets.length > 0 && _config.impact.dependencyGraph.enabled) {
387
+ dependencyGraph = expandByDependencyGraph(_config.path, analysisTargets, _config.impact.dependencyGraph);
388
+ warnings.push(...dependencyGraph.warnings);
389
+ if (dependencyGraph.expandedFiles.length > 0) {
390
+ analysisTargets = uniquePaths([...analysisTargets, ...dependencyGraph.expandedFiles]);
391
+ warnings.push(`Dependency graph expanded impacted files by ${dependencyGraph.expandedFiles.length} (depth=${dependencyGraph.maxDepth}).`);
392
+ }
393
+ }
394
+ const analysis = analyzeFiles(_config.path, analysisTargets, _config);
395
+ warnings.push(...analysis.warnings);
396
+ if (Date.now() > deadline) {
397
+ warnings.push('Time limit exceeded after gap analysis. Skipping coverage and selector steps.');
398
+ }
399
+ let coverage = [];
400
+ let gaps = [];
401
+ let dataTestIds = [];
402
+ let flows = [];
403
+ let flowCatalogSource;
404
+ let recommendedTests = [];
405
+ let testsByFlow;
406
+ let testSuggestions = [];
407
+ const catalog = loadFlowCatalog(_config);
408
+ const flowMappingSource = catalog ? 'catalog' : 'heuristic';
409
+ let testMappingSource = 'heuristic';
410
+ let traceabilityStats;
411
+ if (catalog) {
412
+ flowCatalogSource = catalog.source;
413
+ const mapping = mapChangesToCatalogFlows(catalog, analysisTargets, 'gap', _config);
414
+ flows = mapping.flows;
415
+ testsByFlow = mapping.testsByFlow;
416
+ warnings.push(...mapping.warnings);
417
+ }
418
+ else {
419
+ flows = analysis.flows;
420
+ }
421
+ flows = applyBlastRadius(flows, analysis.files, _config);
422
+ if (!catalog) {
423
+ flows = applyPriorityThresholds(flows, _config);
424
+ }
425
+ if (Date.now() <= deadline) {
426
+ if (catalog && testsByFlow) {
427
+ coverage = mapCatalogTestsToFlows(flows, testsRoot, testsByFlow);
428
+ testMappingSource = 'catalog';
429
+ const coverageMap = new Map();
430
+ for (const entry of coverage) {
431
+ coverageMap.set(entry.flowId, entry.coveredBy);
432
+ }
433
+ gaps = computeGaps(flows, coverageMap);
434
+ recommendedTests = buildRecommendedTestsWithFlags(flows, testsByFlow);
435
+ }
436
+ else {
437
+ const traceability = mapTraceabilityToFlows(testsRoot, _config.impact.traceability, flows);
438
+ warnings.push(...traceability.warnings);
439
+ traceabilityStats = traceability.stats;
440
+ if (traceability.stats.manifestFound && traceability.stats.matchedFlows > 0) {
441
+ coverage = traceability.coverage;
442
+ testMappingSource = 'traceability';
443
+ if (traceability.stats.coverageRatio < 0.8) {
444
+ const tests = discoverTests(testsRoot, testPatterns.patterns);
445
+ const heuristicCoverage = mapTestsToFlows(flows, tests);
446
+ coverage = mergeCoverageWithHeuristicFallback(coverage, heuristicCoverage);
447
+ warnings.push('Applied heuristic fallback for flows not covered by traceability mapping.');
448
+ }
449
+ }
450
+ else {
451
+ const tests = discoverTests(testsRoot, testPatterns.patterns);
452
+ coverage = mapTestsToFlows(flows, tests);
453
+ }
454
+ const coverageMap = new Map();
455
+ for (const entry of coverage) {
456
+ coverageMap.set(entry.flowId, entry.coveredBy);
457
+ }
458
+ gaps = computeGaps(flows, coverageMap);
459
+ recommendedTests = buildRecommendedTestsFromCoverage(flows, coverage);
460
+ }
461
+ }
462
+ if (Date.now() <= deadline) {
463
+ testSuggestions = buildGapTestSuggestions(testsRoot, gaps, frameworkDetection.framework, testPatterns.patterns);
464
+ }
465
+ if (Date.now() <= deadline) {
466
+ dataTestIds = analysis.files
467
+ .filter((file) => file.isUI && file.content)
468
+ .flatMap((file) => findDataTestIdSuggestions(file.relativePath, file.content, file.flowId));
469
+ }
470
+ if (_config.specPDF) {
471
+ warnings.push('Spec PDF provided but parsing is not implemented in v1.');
472
+ }
473
+ const applied = _options.apply && Date.now() <= deadline
474
+ ? applyChanges(_config, analysis.files, dataTestIds, gaps, frameworkDetection.framework, testPatterns.patterns)
475
+ : undefined;
476
+ let pipelineSummary;
477
+ if (_config.pipeline.enabled && frameworkDetection.framework === 'playwright' && gaps.length > 0) {
478
+ pipelineSummary = runPlaywrightPipeline(testsRoot, gaps, _config.pipeline);
479
+ if (pipelineSummary.warnings.length > 0) {
480
+ warnings.push(...pipelineSummary.warnings);
481
+ }
482
+ }
483
+ const reportRoot = testsRoot;
484
+ const report = writeReport(reportRoot, _config, {
485
+ mode: 'gap',
486
+ changedFiles,
487
+ flows: sortFlows(flows),
488
+ coverage,
489
+ gaps,
490
+ dataTestIds,
491
+ framework: frameworkDetection.framework,
492
+ testPatterns: testPatterns.patterns,
493
+ specPDF: _config.specPDF,
494
+ warnings,
495
+ flowCatalog: flowCatalogSource,
496
+ recommendedTests,
497
+ impactModel: {
498
+ schemaVersion: '1.0.0',
499
+ flowMapping: flowMappingSource,
500
+ testMapping: testMappingSource,
501
+ confidenceClass: classifyImpactModelConfidence(flowMappingSource, testMappingSource, dependencyGraph, traceabilityStats, warnings),
502
+ traceability: traceabilityStats,
503
+ dependencyGraph: dependencyGraph
504
+ ? {
505
+ source: dependencyGraph.source,
506
+ enabled: _config.impact.dependencyGraph.enabled,
507
+ seedFiles: dependencyGraph.seedFiles.length,
508
+ expandedFiles: dependencyGraph.expandedFiles.length,
509
+ analyzedFiles: dependencyGraph.analyzedFiles,
510
+ analyzedEdges: dependencyGraph.analyzedEdges,
511
+ maxDepth: dependencyGraph.maxDepth,
512
+ truncated: dependencyGraph.truncated,
513
+ }
514
+ : undefined,
515
+ subsystemRisk: analysis.subsystemRisk.enabled ? analysis.subsystemRisk : undefined,
516
+ },
517
+ testSuggestions,
518
+ pipeline: pipelineSummary,
519
+ applied,
520
+ });
521
+ // eslint-disable-next-line no-console
522
+ console.log(`Gap report: ${report.markdownPath}`);
523
+ // eslint-disable-next-line no-console
524
+ console.log(`Gap data: ${report.jsonPath}`);
525
+ }
526
+ function applyChanges(config, files, dataTestIds, gaps, framework, testPatterns) {
527
+ const patchedFiles = [];
528
+ const suggestionsByFile = new Map();
529
+ for (const suggestion of dataTestIds) {
530
+ const bucket = suggestionsByFile.get(suggestion.file) || [];
531
+ bucket.push(suggestion);
532
+ suggestionsByFile.set(suggestion.file, bucket);
533
+ }
534
+ if (config.selectors.patchOnApply) {
535
+ for (const file of files) {
536
+ const suggestions = suggestionsByFile.get(file.relativePath);
537
+ if (!suggestions || !file.content) {
538
+ continue;
539
+ }
540
+ const updated = applyDataTestIdSuggestions(file.content, suggestions);
541
+ if (updated !== file.content) {
542
+ const fullPath = join(config.path, file.relativePath);
543
+ writeFileSync(fullPath, updated, 'utf-8');
544
+ patchedFiles.push(file.relativePath);
545
+ }
546
+ }
547
+ }
548
+ const fileToFlow = new Map();
549
+ for (const file of files) {
550
+ fileToFlow.set(file.relativePath, file.flowId);
551
+ }
552
+ const testIdsByFlow = new Map();
553
+ for (const suggestion of dataTestIds) {
554
+ const flowId = fileToFlow.get(suggestion.file);
555
+ if (!flowId) {
556
+ continue;
557
+ }
558
+ const bucket = testIdsByFlow.get(flowId) || [];
559
+ bucket.push(suggestion.testId);
560
+ testIdsByFlow.set(flowId, bucket);
561
+ }
562
+ let generatedTests = [];
563
+ let skippedTests = [];
564
+ if (!config.pipeline.enabled) {
565
+ const frameworkType = framework === 'playwright' || framework === 'cypress' || framework === 'selenium' ? framework : 'playwright';
566
+ const testsRoot = config.testsRoot || config.path;
567
+ const generated = generateTests(testsRoot, gaps, frameworkType, testPatterns, testIdsByFlow);
568
+ generatedTests = generated.filter((entry) => entry.created).map((entry) => entry.path);
569
+ skippedTests = generated.filter((entry) => !entry.created).map((entry) => entry.path);
570
+ }
571
+ return { patchedFiles, generatedTests, skippedTests };
572
+ }
@@ -0,0 +1,71 @@
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
+ }