@yasserkhanorg/e2e-agents 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/LICENSE +168 -0
  2. package/README.md +620 -0
  3. package/dist/agent/analysis.d.ts +62 -0
  4. package/dist/agent/analysis.d.ts.map +1 -0
  5. package/dist/agent/analysis.js +292 -0
  6. package/dist/agent/blast_radius.d.ts +4 -0
  7. package/dist/agent/blast_radius.d.ts.map +1 -0
  8. package/dist/agent/blast_radius.js +37 -0
  9. package/dist/agent/cache_utils.d.ts +38 -0
  10. package/dist/agent/cache_utils.d.ts.map +1 -0
  11. package/dist/agent/cache_utils.js +67 -0
  12. package/dist/agent/config.d.ts +148 -0
  13. package/dist/agent/config.d.ts.map +1 -0
  14. package/dist/agent/config.js +640 -0
  15. package/dist/agent/dependency_graph.d.ts +14 -0
  16. package/dist/agent/dependency_graph.d.ts.map +1 -0
  17. package/dist/agent/dependency_graph.js +227 -0
  18. package/dist/agent/feedback.d.ts +55 -0
  19. package/dist/agent/feedback.d.ts.map +1 -0
  20. package/dist/agent/feedback.js +257 -0
  21. package/dist/agent/flags.d.ts +23 -0
  22. package/dist/agent/flags.d.ts.map +1 -0
  23. package/dist/agent/flags.js +171 -0
  24. package/dist/agent/flow_catalog.d.ts +25 -0
  25. package/dist/agent/flow_catalog.d.ts.map +1 -0
  26. package/dist/agent/flow_catalog.js +106 -0
  27. package/dist/agent/flow_mapping.d.ts +10 -0
  28. package/dist/agent/flow_mapping.d.ts.map +1 -0
  29. package/dist/agent/flow_mapping.js +84 -0
  30. package/dist/agent/framework.d.ts +13 -0
  31. package/dist/agent/framework.d.ts.map +1 -0
  32. package/dist/agent/framework.js +149 -0
  33. package/dist/agent/gap_suggestions.d.ts +14 -0
  34. package/dist/agent/gap_suggestions.d.ts.map +1 -0
  35. package/dist/agent/gap_suggestions.js +101 -0
  36. package/dist/agent/generator.d.ts +10 -0
  37. package/dist/agent/generator.d.ts.map +1 -0
  38. package/dist/agent/generator.js +115 -0
  39. package/dist/agent/git.d.ts +11 -0
  40. package/dist/agent/git.d.ts.map +1 -0
  41. package/dist/agent/git.js +90 -0
  42. package/dist/agent/handoff.d.ts +22 -0
  43. package/dist/agent/handoff.d.ts.map +1 -0
  44. package/dist/agent/handoff.js +180 -0
  45. package/dist/agent/impact-analyzer.d.ts +114 -0
  46. package/dist/agent/impact-analyzer.d.ts.map +1 -0
  47. package/dist/agent/impact-analyzer.js +557 -0
  48. package/dist/agent/index.d.ts +21 -0
  49. package/dist/agent/index.d.ts.map +1 -0
  50. package/dist/agent/index.js +38 -0
  51. package/dist/agent/model-router.d.ts +57 -0
  52. package/dist/agent/model-router.d.ts.map +1 -0
  53. package/dist/agent/model-router.js +154 -0
  54. package/dist/agent/operational_insights.d.ts +41 -0
  55. package/dist/agent/operational_insights.d.ts.map +1 -0
  56. package/dist/agent/operational_insights.js +126 -0
  57. package/dist/agent/pipeline.d.ts +23 -0
  58. package/dist/agent/pipeline.d.ts.map +1 -0
  59. package/dist/agent/pipeline.js +609 -0
  60. package/dist/agent/plan.d.ts +91 -0
  61. package/dist/agent/plan.d.ts.map +1 -0
  62. package/dist/agent/plan.js +331 -0
  63. package/dist/agent/playwright_report.d.ts +8 -0
  64. package/dist/agent/playwright_report.d.ts.map +1 -0
  65. package/dist/agent/playwright_report.js +126 -0
  66. package/dist/agent/report-generator.d.ts +24 -0
  67. package/dist/agent/report-generator.d.ts.map +1 -0
  68. package/dist/agent/report-generator.js +250 -0
  69. package/dist/agent/report.d.ts +81 -0
  70. package/dist/agent/report.d.ts.map +1 -0
  71. package/dist/agent/report.js +147 -0
  72. package/dist/agent/runner.d.ts +7 -0
  73. package/dist/agent/runner.d.ts.map +1 -0
  74. package/dist/agent/runner.js +576 -0
  75. package/dist/agent/selectors.d.ts +10 -0
  76. package/dist/agent/selectors.d.ts.map +1 -0
  77. package/dist/agent/selectors.js +75 -0
  78. package/dist/agent/spec-bridge.d.ts +101 -0
  79. package/dist/agent/spec-bridge.d.ts.map +1 -0
  80. package/dist/agent/spec-bridge.js +273 -0
  81. package/dist/agent/spec-builder.d.ts +102 -0
  82. package/dist/agent/spec-builder.d.ts.map +1 -0
  83. package/dist/agent/spec-builder.js +273 -0
  84. package/dist/agent/subsystem_risk.d.ts +23 -0
  85. package/dist/agent/subsystem_risk.d.ts.map +1 -0
  86. package/dist/agent/subsystem_risk.js +207 -0
  87. package/dist/agent/telemetry.d.ts +84 -0
  88. package/dist/agent/telemetry.d.ts.map +1 -0
  89. package/dist/agent/telemetry.js +220 -0
  90. package/dist/agent/test_path.d.ts +2 -0
  91. package/dist/agent/test_path.d.ts.map +1 -0
  92. package/dist/agent/test_path.js +23 -0
  93. package/dist/agent/tests.d.ts +18 -0
  94. package/dist/agent/tests.d.ts.map +1 -0
  95. package/dist/agent/tests.js +106 -0
  96. package/dist/agent/traceability.d.ts +22 -0
  97. package/dist/agent/traceability.d.ts.map +1 -0
  98. package/dist/agent/traceability.js +183 -0
  99. package/dist/agent/traceability_capture.d.ts +18 -0
  100. package/dist/agent/traceability_capture.d.ts.map +1 -0
  101. package/dist/agent/traceability_capture.js +313 -0
  102. package/dist/agent/traceability_ingest.d.ts +21 -0
  103. package/dist/agent/traceability_ingest.d.ts.map +1 -0
  104. package/dist/agent/traceability_ingest.js +237 -0
  105. package/dist/agent/utils.d.ts +13 -0
  106. package/dist/agent/utils.d.ts.map +1 -0
  107. package/dist/agent/utils.js +152 -0
  108. package/dist/agent/validators/selector-validator.d.ts +74 -0
  109. package/dist/agent/validators/selector-validator.d.ts.map +1 -0
  110. package/dist/agent/validators/selector-validator.js +165 -0
  111. package/dist/anthropic_provider.d.ts +65 -0
  112. package/dist/anthropic_provider.d.ts.map +1 -0
  113. package/dist/anthropic_provider.js +332 -0
  114. package/dist/api.d.ts +48 -0
  115. package/dist/api.d.ts.map +1 -0
  116. package/dist/api.js +113 -0
  117. package/dist/base_provider.d.ts +53 -0
  118. package/dist/base_provider.d.ts.map +1 -0
  119. package/dist/base_provider.js +81 -0
  120. package/dist/cli.d.ts +3 -0
  121. package/dist/cli.d.ts.map +1 -0
  122. package/dist/cli.js +843 -0
  123. package/dist/custom_provider.d.ts +20 -0
  124. package/dist/custom_provider.d.ts.map +1 -0
  125. package/dist/custom_provider.js +276 -0
  126. package/dist/e2e-test-gen/index.d.ts +51 -0
  127. package/dist/e2e-test-gen/index.d.ts.map +1 -0
  128. package/dist/e2e-test-gen/index.js +57 -0
  129. package/dist/e2e-test-gen/spec_parser.d.ts +142 -0
  130. package/dist/e2e-test-gen/spec_parser.d.ts.map +1 -0
  131. package/dist/e2e-test-gen/spec_parser.js +786 -0
  132. package/dist/e2e-test-gen/types.d.ts +185 -0
  133. package/dist/e2e-test-gen/types.d.ts.map +1 -0
  134. package/dist/e2e-test-gen/types.js +4 -0
  135. package/dist/esm/agent/analysis.js +287 -0
  136. package/dist/esm/agent/blast_radius.js +34 -0
  137. package/dist/esm/agent/cache_utils.js +63 -0
  138. package/dist/esm/agent/config.js +637 -0
  139. package/dist/esm/agent/dependency_graph.js +224 -0
  140. package/dist/esm/agent/feedback.js +253 -0
  141. package/dist/esm/agent/flags.js +160 -0
  142. package/dist/esm/agent/flow_catalog.js +103 -0
  143. package/dist/esm/agent/flow_mapping.js +81 -0
  144. package/dist/esm/agent/framework.js +145 -0
  145. package/dist/esm/agent/gap_suggestions.js +98 -0
  146. package/dist/esm/agent/generator.js +112 -0
  147. package/dist/esm/agent/git.js +87 -0
  148. package/dist/esm/agent/handoff.js +177 -0
  149. package/dist/esm/agent/impact-analyzer.js +548 -0
  150. package/dist/esm/agent/index.js +22 -0
  151. package/dist/esm/agent/model-router.js +150 -0
  152. package/dist/esm/agent/operational_insights.js +123 -0
  153. package/dist/esm/agent/pipeline.js +605 -0
  154. package/dist/esm/agent/plan.js +324 -0
  155. package/dist/esm/agent/playwright_report.js +123 -0
  156. package/dist/esm/agent/report-generator.js +247 -0
  157. package/dist/esm/agent/report.js +144 -0
  158. package/dist/esm/agent/runner.js +572 -0
  159. package/dist/esm/agent/selectors.js +71 -0
  160. package/dist/esm/agent/spec-bridge.js +267 -0
  161. package/dist/esm/agent/spec-builder.js +267 -0
  162. package/dist/esm/agent/subsystem_risk.js +204 -0
  163. package/dist/esm/agent/telemetry.js +216 -0
  164. package/dist/esm/agent/test_path.js +20 -0
  165. package/dist/esm/agent/tests.js +101 -0
  166. package/dist/esm/agent/traceability.js +180 -0
  167. package/dist/esm/agent/traceability_capture.js +310 -0
  168. package/dist/esm/agent/traceability_ingest.js +234 -0
  169. package/dist/esm/agent/utils.js +138 -0
  170. package/dist/esm/agent/validators/selector-validator.js +160 -0
  171. package/dist/esm/anthropic_provider.js +324 -0
  172. package/dist/esm/api.js +105 -0
  173. package/dist/esm/base_provider.js +77 -0
  174. package/dist/esm/cli.js +841 -0
  175. package/dist/esm/custom_provider.js +272 -0
  176. package/dist/esm/e2e-test-gen/index.js +50 -0
  177. package/dist/esm/e2e-test-gen/spec_parser.js +782 -0
  178. package/dist/esm/e2e-test-gen/types.js +3 -0
  179. package/dist/esm/index.js +16 -0
  180. package/dist/esm/logger.js +89 -0
  181. package/dist/esm/mcp-server.js +465 -0
  182. package/dist/esm/ollama_provider.js +300 -0
  183. package/dist/esm/openai_provider.js +242 -0
  184. package/dist/esm/package.json +3 -0
  185. package/dist/esm/plan-and-test-constants.js +126 -0
  186. package/dist/esm/provider_factory.js +336 -0
  187. package/dist/esm/provider_interface.js +23 -0
  188. package/dist/esm/provider_utils.js +96 -0
  189. package/dist/index.d.ts +31 -0
  190. package/dist/index.d.ts.map +1 -0
  191. package/dist/index.js +41 -0
  192. package/dist/logger.d.ts +23 -0
  193. package/dist/logger.d.ts.map +1 -0
  194. package/dist/logger.js +93 -0
  195. package/dist/mcp-server.d.ts +35 -0
  196. package/dist/mcp-server.d.ts.map +1 -0
  197. package/dist/mcp-server.js +469 -0
  198. package/dist/ollama_provider.d.ts +65 -0
  199. package/dist/ollama_provider.d.ts.map +1 -0
  200. package/dist/ollama_provider.js +308 -0
  201. package/dist/openai_provider.d.ts +23 -0
  202. package/dist/openai_provider.d.ts.map +1 -0
  203. package/dist/openai_provider.js +250 -0
  204. package/dist/plan-and-test-constants.d.ts +110 -0
  205. package/dist/plan-and-test-constants.d.ts.map +1 -0
  206. package/dist/plan-and-test-constants.js +132 -0
  207. package/dist/provider_factory.d.ts +99 -0
  208. package/dist/provider_factory.d.ts.map +1 -0
  209. package/dist/provider_factory.js +341 -0
  210. package/dist/provider_interface.d.ts +358 -0
  211. package/dist/provider_interface.d.ts.map +1 -0
  212. package/dist/provider_interface.js +28 -0
  213. package/dist/provider_utils.d.ts +39 -0
  214. package/dist/provider_utils.d.ts.map +1 -0
  215. package/dist/provider_utils.js +103 -0
  216. package/package.json +101 -0
  217. package/schemas/gap.schema.json +18 -0
  218. package/schemas/impact.schema.json +418 -0
  219. package/schemas/plan.schema.json +285 -0
  220. package/schemas/subsystem-risk-map.schema.json +62 -0
  221. package/schemas/traceability-input.schema.json +122 -0
@@ -0,0 +1,3 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ export {};
@@ -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;