@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.
- package/README.md +95 -8
- package/dist/adapters/cypress.d.ts +10 -0
- package/dist/adapters/cypress.d.ts.map +1 -0
- package/dist/adapters/cypress.js +86 -0
- package/dist/adapters/framework_adapter.d.ts +41 -0
- package/dist/adapters/framework_adapter.d.ts.map +1 -0
- package/dist/adapters/framework_adapter.js +152 -0
- package/dist/adapters/playwright.d.ts +10 -0
- package/dist/adapters/playwright.d.ts.map +1 -0
- package/dist/adapters/playwright.js +86 -0
- package/dist/adapters/pytest.d.ts +10 -0
- package/dist/adapters/pytest.d.ts.map +1 -0
- package/dist/adapters/pytest.js +96 -0
- package/dist/adapters/supertest.d.ts +12 -0
- package/dist/adapters/supertest.d.ts.map +1 -0
- package/dist/adapters/supertest.js +85 -0
- package/dist/agent/config.d.ts +1 -1
- package/dist/agent/config.d.ts.map +1 -1
- package/dist/agent/git.d.ts +1 -0
- package/dist/agent/git.d.ts.map +1 -1
- package/dist/agent/git.js +3 -0
- package/dist/agentic/fix_loop.d.ts.map +1 -1
- package/dist/agentic/fix_loop.js +5 -4
- package/dist/agentic/runner.d.ts +2 -0
- package/dist/agentic/runner.d.ts.map +1 -1
- package/dist/agentic/runner.js +15 -12
- package/dist/agents/cross-impact.d.ts.map +1 -1
- package/dist/agents/cross-impact.js +6 -1
- package/dist/agents/executor.d.ts.map +1 -1
- package/dist/agents/executor.js +6 -1
- package/dist/agents/strategist.d.ts.map +1 -1
- package/dist/agents/strategist.js +6 -1
- package/dist/agents/test-designer.d.ts.map +1 -1
- package/dist/agents/test-designer.js +6 -1
- package/dist/anthropic_provider.d.ts.map +1 -1
- package/dist/anthropic_provider.js +1 -0
- package/dist/base_provider.d.ts +56 -0
- package/dist/base_provider.d.ts.map +1 -1
- package/dist/base_provider.js +123 -1
- package/dist/budget_ledger.d.ts +28 -0
- package/dist/budget_ledger.d.ts.map +1 -0
- package/dist/budget_ledger.js +62 -0
- package/dist/cache/cached_provider.d.ts +45 -0
- package/dist/cache/cached_provider.d.ts.map +1 -0
- package/dist/cache/cached_provider.js +88 -0
- package/dist/cache/response_cache.d.ts +79 -0
- package/dist/cache/response_cache.d.ts.map +1 -0
- package/dist/cache/response_cache.js +177 -0
- package/dist/cli/commands/bootstrap.d.ts +3 -0
- package/dist/cli/commands/bootstrap.d.ts.map +1 -0
- package/dist/cli/commands/bootstrap.js +109 -0
- package/dist/cli/commands/cost_report.d.ts +3 -0
- package/dist/cli/commands/cost_report.d.ts.map +1 -0
- package/dist/cli/commands/cost_report.js +115 -0
- package/dist/cli/commands/crew.d.ts.map +1 -1
- package/dist/cli/commands/crew.js +118 -1
- package/dist/cli/commands/gate.d.ts +3 -0
- package/dist/cli/commands/gate.d.ts.map +1 -0
- package/dist/cli/commands/gate.js +86 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +7 -62
- package/dist/cli/commands/train.d.ts.map +1 -1
- package/dist/cli/commands/train.js +16 -21
- package/dist/cli/defaults.d.ts +35 -0
- package/dist/cli/defaults.d.ts.map +1 -0
- package/dist/cli/defaults.js +125 -0
- package/dist/cli/errors.d.ts +27 -0
- package/dist/cli/errors.d.ts.map +1 -0
- package/dist/cli/errors.js +57 -0
- package/dist/cli/parse_args.d.ts.map +1 -1
- package/dist/cli/parse_args.js +24 -2
- package/dist/cli/types.d.ts +7 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli.js +47 -2
- package/dist/crew/context.d.ts +15 -0
- package/dist/crew/context.d.ts.map +1 -1
- package/dist/crew/orchestrator.d.ts +14 -0
- package/dist/crew/orchestrator.d.ts.map +1 -1
- package/dist/crew/orchestrator.js +162 -4
- package/dist/crew/protocol.d.ts +13 -0
- package/dist/crew/protocol.d.ts.map +1 -1
- package/dist/crew/provider.d.ts +15 -1
- package/dist/crew/provider.d.ts.map +1 -1
- package/dist/crew/provider.js +24 -4
- package/dist/custom_provider.d.ts.map +1 -1
- package/dist/custom_provider.js +1 -0
- package/dist/engine/diff_loader.d.ts.map +1 -1
- package/dist/engine/diff_loader.js +3 -14
- package/dist/engine/impact_engine.d.ts.map +1 -1
- package/dist/engine/impact_engine.js +9 -23
- package/dist/esm/adapters/cypress.js +49 -0
- package/dist/esm/adapters/framework_adapter.js +114 -0
- package/dist/esm/adapters/playwright.js +49 -0
- package/dist/esm/adapters/pytest.js +59 -0
- package/dist/esm/adapters/supertest.js +48 -0
- package/dist/esm/agent/git.js +3 -1
- package/dist/esm/agentic/fix_loop.js +5 -4
- package/dist/esm/agentic/runner.js +15 -12
- package/dist/esm/agents/cross-impact.js +6 -1
- package/dist/esm/agents/executor.js +6 -1
- package/dist/esm/agents/strategist.js +6 -1
- package/dist/esm/agents/test-designer.js +6 -1
- package/dist/esm/anthropic_provider.js +1 -0
- package/dist/esm/base_provider.js +121 -0
- package/dist/esm/budget_ledger.js +58 -0
- package/dist/esm/cache/cached_provider.js +82 -0
- package/dist/esm/cache/response_cache.js +140 -0
- package/dist/esm/cli/commands/bootstrap.js +106 -0
- package/dist/esm/cli/commands/cost_report.js +112 -0
- package/dist/esm/cli/commands/crew.js +118 -1
- package/dist/esm/cli/commands/gate.js +83 -0
- package/dist/esm/cli/commands/init.js +3 -58
- package/dist/esm/cli/commands/train.js +16 -21
- package/dist/esm/cli/defaults.js +118 -0
- package/dist/esm/cli/errors.js +52 -0
- package/dist/esm/cli/parse_args.js +24 -2
- package/dist/esm/cli.js +47 -2
- package/dist/esm/crew/orchestrator.js +162 -4
- package/dist/esm/crew/provider.js +24 -4
- package/dist/esm/custom_provider.js +1 -0
- package/dist/esm/engine/diff_loader.js +1 -12
- package/dist/esm/engine/impact_engine.js +9 -23
- package/dist/esm/index.js +21 -0
- package/dist/esm/knowledge/cluster_utils.js +60 -0
- package/dist/esm/knowledge/kg_bridge.js +381 -0
- package/dist/esm/knowledge/kg_types.js +3 -0
- package/dist/esm/knowledge/route_families.js +89 -0
- package/dist/esm/mcp-server.js +2 -4
- package/dist/esm/metrics/prometheus.js +149 -0
- package/dist/esm/model_router.js +59 -0
- package/dist/esm/ollama_provider.js +1 -0
- package/dist/esm/openai_provider.js +1 -0
- package/dist/esm/pipeline/orchestrator.js +6 -12
- package/dist/esm/pipeline/stage0_preprocess.js +12 -19
- package/dist/esm/pipeline/stage2_coverage.js +1 -0
- package/dist/esm/pipeline/stage3_generation.js +1 -0
- package/dist/esm/progress.js +112 -0
- package/dist/esm/prompts/coverage.js +7 -24
- package/dist/esm/prompts/cross-impact.js +3 -21
- package/dist/esm/prompts/generation.js +158 -36
- package/dist/esm/prompts/generation_profile.js +147 -0
- package/dist/esm/prompts/heal.js +33 -15
- package/dist/esm/prompts/impact.js +3 -22
- package/dist/esm/prompts/json_extract.js +36 -0
- package/dist/esm/prompts/strategist.js +2 -20
- package/dist/esm/prompts/test-designer.js +6 -21
- package/dist/esm/provider_factory.js +6 -4
- package/dist/esm/reporters/junit.js +86 -0
- package/dist/esm/reporters/reporter.js +3 -0
- package/dist/esm/reporters/sarif.js +131 -0
- package/dist/esm/resilience/circuit_breaker.js +78 -0
- package/dist/esm/resilience/retry.js +56 -0
- package/dist/esm/sanitize.js +66 -0
- package/dist/esm/training/kg_scanner.js +115 -0
- package/dist/esm/training/scanner.js +27 -34
- package/dist/esm/version.js +33 -0
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -1
- package/dist/knowledge/cluster_utils.d.ts +28 -0
- package/dist/knowledge/cluster_utils.d.ts.map +1 -0
- package/dist/knowledge/cluster_utils.js +67 -0
- package/dist/knowledge/kg_bridge.d.ts +31 -0
- package/dist/knowledge/kg_bridge.d.ts.map +1 -0
- package/dist/knowledge/kg_bridge.js +388 -0
- package/dist/knowledge/kg_types.d.ts +75 -0
- package/dist/knowledge/kg_types.d.ts.map +1 -0
- package/dist/knowledge/kg_types.js +4 -0
- package/dist/knowledge/route_families.d.ts +18 -0
- package/dist/knowledge/route_families.d.ts.map +1 -1
- package/dist/knowledge/route_families.js +91 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +2 -4
- package/dist/metrics/prometheus.d.ts +37 -0
- package/dist/metrics/prometheus.d.ts.map +1 -0
- package/dist/metrics/prometheus.js +153 -0
- package/dist/model_router.d.ts +28 -0
- package/dist/model_router.d.ts.map +1 -0
- package/dist/model_router.js +63 -0
- package/dist/ollama_provider.d.ts.map +1 -1
- package/dist/ollama_provider.js +1 -0
- package/dist/openai_provider.d.ts.map +1 -1
- package/dist/openai_provider.js +1 -0
- package/dist/pipeline/orchestrator.d.ts +2 -0
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +6 -12
- package/dist/pipeline/stage0_preprocess.d.ts.map +1 -1
- package/dist/pipeline/stage0_preprocess.js +11 -18
- package/dist/pipeline/stage2_coverage.d.ts +2 -0
- package/dist/pipeline/stage2_coverage.d.ts.map +1 -1
- package/dist/pipeline/stage2_coverage.js +1 -0
- package/dist/pipeline/stage3_generation.d.ts +2 -0
- package/dist/pipeline/stage3_generation.d.ts.map +1 -1
- package/dist/pipeline/stage3_generation.js +1 -0
- package/dist/pipeline/stage4_heal.d.ts +2 -0
- package/dist/pipeline/stage4_heal.d.ts.map +1 -1
- package/dist/progress.d.ts +22 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +116 -0
- package/dist/prompts/coverage.d.ts +2 -0
- package/dist/prompts/coverage.d.ts.map +1 -1
- package/dist/prompts/coverage.js +7 -24
- package/dist/prompts/cross-impact.d.ts +1 -0
- package/dist/prompts/cross-impact.d.ts.map +1 -1
- package/dist/prompts/cross-impact.js +3 -21
- package/dist/prompts/generation.d.ts +3 -1
- package/dist/prompts/generation.d.ts.map +1 -1
- package/dist/prompts/generation.js +158 -36
- package/dist/prompts/generation_profile.d.ts +29 -0
- package/dist/prompts/generation_profile.d.ts.map +1 -0
- package/dist/prompts/generation_profile.js +151 -0
- package/dist/prompts/heal.d.ts +3 -1
- package/dist/prompts/heal.d.ts.map +1 -1
- package/dist/prompts/heal.js +33 -15
- package/dist/prompts/impact.d.ts +1 -0
- package/dist/prompts/impact.d.ts.map +1 -1
- package/dist/prompts/impact.js +3 -22
- package/dist/prompts/json_extract.d.ts +14 -0
- package/dist/prompts/json_extract.d.ts.map +1 -0
- package/dist/prompts/json_extract.js +39 -0
- package/dist/prompts/strategist.d.ts.map +1 -1
- package/dist/prompts/strategist.js +2 -20
- package/dist/prompts/test-designer.d.ts +2 -0
- package/dist/prompts/test-designer.d.ts.map +1 -1
- package/dist/prompts/test-designer.js +6 -21
- package/dist/provider_factory.d.ts.map +1 -1
- package/dist/provider_factory.js +6 -4
- package/dist/reporters/junit.d.ts +6 -0
- package/dist/reporters/junit.d.ts.map +1 -0
- package/dist/reporters/junit.js +89 -0
- package/dist/reporters/reporter.d.ts +42 -0
- package/dist/reporters/reporter.d.ts.map +1 -0
- package/dist/reporters/reporter.js +4 -0
- package/dist/reporters/sarif.d.ts +7 -0
- package/dist/reporters/sarif.d.ts.map +1 -0
- package/dist/reporters/sarif.js +134 -0
- package/dist/resilience/circuit_breaker.d.ts +36 -0
- package/dist/resilience/circuit_breaker.d.ts.map +1 -0
- package/dist/resilience/circuit_breaker.js +82 -0
- package/dist/resilience/retry.d.ts +11 -0
- package/dist/resilience/retry.d.ts.map +1 -0
- package/dist/resilience/retry.js +59 -0
- package/dist/sanitize.d.ts +15 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +71 -0
- package/dist/training/kg_scanner.d.ts +13 -0
- package/dist/training/kg_scanner.d.ts.map +1 -0
- package/dist/training/kg_scanner.js +118 -0
- package/dist/training/scanner.d.ts +7 -2
- package/dist/training/scanner.d.ts.map +1 -1
- package/dist/training/scanner.js +27 -34
- package/dist/version.d.ts +6 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +36 -0
- package/package.json +7 -2
- 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:
|
|
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:
|
|
273
|
-
|
|
274
|
-
|
|
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, '<')
|
|
7
|
+
.replace(/>/g, '>')
|
|
8
|
+
.replace(/"/g, '"')
|
|
9
|
+
.replace(/'/g, ''');
|
|
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,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
|
+
}
|