codegraph-engine 2.3.0__py3-none-any.whl → 3.0.1__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 (118) hide show
  1. codegraph/__init__.py +1 -1
  2. codegraph/__main__.py +7 -0
  3. codegraph/agent_brain.py +253 -2
  4. codegraph/agent_capabilities.py +1348 -67
  5. codegraph/agent_rules.py +2 -1
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture_rules.py +123 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/change_impact.py +320 -110
  22. codegraph/changelog.py +170 -0
  23. codegraph/cli.py +1605 -62
  24. codegraph/cli_output.py +17 -2
  25. codegraph/context.py +135 -24
  26. codegraph/context_freshness.py +2 -0
  27. codegraph/database/__init__.py +14 -0
  28. codegraph/database/extractor.py +712 -17
  29. codegraph/database/interrogation.py +388 -2
  30. codegraph/database/schema_drift.py +862 -0
  31. codegraph/dataflow_propagation.py +118 -0
  32. codegraph/dead_code.py +387 -0
  33. codegraph/epistemic.py +15 -0
  34. codegraph/evidence_contract.py +34 -3
  35. codegraph/federation.py +245 -0
  36. codegraph/frameworks.py +234 -12
  37. codegraph/frameworks_nestjs.py +502 -0
  38. codegraph/frameworks_nextjs.py +337 -0
  39. codegraph/frameworks_react.py +457 -0
  40. codegraph/freshness.py +2 -0
  41. codegraph/frontend_html.py +109 -0
  42. codegraph/git.py +2 -0
  43. codegraph/git_state.py +2 -0
  44. codegraph/graph/traversal.py +150 -7
  45. codegraph/graph_csr.py +218 -0
  46. codegraph/incremental.py +150 -0
  47. codegraph/indexing/__init__.py +7 -1
  48. codegraph/indexing/classifier.py +5 -0
  49. codegraph/indexing/indexer.py +292 -38
  50. codegraph/indexing/languages/__init__.py +1 -0
  51. codegraph/indexing/languages/cpp.py +457 -0
  52. codegraph/indexing/languages/go.py +459 -0
  53. codegraph/indexing/languages/jvm.py +583 -0
  54. codegraph/indexing/languages/rust.py +507 -0
  55. codegraph/indexing/models.py +15 -1
  56. codegraph/indexing/parser.py +385 -9
  57. codegraph/indexing/pool.py +136 -0
  58. codegraph/indexing/protocols.py +220 -0
  59. codegraph/indexing/scanner.py +23 -0
  60. codegraph/indexing/templates.py +418 -0
  61. codegraph/interrogation.py +207 -39
  62. codegraph/lsp.py +318 -0
  63. codegraph/lsp_bridge/__init__.py +16 -0
  64. codegraph/lsp_bridge/bridge.py +102 -0
  65. codegraph/lsp_bridge/client.py +207 -0
  66. codegraph/lsp_bridge/models.py +71 -0
  67. codegraph/mcp/server.py +502 -29
  68. codegraph/memory/trajectory.py +282 -0
  69. codegraph/models.py +7 -0
  70. codegraph/monorepo.py +3 -0
  71. codegraph/multi_agent/__init__.py +13 -0
  72. codegraph/multi_agent/coordinator.py +229 -0
  73. codegraph/multi_agent/models.py +76 -0
  74. codegraph/optimizer.py +42 -5
  75. codegraph/polymorphism.py +95 -0
  76. codegraph/pr_review.py +296 -0
  77. codegraph/process_lifecycle.py +50 -12
  78. codegraph/reachability.py +198 -0
  79. codegraph/reachability_index.py +74 -0
  80. codegraph/refactor/__init__.py +48 -0
  81. codegraph/refactor/models.py +114 -0
  82. codegraph/refactor/patcher.py +348 -0
  83. codegraph/refactor/renamer.py +844 -0
  84. codegraph/refactor/transactions.py +414 -0
  85. codegraph/repair.py +233 -0
  86. codegraph/resolver.py +1357 -161
  87. codegraph/retrieval_policy.py +22 -1
  88. codegraph/roblox.py +2174 -0
  89. codegraph/route_composer.py +7 -0
  90. codegraph/route_topology.py +172 -0
  91. codegraph/runtime/__init__.py +25 -0
  92. codegraph/runtime/collector.py +155 -0
  93. codegraph/runtime/db_watcher.py +191 -0
  94. codegraph/runtime/ingestor.py +30 -10
  95. codegraph/runtime/otel_receiver.py +208 -0
  96. codegraph/runtime/port_inspector.py +304 -0
  97. codegraph/runtime/runner.py +155 -6
  98. codegraph/runtime/trace_linker.py +175 -0
  99. codegraph/sandbox.py +215 -0
  100. codegraph/search/hybrid.py +26 -0
  101. codegraph/security/supply_chain.py +188 -0
  102. codegraph/security/taint.py +421 -0
  103. codegraph/semantic_decorators.py +34 -3
  104. codegraph/structural_diff.py +2 -0
  105. codegraph/styles.py +144 -0
  106. codegraph/symbol_history.py +2 -0
  107. codegraph/test_gen.py +437 -0
  108. codegraph/tsconfig.py +236 -0
  109. codegraph/ui/__init__.py +6 -0
  110. codegraph/ui/server.py +876 -0
  111. codegraph/watcher.py +323 -0
  112. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/METADATA +365 -67
  113. codegraph_engine-3.0.1.dist-info/RECORD +164 -0
  114. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/WHEEL +1 -1
  115. codegraph_engine-2.3.0.dist-info/RECORD +0 -94
  116. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/entry_points.txt +0 -0
  117. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/licenses/LICENSE +0 -0
  118. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/top_level.txt +0 -0
codegraph/styles.py ADDED
@@ -0,0 +1,144 @@
1
+ """CSS, SCSS, and CSS Modules Architecture and Selector Analyzer (Pillar 4).
2
+
3
+ Extracts deterministic facts from stylesheets:
4
+ - CSS class selectors (.btn-primary, .invoice-card, .header_title)
5
+ - CSS custom properties/variables (--primary-color, --spacing-md)
6
+ - CSS module boundaries
7
+ - Emits Symbol entities for style search and cross-component impact analysis.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from dataclasses import dataclass
13
+
14
+ from codegraph.indexing.models import Symbol
15
+
16
+ # CSS Class Selector pattern: .className { or .className:hover or .className.active
17
+ # Excludes decimals like .5s, .75em, .0
18
+ _CSS_CLASS_RE = re.compile(
19
+ r"""(?<![\w\.\-])\.([A-Za-z_][A-Za-z0-9_-]*)(?=[ \t\r\n\.:,\{>+~])"""
20
+ )
21
+
22
+ # CSS Custom Property / Variable declaration: --primary-color: #0070f3;
23
+ _CSS_VAR_RE = re.compile(
24
+ r"""(--[A-Za-z_][A-Za-z0-9_-]*)\s*:"""
25
+ )
26
+
27
+ # Hex color detector to ignore #fff, #123456 as ID selectors
28
+ _HEX_COLOR_RE = re.compile(r"^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$")
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class CssClassDef:
33
+ name: str
34
+ file_path: str
35
+ line: int
36
+ is_module: bool
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class CssVariableDef:
41
+ name: str
42
+ file_path: str
43
+ line: int
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class StyleFileResult:
48
+ classes: tuple[CssClassDef, ...] = ()
49
+ variables: tuple[CssVariableDef, ...] = ()
50
+ symbols: tuple[Symbol, ...] = ()
51
+
52
+
53
+ def is_style_file(file_path: str) -> bool:
54
+ """Check if file is a CSS, SCSS, Sass, or Less stylesheet."""
55
+ lower = file_path.lower()
56
+ return lower.endswith((".css", ".scss", ".sass", ".less"))
57
+
58
+
59
+ def parse_style_file(content: str, file_path: str) -> StyleFileResult:
60
+ """Extract CSS classes, custom properties, and symbols from stylesheet."""
61
+ if not is_style_file(file_path):
62
+ return StyleFileResult()
63
+
64
+ is_module = ".module." in file_path.lower()
65
+ classes: list[CssClassDef] = []
66
+ variables: list[CssVariableDef] = []
67
+ symbols: list[Symbol] = []
68
+
69
+ seen_classes: set[str] = set()
70
+ seen_vars: set[str] = set()
71
+
72
+ clean_file = file_path.replace("\\", "/").lstrip("./")
73
+
74
+ for idx, line in enumerate(content.splitlines()):
75
+ lineno = idx + 1
76
+ stripped = line.strip()
77
+
78
+ # Skip comment-only lines
79
+ if stripped.startswith("/*") or stripped.startswith("*") or stripped.startswith("//"):
80
+ continue
81
+
82
+ # Extract CSS variables
83
+ for m_var in _CSS_VAR_RE.finditer(line):
84
+ v_name = m_var.group(1)
85
+ if v_name not in seen_vars:
86
+ seen_vars.add(v_name)
87
+ variables.append(CssVariableDef(name=v_name, file_path=clean_file, line=lineno))
88
+ canon_id = f"{clean_file}::{v_name}"
89
+ symbols.append(
90
+ Symbol(
91
+ id=canon_id,
92
+ canonical_id=canon_id,
93
+ name=v_name,
94
+ qualified_name=v_name,
95
+ kind="style_var",
96
+ language="css",
97
+ module=clean_file,
98
+ path=clean_file,
99
+ file_path=clean_file,
100
+ scope="",
101
+ start_line=lineno,
102
+ end_line=lineno,
103
+ )
104
+ )
105
+
106
+ # Extract CSS classes
107
+ for m_cls in _CSS_CLASS_RE.finditer(line):
108
+ c_name = m_cls.group(1)
109
+ # Avoid pseudo-elements and animations
110
+ if c_name in ("hover", "focus", "active", "visited", "disabled", "first-child", "last-child"):
111
+ continue
112
+ if c_name not in seen_classes:
113
+ seen_classes.add(c_name)
114
+ classes.append(
115
+ CssClassDef(
116
+ name=c_name,
117
+ file_path=clean_file,
118
+ line=lineno,
119
+ is_module=is_module,
120
+ )
121
+ )
122
+ canon_id = f"{clean_file}::{c_name}"
123
+ symbols.append(
124
+ Symbol(
125
+ id=canon_id,
126
+ canonical_id=canon_id,
127
+ name=f".{c_name}",
128
+ qualified_name=f".{c_name}",
129
+ kind="style_class",
130
+ language="css",
131
+ module=clean_file,
132
+ path=clean_file,
133
+ file_path=clean_file,
134
+ scope="",
135
+ start_line=lineno,
136
+ end_line=lineno,
137
+ )
138
+ )
139
+
140
+ return StyleFileResult(
141
+ classes=tuple(classes),
142
+ variables=tuple(variables),
143
+ symbols=tuple(symbols),
144
+ )
@@ -87,6 +87,8 @@ def _run_git(repository: Path, *args: str, timeout: int = 15) -> str | None:
87
87
  cwd=str(repository),
88
88
  capture_output=True,
89
89
  text=True,
90
+ encoding="utf-8",
91
+ errors="replace",
90
92
  timeout=timeout,
91
93
  )
92
94
  return res.stdout if res.returncode == 0 else None
codegraph/test_gen.py ADDED
@@ -0,0 +1,437 @@
1
+ """Automated Test Scaffolding Generator for CodeGraph.
2
+
3
+ Inspects target symbol signatures, parameter types, async flags, downstream callees,
4
+ database read/write entities, and dependency injection providers to generate production-ready,
5
+ syntactically verified test suites (pytest for Python, Jest/Vitest for TS/JS, TestEZ for Luau).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import sqlite3
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+
15
+ @dataclass
16
+ class MockRequirement:
17
+ target: str
18
+ mock_type: str # DB_SESSION, HTTP_CLIENT, SERVICE, REPOSITORY
19
+ description: str
20
+
21
+ def as_dict(self) -> dict[str, Any]:
22
+ return {
23
+ "target": self.target,
24
+ "mock_type": self.mock_type,
25
+ "description": self.description,
26
+ }
27
+
28
+
29
+ @dataclass
30
+ class TestCaseScaffold:
31
+ name: str
32
+ description: str
33
+ test_type: str # HAPPY_PATH, BOUNDARY, ERROR, AUTH_FAILURE
34
+ code: str
35
+
36
+ def as_dict(self) -> dict[str, Any]:
37
+ return {
38
+ "name": self.name,
39
+ "description": self.description,
40
+ "test_type": self.test_type,
41
+ "code": self.code,
42
+ }
43
+
44
+
45
+ @dataclass
46
+ class GeneratedTestScaffold:
47
+ target_symbol: str
48
+ target_file: str
49
+ framework: str # pytest, jest, testez
50
+ suggested_test_file: str
51
+ test_code: str
52
+ mock_requirements: list[MockRequirement] = field(default_factory=list)
53
+ test_cases: list[TestCaseScaffold] = field(default_factory=list)
54
+
55
+ def as_dict(self) -> dict[str, Any]:
56
+ return {
57
+ "target_symbol": self.target_symbol,
58
+ "target_file": self.target_file,
59
+ "framework": self.framework,
60
+ "suggested_test_file": self.suggested_test_file,
61
+ "test_code": self.test_code,
62
+ "mock_requirements": [m.as_dict() for m in self.mock_requirements],
63
+ "test_cases": [t.as_dict() for t in self.test_cases],
64
+ }
65
+
66
+
67
+ def generate_test_scaffolding(
68
+ con: sqlite3.Connection,
69
+ target: str,
70
+ repository: Path | None = None,
71
+ framework: str | None = None,
72
+ ) -> GeneratedTestScaffold:
73
+ """Generate production-ready test scaffolding for a symbol or route."""
74
+ clean_target = target.strip()
75
+
76
+ # 1. Look up symbol by name or canonical_id
77
+ row = con.execute(
78
+ """
79
+ SELECT canonical_id, name, kind, path, start_line, end_line, decorators, language, module
80
+ FROM symbols
81
+ WHERE name = ? OR canonical_id = ? OR name LIKE ?
82
+ ORDER BY CASE WHEN name = ? THEN 0 ELSE 1 END
83
+ LIMIT 1
84
+ """,
85
+ (clean_target, clean_target, f"%{clean_target}%", clean_target),
86
+ ).fetchone()
87
+
88
+ # If not found directly, check framework_routes table
89
+ route_row = None
90
+ if not row:
91
+ route_row = con.execute(
92
+ """
93
+ SELECT http_method, route_path, handler_name, file_path, line
94
+ FROM framework_routes
95
+ WHERE route_path = ? OR route_path LIKE ? OR handler_name = ?
96
+ LIMIT 1
97
+ """,
98
+ (clean_target, f"%{clean_target}%", clean_target),
99
+ ).fetchone()
100
+
101
+ if route_row:
102
+ handler_name = route_row[2]
103
+ row = con.execute(
104
+ """
105
+ SELECT canonical_id, name, kind, path, start_line, end_line, decorators, language, module
106
+ FROM symbols
107
+ WHERE name = ? OR canonical_id LIKE ?
108
+ LIMIT 1
109
+ """,
110
+ (handler_name, f"%{handler_name}%"),
111
+ ).fetchone()
112
+
113
+ if not row and not route_row:
114
+ # Fallback generic scaffold
115
+ return _generate_fallback_scaffold(clean_target, framework or "pytest")
116
+
117
+ sym_name = row[1] if row else (route_row[2] if route_row else clean_target)
118
+ sym_kind = row[2] if row else "function"
119
+ sym_path = row[3] if row else (route_row[3] if route_row else "src/app.py")
120
+ decorators = row[6] if row else ""
121
+ lang = row[7] if row else "python"
122
+ module = row[8] if row else ""
123
+
124
+ # Infer language & framework
125
+ if not framework:
126
+ if lang in ("typescript", "javascript") or sym_path.endswith((".ts", ".tsx", ".js", ".jsx")):
127
+ framework = "jest"
128
+ elif lang in ("luau", "lua") or sym_path.endswith((".lua", ".luau")):
129
+ framework = "testez"
130
+ else:
131
+ framework = "pytest"
132
+
133
+ # 2. Inspect Callees for Mock Requirements
134
+ mock_reqs: list[MockRequirement] = []
135
+ if row:
136
+ callee_rows = con.execute(
137
+ """
138
+ SELECT resolved_symbol_id, callee
139
+ FROM calls
140
+ WHERE source_symbol_id = ?
141
+ LIMIT 20
142
+ """,
143
+ (row[0],),
144
+ ).fetchall()
145
+
146
+ for c_id, t_name in callee_rows:
147
+ target_str = str(t_name or c_id).lower()
148
+ if any(k in target_str for k in ("db", "session", "execute", "commit", "query", "repo")):
149
+ mock_reqs.append(
150
+ MockRequirement(
151
+ target=t_name or c_id or "database",
152
+ mock_type="DB_SESSION",
153
+ description=f"Database access via {t_name or c_id}",
154
+ )
155
+ )
156
+ elif any(k in target_str for k in ("http", "request", "fetch", "post", "get", "api", "client")):
157
+ mock_reqs.append(
158
+ MockRequirement(
159
+ target=t_name or c_id or "httpClient",
160
+ mock_type="HTTP_CLIENT",
161
+ description=f"External HTTP call via {t_name or c_id}",
162
+ )
163
+ )
164
+
165
+ # 3. Check DB entities written
166
+ if row:
167
+ db_rows = con.execute(
168
+ """
169
+ SELECT table_name, operation
170
+ FROM db_queries
171
+ WHERE caller_symbol_id = ?
172
+ LIMIT 10
173
+ """,
174
+ (row[0],),
175
+ ).fetchall()
176
+ for tbl, op in db_rows:
177
+ mock_reqs.append(
178
+ MockRequirement(
179
+ target=tbl,
180
+ mock_type="REPOSITORY",
181
+ description=f"{op} operation on table '{tbl}'",
182
+ )
183
+ )
184
+
185
+ # 4. Generate Code per Framework
186
+ if framework == "pytest":
187
+ return _generate_pytest_scaffold(
188
+ sym_name=sym_name,
189
+ sym_path=sym_path,
190
+ sym_kind=sym_kind,
191
+ decorators=decorators,
192
+ module=module,
193
+ mock_reqs=mock_reqs,
194
+ route_row=route_row,
195
+ )
196
+ elif framework == "jest":
197
+ return _generate_jest_scaffold(
198
+ sym_name=sym_name,
199
+ sym_path=sym_path,
200
+ sym_kind=sym_kind,
201
+ mock_reqs=mock_reqs,
202
+ route_row=route_row,
203
+ )
204
+ else:
205
+ return _generate_testez_scaffold(
206
+ sym_name=sym_name,
207
+ sym_path=sym_path,
208
+ mock_reqs=mock_reqs,
209
+ )
210
+
211
+
212
+ def _generate_pytest_scaffold(
213
+ sym_name: str,
214
+ sym_path: str,
215
+ sym_kind: str,
216
+ decorators: str,
217
+ module: str,
218
+ mock_reqs: list[MockRequirement],
219
+ route_row: tuple[Any, ...] | None,
220
+ ) -> GeneratedTestScaffold:
221
+ """Generate pytest scaffold with fixtures, mocks, and parameterized cases."""
222
+ is_async = "async" in decorators.lower() or "async" in sym_kind.lower()
223
+ is_route = route_row is not None or any(d in decorators for d in ("get", "post", "put", "delete", "route"))
224
+
225
+ test_file_name = f"tests/test_{Path(sym_path).stem}.py"
226
+ import_path = module or sym_path.replace("/", ".").replace(".py", "")
227
+
228
+ code_lines: list[str] = [
229
+ '"""Automated test scaffold generated by CodeGraph MCP."""',
230
+ "from __future__ import annotations",
231
+ "",
232
+ "from unittest.mock import AsyncMock, MagicMock, patch",
233
+ "import pytest",
234
+ "",
235
+ ]
236
+
237
+ if is_route:
238
+ code_lines.extend([
239
+ "from fastapi.testclient import TestClient",
240
+ f"from {import_path} import app, {sym_name}",
241
+ "",
242
+ "@pytest.fixture",
243
+ "def client():",
244
+ " return TestClient(app)",
245
+ "",
246
+ ])
247
+ else:
248
+ code_lines.extend([
249
+ f"from {import_path} import {sym_name}",
250
+ "",
251
+ ])
252
+
253
+ # Fixtures for mocks
254
+ has_db = any(m.mock_type == "DB_SESSION" for m in mock_reqs)
255
+ if has_db:
256
+ code_lines.extend([
257
+ "@pytest.fixture",
258
+ "def mock_db_session():",
259
+ " session = MagicMock()",
260
+ " session.execute.return_value = MagicMock()",
261
+ " session.commit.return_value = None",
262
+ " session.rollback.return_value = None",
263
+ " return session",
264
+ "",
265
+ ])
266
+
267
+ test_cases: list[TestCaseScaffold] = []
268
+
269
+ # Happy Path Test
270
+ async_prefix = "@pytest.mark.anyio\nasync " if is_async else ""
271
+ await_prefix = "await " if is_async else ""
272
+
273
+ if is_route and route_row:
274
+ m = route_row[0].lower()
275
+ p = route_row[1]
276
+ happy_case = (
277
+ f"def test_{sym_name}_success(client):\n"
278
+ f' response = client.{m}("{p}")\n'
279
+ f" assert response.status_code == 200\n"
280
+ f" assert response.json() is not None\n"
281
+ )
282
+ error_case = (
283
+ f"def test_{sym_name}_invalid_payload(client):\n"
284
+ f' response = client.{m}("{p}", json={{"invalid": True}})\n'
285
+ f" assert response.status_code in (400, 422)\n"
286
+ )
287
+ else:
288
+ happy_case = (
289
+ f"{async_prefix}def test_{sym_name}_happy_path():\n"
290
+ f" # Arrange\n"
291
+ f" # Act\n"
292
+ f" result = {await_prefix}{sym_name}()\n"
293
+ f" # Assert\n"
294
+ f" assert result is not None\n"
295
+ )
296
+ error_case = (
297
+ f"{async_prefix}def test_{sym_name}_edge_case_none():\n"
298
+ f" with pytest.raises(Exception):\n"
299
+ f" {await_prefix}{sym_name}(None)\n"
300
+ )
301
+
302
+ test_cases.append(TestCaseScaffold(
303
+ name=f"test_{sym_name}_happy_path",
304
+ description=f"Verify {sym_name} returns expected output on standard inputs.",
305
+ test_type="HAPPY_PATH",
306
+ code=happy_case,
307
+ ))
308
+ test_cases.append(TestCaseScaffold(
309
+ name=f"test_{sym_name}_edge_case",
310
+ description=f"Verify {sym_name} handles invalid or boundary inputs properly.",
311
+ test_type="BOUNDARY",
312
+ code=error_case,
313
+ ))
314
+
315
+ for tc in test_cases:
316
+ code_lines.append(tc.code)
317
+
318
+ return GeneratedTestScaffold(
319
+ target_symbol=sym_name,
320
+ target_file=sym_path,
321
+ framework="pytest",
322
+ suggested_test_file=test_file_name,
323
+ test_code="\n".join(code_lines),
324
+ mock_requirements=mock_reqs,
325
+ test_cases=test_cases,
326
+ )
327
+
328
+
329
+ def _generate_jest_scaffold(
330
+ sym_name: str,
331
+ sym_path: str,
332
+ sym_kind: str,
333
+ mock_reqs: list[MockRequirement],
334
+ route_row: tuple[Any, ...] | None,
335
+ ) -> GeneratedTestScaffold:
336
+ """Generate Jest/Vitest test scaffold."""
337
+ test_file_name = f"tests/{Path(sym_path).stem}.test.ts"
338
+ code_lines: list[str] = [
339
+ '/** Automated test scaffold generated by CodeGraph MCP */',
340
+ f'import {{ {sym_name} }} from "../{sym_path.replace(".ts", "").replace(".js", "")}";',
341
+ 'import { describe, it, expect, vi, beforeEach } from "vitest";',
342
+ '',
343
+ f'describe("{sym_name}", () => {{',
344
+ ' beforeEach(() => {',
345
+ ' vi.clearAllMocks();',
346
+ ' });',
347
+ '',
348
+ f' it("should execute {sym_name} successfully under normal inputs", async () => {{',
349
+ f' const result = await {sym_name}();',
350
+ ' expect(result).toBeDefined();',
351
+ ' });',
352
+ '',
353
+ ' it("should throw or reject when invalid arguments are provided", async () => {',
354
+ f' await expect({sym_name}(null as any)).rejects.toThrow();',
355
+ ' });',
356
+ '});',
357
+ ]
358
+
359
+ test_cases = [
360
+ TestCaseScaffold(
361
+ name=f"{sym_name} success",
362
+ description="Happy path execution",
363
+ test_type="HAPPY_PATH",
364
+ code=f"it('should execute {sym_name} successfully', async () => {{ ... }});",
365
+ ),
366
+ TestCaseScaffold(
367
+ name=f"{sym_name} error handling",
368
+ description="Invalid arguments throw error",
369
+ test_type="ERROR",
370
+ code="it('should throw on invalid arguments', async () => { ... });",
371
+ ),
372
+ ]
373
+
374
+ return GeneratedTestScaffold(
375
+ target_symbol=sym_name,
376
+ target_file=sym_path,
377
+ framework="jest",
378
+ suggested_test_file=test_file_name,
379
+ test_code="\n".join(code_lines),
380
+ mock_requirements=mock_reqs,
381
+ test_cases=test_cases,
382
+ )
383
+
384
+
385
+ def _generate_testez_scaffold(
386
+ sym_name: str,
387
+ sym_path: str,
388
+ mock_reqs: list[MockRequirement],
389
+ ) -> GeneratedTestScaffold:
390
+ """Generate Luau TestEZ scaffold."""
391
+ test_file_name = f"{Path(sym_path).parent}/{Path(sym_path).stem}.spec.luau"
392
+ code_lines: list[str] = [
393
+ '-- Automated TestEZ scaffold generated by CodeGraph MCP',
394
+ 'return function()',
395
+ f' local {sym_name} = require(script.Parent.{Path(sym_path).stem})',
396
+ '',
397
+ f' describe("{sym_name}", function()',
398
+ ' it("should execute properly with valid arguments", function()',
399
+ f' local result = {sym_name}.new()',
400
+ ' expect(result).to.be.ok()',
401
+ ' end)',
402
+ '',
403
+ ' it("should handle error conditions cleanly", function()',
404
+ ' expect(function()',
405
+ f' {sym_name}.process(nil)',
406
+ ' end).to.throw()',
407
+ ' end)',
408
+ ' end)',
409
+ 'end',
410
+ ]
411
+
412
+ return GeneratedTestScaffold(
413
+ target_symbol=sym_name,
414
+ target_file=sym_path,
415
+ framework="testez",
416
+ suggested_test_file=test_file_name,
417
+ test_code="\n".join(code_lines),
418
+ mock_requirements=mock_reqs,
419
+ test_cases=[],
420
+ )
421
+
422
+
423
+ def _generate_fallback_scaffold(target: str, framework: str) -> GeneratedTestScaffold:
424
+ """Generic fallback test scaffold when target is not found in the index."""
425
+ test_code = (
426
+ f'import pytest\n\n'
427
+ f'def test_{target}_placeholder():\n'
428
+ f' # TODO: Implement tests for {target}\n'
429
+ f' assert True\n'
430
+ )
431
+ return GeneratedTestScaffold(
432
+ target_symbol=target,
433
+ target_file="src/unknown.py",
434
+ framework=framework,
435
+ suggested_test_file=f"tests/test_{target}.py",
436
+ test_code=test_code,
437
+ )