@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,548 @@
1
+ /**
2
+ * Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
3
+ * See LICENSE.txt for license information.
4
+ *
5
+ * Impact Analysis Engine
6
+ *
7
+ * Analyzes code changes and identifies which user flows are affected,
8
+ * then maps those flows to test coverage gaps.
9
+ */
10
+ import { execSync } from 'child_process';
11
+ import { existsSync, readFileSync } from 'fs';
12
+ import { join, resolve } from 'path';
13
+ import { minimatch } from 'minimatch';
14
+ // =============================================================================
15
+ // GIT CHANGE DETECTION
16
+ // =============================================================================
17
+ /**
18
+ * Intelligently detect the best git reference for comparison:
19
+ * - If on feature branch: use origin/master, origin/main, or master
20
+ * - If on main branch: use HEAD~1
21
+ * Returns the best available reference to compare against
22
+ */
23
+ export function detectComparisonBase() {
24
+ try {
25
+ const gitRoot = findGitRoot();
26
+ // Get current branch
27
+ const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
28
+ encoding: 'utf-8',
29
+ cwd: gitRoot,
30
+ }).trim();
31
+ // If on main branch, just check last commit
32
+ if (currentBranch === 'master' || currentBranch === 'main') {
33
+ return 'HEAD~1';
34
+ }
35
+ // On feature branch: try to find the main branch
36
+ // Try origin/master first, then origin/main, then master
37
+ const candidates = ['origin/master', 'origin/main', 'master'];
38
+ for (const candidate of candidates) {
39
+ try {
40
+ execSync(`git rev-parse ${candidate}`, {
41
+ encoding: 'utf-8',
42
+ cwd: gitRoot,
43
+ stdio: 'ignore',
44
+ });
45
+ return candidate;
46
+ }
47
+ catch {
48
+ // Try next candidate
49
+ }
50
+ }
51
+ // Fallback if none found
52
+ return 'HEAD~1';
53
+ }
54
+ catch {
55
+ // If git commands fail, default to HEAD~1
56
+ return 'HEAD~1';
57
+ }
58
+ }
59
+ /**
60
+ * Check if a file path is a frontend file
61
+ * Frontend includes: webapp and e2e-tests directories
62
+ */
63
+ function isFrontendFile(filePath) {
64
+ return filePath.startsWith('webapp/') || filePath.startsWith('e2e-tests/');
65
+ }
66
+ /**
67
+ * Get git changes since a given reference
68
+ * Filters to only include frontend files (webapp and e2e-tests)
69
+ */
70
+ export function getGitChanges(since = 'HEAD~1') {
71
+ try {
72
+ const gitRoot = findGitRoot();
73
+ const result = execSync(`git diff --name-status ${since}...HEAD`, {
74
+ encoding: 'utf-8',
75
+ cwd: gitRoot,
76
+ });
77
+ return result
78
+ .trim()
79
+ .split('\n')
80
+ .filter((line) => line.length > 0)
81
+ .map((line) => {
82
+ const [status, path] = line.split('\t');
83
+ return {
84
+ path,
85
+ status: status,
86
+ ref: since,
87
+ };
88
+ })
89
+ .filter((change) => isFrontendFile(change.path));
90
+ }
91
+ catch (error) {
92
+ console.warn(`⚠️ Could not get git changes: ${error.message}`);
93
+ return [];
94
+ }
95
+ }
96
+ /**
97
+ * Get git diff stats for a file
98
+ */
99
+ function getFileDiffStats(filePath, since = 'HEAD~1') {
100
+ try {
101
+ const gitRoot = findGitRoot();
102
+ const result = execSync(`git diff --numstat ${since}...HEAD -- ${filePath}`, {
103
+ encoding: 'utf-8',
104
+ cwd: gitRoot,
105
+ });
106
+ const match = result.match(/(\d+)\s+(\d+)/);
107
+ if (match) {
108
+ return {
109
+ linesAdded: parseInt(match[1], 10),
110
+ linesRemoved: parseInt(match[2], 10),
111
+ };
112
+ }
113
+ }
114
+ catch (error) {
115
+ // Ignore errors
116
+ }
117
+ return { linesAdded: 0, linesRemoved: 0 };
118
+ }
119
+ // =============================================================================
120
+ // FLOW CATALOG LOADING
121
+ // =============================================================================
122
+ /**
123
+ * Load flow catalog from flows.json
124
+ */
125
+ export function loadFlowCatalog(catalogPath) {
126
+ let path = catalogPath;
127
+ if (!path) {
128
+ // Try multiple possible paths for flows.json
129
+ const possiblePaths = [
130
+ // If running from e2e-tests/playwright directory
131
+ join(process.cwd(), '.e2e-ai-agents/flows.json'),
132
+ // If running from monorepo root
133
+ join(process.cwd(), 'e2e-tests/playwright/.e2e-ai-agents/flows.json'),
134
+ ];
135
+ // Find the first path that exists
136
+ path = possiblePaths.find((p) => existsSync(p));
137
+ if (!path) {
138
+ throw new Error(`Flow catalog not found. Tried:\n${possiblePaths.map((p) => ` - ${p}`).join('\n')}`);
139
+ }
140
+ }
141
+ if (!existsSync(path)) {
142
+ throw new Error(`Flow catalog not found at ${path}`);
143
+ }
144
+ const content = readFileSync(path, 'utf-8');
145
+ const data = JSON.parse(content);
146
+ return data.flows || [];
147
+ }
148
+ // =============================================================================
149
+ // FLOW MATCHING
150
+ // =============================================================================
151
+ /**
152
+ * Check if a file path matches a glob pattern
153
+ */
154
+ function pathMatches(filePath, pattern) {
155
+ // Normalize patterns
156
+ const normalizedPattern = pattern.replace(/\\/g, '/');
157
+ const normalizedPath = filePath.replace(/\\/g, '/');
158
+ // Handle ** wildcards
159
+ if (normalizedPattern.includes('**')) {
160
+ // Convert ** pattern to regex
161
+ // Use placeholder to avoid escaping issues with **
162
+ const placeholder = '___DOUBLE_STAR___';
163
+ let regexPattern = normalizedPattern.replace(/\*\*/g, placeholder);
164
+ // Escape regex special chars
165
+ regexPattern = regexPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
166
+ // Replace placeholder with .* (matches anything including /)
167
+ regexPattern = regexPattern.replace(new RegExp(placeholder, 'g'), '.*');
168
+ // Replace remaining * with [^/]* (matches anything except /)
169
+ regexPattern = regexPattern.replace(/\*/g, '[^/]*');
170
+ // Replace ? with [^/]
171
+ regexPattern = regexPattern.replace(/\?/g, '[^/]');
172
+ return new RegExp(`^${regexPattern}$`).test(normalizedPath);
173
+ }
174
+ return minimatch(normalizedPath, normalizedPattern, { matchBase: true });
175
+ }
176
+ /**
177
+ * Calculate confidence score based on match type and quality
178
+ */
179
+ function calculateConfidence(matchType, matchQuality) {
180
+ const typeWeights = {
181
+ path_exact: 100,
182
+ path_pattern: 85,
183
+ keyword_multiple: 75,
184
+ keyword_single: 60,
185
+ import: 50,
186
+ };
187
+ return Math.min(100, typeWeights[matchType] || 50);
188
+ }
189
+ /**
190
+ * Match a flow to changed files
191
+ */
192
+ export function matchFlowToChanges(flow, changes) {
193
+ const reasons = [];
194
+ const affectedFiles = [];
195
+ let matchType = 'path';
196
+ let confidence = 0;
197
+ // Check path matching
198
+ const pathMatchedFiles = changes.filter((change) => flow.paths.some((pattern) => pathMatches(change.file, pattern)));
199
+ if (pathMatchedFiles.length > 0) {
200
+ affectedFiles.push(...pathMatchedFiles.map((c) => c.file));
201
+ confidence = Math.max(confidence, 90);
202
+ matchType = 'path';
203
+ reasons.push(`${pathMatchedFiles.length} file(s) match flow paths: ${pathMatchedFiles.map((c) => c.file).join(', ')}`);
204
+ }
205
+ // Check keyword matching
206
+ const flowKeywords = flow.keywords.map((k) => k.toLowerCase());
207
+ const keywordMatches = [];
208
+ for (const change of changes) {
209
+ const changeContent = [
210
+ ...change.functions,
211
+ ...change.classes,
212
+ ...change.imports.flatMap((i) => [i.from, ...i.names]),
213
+ ]
214
+ .join(' ')
215
+ .toLowerCase();
216
+ for (const keyword of flowKeywords) {
217
+ if (changeContent.includes(keyword)) {
218
+ keywordMatches.push(change.file);
219
+ break;
220
+ }
221
+ }
222
+ }
223
+ if (keywordMatches.length > 0) {
224
+ affectedFiles.push(...keywordMatches.filter((f) => !affectedFiles.includes(f)));
225
+ confidence = Math.max(confidence, keywordMatches.length > 1 ? 75 : 60);
226
+ if (pathMatches.length === 0) {
227
+ matchType = 'keyword';
228
+ }
229
+ else {
230
+ matchType = 'combined';
231
+ }
232
+ reasons.push(`${keywordMatches.length} file(s) contain keywords: ${flowKeywords.slice(0, 3).join(', ')}`);
233
+ }
234
+ // Check import matching
235
+ const importMatches = [];
236
+ const flowPaths = flow.paths.map((p) => p.replace(/\*\*/g, '').toLowerCase());
237
+ for (const change of changes) {
238
+ for (const imp of change.imports) {
239
+ const impFrom = imp.from.toLowerCase();
240
+ for (const flowPath of flowPaths) {
241
+ if (impFrom.includes(flowPath.replace(/[\/\\]/g, ''))) {
242
+ importMatches.push(change.file);
243
+ break;
244
+ }
245
+ }
246
+ }
247
+ }
248
+ if (importMatches.length > 0) {
249
+ affectedFiles.push(...importMatches.filter((f) => !affectedFiles.includes(f)));
250
+ confidence = Math.max(confidence, 50);
251
+ if (pathMatches.length === 0 && keywordMatches.length === 0) {
252
+ matchType = 'import';
253
+ }
254
+ else {
255
+ matchType = 'combined';
256
+ }
257
+ }
258
+ // Return null if no matches
259
+ if (confidence === 0) {
260
+ return null;
261
+ }
262
+ return {
263
+ flow,
264
+ matchType,
265
+ confidence: Math.min(100, confidence),
266
+ affectedFiles: [...new Set(affectedFiles)],
267
+ existingTests: [],
268
+ testGaps: [],
269
+ reasons,
270
+ };
271
+ }
272
+ // =============================================================================
273
+ // TEST ANALYSIS
274
+ // =============================================================================
275
+ /**
276
+ * Find existing tests for a flow
277
+ */
278
+ export function findExistingTests(flow, repoRoot) {
279
+ const root = repoRoot || findGitRoot();
280
+ const tests = [];
281
+ // Use tests array from flow definition
282
+ if (flow.tests && flow.tests.length > 0) {
283
+ for (const testPath of flow.tests) {
284
+ const fullPath = join(root, testPath);
285
+ if (existsSync(fullPath)) {
286
+ tests.push(testPath);
287
+ }
288
+ }
289
+ }
290
+ return tests;
291
+ }
292
+ /**
293
+ * Identify test coverage gaps for a flow
294
+ */
295
+ export function identifyTestGaps(flow, existingTests) {
296
+ const gaps = [];
297
+ // If no existing tests, all scenarios are gaps
298
+ if (existingTests.length === 0) {
299
+ gaps.push(`No existing tests for ${flow.name}`);
300
+ // Suggest specific test scenarios based on audience and keywords
301
+ const audiences = flow.audience || [];
302
+ const keywords = flow.keywords || [];
303
+ if (audiences.length > 0) {
304
+ gaps.push(`Missing scenarios for audiences: ${audiences.slice(0, 2).join(', ')}`);
305
+ }
306
+ if (keywords.length > 0) {
307
+ gaps.push(`Missing tests for: ${keywords.slice(0, 2).join(', ')}`);
308
+ }
309
+ }
310
+ // Check for specific audience coverage
311
+ const audiences = flow.audience || [];
312
+ if (audiences.length > 1) {
313
+ // Suggest multi-audience testing
314
+ gaps.push('Consider testing with different user roles');
315
+ }
316
+ // Check for edge cases based on flow type
317
+ if (flow.keywords.includes('realtime') || flow.keywords.includes('websocket')) {
318
+ gaps.push('Consider testing offline/reconnection scenarios');
319
+ }
320
+ if (flow.keywords.includes('edit') || flow.keywords.includes('delete')) {
321
+ gaps.push('Consider testing permissions/authorization edge cases');
322
+ }
323
+ return gaps;
324
+ }
325
+ // =============================================================================
326
+ // MAIN ANALYSIS
327
+ // =============================================================================
328
+ /**
329
+ * Analyze code impact and return comprehensive report
330
+ */
331
+ export async function analyzeImpact(changes, flows, options = {}) {
332
+ const repoRoot = options.repoRoot || findGitRoot();
333
+ // Analyze each changed file
334
+ const analyses = [];
335
+ for (const change of changes) {
336
+ const analysis = analyzeFile(change.path, change.status);
337
+ if (analysis) {
338
+ analyses.push(analysis);
339
+ }
340
+ }
341
+ if (options.verbose) {
342
+ console.log(`📊 Analyzed ${analyses.length} changed files`);
343
+ }
344
+ // Match changes to flows
345
+ const impactedFlows = [];
346
+ for (const flow of flows) {
347
+ const impact = matchFlowToChanges(flow, analyses);
348
+ if (impact && impact.confidence > 0) {
349
+ // Find existing tests
350
+ impact.existingTests = findExistingTests(flow, repoRoot);
351
+ // Identify test gaps
352
+ impact.testGaps = identifyTestGaps(flow, impact.existingTests);
353
+ impactedFlows.push(impact);
354
+ }
355
+ }
356
+ // Sort by priority and confidence
357
+ impactedFlows.sort((a, b) => {
358
+ const priorityOrder = { P0: 0, P1: 1, P2: 2 };
359
+ const aPriority = priorityOrder[a.flow.priority];
360
+ const bPriority = priorityOrder[b.flow.priority];
361
+ if (aPriority !== bPriority) {
362
+ return aPriority - bPriority;
363
+ }
364
+ return b.confidence - a.confidence;
365
+ });
366
+ if (options.verbose) {
367
+ console.log(`🎯 Found ${impactedFlows.length} affected flows`);
368
+ }
369
+ // Generate recommendations
370
+ const recommendations = generateRecommendations(impactedFlows);
371
+ // Group flows by relationships
372
+ const { groups, ungrouped } = groupFlowsByRelationships(impactedFlows, flows);
373
+ return {
374
+ timestamp: new Date().toISOString(),
375
+ gitRef: changes[0]?.ref || 'HEAD',
376
+ totalChanges: changes.length,
377
+ affectedFlows: impactedFlows,
378
+ flowGroups: groups,
379
+ ungroupedFlows: ungrouped,
380
+ priorityBreakdown: {
381
+ p0: impactedFlows.filter((f) => f.flow.priority === 'P0').length,
382
+ p1: impactedFlows.filter((f) => f.flow.priority === 'P1').length,
383
+ p2: impactedFlows.filter((f) => f.flow.priority === 'P2').length,
384
+ },
385
+ testCoverage: {
386
+ total: impactedFlows.length,
387
+ covered: impactedFlows.filter((f) => f.existingTests.length > 0).length,
388
+ gaps: impactedFlows.filter((f) => f.testGaps.length > 0).length,
389
+ },
390
+ recommendations,
391
+ hasP0Impact: impactedFlows.some((f) => f.flow.priority === 'P0'),
392
+ };
393
+ }
394
+ // =============================================================================
395
+ // HELPER FUNCTIONS
396
+ // =============================================================================
397
+ /**
398
+ * Load flow groups from flows.json
399
+ */
400
+ function loadFlowGroups() {
401
+ const possiblePaths = [
402
+ // If running from e2e-tests/playwright directory
403
+ join(process.cwd(), '.e2e-ai-agents/flows.json'),
404
+ // If running from monorepo root
405
+ join(process.cwd(), 'e2e-tests/playwright/.e2e-ai-agents/flows.json'),
406
+ ];
407
+ const path = possiblePaths.find((p) => existsSync(p));
408
+ if (!path) {
409
+ return {}; // Return empty object if flows.json not found
410
+ }
411
+ try {
412
+ const content = readFileSync(path, 'utf-8');
413
+ const data = JSON.parse(content);
414
+ return data.flowGroups || {};
415
+ }
416
+ catch (error) {
417
+ return {}; // Return empty object on parse error
418
+ }
419
+ }
420
+ /**
421
+ * Group related flows into test journeys based on flowGroup metadata
422
+ */
423
+ function groupFlowsByRelationships(impactedFlows, flowCatalog) {
424
+ const groups = [];
425
+ const grouped = new Set();
426
+ // Load flow group definitions from flows.json
427
+ const flowGroupDefs = loadFlowGroups();
428
+ // For each flow group definition
429
+ for (const [groupId, groupDef] of Object.entries(flowGroupDefs)) {
430
+ const groupFlows = groupDef.flows || [];
431
+ // Find which flows in this group are impacted
432
+ const affectedInGroup = impactedFlows.filter((impact) => groupFlows.includes(impact.flow.id));
433
+ if (affectedInGroup.length > 0) {
434
+ groups.push({
435
+ id: groupId,
436
+ name: groupDef.name || groupId,
437
+ description: groupDef.description || '',
438
+ flows: groupFlows,
439
+ testStrategy: groupDef.testStrategy || 'mixed',
440
+ priority: groupDef.priority || 'P1',
441
+ affectedFlows: affectedInGroup,
442
+ });
443
+ // Mark as grouped
444
+ affectedInGroup.forEach((impact) => grouped.add(impact.flow.id));
445
+ }
446
+ }
447
+ // Remaining ungrouped flows
448
+ const ungrouped = impactedFlows.filter((impact) => !grouped.has(impact.flow.id));
449
+ return { groups, ungrouped };
450
+ }
451
+ /**
452
+ * Analyze a single file to extract functions, classes, and imports
453
+ */
454
+ /**
455
+ * Find the git repository root directory
456
+ */
457
+ function findGitRoot() {
458
+ let currentDir = process.cwd();
459
+ const root = resolve('/');
460
+ while (currentDir !== root) {
461
+ if (existsSync(join(currentDir, '.git'))) {
462
+ return currentDir;
463
+ }
464
+ currentDir = resolve(currentDir, '..');
465
+ }
466
+ // Fallback to process.cwd()
467
+ return process.cwd();
468
+ }
469
+ function analyzeFile(filePath, status) {
470
+ const repoRoot = findGitRoot();
471
+ const fullPath = join(repoRoot, filePath);
472
+ const stats = getFileDiffStats(filePath);
473
+ // Extract imports, functions, and classes from the file
474
+ const functions = [];
475
+ const classes = [];
476
+ const imports = [];
477
+ try {
478
+ const content = readFileSync(fullPath, 'utf-8');
479
+ // Extract imports (handles: import X from 'Y', import * as X from 'Y', etc.)
480
+ const importRegex = /import\s+(?:{([^}]+)}|(?:\*\s+as\s+)?(\w+))\s+from\s+['"]([^'"]+)['"]/g;
481
+ let importMatch;
482
+ while ((importMatch = importRegex.exec(content)) !== null) {
483
+ const namedImports = importMatch[1]?.split(',').map((s) => s.trim().split(/\s+as\s+/)[0]) || [];
484
+ const defaultImport = importMatch[2] ? [importMatch[2]] : [];
485
+ const allNames = [...namedImports, ...defaultImport].filter(Boolean);
486
+ if (allNames.length > 0) {
487
+ imports.push({
488
+ from: importMatch[3],
489
+ names: allNames,
490
+ });
491
+ }
492
+ }
493
+ // Extract function declarations (handles: function X, const X = () => {}, export function X)
494
+ const functionRegex = /(?:export\s+)?(?:async\s+)?(?:function\s+(\w+)|const\s+(\w+)\s*=)/g;
495
+ let functionMatch;
496
+ const seenFunctions = new Set();
497
+ while ((functionMatch = functionRegex.exec(content)) !== null) {
498
+ const funcName = functionMatch[1] || functionMatch[2];
499
+ if (funcName && !seenFunctions.has(funcName)) {
500
+ functions.push(funcName);
501
+ seenFunctions.add(funcName);
502
+ }
503
+ }
504
+ // Extract class declarations
505
+ const classRegex = /(?:export\s+)?class\s+(\w+)/g;
506
+ let classMatch;
507
+ while ((classMatch = classRegex.exec(content)) !== null) {
508
+ if (classMatch[1]) {
509
+ classes.push(classMatch[1]);
510
+ }
511
+ }
512
+ }
513
+ catch {
514
+ // If file doesn't exist or can't be read, continue with empty arrays
515
+ }
516
+ return {
517
+ file: filePath,
518
+ status: status,
519
+ linesAdded: stats.linesAdded,
520
+ linesRemoved: stats.linesRemoved,
521
+ functions,
522
+ classes,
523
+ imports,
524
+ };
525
+ }
526
+ /**
527
+ * Generate actionable recommendations based on impact
528
+ */
529
+ function generateRecommendations(impactedFlows) {
530
+ const recommendations = [];
531
+ const p0Flows = impactedFlows.filter((f) => f.flow.priority === 'P0');
532
+ const p1Flows = impactedFlows.filter((f) => f.flow.priority === 'P1');
533
+ if (p0Flows.length > 0) {
534
+ recommendations.push(`✅ Run critical (P0) flow tests immediately: ${p0Flows.map((f) => f.flow.id).join(', ')}`);
535
+ }
536
+ if (p1Flows.length > 0) {
537
+ recommendations.push(`🟡 Run high-priority (P1) flow tests: ${p1Flows.map((f) => f.flow.id).join(', ')}`);
538
+ }
539
+ const gapFlows = impactedFlows.filter((f) => f.testGaps.length > 0);
540
+ if (gapFlows.length > 0) {
541
+ recommendations.push(`📝 Generate tests to cover ${gapFlows.length} flow(s) with test gaps`);
542
+ }
543
+ const lowConfidenceFlows = impactedFlows.filter((f) => f.confidence < 60);
544
+ if (lowConfidenceFlows.length > 0) {
545
+ recommendations.push(`🔍 Review ${lowConfidenceFlows.length} flow(s) with low confidence matches (< 60%)`);
546
+ }
547
+ return recommendations;
548
+ }
@@ -0,0 +1,22 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ /**
4
+ * Agent Module Exports
5
+ *
6
+ * Centralizes all agent-related functionality including:
7
+ * - Impact analysis (detecting affected flows from code changes)
8
+ * - Model routing (intelligent LLM model selection for cost optimization)
9
+ * - Telemetry (metrics collection and reporting)
10
+ * - Report generation (formatted output)
11
+ * - Spec building (test specification generation)
12
+ */
13
+ // Impact Analysis
14
+ export { analyzeImpact, detectComparisonBase, getGitChanges, loadFlowCatalog, matchFlowToChanges, findExistingTests, identifyTestGaps, } from './impact-analyzer.js';
15
+ // Model Routing (Cost Optimization - TinyDancer pattern)
16
+ export { ModelRouter } from './model-router.js';
17
+ // Telemetry (Metrics Collection)
18
+ export { TelemetryCollector } from './telemetry.js';
19
+ // Report Generation (Console, Markdown, JSON)
20
+ export { generateReports } from './report-generator.js';
21
+ // Spec Bridge (PDF → Playwright specs)
22
+ export { SpecBridge, createAnthropicBridge, createOllamaBridge } from './spec-builder.js';