@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,324 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { mkdirSync, writeFileSync } from 'fs';
4
+ import { dirname, join } from 'path';
5
+ import { minimatch } from 'minimatch';
6
+ const DEFAULT_POLICY = {
7
+ minConfidenceForTargeted: 60,
8
+ safeMergeMinConfidence: 85,
9
+ forceFullOnWarningsAtOrAbove: 2,
10
+ forceFullOnP0WithGaps: true,
11
+ forceFullOnRiskyFiles: true,
12
+ riskyFilePatterns: [
13
+ '**/auth/**',
14
+ '**/login/**',
15
+ '**/permissions/**',
16
+ '**/admin/**',
17
+ '**/security/**',
18
+ '**/migrations/**',
19
+ '**/schema/**',
20
+ '**/*.sql',
21
+ '**/webhook/**',
22
+ ],
23
+ };
24
+ function countPriority(flows) {
25
+ const counts = { p0: 0, p1: 0, p2: 0 };
26
+ for (const flow of flows) {
27
+ if (flow.priority === 'P0')
28
+ counts.p0 += 1;
29
+ else if (flow.priority === 'P1')
30
+ counts.p1 += 1;
31
+ else
32
+ counts.p2 += 1;
33
+ }
34
+ return counts;
35
+ }
36
+ function computeConfidence(impact, p0, p1) {
37
+ let confidence = 85;
38
+ confidence -= Math.min(25, impact.warnings.length * 8);
39
+ confidence -= Math.min(20, impact.gaps.length * 5);
40
+ if (p0 > 0) {
41
+ confidence -= Math.min(10, p0 * 3);
42
+ }
43
+ else if (p1 > 0) {
44
+ confidence -= Math.min(6, p1 * 2);
45
+ }
46
+ if (impact.impactModel) {
47
+ if (impact.impactModel.flowMapping === 'catalog') {
48
+ confidence += 4;
49
+ }
50
+ if (impact.impactModel.testMapping === 'catalog') {
51
+ confidence += 4;
52
+ }
53
+ else if (impact.impactModel.testMapping === 'traceability') {
54
+ confidence += 6;
55
+ }
56
+ if (impact.impactModel.confidenceClass === 'medium') {
57
+ confidence -= 4;
58
+ }
59
+ else if (impact.impactModel.confidenceClass === 'low') {
60
+ confidence -= 12;
61
+ }
62
+ if (impact.impactModel.traceability) {
63
+ if (!impact.impactModel.traceability.manifestFound) {
64
+ confidence -= 6;
65
+ }
66
+ else if (impact.impactModel.traceability.coverageRatio >= 0.8) {
67
+ confidence += 2;
68
+ }
69
+ else if (impact.impactModel.traceability.coverageRatio < 0.5) {
70
+ confidence -= 4;
71
+ }
72
+ }
73
+ if (impact.impactModel.dependencyGraph?.truncated) {
74
+ confidence -= 6;
75
+ }
76
+ if (impact.impactModel.dependencyGraph && impact.impactModel.dependencyGraph.expandedFiles > 0) {
77
+ confidence += 2;
78
+ }
79
+ }
80
+ return Math.max(0, Math.min(100, confidence));
81
+ }
82
+ function findRiskyFiles(changedFiles, patterns) {
83
+ const risky = changedFiles.filter((file) => patterns.some((pattern) => minimatch(file, pattern, { matchBase: true })));
84
+ return [...new Set(risky)];
85
+ }
86
+ function pickRunSet(impact, p0, confidence, policy) {
87
+ const reasons = [];
88
+ const triggeredRules = [];
89
+ const riskyFiles = findRiskyFiles(impact.changedFiles, policy.riskyFilePatterns);
90
+ if (impact.warnings.length > 0) {
91
+ reasons.push('Impact analysis emitted warnings; broader safety coverage is recommended.');
92
+ }
93
+ if (impact.gaps.length > 0) {
94
+ reasons.push('Uncovered P0/P1 flows were detected.');
95
+ }
96
+ if (p0 > 0) {
97
+ reasons.push('P0 flows are impacted by this change set.');
98
+ }
99
+ if (policy.forceFullOnRiskyFiles && riskyFiles.length > 0) {
100
+ triggeredRules.push('risky-files');
101
+ reasons.push(`Risky file patterns matched: ${riskyFiles.join(', ')}`);
102
+ }
103
+ if (impact.impactModel?.confidenceClass === 'low') {
104
+ triggeredRules.push('low-traceability');
105
+ reasons.push('Impact mapping confidence is low (heuristic traceability).');
106
+ }
107
+ if (impact.impactModel?.traceability?.manifestFound && impact.impactModel.traceability.coverageRatio < 0.4) {
108
+ triggeredRules.push('traceability-low-coverage');
109
+ reasons.push('Traceability manifest coverage is low for impacted flows; broader safety run is recommended.');
110
+ }
111
+ if (impact.impactModel?.dependencyGraph?.truncated) {
112
+ triggeredRules.push('dependency-graph-truncated');
113
+ reasons.push('Dependency graph expansion was truncated; broader safety run is recommended.');
114
+ }
115
+ if (confidence < policy.minConfidenceForTargeted) {
116
+ triggeredRules.push('low-confidence');
117
+ }
118
+ if (impact.warnings.length >= policy.forceFullOnWarningsAtOrAbove) {
119
+ triggeredRules.push('warning-threshold');
120
+ }
121
+ if (policy.forceFullOnP0WithGaps && p0 > 0 && impact.gaps.length > 0) {
122
+ triggeredRules.push('p0-with-gaps');
123
+ }
124
+ if (triggeredRules.length > 0) {
125
+ return {
126
+ runSet: 'full',
127
+ reasons: reasons.length > 0 ? reasons : ['Low confidence in targeted recommendation.'],
128
+ triggeredRules,
129
+ riskyFiles,
130
+ };
131
+ }
132
+ if (impact.recommendedTests && impact.recommendedTests.length > 0) {
133
+ return {
134
+ runSet: 'targeted',
135
+ reasons: reasons.length > 0 ? reasons : ['Sufficient confidence for targeted run list.'],
136
+ triggeredRules,
137
+ riskyFiles,
138
+ };
139
+ }
140
+ return {
141
+ runSet: 'smoke',
142
+ reasons: reasons.length > 0 ? reasons : ['No targeted tests were mapped from the impacted flows.'],
143
+ triggeredRules,
144
+ riskyFiles,
145
+ };
146
+ }
147
+ function buildDecision(runSet, confidence, impact, policy) {
148
+ if (impact.gaps.length > 0) {
149
+ return {
150
+ action: 'must-add-tests',
151
+ title: 'Must add tests',
152
+ summary: `Detected ${impact.gaps.length} uncovered P0/P1 flow(s). Add or update tests before merge.`,
153
+ };
154
+ }
155
+ if (runSet === 'smoke' && confidence >= policy.safeMergeMinConfidence && impact.warnings.length === 0) {
156
+ return {
157
+ action: 'safe-to-merge',
158
+ title: 'Safe to merge',
159
+ summary: 'No critical coverage gaps were detected and policy confidence is high.',
160
+ };
161
+ }
162
+ return {
163
+ action: 'run-now',
164
+ title: 'Run now',
165
+ summary: `Execute the ${runSet} suite for this change set.`,
166
+ };
167
+ }
168
+ export function buildPlanFromImpactReport(impact, policyOverride) {
169
+ if (impact.mode !== 'impact') {
170
+ throw new Error(`Plan generation requires impact report data, received mode=${impact.mode}`);
171
+ }
172
+ const policy = { ...DEFAULT_POLICY, ...(policyOverride || {}) };
173
+ const { p0, p1, p2 } = countPriority(impact.flows);
174
+ const confidence = computeConfidence(impact, p0, p1);
175
+ const runSet = pickRunSet(impact, p0, confidence, policy);
176
+ const decision = buildDecision(runSet.runSet, confidence, impact, policy);
177
+ const requiredNewTests = impact.gaps.map((flow) => `${flow.id}: ${flow.name}`);
178
+ return {
179
+ schemaVersion: '1.0.0',
180
+ generatedAt: new Date().toISOString(),
181
+ source: 'impact',
182
+ runSet: runSet.runSet,
183
+ confidence,
184
+ reasons: runSet.reasons,
185
+ recommendedTests: impact.recommendedTests || [],
186
+ requiredNewTests,
187
+ policy: {
188
+ riskyFiles: runSet.riskyFiles,
189
+ triggeredRules: runSet.triggeredRules,
190
+ applied: policy,
191
+ },
192
+ decision,
193
+ metrics: {
194
+ changedFiles: impact.changedFiles.length,
195
+ impactedFlows: impact.flows.length,
196
+ p0Flows: p0,
197
+ p1Flows: p1,
198
+ p2Flows: p2,
199
+ uncoveredP0P1Flows: impact.gaps.length,
200
+ warnings: impact.warnings.length,
201
+ },
202
+ };
203
+ }
204
+ export function attachDeveloperActions(plan, context) {
205
+ const safeSince = context.sinceRef ? ` --since "${context.sinceRef}"` : '';
206
+ const runRecommendedTests = plan.recommendedTests.length > 0
207
+ ? `node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('${context.testsRoot}/.e2e-ai-agents/plan.json','utf8')); const tests=p.recommendedTests.map((t)=>t.replace(/ \\(flags:.*\\)$/,'')); console.log(tests.join(' '));" | xargs npx playwright test`
208
+ : undefined;
209
+ return {
210
+ ...plan,
211
+ nextActions: {
212
+ requiresUserApprovalForGeneration: true,
213
+ runRecommendedTests,
214
+ runSmokeSuite: 'npx playwright test --grep @smoke --project=chrome',
215
+ runFullSuite: 'npx playwright test --project=chrome',
216
+ approveAndGenerate: `npx e2e-ai-agents approve-and-generate --path "${context.appPath}" --tests-root "${context.testsRoot}" --pipeline --pipeline-mcp${safeSince}`,
217
+ generateMissingTests: `npx e2e-ai-agents approve-and-generate --path "${context.appPath}" --tests-root "${context.testsRoot}" --pipeline${safeSince}`,
218
+ healGeneratedTests: `npx e2e-ai-agents approve-and-generate --path "${context.appPath}" --tests-root "${context.testsRoot}" --pipeline --pipeline-mcp${safeSince}`,
219
+ commitGeneratedTests: `npx e2e-ai-agents finalize-generated-tests --path "${context.appPath}" --tests-root "${context.testsRoot}" --commit-message "test(e2e): add generated coverage and healed specs"`,
220
+ openPullRequest: `npx e2e-ai-agents finalize-generated-tests --path "${context.appPath}" --tests-root "${context.testsRoot}" --create-pr`,
221
+ },
222
+ };
223
+ }
224
+ export function writePlanReport(appRoot, plan) {
225
+ const baseDir = join(appRoot, '.e2e-ai-agents');
226
+ mkdirSync(baseDir, { recursive: true });
227
+ const planPath = join(baseDir, 'plan.json');
228
+ writeFileSync(planPath, JSON.stringify(plan, null, 2), 'utf-8');
229
+ return planPath;
230
+ }
231
+ export function renderCiSummaryMarkdown(plan) {
232
+ const lines = [];
233
+ lines.push(`## E2E Agent Recommendation: ${plan.decision.title}`);
234
+ lines.push('');
235
+ lines.push(`- Action: \`${plan.decision.action}\``);
236
+ lines.push(`- Run set: \`${plan.runSet}\``);
237
+ lines.push(`- Confidence: \`${plan.confidence}\``);
238
+ lines.push(`- Summary: ${plan.decision.summary}`);
239
+ if (plan.policy.triggeredRules.length > 0) {
240
+ lines.push(`- Policy triggers: ${plan.policy.triggeredRules.join(', ')}`);
241
+ }
242
+ if (plan.policy.riskyFiles.length > 0) {
243
+ lines.push(`- Risky files: ${plan.policy.riskyFiles.join(', ')}`);
244
+ }
245
+ if (plan.recommendedTests.length > 0) {
246
+ lines.push('');
247
+ lines.push('### Recommended Tests to Run');
248
+ for (const test of plan.recommendedTests) {
249
+ lines.push(`- ${test}`);
250
+ }
251
+ }
252
+ if (plan.requiredNewTests.length > 0) {
253
+ lines.push('');
254
+ lines.push('### Required New Tests');
255
+ for (const gap of plan.requiredNewTests) {
256
+ lines.push(`- ${gap}`);
257
+ }
258
+ }
259
+ if (plan.nextActions) {
260
+ lines.push('');
261
+ lines.push('### PR Actions');
262
+ if (plan.nextActions.runRecommendedTests) {
263
+ lines.push(`- Run recommended tests: \`${plan.nextActions.runRecommendedTests}\``);
264
+ }
265
+ else if (plan.nextActions.runSmokeSuite) {
266
+ lines.push(`- Run smoke fallback: \`${plan.nextActions.runSmokeSuite}\``);
267
+ }
268
+ if (plan.nextActions.approveAndGenerate || plan.nextActions.generateMissingTests) {
269
+ lines.push(`- Approve and generate missing tests: \`${plan.nextActions.approveAndGenerate || plan.nextActions.generateMissingTests}\``);
270
+ }
271
+ if (plan.nextActions.healGeneratedTests) {
272
+ lines.push(`- Heal generated tests: \`${plan.nextActions.healGeneratedTests}\``);
273
+ }
274
+ if (plan.nextActions.commitGeneratedTests) {
275
+ lines.push(`- Commit generated artifacts: \`${plan.nextActions.commitGeneratedTests}\``);
276
+ }
277
+ if (plan.nextActions.openPullRequest) {
278
+ lines.push(`- Open PR with generated updates: \`${plan.nextActions.openPullRequest}\``);
279
+ }
280
+ }
281
+ if (plan.insights?.qualityGates) {
282
+ if (plan.insights.qualityGates.failed.length > 0) {
283
+ lines.push('');
284
+ lines.push('### Quality Gates Failed');
285
+ for (const gate of plan.insights.qualityGates.failed) {
286
+ lines.push(`- ${gate.name}${gate.details ? `: ${gate.details}` : ''}`);
287
+ }
288
+ }
289
+ if (plan.insights.qualityGates.warnings.length > 0) {
290
+ lines.push('');
291
+ lines.push('### Quality Gate Warnings');
292
+ for (const gate of plan.insights.qualityGates.warnings) {
293
+ lines.push(`- ${gate.name}${gate.details ? `: ${gate.details}` : ''}`);
294
+ }
295
+ }
296
+ }
297
+ if (plan.insights?.flaky && plan.insights.flaky.highRiskRecommendedTests.length > 0) {
298
+ lines.push('');
299
+ lines.push('### Flaky Risk Alerts');
300
+ for (const item of plan.insights.flaky.highRiskRecommendedTests) {
301
+ const rate = item.flakeRate30d !== undefined ? item.flakeRate30d : item.flakeRate;
302
+ const trend = item.trend ? `, trend=${item.trend}` : '';
303
+ const subsystem = item.subsystem ? `, subsystem=${item.subsystem}` : '';
304
+ const qstate = item.quarantineState && item.quarantineState !== 'none' ? `, quarantine=${item.quarantineState}` : '';
305
+ lines.push(`- ${item.test} (flakeRate=${rate}${trend}${subsystem}${qstate})`);
306
+ }
307
+ if (plan.insights.flaky.ownerMentions && plan.insights.flaky.ownerMentions.length > 0) {
308
+ lines.push(`- Notify owners: ${plan.insights.flaky.ownerMentions.join(', ')}`);
309
+ }
310
+ }
311
+ if (plan.insights?.calibration) {
312
+ lines.push('');
313
+ lines.push('### Historical Calibration');
314
+ lines.push(`- precision=${plan.insights.calibration.precision}, recall=${plan.insights.calibration.recall}, falseNegativeRate=${plan.insights.calibration.falseNegativeRate}`);
315
+ }
316
+ return lines.join('\n');
317
+ }
318
+ export function writeCiSummary(appRoot, markdown, relativePath = '.e2e-ai-agents/ci-summary.md') {
319
+ const fullPath = join(appRoot, relativePath);
320
+ const dir = dirname(fullPath);
321
+ mkdirSync(dir, { recursive: true });
322
+ writeFileSync(fullPath, markdown, 'utf-8');
323
+ return fullPath;
324
+ }
@@ -0,0 +1,123 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { readFileSync } from 'fs';
4
+ import { resolve } from 'path';
5
+ import { normalizePath } from './utils.js';
6
+ function asRecord(value) {
7
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
8
+ return null;
9
+ }
10
+ return value;
11
+ }
12
+ function asArray(value) {
13
+ return Array.isArray(value) ? value : [];
14
+ }
15
+ function isFailureStatus(value) {
16
+ return value === 'failed' || value === 'timedOut' || value === 'interrupted';
17
+ }
18
+ function classifyTestInstability(testNode) {
19
+ const outcome = typeof testNode.outcome === 'string' ? testNode.outcome : '';
20
+ if (outcome === 'unexpected') {
21
+ return 'failed';
22
+ }
23
+ if (outcome === 'flaky') {
24
+ return 'flaky';
25
+ }
26
+ const status = typeof testNode.status === 'string' ? testNode.status : '';
27
+ if (status === 'flaky') {
28
+ return 'flaky';
29
+ }
30
+ if (isFailureStatus(status)) {
31
+ return 'failed';
32
+ }
33
+ let sawFailure = false;
34
+ let sawPass = false;
35
+ for (const resultValue of asArray(testNode.results)) {
36
+ const resultNode = asRecord(resultValue);
37
+ if (!resultNode) {
38
+ continue;
39
+ }
40
+ const resultStatus = typeof resultNode.status === 'string' ? resultNode.status : '';
41
+ if (isFailureStatus(resultStatus)) {
42
+ sawFailure = true;
43
+ }
44
+ else if (resultStatus === 'passed') {
45
+ sawPass = true;
46
+ }
47
+ }
48
+ if (sawFailure && sawPass) {
49
+ return 'flaky';
50
+ }
51
+ if (sawFailure) {
52
+ return 'failed';
53
+ }
54
+ return 'stable';
55
+ }
56
+ function relativize(pathValue, roots) {
57
+ const normalized = normalizePath(pathValue);
58
+ for (const root of roots) {
59
+ const normalizedRoot = normalizePath(resolve(root));
60
+ if (normalized === normalizedRoot) {
61
+ return '.';
62
+ }
63
+ if (normalized.startsWith(`${normalizedRoot}/`)) {
64
+ return normalized.slice(normalizedRoot.length + 1);
65
+ }
66
+ }
67
+ return normalized;
68
+ }
69
+ function collectUnstableSpecs(value, roots, unstableMap) {
70
+ const node = asRecord(value);
71
+ if (!node) {
72
+ return;
73
+ }
74
+ for (const specValue of asArray(node.specs)) {
75
+ const specNode = asRecord(specValue);
76
+ if (!specNode || typeof specNode.file !== 'string') {
77
+ continue;
78
+ }
79
+ let failingTests = 0;
80
+ let flakyTests = 0;
81
+ for (const testValue of asArray(specNode.tests)) {
82
+ const testNode = asRecord(testValue);
83
+ if (!testNode) {
84
+ continue;
85
+ }
86
+ const instability = classifyTestInstability(testNode);
87
+ if (instability === 'failed') {
88
+ failingTests += 1;
89
+ }
90
+ else if (instability === 'flaky') {
91
+ flakyTests += 1;
92
+ }
93
+ }
94
+ if (failingTests === 0 && flakyTests === 0) {
95
+ continue;
96
+ }
97
+ const specPath = relativize(specNode.file, roots);
98
+ const existing = unstableMap.get(specPath);
99
+ const mergedFailing = (existing?.failingTests || 0) + failingTests;
100
+ const mergedFlaky = (existing?.flakyTests || 0) + flakyTests;
101
+ unstableMap.set(specPath, {
102
+ specPath,
103
+ status: mergedFailing > 0 ? 'failed' : 'flaky',
104
+ failingTests: mergedFailing,
105
+ flakyTests: mergedFlaky,
106
+ });
107
+ }
108
+ for (const suite of asArray(node.suites)) {
109
+ collectUnstableSpecs(suite, roots, unstableMap);
110
+ }
111
+ }
112
+ export function extractPlaywrightUnstableSpecs(reportPath, roots) {
113
+ const fullPath = resolve(reportPath);
114
+ const raw = JSON.parse(readFileSync(fullPath, 'utf-8'));
115
+ const unstableMap = new Map();
116
+ collectUnstableSpecs(raw, roots, unstableMap);
117
+ return Array.from(unstableMap.values()).sort((a, b) => {
118
+ if (a.status !== b.status) {
119
+ return a.status === 'failed' ? -1 : 1;
120
+ }
121
+ return a.specPath.localeCompare(b.specPath);
122
+ });
123
+ }