@voria/cli 0.0.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 (67) hide show
  1. package/README.md +439 -0
  2. package/bin/voria +730 -0
  3. package/docs/ARCHITECTURE.md +419 -0
  4. package/docs/CHANGELOG.md +189 -0
  5. package/docs/CONTRIBUTING.md +447 -0
  6. package/docs/DESIGN_DECISIONS.md +380 -0
  7. package/docs/DEVELOPMENT.md +535 -0
  8. package/docs/EXAMPLES.md +434 -0
  9. package/docs/INSTALL.md +335 -0
  10. package/docs/IPC_PROTOCOL.md +310 -0
  11. package/docs/LLM_INTEGRATION.md +416 -0
  12. package/docs/MODULES.md +470 -0
  13. package/docs/PERFORMANCE.md +346 -0
  14. package/docs/PLUGINS.md +432 -0
  15. package/docs/QUICKSTART.md +184 -0
  16. package/docs/README.md +133 -0
  17. package/docs/ROADMAP.md +346 -0
  18. package/docs/SECURITY.md +334 -0
  19. package/docs/TROUBLESHOOTING.md +565 -0
  20. package/docs/USER_GUIDE.md +700 -0
  21. package/package.json +63 -0
  22. package/python/voria/__init__.py +8 -0
  23. package/python/voria/__pycache__/__init__.cpython-312.pyc +0 -0
  24. package/python/voria/__pycache__/engine.cpython-312.pyc +0 -0
  25. package/python/voria/core/__init__.py +1 -0
  26. package/python/voria/core/__pycache__/__init__.cpython-312.pyc +0 -0
  27. package/python/voria/core/__pycache__/setup.cpython-312.pyc +0 -0
  28. package/python/voria/core/agent/__init__.py +9 -0
  29. package/python/voria/core/agent/__pycache__/__init__.cpython-312.pyc +0 -0
  30. package/python/voria/core/agent/__pycache__/loop.cpython-312.pyc +0 -0
  31. package/python/voria/core/agent/loop.py +343 -0
  32. package/python/voria/core/executor/__init__.py +19 -0
  33. package/python/voria/core/executor/__pycache__/__init__.cpython-312.pyc +0 -0
  34. package/python/voria/core/executor/__pycache__/executor.cpython-312.pyc +0 -0
  35. package/python/voria/core/executor/executor.py +431 -0
  36. package/python/voria/core/github/__init__.py +33 -0
  37. package/python/voria/core/github/__pycache__/__init__.cpython-312.pyc +0 -0
  38. package/python/voria/core/github/__pycache__/client.cpython-312.pyc +0 -0
  39. package/python/voria/core/github/client.py +438 -0
  40. package/python/voria/core/llm/__init__.py +55 -0
  41. package/python/voria/core/llm/__pycache__/__init__.cpython-312.pyc +0 -0
  42. package/python/voria/core/llm/__pycache__/base.cpython-312.pyc +0 -0
  43. package/python/voria/core/llm/__pycache__/claude_provider.cpython-312.pyc +0 -0
  44. package/python/voria/core/llm/__pycache__/gemini_provider.cpython-312.pyc +0 -0
  45. package/python/voria/core/llm/__pycache__/modal_provider.cpython-312.pyc +0 -0
  46. package/python/voria/core/llm/__pycache__/model_discovery.cpython-312.pyc +0 -0
  47. package/python/voria/core/llm/__pycache__/openai_provider.cpython-312.pyc +0 -0
  48. package/python/voria/core/llm/base.py +152 -0
  49. package/python/voria/core/llm/claude_provider.py +188 -0
  50. package/python/voria/core/llm/gemini_provider.py +148 -0
  51. package/python/voria/core/llm/modal_provider.py +228 -0
  52. package/python/voria/core/llm/model_discovery.py +289 -0
  53. package/python/voria/core/llm/openai_provider.py +146 -0
  54. package/python/voria/core/patcher/__init__.py +9 -0
  55. package/python/voria/core/patcher/__pycache__/__init__.cpython-312.pyc +0 -0
  56. package/python/voria/core/patcher/__pycache__/patcher.cpython-312.pyc +0 -0
  57. package/python/voria/core/patcher/patcher.py +375 -0
  58. package/python/voria/core/planner/__init__.py +1 -0
  59. package/python/voria/core/setup.py +201 -0
  60. package/python/voria/core/token_manager/__init__.py +29 -0
  61. package/python/voria/core/token_manager/__pycache__/__init__.cpython-312.pyc +0 -0
  62. package/python/voria/core/token_manager/__pycache__/manager.cpython-312.pyc +0 -0
  63. package/python/voria/core/token_manager/manager.py +241 -0
  64. package/python/voria/engine.py +1185 -0
  65. package/python/voria/plugins/__init__.py +1 -0
  66. package/python/voria/plugins/python/__init__.py +1 -0
  67. package/python/voria/plugins/typescript/__init__.py +1 -0
@@ -0,0 +1,431 @@
1
+ """
2
+ Test Executor Module - Run tests and parse results
3
+
4
+ Supports multiple test frameworks:
5
+ - pytest (Python)
6
+ - jest (JavaScript/TypeScript)
7
+ - go test (Go)
8
+ - Custom frameworks
9
+ """
10
+
11
+ import asyncio
12
+ import json
13
+ import re
14
+ from pathlib import Path
15
+ from typing import Dict, List, Optional, Tuple
16
+ from dataclasses import dataclass
17
+ from enum import Enum
18
+ import logging
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class TestStatus(Enum):
24
+ """Test execution status"""
25
+
26
+ PASSED = "passed"
27
+ FAILED = "failed"
28
+ ERROR = "error"
29
+ SKIPPED = "skipped"
30
+
31
+
32
+ @dataclass
33
+ class TestResult:
34
+ """Single test result"""
35
+
36
+ name: str
37
+ status: TestStatus
38
+ duration: float = 0.0
39
+ message: str = ""
40
+ error_type: Optional[str] = None
41
+ stacktrace: Optional[str] = None
42
+
43
+
44
+ @dataclass
45
+ class TestSuiteResult:
46
+ """Complete test suite result"""
47
+
48
+ framework: str
49
+ total: int
50
+ passed: int
51
+ failed: int
52
+ skipped: int
53
+ duration: float
54
+ results: List[TestResult]
55
+ stdout: str = ""
56
+ stderr: str = ""
57
+ returncode: int = 0
58
+
59
+
60
+ class PytestParser:
61
+ """Parse pytest output"""
62
+
63
+ @staticmethod
64
+ async def run(test_dir: str = "tests/", repo_path: str = ".") -> TestSuiteResult:
65
+ """Run pytest and parse results"""
66
+
67
+ import subprocess
68
+
69
+ repo = Path(repo_path)
70
+ test_path = repo / test_dir
71
+
72
+ if not test_path.exists():
73
+ logger.warning(f"Test directory not found: {test_path}")
74
+ return TestSuiteResult(
75
+ framework="pytest",
76
+ total=0,
77
+ passed=0,
78
+ failed=0,
79
+ skipped=0,
80
+ duration=0,
81
+ results=[],
82
+ stderr=f"Test directory not found: {test_dir}",
83
+ returncode=-1,
84
+ )
85
+
86
+ try:
87
+ # Run pytest with JSON output
88
+ cmd = [
89
+ "python3",
90
+ "-m",
91
+ "pytest",
92
+ str(test_path),
93
+ "-v",
94
+ "--tb=short",
95
+ "--json-report",
96
+ "--json-report-file=/tmp/pytest-report.json",
97
+ ]
98
+
99
+ result = await asyncio.create_subprocess_shell(
100
+ " ".join(cmd),
101
+ stdout=asyncio.subprocess.PIPE,
102
+ stderr=asyncio.subprocess.PIPE,
103
+ cwd=str(repo),
104
+ )
105
+
106
+ stdout, stderr = await result.communicate()
107
+ stdout_str = stdout.decode()
108
+ stderr_str = stderr.decode()
109
+
110
+ return PytestParser.parse_output(stdout_str, stderr_str, result.returncode)
111
+
112
+ except Exception as e:
113
+ logger.error(f"Pytest execution failed: {e}")
114
+ return TestSuiteResult(
115
+ framework="pytest",
116
+ total=0,
117
+ passed=0,
118
+ failed=0,
119
+ skipped=0,
120
+ duration=0,
121
+ results=[],
122
+ stderr=str(e),
123
+ returncode=-1,
124
+ )
125
+
126
+ @staticmethod
127
+ def parse_output(stdout: str, stderr: str, returncode: int) -> TestSuiteResult:
128
+ """Parse pytest output"""
129
+
130
+ results = []
131
+
132
+ # Match pytest summary line: "passed 10 failed 2 skipped 1 in 0.5s"
133
+ summary_match = re.search(
134
+ r"(\d+)\s+passed(?:\s+(\d+)\s+failed)?(?:\s+(\d+)\s+skipped)?(?:\s+in\s+([\d.]+)s)?",
135
+ stdout,
136
+ )
137
+
138
+ passed = int(summary_match.group(1)) if summary_match else 0
139
+ failed = (
140
+ int(summary_match.group(2))
141
+ if summary_match and summary_match.group(2)
142
+ else 0
143
+ )
144
+ skipped = (
145
+ int(summary_match.group(3))
146
+ if summary_match and summary_match.group(3)
147
+ else 0
148
+ )
149
+ duration = (
150
+ float(summary_match.group(4))
151
+ if summary_match and summary_match.group(4)
152
+ else 0.0
153
+ )
154
+
155
+ # Parse individual test results
156
+ # Match: "test_file.py::test_name PASSED"
157
+ for match in re.finditer(r"(\S+::\S+)\s+(PASSED|FAILED|SKIPPED|ERROR)", stdout):
158
+ test_name = match.group(1)
159
+ status_str = match.group(2)
160
+
161
+ status_map = {
162
+ "PASSED": TestStatus.PASSED,
163
+ "FAILED": TestStatus.FAILED,
164
+ "SKIPPED": TestStatus.SKIPPED,
165
+ "ERROR": TestStatus.ERROR,
166
+ }
167
+
168
+ results.append(
169
+ TestResult(
170
+ name=test_name, status=status_map.get(status_str, TestStatus.ERROR)
171
+ )
172
+ )
173
+
174
+ return TestSuiteResult(
175
+ framework="pytest",
176
+ total=passed + failed + skipped,
177
+ passed=passed,
178
+ failed=failed,
179
+ skipped=skipped,
180
+ duration=duration,
181
+ results=results,
182
+ stdout=stdout,
183
+ stderr=stderr,
184
+ returncode=returncode,
185
+ )
186
+
187
+
188
+ class JestParser:
189
+ """Parse jest output"""
190
+
191
+ @staticmethod
192
+ async def run(test_dir: str = "tests/", repo_path: str = ".") -> TestSuiteResult:
193
+ """Run jest and parse results"""
194
+
195
+ import subprocess
196
+
197
+ repo = Path(repo_path)
198
+ test_path = repo / test_dir
199
+
200
+ if not test_path.exists():
201
+ logger.warning(f"Test directory not found: {test_path}")
202
+ return TestSuiteResult(
203
+ framework="jest",
204
+ total=0,
205
+ passed=0,
206
+ failed=0,
207
+ skipped=0,
208
+ duration=0,
209
+ results=[],
210
+ stderr=f"Test directory not found: {test_dir}",
211
+ returncode=-1,
212
+ )
213
+
214
+ try:
215
+ # Run jest with JSON output
216
+ cmd = ["npx", "jest", str(test_path), "--json", "--verbose"]
217
+
218
+ result = await asyncio.create_subprocess_shell(
219
+ " ".join(cmd),
220
+ stdout=asyncio.subprocess.PIPE,
221
+ stderr=asyncio.subprocess.PIPE,
222
+ cwd=str(repo),
223
+ )
224
+
225
+ stdout, stderr = await result.communicate()
226
+ stdout_str = stdout.decode()
227
+ stderr_str = stderr.decode()
228
+
229
+ return JestParser.parse_output(stdout_str, stderr_str, result.returncode)
230
+
231
+ except Exception as e:
232
+ logger.error(f"Jest execution failed: {e}")
233
+ return TestSuiteResult(
234
+ framework="jest",
235
+ total=0,
236
+ passed=0,
237
+ failed=0,
238
+ skipped=0,
239
+ duration=0,
240
+ results=[],
241
+ stderr=str(e),
242
+ returncode=-1,
243
+ )
244
+
245
+ @staticmethod
246
+ def parse_output(stdout: str, stderr: str, returncode: int) -> TestSuiteResult:
247
+ """Parse jest JSON output"""
248
+
249
+ results = []
250
+
251
+ try:
252
+ # Jest outputs JSON
253
+ data = json.loads(stdout)
254
+
255
+ passed = data.get("numPassedTests", 0)
256
+ failed = data.get("numFailedTests", 0)
257
+ skipped = data.get("numPendingTests", 0)
258
+ duration = (
259
+ data.get("testResults", [{}])[0].get("perfStats", {}).get("end", 0)
260
+ / 1000.0
261
+ )
262
+
263
+ # Parse test results
264
+ for test_result in data.get("testResults", []):
265
+ for assertion in test_result.get("assertionResults", []):
266
+ status_map = {
267
+ "pass": TestStatus.PASSED,
268
+ "fail": TestStatus.FAILED,
269
+ "skip": TestStatus.SKIPPED,
270
+ "todo": TestStatus.SKIPPED,
271
+ }
272
+
273
+ results.append(
274
+ TestResult(
275
+ name=assertion.get("fullName", "unknown"),
276
+ status=status_map.get(
277
+ assertion.get("status", "error"), TestStatus.ERROR
278
+ ),
279
+ duration=assertion.get("duration", 0) / 1000.0,
280
+ message=(
281
+ assertion.get("failureMessages", [""])[0]
282
+ if assertion.get("failureMessages")
283
+ else ""
284
+ ),
285
+ )
286
+ )
287
+
288
+ except json.JSONDecodeError:
289
+ # Fallback: parse text output
290
+ passed = len(re.findall(r"✓", stdout))
291
+ failed = len(re.findall(r"✕", stdout))
292
+
293
+ return TestSuiteResult(
294
+ framework="jest",
295
+ total=passed + failed + skipped,
296
+ passed=passed,
297
+ failed=failed,
298
+ skipped=skipped,
299
+ duration=duration,
300
+ results=results,
301
+ stdout=stdout,
302
+ stderr=stderr,
303
+ returncode=returncode,
304
+ )
305
+
306
+
307
+ class TestExecutor:
308
+ """Execute tests and parse results"""
309
+
310
+ def __init__(self, repo_path: str = "."):
311
+ self.repo_path = Path(repo_path)
312
+
313
+ async def detect_framework(self) -> Optional[str]:
314
+ """Detect test framework from files"""
315
+
316
+ repo = self.repo_path
317
+
318
+ # Check for pytest
319
+ if (repo / "setup.py").exists() or (repo / "pyproject.toml").exists():
320
+ if (repo / "tests").exists() or any(repo.glob("test_*.py")):
321
+ return "pytest"
322
+
323
+ # Check for jest
324
+ if (repo / "package.json").exists():
325
+ if (repo / "tests").exists() or (repo / "__tests__").exists():
326
+ package_data = json.loads((repo / "package.json").read_text())
327
+ if "jest" in package_data.get("devDependencies", {}):
328
+ return "jest"
329
+
330
+ # Check for go tests
331
+ if any(repo.glob("*_test.go")):
332
+ return "go"
333
+
334
+ return None
335
+
336
+ async def run_tests(
337
+ self,
338
+ framework: Optional[str] = None,
339
+ test_dir: str = "tests/",
340
+ language: Optional[str] = None,
341
+ ) -> TestSuiteResult:
342
+ """
343
+ Run tests with specified framework
344
+
345
+ Args:
346
+ framework: Test framework (pytest, jest, go, or auto-detect)
347
+ test_dir: Directory containing tests
348
+ language: Programming language (python, javascript, go)
349
+
350
+ Returns:
351
+ TestSuiteResult with all test results
352
+ """
353
+
354
+ # Auto-detect if not specified
355
+ if not framework:
356
+ framework = await self.detect_framework()
357
+ logger.info(f"Detected framework: {framework}")
358
+
359
+ if not framework:
360
+ return TestSuiteResult(
361
+ framework="unknown",
362
+ total=0,
363
+ passed=0,
364
+ failed=0,
365
+ skipped=0,
366
+ duration=0,
367
+ results=[],
368
+ stderr="Could not detect test framework",
369
+ returncode=-1,
370
+ )
371
+
372
+ framework = framework.lower()
373
+
374
+ if framework == "pytest":
375
+ return await PytestParser.run(test_dir, str(self.repo_path))
376
+ elif framework == "jest":
377
+ return await JestParser.run(test_dir, str(self.repo_path))
378
+ else:
379
+ return TestSuiteResult(
380
+ framework=framework,
381
+ total=0,
382
+ passed=0,
383
+ failed=0,
384
+ skipped=0,
385
+ duration=0,
386
+ results=[],
387
+ stderr=f"Unsupported framework: {framework}",
388
+ returncode=-1,
389
+ )
390
+
391
+ def format_results(self, result: TestSuiteResult) -> str:
392
+ """Format test results as string"""
393
+
394
+ status_emoji = "✅" if result.failed == 0 else "❌"
395
+
396
+ summary = f"""{status_emoji} {result.framework} Results:
397
+ Total: {result.total}
398
+ Passed: {result.passed}
399
+ Failed: {result.failed}
400
+ Skipped: {result.skipped}
401
+ Duration: {result.duration:.2f}s
402
+ """
403
+
404
+ if result.failed > 0:
405
+ summary += f"\nFailed Tests:\n"
406
+ for test in result.results:
407
+ if test.status == TestStatus.FAILED:
408
+ summary += f" ❌ {test.name}\n"
409
+ if test.message:
410
+ summary += f" {test.message[:100]}\n"
411
+
412
+ return summary
413
+
414
+
415
+ async def test_executor():
416
+ """Test the test executor"""
417
+
418
+ executor = TestExecutor("/home/ansh/voria")
419
+
420
+ # Detect framework
421
+ framework = await executor.detect_framework()
422
+ print(f"Detected framework: {framework}")
423
+
424
+ # Run tests (this would fail without pytest installed, but shows the flow)
425
+ if framework == "pytest":
426
+ result = await executor.run_tests(framework="pytest")
427
+ print(executor.format_results(result))
428
+
429
+
430
+ if __name__ == "__main__":
431
+ asyncio.run(test_executor())
@@ -0,0 +1,33 @@
1
+ """GitHub Integration Module
2
+
3
+ Provides GitHub API access for fetching issues and managing pull requests.
4
+
5
+ Usage:
6
+ from voria.core.github import GitHubClient, print_token_guide
7
+
8
+ # Guide user on how to get GitHub token
9
+ print_token_guide()
10
+
11
+ # Or get token interactively
12
+ from voria.core.github import get_github_token
13
+ token = get_github_token()
14
+
15
+ client = GitHubClient(token="ghp_...")
16
+ issue = await client.fetch_issue("owner", "repo", 123)
17
+ """
18
+
19
+ from .client import (
20
+ GitHubClient,
21
+ GitHubIssue,
22
+ print_token_guide,
23
+ get_github_token,
24
+ GITHUB_TOKEN_GUIDE,
25
+ )
26
+
27
+ __all__ = [
28
+ "GitHubClient",
29
+ "GitHubIssue",
30
+ "print_token_guide",
31
+ "get_github_token",
32
+ "GITHUB_TOKEN_GUIDE",
33
+ ]