@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,204 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { matchGlob, normalizePath } from './utils.js';
5
+ const PRIORITY_RANK = {
6
+ P0: 0,
7
+ P1: 1,
8
+ P2: 2,
9
+ };
10
+ function coerceNumber(value) {
11
+ if (typeof value === 'number' && Number.isFinite(value)) {
12
+ return value;
13
+ }
14
+ if (typeof value === 'string') {
15
+ const parsed = Number(value);
16
+ if (Number.isFinite(parsed)) {
17
+ return parsed;
18
+ }
19
+ }
20
+ return undefined;
21
+ }
22
+ function normalizePriority(value) {
23
+ if (value === 'P0' || value === 'P1' || value === 'P2') {
24
+ return value;
25
+ }
26
+ return undefined;
27
+ }
28
+ function parseStringArray(value) {
29
+ if (!Array.isArray(value)) {
30
+ return [];
31
+ }
32
+ return value
33
+ .filter((item) => typeof item === 'string')
34
+ .map((item) => item.trim())
35
+ .filter(Boolean);
36
+ }
37
+ function parsePathArray(value) {
38
+ return parseStringArray(value).map((item) => normalizePath(item));
39
+ }
40
+ function parseKeywords(value) {
41
+ if (!Array.isArray(value)) {
42
+ return [];
43
+ }
44
+ return Array.from(new Set(value
45
+ .filter((item) => typeof item === 'string')
46
+ .map((item) => item.trim().toLowerCase())
47
+ .filter(Boolean)));
48
+ }
49
+ function parseRules(rawRules, warnings) {
50
+ if (!Array.isArray(rawRules)) {
51
+ warnings.push('Subsystem risk map has no "rules" array.');
52
+ return [];
53
+ }
54
+ const parsed = [];
55
+ for (let i = 0; i < rawRules.length; i += 1) {
56
+ const rawRule = rawRules[i];
57
+ if (!rawRule || typeof rawRule !== 'object') {
58
+ continue;
59
+ }
60
+ const patterns = parsePathArray(rawRule.patterns);
61
+ if (patterns.length === 0) {
62
+ warnings.push(`Subsystem risk rule at index ${i} has no valid patterns and was skipped.`);
63
+ continue;
64
+ }
65
+ const id = typeof rawRule.id === 'string' && rawRule.id.trim()
66
+ ? rawRule.id.trim()
67
+ : `rule-${i + 1}`;
68
+ const description = typeof rawRule.description === 'string' ? rawRule.description.trim() : undefined;
69
+ const reasons = parseStringArray(rawRule.reasons);
70
+ const keywords = parseKeywords(rawRule.keywords);
71
+ const scoreDelta = coerceNumber(rawRule.scoreDelta) ?? 0;
72
+ const priorityFloor = normalizePriority(rawRule.priorityFloor);
73
+ const normalizedReasons = reasons.length > 0
74
+ ? reasons
75
+ : (description ? [description] : []);
76
+ parsed.push({
77
+ id,
78
+ description,
79
+ patterns,
80
+ scoreDelta,
81
+ priorityFloor,
82
+ reasons: normalizedReasons,
83
+ keywords,
84
+ });
85
+ }
86
+ return parsed;
87
+ }
88
+ function comparePriority(a, b) {
89
+ return PRIORITY_RANK[a] <= PRIORITY_RANK[b] ? a : b;
90
+ }
91
+ export function loadSubsystemRiskResolver(config) {
92
+ const mapPath = normalizePath(config.mapPath);
93
+ const warnings = [];
94
+ if (!config.enabled) {
95
+ return {
96
+ info: {
97
+ source: 'map',
98
+ enabled: false,
99
+ mapPath,
100
+ mapFound: false,
101
+ rulesLoaded: 0,
102
+ },
103
+ warnings,
104
+ matchFile: () => [],
105
+ };
106
+ }
107
+ if (!existsSync(config.mapPath)) {
108
+ warnings.push(`Subsystem risk map file not found: ${config.mapPath}`);
109
+ return {
110
+ info: {
111
+ source: 'map',
112
+ enabled: true,
113
+ mapPath,
114
+ mapFound: false,
115
+ rulesLoaded: 0,
116
+ },
117
+ warnings,
118
+ matchFile: () => [],
119
+ };
120
+ }
121
+ let raw;
122
+ try {
123
+ raw = JSON.parse(readFileSync(config.mapPath, 'utf-8'));
124
+ }
125
+ catch {
126
+ warnings.push(`Subsystem risk map is invalid JSON: ${config.mapPath}`);
127
+ return {
128
+ info: {
129
+ source: 'map',
130
+ enabled: true,
131
+ mapPath,
132
+ mapFound: true,
133
+ rulesLoaded: 0,
134
+ },
135
+ warnings,
136
+ matchFile: () => [],
137
+ };
138
+ }
139
+ const rules = parseRules(raw.rules, warnings);
140
+ if (rules.length === 0) {
141
+ warnings.push(`Subsystem risk map loaded but no valid rules were found: ${config.mapPath}`);
142
+ }
143
+ const maxRules = Math.max(1, Math.round(config.maxRulesPerFile));
144
+ return {
145
+ info: {
146
+ source: 'map',
147
+ enabled: true,
148
+ mapPath,
149
+ mapFound: true,
150
+ rulesLoaded: rules.length,
151
+ },
152
+ warnings,
153
+ matchFile: (relativePath) => {
154
+ const normalizedPath = normalizePath(relativePath);
155
+ const matches = [];
156
+ for (const rule of rules) {
157
+ const matched = rule.patterns.some((pattern) => matchGlob(normalizedPath, pattern));
158
+ if (!matched) {
159
+ continue;
160
+ }
161
+ const reasons = rule.reasons.length > 0
162
+ ? rule.reasons
163
+ : [`Subsystem risk rule matched: ${rule.id}`];
164
+ matches.push({
165
+ ruleId: rule.id,
166
+ scoreDelta: rule.scoreDelta,
167
+ priorityFloor: rule.priorityFloor,
168
+ reasons,
169
+ keywords: rule.keywords,
170
+ priorityRank: rule.priorityFloor ? PRIORITY_RANK[rule.priorityFloor] : 99,
171
+ });
172
+ }
173
+ if (matches.length <= maxRules) {
174
+ return matches.map(({ priorityRank, ...rest }) => rest);
175
+ }
176
+ matches.sort((a, b) => {
177
+ const deltaDiff = Math.abs(b.scoreDelta) - Math.abs(a.scoreDelta);
178
+ if (deltaDiff !== 0) {
179
+ return deltaDiff;
180
+ }
181
+ const priorityDiff = a.priorityRank - b.priorityRank;
182
+ if (priorityDiff !== 0) {
183
+ return priorityDiff;
184
+ }
185
+ return a.ruleId.localeCompare(b.ruleId);
186
+ });
187
+ const capped = matches.slice(0, maxRules).map(({ priorityRank, ...rest }) => rest);
188
+ let floor;
189
+ for (const match of capped) {
190
+ if (match.priorityFloor) {
191
+ floor = floor ? comparePriority(floor, match.priorityFloor) : match.priorityFloor;
192
+ }
193
+ }
194
+ if (floor) {
195
+ for (const match of capped) {
196
+ if (!match.priorityFloor) {
197
+ match.priorityFloor = floor;
198
+ }
199
+ }
200
+ }
201
+ return capped;
202
+ },
203
+ };
204
+ }
@@ -0,0 +1,216 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ /**
4
+ * Telemetry Collection System (Phase A2)
5
+ *
6
+ * Tracks costs, performance, and success metrics for test generation operations.
7
+ * Provides visibility into:
8
+ * - Cost per operation (input/output tokens * model rate)
9
+ * - Model usage breakdown (Haiku, Sonnet, Opus)
10
+ * - Success rate by operation
11
+ * - Performance metrics (duration, tokens used)
12
+ *
13
+ * Data stored in: `.e2e-ai-agents/metrics/YYYY-MM-DD.json`
14
+ */
15
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
16
+ import { join } from 'path';
17
+ import { randomUUID } from 'crypto';
18
+ const MODEL_RATES = {
19
+ 'claude-haiku-4-0-20250430': 0.25 / 1000000, // per input token
20
+ 'claude-sonnet-4-5-20250929': 3 / 1000000,
21
+ 'claude-opus-4-6-20250820': 15 / 1000000,
22
+ };
23
+ export class TelemetryCollector {
24
+ constructor(metricsDir = '.e2e-ai-agents/metrics') {
25
+ this.metrics = new Map();
26
+ this.metricsDir = metricsDir;
27
+ this.ensureMetricsDir();
28
+ this.loadTodayMetrics();
29
+ }
30
+ /**
31
+ * Ensure metrics directory exists
32
+ */
33
+ ensureMetricsDir() {
34
+ if (!existsSync(this.metricsDir)) {
35
+ mkdirSync(this.metricsDir, { recursive: true });
36
+ }
37
+ }
38
+ /**
39
+ * Get today's metrics file path
40
+ */
41
+ getTodayPath() {
42
+ const now = new Date();
43
+ const date = now.toISOString().split('T')[0]; // YYYY-MM-DD
44
+ return join(this.metricsDir, `${date}.json`);
45
+ }
46
+ /**
47
+ * Load metrics from disk
48
+ */
49
+ loadTodayMetrics() {
50
+ const path = this.getTodayPath();
51
+ if (existsSync(path)) {
52
+ try {
53
+ const data = JSON.parse(readFileSync(path, 'utf-8'));
54
+ this.metrics.set(path, data);
55
+ }
56
+ catch (error) {
57
+ console.error(`Failed to load metrics from ${path}:`, error);
58
+ }
59
+ }
60
+ }
61
+ /**
62
+ * Track a metric
63
+ */
64
+ track(metric) {
65
+ const fullMetric = {
66
+ id: randomUUID().substring(0, 8),
67
+ ...metric,
68
+ };
69
+ const path = this.getTodayPath();
70
+ const metrics = this.metrics.get(path) || [];
71
+ metrics.push(fullMetric);
72
+ this.metrics.set(path, metrics);
73
+ // Persist to disk
74
+ this.saveMetrics();
75
+ }
76
+ /**
77
+ * Save metrics to disk
78
+ */
79
+ saveMetrics() {
80
+ this.metrics.forEach((metrics, path) => {
81
+ writeFileSync(path, JSON.stringify(metrics, null, 2), 'utf-8');
82
+ });
83
+ }
84
+ /**
85
+ * Calculate cost for a metric
86
+ */
87
+ static calculateCost(model, tokensInput, tokensOutput) {
88
+ const inputRate = MODEL_RATES[model] || 0.003 / 1000000; // Default estimate
89
+ const outputRate = inputRate * 3; // Output usually 3x input cost
90
+ return tokensInput * inputRate + tokensOutput * outputRate;
91
+ }
92
+ /**
93
+ * Generate report for a date range
94
+ */
95
+ generateReport(since, until) {
96
+ const start = since || new Date(new Date().setDate(new Date().getDate() - 7)); // Default: last 7 days
97
+ const end = until || new Date();
98
+ // Collect all metrics in date range
99
+ const allMetrics = [];
100
+ this.metrics.forEach((metrics) => {
101
+ metrics.forEach((m) => {
102
+ const metricDate = new Date(m.timestamp);
103
+ if (metricDate >= start && metricDate <= end) {
104
+ allMetrics.push(m);
105
+ }
106
+ });
107
+ });
108
+ // Calculate summary
109
+ const successCount = allMetrics.filter((m) => m.success).length;
110
+ const failureCount = allMetrics.length - successCount;
111
+ const totalCost = allMetrics.reduce((sum, m) => sum + m.costUsd, 0);
112
+ const avgCost = allMetrics.length > 0 ? totalCost / allMetrics.length : 0;
113
+ const totalTokens = allMetrics.reduce((sum, m) => sum + (m.tokensInput + m.tokensOutput), 0);
114
+ const avgDuration = allMetrics.length > 0 ? allMetrics.reduce((sum, m) => sum + m.durationMs, 0) / allMetrics.length / 1000 : 0;
115
+ // By model
116
+ const byModel = {};
117
+ allMetrics.forEach((m) => {
118
+ if (!byModel[m.model]) {
119
+ byModel[m.model] = { count: 0, totalCost: 0, successCount: 0 };
120
+ }
121
+ byModel[m.model].count += 1;
122
+ byModel[m.model].totalCost += m.costUsd;
123
+ if (m.success)
124
+ byModel[m.model].successCount += 1;
125
+ });
126
+ Object.keys(byModel).forEach((model) => {
127
+ const data = byModel[model];
128
+ byModel[model] = {
129
+ count: data.count,
130
+ totalCost: data.totalCost,
131
+ avgCost: data.totalCost / data.count,
132
+ successRate: (data.successCount / data.count) * 100,
133
+ };
134
+ });
135
+ // By operation
136
+ const byOperation = {};
137
+ allMetrics.forEach((m) => {
138
+ if (!byOperation[m.operation]) {
139
+ byOperation[m.operation] = { count: 0, totalCost: 0, totalDuration: 0, successCount: 0 };
140
+ }
141
+ byOperation[m.operation].count += 1;
142
+ byOperation[m.operation].totalCost += m.costUsd;
143
+ byOperation[m.operation].totalDuration += m.durationMs;
144
+ if (m.success)
145
+ byOperation[m.operation].successCount += 1;
146
+ });
147
+ Object.keys(byOperation).forEach((op) => {
148
+ const data = byOperation[op];
149
+ byOperation[op] = {
150
+ count: data.count,
151
+ totalCost: data.totalCost,
152
+ avgDuration: data.totalDuration / data.count / 1000,
153
+ successRate: (data.successCount / data.count) * 100,
154
+ };
155
+ });
156
+ return {
157
+ period: {
158
+ start: start.toISOString().split('T')[0],
159
+ end: end.toISOString().split('T')[0],
160
+ },
161
+ summary: {
162
+ totalOperations: allMetrics.length,
163
+ successCount,
164
+ failureCount,
165
+ successRate: allMetrics.length > 0 ? (successCount / allMetrics.length) * 100 : 0,
166
+ totalCost,
167
+ avgCost,
168
+ totalTokens,
169
+ avgDuration,
170
+ },
171
+ byModel,
172
+ byOperation,
173
+ };
174
+ }
175
+ /**
176
+ * Format report for console output
177
+ */
178
+ static formatReport(report) {
179
+ const lines = [
180
+ '',
181
+ '📊 Test Generation Metrics',
182
+ `Period: ${report.period.start} to ${report.period.end}`,
183
+ '═'.repeat(50),
184
+ '',
185
+ `Total Operations: ${report.summary.totalOperations}`,
186
+ `Success Rate: ${report.summary.successRate.toFixed(1)}% (${report.summary.successCount}/${report.summary.totalOperations})`,
187
+ `Total Cost: $${report.summary.totalCost.toFixed(2)}`,
188
+ `Avg Cost/Op: $${report.summary.avgCost.toFixed(4)}`,
189
+ `Avg Duration: ${report.summary.avgDuration.toFixed(1)}s`,
190
+ `Total Tokens: ${report.summary.totalTokens.toLocaleString()}`,
191
+ '',
192
+ 'Model Usage:',
193
+ ];
194
+ Object.entries(report.byModel).forEach(([model, data]) => {
195
+ const shortName = model.includes('haiku') ? 'Haiku' : model.includes('sonnet') ? 'Sonnet' : 'Opus';
196
+ lines.push(` ${shortName}: ${data.count} ops - $${data.totalCost.toFixed(2)} (avg $${data.avgCost.toFixed(4)}, ${data.successRate.toFixed(0)}% success)`);
197
+ });
198
+ lines.push('', 'By Operation:');
199
+ Object.entries(report.byOperation).forEach(([op, data]) => {
200
+ lines.push(` ${op}: ${data.count} ops - $${data.totalCost.toFixed(2)} (${data.avgDuration.toFixed(1)}s avg, ${data.successRate.toFixed(0)}% success)`);
201
+ });
202
+ lines.push('');
203
+ return lines.join('\n');
204
+ }
205
+ /**
206
+ * Export metrics as JSON
207
+ */
208
+ exportJson(filepath) {
209
+ const allMetrics = [];
210
+ this.metrics.forEach((metrics) => {
211
+ allMetrics.push(...metrics);
212
+ });
213
+ writeFileSync(filepath, JSON.stringify(allMetrics, null, 2), 'utf-8');
214
+ console.log(` ✓ Exported ${allMetrics.length} metrics to ${filepath}`);
215
+ }
216
+ }
@@ -0,0 +1,20 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ function normalizeTestName(test) {
4
+ return test.replace(/ \(flags:.*\)$/, '').trim();
5
+ }
6
+ export function inferSubsystemFromTestPath(test) {
7
+ const normalized = normalizeTestName(test).replace(/^\/+/, '');
8
+ const parts = normalized.split('/').filter(Boolean);
9
+ if (parts.length === 0) {
10
+ return 'unknown';
11
+ }
12
+ const specsIdx = parts.findIndex((part) => part === 'specs' || part === 'tests');
13
+ if (specsIdx >= 0 && specsIdx + 1 < parts.length) {
14
+ return parts[specsIdx + 1] || 'unknown';
15
+ }
16
+ if (parts.length >= 2 && (parts[0] === 'e2e-tests' || parts[0] === 'playwright')) {
17
+ return parts[1] || 'unknown';
18
+ }
19
+ return parts[0] || 'unknown';
20
+ }
@@ -0,0 +1,101 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { existsSync } from 'fs';
4
+ import { globSync } from 'glob';
5
+ import { join } from 'path';
6
+ import { normalizePath, safeReadTextFile, tokenize, uniqueTokens } from './utils.js';
7
+ export function discoverTests(appRoot, patterns) {
8
+ const files = new Set();
9
+ for (const pattern of patterns) {
10
+ const matches = globSync(pattern, {
11
+ cwd: appRoot,
12
+ ignore: ['**/node_modules/**', '**/.git/**'],
13
+ nodir: true,
14
+ });
15
+ for (const match of matches) {
16
+ files.add(normalizePath(match));
17
+ }
18
+ }
19
+ return Array.from(files).map((relativePath) => {
20
+ const fullPath = join(appRoot, relativePath);
21
+ const content = safeReadTextFile(fullPath);
22
+ return { path: relativePath, content };
23
+ });
24
+ }
25
+ function buildFlowKeywords(flow) {
26
+ const tokens = [];
27
+ tokens.push(...tokenize(flow.id));
28
+ tokens.push(...tokenize(flow.name));
29
+ tokens.push(...flow.keywords);
30
+ return uniqueTokens(tokens);
31
+ }
32
+ export function mapTestsToFlows(flows, tests) {
33
+ const coverage = [];
34
+ for (const flow of flows) {
35
+ const keywords = buildFlowKeywords(flow);
36
+ const matched = [];
37
+ let score = 0;
38
+ for (const test of tests) {
39
+ const haystack = `${test.path} ${test.content || ''}`.toLowerCase();
40
+ let localScore = 0;
41
+ for (const keyword of keywords) {
42
+ if (keyword && haystack.includes(keyword.toLowerCase())) {
43
+ localScore += 1;
44
+ }
45
+ }
46
+ if (localScore > 0) {
47
+ matched.push(test.path);
48
+ score += localScore;
49
+ }
50
+ }
51
+ coverage.push({
52
+ flowId: flow.id,
53
+ flowName: flow.name,
54
+ priority: flow.priority,
55
+ coveredBy: matched,
56
+ score,
57
+ source: 'heuristic',
58
+ });
59
+ }
60
+ return coverage;
61
+ }
62
+ function resolveExpectedTests(testsRoot, expectedTests) {
63
+ const resolved = [];
64
+ for (const entry of expectedTests) {
65
+ if (!entry) {
66
+ continue;
67
+ }
68
+ resolved.push(normalizePath(entry));
69
+ }
70
+ return resolved;
71
+ }
72
+ export function mapCatalogTestsToFlows(flows, testsRoot, testsByFlow) {
73
+ return flows.map((flow) => {
74
+ const expectedTests = resolveExpectedTests(testsRoot, testsByFlow.get(flow.id) || []);
75
+ const coveredBy = [];
76
+ for (const expected of expectedTests) {
77
+ const isAbsolute = expected.startsWith('/');
78
+ const globTarget = isAbsolute ? expected : expected;
79
+ if (expected.includes('*') || expected.includes('?') || expected.includes('{')) {
80
+ const matches = globSync(globTarget, { cwd: isAbsolute ? undefined : testsRoot, nodir: true });
81
+ if (matches.length > 0) {
82
+ coveredBy.push(...matches.map((match) => normalizePath(match)));
83
+ }
84
+ continue;
85
+ }
86
+ const fullPath = isAbsolute ? expected : join(testsRoot, expected);
87
+ if (existsSync(fullPath)) {
88
+ coveredBy.push(isAbsolute ? normalizePath(expected) : expected);
89
+ }
90
+ }
91
+ return {
92
+ flowId: flow.id,
93
+ flowName: flow.name,
94
+ priority: flow.priority,
95
+ coveredBy: Array.from(new Set(coveredBy)),
96
+ score: coveredBy.length,
97
+ expectedTests,
98
+ source: 'catalog',
99
+ };
100
+ });
101
+ }