@yasserkhanorg/e2e-agents 1.8.5 → 1.9.5

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 (256) 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/cluster_utils.js +60 -0
  125. package/dist/esm/knowledge/kg_bridge.js +381 -0
  126. package/dist/esm/knowledge/kg_types.js +3 -0
  127. package/dist/esm/knowledge/route_families.js +89 -0
  128. package/dist/esm/mcp-server.js +2 -4
  129. package/dist/esm/metrics/prometheus.js +149 -0
  130. package/dist/esm/model_router.js +59 -0
  131. package/dist/esm/ollama_provider.js +1 -0
  132. package/dist/esm/openai_provider.js +1 -0
  133. package/dist/esm/pipeline/orchestrator.js +6 -12
  134. package/dist/esm/pipeline/stage0_preprocess.js +12 -19
  135. package/dist/esm/pipeline/stage2_coverage.js +1 -0
  136. package/dist/esm/pipeline/stage3_generation.js +1 -0
  137. package/dist/esm/progress.js +112 -0
  138. package/dist/esm/prompts/coverage.js +7 -24
  139. package/dist/esm/prompts/cross-impact.js +3 -21
  140. package/dist/esm/prompts/generation.js +158 -36
  141. package/dist/esm/prompts/generation_profile.js +147 -0
  142. package/dist/esm/prompts/heal.js +33 -15
  143. package/dist/esm/prompts/impact.js +3 -22
  144. package/dist/esm/prompts/json_extract.js +36 -0
  145. package/dist/esm/prompts/strategist.js +2 -20
  146. package/dist/esm/prompts/test-designer.js +6 -21
  147. package/dist/esm/provider_factory.js +6 -4
  148. package/dist/esm/reporters/junit.js +86 -0
  149. package/dist/esm/reporters/reporter.js +3 -0
  150. package/dist/esm/reporters/sarif.js +131 -0
  151. package/dist/esm/resilience/circuit_breaker.js +78 -0
  152. package/dist/esm/resilience/retry.js +56 -0
  153. package/dist/esm/sanitize.js +66 -0
  154. package/dist/esm/training/kg_scanner.js +115 -0
  155. package/dist/esm/training/scanner.js +27 -34
  156. package/dist/esm/version.js +33 -0
  157. package/dist/index.d.ts +21 -1
  158. package/dist/index.d.ts.map +1 -1
  159. package/dist/index.js +45 -1
  160. package/dist/knowledge/cluster_utils.d.ts +28 -0
  161. package/dist/knowledge/cluster_utils.d.ts.map +1 -0
  162. package/dist/knowledge/cluster_utils.js +67 -0
  163. package/dist/knowledge/kg_bridge.d.ts +31 -0
  164. package/dist/knowledge/kg_bridge.d.ts.map +1 -0
  165. package/dist/knowledge/kg_bridge.js +388 -0
  166. package/dist/knowledge/kg_types.d.ts +75 -0
  167. package/dist/knowledge/kg_types.d.ts.map +1 -0
  168. package/dist/knowledge/kg_types.js +4 -0
  169. package/dist/knowledge/route_families.d.ts +18 -0
  170. package/dist/knowledge/route_families.d.ts.map +1 -1
  171. package/dist/knowledge/route_families.js +91 -0
  172. package/dist/mcp-server.d.ts.map +1 -1
  173. package/dist/mcp-server.js +2 -4
  174. package/dist/metrics/prometheus.d.ts +37 -0
  175. package/dist/metrics/prometheus.d.ts.map +1 -0
  176. package/dist/metrics/prometheus.js +153 -0
  177. package/dist/model_router.d.ts +28 -0
  178. package/dist/model_router.d.ts.map +1 -0
  179. package/dist/model_router.js +63 -0
  180. package/dist/ollama_provider.d.ts.map +1 -1
  181. package/dist/ollama_provider.js +1 -0
  182. package/dist/openai_provider.d.ts.map +1 -1
  183. package/dist/openai_provider.js +1 -0
  184. package/dist/pipeline/orchestrator.d.ts +2 -0
  185. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  186. package/dist/pipeline/orchestrator.js +6 -12
  187. package/dist/pipeline/stage0_preprocess.d.ts.map +1 -1
  188. package/dist/pipeline/stage0_preprocess.js +11 -18
  189. package/dist/pipeline/stage2_coverage.d.ts +2 -0
  190. package/dist/pipeline/stage2_coverage.d.ts.map +1 -1
  191. package/dist/pipeline/stage2_coverage.js +1 -0
  192. package/dist/pipeline/stage3_generation.d.ts +2 -0
  193. package/dist/pipeline/stage3_generation.d.ts.map +1 -1
  194. package/dist/pipeline/stage3_generation.js +1 -0
  195. package/dist/pipeline/stage4_heal.d.ts +2 -0
  196. package/dist/pipeline/stage4_heal.d.ts.map +1 -1
  197. package/dist/progress.d.ts +22 -0
  198. package/dist/progress.d.ts.map +1 -0
  199. package/dist/progress.js +116 -0
  200. package/dist/prompts/coverage.d.ts +2 -0
  201. package/dist/prompts/coverage.d.ts.map +1 -1
  202. package/dist/prompts/coverage.js +7 -24
  203. package/dist/prompts/cross-impact.d.ts +1 -0
  204. package/dist/prompts/cross-impact.d.ts.map +1 -1
  205. package/dist/prompts/cross-impact.js +3 -21
  206. package/dist/prompts/generation.d.ts +3 -1
  207. package/dist/prompts/generation.d.ts.map +1 -1
  208. package/dist/prompts/generation.js +158 -36
  209. package/dist/prompts/generation_profile.d.ts +29 -0
  210. package/dist/prompts/generation_profile.d.ts.map +1 -0
  211. package/dist/prompts/generation_profile.js +151 -0
  212. package/dist/prompts/heal.d.ts +3 -1
  213. package/dist/prompts/heal.d.ts.map +1 -1
  214. package/dist/prompts/heal.js +33 -15
  215. package/dist/prompts/impact.d.ts +1 -0
  216. package/dist/prompts/impact.d.ts.map +1 -1
  217. package/dist/prompts/impact.js +3 -22
  218. package/dist/prompts/json_extract.d.ts +14 -0
  219. package/dist/prompts/json_extract.d.ts.map +1 -0
  220. package/dist/prompts/json_extract.js +39 -0
  221. package/dist/prompts/strategist.d.ts.map +1 -1
  222. package/dist/prompts/strategist.js +2 -20
  223. package/dist/prompts/test-designer.d.ts +2 -0
  224. package/dist/prompts/test-designer.d.ts.map +1 -1
  225. package/dist/prompts/test-designer.js +6 -21
  226. package/dist/provider_factory.d.ts.map +1 -1
  227. package/dist/provider_factory.js +6 -4
  228. package/dist/reporters/junit.d.ts +6 -0
  229. package/dist/reporters/junit.d.ts.map +1 -0
  230. package/dist/reporters/junit.js +89 -0
  231. package/dist/reporters/reporter.d.ts +42 -0
  232. package/dist/reporters/reporter.d.ts.map +1 -0
  233. package/dist/reporters/reporter.js +4 -0
  234. package/dist/reporters/sarif.d.ts +7 -0
  235. package/dist/reporters/sarif.d.ts.map +1 -0
  236. package/dist/reporters/sarif.js +134 -0
  237. package/dist/resilience/circuit_breaker.d.ts +36 -0
  238. package/dist/resilience/circuit_breaker.d.ts.map +1 -0
  239. package/dist/resilience/circuit_breaker.js +82 -0
  240. package/dist/resilience/retry.d.ts +11 -0
  241. package/dist/resilience/retry.d.ts.map +1 -0
  242. package/dist/resilience/retry.js +59 -0
  243. package/dist/sanitize.d.ts +15 -0
  244. package/dist/sanitize.d.ts.map +1 -0
  245. package/dist/sanitize.js +71 -0
  246. package/dist/training/kg_scanner.d.ts +13 -0
  247. package/dist/training/kg_scanner.d.ts.map +1 -0
  248. package/dist/training/kg_scanner.js +118 -0
  249. package/dist/training/scanner.d.ts +7 -2
  250. package/dist/training/scanner.d.ts.map +1 -1
  251. package/dist/training/scanner.js +27 -34
  252. package/dist/version.d.ts +6 -0
  253. package/dist/version.d.ts.map +1 -0
  254. package/dist/version.js +36 -0
  255. package/package.json +7 -2
  256. package/schemas/route-families.schema.json +31 -1
@@ -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, '&')
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
+ }
@@ -0,0 +1,115 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { deriveClusterId, SKIP_DIRS_WITH_TESTS } from '../knowledge/cluster_utils.js';
4
+ /**
5
+ * Converts KG nodes/edges into a ScanResult compatible with the filesystem scanner output.
6
+ * Groups nodes by their containing module/directory to form families.
7
+ */
8
+ export function scanFromKnowledgeGraph(kg) {
9
+ const clusters = new Map();
10
+ // Group nodes into clusters by directory/module
11
+ for (const node of kg.nodes) {
12
+ if (node.layer === 'infra')
13
+ continue; // skip infrastructure nodes
14
+ const clusterId = deriveClusterId(node, SKIP_DIRS_WITH_TESTS);
15
+ if (!clusterId)
16
+ continue;
17
+ if (!clusters.has(clusterId)) {
18
+ clusters.set(clusterId, []);
19
+ }
20
+ clusters.get(clusterId).push(node);
21
+ }
22
+ let totalSourceFiles = 0;
23
+ let totalTestFiles = 0;
24
+ const families = [];
25
+ for (const [id, nodes] of clusters) {
26
+ const webappPaths = [];
27
+ const serverPaths = [];
28
+ const specDirs = [];
29
+ const tags = [];
30
+ const seenDirs = new Set();
31
+ for (const node of nodes) {
32
+ if (!node.filePath)
33
+ continue;
34
+ const normalized = node.filePath.replace(/\\/g, '/');
35
+ if (node.layer === 'test') {
36
+ totalTestFiles++;
37
+ const dir = normalized.split('/').slice(0, -1).join('/');
38
+ if (dir && !seenDirs.has(dir)) {
39
+ seenDirs.add(dir);
40
+ specDirs.push(dir + '/');
41
+ }
42
+ continue;
43
+ }
44
+ totalSourceFiles++;
45
+ const glob = buildGlobFromPath(normalized);
46
+ if (node.layer === 'api' || node.layer === 'service' || node.layer === 'data') {
47
+ serverPaths.push(glob);
48
+ }
49
+ else if (node.layer === 'ui') {
50
+ webappPaths.push(glob);
51
+ }
52
+ else {
53
+ // Default assignment based on file path heuristics
54
+ if (isLikelyServerPath(normalized)) {
55
+ serverPaths.push(glob);
56
+ }
57
+ else {
58
+ webappPaths.push(glob);
59
+ }
60
+ }
61
+ // Extract tags from node metadata
62
+ if (node.tags) {
63
+ tags.push(...node.tags);
64
+ }
65
+ }
66
+ if (webappPaths.length === 0 && serverPaths.length === 0 && specDirs.length === 0) {
67
+ continue;
68
+ }
69
+ families.push({
70
+ id,
71
+ routes: [`/${id}`],
72
+ webappPaths: [...new Set(webappPaths)],
73
+ serverPaths: [...new Set(serverPaths)],
74
+ specDirs: [...new Set(specDirs)],
75
+ cypressSpecDirs: [],
76
+ tags: [...new Set(tags)],
77
+ features: [],
78
+ routesGuessed: true,
79
+ });
80
+ }
81
+ return {
82
+ families,
83
+ unmatchedSourceDirs: [],
84
+ unmatchedTestDirs: [],
85
+ stats: {
86
+ totalSourceFiles,
87
+ totalTestFiles,
88
+ familyCount: families.length,
89
+ },
90
+ };
91
+ }
92
+ // ---------------------------------------------------------------------------
93
+ // Internal helpers
94
+ // ---------------------------------------------------------------------------
95
+ // deriveClusterId and deriveClusterIdFromPath imported from cluster_utils.ts
96
+ function buildGlobFromPath(filePath) {
97
+ // Reject paths with traversal or null bytes
98
+ if (filePath.includes('..') || filePath.includes('\0')) {
99
+ return '';
100
+ }
101
+ // Convert a file path to a glob pattern matching the directory
102
+ const dir = filePath.split('/').slice(0, -1).join('/');
103
+ return dir ? `${dir}/*` : `${filePath}*`;
104
+ }
105
+ function isLikelyServerPath(filePath) {
106
+ const lower = filePath.toLowerCase();
107
+ return lower.includes('/server/') ||
108
+ lower.includes('/api/') ||
109
+ lower.includes('/routes/') ||
110
+ lower.includes('/controllers/') ||
111
+ lower.includes('/services/') ||
112
+ lower.includes('/models/') ||
113
+ lower.endsWith('.go') ||
114
+ lower.endsWith('.py');
115
+ }