@trac3er/oh-my-god 2.0.0 → 2.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.
- package/.claude-plugin/marketplace.json +8 -8
- package/.claude-plugin/plugin.json +5 -4
- package/.claude-plugin/scripts/uninstall.sh +74 -3
- package/.claude-plugin/scripts/update.sh +78 -3
- package/.coveragerc +26 -0
- package/.mcp.json +4 -4
- package/CHANGELOG.md +14 -0
- package/CODE_OF_CONDUCT.md +27 -0
- package/CONTRIBUTING.md +62 -0
- package/OMG-setup.sh +1201 -355
- package/README.md +77 -56
- package/SECURITY.md +25 -0
- package/agents/__init__.py +1 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-architect-mode.md +3 -5
- package/agents/omg-backend-engineer.md +3 -5
- package/agents/omg-database-engineer.md +3 -5
- package/agents/omg-frontend-designer.md +4 -5
- package/agents/omg-implement-mode.md +4 -5
- package/agents/omg-infra-engineer.md +3 -5
- package/agents/omg-research-mode.md +4 -6
- package/agents/omg-security-auditor.md +3 -5
- package/agents/omg-testing-engineer.md +3 -5
- package/build/lib/yaml.py +321 -0
- package/commands/OMG:ai-commit.md +101 -14
- package/commands/OMG:arch.md +302 -19
- package/commands/OMG:ccg.md +12 -7
- package/commands/OMG:compat.md +25 -17
- package/commands/OMG:cost.md +173 -13
- package/commands/OMG:crazy.md +1 -1
- package/commands/OMG:create-agent.md +170 -20
- package/commands/OMG:deps.md +235 -17
- package/commands/OMG:domain-init.md +1 -1
- package/commands/OMG:escalate.md +41 -12
- package/commands/OMG:health-check.md +37 -13
- package/commands/OMG:init.md +122 -14
- package/commands/OMG:project-init.md +1 -1
- package/commands/OMG:session-branch.md +76 -9
- package/commands/OMG:session-fork.md +42 -5
- package/commands/OMG:session-merge.md +124 -8
- package/commands/OMG:setup.md +69 -12
- package/commands/OMG:stats.md +215 -14
- package/commands/OMG:teams.md +19 -10
- package/config/lsp_languages.yaml +8 -0
- package/hooks/__init__.py +0 -0
- package/hooks/_agent_registry.py +423 -0
- package/hooks/_analytics.py +291 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +569 -0
- package/hooks/_compression_optimizer.py +119 -0
- package/hooks/_cost_ledger.py +176 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/_protected_context.py +150 -0
- package/hooks/_token_counter.py +221 -0
- package/hooks/branch_manager.py +236 -0
- package/hooks/budget_governor.py +232 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/compression_feedback.py +254 -0
- package/hooks/config-guard.py +216 -0
- package/hooks/context_pressure.py +53 -0
- package/hooks/credential_store.py +1020 -0
- package/hooks/fetch-rate-limits.py +212 -0
- package/hooks/firewall.py +48 -0
- package/hooks/hashline-formatter-bridge.py +224 -0
- package/hooks/hashline-injector.py +273 -0
- package/hooks/hashline-validator.py +216 -0
- package/hooks/idle-detector.py +95 -0
- package/hooks/intentgate-keyword-detector.py +188 -0
- package/hooks/magic-keyword-router.py +195 -0
- package/hooks/policy_engine.py +505 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +219 -0
- package/hooks/post_write.py +46 -0
- package/hooks/pre-compact.py +398 -0
- package/hooks/pre-tool-inject.py +98 -0
- package/hooks/prompt-enhancer.py +672 -0
- package/hooks/quality-runner.py +191 -0
- package/hooks/query.py +512 -0
- package/hooks/secret-guard.py +61 -0
- package/hooks/secret_audit.py +144 -0
- package/hooks/session-end-capture.py +137 -0
- package/hooks/session-start.py +277 -0
- package/hooks/setup_wizard.py +582 -0
- package/hooks/shadow_manager.py +297 -0
- package/hooks/state_migration.py +225 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +945 -0
- package/hooks/test-validator.py +361 -0
- package/hooks/test_generator_hook.py +123 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +149 -0
- package/hooks/trust_review.py +585 -0
- package/hud/omg-hud.mjs +31 -1
- package/lab/__init__.py +1 -0
- package/lab/pipeline.py +75 -0
- package/lab/policies.py +52 -0
- package/package.json +7 -18
- package/plugins/README.md +33 -61
- package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
- package/plugins/advanced/commands/OMG:learn.md +1 -1
- package/plugins/advanced/commands/OMG:security-review.md +3 -3
- package/plugins/advanced/commands/OMG:ship.md +1 -1
- package/plugins/advanced/plugin.json +1 -1
- package/plugins/core/plugin.json +8 -3
- package/plugins/dephealth/__init__.py +0 -0
- package/plugins/dephealth/cve_scanner.py +188 -0
- package/plugins/dephealth/license_checker.py +135 -0
- package/plugins/dephealth/manifest_detector.py +423 -0
- package/plugins/dephealth/vuln_analyzer.py +169 -0
- package/plugins/testgen/__init__.py +0 -0
- package/plugins/testgen/codamosa_engine.py +402 -0
- package/plugins/testgen/edge_case_synthesizer.py +184 -0
- package/plugins/testgen/framework_detector.py +271 -0
- package/plugins/testgen/skeleton_generator.py +219 -0
- package/plugins/viz/__init__.py +0 -0
- package/plugins/viz/ast_parser.py +139 -0
- package/plugins/viz/diagram_generator.py +192 -0
- package/plugins/viz/graph_builder.py +444 -0
- package/plugins/viz/native_parsers.py +259 -0
- package/plugins/viz/regex_parser.py +112 -0
- package/pyproject.toml +81 -0
- package/rules/contextual/write-verify.md +2 -2
- package/rules/core/00-truth.md +1 -1
- package/rules/core/01-surgical.md +1 -1
- package/rules/core/02-circuit-breaker.md +2 -2
- package/rules/core/03-ensemble.md +3 -3
- package/rules/core/04-testing.md +3 -3
- package/runtime/__init__.py +32 -0
- package/runtime/adapters/__init__.py +13 -0
- package/runtime/adapters/claude.py +60 -0
- package/runtime/adapters/gpt.py +53 -0
- package/runtime/adapters/local.py +53 -0
- package/runtime/adoption.py +212 -0
- package/runtime/business_workflow.py +220 -0
- package/runtime/cli_provider.py +85 -0
- package/runtime/compat.py +1299 -0
- package/runtime/custom_agent_loader.py +366 -0
- package/runtime/dispatcher.py +47 -0
- package/runtime/ecosystem.py +371 -0
- package/runtime/legacy_compat.py +7 -0
- package/runtime/mcp_config_writers.py +115 -0
- package/runtime/mcp_lifecycle.py +153 -0
- package/runtime/mcp_memory_server.py +135 -0
- package/runtime/memory_parsers/__init__.py +0 -0
- package/runtime/memory_parsers/chatgpt_parser.py +257 -0
- package/runtime/memory_parsers/claude_import.py +107 -0
- package/runtime/memory_parsers/export.py +97 -0
- package/runtime/memory_parsers/gemini_import.py +91 -0
- package/runtime/memory_parsers/kimi_import.py +91 -0
- package/runtime/memory_store.py +215 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/providers/__init__.py +0 -0
- package/runtime/providers/codex_provider.py +112 -0
- package/runtime/providers/gemini_provider.py +128 -0
- package/runtime/providers/kimi_provider.py +151 -0
- package/runtime/providers/opencode_provider.py +144 -0
- package/runtime/subagent_dispatcher.py +362 -0
- package/runtime/team_router.py +1167 -0
- package/runtime/tmux_session_manager.py +169 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-contract-snapshot.py +12 -0
- package/scripts/check-omg-public-ready.py +193 -0
- package/scripts/check-omg-standalone-clean.py +103 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-legacy.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +492 -0
- package/scripts/settings-merge.py +283 -0
- package/scripts/verify-standalone.sh +8 -4
- package/settings.json +126 -29
- package/templates/profile.yaml +1 -1
- package/tools/__init__.py +2 -0
- package/tools/browser_consent.py +289 -0
- package/tools/browser_stealth.py +481 -0
- package/tools/browser_tool.py +448 -0
- package/tools/changelog_generator.py +347 -0
- package/tools/commit_splitter.py +746 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -0
- package/tools/dashboard_generator.py +300 -0
- package/tools/git_inspector.py +298 -0
- package/tools/lsp_client.py +275 -0
- package/tools/lsp_discovery.py +231 -0
- package/tools/lsp_operations.py +392 -0
- package/tools/pr_generator.py +404 -0
- package/tools/python_repl.py +656 -0
- package/tools/python_sandbox.py +609 -0
- package/tools/search_providers/__init__.py +77 -0
- package/tools/search_providers/brave.py +115 -0
- package/tools/search_providers/exa.py +116 -0
- package/tools/search_providers/jina.py +104 -0
- package/tools/search_providers/perplexity.py +139 -0
- package/tools/search_providers/synthetic.py +74 -0
- package/tools/session_snapshot.py +736 -0
- package/tools/ssh_manager.py +912 -0
- package/tools/theme_engine.py +294 -0
- package/tools/theme_selector.py +137 -0
- package/tools/web_search.py +622 -0
- package/yaml.py +321 -0
- package/.claude-plugin/scripts/install.sh +0 -9
- package/bun.lock +0 -23
- package/bunfig.toml +0 -3
- package/hooks/_budget.ts +0 -1
- package/hooks/_common.ts +0 -63
- package/hooks/circuit-breaker.ts +0 -101
- package/hooks/config-guard.ts +0 -4
- package/hooks/firewall.ts +0 -20
- package/hooks/policy_engine.ts +0 -156
- package/hooks/post-tool-failure.ts +0 -22
- package/hooks/post-write.ts +0 -4
- package/hooks/pre-tool-inject.ts +0 -4
- package/hooks/prompt-enhancer.ts +0 -46
- package/hooks/quality-runner.ts +0 -24
- package/hooks/secret-guard.ts +0 -4
- package/hooks/session-end-capture.ts +0 -19
- package/hooks/session-start.ts +0 -19
- package/hooks/shadow_manager.ts +0 -81
- package/hooks/stop-gate.ts +0 -22
- package/hooks/stop_dispatcher.ts +0 -147
- package/hooks/test-generator-hook.ts +0 -4
- package/hooks/tool-ledger.ts +0 -27
- package/hooks/trust_review.ts +0 -175
- package/lab/pipeline.ts +0 -75
- package/lab/policies.ts +0 -68
- package/runtime/common.ts +0 -111
- package/runtime/compat.ts +0 -174
- package/runtime/dispatcher.ts +0 -25
- package/runtime/ecosystem.ts +0 -186
- package/runtime/provider_bootstrap.ts +0 -99
- package/runtime/provider_smoke.ts +0 -34
- package/runtime/release_readiness.ts +0 -186
- package/runtime/team_router.ts +0 -144
- package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
- package/scripts/check-omg-standalone-clean.ts +0 -12
- package/scripts/check-runtime-clean.ts +0 -94
- package/scripts/omg.ts +0 -352
- package/scripts/settings-merge.ts +0 -93
- package/tools/commit_splitter.ts +0 -23
- package/tools/git_inspector.ts +0 -18
- package/tools/session_snapshot.ts +0 -47
- package/trac3er-oh-my-god-2.0.0.tgz +0 -0
- package/tsconfig.json +0 -15
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""CodaMosa-inspired iterative test generator engine.
|
|
2
|
+
|
|
3
|
+
Implements an iterative coverage-driven loop inspired by CodaMosa (ICSE 2023):
|
|
4
|
+
1. Run existing tests + collect coverage
|
|
5
|
+
2. Parse coverage report to find uncovered functions/lines
|
|
6
|
+
3. Generate targeted tests for uncovered code using skeleton_generator
|
|
7
|
+
4. Write new tests to test file
|
|
8
|
+
5. Run tests again, verify coverage improved
|
|
9
|
+
6. Stop if target_coverage met OR max_iterations reached
|
|
10
|
+
|
|
11
|
+
Fallback: when coverage tool is unavailable, calls generate_test_skeleton()
|
|
12
|
+
once and returns ``{"fallback_used": True, "iterations": 1}``.
|
|
13
|
+
|
|
14
|
+
Feature flag: TEST_GENERATION (default False).
|
|
15
|
+
Stdlib only: subprocess, json, pathlib, re.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import subprocess
|
|
23
|
+
from dataclasses import asdict
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
# Lazy imports for crash isolation — these modules live in the same package
|
|
27
|
+
# and in hooks/. Import errors are caught at call sites.
|
|
28
|
+
_SUBPROCESS_TIMEOUT = 60 # seconds
|
|
29
|
+
_MAX_ITERATIONS_CAP = 5
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _import_get_feature_flag():
|
|
33
|
+
"""Lazy import get_feature_flag from hooks/_common.py."""
|
|
34
|
+
try:
|
|
35
|
+
import sys
|
|
36
|
+
import os
|
|
37
|
+
|
|
38
|
+
hooks_dir = str(Path(__file__).resolve().parent.parent.parent / "hooks")
|
|
39
|
+
if hooks_dir not in sys.path:
|
|
40
|
+
sys.path.insert(0, hooks_dir)
|
|
41
|
+
from _common import get_feature_flag # type: ignore[import-untyped]
|
|
42
|
+
|
|
43
|
+
return get_feature_flag
|
|
44
|
+
except Exception:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_feature_flag(flag_name: str, default: bool = False) -> bool:
|
|
49
|
+
"""Get feature flag, with fallback to *default* on import failure."""
|
|
50
|
+
fn = _import_get_feature_flag()
|
|
51
|
+
if fn is not None:
|
|
52
|
+
return fn(flag_name, default)
|
|
53
|
+
return default
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _empty_result() -> dict:
|
|
57
|
+
"""Return a no-op result dict."""
|
|
58
|
+
return {
|
|
59
|
+
"iterations": 0,
|
|
60
|
+
"initial_coverage": 0.0,
|
|
61
|
+
"final_coverage": 0.0,
|
|
62
|
+
"tests_generated": 0,
|
|
63
|
+
"fallback_used": False,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Coverage subprocess helpers
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _run_coverage_subprocess(
|
|
73
|
+
project_dir: str,
|
|
74
|
+
source_file: str,
|
|
75
|
+
framework: str,
|
|
76
|
+
) -> dict:
|
|
77
|
+
"""Run coverage tool and return parsed result.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
{"file_coverage": float, "uncovered_lines": list[int]}
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
FileNotFoundError, OSError, subprocess.TimeoutExpired on failure.
|
|
84
|
+
"""
|
|
85
|
+
source_path = Path(source_file)
|
|
86
|
+
rel_source = str(source_path.relative_to(project_dir)) if source_path.is_absolute() else str(source_path)
|
|
87
|
+
|
|
88
|
+
if framework in ("pytest", "unknown"):
|
|
89
|
+
return _run_pytest_coverage(project_dir, rel_source)
|
|
90
|
+
if framework in ("jest", "vitest"):
|
|
91
|
+
return _run_jest_coverage(project_dir, rel_source)
|
|
92
|
+
if framework in ("go test", "go"):
|
|
93
|
+
return _run_go_coverage(project_dir, rel_source)
|
|
94
|
+
|
|
95
|
+
# Unsupported framework — raise so caller triggers fallback
|
|
96
|
+
raise FileNotFoundError(f"No coverage runner for framework: {framework}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _run_pytest_coverage(project_dir: str, rel_source: str) -> dict:
|
|
100
|
+
"""Run pytest --cov and parse coverage json report."""
|
|
101
|
+
cov_json_path = Path(project_dir) / ".coverage_codamosa.json"
|
|
102
|
+
|
|
103
|
+
argv = [
|
|
104
|
+
"python", "-m", "pytest",
|
|
105
|
+
f"--cov={Path(rel_source).stem}",
|
|
106
|
+
"--cov-report", f"json:{cov_json_path}",
|
|
107
|
+
"--quiet", "--no-header",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
result = subprocess.run(
|
|
111
|
+
argv,
|
|
112
|
+
capture_output=True,
|
|
113
|
+
text=True,
|
|
114
|
+
timeout=_SUBPROCESS_TIMEOUT,
|
|
115
|
+
cwd=project_dir,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return _parse_coverage_json(str(cov_json_path), rel_source)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _parse_coverage_json(json_path: str, rel_source: str) -> dict:
|
|
122
|
+
"""Parse coverage.json produced by pytest-cov."""
|
|
123
|
+
path = Path(json_path)
|
|
124
|
+
if not path.is_file():
|
|
125
|
+
raise FileNotFoundError(f"Coverage report not found: {json_path}")
|
|
126
|
+
|
|
127
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
128
|
+
|
|
129
|
+
# coverage json format: {"files": {"path": {"summary": {"percent_covered": X}, "missing_lines": [...]}}}
|
|
130
|
+
files = data.get("files", {})
|
|
131
|
+
|
|
132
|
+
# Try exact match first, then basename match
|
|
133
|
+
file_data = files.get(rel_source)
|
|
134
|
+
if file_data is None:
|
|
135
|
+
for key, val in files.items():
|
|
136
|
+
if Path(key).name == Path(rel_source).name:
|
|
137
|
+
file_data = val
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
if file_data is None:
|
|
141
|
+
# File not in report — 0 coverage
|
|
142
|
+
return {"file_coverage": 0.0, "uncovered_lines": []}
|
|
143
|
+
|
|
144
|
+
summary = file_data.get("summary", {})
|
|
145
|
+
pct = summary.get("percent_covered", 0.0)
|
|
146
|
+
missing = file_data.get("missing_lines", [])
|
|
147
|
+
|
|
148
|
+
return {"file_coverage": float(pct), "uncovered_lines": list(missing)}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _run_jest_coverage(project_dir: str, rel_source: str) -> dict:
|
|
152
|
+
"""Run jest/vitest --coverage and parse coverage-summary.json."""
|
|
153
|
+
argv = ["npx", "--no-install", "jest", "--coverage", "--coverageReporters=json-summary", "--silent"]
|
|
154
|
+
|
|
155
|
+
subprocess.run(
|
|
156
|
+
argv,
|
|
157
|
+
capture_output=True,
|
|
158
|
+
text=True,
|
|
159
|
+
timeout=_SUBPROCESS_TIMEOUT,
|
|
160
|
+
cwd=project_dir,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
summary_path = Path(project_dir) / "coverage" / "coverage-summary.json"
|
|
164
|
+
if not summary_path.is_file():
|
|
165
|
+
raise FileNotFoundError("Jest coverage summary not found")
|
|
166
|
+
|
|
167
|
+
data = json.loads(summary_path.read_text(encoding="utf-8"))
|
|
168
|
+
total = data.get("total", {}).get("lines", {})
|
|
169
|
+
pct = total.get("pct", 0.0)
|
|
170
|
+
|
|
171
|
+
return {"file_coverage": float(pct), "uncovered_lines": []}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _run_go_coverage(project_dir: str, rel_source: str) -> dict:
|
|
175
|
+
"""Run go test -coverprofile and parse cover.out."""
|
|
176
|
+
cover_path = Path(project_dir) / "cover.out"
|
|
177
|
+
argv = ["go", "test", "-coverprofile", str(cover_path), "./..."]
|
|
178
|
+
|
|
179
|
+
subprocess.run(
|
|
180
|
+
argv,
|
|
181
|
+
capture_output=True,
|
|
182
|
+
text=True,
|
|
183
|
+
timeout=_SUBPROCESS_TIMEOUT,
|
|
184
|
+
cwd=project_dir,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if not cover_path.is_file():
|
|
188
|
+
raise FileNotFoundError("Go coverage profile not found")
|
|
189
|
+
|
|
190
|
+
return _parse_go_cover(str(cover_path))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _parse_go_cover(cover_path: str) -> dict:
|
|
194
|
+
"""Parse Go cover.out format: ``file:startLine.startCol,endLine.endCol N count``."""
|
|
195
|
+
content = Path(cover_path).read_text(encoding="utf-8")
|
|
196
|
+
total_stmts = 0
|
|
197
|
+
covered_stmts = 0
|
|
198
|
+
uncovered_lines: list[int] = []
|
|
199
|
+
|
|
200
|
+
for line in content.splitlines():
|
|
201
|
+
if line.startswith("mode:"):
|
|
202
|
+
continue
|
|
203
|
+
match = re.match(r"(.+):(\d+)\.\d+,(\d+)\.\d+\s+(\d+)\s+(\d+)", line)
|
|
204
|
+
if not match:
|
|
205
|
+
continue
|
|
206
|
+
start_line = int(match.group(2))
|
|
207
|
+
stmts = int(match.group(4))
|
|
208
|
+
count = int(match.group(5))
|
|
209
|
+
total_stmts += stmts
|
|
210
|
+
if count > 0:
|
|
211
|
+
covered_stmts += stmts
|
|
212
|
+
else:
|
|
213
|
+
uncovered_lines.append(start_line)
|
|
214
|
+
|
|
215
|
+
pct = (covered_stmts / total_stmts * 100) if total_stmts > 0 else 0.0
|
|
216
|
+
return {"file_coverage": pct, "uncovered_lines": uncovered_lines}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _run_tests_subprocess(project_dir: str, framework: str) -> bool:
|
|
220
|
+
"""Run the project's test suite. Returns True if tests pass."""
|
|
221
|
+
if framework in ("pytest", "unknown"):
|
|
222
|
+
argv = ["python", "-m", "pytest", "--quiet", "--no-header"]
|
|
223
|
+
elif framework in ("jest", "vitest"):
|
|
224
|
+
argv = ["npx", "--no-install", framework, "--silent"]
|
|
225
|
+
elif framework in ("go test", "go"):
|
|
226
|
+
argv = ["go", "test", "./..."]
|
|
227
|
+
else:
|
|
228
|
+
return True # Can't run → assume ok
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
result = subprocess.run(
|
|
232
|
+
argv,
|
|
233
|
+
capture_output=True,
|
|
234
|
+
text=True,
|
|
235
|
+
timeout=_SUBPROCESS_TIMEOUT,
|
|
236
|
+
cwd=project_dir,
|
|
237
|
+
)
|
|
238
|
+
return result.returncode == 0
|
|
239
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
# Skeleton generation integration
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _generate_targeted_tests(
|
|
249
|
+
source_file: str,
|
|
250
|
+
uncovered_lines: list[int],
|
|
251
|
+
framework_info: dict,
|
|
252
|
+
iteration: int,
|
|
253
|
+
) -> str:
|
|
254
|
+
"""Generate test skeleton targeting uncovered code.
|
|
255
|
+
|
|
256
|
+
Uses skeleton_generator.generate_test_skeleton as the base, then
|
|
257
|
+
annotates with iteration metadata.
|
|
258
|
+
"""
|
|
259
|
+
try:
|
|
260
|
+
from plugins.testgen.skeleton_generator import generate_test_skeleton
|
|
261
|
+
except ImportError:
|
|
262
|
+
return ""
|
|
263
|
+
|
|
264
|
+
skeleton = generate_test_skeleton(source_file, framework_info)
|
|
265
|
+
if not skeleton:
|
|
266
|
+
return ""
|
|
267
|
+
|
|
268
|
+
# Tag with iteration for traceability
|
|
269
|
+
header = f"# CodaMosa iteration {iteration} — targeting uncovered lines: {uncovered_lines[:10]}\n"
|
|
270
|
+
return header + skeleton
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
# Main entry point
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def run_codamosa(
|
|
279
|
+
project_dir: str,
|
|
280
|
+
source_file: str,
|
|
281
|
+
target_coverage: int = 80,
|
|
282
|
+
max_iterations: int = 5,
|
|
283
|
+
) -> dict:
|
|
284
|
+
"""Run CodaMosa-inspired iterative test generation loop.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
project_dir: Absolute or relative path to the project root.
|
|
288
|
+
source_file: Path to the source file to generate tests for.
|
|
289
|
+
target_coverage: Target line coverage percentage (0-100).
|
|
290
|
+
max_iterations: Max iteration count (hard-capped at 5).
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Dict with keys: iterations, initial_coverage, final_coverage,
|
|
294
|
+
tests_generated, fallback_used.
|
|
295
|
+
"""
|
|
296
|
+
# Feature flag gate
|
|
297
|
+
if not get_feature_flag("TEST_GENERATION", default=False):
|
|
298
|
+
return _empty_result()
|
|
299
|
+
|
|
300
|
+
# Hard cap
|
|
301
|
+
max_iterations = min(max_iterations, _MAX_ITERATIONS_CAP)
|
|
302
|
+
|
|
303
|
+
# Validate source file
|
|
304
|
+
source_path = Path(source_file)
|
|
305
|
+
if not source_path.exists() or not source_path.read_text(encoding="utf-8").strip():
|
|
306
|
+
return {
|
|
307
|
+
"iterations": 0,
|
|
308
|
+
"initial_coverage": 0.0,
|
|
309
|
+
"final_coverage": 0.0,
|
|
310
|
+
"tests_generated": 0,
|
|
311
|
+
"fallback_used": False,
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# Detect framework
|
|
315
|
+
framework = "unknown"
|
|
316
|
+
framework_dict: dict = {"framework": "unknown"}
|
|
317
|
+
detected_test_dir = "tests"
|
|
318
|
+
try:
|
|
319
|
+
from plugins.testgen.framework_detector import detect_test_framework # noqa: F811
|
|
320
|
+
|
|
321
|
+
fw_info = detect_test_framework(project_dir)
|
|
322
|
+
framework = fw_info.framework
|
|
323
|
+
detected_test_dir = fw_info.test_dir or "tests"
|
|
324
|
+
framework_dict = {
|
|
325
|
+
"framework": fw_info.framework,
|
|
326
|
+
"config_file": fw_info.config_file,
|
|
327
|
+
"test_dir": fw_info.test_dir,
|
|
328
|
+
"assertion_style": fw_info.assertion_style,
|
|
329
|
+
"mock_library": fw_info.mock_library,
|
|
330
|
+
}
|
|
331
|
+
except ImportError:
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
# Determine test output path
|
|
335
|
+
test_dir = Path(project_dir) / detected_test_dir
|
|
336
|
+
test_dir.mkdir(parents=True, exist_ok=True)
|
|
337
|
+
test_file = test_dir / f"test_{source_path.stem}_codamosa.py"
|
|
338
|
+
|
|
339
|
+
initial_coverage = 0.0
|
|
340
|
+
current_coverage = 0.0
|
|
341
|
+
tests_generated = 0
|
|
342
|
+
completed_iterations = 0
|
|
343
|
+
|
|
344
|
+
for iteration in range(1, max_iterations + 1):
|
|
345
|
+
completed_iterations = iteration
|
|
346
|
+
|
|
347
|
+
# Step 1: Run coverage
|
|
348
|
+
try:
|
|
349
|
+
cov_result = _run_coverage_subprocess(project_dir, source_file, framework)
|
|
350
|
+
except (FileNotFoundError, OSError, subprocess.TimeoutExpired, Exception):
|
|
351
|
+
# Fallback: generate skeleton once and return
|
|
352
|
+
return _do_fallback(source_file, framework_dict)
|
|
353
|
+
|
|
354
|
+
file_cov = cov_result.get("file_coverage", 0.0)
|
|
355
|
+
uncovered = cov_result.get("uncovered_lines", [])
|
|
356
|
+
|
|
357
|
+
if iteration == 1:
|
|
358
|
+
initial_coverage = file_cov
|
|
359
|
+
current_coverage = file_cov
|
|
360
|
+
|
|
361
|
+
# Step 2: Check if target met
|
|
362
|
+
if current_coverage >= target_coverage:
|
|
363
|
+
break
|
|
364
|
+
|
|
365
|
+
# Step 3: Generate targeted tests
|
|
366
|
+
new_tests = _generate_targeted_tests(source_file, uncovered, framework_dict, iteration)
|
|
367
|
+
if new_tests:
|
|
368
|
+
# Step 4: Append to test file
|
|
369
|
+
mode = "a" if test_file.exists() else "w"
|
|
370
|
+
with open(test_file, mode, encoding="utf-8") as f:
|
|
371
|
+
f.write("\n\n" + new_tests if mode == "a" else new_tests)
|
|
372
|
+
tests_generated += 1
|
|
373
|
+
|
|
374
|
+
# Step 5: Run tests to verify they pass
|
|
375
|
+
_run_tests_subprocess(project_dir, framework)
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
"iterations": completed_iterations,
|
|
379
|
+
"initial_coverage": initial_coverage,
|
|
380
|
+
"final_coverage": current_coverage,
|
|
381
|
+
"tests_generated": tests_generated,
|
|
382
|
+
"fallback_used": False,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _do_fallback(source_file: str, framework_dict: dict) -> dict:
|
|
387
|
+
"""Fallback to single-pass skeleton generation."""
|
|
388
|
+
try:
|
|
389
|
+
from plugins.testgen.skeleton_generator import generate_test_skeleton
|
|
390
|
+
|
|
391
|
+
skeleton = generate_test_skeleton(source_file, framework_dict)
|
|
392
|
+
generated = 1 if skeleton else 0
|
|
393
|
+
except ImportError:
|
|
394
|
+
generated = 0
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
"iterations": 1,
|
|
398
|
+
"initial_coverage": 0.0,
|
|
399
|
+
"final_coverage": 0.0,
|
|
400
|
+
"tests_generated": generated,
|
|
401
|
+
"fallback_used": True,
|
|
402
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_SIGNATURE_RE = re.compile(r"^\s*def\s+(?P<name>\w+)\s*\((?P<params>.*)\)\s*(?:->\s*[^:]+)?\s*:?\s*$")
|
|
8
|
+
_PARAM_RE = re.compile(r"^\s*(?P<name>\w+)(?:\s*:\s*(?P<type>[^=]+?))?(?:\s*=\s*(?P<default>.+))?\s*$")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _parse_signature(function_signature: str) -> tuple[str, list[dict[str, str]]]:
|
|
12
|
+
match = _SIGNATURE_RE.match(function_signature)
|
|
13
|
+
if not match:
|
|
14
|
+
return "target", []
|
|
15
|
+
|
|
16
|
+
func_name = match.group("name")
|
|
17
|
+
raw_params = match.group("params").strip()
|
|
18
|
+
if not raw_params:
|
|
19
|
+
return func_name, []
|
|
20
|
+
|
|
21
|
+
params: list[dict[str, str]] = []
|
|
22
|
+
for chunk in raw_params.split(","):
|
|
23
|
+
part = chunk.strip()
|
|
24
|
+
if not part:
|
|
25
|
+
continue
|
|
26
|
+
p_match = _PARAM_RE.match(part)
|
|
27
|
+
if not p_match:
|
|
28
|
+
continue
|
|
29
|
+
p_name = p_match.group("name")
|
|
30
|
+
p_type = (p_match.group("type") or "any").strip().lower()
|
|
31
|
+
params.append({"name": p_name, "type": p_type})
|
|
32
|
+
return func_name, params
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_collection(type_hint: str) -> bool:
|
|
36
|
+
return any(key in type_hint for key in ("list", "dict", "str", "tuple", "set"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _base_py_value(type_hint: str) -> str:
|
|
40
|
+
if "int" in type_hint:
|
|
41
|
+
return "1"
|
|
42
|
+
if "float" in type_hint:
|
|
43
|
+
return "1.0"
|
|
44
|
+
if "bool" in type_hint:
|
|
45
|
+
return "True"
|
|
46
|
+
if "list" in type_hint:
|
|
47
|
+
return "[1]"
|
|
48
|
+
if "dict" in type_hint:
|
|
49
|
+
return "{'k': 1}"
|
|
50
|
+
if "str" in type_hint:
|
|
51
|
+
return "'ok'"
|
|
52
|
+
return "None"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _base_js_value(type_hint: str) -> str:
|
|
56
|
+
if "int" in type_hint or "float" in type_hint:
|
|
57
|
+
return "1"
|
|
58
|
+
if "bool" in type_hint:
|
|
59
|
+
return "true"
|
|
60
|
+
if "list" in type_hint:
|
|
61
|
+
return "[1]"
|
|
62
|
+
if "dict" in type_hint:
|
|
63
|
+
return "{k: 1}"
|
|
64
|
+
if "str" in type_hint:
|
|
65
|
+
return "'ok'"
|
|
66
|
+
return "null"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _build_py_test(func_name: str, case_name: str, kwargs: dict[str, str]) -> str:
|
|
70
|
+
args = ", ".join(f"{key}={value}" for key, value in kwargs.items())
|
|
71
|
+
return (
|
|
72
|
+
f"def test_{func_name}_{case_name}():\n"
|
|
73
|
+
f" with pytest.raises(Exception):\n"
|
|
74
|
+
f" {func_name}({args})\n"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _build_jest_test(func_name: str, case_name: str, args: list[str]) -> str:
|
|
79
|
+
call_args = ", ".join(args)
|
|
80
|
+
return (
|
|
81
|
+
f"it('should handle {case_name}', () => {{\n"
|
|
82
|
+
f" expect(() => {func_name}({call_args})).toThrow();\n"
|
|
83
|
+
f"}});\n"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def synthesize_edge_cases(function_signature: str, framework: str) -> list[str]:
|
|
88
|
+
func_name, params = _parse_signature(function_signature)
|
|
89
|
+
target = framework.lower().strip()
|
|
90
|
+
|
|
91
|
+
if target in {"jest", "vitest"}:
|
|
92
|
+
as_pytest = False
|
|
93
|
+
else:
|
|
94
|
+
as_pytest = True
|
|
95
|
+
|
|
96
|
+
generated: list[str] = []
|
|
97
|
+
|
|
98
|
+
for idx, param in enumerate(params):
|
|
99
|
+
name = param["name"]
|
|
100
|
+
p_type = param["type"]
|
|
101
|
+
|
|
102
|
+
if as_pytest:
|
|
103
|
+
kwargs = {p["name"]: _base_py_value(p["type"]) for p in params}
|
|
104
|
+
kwargs[name] = "None"
|
|
105
|
+
generated.append(_build_py_test(func_name, f"null_{name}_{idx}", kwargs))
|
|
106
|
+
else:
|
|
107
|
+
args = [_base_js_value(p["type"]) for p in params]
|
|
108
|
+
args[idx] = "null"
|
|
109
|
+
generated.append(_build_jest_test(func_name, f"null_{name}_{idx}", args))
|
|
110
|
+
|
|
111
|
+
if _is_collection(p_type):
|
|
112
|
+
if as_pytest:
|
|
113
|
+
kwargs = {p["name"]: _base_py_value(p["type"]) for p in params}
|
|
114
|
+
if "list" in p_type:
|
|
115
|
+
kwargs[name] = "[]"
|
|
116
|
+
elif "dict" in p_type:
|
|
117
|
+
kwargs[name] = "{}"
|
|
118
|
+
elif "str" in p_type:
|
|
119
|
+
kwargs[name] = "''"
|
|
120
|
+
else:
|
|
121
|
+
kwargs[name] = "[]"
|
|
122
|
+
generated.append(_build_py_test(func_name, f"empty_{name}_{idx}", kwargs))
|
|
123
|
+
else:
|
|
124
|
+
args = [_base_js_value(p["type"]) for p in params]
|
|
125
|
+
if "list" in p_type:
|
|
126
|
+
args[idx] = "[]"
|
|
127
|
+
elif "dict" in p_type:
|
|
128
|
+
args[idx] = "{}"
|
|
129
|
+
elif "str" in p_type:
|
|
130
|
+
args[idx] = "''"
|
|
131
|
+
else:
|
|
132
|
+
args[idx] = "[]"
|
|
133
|
+
generated.append(_build_jest_test(func_name, f"empty_{name}_{idx}", args))
|
|
134
|
+
|
|
135
|
+
if "int" in p_type:
|
|
136
|
+
if as_pytest:
|
|
137
|
+
for boundary_name, boundary_value in (
|
|
138
|
+
("zero", "0"),
|
|
139
|
+
("negative", "-1"),
|
|
140
|
+
("maxsize", str(sys.maxsize)),
|
|
141
|
+
):
|
|
142
|
+
kwargs = {p["name"]: _base_py_value(p["type"]) for p in params}
|
|
143
|
+
kwargs[name] = boundary_value
|
|
144
|
+
generated.append(_build_py_test(func_name, f"{boundary_name}_{name}_{idx}", kwargs))
|
|
145
|
+
else:
|
|
146
|
+
for boundary_name, boundary_value in (
|
|
147
|
+
("zero", "0"),
|
|
148
|
+
("negative", "-1"),
|
|
149
|
+
("maxsize", "Number.MAX_SAFE_INTEGER"),
|
|
150
|
+
):
|
|
151
|
+
args = [_base_js_value(p["type"]) for p in params]
|
|
152
|
+
args[idx] = boundary_value
|
|
153
|
+
generated.append(_build_jest_test(func_name, f"{boundary_name}_{name}_{idx}", args))
|
|
154
|
+
|
|
155
|
+
if as_pytest:
|
|
156
|
+
kwargs = {p["name"]: _base_py_value(p["type"]) for p in params}
|
|
157
|
+
kwargs[name] = "'wrong_type'"
|
|
158
|
+
generated.append(_build_py_test(func_name, f"type_mismatch_{name}_{idx}", kwargs))
|
|
159
|
+
else:
|
|
160
|
+
args = [_base_js_value(p["type"]) for p in params]
|
|
161
|
+
args[idx] = "'wrong_type'"
|
|
162
|
+
generated.append(_build_jest_test(func_name, f"type_mismatch_{name}_{idx}", args))
|
|
163
|
+
|
|
164
|
+
if _is_collection(p_type):
|
|
165
|
+
if as_pytest:
|
|
166
|
+
kwargs = {p["name"]: _base_py_value(p["type"]) for p in params}
|
|
167
|
+
if "str" in p_type:
|
|
168
|
+
kwargs[name] = "'x' * 10000"
|
|
169
|
+
elif "dict" in p_type:
|
|
170
|
+
kwargs[name] = "{str(i): i for i in range(10000)}"
|
|
171
|
+
else:
|
|
172
|
+
kwargs[name] = "[0] * 10000"
|
|
173
|
+
generated.append(_build_py_test(func_name, f"large_{name}_{idx}", kwargs))
|
|
174
|
+
else:
|
|
175
|
+
args = [_base_js_value(p["type"]) for p in params]
|
|
176
|
+
if "str" in p_type:
|
|
177
|
+
args[idx] = "'x'.repeat(10000)"
|
|
178
|
+
elif "dict" in p_type:
|
|
179
|
+
args[idx] = "Object.fromEntries(Array.from({ length: 10000 }, (_, i) => [String(i), i]))"
|
|
180
|
+
else:
|
|
181
|
+
args[idx] = "new Array(10000).fill(0)"
|
|
182
|
+
generated.append(_build_jest_test(func_name, f"large_{name}_{idx}", args))
|
|
183
|
+
|
|
184
|
+
return generated
|