codegraph-engine 2.1.2__tar.gz → 2.1.3__tar.gz
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_engine-2.1.2 → codegraph_engine-2.1.3}/PKG-INFO +1 -1
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/pyproject.toml +1 -1
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/__init__.py +1 -1
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/frameworks.py +13 -7
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/indexing/indexer.py +27 -1
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph_engine.egg-info/PKG-INFO +1 -1
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph_engine.egg-info/SOURCES.txt +1 -0
- codegraph_engine-2.1.3/tests/test_framework_routes_persistence.py +363 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_platform_resources.py +1 -1
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/LICENSE +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/README.md +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/setup.cfg +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/agent.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/architecture.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/audit.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/cache.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/cli.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/config.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/constraints.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/context.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/epistemic.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/errors.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/evidence/__init__.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/evidence/citations.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/freshness.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/git.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/graph/__init__.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/graph/models.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/graph/traversal.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/indexing/__init__.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/indexing/classifier.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/indexing/models.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/indexing/parser.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/indexing/scanner.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/indexing/test_framework.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/interrogation.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/llm/__init__.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/llm/base.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/llm/context.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/mcp/__init__.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/mcp/server.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/memory/__init__.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/memory/store.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/models.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/observability.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/optimizer.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/planner.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/py.typed +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/query_expansion.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/ranking.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/resolver.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/resources/__init__.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/resources/cache.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/resources/coalescer.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/resources/debouncer.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/resources/governor.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/resources/monitor.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/resources/policy.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/retrieval_policy.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/search/__init__.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/search/hybrid.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/search/semantic.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/security/__init__.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/security/paths.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/target_resolver.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph/task.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph_engine.egg-info/dependency_links.txt +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph_engine.egg-info/entry_points.txt +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph_engine.egg-info/requires.txt +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph_engine.egg-info/top_level.txt +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_active_coding_protection.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_adaptive_planning.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_adversarial_edge_cases.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_agent_ux_hardening.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_benchmark_infra.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_bounded_caches.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_cli_doctor_privacy.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_context_budget.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_context_cache.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_context_compiler_v2.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_core.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_database_integrity.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_debouncer.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_determinism_and_soak.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_developer_audit.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_evaluation_framework.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_framework_analyzers.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_git_intelligence.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_hardening.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_interrogation_contracts.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_latency_modes_and_parallel.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_mcp_integration.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_phase2.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_ranking_engine.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_reference_resolution.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_request_coalescer.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_resource_governor.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_retrieval_planner.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_scanner_security.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_single_pass_parser.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_symbol_identity.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_task_ambiguity.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_task_mcp_tools.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_task_normalization.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_task_spec.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v211_factory_resolution.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v211_features.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v211_imports_dependents_cli.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v211_recursive_frameworks.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v21_diagnostics.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v21_hard_exclusions.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v21_query_expansion.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v21_retrieval_policy.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_v21_target_resolver.py +0 -0
- {codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/tests/test_verify_evidence.py +0 -0
|
@@ -79,6 +79,7 @@ class RouteDetection:
|
|
|
79
79
|
resolution_status: str = "RESOLVED" # RESOLVED | UNRESOLVED | AMBIGUOUS
|
|
80
80
|
evidence: str = ""
|
|
81
81
|
module: str = ""
|
|
82
|
+
endpoint_id_override: str | None = None
|
|
82
83
|
|
|
83
84
|
@property
|
|
84
85
|
def route_signature(self) -> str:
|
|
@@ -87,13 +88,18 @@ class RouteDetection:
|
|
|
87
88
|
|
|
88
89
|
@property
|
|
89
90
|
def endpoint_id(self) -> str:
|
|
90
|
-
"""Stable, globally unique endpoint identifier
|
|
91
|
-
if self.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
"""Stable, deterministic, globally unique endpoint identifier for a route declaration."""
|
|
92
|
+
if self.endpoint_id_override:
|
|
93
|
+
return self.endpoint_id_override
|
|
94
|
+
|
|
95
|
+
clean_path = self.file_path.replace("\\", "/").strip()
|
|
96
|
+
while clean_path.startswith("./"):
|
|
97
|
+
clean_path = clean_path[2:]
|
|
98
|
+
clean_path = clean_path.lstrip("/") or "root"
|
|
99
|
+
|
|
100
|
+
handler_part = f"#{self.handler_name}" if self.handler_name else ""
|
|
101
|
+
fw_part = f":{self.framework}" if self.framework else ""
|
|
102
|
+
return f"API_ENDPOINT{fw_part}:{clean_path}:{self.line}:{self.http_method} {self.normalized_route}{handler_part}"
|
|
97
103
|
|
|
98
104
|
def as_dict(self) -> dict[str, object]:
|
|
99
105
|
return {
|
|
@@ -655,7 +655,18 @@ class Indexer:
|
|
|
655
655
|
con.executemany(
|
|
656
656
|
"INSERT INTO framework_routes(endpoint_id, framework, http_method, route_path, "
|
|
657
657
|
"normalized_route, handler_name, handler_canonical_id, file_path, line, evidence, confidence) "
|
|
658
|
-
"VALUES (?,?,?,?,?,?,?,?,?,?,?)"
|
|
658
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?,?) "
|
|
659
|
+
"ON CONFLICT(endpoint_id) DO UPDATE SET "
|
|
660
|
+
"framework=excluded.framework, "
|
|
661
|
+
"http_method=excluded.http_method, "
|
|
662
|
+
"route_path=excluded.route_path, "
|
|
663
|
+
"normalized_route=excluded.normalized_route, "
|
|
664
|
+
"handler_name=excluded.handler_name, "
|
|
665
|
+
"handler_canonical_id=excluded.handler_canonical_id, "
|
|
666
|
+
"file_path=excluded.file_path, "
|
|
667
|
+
"line=excluded.line, "
|
|
668
|
+
"evidence=excluded.evidence, "
|
|
669
|
+
"confidence=excluded.confidence",
|
|
659
670
|
[
|
|
660
671
|
(
|
|
661
672
|
r.endpoint_id,
|
|
@@ -794,6 +805,7 @@ class Indexer:
|
|
|
794
805
|
line=int(r["line"]),
|
|
795
806
|
evidence=str(r["evidence"]),
|
|
796
807
|
confidence=str(r["confidence"]),
|
|
808
|
+
endpoint_id_override=str(r["endpoint_id"]),
|
|
797
809
|
)
|
|
798
810
|
for r in rt_rows
|
|
799
811
|
]
|
|
@@ -930,6 +942,20 @@ def check_database_health(con: sqlite3.Connection, repository: Path | None = Non
|
|
|
930
942
|
if parser_ver and parser_ver != PARSER_VERSION:
|
|
931
943
|
issues.append(f"Parser version mismatch: indexed with {parser_ver}, active parser is {PARSER_VERSION}.")
|
|
932
944
|
|
|
945
|
+
# 8. Orphan framework routes
|
|
946
|
+
orphan_routes = con.execute(
|
|
947
|
+
"SELECT count(*) FROM framework_routes r LEFT JOIN files f ON r.file_path = f.path WHERE f.path IS NULL"
|
|
948
|
+
).fetchone()[0]
|
|
949
|
+
if orphan_routes > 0:
|
|
950
|
+
issues.append(f"Found {orphan_routes} orphan route(s) referencing non-existent files.")
|
|
951
|
+
|
|
952
|
+
# 9. Duplicate endpoint IDs
|
|
953
|
+
dup_routes = con.execute(
|
|
954
|
+
"SELECT endpoint_id, count(*) FROM framework_routes GROUP BY endpoint_id HAVING count(*) > 1"
|
|
955
|
+
).fetchall()
|
|
956
|
+
if dup_routes:
|
|
957
|
+
issues.append(f"Found {len(dup_routes)} duplicate route endpoint ID(s).")
|
|
958
|
+
|
|
933
959
|
status = "OK" if not issues else "ISSUES_FOUND"
|
|
934
960
|
return {
|
|
935
961
|
"status": status,
|
|
@@ -81,6 +81,7 @@ tests/test_determinism_and_soak.py
|
|
|
81
81
|
tests/test_developer_audit.py
|
|
82
82
|
tests/test_evaluation_framework.py
|
|
83
83
|
tests/test_framework_analyzers.py
|
|
84
|
+
tests/test_framework_routes_persistence.py
|
|
84
85
|
tests/test_git_intelligence.py
|
|
85
86
|
tests/test_hardening.py
|
|
86
87
|
tests/test_interrogation_contracts.py
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""Regression tests for framework route identity, collision prevention, and idempotent persistence."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from typer.testing import CliRunner
|
|
7
|
+
|
|
8
|
+
from codegraph.cli import app
|
|
9
|
+
from codegraph.frameworks import RouteDetection, analyze_js_ts_line
|
|
10
|
+
from codegraph.indexing import Indexer
|
|
11
|
+
from codegraph.indexing.indexer import check_database_health
|
|
12
|
+
from codegraph.target_resolver import TargetType, resolve_target
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_deterministic_endpoint_ids() -> None:
|
|
16
|
+
"""Verify endpoint_id is stable, deterministic, and includes declaration evidence."""
|
|
17
|
+
r1 = RouteDetection(
|
|
18
|
+
framework="express",
|
|
19
|
+
http_method="GET",
|
|
20
|
+
route_path="/products",
|
|
21
|
+
normalized_route="/products",
|
|
22
|
+
handler_name="getProducts",
|
|
23
|
+
handler_canonical_id="routes.products.getProducts",
|
|
24
|
+
file_path="src/routes/products.ts",
|
|
25
|
+
line=12,
|
|
26
|
+
column=4,
|
|
27
|
+
confidence="HIGH",
|
|
28
|
+
resolution_status="RESOLVED",
|
|
29
|
+
evidence="router.get('/products', getProducts)",
|
|
30
|
+
module="src.routes.products",
|
|
31
|
+
)
|
|
32
|
+
r2 = RouteDetection(
|
|
33
|
+
framework="express",
|
|
34
|
+
http_method="GET",
|
|
35
|
+
route_path="/products",
|
|
36
|
+
normalized_route="/products",
|
|
37
|
+
handler_name="getProducts",
|
|
38
|
+
handler_canonical_id="routes.products.getProducts",
|
|
39
|
+
file_path="src/routes/products.ts",
|
|
40
|
+
line=12,
|
|
41
|
+
column=4,
|
|
42
|
+
confidence="HIGH",
|
|
43
|
+
resolution_status="RESOLVED",
|
|
44
|
+
evidence="router.get('/products', getProducts)",
|
|
45
|
+
module="src.routes.products",
|
|
46
|
+
)
|
|
47
|
+
# Must be deterministic and stable
|
|
48
|
+
assert r1.endpoint_id == r2.endpoint_id
|
|
49
|
+
assert r1.endpoint_id == "API_ENDPOINT:express:src/routes/products.ts:12:GET /products#getProducts"
|
|
50
|
+
|
|
51
|
+
# Override takes precedence if provided (e.g. from existing DB records)
|
|
52
|
+
r_override = RouteDetection(
|
|
53
|
+
framework="express",
|
|
54
|
+
http_method="GET",
|
|
55
|
+
route_path="/products",
|
|
56
|
+
normalized_route="/products",
|
|
57
|
+
handler_name="getProducts",
|
|
58
|
+
handler_canonical_id="routes.products.getProducts",
|
|
59
|
+
file_path="src/routes/products.ts",
|
|
60
|
+
line=12,
|
|
61
|
+
endpoint_id_override="CUSTOM_ID_123",
|
|
62
|
+
)
|
|
63
|
+
assert r_override.endpoint_id == "CUSTOM_ID_123"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_endpoint_id_collision_prevention_same_file() -> None:
|
|
67
|
+
"""Verify distinct route declarations in the same file for same path produce distinct endpoint_ids."""
|
|
68
|
+
lines = [
|
|
69
|
+
"router.get('/products', authCheck);",
|
|
70
|
+
"router.get('/products', listProducts);",
|
|
71
|
+
]
|
|
72
|
+
routes: list[RouteDetection] = []
|
|
73
|
+
for idx, line in enumerate(lines):
|
|
74
|
+
routes.extend(analyze_js_ts_line(line, idx + 1, "routes/products.js", "routes.products", set()))
|
|
75
|
+
|
|
76
|
+
assert len(routes) == 2
|
|
77
|
+
assert routes[0].route_signature == "GET /products"
|
|
78
|
+
assert routes[1].route_signature == "GET /products"
|
|
79
|
+
# Route signature is identical, but endpoint_id must be distinct!
|
|
80
|
+
assert routes[0].endpoint_id != routes[1].endpoint_id
|
|
81
|
+
assert "authCheck" in routes[0].endpoint_id
|
|
82
|
+
assert "listProducts" in routes[1].endpoint_id
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_two_files_with_same_method_and_path(tmp_path: Path) -> None:
|
|
86
|
+
"""Verify two distinct files with identical method + path can coexist without collisions."""
|
|
87
|
+
routes_dir = tmp_path / "routes"
|
|
88
|
+
routes_dir.mkdir(parents=True)
|
|
89
|
+
|
|
90
|
+
file_a = routes_dir / "a.ts"
|
|
91
|
+
file_a.write_text("router.get('/products', handlerA);\n", encoding="utf-8")
|
|
92
|
+
|
|
93
|
+
file_b = routes_dir / "b.ts"
|
|
94
|
+
file_b.write_text("router.get('/products', handlerB);\n", encoding="utf-8")
|
|
95
|
+
|
|
96
|
+
indexer = Indexer(tmp_path)
|
|
97
|
+
res = indexer.index()
|
|
98
|
+
assert res["indexed"] == 2
|
|
99
|
+
|
|
100
|
+
with indexer.session() as con:
|
|
101
|
+
rows = con.execute(
|
|
102
|
+
"SELECT endpoint_id, file_path, handler_name, line FROM framework_routes ORDER BY file_path"
|
|
103
|
+
).fetchall()
|
|
104
|
+
assert len(rows) == 2
|
|
105
|
+
assert rows[0]["file_path"] == "routes/a.ts"
|
|
106
|
+
assert rows[0]["handler_name"] == "handlerA"
|
|
107
|
+
assert rows[1]["file_path"] == "routes/b.ts"
|
|
108
|
+
assert rows[1]["handler_name"] == "handlerB"
|
|
109
|
+
# Neither overwrote the other!
|
|
110
|
+
assert rows[0]["endpoint_id"] != rows[1]["endpoint_id"]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_same_route_declaration_discovered_twice(tmp_path: Path) -> None:
|
|
114
|
+
"""Verify exact duplicate route declaration handled idempotently via ON CONFLICT without crash."""
|
|
115
|
+
indexer = Indexer(tmp_path)
|
|
116
|
+
indexer.index()
|
|
117
|
+
|
|
118
|
+
with indexer.session() as con:
|
|
119
|
+
# File must exist to satisfy foreign key
|
|
120
|
+
con.execute(
|
|
121
|
+
"INSERT INTO files(path, hash, language, indexed_at, status) VALUES ('app.js', 'h1', 'javascript', 0, 'ok')"
|
|
122
|
+
)
|
|
123
|
+
r = RouteDetection(
|
|
124
|
+
framework="express",
|
|
125
|
+
http_method="GET",
|
|
126
|
+
route_path="/items",
|
|
127
|
+
normalized_route="/items",
|
|
128
|
+
handler_name="getItems",
|
|
129
|
+
handler_canonical_id="app.getItems",
|
|
130
|
+
file_path="app.js",
|
|
131
|
+
line=10,
|
|
132
|
+
evidence="router.get('/items', getItems)",
|
|
133
|
+
)
|
|
134
|
+
query = (
|
|
135
|
+
"INSERT INTO framework_routes(endpoint_id, framework, http_method, route_path, "
|
|
136
|
+
"normalized_route, handler_name, handler_canonical_id, file_path, line, evidence, confidence) "
|
|
137
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?,?) "
|
|
138
|
+
"ON CONFLICT(endpoint_id) DO UPDATE SET "
|
|
139
|
+
"framework=excluded.framework, "
|
|
140
|
+
"http_method=excluded.http_method, "
|
|
141
|
+
"route_path=excluded.route_path, "
|
|
142
|
+
"normalized_route=excluded.normalized_route, "
|
|
143
|
+
"handler_name=excluded.handler_name, "
|
|
144
|
+
"handler_canonical_id=excluded.handler_canonical_id, "
|
|
145
|
+
"file_path=excluded.file_path, "
|
|
146
|
+
"line=excluded.line, "
|
|
147
|
+
"evidence=excluded.evidence, "
|
|
148
|
+
"confidence=excluded.confidence"
|
|
149
|
+
)
|
|
150
|
+
# Execute twice with identical endpoint_id in same transaction
|
|
151
|
+
con.execute(query, (r.endpoint_id, r.framework, r.http_method, r.route_path, r.normalized_route, r.handler_name, r.handler_canonical_id, "app.js", r.line, r.evidence, r.confidence))
|
|
152
|
+
con.execute(query, (r.endpoint_id, r.framework, r.http_method, r.route_path, r.normalized_route, r.handler_name, r.handler_canonical_id, "app.js", r.line, "updated evidence", r.confidence))
|
|
153
|
+
|
|
154
|
+
row = con.execute("SELECT count(*), evidence FROM framework_routes WHERE endpoint_id=?", (r.endpoint_id,)).fetchone()
|
|
155
|
+
assert row[0] == 1
|
|
156
|
+
assert row[1] == "updated evidence"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_repeated_indexing(tmp_path: Path) -> None:
|
|
160
|
+
"""Verify repeated indexing does not crash and produces consistent route count."""
|
|
161
|
+
(tmp_path / "server.js").write_text(
|
|
162
|
+
"app.get('/health', getHealth);\n"
|
|
163
|
+
"app.post('/login', postLogin);\n",
|
|
164
|
+
encoding="utf-8",
|
|
165
|
+
)
|
|
166
|
+
indexer = Indexer(tmp_path)
|
|
167
|
+
|
|
168
|
+
# First index
|
|
169
|
+
res1 = indexer.index()
|
|
170
|
+
assert res1["indexed"] == 1
|
|
171
|
+
|
|
172
|
+
with indexer.session() as con:
|
|
173
|
+
count1 = con.execute("SELECT count(*) FROM framework_routes").fetchone()[0]
|
|
174
|
+
assert count1 == 2
|
|
175
|
+
|
|
176
|
+
# Second index immediately (files unchanged)
|
|
177
|
+
res2 = indexer.index()
|
|
178
|
+
assert res2["unchanged"] == 1
|
|
179
|
+
|
|
180
|
+
with indexer.session() as con:
|
|
181
|
+
count2 = con.execute("SELECT count(*) FROM framework_routes").fetchone()[0]
|
|
182
|
+
assert count2 == 2
|
|
183
|
+
|
|
184
|
+
# Third index with force_reparse
|
|
185
|
+
with indexer.session() as con:
|
|
186
|
+
# Force reparse by setting parser version to dummy
|
|
187
|
+
con.execute("UPDATE metadata SET value='old_parser' WHERE key='parser_version'")
|
|
188
|
+
|
|
189
|
+
res3 = indexer.index()
|
|
190
|
+
assert res3["indexed"] == 1
|
|
191
|
+
|
|
192
|
+
with indexer.session() as con:
|
|
193
|
+
count3 = con.execute("SELECT count(*) FROM framework_routes").fetchone()[0]
|
|
194
|
+
assert count3 == 2
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_incremental_route_reindex(tmp_path: Path) -> None:
|
|
198
|
+
"""Verify modifying a route file replaces old routes cleanly without orphan rows."""
|
|
199
|
+
file_path = tmp_path / "routes.js"
|
|
200
|
+
file_path.write_text("router.get('/products', oldHandler);\n", encoding="utf-8")
|
|
201
|
+
|
|
202
|
+
indexer = Indexer(tmp_path)
|
|
203
|
+
indexer.index()
|
|
204
|
+
|
|
205
|
+
with indexer.session() as con:
|
|
206
|
+
rows = con.execute("SELECT handler_name, line FROM framework_routes WHERE file_path='routes.js'").fetchall()
|
|
207
|
+
assert len(rows) == 1
|
|
208
|
+
assert rows[0]["handler_name"] == "oldHandler"
|
|
209
|
+
assert rows[0]["line"] == 1
|
|
210
|
+
|
|
211
|
+
# Modify file: shift line down and add another route
|
|
212
|
+
file_path.write_text(
|
|
213
|
+
"// added comment header\n"
|
|
214
|
+
"// line 2\n"
|
|
215
|
+
"router.get('/products', newHandler);\n"
|
|
216
|
+
"router.delete('/products', deleteHandler);\n",
|
|
217
|
+
encoding="utf-8",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
indexer.index()
|
|
221
|
+
|
|
222
|
+
with indexer.session() as con:
|
|
223
|
+
rows = con.execute("SELECT handler_name, http_method, line FROM framework_routes WHERE file_path='routes.js' ORDER BY line").fetchall()
|
|
224
|
+
assert len(rows) == 2
|
|
225
|
+
# Old route removed, new routes added at correct lines
|
|
226
|
+
assert rows[0]["handler_name"] == "newHandler"
|
|
227
|
+
assert rows[0]["line"] == 3
|
|
228
|
+
assert rows[1]["handler_name"] == "deleteHandler"
|
|
229
|
+
assert rows[1]["line"] == 4
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_route_foreign_key_and_evidence_integrity(tmp_path: Path) -> None:
|
|
233
|
+
"""Verify foreign key integrity on framework_routes and evidence persistence."""
|
|
234
|
+
(tmp_path / "app.py").write_text(
|
|
235
|
+
"from fastapi import FastAPI\n"
|
|
236
|
+
"app = FastAPI()\n"
|
|
237
|
+
"@app.get('/users/{user_id}')\n"
|
|
238
|
+
"def get_user(user_id: int):\n"
|
|
239
|
+
" return {'id': user_id}\n",
|
|
240
|
+
encoding="utf-8",
|
|
241
|
+
)
|
|
242
|
+
indexer = Indexer(tmp_path)
|
|
243
|
+
indexer.index()
|
|
244
|
+
|
|
245
|
+
with indexer.session() as con:
|
|
246
|
+
# Check foreign key to files table
|
|
247
|
+
row = con.execute(
|
|
248
|
+
"SELECT r.endpoint_id, r.file_path, f.path, r.evidence "
|
|
249
|
+
"FROM framework_routes r JOIN files f ON r.file_path = f.path"
|
|
250
|
+
).fetchone()
|
|
251
|
+
assert row is not None
|
|
252
|
+
assert row["file_path"] == "app.py"
|
|
253
|
+
assert "FastAPI decorator" in row["evidence"]
|
|
254
|
+
|
|
255
|
+
# Doctor health check
|
|
256
|
+
health = check_database_health(con, tmp_path)
|
|
257
|
+
assert health["status"] == "OK"
|
|
258
|
+
assert health["issues"] == []
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_framework_route_queries_after_deduplication(tmp_path: Path) -> None:
|
|
262
|
+
"""Verify CLI and target resolver queries against framework_routes work accurately."""
|
|
263
|
+
(tmp_path / "routes.js").write_text(
|
|
264
|
+
"router.get('/items', getItems);\n"
|
|
265
|
+
"router.post('/items', createItem);\n",
|
|
266
|
+
encoding="utf-8",
|
|
267
|
+
)
|
|
268
|
+
indexer = Indexer(tmp_path)
|
|
269
|
+
indexer.index()
|
|
270
|
+
|
|
271
|
+
runner = CliRunner()
|
|
272
|
+
res = runner.invoke(app, ["routes", str(tmp_path), "--json"])
|
|
273
|
+
assert res.exit_code == 0
|
|
274
|
+
assert "/items" in res.stdout
|
|
275
|
+
assert "getItems" in res.stdout
|
|
276
|
+
assert "createItem" in res.stdout
|
|
277
|
+
|
|
278
|
+
with indexer.session() as con:
|
|
279
|
+
target_res = resolve_target("/items", con)
|
|
280
|
+
assert target_res.target_type == TargetType.API_ENDPOINT
|
|
281
|
+
assert target_res.confidence == "HIGH"
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def test_check_database_health_detects_route_issues(tmp_path: Path) -> None:
|
|
285
|
+
"""Verify check_database_health flags orphan routes and duplicate endpoint IDs."""
|
|
286
|
+
indexer = Indexer(tmp_path)
|
|
287
|
+
indexer.index()
|
|
288
|
+
|
|
289
|
+
with indexer.session() as con:
|
|
290
|
+
# 1. Initially healthy
|
|
291
|
+
assert check_database_health(con, tmp_path)["status"] == "OK"
|
|
292
|
+
|
|
293
|
+
# 2. Insert orphan route referencing non-existent file
|
|
294
|
+
con.execute("PRAGMA foreign_keys = OFF")
|
|
295
|
+
con.execute(
|
|
296
|
+
"INSERT INTO framework_routes(endpoint_id, framework, http_method, route_path, normalized_route, handler_name, handler_canonical_id, file_path, line, evidence) "
|
|
297
|
+
"VALUES ('ep_orphan', 'express', 'GET', '/orphan', '/orphan', 'h', 'h', 'non_existent.js', 1, 'ev')"
|
|
298
|
+
)
|
|
299
|
+
h_orphan = check_database_health(con, tmp_path)
|
|
300
|
+
assert h_orphan["status"] == "ISSUES_FOUND"
|
|
301
|
+
assert any("orphan route" in issue for issue in h_orphan["issues"])
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_fastapi_duplicate_overlapping_routes(tmp_path: Path) -> None:
|
|
305
|
+
"""Verify FastAPI routes with duplicate/overlapping route paths in same file index without collisions."""
|
|
306
|
+
app_file = tmp_path / "app.py"
|
|
307
|
+
app_file.write_text(
|
|
308
|
+
"from fastapi import FastAPI\n"
|
|
309
|
+
"app = FastAPI()\n"
|
|
310
|
+
"@app.get('/items')\n"
|
|
311
|
+
"def get_items_summary():\n"
|
|
312
|
+
" return []\n"
|
|
313
|
+
"@app.get('/items')\n"
|
|
314
|
+
"def get_items_detail():\n"
|
|
315
|
+
" return []\n",
|
|
316
|
+
encoding="utf-8",
|
|
317
|
+
)
|
|
318
|
+
indexer = Indexer(tmp_path)
|
|
319
|
+
res = indexer.index()
|
|
320
|
+
assert res["indexed"] == 1
|
|
321
|
+
|
|
322
|
+
with indexer.session() as con:
|
|
323
|
+
rows = con.execute(
|
|
324
|
+
"SELECT endpoint_id, handler_name, line FROM framework_routes WHERE file_path='app.py' ORDER BY line"
|
|
325
|
+
).fetchall()
|
|
326
|
+
assert len(rows) == 2
|
|
327
|
+
assert rows[0]["handler_name"] == "get_items_summary"
|
|
328
|
+
assert rows[1]["handler_name"] == "get_items_detail"
|
|
329
|
+
assert rows[0]["endpoint_id"] != rows[1]["endpoint_id"]
|
|
330
|
+
assert "get_items_summary" in rows[0]["endpoint_id"]
|
|
331
|
+
assert "get_items_detail" in rows[1]["endpoint_id"]
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_fastapi_and_express_both_work_in_same_repo(tmp_path: Path) -> None:
|
|
335
|
+
"""Verify both FastAPI (Python) and Express (JS/TS) routes index and coexist without collisions."""
|
|
336
|
+
(tmp_path / "main.py").write_text(
|
|
337
|
+
"from fastapi import FastAPI\n"
|
|
338
|
+
"app = FastAPI()\n"
|
|
339
|
+
"@app.get('/api/health')\n"
|
|
340
|
+
"def py_health():\n"
|
|
341
|
+
" return {'ok': True}\n",
|
|
342
|
+
encoding="utf-8",
|
|
343
|
+
)
|
|
344
|
+
(tmp_path / "server.js").write_text(
|
|
345
|
+
"app.get('/api/health', jsHealth);\n",
|
|
346
|
+
encoding="utf-8",
|
|
347
|
+
)
|
|
348
|
+
indexer = Indexer(tmp_path)
|
|
349
|
+
res = indexer.index()
|
|
350
|
+
assert res["indexed"] == 2
|
|
351
|
+
|
|
352
|
+
with indexer.session() as con:
|
|
353
|
+
rows = con.execute(
|
|
354
|
+
"SELECT endpoint_id, framework, file_path, handler_name FROM framework_routes ORDER BY framework"
|
|
355
|
+
).fetchall()
|
|
356
|
+
assert len(rows) == 2
|
|
357
|
+
assert rows[0]["framework"] == "express"
|
|
358
|
+
assert rows[0]["handler_name"] == "jsHealth"
|
|
359
|
+
assert rows[1]["framework"] == "fastapi"
|
|
360
|
+
assert rows[1]["handler_name"] == "py_health"
|
|
361
|
+
assert "express" in rows[0]["endpoint_id"]
|
|
362
|
+
assert "fastapi" in rows[1]["endpoint_id"]
|
|
363
|
+
assert rows[0]["endpoint_id"] != rows[1]["endpoint_id"]
|
|
@@ -152,7 +152,7 @@ def test_cli_startup_on_windows(tmp_path: Path) -> None:
|
|
|
152
152
|
# 1. Version command
|
|
153
153
|
res_ver = runner.invoke(app, ["--version"])
|
|
154
154
|
assert res_ver.exit_code == 0
|
|
155
|
-
assert "2.1.
|
|
155
|
+
assert "2.1.3" in res_ver.stdout
|
|
156
156
|
|
|
157
157
|
# 2. Help command
|
|
158
158
|
res_help = runner.invoke(app, ["--help"], prog_name="codegraph")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph_engine.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph_engine.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph_engine.egg-info/requires.txt
RENAMED
|
File without changes
|
{codegraph_engine-2.1.2 → codegraph_engine-2.1.3}/src/codegraph_engine.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|