@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.
- package/LICENSE +168 -0
- package/README.md +620 -0
- package/dist/agent/analysis.d.ts +62 -0
- package/dist/agent/analysis.d.ts.map +1 -0
- package/dist/agent/analysis.js +292 -0
- package/dist/agent/blast_radius.d.ts +4 -0
- package/dist/agent/blast_radius.d.ts.map +1 -0
- package/dist/agent/blast_radius.js +37 -0
- package/dist/agent/cache_utils.d.ts +38 -0
- package/dist/agent/cache_utils.d.ts.map +1 -0
- package/dist/agent/cache_utils.js +67 -0
- package/dist/agent/config.d.ts +148 -0
- package/dist/agent/config.d.ts.map +1 -0
- package/dist/agent/config.js +640 -0
- package/dist/agent/dependency_graph.d.ts +14 -0
- package/dist/agent/dependency_graph.d.ts.map +1 -0
- package/dist/agent/dependency_graph.js +227 -0
- package/dist/agent/feedback.d.ts +55 -0
- package/dist/agent/feedback.d.ts.map +1 -0
- package/dist/agent/feedback.js +257 -0
- package/dist/agent/flags.d.ts +23 -0
- package/dist/agent/flags.d.ts.map +1 -0
- package/dist/agent/flags.js +171 -0
- package/dist/agent/flow_catalog.d.ts +25 -0
- package/dist/agent/flow_catalog.d.ts.map +1 -0
- package/dist/agent/flow_catalog.js +106 -0
- package/dist/agent/flow_mapping.d.ts +10 -0
- package/dist/agent/flow_mapping.d.ts.map +1 -0
- package/dist/agent/flow_mapping.js +84 -0
- package/dist/agent/framework.d.ts +13 -0
- package/dist/agent/framework.d.ts.map +1 -0
- package/dist/agent/framework.js +149 -0
- package/dist/agent/gap_suggestions.d.ts +14 -0
- package/dist/agent/gap_suggestions.d.ts.map +1 -0
- package/dist/agent/gap_suggestions.js +101 -0
- package/dist/agent/generator.d.ts +10 -0
- package/dist/agent/generator.d.ts.map +1 -0
- package/dist/agent/generator.js +115 -0
- package/dist/agent/git.d.ts +11 -0
- package/dist/agent/git.d.ts.map +1 -0
- package/dist/agent/git.js +90 -0
- package/dist/agent/handoff.d.ts +22 -0
- package/dist/agent/handoff.d.ts.map +1 -0
- package/dist/agent/handoff.js +180 -0
- package/dist/agent/impact-analyzer.d.ts +114 -0
- package/dist/agent/impact-analyzer.d.ts.map +1 -0
- package/dist/agent/impact-analyzer.js +557 -0
- package/dist/agent/index.d.ts +21 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +38 -0
- package/dist/agent/model-router.d.ts +57 -0
- package/dist/agent/model-router.d.ts.map +1 -0
- package/dist/agent/model-router.js +154 -0
- package/dist/agent/operational_insights.d.ts +41 -0
- package/dist/agent/operational_insights.d.ts.map +1 -0
- package/dist/agent/operational_insights.js +126 -0
- package/dist/agent/pipeline.d.ts +23 -0
- package/dist/agent/pipeline.d.ts.map +1 -0
- package/dist/agent/pipeline.js +609 -0
- package/dist/agent/plan.d.ts +91 -0
- package/dist/agent/plan.d.ts.map +1 -0
- package/dist/agent/plan.js +331 -0
- package/dist/agent/playwright_report.d.ts +8 -0
- package/dist/agent/playwright_report.d.ts.map +1 -0
- package/dist/agent/playwright_report.js +126 -0
- package/dist/agent/report-generator.d.ts +24 -0
- package/dist/agent/report-generator.d.ts.map +1 -0
- package/dist/agent/report-generator.js +250 -0
- package/dist/agent/report.d.ts +81 -0
- package/dist/agent/report.d.ts.map +1 -0
- package/dist/agent/report.js +147 -0
- package/dist/agent/runner.d.ts +7 -0
- package/dist/agent/runner.d.ts.map +1 -0
- package/dist/agent/runner.js +576 -0
- package/dist/agent/selectors.d.ts +10 -0
- package/dist/agent/selectors.d.ts.map +1 -0
- package/dist/agent/selectors.js +75 -0
- package/dist/agent/spec-bridge.d.ts +101 -0
- package/dist/agent/spec-bridge.d.ts.map +1 -0
- package/dist/agent/spec-bridge.js +273 -0
- package/dist/agent/spec-builder.d.ts +102 -0
- package/dist/agent/spec-builder.d.ts.map +1 -0
- package/dist/agent/spec-builder.js +273 -0
- package/dist/agent/subsystem_risk.d.ts +23 -0
- package/dist/agent/subsystem_risk.d.ts.map +1 -0
- package/dist/agent/subsystem_risk.js +207 -0
- package/dist/agent/telemetry.d.ts +84 -0
- package/dist/agent/telemetry.d.ts.map +1 -0
- package/dist/agent/telemetry.js +220 -0
- package/dist/agent/test_path.d.ts +2 -0
- package/dist/agent/test_path.d.ts.map +1 -0
- package/dist/agent/test_path.js +23 -0
- package/dist/agent/tests.d.ts +18 -0
- package/dist/agent/tests.d.ts.map +1 -0
- package/dist/agent/tests.js +106 -0
- package/dist/agent/traceability.d.ts +22 -0
- package/dist/agent/traceability.d.ts.map +1 -0
- package/dist/agent/traceability.js +183 -0
- package/dist/agent/traceability_capture.d.ts +18 -0
- package/dist/agent/traceability_capture.d.ts.map +1 -0
- package/dist/agent/traceability_capture.js +313 -0
- package/dist/agent/traceability_ingest.d.ts +21 -0
- package/dist/agent/traceability_ingest.d.ts.map +1 -0
- package/dist/agent/traceability_ingest.js +237 -0
- package/dist/agent/utils.d.ts +13 -0
- package/dist/agent/utils.d.ts.map +1 -0
- package/dist/agent/utils.js +152 -0
- package/dist/agent/validators/selector-validator.d.ts +74 -0
- package/dist/agent/validators/selector-validator.d.ts.map +1 -0
- package/dist/agent/validators/selector-validator.js +165 -0
- package/dist/anthropic_provider.d.ts +65 -0
- package/dist/anthropic_provider.d.ts.map +1 -0
- package/dist/anthropic_provider.js +332 -0
- package/dist/api.d.ts +48 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +113 -0
- package/dist/base_provider.d.ts +53 -0
- package/dist/base_provider.d.ts.map +1 -0
- package/dist/base_provider.js +81 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +843 -0
- package/dist/custom_provider.d.ts +20 -0
- package/dist/custom_provider.d.ts.map +1 -0
- package/dist/custom_provider.js +276 -0
- package/dist/e2e-test-gen/index.d.ts +51 -0
- package/dist/e2e-test-gen/index.d.ts.map +1 -0
- package/dist/e2e-test-gen/index.js +57 -0
- package/dist/e2e-test-gen/spec_parser.d.ts +142 -0
- package/dist/e2e-test-gen/spec_parser.d.ts.map +1 -0
- package/dist/e2e-test-gen/spec_parser.js +786 -0
- package/dist/e2e-test-gen/types.d.ts +185 -0
- package/dist/e2e-test-gen/types.d.ts.map +1 -0
- package/dist/e2e-test-gen/types.js +4 -0
- package/dist/esm/agent/analysis.js +287 -0
- package/dist/esm/agent/blast_radius.js +34 -0
- package/dist/esm/agent/cache_utils.js +63 -0
- package/dist/esm/agent/config.js +637 -0
- package/dist/esm/agent/dependency_graph.js +224 -0
- package/dist/esm/agent/feedback.js +253 -0
- package/dist/esm/agent/flags.js +160 -0
- package/dist/esm/agent/flow_catalog.js +103 -0
- package/dist/esm/agent/flow_mapping.js +81 -0
- package/dist/esm/agent/framework.js +145 -0
- package/dist/esm/agent/gap_suggestions.js +98 -0
- package/dist/esm/agent/generator.js +112 -0
- package/dist/esm/agent/git.js +87 -0
- package/dist/esm/agent/handoff.js +177 -0
- package/dist/esm/agent/impact-analyzer.js +548 -0
- package/dist/esm/agent/index.js +22 -0
- package/dist/esm/agent/model-router.js +150 -0
- package/dist/esm/agent/operational_insights.js +123 -0
- package/dist/esm/agent/pipeline.js +605 -0
- package/dist/esm/agent/plan.js +324 -0
- package/dist/esm/agent/playwright_report.js +123 -0
- package/dist/esm/agent/report-generator.js +247 -0
- package/dist/esm/agent/report.js +144 -0
- package/dist/esm/agent/runner.js +572 -0
- package/dist/esm/agent/selectors.js +71 -0
- package/dist/esm/agent/spec-bridge.js +267 -0
- package/dist/esm/agent/spec-builder.js +267 -0
- package/dist/esm/agent/subsystem_risk.js +204 -0
- package/dist/esm/agent/telemetry.js +216 -0
- package/dist/esm/agent/test_path.js +20 -0
- package/dist/esm/agent/tests.js +101 -0
- package/dist/esm/agent/traceability.js +180 -0
- package/dist/esm/agent/traceability_capture.js +310 -0
- package/dist/esm/agent/traceability_ingest.js +234 -0
- package/dist/esm/agent/utils.js +138 -0
- package/dist/esm/agent/validators/selector-validator.js +160 -0
- package/dist/esm/anthropic_provider.js +324 -0
- package/dist/esm/api.js +105 -0
- package/dist/esm/base_provider.js +77 -0
- package/dist/esm/cli.js +841 -0
- package/dist/esm/custom_provider.js +272 -0
- package/dist/esm/e2e-test-gen/index.js +50 -0
- package/dist/esm/e2e-test-gen/spec_parser.js +782 -0
- package/dist/esm/e2e-test-gen/types.js +3 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/logger.js +89 -0
- package/dist/esm/mcp-server.js +465 -0
- package/dist/esm/ollama_provider.js +300 -0
- package/dist/esm/openai_provider.js +242 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/plan-and-test-constants.js +126 -0
- package/dist/esm/provider_factory.js +336 -0
- package/dist/esm/provider_interface.js +23 -0
- package/dist/esm/provider_utils.js +96 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +93 -0
- package/dist/mcp-server.d.ts +35 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +469 -0
- package/dist/ollama_provider.d.ts +65 -0
- package/dist/ollama_provider.d.ts.map +1 -0
- package/dist/ollama_provider.js +308 -0
- package/dist/openai_provider.d.ts +23 -0
- package/dist/openai_provider.d.ts.map +1 -0
- package/dist/openai_provider.js +250 -0
- package/dist/plan-and-test-constants.d.ts +110 -0
- package/dist/plan-and-test-constants.d.ts.map +1 -0
- package/dist/plan-and-test-constants.js +132 -0
- package/dist/provider_factory.d.ts +99 -0
- package/dist/provider_factory.d.ts.map +1 -0
- package/dist/provider_factory.js +341 -0
- package/dist/provider_interface.d.ts +358 -0
- package/dist/provider_interface.d.ts.map +1 -0
- package/dist/provider_interface.js +28 -0
- package/dist/provider_utils.d.ts +39 -0
- package/dist/provider_utils.d.ts.map +1 -0
- package/dist/provider_utils.js +103 -0
- package/package.json +101 -0
- package/schemas/gap.schema.json +18 -0
- package/schemas/impact.schema.json +418 -0
- package/schemas/plan.schema.json +285 -0
- package/schemas/subsystem-risk-map.schema.json +62 -0
- 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
|
+
}
|