@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,16 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
export { LLMProviderError, UnsupportedCapabilityError } from './provider_interface.js';
|
|
4
|
+
// Provider implementations
|
|
5
|
+
export { AnthropicProvider, checkAnthropicSetup } from './anthropic_provider.js';
|
|
6
|
+
export { OllamaProvider, checkOllamaSetup } from './ollama_provider.js';
|
|
7
|
+
export { OpenAIProvider, checkOpenAISetup } from './openai_provider.js';
|
|
8
|
+
export { CustomProvider } from './custom_provider.js';
|
|
9
|
+
// Factory
|
|
10
|
+
export { LLMProviderFactory, validateProviderSetup } from './provider_factory.js';
|
|
11
|
+
// Agent API (impact, gap, suggest, traceability ingest)
|
|
12
|
+
export { analyzeImpact, findGaps, recommendTests, handoffGeneratedTests, ingestTraceability, captureTraceability } from './api.js';
|
|
13
|
+
export { appendFeedbackAndRecompute, readCalibration } from './agent/feedback.js';
|
|
14
|
+
export { finalizeGeneratedTests } from './agent/handoff.js';
|
|
15
|
+
export { ingestTraceabilityInput } from './agent/traceability_ingest.js';
|
|
16
|
+
export { captureTraceabilityInput } from './agent/traceability_capture.js';
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
/**
|
|
4
|
+
* Simple structured logging system
|
|
5
|
+
* Replaces 18 console.log statements with configurable logging
|
|
6
|
+
* Environment variable: LOG_LEVEL (ERROR, WARN, INFO, DEBUG)
|
|
7
|
+
*/
|
|
8
|
+
export var LogLevel;
|
|
9
|
+
(function (LogLevel) {
|
|
10
|
+
LogLevel[LogLevel["ERROR"] = 0] = "ERROR";
|
|
11
|
+
LogLevel[LogLevel["WARN"] = 1] = "WARN";
|
|
12
|
+
LogLevel[LogLevel["INFO"] = 2] = "INFO";
|
|
13
|
+
LogLevel[LogLevel["DEBUG"] = 3] = "DEBUG";
|
|
14
|
+
})(LogLevel || (LogLevel = {}));
|
|
15
|
+
/**
|
|
16
|
+
* Get log level from environment variable
|
|
17
|
+
*/
|
|
18
|
+
function getLogLevelFromEnv() {
|
|
19
|
+
const level = process.env.LOG_LEVEL?.toUpperCase() || 'INFO';
|
|
20
|
+
switch (level) {
|
|
21
|
+
case 'ERROR':
|
|
22
|
+
return LogLevel.ERROR;
|
|
23
|
+
case 'WARN':
|
|
24
|
+
return LogLevel.WARN;
|
|
25
|
+
case 'INFO':
|
|
26
|
+
return LogLevel.INFO;
|
|
27
|
+
case 'DEBUG':
|
|
28
|
+
return LogLevel.DEBUG;
|
|
29
|
+
default:
|
|
30
|
+
return LogLevel.INFO;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Convert log level to string
|
|
35
|
+
*/
|
|
36
|
+
function logLevelToString(level) {
|
|
37
|
+
switch (level) {
|
|
38
|
+
case LogLevel.ERROR:
|
|
39
|
+
return 'ERROR';
|
|
40
|
+
case LogLevel.WARN:
|
|
41
|
+
return 'WARN';
|
|
42
|
+
case LogLevel.INFO:
|
|
43
|
+
return 'INFO';
|
|
44
|
+
case LogLevel.DEBUG:
|
|
45
|
+
return 'DEBUG';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export class Logger {
|
|
49
|
+
constructor(minLevel) {
|
|
50
|
+
this.level = minLevel ?? getLogLevelFromEnv();
|
|
51
|
+
}
|
|
52
|
+
error(message, context) {
|
|
53
|
+
if (this.level >= LogLevel.ERROR) {
|
|
54
|
+
this.log(LogLevel.ERROR, message, context);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
warn(message, context) {
|
|
58
|
+
if (this.level >= LogLevel.WARN) {
|
|
59
|
+
this.log(LogLevel.WARN, message, context);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
info(message, context) {
|
|
63
|
+
if (this.level >= LogLevel.INFO) {
|
|
64
|
+
this.log(LogLevel.INFO, message, context);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
debug(message, context) {
|
|
68
|
+
if (this.level >= LogLevel.DEBUG) {
|
|
69
|
+
this.log(LogLevel.DEBUG, message, context);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
setLevel(level) {
|
|
73
|
+
this.level = level;
|
|
74
|
+
}
|
|
75
|
+
log(level, message, context) {
|
|
76
|
+
const timestamp = new Date().toISOString();
|
|
77
|
+
const levelStr = logLevelToString(level);
|
|
78
|
+
const contextStr = context ? ` ${JSON.stringify(context)}` : '';
|
|
79
|
+
const output = `[${timestamp}] [${levelStr}] ${message}${contextStr}`;
|
|
80
|
+
if (level <= LogLevel.WARN) {
|
|
81
|
+
console.error(output);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log(output);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Global logger instance
|
|
89
|
+
export const logger = new Logger();
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
/**
|
|
4
|
+
* MCP Server for E2E Agents - SECURITY HARDENED
|
|
5
|
+
* Exposes tools for Claude and Playwright agents to discover, generate, and heal tests
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from 'child_process';
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
import { globSync } from 'glob';
|
|
11
|
+
/**
|
|
12
|
+
* SECURITY: Path validation helper
|
|
13
|
+
* Prevents directory traversal attacks
|
|
14
|
+
*/
|
|
15
|
+
function validatePathIsWithinRoot(filePath, rootPath) {
|
|
16
|
+
try {
|
|
17
|
+
const normalized = resolve(filePath);
|
|
18
|
+
const normalizedRoot = resolve(rootPath);
|
|
19
|
+
return normalized.startsWith(normalizedRoot + '/') || normalized === normalizedRoot;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* SECURITY: Input validation for shell arguments
|
|
27
|
+
* Prevents command injection attacks
|
|
28
|
+
*/
|
|
29
|
+
function validatePlaywrightPattern(pattern) {
|
|
30
|
+
// Allow alphanumeric, dots, dashes, slashes, asterisks, underscores only
|
|
31
|
+
return /^[a-zA-Z0-9_\-.*\/]+$/.test(pattern) && !pattern.includes('..') && pattern.length < 512;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* SECURITY: Validate git refs to prevent argument injection
|
|
35
|
+
*/
|
|
36
|
+
function validateGitRef(ref) {
|
|
37
|
+
// Allow standard git ref patterns: branches, tags, commit hashes
|
|
38
|
+
// Blocks patterns that start with -- (options) or contain spaces
|
|
39
|
+
return (/^[a-zA-Z0-9_\-./~^]+$/.test(ref) &&
|
|
40
|
+
!ref.startsWith('--') &&
|
|
41
|
+
ref.length < 256 &&
|
|
42
|
+
!ref.includes('\n') &&
|
|
43
|
+
!ref.includes('\0'));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* SECURITY: Validate browser names against allowlist
|
|
47
|
+
*/
|
|
48
|
+
function validateBrowsers(browsers) {
|
|
49
|
+
const allowedBrowsers = new Set(['chromium', 'firefox', 'webkit']);
|
|
50
|
+
return browsers.length > 0 && browsers.length <= 3 && browsers.every((b) => allowedBrowsers.has(b));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* SECURITY: Glob pattern validation
|
|
54
|
+
* Restricts to test-related patterns to prevent enumeration of sensitive files
|
|
55
|
+
*/
|
|
56
|
+
function validateGlobPattern(pattern) {
|
|
57
|
+
// Block attempts to enumerate sensitive patterns
|
|
58
|
+
const blockedPatterns = [/\*\*\/\*\*/, /\.env/, /\.pem/, /\.key/, /aws|credentials|secret|password/i];
|
|
59
|
+
if (pattern.length > 256)
|
|
60
|
+
return false;
|
|
61
|
+
if (blockedPatterns.some((p) => p.test(pattern)))
|
|
62
|
+
return false;
|
|
63
|
+
if (pattern.includes('..'))
|
|
64
|
+
return false;
|
|
65
|
+
return /^[a-zA-Z0-9_\-.*\/]+$/.test(pattern);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* SECURITY: Sanitize error messages to prevent information leakage
|
|
69
|
+
*/
|
|
70
|
+
function sanitizeError(error, operation) {
|
|
71
|
+
if (error instanceof Error) {
|
|
72
|
+
// Only return safe error message, hide internal details
|
|
73
|
+
if (error.message.includes('ENOENT')) {
|
|
74
|
+
return `File not found (${operation})`;
|
|
75
|
+
}
|
|
76
|
+
if (error.message.includes('EACCES')) {
|
|
77
|
+
return `Permission denied (${operation})`;
|
|
78
|
+
}
|
|
79
|
+
if (error.message.includes('EISDIR')) {
|
|
80
|
+
return `Is a directory (${operation})`;
|
|
81
|
+
}
|
|
82
|
+
return `Operation failed: ${operation}`;
|
|
83
|
+
}
|
|
84
|
+
return 'An unexpected error occurred';
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* SECURITY: Rate limiter helper
|
|
88
|
+
*/
|
|
89
|
+
class RateLimiter {
|
|
90
|
+
constructor(maxRequests = 100, windowMs = 60000) {
|
|
91
|
+
this.requests = [];
|
|
92
|
+
this.maxRequests = maxRequests;
|
|
93
|
+
this.windowMs = windowMs;
|
|
94
|
+
}
|
|
95
|
+
isAllowed() {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
this.requests = this.requests.filter((time) => now - time < this.windowMs);
|
|
98
|
+
if (this.requests.length >= this.maxRequests) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
this.requests.push(now);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* MCP Server for autonomous test discovery, generation, and healing
|
|
107
|
+
* Provides tools for Claude to interact with test framework
|
|
108
|
+
*/
|
|
109
|
+
export class E2EAgentsMCPServer {
|
|
110
|
+
constructor(repoRoot = process.cwd()) {
|
|
111
|
+
this.repoRoot = repoRoot;
|
|
112
|
+
this.tools = this.defineTools();
|
|
113
|
+
this.rateLimiter = new RateLimiter(100, 60000); // 100 requests per minute
|
|
114
|
+
}
|
|
115
|
+
defineTools() {
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
name: 'discover_tests',
|
|
119
|
+
description: 'Discover tests that need to be written based on code changes',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
since: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
description: 'Git ref to compare against (e.g., HEAD~5, main)',
|
|
126
|
+
},
|
|
127
|
+
pattern: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
description: "Test file pattern to search (e.g., '**/*.spec.ts')",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'read_file',
|
|
136
|
+
description: 'Read a file from the repository',
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
path: {
|
|
141
|
+
type: 'string',
|
|
142
|
+
description: 'File path relative to repo root',
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
required: ['path'],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'write_file',
|
|
150
|
+
description: 'Write or create a file in the repository',
|
|
151
|
+
inputSchema: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
properties: {
|
|
154
|
+
path: {
|
|
155
|
+
type: 'string',
|
|
156
|
+
description: 'File path relative to repo root',
|
|
157
|
+
},
|
|
158
|
+
content: {
|
|
159
|
+
type: 'string',
|
|
160
|
+
description: 'File content to write',
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
required: ['path', 'content'],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'run_tests',
|
|
168
|
+
description: 'Run Playwright tests matching a pattern',
|
|
169
|
+
inputSchema: {
|
|
170
|
+
type: 'object',
|
|
171
|
+
properties: {
|
|
172
|
+
pattern: {
|
|
173
|
+
type: 'string',
|
|
174
|
+
description: "Test file pattern (e.g., 'tests/**/*.spec.ts')",
|
|
175
|
+
},
|
|
176
|
+
browsers: {
|
|
177
|
+
type: 'array',
|
|
178
|
+
items: { type: 'string' },
|
|
179
|
+
description: 'Browsers to test (chromium, firefox, webkit)',
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'get_git_changes',
|
|
186
|
+
description: 'Get files changed since a git reference',
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
since: {
|
|
191
|
+
type: 'string',
|
|
192
|
+
description: 'Git ref to compare against (e.g., HEAD~5, main)',
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'get_repository_context',
|
|
199
|
+
description: 'Get repository structure and project metadata',
|
|
200
|
+
inputSchema: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
properties: {
|
|
203
|
+
include: {
|
|
204
|
+
type: 'array',
|
|
205
|
+
items: { type: 'string' },
|
|
206
|
+
description: 'What to include (package.json, tsconfig, playwright.config, tests)',
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Handle tool calls from Claude/Playwright agents
|
|
215
|
+
* SECURITY: Rate limiting enforced
|
|
216
|
+
*/
|
|
217
|
+
async callTool(name, args) {
|
|
218
|
+
// SECURITY: Rate limiting
|
|
219
|
+
if (!this.rateLimiter.isAllowed()) {
|
|
220
|
+
return JSON.stringify({ error: 'Rate limit exceeded. Too many requests.' });
|
|
221
|
+
}
|
|
222
|
+
switch (name) {
|
|
223
|
+
case 'discover_tests':
|
|
224
|
+
return this.discoverTests(args);
|
|
225
|
+
case 'read_file':
|
|
226
|
+
return this.readFile(args);
|
|
227
|
+
case 'write_file':
|
|
228
|
+
return this.writeFile(args);
|
|
229
|
+
case 'run_tests':
|
|
230
|
+
return this.runTests(args);
|
|
231
|
+
case 'get_git_changes':
|
|
232
|
+
return this.getGitChanges(args);
|
|
233
|
+
case 'get_repository_context':
|
|
234
|
+
return this.getRepositoryContext(args);
|
|
235
|
+
default:
|
|
236
|
+
return JSON.stringify({ error: 'Unknown tool' });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
discoverTests(args) {
|
|
240
|
+
try {
|
|
241
|
+
const since = args.since || 'HEAD~5';
|
|
242
|
+
const pattern = args.pattern || '**/*.spec.ts';
|
|
243
|
+
// SECURITY: Validate inputs
|
|
244
|
+
if (!validateGitRef(since)) {
|
|
245
|
+
return JSON.stringify({ error: 'Invalid git reference format' });
|
|
246
|
+
}
|
|
247
|
+
if (!validateGlobPattern(pattern)) {
|
|
248
|
+
return JSON.stringify({ error: 'Invalid pattern format' });
|
|
249
|
+
}
|
|
250
|
+
// Get changed files
|
|
251
|
+
const changedFiles = this.getChangedFiles(since);
|
|
252
|
+
// Find test files that might need updating
|
|
253
|
+
const testFiles = globSync(pattern, { cwd: this.repoRoot });
|
|
254
|
+
return JSON.stringify({
|
|
255
|
+
changedFiles,
|
|
256
|
+
existingTests: testFiles,
|
|
257
|
+
recommendedTests: this.analyzeChangesForTests(changedFiles, testFiles),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
return JSON.stringify({ error: sanitizeError(error, 'discover_tests') });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
readFile(args) {
|
|
265
|
+
try {
|
|
266
|
+
// SECURITY: Path traversal prevention
|
|
267
|
+
const filePath = resolve(this.repoRoot, args.path);
|
|
268
|
+
if (!validatePathIsWithinRoot(filePath, this.repoRoot)) {
|
|
269
|
+
return JSON.stringify({ error: 'Access denied' });
|
|
270
|
+
}
|
|
271
|
+
if (!existsSync(filePath)) {
|
|
272
|
+
return JSON.stringify({ error: 'File not found' });
|
|
273
|
+
}
|
|
274
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
275
|
+
return JSON.stringify({ path: args.path, content });
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
return JSON.stringify({ error: sanitizeError(error, 'read_file') });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
writeFile(args) {
|
|
282
|
+
try {
|
|
283
|
+
// SECURITY: Path traversal prevention
|
|
284
|
+
const filePath = resolve(this.repoRoot, args.path);
|
|
285
|
+
if (!validatePathIsWithinRoot(filePath, this.repoRoot)) {
|
|
286
|
+
return JSON.stringify({ error: 'Access denied' });
|
|
287
|
+
}
|
|
288
|
+
// SECURITY: Size limit to prevent resource exhaustion
|
|
289
|
+
if (args.content.length > 10 * 1024 * 1024) {
|
|
290
|
+
// 10MB limit
|
|
291
|
+
return JSON.stringify({ error: 'File too large' });
|
|
292
|
+
}
|
|
293
|
+
writeFileSync(filePath, args.content, 'utf-8');
|
|
294
|
+
return JSON.stringify({ success: true, path: args.path });
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
return JSON.stringify({ error: sanitizeError(error, 'write_file') });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
runTests(args) {
|
|
301
|
+
try {
|
|
302
|
+
const pattern = args.pattern || '**/*.spec.ts';
|
|
303
|
+
const browsers = args.browsers || ['chromium'];
|
|
304
|
+
// SECURITY: Validate inputs
|
|
305
|
+
if (!validatePlaywrightPattern(pattern)) {
|
|
306
|
+
return JSON.stringify({ error: 'Invalid test pattern' });
|
|
307
|
+
}
|
|
308
|
+
if (!validateBrowsers(browsers)) {
|
|
309
|
+
return JSON.stringify({ error: 'Invalid browser specification' });
|
|
310
|
+
}
|
|
311
|
+
const projectArgs = browsers.flatMap((browser) => ['--project', browser]);
|
|
312
|
+
// SECURITY: Use -- to separate playwright options from test args
|
|
313
|
+
const result = spawnSync('npx', [
|
|
314
|
+
'playwright',
|
|
315
|
+
'test',
|
|
316
|
+
...projectArgs,
|
|
317
|
+
'--',
|
|
318
|
+
pattern,
|
|
319
|
+
], {
|
|
320
|
+
cwd: this.repoRoot,
|
|
321
|
+
encoding: 'utf-8',
|
|
322
|
+
timeout: 300000, // 5 minute timeout
|
|
323
|
+
maxBuffer: 1024 * 1024, // 1MB output limit
|
|
324
|
+
});
|
|
325
|
+
if (result.error) {
|
|
326
|
+
return JSON.stringify({
|
|
327
|
+
success: false,
|
|
328
|
+
error: 'Test execution failed',
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
// SECURITY: Don't leak full stdout/stderr, summarize instead
|
|
332
|
+
const stdout = result.stdout ? result.stdout.substring(0, 5000) : '';
|
|
333
|
+
const stderr = result.stderr ? result.stderr.substring(0, 5000) : '';
|
|
334
|
+
return JSON.stringify({
|
|
335
|
+
success: result.status === 0,
|
|
336
|
+
summary: `Exit code: ${result.status}`,
|
|
337
|
+
testsPassed: stdout.includes('passed'),
|
|
338
|
+
testsFailed: stdout.includes('failed'),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
return JSON.stringify({
|
|
343
|
+
success: false,
|
|
344
|
+
error: 'Test execution error',
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
getGitChanges(args) {
|
|
349
|
+
try {
|
|
350
|
+
const since = args.since || 'HEAD~5';
|
|
351
|
+
// SECURITY: Validate git ref
|
|
352
|
+
if (!validateGitRef(since)) {
|
|
353
|
+
return JSON.stringify({ error: 'Invalid git reference format' });
|
|
354
|
+
}
|
|
355
|
+
// SECURITY: Use -- to separate git options from refs
|
|
356
|
+
const result = spawnSync('git', ['diff', '--name-only', '--', `${since}..HEAD`], {
|
|
357
|
+
cwd: this.repoRoot,
|
|
358
|
+
encoding: 'utf-8',
|
|
359
|
+
timeout: 30000,
|
|
360
|
+
});
|
|
361
|
+
if (result.error) {
|
|
362
|
+
return JSON.stringify({ error: 'Git operation failed' });
|
|
363
|
+
}
|
|
364
|
+
const changedFiles = result.stdout.trim().split('\n').filter((f) => f);
|
|
365
|
+
return JSON.stringify({ changedFiles });
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
return JSON.stringify({ error: 'Git operation error' });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
getRepositoryContext(args) {
|
|
372
|
+
try {
|
|
373
|
+
const defaultInclude = ['package.json', 'tsconfig.json', 'playwright.config.ts', 'playwright.config.js'];
|
|
374
|
+
const include = args.include || defaultInclude;
|
|
375
|
+
// SECURITY: Limit to allowed filenames
|
|
376
|
+
const allowedFiles = new Set([
|
|
377
|
+
'package.json',
|
|
378
|
+
'tsconfig.json',
|
|
379
|
+
'tsconfig.base.json',
|
|
380
|
+
'playwright.config.ts',
|
|
381
|
+
'playwright.config.js',
|
|
382
|
+
'jest.config.js',
|
|
383
|
+
'.npmrc',
|
|
384
|
+
'README.md',
|
|
385
|
+
]);
|
|
386
|
+
const context = {};
|
|
387
|
+
for (const file of include) {
|
|
388
|
+
// SECURITY: Validate each path
|
|
389
|
+
if (!allowedFiles.has(file)) {
|
|
390
|
+
continue; // Skip non-allowed files
|
|
391
|
+
}
|
|
392
|
+
const filePath = resolve(this.repoRoot, file);
|
|
393
|
+
if (!validatePathIsWithinRoot(filePath, this.repoRoot)) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (existsSync(filePath)) {
|
|
397
|
+
try {
|
|
398
|
+
context[file] = readFileSync(filePath, 'utf-8');
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
// Ignore read errors for individual files
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// Add test structure with safe globbing
|
|
406
|
+
const testFiles = globSync('**/*.spec.ts', {
|
|
407
|
+
cwd: this.repoRoot,
|
|
408
|
+
ignore: 'node_modules/**',
|
|
409
|
+
maxDepth: 5,
|
|
410
|
+
});
|
|
411
|
+
context.testFiles = testFiles.slice(0, 100); // Limit to 100 files
|
|
412
|
+
return JSON.stringify(context);
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
return JSON.stringify({ error: sanitizeError(error, 'get_repository_context') });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
getChangedFiles(since) {
|
|
419
|
+
try {
|
|
420
|
+
// SECURITY: Validate git ref before use
|
|
421
|
+
if (!validateGitRef(since)) {
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
// SECURITY: Use -- separator
|
|
425
|
+
const result = spawnSync('git', ['diff', '--name-only', '--', `${since}..HEAD`], {
|
|
426
|
+
cwd: this.repoRoot,
|
|
427
|
+
encoding: 'utf-8',
|
|
428
|
+
timeout: 30000,
|
|
429
|
+
});
|
|
430
|
+
if (result.error) {
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
return result.stdout.trim().split('\n').filter((f) => f);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return [];
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
analyzeChangesForTests(changedFiles, existingTests) {
|
|
440
|
+
// Simple heuristic: if a source file changed, suggest a test for it
|
|
441
|
+
return changedFiles
|
|
442
|
+
.filter((f) => !f.endsWith('.spec.ts') && !f.endsWith('.test.ts'))
|
|
443
|
+
.slice(0, 10) // Limit results
|
|
444
|
+
.map((f) => {
|
|
445
|
+
const testFile = f.replace(/\.(ts|js)$/, '.spec.ts');
|
|
446
|
+
return testFile;
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Get all available tools
|
|
451
|
+
*/
|
|
452
|
+
getTools() {
|
|
453
|
+
return this.tools;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Start MCP server
|
|
458
|
+
* Usage: node dist/mcp-server.js
|
|
459
|
+
*/
|
|
460
|
+
if (require.main === module) {
|
|
461
|
+
const server = new E2EAgentsMCPServer();
|
|
462
|
+
console.log('E2E Agents MCP Server started');
|
|
463
|
+
console.log('Tools:', server.getTools().map((t) => t.name).join(', '));
|
|
464
|
+
}
|
|
465
|
+
export default E2EAgentsMCPServer;
|