@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.
Files changed (243) hide show
  1. package/.claude-plugin/marketplace.json +8 -8
  2. package/.claude-plugin/plugin.json +5 -4
  3. package/.claude-plugin/scripts/uninstall.sh +74 -3
  4. package/.claude-plugin/scripts/update.sh +78 -3
  5. package/.coveragerc +26 -0
  6. package/.mcp.json +4 -4
  7. package/CHANGELOG.md +14 -0
  8. package/CODE_OF_CONDUCT.md +27 -0
  9. package/CONTRIBUTING.md +62 -0
  10. package/OMG-setup.sh +1201 -355
  11. package/README.md +77 -56
  12. package/SECURITY.md +25 -0
  13. package/agents/__init__.py +1 -0
  14. package/agents/model_roles.py +196 -0
  15. package/agents/omg-architect-mode.md +3 -5
  16. package/agents/omg-backend-engineer.md +3 -5
  17. package/agents/omg-database-engineer.md +3 -5
  18. package/agents/omg-frontend-designer.md +4 -5
  19. package/agents/omg-implement-mode.md +4 -5
  20. package/agents/omg-infra-engineer.md +3 -5
  21. package/agents/omg-research-mode.md +4 -6
  22. package/agents/omg-security-auditor.md +3 -5
  23. package/agents/omg-testing-engineer.md +3 -5
  24. package/build/lib/yaml.py +321 -0
  25. package/commands/OMG:ai-commit.md +101 -14
  26. package/commands/OMG:arch.md +302 -19
  27. package/commands/OMG:ccg.md +12 -7
  28. package/commands/OMG:compat.md +25 -17
  29. package/commands/OMG:cost.md +173 -13
  30. package/commands/OMG:crazy.md +1 -1
  31. package/commands/OMG:create-agent.md +170 -20
  32. package/commands/OMG:deps.md +235 -17
  33. package/commands/OMG:domain-init.md +1 -1
  34. package/commands/OMG:escalate.md +41 -12
  35. package/commands/OMG:health-check.md +37 -13
  36. package/commands/OMG:init.md +122 -14
  37. package/commands/OMG:project-init.md +1 -1
  38. package/commands/OMG:session-branch.md +76 -9
  39. package/commands/OMG:session-fork.md +42 -5
  40. package/commands/OMG:session-merge.md +124 -8
  41. package/commands/OMG:setup.md +69 -12
  42. package/commands/OMG:stats.md +215 -14
  43. package/commands/OMG:teams.md +19 -10
  44. package/config/lsp_languages.yaml +8 -0
  45. package/hooks/__init__.py +0 -0
  46. package/hooks/_agent_registry.py +423 -0
  47. package/hooks/_analytics.py +291 -0
  48. package/hooks/_budget.py +31 -0
  49. package/hooks/_common.py +569 -0
  50. package/hooks/_compression_optimizer.py +119 -0
  51. package/hooks/_cost_ledger.py +176 -0
  52. package/hooks/_learnings.py +126 -0
  53. package/hooks/_memory.py +103 -0
  54. package/hooks/_protected_context.py +150 -0
  55. package/hooks/_token_counter.py +221 -0
  56. package/hooks/branch_manager.py +236 -0
  57. package/hooks/budget_governor.py +232 -0
  58. package/hooks/circuit-breaker.py +270 -0
  59. package/hooks/compression_feedback.py +254 -0
  60. package/hooks/config-guard.py +216 -0
  61. package/hooks/context_pressure.py +53 -0
  62. package/hooks/credential_store.py +1020 -0
  63. package/hooks/fetch-rate-limits.py +212 -0
  64. package/hooks/firewall.py +48 -0
  65. package/hooks/hashline-formatter-bridge.py +224 -0
  66. package/hooks/hashline-injector.py +273 -0
  67. package/hooks/hashline-validator.py +216 -0
  68. package/hooks/idle-detector.py +95 -0
  69. package/hooks/intentgate-keyword-detector.py +188 -0
  70. package/hooks/magic-keyword-router.py +195 -0
  71. package/hooks/policy_engine.py +505 -0
  72. package/hooks/post-tool-failure.py +19 -0
  73. package/hooks/post-write.py +219 -0
  74. package/hooks/post_write.py +46 -0
  75. package/hooks/pre-compact.py +398 -0
  76. package/hooks/pre-tool-inject.py +98 -0
  77. package/hooks/prompt-enhancer.py +672 -0
  78. package/hooks/quality-runner.py +191 -0
  79. package/hooks/query.py +512 -0
  80. package/hooks/secret-guard.py +61 -0
  81. package/hooks/secret_audit.py +144 -0
  82. package/hooks/session-end-capture.py +137 -0
  83. package/hooks/session-start.py +277 -0
  84. package/hooks/setup_wizard.py +582 -0
  85. package/hooks/shadow_manager.py +297 -0
  86. package/hooks/state_migration.py +225 -0
  87. package/hooks/stop-gate.py +7 -0
  88. package/hooks/stop_dispatcher.py +945 -0
  89. package/hooks/test-validator.py +361 -0
  90. package/hooks/test_generator_hook.py +123 -0
  91. package/hooks/todo-state-tracker.py +114 -0
  92. package/hooks/tool-ledger.py +149 -0
  93. package/hooks/trust_review.py +585 -0
  94. package/hud/omg-hud.mjs +31 -1
  95. package/lab/__init__.py +1 -0
  96. package/lab/pipeline.py +75 -0
  97. package/lab/policies.py +52 -0
  98. package/package.json +7 -18
  99. package/plugins/README.md +33 -61
  100. package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
  101. package/plugins/advanced/commands/OMG:learn.md +1 -1
  102. package/plugins/advanced/commands/OMG:security-review.md +3 -3
  103. package/plugins/advanced/commands/OMG:ship.md +1 -1
  104. package/plugins/advanced/plugin.json +1 -1
  105. package/plugins/core/plugin.json +8 -3
  106. package/plugins/dephealth/__init__.py +0 -0
  107. package/plugins/dephealth/cve_scanner.py +188 -0
  108. package/plugins/dephealth/license_checker.py +135 -0
  109. package/plugins/dephealth/manifest_detector.py +423 -0
  110. package/plugins/dephealth/vuln_analyzer.py +169 -0
  111. package/plugins/testgen/__init__.py +0 -0
  112. package/plugins/testgen/codamosa_engine.py +402 -0
  113. package/plugins/testgen/edge_case_synthesizer.py +184 -0
  114. package/plugins/testgen/framework_detector.py +271 -0
  115. package/plugins/testgen/skeleton_generator.py +219 -0
  116. package/plugins/viz/__init__.py +0 -0
  117. package/plugins/viz/ast_parser.py +139 -0
  118. package/plugins/viz/diagram_generator.py +192 -0
  119. package/plugins/viz/graph_builder.py +444 -0
  120. package/plugins/viz/native_parsers.py +259 -0
  121. package/plugins/viz/regex_parser.py +112 -0
  122. package/pyproject.toml +81 -0
  123. package/rules/contextual/write-verify.md +2 -2
  124. package/rules/core/00-truth.md +1 -1
  125. package/rules/core/01-surgical.md +1 -1
  126. package/rules/core/02-circuit-breaker.md +2 -2
  127. package/rules/core/03-ensemble.md +3 -3
  128. package/rules/core/04-testing.md +3 -3
  129. package/runtime/__init__.py +32 -0
  130. package/runtime/adapters/__init__.py +13 -0
  131. package/runtime/adapters/claude.py +60 -0
  132. package/runtime/adapters/gpt.py +53 -0
  133. package/runtime/adapters/local.py +53 -0
  134. package/runtime/adoption.py +212 -0
  135. package/runtime/business_workflow.py +220 -0
  136. package/runtime/cli_provider.py +85 -0
  137. package/runtime/compat.py +1299 -0
  138. package/runtime/custom_agent_loader.py +366 -0
  139. package/runtime/dispatcher.py +47 -0
  140. package/runtime/ecosystem.py +371 -0
  141. package/runtime/legacy_compat.py +7 -0
  142. package/runtime/mcp_config_writers.py +115 -0
  143. package/runtime/mcp_lifecycle.py +153 -0
  144. package/runtime/mcp_memory_server.py +135 -0
  145. package/runtime/memory_parsers/__init__.py +0 -0
  146. package/runtime/memory_parsers/chatgpt_parser.py +257 -0
  147. package/runtime/memory_parsers/claude_import.py +107 -0
  148. package/runtime/memory_parsers/export.py +97 -0
  149. package/runtime/memory_parsers/gemini_import.py +91 -0
  150. package/runtime/memory_parsers/kimi_import.py +91 -0
  151. package/runtime/memory_store.py +215 -0
  152. package/runtime/omc_compat.py +7 -0
  153. package/runtime/providers/__init__.py +0 -0
  154. package/runtime/providers/codex_provider.py +112 -0
  155. package/runtime/providers/gemini_provider.py +128 -0
  156. package/runtime/providers/kimi_provider.py +151 -0
  157. package/runtime/providers/opencode_provider.py +144 -0
  158. package/runtime/subagent_dispatcher.py +362 -0
  159. package/runtime/team_router.py +1167 -0
  160. package/runtime/tmux_session_manager.py +169 -0
  161. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  162. package/scripts/check-omg-contract-snapshot.py +12 -0
  163. package/scripts/check-omg-public-ready.py +193 -0
  164. package/scripts/check-omg-standalone-clean.py +103 -0
  165. package/scripts/legacy_to_omg_migrate.py +29 -0
  166. package/scripts/migrate-legacy.py +464 -0
  167. package/scripts/omc_to_omg_migrate.py +12 -0
  168. package/scripts/omg.py +492 -0
  169. package/scripts/settings-merge.py +283 -0
  170. package/scripts/verify-standalone.sh +8 -4
  171. package/settings.json +126 -29
  172. package/templates/profile.yaml +1 -1
  173. package/tools/__init__.py +2 -0
  174. package/tools/browser_consent.py +289 -0
  175. package/tools/browser_stealth.py +481 -0
  176. package/tools/browser_tool.py +448 -0
  177. package/tools/changelog_generator.py +347 -0
  178. package/tools/commit_splitter.py +746 -0
  179. package/tools/config_discovery.py +151 -0
  180. package/tools/config_merger.py +449 -0
  181. package/tools/dashboard_generator.py +300 -0
  182. package/tools/git_inspector.py +298 -0
  183. package/tools/lsp_client.py +275 -0
  184. package/tools/lsp_discovery.py +231 -0
  185. package/tools/lsp_operations.py +392 -0
  186. package/tools/pr_generator.py +404 -0
  187. package/tools/python_repl.py +656 -0
  188. package/tools/python_sandbox.py +609 -0
  189. package/tools/search_providers/__init__.py +77 -0
  190. package/tools/search_providers/brave.py +115 -0
  191. package/tools/search_providers/exa.py +116 -0
  192. package/tools/search_providers/jina.py +104 -0
  193. package/tools/search_providers/perplexity.py +139 -0
  194. package/tools/search_providers/synthetic.py +74 -0
  195. package/tools/session_snapshot.py +736 -0
  196. package/tools/ssh_manager.py +912 -0
  197. package/tools/theme_engine.py +294 -0
  198. package/tools/theme_selector.py +137 -0
  199. package/tools/web_search.py +622 -0
  200. package/yaml.py +321 -0
  201. package/.claude-plugin/scripts/install.sh +0 -9
  202. package/bun.lock +0 -23
  203. package/bunfig.toml +0 -3
  204. package/hooks/_budget.ts +0 -1
  205. package/hooks/_common.ts +0 -63
  206. package/hooks/circuit-breaker.ts +0 -101
  207. package/hooks/config-guard.ts +0 -4
  208. package/hooks/firewall.ts +0 -20
  209. package/hooks/policy_engine.ts +0 -156
  210. package/hooks/post-tool-failure.ts +0 -22
  211. package/hooks/post-write.ts +0 -4
  212. package/hooks/pre-tool-inject.ts +0 -4
  213. package/hooks/prompt-enhancer.ts +0 -46
  214. package/hooks/quality-runner.ts +0 -24
  215. package/hooks/secret-guard.ts +0 -4
  216. package/hooks/session-end-capture.ts +0 -19
  217. package/hooks/session-start.ts +0 -19
  218. package/hooks/shadow_manager.ts +0 -81
  219. package/hooks/stop-gate.ts +0 -22
  220. package/hooks/stop_dispatcher.ts +0 -147
  221. package/hooks/test-generator-hook.ts +0 -4
  222. package/hooks/tool-ledger.ts +0 -27
  223. package/hooks/trust_review.ts +0 -175
  224. package/lab/pipeline.ts +0 -75
  225. package/lab/policies.ts +0 -68
  226. package/runtime/common.ts +0 -111
  227. package/runtime/compat.ts +0 -174
  228. package/runtime/dispatcher.ts +0 -25
  229. package/runtime/ecosystem.ts +0 -186
  230. package/runtime/provider_bootstrap.ts +0 -99
  231. package/runtime/provider_smoke.ts +0 -34
  232. package/runtime/release_readiness.ts +0 -186
  233. package/runtime/team_router.ts +0 -144
  234. package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
  235. package/scripts/check-omg-standalone-clean.ts +0 -12
  236. package/scripts/check-runtime-clean.ts +0 -94
  237. package/scripts/omg.ts +0 -352
  238. package/scripts/settings-merge.ts +0 -93
  239. package/tools/commit_splitter.ts +0 -23
  240. package/tools/git_inspector.ts +0 -18
  241. package/tools/session_snapshot.ts +0 -47
  242. package/trac3er-oh-my-god-2.0.0.tgz +0 -0
  243. 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