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.
- codegraph/__init__.py +1 -1
- codegraph/__main__.py +7 -0
- codegraph/agent_brain.py +253 -2
- codegraph/agent_capabilities.py +1348 -67
- codegraph/agent_rules.py +2 -1
- codegraph/api_drift.py +455 -0
- codegraph/architecture_rules.py +123 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/change_impact.py +320 -110
- codegraph/changelog.py +170 -0
- codegraph/cli.py +1605 -62
- codegraph/cli_output.py +17 -2
- codegraph/context.py +135 -24
- codegraph/context_freshness.py +2 -0
- codegraph/database/__init__.py +14 -0
- codegraph/database/extractor.py +712 -17
- codegraph/database/interrogation.py +388 -2
- codegraph/database/schema_drift.py +862 -0
- codegraph/dataflow_propagation.py +118 -0
- codegraph/dead_code.py +387 -0
- codegraph/epistemic.py +15 -0
- codegraph/evidence_contract.py +34 -3
- codegraph/federation.py +245 -0
- codegraph/frameworks.py +234 -12
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +2 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +2 -0
- codegraph/git_state.py +2 -0
- codegraph/graph/traversal.py +150 -7
- codegraph/graph_csr.py +218 -0
- codegraph/incremental.py +150 -0
- codegraph/indexing/__init__.py +7 -1
- codegraph/indexing/classifier.py +5 -0
- codegraph/indexing/indexer.py +292 -38
- codegraph/indexing/languages/__init__.py +1 -0
- codegraph/indexing/languages/cpp.py +457 -0
- codegraph/indexing/languages/go.py +459 -0
- codegraph/indexing/languages/jvm.py +583 -0
- codegraph/indexing/languages/rust.py +507 -0
- codegraph/indexing/models.py +15 -1
- codegraph/indexing/parser.py +385 -9
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/protocols.py +220 -0
- codegraph/indexing/scanner.py +23 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/interrogation.py +207 -39
- codegraph/lsp.py +318 -0
- codegraph/lsp_bridge/__init__.py +16 -0
- codegraph/lsp_bridge/bridge.py +102 -0
- codegraph/lsp_bridge/client.py +207 -0
- codegraph/lsp_bridge/models.py +71 -0
- codegraph/mcp/server.py +502 -29
- codegraph/memory/trajectory.py +282 -0
- codegraph/models.py +7 -0
- codegraph/monorepo.py +3 -0
- codegraph/multi_agent/__init__.py +13 -0
- codegraph/multi_agent/coordinator.py +229 -0
- codegraph/multi_agent/models.py +76 -0
- codegraph/optimizer.py +42 -5
- codegraph/polymorphism.py +95 -0
- codegraph/pr_review.py +296 -0
- codegraph/process_lifecycle.py +50 -12
- codegraph/reachability.py +198 -0
- codegraph/reachability_index.py +74 -0
- codegraph/refactor/__init__.py +48 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/patcher.py +348 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/repair.py +233 -0
- codegraph/resolver.py +1357 -161
- codegraph/retrieval_policy.py +22 -1
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +7 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +25 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +30 -10
- codegraph/runtime/otel_receiver.py +208 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/runner.py +155 -6
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/sandbox.py +215 -0
- codegraph/search/hybrid.py +26 -0
- codegraph/security/supply_chain.py +188 -0
- codegraph/security/taint.py +421 -0
- codegraph/semantic_decorators.py +34 -3
- codegraph/structural_diff.py +2 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +2 -0
- codegraph/test_gen.py +437 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +876 -0
- codegraph/watcher.py +323 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/METADATA +365 -67
- codegraph_engine-3.0.1.dist-info/RECORD +164 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/WHEEL +1 -1
- codegraph_engine-2.3.0.dist-info/RECORD +0 -94
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
+
)
|
codegraph/symbol_history.py
CHANGED
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
|
+
)
|