@yasserkhanorg/e2e-agents 1.8.5 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/README.md +95 -8
  2. package/dist/adapters/cypress.d.ts +10 -0
  3. package/dist/adapters/cypress.d.ts.map +1 -0
  4. package/dist/adapters/cypress.js +86 -0
  5. package/dist/adapters/framework_adapter.d.ts +41 -0
  6. package/dist/adapters/framework_adapter.d.ts.map +1 -0
  7. package/dist/adapters/framework_adapter.js +152 -0
  8. package/dist/adapters/playwright.d.ts +10 -0
  9. package/dist/adapters/playwright.d.ts.map +1 -0
  10. package/dist/adapters/playwright.js +86 -0
  11. package/dist/adapters/pytest.d.ts +10 -0
  12. package/dist/adapters/pytest.d.ts.map +1 -0
  13. package/dist/adapters/pytest.js +96 -0
  14. package/dist/adapters/supertest.d.ts +12 -0
  15. package/dist/adapters/supertest.d.ts.map +1 -0
  16. package/dist/adapters/supertest.js +85 -0
  17. package/dist/agent/config.d.ts +1 -1
  18. package/dist/agent/config.d.ts.map +1 -1
  19. package/dist/agent/git.d.ts +1 -0
  20. package/dist/agent/git.d.ts.map +1 -1
  21. package/dist/agent/git.js +3 -0
  22. package/dist/agentic/fix_loop.d.ts.map +1 -1
  23. package/dist/agentic/fix_loop.js +5 -4
  24. package/dist/agentic/runner.d.ts +2 -0
  25. package/dist/agentic/runner.d.ts.map +1 -1
  26. package/dist/agentic/runner.js +15 -12
  27. package/dist/agents/cross-impact.d.ts.map +1 -1
  28. package/dist/agents/cross-impact.js +6 -1
  29. package/dist/agents/executor.d.ts.map +1 -1
  30. package/dist/agents/executor.js +6 -1
  31. package/dist/agents/strategist.d.ts.map +1 -1
  32. package/dist/agents/strategist.js +6 -1
  33. package/dist/agents/test-designer.d.ts.map +1 -1
  34. package/dist/agents/test-designer.js +6 -1
  35. package/dist/anthropic_provider.d.ts.map +1 -1
  36. package/dist/anthropic_provider.js +1 -0
  37. package/dist/base_provider.d.ts +56 -0
  38. package/dist/base_provider.d.ts.map +1 -1
  39. package/dist/base_provider.js +123 -1
  40. package/dist/budget_ledger.d.ts +28 -0
  41. package/dist/budget_ledger.d.ts.map +1 -0
  42. package/dist/budget_ledger.js +62 -0
  43. package/dist/cache/cached_provider.d.ts +45 -0
  44. package/dist/cache/cached_provider.d.ts.map +1 -0
  45. package/dist/cache/cached_provider.js +88 -0
  46. package/dist/cache/response_cache.d.ts +79 -0
  47. package/dist/cache/response_cache.d.ts.map +1 -0
  48. package/dist/cache/response_cache.js +177 -0
  49. package/dist/cli/commands/bootstrap.d.ts +3 -0
  50. package/dist/cli/commands/bootstrap.d.ts.map +1 -0
  51. package/dist/cli/commands/bootstrap.js +109 -0
  52. package/dist/cli/commands/cost_report.d.ts +3 -0
  53. package/dist/cli/commands/cost_report.d.ts.map +1 -0
  54. package/dist/cli/commands/cost_report.js +115 -0
  55. package/dist/cli/commands/crew.d.ts.map +1 -1
  56. package/dist/cli/commands/crew.js +118 -1
  57. package/dist/cli/commands/gate.d.ts +3 -0
  58. package/dist/cli/commands/gate.d.ts.map +1 -0
  59. package/dist/cli/commands/gate.js +86 -0
  60. package/dist/cli/commands/init.d.ts.map +1 -1
  61. package/dist/cli/commands/init.js +7 -62
  62. package/dist/cli/commands/train.d.ts.map +1 -1
  63. package/dist/cli/commands/train.js +16 -21
  64. package/dist/cli/defaults.d.ts +35 -0
  65. package/dist/cli/defaults.d.ts.map +1 -0
  66. package/dist/cli/defaults.js +125 -0
  67. package/dist/cli/errors.d.ts +27 -0
  68. package/dist/cli/errors.d.ts.map +1 -0
  69. package/dist/cli/errors.js +57 -0
  70. package/dist/cli/parse_args.d.ts.map +1 -1
  71. package/dist/cli/parse_args.js +24 -2
  72. package/dist/cli/types.d.ts +7 -1
  73. package/dist/cli/types.d.ts.map +1 -1
  74. package/dist/cli.js +47 -2
  75. package/dist/crew/context.d.ts +15 -0
  76. package/dist/crew/context.d.ts.map +1 -1
  77. package/dist/crew/orchestrator.d.ts +14 -0
  78. package/dist/crew/orchestrator.d.ts.map +1 -1
  79. package/dist/crew/orchestrator.js +162 -4
  80. package/dist/crew/protocol.d.ts +13 -0
  81. package/dist/crew/protocol.d.ts.map +1 -1
  82. package/dist/crew/provider.d.ts +15 -1
  83. package/dist/crew/provider.d.ts.map +1 -1
  84. package/dist/crew/provider.js +24 -4
  85. package/dist/custom_provider.d.ts.map +1 -1
  86. package/dist/custom_provider.js +1 -0
  87. package/dist/engine/diff_loader.d.ts.map +1 -1
  88. package/dist/engine/diff_loader.js +3 -14
  89. package/dist/engine/impact_engine.d.ts.map +1 -1
  90. package/dist/engine/impact_engine.js +9 -23
  91. package/dist/esm/adapters/cypress.js +49 -0
  92. package/dist/esm/adapters/framework_adapter.js +114 -0
  93. package/dist/esm/adapters/playwright.js +49 -0
  94. package/dist/esm/adapters/pytest.js +59 -0
  95. package/dist/esm/adapters/supertest.js +48 -0
  96. package/dist/esm/agent/git.js +3 -1
  97. package/dist/esm/agentic/fix_loop.js +5 -4
  98. package/dist/esm/agentic/runner.js +15 -12
  99. package/dist/esm/agents/cross-impact.js +6 -1
  100. package/dist/esm/agents/executor.js +6 -1
  101. package/dist/esm/agents/strategist.js +6 -1
  102. package/dist/esm/agents/test-designer.js +6 -1
  103. package/dist/esm/anthropic_provider.js +1 -0
  104. package/dist/esm/base_provider.js +121 -0
  105. package/dist/esm/budget_ledger.js +58 -0
  106. package/dist/esm/cache/cached_provider.js +82 -0
  107. package/dist/esm/cache/response_cache.js +140 -0
  108. package/dist/esm/cli/commands/bootstrap.js +106 -0
  109. package/dist/esm/cli/commands/cost_report.js +112 -0
  110. package/dist/esm/cli/commands/crew.js +118 -1
  111. package/dist/esm/cli/commands/gate.js +83 -0
  112. package/dist/esm/cli/commands/init.js +3 -58
  113. package/dist/esm/cli/commands/train.js +16 -21
  114. package/dist/esm/cli/defaults.js +118 -0
  115. package/dist/esm/cli/errors.js +52 -0
  116. package/dist/esm/cli/parse_args.js +24 -2
  117. package/dist/esm/cli.js +47 -2
  118. package/dist/esm/crew/orchestrator.js +162 -4
  119. package/dist/esm/crew/provider.js +24 -4
  120. package/dist/esm/custom_provider.js +1 -0
  121. package/dist/esm/engine/diff_loader.js +1 -12
  122. package/dist/esm/engine/impact_engine.js +9 -23
  123. package/dist/esm/index.js +21 -0
  124. package/dist/esm/knowledge/api_surface.js +265 -34
  125. package/dist/esm/knowledge/cluster_utils.js +60 -0
  126. package/dist/esm/knowledge/failure_history.js +121 -0
  127. package/dist/esm/knowledge/kg_bridge.js +381 -0
  128. package/dist/esm/knowledge/kg_types.js +3 -0
  129. package/dist/esm/knowledge/route_families.js +119 -0
  130. package/dist/esm/mcp-server.js +2 -4
  131. package/dist/esm/metrics/prometheus.js +149 -0
  132. package/dist/esm/model_router.js +59 -0
  133. package/dist/esm/ollama_provider.js +1 -0
  134. package/dist/esm/openai_provider.js +1 -0
  135. package/dist/esm/pipeline/orchestrator.js +6 -12
  136. package/dist/esm/pipeline/stage0_preprocess.js +12 -19
  137. package/dist/esm/pipeline/stage1_impact.js +19 -3
  138. package/dist/esm/pipeline/stage2_coverage.js +29 -7
  139. package/dist/esm/pipeline/stage3_generation.js +21 -1
  140. package/dist/esm/progress.js +112 -0
  141. package/dist/esm/prompts/coverage.js +17 -24
  142. package/dist/esm/prompts/cross-impact.js +3 -21
  143. package/dist/esm/prompts/generation.js +201 -45
  144. package/dist/esm/prompts/generation_profile.js +147 -0
  145. package/dist/esm/prompts/heal.js +33 -15
  146. package/dist/esm/prompts/impact.js +3 -22
  147. package/dist/esm/prompts/json_extract.js +36 -0
  148. package/dist/esm/prompts/strategist.js +2 -20
  149. package/dist/esm/prompts/test-designer.js +6 -21
  150. package/dist/esm/provider_factory.js +6 -4
  151. package/dist/esm/reporters/junit.js +86 -0
  152. package/dist/esm/reporters/reporter.js +3 -0
  153. package/dist/esm/reporters/sarif.js +131 -0
  154. package/dist/esm/resilience/circuit_breaker.js +78 -0
  155. package/dist/esm/resilience/retry.js +56 -0
  156. package/dist/esm/sanitize.js +66 -0
  157. package/dist/esm/training/kg_scanner.js +115 -0
  158. package/dist/esm/training/scanner.js +27 -34
  159. package/dist/esm/validation/guardrails.js +5 -0
  160. package/dist/esm/version.js +33 -0
  161. package/dist/index.d.ts +21 -1
  162. package/dist/index.d.ts.map +1 -1
  163. package/dist/index.js +45 -1
  164. package/dist/knowledge/api_surface.d.ts +12 -0
  165. package/dist/knowledge/api_surface.d.ts.map +1 -1
  166. package/dist/knowledge/api_surface.js +268 -34
  167. package/dist/knowledge/cluster_utils.d.ts +28 -0
  168. package/dist/knowledge/cluster_utils.d.ts.map +1 -0
  169. package/dist/knowledge/cluster_utils.js +67 -0
  170. package/dist/knowledge/failure_history.d.ts +39 -0
  171. package/dist/knowledge/failure_history.d.ts.map +1 -0
  172. package/dist/knowledge/failure_history.js +128 -0
  173. package/dist/knowledge/kg_bridge.d.ts +31 -0
  174. package/dist/knowledge/kg_bridge.d.ts.map +1 -0
  175. package/dist/knowledge/kg_bridge.js +388 -0
  176. package/dist/knowledge/kg_types.d.ts +75 -0
  177. package/dist/knowledge/kg_types.d.ts.map +1 -0
  178. package/dist/knowledge/kg_types.js +4 -0
  179. package/dist/knowledge/route_families.d.ts +29 -0
  180. package/dist/knowledge/route_families.d.ts.map +1 -1
  181. package/dist/knowledge/route_families.js +122 -0
  182. package/dist/mcp-server.d.ts.map +1 -1
  183. package/dist/mcp-server.js +2 -4
  184. package/dist/metrics/prometheus.d.ts +37 -0
  185. package/dist/metrics/prometheus.d.ts.map +1 -0
  186. package/dist/metrics/prometheus.js +153 -0
  187. package/dist/model_router.d.ts +28 -0
  188. package/dist/model_router.d.ts.map +1 -0
  189. package/dist/model_router.js +63 -0
  190. package/dist/ollama_provider.d.ts.map +1 -1
  191. package/dist/ollama_provider.js +1 -0
  192. package/dist/openai_provider.d.ts.map +1 -1
  193. package/dist/openai_provider.js +1 -0
  194. package/dist/pipeline/orchestrator.d.ts +2 -0
  195. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  196. package/dist/pipeline/orchestrator.js +6 -12
  197. package/dist/pipeline/stage0_preprocess.d.ts.map +1 -1
  198. package/dist/pipeline/stage0_preprocess.js +11 -18
  199. package/dist/pipeline/stage1_impact.d.ts +1 -1
  200. package/dist/pipeline/stage1_impact.d.ts.map +1 -1
  201. package/dist/pipeline/stage1_impact.js +18 -2
  202. package/dist/pipeline/stage2_coverage.d.ts +2 -0
  203. package/dist/pipeline/stage2_coverage.d.ts.map +1 -1
  204. package/dist/pipeline/stage2_coverage.js +29 -7
  205. package/dist/pipeline/stage3_generation.d.ts +2 -0
  206. package/dist/pipeline/stage3_generation.d.ts.map +1 -1
  207. package/dist/pipeline/stage3_generation.js +21 -1
  208. package/dist/pipeline/stage4_heal.d.ts +2 -0
  209. package/dist/pipeline/stage4_heal.d.ts.map +1 -1
  210. package/dist/progress.d.ts +22 -0
  211. package/dist/progress.d.ts.map +1 -0
  212. package/dist/progress.js +116 -0
  213. package/dist/prompts/coverage.d.ts +2 -0
  214. package/dist/prompts/coverage.d.ts.map +1 -1
  215. package/dist/prompts/coverage.js +17 -24
  216. package/dist/prompts/cross-impact.d.ts +1 -0
  217. package/dist/prompts/cross-impact.d.ts.map +1 -1
  218. package/dist/prompts/cross-impact.js +3 -21
  219. package/dist/prompts/generation.d.ts +4 -2
  220. package/dist/prompts/generation.d.ts.map +1 -1
  221. package/dist/prompts/generation.js +201 -45
  222. package/dist/prompts/generation_profile.d.ts +29 -0
  223. package/dist/prompts/generation_profile.d.ts.map +1 -0
  224. package/dist/prompts/generation_profile.js +151 -0
  225. package/dist/prompts/heal.d.ts +3 -1
  226. package/dist/prompts/heal.d.ts.map +1 -1
  227. package/dist/prompts/heal.js +33 -15
  228. package/dist/prompts/impact.d.ts +1 -0
  229. package/dist/prompts/impact.d.ts.map +1 -1
  230. package/dist/prompts/impact.js +3 -22
  231. package/dist/prompts/json_extract.d.ts +14 -0
  232. package/dist/prompts/json_extract.d.ts.map +1 -0
  233. package/dist/prompts/json_extract.js +39 -0
  234. package/dist/prompts/strategist.d.ts.map +1 -1
  235. package/dist/prompts/strategist.js +2 -20
  236. package/dist/prompts/test-designer.d.ts +2 -0
  237. package/dist/prompts/test-designer.d.ts.map +1 -1
  238. package/dist/prompts/test-designer.js +6 -21
  239. package/dist/provider_factory.d.ts.map +1 -1
  240. package/dist/provider_factory.js +6 -4
  241. package/dist/reporters/junit.d.ts +6 -0
  242. package/dist/reporters/junit.d.ts.map +1 -0
  243. package/dist/reporters/junit.js +89 -0
  244. package/dist/reporters/reporter.d.ts +42 -0
  245. package/dist/reporters/reporter.d.ts.map +1 -0
  246. package/dist/reporters/reporter.js +4 -0
  247. package/dist/reporters/sarif.d.ts +7 -0
  248. package/dist/reporters/sarif.d.ts.map +1 -0
  249. package/dist/reporters/sarif.js +134 -0
  250. package/dist/resilience/circuit_breaker.d.ts +36 -0
  251. package/dist/resilience/circuit_breaker.d.ts.map +1 -0
  252. package/dist/resilience/circuit_breaker.js +82 -0
  253. package/dist/resilience/retry.d.ts +11 -0
  254. package/dist/resilience/retry.d.ts.map +1 -0
  255. package/dist/resilience/retry.js +59 -0
  256. package/dist/sanitize.d.ts +15 -0
  257. package/dist/sanitize.d.ts.map +1 -0
  258. package/dist/sanitize.js +71 -0
  259. package/dist/training/kg_scanner.d.ts +13 -0
  260. package/dist/training/kg_scanner.d.ts.map +1 -0
  261. package/dist/training/kg_scanner.js +118 -0
  262. package/dist/training/scanner.d.ts +7 -2
  263. package/dist/training/scanner.d.ts.map +1 -1
  264. package/dist/training/scanner.js +27 -34
  265. package/dist/validation/guardrails.d.ts +2 -0
  266. package/dist/validation/guardrails.d.ts.map +1 -1
  267. package/dist/validation/guardrails.js +5 -0
  268. package/dist/validation/output_schema.d.ts +3 -0
  269. package/dist/validation/output_schema.d.ts.map +1 -1
  270. package/dist/version.d.ts +6 -0
  271. package/dist/version.d.ts.map +1 -0
  272. package/dist/version.js +36 -0
  273. package/package.json +7 -2
  274. package/schemas/route-families.schema.json +31 -1
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
2
  // See LICENSE.txt for license information.
3
+ import { extractJsonFromResponse } from './json_extract.js';
3
4
  import { formatSpecsForPrompt } from '../knowledge/spec_index.js';
4
5
  import { formatApiSurfaceForPrompt } from '../knowledge/api_surface.js';
5
6
  export function buildImpactPrompt(ctx) {
@@ -23,7 +24,7 @@ export function buildImpactPrompt(ctx) {
23
24
  })
24
25
  .join('\n\n');
25
26
  return [
26
- 'You are analyzing code changes in Mattermost to identify impacted user-facing flows.',
27
+ `You are analyzing code changes in ${ctx.projectName || 'Mattermost'} to identify impacted user-facing flows.`,
27
28
  '',
28
29
  `ROUTE FAMILY: ${ctx.family.id}`,
29
30
  `ROUTES: ${familyRoutes}`,
@@ -58,25 +59,5 @@ export function buildImpactPrompt(ctx) {
58
59
  ].filter(Boolean).join('\n');
59
60
  }
60
61
  export function parseImpactResponse(text) {
61
- // Try to extract JSON from the response
62
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
63
- const candidates = fenced ? [fenced[1], text] : [text];
64
- for (const candidate of candidates) {
65
- const start = candidate.indexOf('{');
66
- const end = candidate.lastIndexOf('}');
67
- if (start < 0 || end <= start) {
68
- continue;
69
- }
70
- const raw = candidate.slice(start, end + 1);
71
- try {
72
- const parsed = JSON.parse(raw);
73
- if (parsed && Array.isArray(parsed.flows)) {
74
- return parsed;
75
- }
76
- }
77
- catch {
78
- continue;
79
- }
80
- }
81
- return null;
62
+ return extractJsonFromResponse(text, (obj) => obj != null && typeof obj === 'object' && Array.isArray(obj.flows));
82
63
  }
@@ -0,0 +1,36 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ /**
4
+ * Shared JSON extraction from LLM text responses.
5
+ * Handles fenced code blocks, bare JSON, and partial text.
6
+ */
7
+ /**
8
+ * Extract and parse JSON from LLM response text.
9
+ * Tries fenced code blocks first, then raw text.
10
+ * Returns null if no valid JSON found.
11
+ *
12
+ * @param text - Raw LLM response text
13
+ * @param validate - Predicate to check if parsed object has the expected shape
14
+ */
15
+ export function extractJsonFromResponse(text, validate) {
16
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
17
+ const candidates = fenced ? [fenced[1], text] : [text];
18
+ for (const candidate of candidates) {
19
+ const start = candidate.indexOf('{');
20
+ const end = candidate.lastIndexOf('}');
21
+ if (start < 0 || end <= start) {
22
+ continue;
23
+ }
24
+ const raw = candidate.slice(start, end + 1);
25
+ try {
26
+ const parsed = JSON.parse(raw);
27
+ if (validate(parsed)) {
28
+ return parsed;
29
+ }
30
+ }
31
+ catch {
32
+ continue;
33
+ }
34
+ }
35
+ return null;
36
+ }
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
2
  // See LICENSE.txt for license information.
3
3
  import { sanitizeForPrompt } from '../crew/sanitize.js';
4
+ import { extractJsonFromResponse } from './json_extract.js';
4
5
  export function buildStrategistPrompt(ctx) {
5
6
  const flowsBlock = ctx.impactedFlows
6
7
  .map((f) => {
@@ -56,24 +57,5 @@ export function buildStrategistPrompt(ctx) {
56
57
  ].join('\n');
57
58
  }
58
59
  export function parseStrategistResponse(text) {
59
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
60
- const candidates = fenced ? [fenced[1], text] : [text];
61
- for (const candidate of candidates) {
62
- const start = candidate.indexOf('{');
63
- const end = candidate.lastIndexOf('}');
64
- if (start < 0 || end <= start) {
65
- continue;
66
- }
67
- const raw = candidate.slice(start, end + 1);
68
- try {
69
- const parsed = JSON.parse(raw);
70
- if (parsed && Array.isArray(parsed.strategy)) {
71
- return parsed;
72
- }
73
- }
74
- catch {
75
- continue;
76
- }
77
- }
78
- return null;
60
+ return extractJsonFromResponse(text, (obj) => obj != null && typeof obj === 'object' && Array.isArray(obj.strategy));
79
61
  }
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
2
  // See LICENSE.txt for license information.
3
+ import { extractJsonFromResponse } from './json_extract.js';
3
4
  import { formatApiSurfaceForPrompt } from '../knowledge/api_surface.js';
4
5
  import { sanitizeForPrompt } from '../crew/sanitize.js';
5
6
  export function buildTestDesignerPrompt(ctx) {
@@ -24,7 +25,7 @@ export function buildTestDesignerPrompt(ctx) {
24
25
  : 'None detected.';
25
26
  const categories = ctx.strategy.testCategories.join(', ');
26
27
  return [
27
- 'You are a senior QA engineer designing comprehensive test cases for a Mattermost user flow.',
28
+ `You are a senior QA engineer designing comprehensive test cases for a ${ctx.profile?.projectName || 'Mattermost'} user flow.`,
28
29
  '',
29
30
  `FLOW: ${ctx.flow.flowName}`,
30
31
  `Flow ID: ${ctx.flow.flowId}`,
@@ -84,24 +85,8 @@ export function buildTestDesignerPrompt(ctx) {
84
85
  ].join('\n');
85
86
  }
86
87
  export function parseTestDesignerResponse(text) {
87
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
88
- const candidates = fenced ? [fenced[1], text] : [text];
89
- for (const candidate of candidates) {
90
- const start = candidate.indexOf('{');
91
- const end = candidate.lastIndexOf('}');
92
- if (start < 0 || end <= start) {
93
- continue;
94
- }
95
- const raw = candidate.slice(start, end + 1);
96
- try {
97
- const parsed = JSON.parse(raw);
98
- if (parsed?.testDesign?.testCases && Array.isArray(parsed.testDesign.testCases)) {
99
- return parsed;
100
- }
101
- }
102
- catch {
103
- continue;
104
- }
105
- }
106
- return null;
88
+ return extractJsonFromResponse(text, (obj) => {
89
+ const r = obj;
90
+ return r?.testDesign?.testCases != null && Array.isArray(r.testDesign.testCases);
91
+ });
107
92
  }
@@ -263,15 +263,17 @@ class HybridProvider {
263
263
  const primaryStats = this.primary.getUsageStats();
264
264
  const fallbackStats = this.fallback.getUsageStats();
265
265
  // Combine stats
266
+ const totalRequests = primaryStats.requestCount + fallbackStats.requestCount;
266
267
  return {
267
- requestCount: primaryStats.requestCount + fallbackStats.requestCount,
268
+ requestCount: totalRequests,
268
269
  totalInputTokens: primaryStats.totalInputTokens + fallbackStats.totalInputTokens,
269
270
  totalOutputTokens: primaryStats.totalOutputTokens + fallbackStats.totalOutputTokens,
270
271
  totalTokens: primaryStats.totalTokens + fallbackStats.totalTokens,
271
272
  totalCost: primaryStats.totalCost + fallbackStats.totalCost,
272
- averageResponseTimeMs: (primaryStats.averageResponseTimeMs * primaryStats.requestCount +
273
- fallbackStats.averageResponseTimeMs * fallbackStats.requestCount) /
274
- (primaryStats.requestCount + fallbackStats.requestCount),
273
+ averageResponseTimeMs: totalRequests > 0
274
+ ? (primaryStats.averageResponseTimeMs * primaryStats.requestCount +
275
+ fallbackStats.averageResponseTimeMs * fallbackStats.requestCount) / totalRequests
276
+ : 0,
275
277
  failedRequests: primaryStats.failedRequests + fallbackStats.failedRequests,
276
278
  startTime: new Date(Math.min(primaryStats.startTime.getTime(), fallbackStats.startTime.getTime())),
277
279
  lastUpdated: new Date(Math.max(primaryStats.lastUpdated.getTime(), fallbackStats.lastUpdated.getTime())),
@@ -0,0 +1,86 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ function escapeXml(str) {
4
+ return str
5
+ .replace(/&/g, '&amp;')
6
+ .replace(/</g, '&lt;')
7
+ .replace(/>/g, '&gt;')
8
+ .replace(/"/g, '&quot;')
9
+ .replace(/'/g, '&apos;');
10
+ }
11
+ function buildTestCase(tc, flowName) {
12
+ const className = escapeXml(flowName.replace(/\s+/g, '.'));
13
+ const testName = escapeXml(tc.name);
14
+ return ` <testcase classname="${className}" name="${testName}" status="${escapeXml(tc.priority)}">\n` +
15
+ ` <properties>\n` +
16
+ ` <property name="type" value="${escapeXml(tc.type)}" />\n` +
17
+ ` <property name="priority" value="${escapeXml(tc.priority)}" />\n` +
18
+ ` </properties>\n` +
19
+ ` </testcase>`;
20
+ }
21
+ function buildFailureCase(finding) {
22
+ const name = escapeXml(finding.title);
23
+ return ` <testcase classname="findings" name="${name}">\n` +
24
+ ` <failure message="${escapeXml(finding.title)}" type="${escapeXml(finding.severity)}">${escapeXml(finding.description)}</failure>\n` +
25
+ ` </testcase>`;
26
+ }
27
+ export const junitReporter = {
28
+ name: 'junit',
29
+ extension: '.xml',
30
+ format(results) {
31
+ const suites = [];
32
+ // Build a lookup from flowName -> test cases
33
+ const designsByFlow = new Map();
34
+ for (const design of results.testDesigns) {
35
+ designsByFlow.set(design.flowName, design.testCases);
36
+ }
37
+ // High-severity findings as failure cases
38
+ const highFindings = results.findings.filter((f) => f.severity === 'high');
39
+ // Each strategy entry becomes a test suite
40
+ for (const entry of results.strategyEntries) {
41
+ const testCases = designsByFlow.get(entry.flowName) ?? [];
42
+ const failures = highFindings.filter((f) => f.title.includes(entry.flowName));
43
+ const totalTests = testCases.length + failures.length;
44
+ const casesXml = testCases.map((tc) => buildTestCase(tc, entry.flowName)).join('\n');
45
+ const failuresXml = failures.map((f) => buildFailureCase(f)).join('\n');
46
+ const allCases = [casesXml, failuresXml].filter(Boolean).join('\n');
47
+ // Warnings as system-out
48
+ const warningsText = results.warnings.length > 0
49
+ ? ` <system-out>${escapeXml(results.warnings.join('\n'))}</system-out>`
50
+ : '';
51
+ suites.push(` <testsuite name="${escapeXml(entry.flowName)}" tests="${totalTests}" failures="${failures.length}" ` +
52
+ `id="${escapeXml(entry.flowId)}">\n` +
53
+ ` <properties>\n` +
54
+ ` <property name="priority" value="${escapeXml(entry.priority)}" />\n` +
55
+ ` <property name="approach" value="${escapeXml(entry.approach)}" />\n` +
56
+ ` <property name="rationale" value="${escapeXml(entry.rationale)}" />\n` +
57
+ ` </properties>\n` +
58
+ (allCases ? allCases + '\n' : '') +
59
+ (warningsText ? warningsText + '\n' : '') +
60
+ ` </testsuite>`);
61
+ }
62
+ // Remaining high findings not tied to a strategy entry
63
+ const coveredFlowNames = new Set(results.strategyEntries.map((e) => e.flowName));
64
+ const uncoveredFindings = highFindings.filter((f) => !Array.from(coveredFlowNames).some((name) => f.title.includes(name)));
65
+ if (uncoveredFindings.length > 0) {
66
+ const failureCases = uncoveredFindings.map((f) => buildFailureCase(f)).join('\n');
67
+ suites.push(` <testsuite name="findings" tests="${uncoveredFindings.length}" failures="${uncoveredFindings.length}">\n` +
68
+ failureCases + '\n' +
69
+ ` </testsuite>`);
70
+ }
71
+ const totalTests = results.testDesigns.reduce((sum, d) => sum + d.testCases.length, 0) + highFindings.length;
72
+ const totalFailures = highFindings.length;
73
+ return `<?xml version="1.0" encoding="UTF-8"?>\n` +
74
+ `<testsuites name="e2e-agents: ${escapeXml(results.workflow)}" ` +
75
+ `tests="${totalTests}" failures="${totalFailures}" ` +
76
+ `time="0">\n` +
77
+ ` <properties>\n` +
78
+ ` <property name="changedFiles" value="${results.changedFiles}" />\n` +
79
+ ` <property name="impactedFlows" value="${results.impactedFlows}" />\n` +
80
+ ` <property name="cost" value="${results.cost}" />\n` +
81
+ ` <property name="tokens" value="${results.tokens}" />\n` +
82
+ ` </properties>\n` +
83
+ suites.join('\n') + '\n' +
84
+ `</testsuites>\n`;
85
+ },
86
+ };
@@ -0,0 +1,3 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ export {};
@@ -0,0 +1,131 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ function severityToLevel(severity) {
4
+ switch (severity.toLowerCase()) {
5
+ case 'high':
6
+ case 'critical':
7
+ return 'error';
8
+ case 'medium':
9
+ return 'warning';
10
+ case 'low':
11
+ case 'info':
12
+ return 'note';
13
+ default:
14
+ return 'note';
15
+ }
16
+ }
17
+ function riskToLevel(risk) {
18
+ switch (risk.toLowerCase()) {
19
+ case 'high':
20
+ return 'warning';
21
+ case 'medium':
22
+ return 'note';
23
+ default:
24
+ return 'none';
25
+ }
26
+ }
27
+ export const sarifReporter = {
28
+ name: 'sarif',
29
+ extension: '.sarif',
30
+ format(results) {
31
+ const rules = [];
32
+ const sarifResults = [];
33
+ const ruleIds = new Set();
34
+ function ensureRule(id, description, level) {
35
+ if (!ruleIds.has(id)) {
36
+ ruleIds.add(id);
37
+ rules.push({
38
+ id,
39
+ shortDescription: { text: description },
40
+ defaultConfiguration: { level },
41
+ });
42
+ }
43
+ }
44
+ // Findings -> results
45
+ for (const finding of results.findings) {
46
+ const level = severityToLevel(finding.severity);
47
+ const ruleId = `finding/${finding.severity}`;
48
+ ensureRule(ruleId, `Finding (${finding.severity})`, level);
49
+ sarifResults.push({
50
+ ruleId,
51
+ level,
52
+ message: { text: `${finding.title}: ${finding.description}` },
53
+ properties: {
54
+ severity: finding.severity,
55
+ },
56
+ });
57
+ }
58
+ // Strategy entries without matching test designs -> coverage gap results
59
+ const designedFlows = new Set(results.testDesigns.map((d) => d.flowName));
60
+ for (const entry of results.strategyEntries) {
61
+ if (!designedFlows.has(entry.flowName)) {
62
+ const ruleId = 'coverage/gap';
63
+ ensureRule(ruleId, 'Missing test coverage for impacted flow', 'warning');
64
+ sarifResults.push({
65
+ ruleId,
66
+ level: 'warning',
67
+ message: {
68
+ text: `Flow "${entry.flowName}" (${entry.flowId}) has strategy but no test design. ` +
69
+ `Priority: ${entry.priority}, approach: ${entry.approach}.`,
70
+ },
71
+ properties: {
72
+ flowId: entry.flowId,
73
+ priority: entry.priority,
74
+ },
75
+ });
76
+ }
77
+ }
78
+ // High-risk cross-impacts -> warning results
79
+ for (const impact of results.crossImpacts) {
80
+ const level = riskToLevel(impact.riskLevel);
81
+ if (level === 'none') {
82
+ continue;
83
+ }
84
+ const ruleId = `cross-impact/${impact.riskLevel}`;
85
+ ensureRule(ruleId, `Cross-impact (${impact.riskLevel} risk)`, level);
86
+ sarifResults.push({
87
+ ruleId,
88
+ level,
89
+ message: {
90
+ text: `Cross-impact: "${impact.sourceFamily}" affects "${impact.affectedFamily}" ` +
91
+ `with ${impact.riskLevel} risk.`,
92
+ },
93
+ properties: {
94
+ sourceFamily: impact.sourceFamily,
95
+ affectedFamily: impact.affectedFamily,
96
+ riskLevel: impact.riskLevel,
97
+ },
98
+ });
99
+ }
100
+ const run = {
101
+ tool: {
102
+ driver: {
103
+ name: 'e2e-agents',
104
+ version: '1.8.5',
105
+ informationUri: 'https://github.com/mattermost/e2e-agents',
106
+ rules,
107
+ },
108
+ },
109
+ results: sarifResults,
110
+ invocations: [
111
+ {
112
+ executionSuccessful: true,
113
+ properties: {
114
+ workflow: results.workflow,
115
+ changedFiles: results.changedFiles,
116
+ impactedFlows: results.impactedFlows,
117
+ cost: results.cost,
118
+ tokens: results.tokens,
119
+ warnings: results.warnings,
120
+ },
121
+ },
122
+ ],
123
+ };
124
+ const sarif = {
125
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json',
126
+ version: '2.1.0',
127
+ runs: [run],
128
+ };
129
+ return JSON.stringify(sarif, null, 2) + '\n';
130
+ },
131
+ };
@@ -0,0 +1,78 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ const DEFAULT_CONFIG = {
4
+ failureThreshold: 3,
5
+ cooldownMs: 60000,
6
+ };
7
+ export class CircuitBreaker {
8
+ constructor(config = {}) {
9
+ this.state = 'closed';
10
+ this.failures = 0;
11
+ this.lastFailureTime = 0;
12
+ this.config = { ...DEFAULT_CONFIG, ...config };
13
+ }
14
+ /** Returns the derived state without mutating internal state. */
15
+ get currentState() {
16
+ if (this.state === 'open' && Date.now() - this.lastFailureTime >= this.config.cooldownMs) {
17
+ return 'half-open';
18
+ }
19
+ return this.state;
20
+ }
21
+ get isOpen() {
22
+ return this.currentState === 'open';
23
+ }
24
+ /**
25
+ * Execute a function with circuit breaker protection.
26
+ * If the circuit is open, the fallback is called instead.
27
+ */
28
+ async call(fn, fallback) {
29
+ // Transition from open to half-open if cooldown has elapsed
30
+ if (this.state === 'open') {
31
+ if (Date.now() - this.lastFailureTime >= this.config.cooldownMs) {
32
+ this.state = 'half-open';
33
+ }
34
+ else {
35
+ return fallback();
36
+ }
37
+ }
38
+ // At this point state is 'closed' or 'half-open'
39
+ const stateBeforeCall = this.state;
40
+ try {
41
+ const result = await fn();
42
+ this.onSuccess();
43
+ return result;
44
+ }
45
+ catch (error) {
46
+ const shouldCount = !this.config.shouldCount || this.config.shouldCount(error);
47
+ if (shouldCount) {
48
+ this.onFailure();
49
+ }
50
+ // In half-open state, a failure re-opens the circuit
51
+ if (stateBeforeCall === 'half-open') {
52
+ throw error;
53
+ }
54
+ // In closed state, if failures hit threshold the circuit opened
55
+ if (shouldCount && this.failures >= this.config.failureThreshold) {
56
+ return fallback();
57
+ }
58
+ throw error;
59
+ }
60
+ }
61
+ onSuccess() {
62
+ this.failures = 0;
63
+ this.state = 'closed';
64
+ }
65
+ onFailure() {
66
+ this.failures++;
67
+ this.lastFailureTime = Date.now();
68
+ if (this.failures >= this.config.failureThreshold) {
69
+ this.state = 'open';
70
+ }
71
+ }
72
+ /** Reset the circuit breaker to closed state */
73
+ reset() {
74
+ this.state = 'closed';
75
+ this.failures = 0;
76
+ this.lastFailureTime = 0;
77
+ }
78
+ }
@@ -0,0 +1,56 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ const DEFAULT_RETRY_CONFIG = {
4
+ maxRetries: 2,
5
+ baseDelayMs: 1000,
6
+ maxDelayMs: 10000,
7
+ jitter: true,
8
+ };
9
+ /** Errors that should be retried (transient failures) */
10
+ function isRetryable(error) {
11
+ if (!(error instanceof Error))
12
+ return false;
13
+ const msg = error.message.toLowerCase();
14
+ // Rate limits
15
+ if (msg.includes('rate limit') || msg.includes('429') || msg.includes('too many requests'))
16
+ return true;
17
+ // Server errors
18
+ if (msg.includes('500') || msg.includes('502') || msg.includes('503') || msg.includes('504'))
19
+ return true;
20
+ if (msg.includes('internal server error') || msg.includes('bad gateway') || msg.includes('service unavailable'))
21
+ return true;
22
+ // Network errors
23
+ if (msg.includes('econnreset') || msg.includes('econnrefused') || msg.includes('etimedout'))
24
+ return true;
25
+ if (msg.includes('socket hang up') || msg.includes('network error'))
26
+ return true;
27
+ // Overloaded
28
+ if (msg.includes('overloaded') || msg.includes('capacity'))
29
+ return true;
30
+ return false;
31
+ }
32
+ function computeDelay(attempt, config) {
33
+ const exponential = Math.min(config.baseDelayMs * Math.pow(2, attempt), config.maxDelayMs);
34
+ if (!config.jitter)
35
+ return exponential;
36
+ // Full jitter: random between 0 and exponential
37
+ return Math.floor(Math.random() * exponential);
38
+ }
39
+ export async function withRetry(fn, config = {}) {
40
+ const cfg = { ...DEFAULT_RETRY_CONFIG, ...config };
41
+ let lastError;
42
+ for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
43
+ try {
44
+ return await fn();
45
+ }
46
+ catch (error) {
47
+ lastError = error;
48
+ if (attempt >= cfg.maxRetries || !isRetryable(error)) {
49
+ throw error;
50
+ }
51
+ const delay = computeDelay(attempt, cfg);
52
+ await new Promise((resolve) => setTimeout(resolve, delay));
53
+ }
54
+ }
55
+ throw lastError;
56
+ }
@@ -0,0 +1,66 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ /**
4
+ * Secret scanning and sanitization utilities.
5
+ * Prevents API keys and credentials from leaking into artifacts, logs, and output.
6
+ *
7
+ * Patterns are stored WITHOUT the global flag to avoid shared mutable lastIndex state.
8
+ * New RegExp instances with /g are created per call for safe concurrent usage.
9
+ */
10
+ const SECRET_PATTERNS = [
11
+ // Anthropic API keys (must be checked before generic sk- pattern)
12
+ /sk-ant-[a-zA-Z0-9_-]{20,}/,
13
+ // OpenAI API keys (negative lookahead to avoid matching Anthropic keys)
14
+ /sk-(?!ant-)[a-zA-Z0-9]{20,}/,
15
+ // Generic API key patterns
16
+ /(?:api[_-]?key|api[_-]?secret|access[_-]?token|auth[_-]?token)['":\s=]+['"]?([a-zA-Z0-9_\-./]{20,})['"]?/i,
17
+ // Bearer tokens
18
+ /Bearer\s+[a-zA-Z0-9_\-./]{20,}/,
19
+ // AWS keys
20
+ /AKIA[0-9A-Z]{16}/,
21
+ // GitHub tokens
22
+ /gh[ps]_[a-zA-Z0-9]{36,}/,
23
+ /github_pat_[a-zA-Z0-9_]{22,}/,
24
+ ];
25
+ /**
26
+ * Sanitize a string by replacing detected secrets with [REDACTED].
27
+ */
28
+ export function sanitizeSecrets(text) {
29
+ let result = text;
30
+ for (const pattern of SECRET_PATTERNS) {
31
+ result = result.replace(new RegExp(pattern, 'gi'), '[REDACTED]');
32
+ }
33
+ return result;
34
+ }
35
+ /**
36
+ * Check if a string contains any detectable secrets.
37
+ */
38
+ export function containsSecrets(text) {
39
+ for (const pattern of SECRET_PATTERNS) {
40
+ if (new RegExp(pattern, 'i').test(text))
41
+ return true;
42
+ }
43
+ return false;
44
+ }
45
+ /**
46
+ * Deep-sanitize a JSON-serializable object.
47
+ * Recursively walks all string values and sanitizes them.
48
+ * Tracks seen objects to prevent stack overflow on circular references.
49
+ */
50
+ export function sanitizeObject(obj, _seen) {
51
+ if (typeof obj === 'string')
52
+ return sanitizeSecrets(obj);
53
+ if (obj === null || typeof obj !== 'object')
54
+ return obj;
55
+ const seen = _seen ?? new WeakSet();
56
+ if (seen.has(obj))
57
+ return '[Circular]';
58
+ seen.add(obj);
59
+ if (Array.isArray(obj))
60
+ return obj.map((item) => sanitizeObject(item, seen));
61
+ const result = {};
62
+ for (const [key, value] of Object.entries(obj)) {
63
+ result[key] = sanitizeObject(value, seen);
64
+ }
65
+ return result;
66
+ }