foundry-mcp 0.3.3__py3-none-any.whl

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 (135) hide show
  1. foundry_mcp/__init__.py +7 -0
  2. foundry_mcp/cli/__init__.py +80 -0
  3. foundry_mcp/cli/__main__.py +9 -0
  4. foundry_mcp/cli/agent.py +96 -0
  5. foundry_mcp/cli/commands/__init__.py +37 -0
  6. foundry_mcp/cli/commands/cache.py +137 -0
  7. foundry_mcp/cli/commands/dashboard.py +148 -0
  8. foundry_mcp/cli/commands/dev.py +446 -0
  9. foundry_mcp/cli/commands/journal.py +377 -0
  10. foundry_mcp/cli/commands/lifecycle.py +274 -0
  11. foundry_mcp/cli/commands/modify.py +824 -0
  12. foundry_mcp/cli/commands/plan.py +633 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +652 -0
  15. foundry_mcp/cli/commands/session.py +479 -0
  16. foundry_mcp/cli/commands/specs.py +856 -0
  17. foundry_mcp/cli/commands/tasks.py +807 -0
  18. foundry_mcp/cli/commands/testing.py +676 -0
  19. foundry_mcp/cli/commands/validate.py +982 -0
  20. foundry_mcp/cli/config.py +98 -0
  21. foundry_mcp/cli/context.py +259 -0
  22. foundry_mcp/cli/flags.py +266 -0
  23. foundry_mcp/cli/logging.py +212 -0
  24. foundry_mcp/cli/main.py +44 -0
  25. foundry_mcp/cli/output.py +122 -0
  26. foundry_mcp/cli/registry.py +110 -0
  27. foundry_mcp/cli/resilience.py +178 -0
  28. foundry_mcp/cli/transcript.py +217 -0
  29. foundry_mcp/config.py +850 -0
  30. foundry_mcp/core/__init__.py +144 -0
  31. foundry_mcp/core/ai_consultation.py +1636 -0
  32. foundry_mcp/core/cache.py +195 -0
  33. foundry_mcp/core/capabilities.py +446 -0
  34. foundry_mcp/core/concurrency.py +898 -0
  35. foundry_mcp/core/context.py +540 -0
  36. foundry_mcp/core/discovery.py +1603 -0
  37. foundry_mcp/core/error_collection.py +728 -0
  38. foundry_mcp/core/error_store.py +592 -0
  39. foundry_mcp/core/feature_flags.py +592 -0
  40. foundry_mcp/core/health.py +749 -0
  41. foundry_mcp/core/journal.py +694 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1350 -0
  44. foundry_mcp/core/llm_patterns.py +510 -0
  45. foundry_mcp/core/llm_provider.py +1569 -0
  46. foundry_mcp/core/logging_config.py +374 -0
  47. foundry_mcp/core/metrics_persistence.py +584 -0
  48. foundry_mcp/core/metrics_registry.py +327 -0
  49. foundry_mcp/core/metrics_store.py +641 -0
  50. foundry_mcp/core/modifications.py +224 -0
  51. foundry_mcp/core/naming.py +123 -0
  52. foundry_mcp/core/observability.py +1216 -0
  53. foundry_mcp/core/otel.py +452 -0
  54. foundry_mcp/core/otel_stubs.py +264 -0
  55. foundry_mcp/core/pagination.py +255 -0
  56. foundry_mcp/core/progress.py +317 -0
  57. foundry_mcp/core/prometheus.py +577 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +546 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +511 -0
  61. foundry_mcp/core/prompts/plan_review.py +623 -0
  62. foundry_mcp/core/providers/__init__.py +225 -0
  63. foundry_mcp/core/providers/base.py +476 -0
  64. foundry_mcp/core/providers/claude.py +460 -0
  65. foundry_mcp/core/providers/codex.py +619 -0
  66. foundry_mcp/core/providers/cursor_agent.py +642 -0
  67. foundry_mcp/core/providers/detectors.py +488 -0
  68. foundry_mcp/core/providers/gemini.py +405 -0
  69. foundry_mcp/core/providers/opencode.py +616 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +302 -0
  71. foundry_mcp/core/providers/package-lock.json +24 -0
  72. foundry_mcp/core/providers/package.json +25 -0
  73. foundry_mcp/core/providers/registry.py +607 -0
  74. foundry_mcp/core/providers/test_provider.py +171 -0
  75. foundry_mcp/core/providers/validation.py +729 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/resilience.py +600 -0
  78. foundry_mcp/core/responses.py +934 -0
  79. foundry_mcp/core/review.py +366 -0
  80. foundry_mcp/core/security.py +438 -0
  81. foundry_mcp/core/spec.py +1650 -0
  82. foundry_mcp/core/task.py +1289 -0
  83. foundry_mcp/core/testing.py +450 -0
  84. foundry_mcp/core/validation.py +2081 -0
  85. foundry_mcp/dashboard/__init__.py +32 -0
  86. foundry_mcp/dashboard/app.py +119 -0
  87. foundry_mcp/dashboard/components/__init__.py +17 -0
  88. foundry_mcp/dashboard/components/cards.py +88 -0
  89. foundry_mcp/dashboard/components/charts.py +234 -0
  90. foundry_mcp/dashboard/components/filters.py +136 -0
  91. foundry_mcp/dashboard/components/tables.py +195 -0
  92. foundry_mcp/dashboard/data/__init__.py +11 -0
  93. foundry_mcp/dashboard/data/stores.py +433 -0
  94. foundry_mcp/dashboard/launcher.py +289 -0
  95. foundry_mcp/dashboard/views/__init__.py +12 -0
  96. foundry_mcp/dashboard/views/errors.py +217 -0
  97. foundry_mcp/dashboard/views/metrics.py +174 -0
  98. foundry_mcp/dashboard/views/overview.py +160 -0
  99. foundry_mcp/dashboard/views/providers.py +83 -0
  100. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  101. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  102. foundry_mcp/prompts/__init__.py +9 -0
  103. foundry_mcp/prompts/workflows.py +525 -0
  104. foundry_mcp/resources/__init__.py +9 -0
  105. foundry_mcp/resources/specs.py +591 -0
  106. foundry_mcp/schemas/__init__.py +38 -0
  107. foundry_mcp/schemas/sdd-spec-schema.json +386 -0
  108. foundry_mcp/server.py +164 -0
  109. foundry_mcp/tools/__init__.py +10 -0
  110. foundry_mcp/tools/unified/__init__.py +71 -0
  111. foundry_mcp/tools/unified/authoring.py +1487 -0
  112. foundry_mcp/tools/unified/context_helpers.py +98 -0
  113. foundry_mcp/tools/unified/documentation_helpers.py +198 -0
  114. foundry_mcp/tools/unified/environment.py +939 -0
  115. foundry_mcp/tools/unified/error.py +462 -0
  116. foundry_mcp/tools/unified/health.py +225 -0
  117. foundry_mcp/tools/unified/journal.py +841 -0
  118. foundry_mcp/tools/unified/lifecycle.py +632 -0
  119. foundry_mcp/tools/unified/metrics.py +777 -0
  120. foundry_mcp/tools/unified/plan.py +745 -0
  121. foundry_mcp/tools/unified/pr.py +294 -0
  122. foundry_mcp/tools/unified/provider.py +629 -0
  123. foundry_mcp/tools/unified/review.py +685 -0
  124. foundry_mcp/tools/unified/review_helpers.py +299 -0
  125. foundry_mcp/tools/unified/router.py +102 -0
  126. foundry_mcp/tools/unified/server.py +580 -0
  127. foundry_mcp/tools/unified/spec.py +808 -0
  128. foundry_mcp/tools/unified/task.py +2202 -0
  129. foundry_mcp/tools/unified/test.py +370 -0
  130. foundry_mcp/tools/unified/verification.py +520 -0
  131. foundry_mcp-0.3.3.dist-info/METADATA +337 -0
  132. foundry_mcp-0.3.3.dist-info/RECORD +135 -0
  133. foundry_mcp-0.3.3.dist-info/WHEEL +4 -0
  134. foundry_mcp-0.3.3.dist-info/entry_points.txt +3 -0
  135. foundry_mcp-0.3.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,450 @@
1
+ """
2
+ Testing operations for foundry-mcp.
3
+ Provides functions for running tests and test discovery.
4
+ """
5
+
6
+ import subprocess
7
+ import uuid
8
+ from dataclasses import dataclass, field
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List, Optional
12
+
13
+
14
+ # Schema version for compatibility tracking
15
+ SCHEMA_VERSION = "1.0.0"
16
+
17
+
18
+ # Presets for common test configurations
19
+ TEST_PRESETS = {
20
+ "quick": {
21
+ "timeout": 60,
22
+ "verbose": False,
23
+ "fail_fast": True,
24
+ "markers": "not slow",
25
+ },
26
+ "full": {
27
+ "timeout": 300,
28
+ "verbose": True,
29
+ "fail_fast": False,
30
+ "markers": None,
31
+ },
32
+ "unit": {
33
+ "timeout": 120,
34
+ "verbose": True,
35
+ "fail_fast": False,
36
+ "markers": "unit",
37
+ "pattern": "test_*.py",
38
+ },
39
+ "integration": {
40
+ "timeout": 300,
41
+ "verbose": True,
42
+ "fail_fast": False,
43
+ "markers": "integration",
44
+ },
45
+ "smoke": {
46
+ "timeout": 30,
47
+ "verbose": False,
48
+ "fail_fast": True,
49
+ "markers": "smoke",
50
+ },
51
+ }
52
+
53
+
54
+ # Data structures
55
+
56
+ @dataclass
57
+ class TestResult:
58
+ """
59
+ Result of a single test.
60
+ """
61
+ name: str
62
+ outcome: str # passed, failed, skipped, error
63
+ duration: float = 0.0
64
+ message: Optional[str] = None
65
+ file_path: Optional[str] = None
66
+ line_number: Optional[int] = None
67
+ stdout: Optional[str] = None
68
+ stderr: Optional[str] = None
69
+
70
+
71
+ @dataclass
72
+ class TestRunResult:
73
+ """
74
+ Result of a test run.
75
+ """
76
+ success: bool
77
+ execution_id: str = ""
78
+ schema_version: str = SCHEMA_VERSION
79
+ timestamp: str = ""
80
+ duration: float = 0.0
81
+ total: int = 0
82
+ passed: int = 0
83
+ failed: int = 0
84
+ skipped: int = 0
85
+ errors: int = 0
86
+ tests: List[TestResult] = field(default_factory=list)
87
+ command: str = ""
88
+ cwd: str = ""
89
+ stdout: str = ""
90
+ stderr: str = ""
91
+ error: Optional[str] = None
92
+ metadata: Dict[str, Any] = field(default_factory=dict)
93
+
94
+ def __post_init__(self):
95
+ if not self.execution_id:
96
+ self.execution_id = str(uuid.uuid4())[:8]
97
+ if not self.timestamp:
98
+ self.timestamp = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
99
+
100
+
101
+ @dataclass
102
+ class DiscoveredTest:
103
+ """
104
+ A discovered test.
105
+ """
106
+ name: str
107
+ file_path: str
108
+ line_number: Optional[int] = None
109
+ markers: List[str] = field(default_factory=list)
110
+ docstring: Optional[str] = None
111
+
112
+
113
+ @dataclass
114
+ class TestDiscoveryResult:
115
+ """
116
+ Result of test discovery.
117
+ """
118
+ success: bool
119
+ schema_version: str = SCHEMA_VERSION
120
+ timestamp: str = ""
121
+ total: int = 0
122
+ tests: List[DiscoveredTest] = field(default_factory=list)
123
+ test_files: List[str] = field(default_factory=list)
124
+ error: Optional[str] = None
125
+ metadata: Dict[str, Any] = field(default_factory=dict)
126
+
127
+ def __post_init__(self):
128
+ if not self.timestamp:
129
+ self.timestamp = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
130
+ self.total = len(self.tests)
131
+
132
+
133
+ # Main test runner
134
+
135
+ class TestRunner:
136
+ """
137
+ Test runner for pytest-based projects.
138
+ """
139
+
140
+ def __init__(self, workspace: Optional[Path] = None):
141
+ """
142
+ Initialize test runner.
143
+
144
+ Args:
145
+ workspace: Repository root (defaults to current directory)
146
+ """
147
+ self.workspace = workspace or Path.cwd()
148
+
149
+ def run_tests(
150
+ self,
151
+ target: Optional[str] = None,
152
+ preset: Optional[str] = None,
153
+ timeout: int = 300,
154
+ verbose: bool = True,
155
+ fail_fast: bool = False,
156
+ markers: Optional[str] = None,
157
+ extra_args: Optional[List[str]] = None,
158
+ ) -> TestRunResult:
159
+ """
160
+ Run tests using pytest.
161
+
162
+ Args:
163
+ target: Test target (file, directory, or test name)
164
+ preset: Use a preset configuration (quick, full, unit, integration, smoke)
165
+ timeout: Timeout in seconds
166
+ verbose: Enable verbose output
167
+ fail_fast: Stop on first failure
168
+ markers: Pytest markers expression
169
+ extra_args: Additional pytest arguments
170
+
171
+ Returns:
172
+ TestRunResult with test outcomes
173
+ """
174
+ # Apply preset if specified
175
+ if preset and preset in TEST_PRESETS:
176
+ preset_config = TEST_PRESETS[preset]
177
+ timeout = preset_config.get("timeout", timeout)
178
+ verbose = preset_config.get("verbose", verbose)
179
+ fail_fast = preset_config.get("fail_fast", fail_fast)
180
+ markers = preset_config.get("markers", markers)
181
+
182
+ # Build command
183
+ cmd = ["python", "-m", "pytest"]
184
+
185
+ if target:
186
+ cmd.append(target)
187
+
188
+ if verbose:
189
+ cmd.append("-v")
190
+
191
+ if fail_fast:
192
+ cmd.append("-x")
193
+
194
+ if markers:
195
+ cmd.extend(["-m", markers])
196
+
197
+ # Add JSON output for parsing
198
+ cmd.append("--tb=short")
199
+
200
+ if extra_args:
201
+ cmd.extend(extra_args)
202
+
203
+ command_str = " ".join(cmd)
204
+
205
+ try:
206
+ result = subprocess.run(
207
+ cmd,
208
+ cwd=str(self.workspace),
209
+ capture_output=True,
210
+ text=True,
211
+ timeout=timeout,
212
+ )
213
+
214
+ # Parse output
215
+ tests, passed, failed, skipped, errors = self._parse_pytest_output(result.stdout)
216
+
217
+ return TestRunResult(
218
+ success=result.returncode == 0,
219
+ duration=0.0, # Would need timing wrapper
220
+ total=len(tests),
221
+ passed=passed,
222
+ failed=failed,
223
+ skipped=skipped,
224
+ errors=errors,
225
+ tests=tests,
226
+ command=command_str,
227
+ cwd=str(self.workspace),
228
+ stdout=result.stdout,
229
+ stderr=result.stderr,
230
+ metadata={
231
+ "return_code": result.returncode,
232
+ "preset": preset,
233
+ "target": target,
234
+ },
235
+ )
236
+
237
+ except subprocess.TimeoutExpired:
238
+ return TestRunResult(
239
+ success=False,
240
+ command=command_str,
241
+ cwd=str(self.workspace),
242
+ error=f"Test run timed out after {timeout} seconds",
243
+ metadata={"timeout": timeout},
244
+ )
245
+
246
+ except FileNotFoundError:
247
+ return TestRunResult(
248
+ success=False,
249
+ command=command_str,
250
+ cwd=str(self.workspace),
251
+ error="pytest not found. Install with: pip install pytest",
252
+ )
253
+
254
+ except Exception as e:
255
+ return TestRunResult(
256
+ success=False,
257
+ command=command_str,
258
+ cwd=str(self.workspace),
259
+ error=str(e),
260
+ )
261
+
262
+ def _parse_pytest_output(self, output: str) -> tuple:
263
+ """
264
+ Parse pytest output to extract test results.
265
+
266
+ Returns:
267
+ Tuple of (tests, passed, failed, skipped, errors)
268
+ """
269
+ tests = []
270
+ passed = 0
271
+ failed = 0
272
+ skipped = 0
273
+ errors = 0
274
+
275
+ lines = output.split("\n")
276
+
277
+ for line in lines:
278
+ line = line.strip()
279
+
280
+ # Parse individual test results
281
+ if "::" in line:
282
+ if " PASSED" in line:
283
+ name = line.split(" PASSED")[0].strip()
284
+ tests.append(TestResult(name=name, outcome="passed"))
285
+ passed += 1
286
+ elif " FAILED" in line:
287
+ name = line.split(" FAILED")[0].strip()
288
+ tests.append(TestResult(name=name, outcome="failed"))
289
+ failed += 1
290
+ elif " SKIPPED" in line:
291
+ name = line.split(" SKIPPED")[0].strip()
292
+ tests.append(TestResult(name=name, outcome="skipped"))
293
+ skipped += 1
294
+ elif " ERROR" in line:
295
+ name = line.split(" ERROR")[0].strip()
296
+ tests.append(TestResult(name=name, outcome="error"))
297
+ errors += 1
298
+
299
+ # Parse summary line
300
+ if "passed" in line.lower() and ("failed" in line.lower() or "error" in line.lower() or "skipped" in line.lower()):
301
+ # Try to extract counts from summary like "5 passed, 2 failed"
302
+ import re
303
+ passed_match = re.search(r"(\d+) passed", line)
304
+ failed_match = re.search(r"(\d+) failed", line)
305
+ skipped_match = re.search(r"(\d+) skipped", line)
306
+ error_match = re.search(r"(\d+) error", line)
307
+
308
+ if passed_match:
309
+ passed = int(passed_match.group(1))
310
+ if failed_match:
311
+ failed = int(failed_match.group(1))
312
+ if skipped_match:
313
+ skipped = int(skipped_match.group(1))
314
+ if error_match:
315
+ errors = int(error_match.group(1))
316
+
317
+ return tests, passed, failed, skipped, errors
318
+
319
+ def discover_tests(
320
+ self,
321
+ target: Optional[str] = None,
322
+ pattern: str = "test_*.py",
323
+ ) -> TestDiscoveryResult:
324
+ """
325
+ Discover tests without running them.
326
+
327
+ Args:
328
+ target: Directory or file to search
329
+ pattern: File pattern for test files
330
+
331
+ Returns:
332
+ TestDiscoveryResult with discovered tests
333
+ """
334
+ cmd = ["python", "-m", "pytest", "--collect-only", "-q"]
335
+
336
+ if target:
337
+ cmd.append(target)
338
+
339
+ try:
340
+ result = subprocess.run(
341
+ cmd,
342
+ cwd=str(self.workspace),
343
+ capture_output=True,
344
+ text=True,
345
+ timeout=60,
346
+ )
347
+
348
+ tests, test_files = self._parse_collect_output(result.stdout)
349
+
350
+ return TestDiscoveryResult(
351
+ success=result.returncode == 0,
352
+ tests=tests,
353
+ test_files=test_files,
354
+ metadata={
355
+ "target": target,
356
+ "pattern": pattern,
357
+ },
358
+ )
359
+
360
+ except subprocess.TimeoutExpired:
361
+ return TestDiscoveryResult(
362
+ success=False,
363
+ error="Test discovery timed out",
364
+ )
365
+
366
+ except Exception as e:
367
+ return TestDiscoveryResult(
368
+ success=False,
369
+ error=str(e),
370
+ )
371
+
372
+ def _parse_collect_output(self, output: str) -> tuple:
373
+ """
374
+ Parse pytest --collect-only output.
375
+
376
+ Returns:
377
+ Tuple of (tests, test_files)
378
+ """
379
+ tests = []
380
+ test_files = set()
381
+
382
+ for line in output.split("\n"):
383
+ line = line.strip()
384
+ if "::" in line and not line.startswith("="):
385
+ # Parse test path like "tests/test_foo.py::TestClass::test_method"
386
+ parts = line.split("::")
387
+ if parts:
388
+ file_path = parts[0]
389
+ test_files.add(file_path)
390
+
391
+ tests.append(DiscoveredTest(
392
+ name=line,
393
+ file_path=file_path,
394
+ ))
395
+
396
+ return tests, list(test_files)
397
+
398
+
399
+ # Convenience functions
400
+
401
+ def run_tests(
402
+ target: Optional[str] = None,
403
+ preset: Optional[str] = None,
404
+ workspace: Optional[Path] = None,
405
+ **kwargs,
406
+ ) -> TestRunResult:
407
+ """
408
+ Run tests using pytest.
409
+
410
+ Args:
411
+ target: Test target
412
+ preset: Preset configuration
413
+ workspace: Repository root
414
+ **kwargs: Additional arguments for TestRunner.run_tests
415
+
416
+ Returns:
417
+ TestRunResult with test outcomes
418
+ """
419
+ runner = TestRunner(workspace)
420
+ return runner.run_tests(target, preset, **kwargs)
421
+
422
+
423
+ def discover_tests(
424
+ target: Optional[str] = None,
425
+ workspace: Optional[Path] = None,
426
+ pattern: str = "test_*.py",
427
+ ) -> TestDiscoveryResult:
428
+ """
429
+ Discover tests without running them.
430
+
431
+ Args:
432
+ target: Directory or file to search
433
+ workspace: Repository root
434
+ pattern: File pattern
435
+
436
+ Returns:
437
+ TestDiscoveryResult with discovered tests
438
+ """
439
+ runner = TestRunner(workspace)
440
+ return runner.discover_tests(target, pattern)
441
+
442
+
443
+ def get_presets() -> Dict[str, Dict[str, Any]]:
444
+ """
445
+ Get available test presets.
446
+
447
+ Returns:
448
+ Dict of preset names to configurations
449
+ """
450
+ return TEST_PRESETS.copy()