) -> i32 {\n"
+ " xs.iter().map(|x| x + 1).sum()\n"
+ " }\n"
+ "}\n",
+ )
+ ingestor = _capture(tmp_path, "crate")
+ refs = _refs(ingestor)
+ closures = _closure_nodes(ingestor)
+ assert closures
+ for closure in closures:
+ assert any(to == closure and frm == "crate.lib.Db.purge" for frm, to in refs), (
+ f"closure {closure} has no REFERENCES edge from Db.purge"
+ )
diff --git a/codebase_rag/tests/test_rust_containment_oracle.py b/codebase_rag/tests/test_rust_containment_oracle.py
new file mode 100644
index 000000000..9c0820b58
--- /dev/null
+++ b/codebase_rag/tests/test_rust_containment_oracle.py
@@ -0,0 +1,89 @@
+# (H) Covers Rust containment-edge validation: cgr's DEFINES (module -> item /
+# (H) nested module) and DEFINES_METHOD (struct/trait -> method) edges are graded
+# (H) against the independent syn oracle (evals/oracles/rs_oracle), joined on
+# (H) (kind, file, line) endpoints. Exercises an inherent impl, a trait method,
+# (H) and an impl inside a nested `mod` (cross-module type resolution).
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+
+from codebase_rag import constants as cs
+from codebase_rag.parser_loader import load_parsers
+from evals import constants as ec
+from evals.cgr_graph import extract_cgr_rust_graph
+from evals.oracles import run_rust_oracle, rust_available
+from evals.score import score_edge_types
+
+RS_SRC = """\
+pub trait Shape {
+ fn area(&self) -> f64 { 0.0 }
+}
+
+pub struct Point {
+ x: i32,
+}
+
+impl Point {
+ pub fn new() -> Point {
+ Point { x: 0 }
+ }
+}
+
+impl Shape for Point {
+ fn area(&self) -> f64 {
+ 1.0
+ }
+}
+
+pub fn free() -> i32 {
+ 1
+}
+
+pub mod inner {
+ pub struct Widget {
+ w: i32,
+ }
+
+ impl Widget {
+ pub fn build(&self) -> i32 {
+ self.w
+ }
+ }
+}
+"""
+
+
+def _require_rust() -> None:
+ if not rust_available():
+ pytest.skip("cargo toolchain not available")
+ if cs.SupportedLanguage.RUST not in load_parsers()[0]:
+ pytest.skip("rust parser not available")
+
+
+def test_cgr_matches_syn_oracle_on_containment_edges(tmp_path: Path) -> None:
+ _require_rust()
+ project = tmp_path / "rs_edge"
+ (project / "src").mkdir(parents=True)
+ (project / "Cargo.toml").write_text(
+ encoding="utf-8", data='[package]\nname = "rs_edge"\nversion = "0.1.0"\n'
+ )
+ (project / "src" / "lib.rs").write_text(RS_SRC, encoding="utf-8")
+
+ cgr = extract_cgr_rust_graph(project, project.name)
+ oracle = run_rust_oracle(project)
+
+ result = score_edge_types(cgr, oracle, ec.SCORED_EDGE_TYPES)
+ by_label = {row["label"]: row for row in result.rows}
+ for label in (
+ cs.RelationshipType.DEFINES.value,
+ cs.RelationshipType.DEFINES_METHOD.value,
+ ):
+ row = by_label.get(label)
+ assert row is not None, (label, by_label, result.diff)
+ assert row["precision"] == 1.0 and row["recall"] == 1.0, (
+ label,
+ row,
+ result.diff,
+ )
diff --git a/codebase_rag/tests/test_rust_external_impl_containment_oracle.py b/codebase_rag/tests/test_rust_external_impl_containment_oracle.py
new file mode 100644
index 000000000..4be2266af
--- /dev/null
+++ b/codebase_rag/tests/test_rust_external_impl_containment_oracle.py
@@ -0,0 +1,48 @@
+# (H) Methods of an impl whose target is NOT a locally declared type
+# (H) (`impl TInput for Box
`) have no owner node cgr can attach to, so
+# (H) cgr anchors them to the module with DEFINES (the documented
+# (H) orphan-prevention fallback). The syn oracle emitted NO containment for
+# (H) them, so every such method graded as a false-positive edge on thrift
+# (H) (53 in src/protocol/mod.rs). The oracle must model the fallback.
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+
+from codebase_rag import constants as cs
+from evals.cgr_graph import extract_cgr_rust_graph
+from evals.oracles import run_rust_oracle, rust_available
+from evals.score import score_edge_types
+
+RS_SRC = """\
+pub trait TInput {
+ fn read_begin(&mut self) -> u32;
+}
+
+impl
TInput for Box
+where
+ P: TInput + ?Sized,
+{
+ fn read_begin(&mut self) -> u32 {
+ (**self).read_begin()
+ }
+}
+"""
+
+
+@pytest.mark.skipif(not rust_available(), reason="cargo toolchain not available")
+def test_impl_on_external_type_methods_anchor_to_module(tmp_path: Path) -> None:
+ (tmp_path / "lib.rs").write_text(RS_SRC)
+
+ cgr = extract_cgr_rust_graph(tmp_path, tmp_path.name)
+ oracle = run_rust_oracle(tmp_path)
+
+ result = score_edge_types(
+ cgr,
+ oracle,
+ (cs.RelationshipType.DEFINES, cs.RelationshipType.DEFINES_METHOD),
+ )
+ for row in result.rows:
+ assert row["fp"] == 0, result.rows
+ assert row["fn"] == 0, result.rows
diff --git a/codebase_rag/tests/test_rust_free_function_return_types.py b/codebase_rag/tests/test_rust_free_function_return_types.py
new file mode 100644
index 000000000..1332a8501
--- /dev/null
+++ b/codebase_rag/tests/test_rust_free_function_return_types.py
@@ -0,0 +1,108 @@
+# (H) Two adjacent gaps left open by the trait-object receiver fix: a free
+# (H) Rust fn's return type was never recorded in method_return_types (only
+# (H) impl methods were, in class ingest), and a bare `let s = make()`
+# (H) single-segment chain bailed out of call-return inference. Together they
+# (H) left a free-function factory's result untyped, so a call on it fell to
+# (H) the name-only trie fallback (or was dropped as external) instead of
+# (H) binding the trait method.
+from __future__ import annotations
+
+from pathlib import Path
+from unittest.mock import MagicMock
+
+import pytest
+from tree_sitter import Language, Parser
+
+from codebase_rag import constants as cs
+from codebase_rag.parsers.rs import type_inference as rs_ti
+from codebase_rag.tests.conftest import run_updater
+from evals.dead_code import cgr_dead_code, default_dead_code_config
+
+try:
+ import tree_sitter_rust as tsrust
+
+ RUST_AVAILABLE = True
+except ImportError:
+ RUST_AVAILABLE = False
+
+TRAIT_AND_IMPLS = (
+ "trait Svc { fn run(&self) -> i32; }\n"
+ "struct Alpha;\n"
+ "impl Svc for Alpha { fn run(&self) -> i32 { 1 } }\n"
+ "struct Beta;\n"
+ "impl Svc for Beta { fn run(&self) -> i32 { 2 } }\n"
+)
+
+FREE_FN_FACTORY = TRAIT_AND_IMPLS + (
+ "pub fn make() -> Box { Box::new(Alpha) }\n"
+ "pub fn try_make() -> Result, ()> { Ok(make()) }\n"
+ "pub fn use_made() -> i32 { let s = make(); s.run() }\n"
+ "pub fn use_tried() -> i32 { let s = try_make().unwrap(); s.run() }\n"
+)
+
+
+def _run_calls(
+ temp_repo: Path, mock_ingestor: MagicMock, files: dict[str, str]
+) -> set[tuple[str, str]]:
+ for name, source in files.items():
+ (temp_repo / name).write_text(source, encoding="utf-8")
+ run_updater(temp_repo, mock_ingestor, skip_if_missing="rust")
+ return {
+ (c.args[0][2], c.args[2][2])
+ for c in mock_ingestor.ensure_relationship_batch.call_args_list
+ if c.args[1] == cs.RelationshipType.CALLS and c.args[2][2].endswith(".run")
+ }
+
+
+@pytest.mark.parametrize("caller", ["use_made", "use_tried"])
+def test_free_fn_factory_result_binds_to_trait_method(
+ temp_repo: Path, mock_ingestor: MagicMock, caller: str
+) -> None:
+ calls = _run_calls(temp_repo, mock_ingestor, {"m.rs": FREE_FN_FACTORY})
+ bound = {callee for c, callee in calls if c.endswith(f".{caller}")}
+ assert bound == {f"{temp_repo.name}.m.Svc.run"}, sorted(calls)
+
+
+def test_imported_free_fn_factory_result_binds_to_trait_method(
+ temp_repo: Path, mock_ingestor: MagicMock
+) -> None:
+ # (H) The factory lives in another module and is brought in by `use`; the
+ # (H) bare-name call must resolve through the import's `::` path to the
+ # (H) function's registry qn before the return-type lookup.
+ files = {
+ "factory.rs": TRAIT_AND_IMPLS
+ + "pub fn make() -> Box { Box::new(Alpha) }\n",
+ "m.rs": (
+ "use crate::factory::{make, Svc};\n"
+ "pub fn use_made() -> i32 { let s = make(); s.run() }\n"
+ ),
+ }
+ calls = _run_calls(temp_repo, mock_ingestor, files)
+ bound = {callee for c, callee in calls if c.endswith(".use_made")}
+ assert bound == {f"{temp_repo.name}.factory.Svc.run"}, sorted(calls)
+
+
+def test_free_fn_factory_keeps_all_impls_alive(tmp_path: Path) -> None:
+ root = tmp_path / "rfree"
+ root.mkdir()
+ (root / "m.rs").write_text(
+ TRAIT_AND_IMPLS
+ + "pub fn make() -> Box { Box::new(Alpha) }\n"
+ + "pub fn use_made() -> i32 { let s = make(); s.run() }\n",
+ encoding="utf-8",
+ )
+ dead = cgr_dead_code(root, "proj", default_dead_code_config(False, False))
+ assert not [d for d in dead if d.endswith(".run")], sorted(dead)
+
+
+@pytest.mark.skipif(not RUST_AVAILABLE, reason="tree-sitter-rust not installed")
+def test_bare_identifier_binding_is_not_a_call() -> None:
+ # (H) `let f = make;` is a move/fn-pointer binding, not a call: `f` holds
+ # (H) the function itself, not a value of its return type, so no chain
+ # (H) binding may be collected for it. Only an invoked base qualifies.
+ parser = Parser(Language(tsrust.language()))
+ src = b"fn user() { let f = make; let s = make(); }\n"
+ fn_node = parser.parse(src).root_node.children[0]
+ bindings = dict(rs_ti.RustTypeInferenceEngine().collect_call_var_bindings(fn_node))
+ assert "f" not in bindings, bindings
+ assert bindings.get("s") == ["make"]
diff --git a/codebase_rag/tests/test_rust_impl_method_call_qn.py b/codebase_rag/tests/test_rust_impl_method_call_qn.py
new file mode 100644
index 000000000..72985416b
--- /dev/null
+++ b/codebase_rag/tests/test_rust_impl_method_call_qn.py
@@ -0,0 +1,64 @@
+from pathlib import Path
+
+from evals.cgr_graph import _capture
+
+
+def _make_crate(root: Path) -> None:
+ root.mkdir(parents=True, exist_ok=True)
+ (root / "lib.rs").write_text(
+ "pub struct Chars<'a> { s: &'a str }\n\n"
+ "impl<'a> Chars<'a> {\n"
+ " pub fn as_str(&self) -> &'a str { self.s }\n"
+ "}\n\n"
+ "pub trait Thing { fn go(&self) -> usize; }\n\n"
+ "impl<'a> Thing for Chars<'a> {\n"
+ " fn go(&self) -> usize { self.as_str().len() }\n"
+ "}\n",
+ encoding="utf-8",
+ )
+
+
+def test_rust_generic_impl_method_caller_qn_strips_generics(tmp_path: Path) -> None:
+ # (H) A method in a generic impl block (`impl<'a> Thing for Chars<'a>`) is
+ # (H) registered on the bare type node (crate.lib.Chars.go). The call inside it
+ # (H) must be attributed to that bare-type caller qn, not a generic-bearing
+ # (H) crate.lib.Chars<'a>.go that matches no node (which drops the CALLS edge).
+ _make_crate(tmp_path)
+ ingestor = _capture(tmp_path, "crate")
+ calls = {
+ (str(from_val), str(to_val))
+ for _fl, from_val, rel, _tl, to_val in ingestor.rels
+ if rel == "CALLS"
+ }
+ node_qns = {str(uid) for (_label, uid) in ingestor.nodes}
+
+ assert "crate.lib.Chars.go" in node_qns
+ assert ("crate.lib.Chars.go", "crate.lib.Chars.as_str") in calls
+ assert ("crate.lib.Chars<'a>.go", "crate.lib.Chars.as_str") not in calls
+
+
+def _make_super_import_crate(root: Path) -> None:
+ root.mkdir(parents=True, exist_ok=True)
+ (root / "lib.rs").write_text("pub mod a;\npub mod b;\n", encoding="utf-8")
+ (root / "b.rs").write_text("pub fn helper() -> i32 { 1 }\n", encoding="utf-8")
+ (root / "a.rs").write_text(
+ "use super::b::helper;\n\npub fn run() -> i32 { helper() }\n",
+ encoding="utf-8",
+ )
+
+
+def test_rust_super_imported_free_fn_call_resolves(tmp_path: Path) -> None:
+ # (H) A free function imported by a Rust relative path (`use super::b::helper`)
+ # (H) and called bare must resolve to the sibling-module function node
+ # (H) (crate.b.helper). The import target is recorded as raw `super::b::helper`
+ # (H) (`::`-separated, not project-prefixed), so the external-import guard must
+ # (H) not mistake it for an external symbol and suppress the trie fallback.
+ _make_super_import_crate(tmp_path)
+ ingestor = _capture(tmp_path, "crate")
+ calls = {
+ (str(from_val), str(to_val))
+ for _fl, from_val, rel, _tl, to_val in ingestor.rels
+ if rel == "CALLS"
+ }
+
+ assert ("crate.a.run", "crate.b.helper") in calls
diff --git a/codebase_rag/tests/test_rust_impl_primitive_target.py b/codebase_rag/tests/test_rust_impl_primitive_target.py
new file mode 100644
index 000000000..a6be79f62
--- /dev/null
+++ b/codebase_rag/tests/test_rust_impl_primitive_target.py
@@ -0,0 +1,44 @@
+# (H) Regression: methods in an `impl Trait for ` block (e.g.
+# (H) `impl From for u8`) must be captured. The impl target `u8` is a
+# (H) `primitive_type` node, which extract_impl_target did not recognise, so every
+# (H) method in such a block was silently dropped.
+from __future__ import annotations
+
+from pathlib import Path
+from unittest.mock import MagicMock
+
+from codebase_rag.constants import KEY_QUALIFIED_NAME, NodeLabel
+from codebase_rag.tests.conftest import create_and_run_updater, get_nodes
+
+
+def test_rust_method_on_primitive_impl_target_is_captured(
+ temp_repo: Path, mock_ingestor: MagicMock
+) -> None:
+ project = temp_repo / "rs_prim"
+ (project / "src").mkdir(parents=True)
+ (project / "Cargo.toml").write_text(
+ encoding="utf-8", data='[package]\nname = "rs_prim"\nversion = "0.1.0"\n'
+ )
+ (project / "src" / "lib.rs").write_text(
+ encoding="utf-8",
+ data="""pub enum Foo { A, B }
+
+impl From for u8 {
+ fn from(value: Foo) -> Self {
+ match value {
+ Foo::A => 0,
+ Foo::B => 1,
+ }
+ }
+}
+""",
+ )
+ create_and_run_updater(project, mock_ingestor, skip_if_missing="rust")
+
+ method_qns = {
+ str(node[0][1].get(KEY_QUALIFIED_NAME))
+ for node in get_nodes(mock_ingestor, NodeLabel.METHOD)
+ }
+ assert any(qn.endswith(".u8.from") for qn in method_qns), (
+ f"from() on impl-for-u8 not captured: {method_qns}"
+ )
diff --git a/codebase_rag/tests/test_rust_inheritance_edges.py b/codebase_rag/tests/test_rust_inheritance_edges.py
new file mode 100644
index 000000000..88bd34c58
--- /dev/null
+++ b/codebase_rag/tests/test_rust_inheritance_edges.py
@@ -0,0 +1,49 @@
+# (H) Rust inheritance was uncaptured: `impl Trait for Type` means Type
+# (H) IMPLEMENTS Trait, and a supertrait bound `trait Sub: Super` means Sub
+# (H) INHERITS Super. cgr emitted neither (impl blocks and trait bounds were
+# (H) never turned into inheritance edges).
+from __future__ import annotations
+
+from pathlib import Path
+from unittest.mock import MagicMock
+
+from codebase_rag.constants import RelationshipType
+from codebase_rag.tests.conftest import create_and_run_updater, get_relationships
+
+_RS = """\
+pub trait Shape {}
+pub trait Drawable: Shape {}
+
+pub struct Circle;
+
+impl Shape for Circle {}
+impl Drawable for Circle {}
+"""
+
+
+def _pairs(mock_ingestor: MagicMock, rel: str) -> set[tuple[str, str]]:
+ return {
+ (call[0][0][2], call[0][2][2]) for call in get_relationships(mock_ingestor, rel)
+ }
+
+
+def test_rust_impl_and_supertrait_edges(
+ temp_repo: Path, mock_ingestor: MagicMock
+) -> None:
+ project = temp_repo / "rs_inh"
+ (project / "src").mkdir(parents=True)
+ (project / "Cargo.toml").write_text(
+ encoding="utf-8", data='[package]\nname = "rs_inh"\nversion = "0.1.0"\n'
+ )
+ (project / "src" / "lib.rs").write_text(encoding="utf-8", data=_RS)
+ create_and_run_updater(project, mock_ingestor, skip_if_missing="rust")
+
+ inherits = _pairs(mock_ingestor, RelationshipType.INHERITS.value)
+ implements = _pairs(mock_ingestor, RelationshipType.IMPLEMENTS.value)
+ base = "rs_inh.src.lib"
+
+ # (H) impl Trait for Type -> Type IMPLEMENTS Trait.
+ assert (f"{base}.Circle", f"{base}.Shape") in implements, implements
+ assert (f"{base}.Circle", f"{base}.Drawable") in implements, implements
+ # (H) Supertrait bound -> Sub INHERITS Super.
+ assert (f"{base}.Drawable", f"{base}.Shape") in inherits, inherits
diff --git a/codebase_rag/tests/test_rust_inheritance_oracle.py b/codebase_rag/tests/test_rust_inheritance_oracle.py
new file mode 100644
index 000000000..3204a224e
--- /dev/null
+++ b/codebase_rag/tests/test_rust_inheritance_oracle.py
@@ -0,0 +1,59 @@
+# (H) Covers Rust inheritance-edge validation: cgr's INHERITS (supertrait bound)
+# (H) and IMPLEMENTS (`impl Trait for Type`) edges are graded against the syn
+# (H) oracle, by (source node, base SIMPLE NAME).
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+
+from codebase_rag import constants as cs
+from codebase_rag.parser_loader import load_parsers
+from evals import constants as ec
+from evals.cgr_graph import extract_cgr_rust_graph
+from evals.oracles import run_rust_oracle, rust_available
+from evals.score import score_name_edge_types
+
+RS_SRC = """\
+pub trait Shape {}
+pub trait Drawable: Shape {}
+
+pub struct Circle;
+
+impl Shape for Circle {}
+impl Drawable for Circle {}
+"""
+
+
+def _require_rust() -> None:
+ if not rust_available():
+ pytest.skip("cargo toolchain not available")
+ if cs.SupportedLanguage.RUST not in load_parsers()[0]:
+ pytest.skip("rust parser not available")
+
+
+def test_cgr_matches_syn_oracle_on_inheritance_edges(tmp_path: Path) -> None:
+ _require_rust()
+ project = tmp_path / "rs_inh_edge"
+ (project / "src").mkdir(parents=True)
+ (project / "Cargo.toml").write_text(
+ encoding="utf-8", data='[package]\nname = "rs_inh_edge"\nversion = "0.1.0"\n'
+ )
+ (project / "src" / "lib.rs").write_text(RS_SRC, encoding="utf-8")
+
+ cgr = extract_cgr_rust_graph(project, project.name)
+ oracle = run_rust_oracle(project)
+
+ result = score_name_edge_types(cgr, oracle, ec.INHERITANCE_NAME_EDGE_TYPES)
+ by_label = {row["label"]: row for row in result.rows}
+ for label in (
+ cs.RelationshipType.INHERITS.value,
+ cs.RelationshipType.IMPLEMENTS.value,
+ ):
+ row = by_label.get(label)
+ assert row is not None, (label, by_label, result.diff)
+ assert row["precision"] == 1.0 and row["recall"] == 1.0, (
+ label,
+ row,
+ result.diff,
+ )
diff --git a/codebase_rag/tests/test_rust_nested_module_containment.py b/codebase_rag/tests/test_rust_nested_module_containment.py
new file mode 100644
index 000000000..7a924ed4c
--- /dev/null
+++ b/codebase_rag/tests/test_rust_nested_module_containment.py
@@ -0,0 +1,85 @@
+# (H) Rust nested-module containment. cgr qualifies items inside `mod inner`
+# (H) with the module path (proj...inner.X), but used to (a) DEFINE them from the
+# (H) FILE module while leaving the inner Module node an orphan, and (b) qualify
+# (H) impl methods inside the mod against the file module, producing a phantom
+# (H) DEFINES_METHOD parent that never matched the real type node. Containment
+# (H) must be module-nested: file module -> inner module -> its items, and an
+# (H) impl method binds to the type under its enclosing module path.
+from __future__ import annotations
+
+from pathlib import Path
+from unittest.mock import MagicMock
+
+from codebase_rag.constants import KEY_QUALIFIED_NAME, NodeLabel, RelationshipType
+from codebase_rag.tests.conftest import (
+ create_and_run_updater,
+ get_nodes,
+ get_relationships,
+)
+
+_RS = """pub mod inner {
+ pub fn helper() -> i32 { 1 }
+
+ pub struct Widget { w: i32 }
+
+ impl Widget {
+ pub fn build(&self) -> i32 { self.w }
+ }
+}
+"""
+
+
+def _project(temp_repo: Path) -> Path:
+ project = temp_repo / "rs_mod"
+ (project / "src").mkdir(parents=True)
+ (project / "Cargo.toml").write_text(
+ encoding="utf-8", data='[package]\nname = "rs_mod"\nversion = "0.1.0"\n'
+ )
+ (project / "src" / "lib.rs").write_text(encoding="utf-8", data=_RS)
+ return project
+
+
+def _defines_pairs(mock_ingestor: MagicMock) -> set[tuple[str, str, str]]:
+ # (H) (parent_label, parent_qn, child_qn) for DEFINES edges.
+ return {
+ (call[0][0][0], call[0][0][2], call[0][2][2])
+ for call in get_relationships(mock_ingestor, RelationshipType.DEFINES.value)
+ }
+
+
+def test_rust_nested_module_is_module_nested(
+ temp_repo: Path, mock_ingestor: MagicMock
+) -> None:
+ create_and_run_updater(_project(temp_repo), mock_ingestor, skip_if_missing="rust")
+ file_mod = "rs_mod.src.lib"
+ inner = f"{file_mod}.inner"
+ pairs = _defines_pairs(mock_ingestor)
+
+ # (H) file module DEFINES the inner module (no longer an orphan node).
+ assert (NodeLabel.MODULE.value, file_mod, inner) in pairs, pairs
+ # (H) inner module DEFINES its own items, not the file module.
+ assert (NodeLabel.MODULE.value, inner, f"{inner}.helper") in pairs, pairs
+ assert (NodeLabel.MODULE.value, inner, f"{inner}.Widget") in pairs, pairs
+
+
+def test_rust_impl_method_in_module_binds_to_nested_type(
+ temp_repo: Path, mock_ingestor: MagicMock
+) -> None:
+ create_and_run_updater(_project(temp_repo), mock_ingestor, skip_if_missing="rust")
+ inner = "rs_mod.src.lib.inner"
+
+ method_qns = {
+ str(node[0][1].get(KEY_QUALIFIED_NAME))
+ for node in get_nodes(mock_ingestor, NodeLabel.METHOD)
+ }
+ assert f"{inner}.Widget.build" in method_qns, method_qns
+
+ defines_method = {
+ (call[0][0][2], call[0][2][2])
+ for call in get_relationships(
+ mock_ingestor, RelationshipType.DEFINES_METHOD.value
+ )
+ }
+ assert (f"{inner}.Widget", f"{inner}.Widget.build") in defines_method, (
+ defines_method
+ )
diff --git a/codebase_rag/tests/test_rust_node_type.py b/codebase_rag/tests/test_rust_node_type.py
new file mode 100644
index 000000000..edfa95e13
--- /dev/null
+++ b/codebase_rag/tests/test_rust_node_type.py
@@ -0,0 +1,99 @@
+from __future__ import annotations
+
+from pathlib import Path
+from unittest.mock import MagicMock
+
+import pytest
+
+from codebase_rag import constants as cs
+from codebase_rag.parsers.class_ingest.node_type import determine_node_type
+from codebase_rag.tests.conftest import (
+ create_mock_node,
+ get_node_names,
+ run_updater,
+)
+from codebase_rag.types_defs import NodeType
+
+
+@pytest.mark.parametrize(
+ ("ts_node_type", "expected"),
+ [
+ (cs.TS_RS_ENUM_ITEM, NodeType.ENUM),
+ (cs.TS_RS_TRAIT_ITEM, NodeType.INTERFACE),
+ (cs.TS_RS_TYPE_ITEM, NodeType.TYPE),
+ (cs.TS_RS_UNION_ITEM, NodeType.UNION),
+ (cs.TS_RS_STRUCT_ITEM, NodeType.CLASS),
+ ],
+)
+def test_determine_node_type_rust(ts_node_type: str, expected: NodeType) -> None:
+ node = create_mock_node(ts_node_type)
+ result = determine_node_type(node, "Foo", "crate::Foo", cs.SupportedLanguage.RUST)
+ assert result == expected
+
+
+@pytest.fixture
+def rust_node_type_project(temp_repo: Path) -> Path:
+ project_path = temp_repo / "rust_node_type_test"
+ project_path.mkdir()
+ (project_path / "Cargo.toml").write_text(
+ encoding="utf-8",
+ data='[package]\nname = "rust_node_type_test"\nversion = "0.1.0"\n',
+ )
+ (project_path / "src").mkdir()
+ (project_path / "src" / "lib.rs").write_text(encoding="utf-8", data="")
+ (project_path / "types.rs").write_text(
+ encoding="utf-8",
+ data=(
+ "pub enum Color { Red, Green, Blue }\n"
+ "pub trait Drawable { fn draw(&self); }\n"
+ "pub type Pair = (i32, i32);\n"
+ "pub union IntOrFloat { i: i32, f: f32 }\n"
+ "pub struct Point { pub x: f64, pub y: f64 }\n"
+ ),
+ )
+ return project_path
+
+
+def test_rust_enum_label(
+ rust_node_type_project: Path, mock_ingestor: MagicMock
+) -> None:
+ run_updater(rust_node_type_project, mock_ingestor, skip_if_missing="rust")
+ enum_names = get_node_names(mock_ingestor, NodeType.ENUM)
+ assert len(enum_names) == 1
+ assert enum_names.pop().endswith(".Color")
+
+
+def test_rust_trait_label(
+ rust_node_type_project: Path, mock_ingestor: MagicMock
+) -> None:
+ run_updater(rust_node_type_project, mock_ingestor, skip_if_missing="rust")
+ interface_names = get_node_names(mock_ingestor, NodeType.INTERFACE)
+ assert len(interface_names) == 1
+ assert interface_names.pop().endswith(".Drawable")
+
+
+def test_rust_type_alias_label(
+ rust_node_type_project: Path, mock_ingestor: MagicMock
+) -> None:
+ run_updater(rust_node_type_project, mock_ingestor, skip_if_missing="rust")
+ type_names = get_node_names(mock_ingestor, NodeType.TYPE)
+ assert len(type_names) == 1
+ assert type_names.pop().endswith(".Pair")
+
+
+def test_rust_union_label(
+ rust_node_type_project: Path, mock_ingestor: MagicMock
+) -> None:
+ run_updater(rust_node_type_project, mock_ingestor, skip_if_missing="rust")
+ union_names = get_node_names(mock_ingestor, NodeType.UNION)
+ assert len(union_names) == 1
+ assert union_names.pop().endswith(".IntOrFloat")
+
+
+def test_rust_struct_label(
+ rust_node_type_project: Path, mock_ingestor: MagicMock
+) -> None:
+ run_updater(rust_node_type_project, mock_ingestor, skip_if_missing="rust")
+ class_names = get_node_names(mock_ingestor, NodeType.CLASS)
+ assert len(class_names) == 1
+ assert class_names.pop().endswith(".Point")
diff --git a/codebase_rag/tests/test_rust_receiver_resolution.py b/codebase_rag/tests/test_rust_receiver_resolution.py
new file mode 100644
index 000000000..cf610fb7e
--- /dev/null
+++ b/codebase_rag/tests/test_rust_receiver_resolution.py
@@ -0,0 +1,588 @@
+from pathlib import Path
+
+from evals.cgr_graph import _capture
+
+
+def _calls(tmp_path: Path) -> set[tuple[str, str]]:
+ ingestor = _capture(tmp_path, "crate")
+ return {
+ (str(from_val), str(to_val))
+ for _fl, from_val, rel, _tl, to_val in ingestor.rels
+ if rel == "CALLS"
+ }
+
+
+def _make_crate(root: Path, body: str) -> None:
+ root.mkdir(parents=True, exist_ok=True)
+ (root / "lib.rs").write_text(body, encoding="utf-8")
+
+
+# (H) Every fixture defines the called method name on more than one type (an `Aaa`
+# (H) decoy sorting before the real type) so the name-only trie fallback is ambiguous
+# (H) and would pick the wrong `Aaa` alphabetically: only real receiver-type inference
+# (H) produces the correct edge. Mirrors mini-redis, where `apply`/`run`/`new`/
+# (H) `into_frame` live on many command types.
+
+
+def test_self_receiver_dispatch(tmp_path: Path) -> None:
+ # (H) `self.accept()` inside a method must resolve to the impl target's method.
+ _make_crate(
+ tmp_path,
+ "pub struct Aaa {}\n"
+ "impl Aaa {\n"
+ " fn accept(&self) -> i32 { 2 }\n"
+ "}\n"
+ "pub struct Listener {}\n"
+ "impl Listener {\n"
+ " fn accept(&self) -> i32 { 1 }\n"
+ " fn run(&self) -> i32 { self.accept() }\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.lib.Listener.run", "crate.lib.Listener.accept") in calls
+ assert ("crate.lib.Listener.run", "crate.lib.Aaa.accept") not in calls
+
+
+def test_struct_literal_binding_dispatch(tmp_path: Path) -> None:
+ # (H) `let s = Listener {}; s.run()` binds s to Listener via the struct literal.
+ _make_crate(
+ tmp_path,
+ "pub struct Aaa {}\n"
+ "impl Aaa {\n"
+ " fn run(&self) -> i32 { 2 }\n"
+ "}\n"
+ "pub struct Listener {}\n"
+ "impl Listener {\n"
+ " fn run(&self) -> i32 { 1 }\n"
+ "}\n"
+ "pub fn go() -> i32 {\n"
+ " let mut server = Listener {};\n"
+ " server.run()\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.lib.go", "crate.lib.Listener.run") in calls
+ assert ("crate.lib.go", "crate.lib.Aaa.run") not in calls
+
+
+def test_field_type_receiver_dispatch(tmp_path: Path) -> None:
+ # (H) `self.shutdown.is_shutdown()` resolves through the struct field's type.
+ _make_crate(
+ tmp_path,
+ "pub struct Aaa {}\n"
+ "impl Aaa {\n"
+ " fn is_shutdown(&self) -> bool { false }\n"
+ "}\n"
+ "pub struct Shutdown {}\n"
+ "impl Shutdown {\n"
+ " fn is_shutdown(&self) -> bool { true }\n"
+ "}\n"
+ "pub struct Handler { shutdown: Shutdown }\n"
+ "impl Handler {\n"
+ " fn run(&self) -> bool { self.shutdown.is_shutdown() }\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.lib.Handler.run", "crate.lib.Shutdown.is_shutdown") in calls
+ assert ("crate.lib.Handler.run", "crate.lib.Aaa.is_shutdown") not in calls
+
+
+def test_guard_deref_field_hop_binding_dispatch(tmp_path: Path) -> None:
+ # (H) `let state = self.shared.state.lock().unwrap()` inside impl Db: type `state`
+ # (H) by hopping self -> Db, field shared: Arc -> Shared, field
+ # (H) state: Mutex -> State (deref through Arc/Mutex), then lock()/unwrap()
+ # (H) as guard-accessor identities -> State. `state.next_expiration()` must then
+ # (H) dispatch to State.next_expiration (mini-redis Db.set).
+ _make_crate(
+ tmp_path,
+ "use std::sync::{Arc, Mutex};\n"
+ "pub struct Aaa {}\n"
+ "impl Aaa {\n fn next_expiration(&self) -> i32 { 2 }\n}\n"
+ "pub struct State {}\n"
+ "impl State {\n fn next_expiration(&self) -> i32 { 1 }\n}\n"
+ "pub struct Shared { state: Mutex }\n"
+ "pub struct Db { shared: Arc }\n"
+ "impl Db {\n"
+ " fn set(&self) -> i32 {\n"
+ " let state = self.shared.state.lock().unwrap();\n"
+ " state.next_expiration()\n"
+ " }\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.lib.Db.set", "crate.lib.State.next_expiration") in calls
+ assert ("crate.lib.Db.set", "crate.lib.Aaa.next_expiration") not in calls
+
+
+def test_guard_wrapper_local_not_erased_to_inner(tmp_path: Path) -> None:
+ # (H) A Mutex/RwLock does NOT deref-coerce: a bare `let m: Mutex` receiver
+ # (H) can only reach Inner via a lock/borrow hop, so `m` must stay typed as the
+ # (H) wrapper -- NOT erased to Inner (which would mis-resolve a direct `m.work()`
+ # (H) to Inner.work). `work` is defined on two types so the trie can't mask a
+ # (H) precise mis-resolution.
+ _make_crate(
+ tmp_path,
+ "use std::sync::Mutex;\n"
+ "pub struct Aaa {}\n"
+ "impl Aaa {\n fn work(&self) -> i32 { 2 }\n}\n"
+ "pub struct Inner {}\n"
+ "impl Inner {\n fn work(&self) -> i32 { 1 }\n}\n"
+ "pub fn go(m: Mutex) -> i32 {\n"
+ " m.work()\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ # (H) m is a Mutex receiver; a direct m.work() must NOT bind to Inner.work
+ assert ("crate.lib.go", "crate.lib.Inner.work") not in calls
+
+
+def test_guard_field_direct_call_not_erased_to_inner(tmp_path: Path) -> None:
+ # (H) A guard-wrapped FIELD keeps its wrapper type in the field map: a DIRECT
+ # (H) `self.state.work()` (no lock) must NOT resolve to Inner.work. The inner is
+ # (H) applied only when a lock/borrow accessor intervenes (see the field-hop test).
+ _make_crate(
+ tmp_path,
+ "use std::sync::Mutex;\n"
+ "pub struct Aaa {}\n"
+ "impl Aaa {\n fn work(&self) -> i32 { 2 }\n}\n"
+ "pub struct Inner {}\n"
+ "impl Inner {\n fn work(&self) -> i32 { 1 }\n}\n"
+ "pub struct Holder { state: Mutex }\n"
+ "impl Holder {\n"
+ " fn go(&self) -> i32 {\n"
+ " self.state.work()\n"
+ " }\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.lib.Holder.go", "crate.lib.Inner.work") not in calls
+
+
+def test_closure_captured_local_receiver_dispatch(tmp_path: Path) -> None:
+ # (H) A closure captures a local of its enclosing scope: `state` is typed in
+ # (H) Db.set (guard-deref field hop) but `state.next_expiration()` lives inside
+ # (H) `expire.map(|_| state.next_expiration())`. The closure's own var map only
+ # (H) sees the closure body, so the captured `state` is untyped unless the closure
+ # (H) inherits the enclosing scope's locals. Mirrors mini-redis Db.set.
+ _make_crate(
+ tmp_path,
+ "use std::sync::{Arc, Mutex};\n"
+ "pub struct Aaa {}\n"
+ "impl Aaa {\n fn next_expiration(&self) -> i32 { 2 }\n}\n"
+ "pub struct State {}\n"
+ "impl State {\n fn next_expiration(&self) -> i32 { 1 }\n}\n"
+ "pub struct Shared { state: Mutex }\n"
+ "pub struct Db { shared: Arc }\n"
+ "impl Db {\n"
+ " fn set(&self, expire: Option) -> i32 {\n"
+ " let state = self.shared.state.lock().unwrap();\n"
+ " expire.map(|_when| state.next_expiration()).unwrap_or(0)\n"
+ " }\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ assert any(
+ frm.startswith("crate.lib.Db.set") and to == "crate.lib.State.next_expiration"
+ for frm, to in calls
+ ), "closure call did not resolve to State.next_expiration via captured local"
+ assert not any(to == "crate.lib.Aaa.next_expiration" for _frm, to in calls)
+
+
+def test_named_nested_fn_calls_not_bubbled_to_enclosing(tmp_path: Path) -> None:
+ # (H) A NAMED nested `fn inner()` gets its own caller node and must OWN its body's
+ # (H) calls: `inner`'s `w.work()` belongs to inner only, NOT also to the enclosing
+ # (H) `outer` (a spurious duplicate edge). Anonymous closures still bubble; named
+ # (H) nested fns do not. The caller qn is the one the definition pass REGISTERED
+ # (H) (crate.lib.inner is the actual Function node); the old crate.lib.outer.inner
+ # (H) attribution was a phantom FROM endpoint the database dropped (issue #652).
+ _make_crate(
+ tmp_path,
+ "pub struct Worker {}\n"
+ "impl Worker {\n fn work(&self) -> i32 { 1 }\n}\n"
+ "pub fn outer(w: Worker) -> i32 {\n"
+ " fn inner(w: &Worker) -> i32 { w.work() }\n"
+ " inner(&w)\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.lib.inner", "crate.lib.Worker.work") in calls
+ assert ("crate.lib.outer", "crate.lib.Worker.work") not in calls
+
+
+def test_let_assoc_call_return_type_dispatch(tmp_path: Path) -> None:
+ # (H) `let cmd = Command::from_frame(f); cmd.apply()` types cmd from the
+ # (H) associated function's return type (Command).
+ _make_crate(
+ tmp_path,
+ "pub struct Aaa {}\n"
+ "impl Aaa {\n"
+ " fn apply(&self) -> i32 { 2 }\n"
+ "}\n"
+ "pub struct Command {}\n"
+ "impl Command {\n"
+ " pub fn from_frame(f: i32) -> Command { Command {} }\n"
+ " pub fn apply(&self) -> i32 { 1 }\n"
+ "}\n"
+ "pub fn go() -> i32 {\n"
+ " let cmd = Command::from_frame(0);\n"
+ " cmd.apply()\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.lib.go", "crate.lib.Command.apply") in calls
+ assert ("crate.lib.go", "crate.lib.Aaa.apply") not in calls
+
+
+def test_let_assoc_call_result_wrapper_return_type(tmp_path: Path) -> None:
+ # (H) A Result return type is stripped to Command so the unwrapped
+ # (H) local still dispatches.
+ _make_crate(
+ tmp_path,
+ "pub struct Aaa {}\n"
+ "impl Aaa {\n"
+ " fn apply(&self) -> i32 { 2 }\n"
+ "}\n"
+ "pub struct Command {}\n"
+ "impl Command {\n"
+ " pub fn from_frame(f: i32) -> crate::Result { Ok(Command {}) }\n"
+ " pub fn apply(&self) -> i32 { 1 }\n"
+ "}\n"
+ "pub fn go() -> i32 {\n"
+ " let cmd = Command::from_frame(0).unwrap();\n"
+ " cmd.apply()\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.lib.go", "crate.lib.Command.apply") in calls
+ assert ("crate.lib.go", "crate.lib.Aaa.apply") not in calls
+
+
+def test_enum_match_binding_dispatch(tmp_path: Path) -> None:
+ # (H) `Get(cmd) => cmd.apply()` binds cmd to the variant's payload type (Get).
+ _make_crate(
+ tmp_path,
+ "pub struct Aaa {}\n"
+ "impl Aaa {\n"
+ " fn apply(&self) -> i32 { 2 }\n"
+ "}\n"
+ "pub struct Get {}\n"
+ "impl Get {\n"
+ " fn apply(&self) -> i32 { 1 }\n"
+ "}\n"
+ "pub enum Command { Get(Get) }\n"
+ "impl Command {\n"
+ " fn apply(&self) -> i32 {\n"
+ " use Command::*;\n"
+ " match self {\n"
+ " Get(cmd) => cmd.apply(),\n"
+ " }\n"
+ " }\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.lib.Command.apply", "crate.lib.Get.apply") in calls
+ # (H) must not mis-resolve to the enclosing type's same-named method
+ assert ("crate.lib.Command.apply", "crate.lib.Command.apply") not in calls
+
+
+def test_enum_match_multiarm_binding_shadowing(tmp_path: Path) -> None:
+ # (H) Every arm reuses the binding name `cmd` for a DIFFERENT variant type. A flat
+ # (H) var_types map keeps only the last arm's type, so all `cmd.apply()` collapse
+ # (H) to the last variant. Each arm's `cmd.apply()` must dispatch to ITS OWN
+ # (H) variant's method (mini-redis Command.apply dispatch).
+ _make_crate(
+ tmp_path,
+ "pub struct Get {}\n"
+ "impl Get {\n fn apply(&self) -> i32 { 1 }\n}\n"
+ "pub struct Set {}\n"
+ "impl Set {\n fn apply(&self) -> i32 { 2 }\n}\n"
+ "pub struct Ping {}\n"
+ "impl Ping {\n fn apply(&self) -> i32 { 3 }\n}\n"
+ "pub enum Command { Get(Get), Set(Set), Ping(Ping) }\n"
+ "impl Command {\n"
+ " fn apply(&self) -> i32 {\n"
+ " use Command::*;\n"
+ " match self {\n"
+ " Get(cmd) => cmd.apply(),\n"
+ " Set(cmd) => cmd.apply(),\n"
+ " Ping(cmd) => cmd.apply(),\n"
+ " }\n"
+ " }\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.lib.Command.apply", "crate.lib.Get.apply") in calls
+ assert ("crate.lib.Command.apply", "crate.lib.Set.apply") in calls
+ assert ("crate.lib.Command.apply", "crate.lib.Ping.apply") in calls
+
+
+def test_nested_match_binding_not_leaked_to_outer_arm(tmp_path: Path) -> None:
+ # (H) A nested `match inner { Foo(x) => ... }` rebinds `x` only within its own arm.
+ # (H) The outer arm's `x.tag()` (x = the Bar param) must stay Bar.tag -- the nested
+ # (H) Foo binding must NOT be scoped to the whole outer arm (which would mis-overlay
+ # (H) the outer call to Foo.tag).
+ _make_crate(
+ tmp_path,
+ "pub struct Bar {}\n"
+ "impl Bar {\n fn tag(&self) -> i32 { 1 }\n}\n"
+ "pub struct Foo {}\n"
+ "impl Foo {\n fn tag(&self) -> i32 { 2 }\n}\n"
+ "pub enum Inner { Foo(Foo) }\n"
+ "pub enum E { A(Bar) }\n"
+ "impl E {\n"
+ " fn run(&self, x: Bar, inner: Inner) -> i32 {\n"
+ " use Inner::*;\n"
+ " match self {\n"
+ " E::A(_) => {\n"
+ " match inner {\n"
+ " Foo(x) => x.tag(),\n"
+ " }\n"
+ " x.tag()\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ # (H) nested arm's x:Foo resolves to Foo.tag; outer arm's x (param Bar) to Bar.tag
+ assert ("crate.lib.E.run", "crate.lib.Foo.tag") in calls
+ assert ("crate.lib.E.run", "crate.lib.Bar.tag") in calls
+
+
+def test_assoc_fn_chain_dispatch(tmp_path: Path) -> None:
+ # (H) `Ping::new(msg).into_frame()` resolves into_frame on the type Ping::new
+ # (H) returns (Ping).
+ _make_crate(
+ tmp_path,
+ "pub struct Frame {}\n"
+ "pub struct Aaa {}\n"
+ "impl Aaa {\n"
+ " pub fn new(msg: i32) -> Aaa { Aaa {} }\n"
+ " pub fn into_frame(self) -> Frame { Frame {} }\n"
+ "}\n"
+ "pub struct Ping {}\n"
+ "impl Ping {\n"
+ " pub fn new(msg: i32) -> Ping { Ping {} }\n"
+ " pub fn into_frame(self) -> Frame { Frame {} }\n"
+ "}\n"
+ "pub fn go() -> i32 {\n"
+ " let frame = Ping::new(0).into_frame();\n"
+ " 1\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.lib.go", "crate.lib.Ping.new") in calls
+ assert ("crate.lib.go", "crate.lib.Ping.into_frame") in calls
+ assert ("crate.lib.go", "crate.lib.Aaa.into_frame") not in calls
+
+
+def test_imported_assoc_call_return_type_dispatch(tmp_path: Path) -> None:
+ # (H) `use crate::cmd::Command; let cmd = Command::from_frame(f); cmd.apply()`:
+ # (H) the type is IMPORTED, so its import target is a raw `::`-path, not a registry
+ # (H) qn. The call-return binding must resolve it to the real class node
+ # (H) (crate.cmd.Command) to look up from_frame's recorded return type -- else it
+ # (H) falls back to the ambiguous trie path.
+ tmp_path.mkdir(parents=True, exist_ok=True)
+ (tmp_path / "lib.rs").write_text("pub mod cmd;\npub mod app;\n", encoding="utf-8")
+ (tmp_path / "cmd.rs").write_text(
+ "pub struct Aaa {}\n"
+ "impl Aaa {\n"
+ " pub fn apply(&self) -> i32 { 2 }\n"
+ "}\n"
+ "pub struct Command {}\n"
+ "impl Command {\n"
+ " pub fn from_frame(f: i32) -> Command { Command {} }\n"
+ " pub fn apply(&self) -> i32 { 1 }\n"
+ "}\n",
+ encoding="utf-8",
+ )
+ (tmp_path / "app.rs").write_text(
+ "use crate::cmd::Command;\n"
+ "pub fn go() -> i32 {\n"
+ " let cmd = Command::from_frame(0);\n"
+ " cmd.apply()\n"
+ "}\n",
+ encoding="utf-8",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.app.go", "crate.cmd.Command.apply") in calls
+ assert ("crate.app.go", "crate.cmd.Aaa.apply") not in calls
+
+
+def test_reference_return_type_chained_dispatch(tmp_path: Path) -> None:
+ # (H) A method returning a reference (`fn frame(&self) -> &Frame`) must still
+ # (H) yield the referent type so a chained call (`self.frame().push_int()`)
+ # (H) resolves to Frame.push_int, not the ambiguous trie pick.
+ _make_crate(
+ tmp_path,
+ "pub struct Aaa {}\n"
+ "impl Aaa {\n"
+ " fn push_int(&self) -> i32 { 2 }\n"
+ "}\n"
+ "pub struct Frame {}\n"
+ "impl Frame {\n"
+ " fn push_int(&self) -> i32 { 1 }\n"
+ "}\n"
+ "pub struct Holder {}\n"
+ "impl Holder {\n"
+ " fn frame(&self) -> &Frame { &Frame {} }\n"
+ " fn go(&self) -> i32 { self.frame().push_int() }\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.lib.Holder.go", "crate.lib.Frame.push_int") in calls
+ assert ("crate.lib.Holder.go", "crate.lib.Aaa.push_int") not in calls
+
+
+def test_imported_type_disambiguated_by_path(tmp_path: Path) -> None:
+ # (H) Two `Command` types in different modules whose `mk()` returns DIFFERENT
+ # (H) types (cmd.Command -> Real, other.Command -> Fake). `use crate::cmd::Command`
+ # (H) must resolve the call-return base to crate.cmd.Command so `x.run()` lands on
+ # (H) Real.run. A simple-name-only match would pick the alphabetically-first
+ # (H) crate.aaa.Command, type x as Fake, and mis-resolve to Fake.run.
+ tmp_path.mkdir(parents=True, exist_ok=True)
+ (tmp_path / "lib.rs").write_text(
+ "pub mod aaa;\npub mod cmd;\npub mod types;\npub mod app;\n",
+ encoding="utf-8",
+ )
+ (tmp_path / "types.rs").write_text(
+ "pub struct Real {}\n"
+ "impl Real {\n"
+ " pub fn run(&self) -> i32 { 1 }\n"
+ "}\n"
+ "pub struct Fake {}\n"
+ "impl Fake {\n"
+ " pub fn run(&self) -> i32 { 2 }\n"
+ "}\n",
+ encoding="utf-8",
+ )
+ (tmp_path / "aaa.rs").write_text(
+ "use crate::types::Fake;\n"
+ "pub struct Command {}\n"
+ "impl Command {\n"
+ " pub fn mk() -> Fake { Fake {} }\n"
+ "}\n",
+ encoding="utf-8",
+ )
+ (tmp_path / "cmd.rs").write_text(
+ "use crate::types::Real;\n"
+ "pub struct Command {}\n"
+ "impl Command {\n"
+ " pub fn mk() -> Real { Real {} }\n"
+ "}\n",
+ encoding="utf-8",
+ )
+ (tmp_path / "app.rs").write_text(
+ "use crate::cmd::Command;\n"
+ "pub fn go() -> i32 {\n"
+ " let x = Command::mk();\n"
+ " x.run()\n"
+ "}\n",
+ encoding="utf-8",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.app.go", "crate.types.Real.run") in calls
+ assert ("crate.app.go", "crate.types.Fake.run") not in calls
+
+
+def test_fully_qualified_inline_assoc_call_dispatch(tmp_path: Path) -> None:
+ # (H) A fully-qualified inline associated call with NO `use` import
+ # (H) (`let x = crate::cmd::Command::mk()`) must keep the qualified path so the
+ # (H) return-type base resolves to crate.cmd.Command (mk -> Real), not the
+ # (H) alphabetically-first crate.aaa.Command (mk -> Fake).
+ tmp_path.mkdir(parents=True, exist_ok=True)
+ (tmp_path / "lib.rs").write_text(
+ "pub mod aaa;\npub mod cmd;\npub mod types;\npub mod app;\n",
+ encoding="utf-8",
+ )
+ (tmp_path / "types.rs").write_text(
+ "pub struct Real {}\n"
+ "impl Real {\n"
+ " pub fn run(&self) -> i32 { 1 }\n"
+ "}\n"
+ "pub struct Fake {}\n"
+ "impl Fake {\n"
+ " pub fn run(&self) -> i32 { 2 }\n"
+ "}\n",
+ encoding="utf-8",
+ )
+ (tmp_path / "aaa.rs").write_text(
+ "use crate::types::Fake;\n"
+ "pub struct Command {}\n"
+ "impl Command {\n"
+ " pub fn mk() -> Fake { Fake {} }\n"
+ "}\n",
+ encoding="utf-8",
+ )
+ (tmp_path / "cmd.rs").write_text(
+ "use crate::types::Real;\n"
+ "pub struct Command {}\n"
+ "impl Command {\n"
+ " pub fn mk() -> Real { Real {} }\n"
+ "}\n",
+ encoding="utf-8",
+ )
+ (tmp_path / "app.rs").write_text(
+ "pub fn go() -> i32 {\n"
+ " let x = crate::cmd::Command::mk();\n"
+ " x.run()\n"
+ "}\n",
+ encoding="utf-8",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.app.go", "crate.types.Real.run") in calls
+ assert ("crate.app.go", "crate.types.Fake.run") not in calls
+
+
+def test_macro_buried_receiver_call_dispatch(tmp_path: Path) -> None:
+ # (H) A `receiver.method()` call buried inside a macro token stream
+ # (H) (`sel! { res = server.run() => {} }`, like tokio::select!) loses its
+ # (H) field_expression structure -- the receiver `server` becomes a loose token.
+ # (H) The reconstructed call must be `server.run` so it dispatches to the local's
+ # (H) type (Listener.run), NOT the same-module free fn `run` (a false self-edge
+ # (H) that severed the whole server/command cluster in mini-redis).
+ _make_crate(
+ tmp_path,
+ "pub struct Listener {}\n"
+ "impl Listener {\n"
+ " fn run(&self) -> i32 { 1 }\n"
+ "}\n"
+ "pub fn run(server: Listener) -> i32 {\n"
+ " sel! {\n"
+ " res = server.run() => {}\n"
+ " }\n"
+ " 0\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.lib.run", "crate.lib.Listener.run") in calls
+ # (H) must not mis-resolve to the same-module free function (self-edge)
+ assert ("crate.lib.run", "crate.lib.run") not in calls
+
+
+def test_macro_buried_field_hop_call_dispatch(tmp_path: Path) -> None:
+ # (H) A field-hop receiver buried in a macro (`self.shutdown.recv()`) must
+ # (H) reconstruct the full chain so it hops self -> field type -> method.
+ _make_crate(
+ tmp_path,
+ "pub struct Aaa {}\n"
+ "impl Aaa {\n"
+ " fn recv(&self) -> i32 { 2 }\n"
+ "}\n"
+ "pub struct Shutdown {}\n"
+ "impl Shutdown {\n"
+ " fn recv(&self) -> i32 { 1 }\n"
+ "}\n"
+ "pub struct Handler { shutdown: Shutdown }\n"
+ "impl Handler {\n"
+ " fn run(&self) -> i32 {\n"
+ " sel! {\n"
+ " x = self.shutdown.recv() => {}\n"
+ " }\n"
+ " 0\n"
+ " }\n"
+ "}\n",
+ )
+ calls = _calls(tmp_path)
+ assert ("crate.lib.Handler.run", "crate.lib.Shutdown.recv") in calls
+ assert ("crate.lib.Handler.run", "crate.lib.Aaa.recv") not in calls
diff --git a/codebase_rag/tests/test_rust_retrieval_eval.py b/codebase_rag/tests/test_rust_retrieval_eval.py
new file mode 100644
index 000000000..853da5583
--- /dev/null
+++ b/codebase_rag/tests/test_rust_retrieval_eval.py
@@ -0,0 +1,65 @@
+from pathlib import Path
+
+import pytest
+
+from evals import constants as ec
+from evals.oracles import rust_available
+from evals.rust_retrieval import (
+ cgr_rust_call_edges,
+ oracle_rust_call_edges,
+ score_rust_retrieval,
+)
+
+needs_rust = pytest.mark.skipif(
+ not rust_available(), reason="rust toolchain not installed"
+)
+
+
+def _make_crate(root: Path) -> None:
+ root.mkdir(parents=True, exist_ok=True)
+ (root / "lib.rs").write_text(
+ "pub struct T;\n\n"
+ "impl T {\n"
+ " pub fn helper(&self) -> i32 { 1 }\n"
+ " pub fn caller(&self) -> i32 { self.helper() }\n"
+ " pub fn make() -> T { T }\n"
+ " pub fn orphan(&self) -> i32 { 9 }\n"
+ "}\n\n"
+ "pub fn free() -> i32 { 2 }\n\n"
+ "pub fn use_it() -> i32 {\n"
+ " let t = T::make();\n"
+ " free() + t.caller()\n"
+ "}\n",
+ encoding="utf-8",
+ )
+
+
+@needs_rust
+def test_oracle_captures_first_party_rust_calls(tmp_path: Path) -> None:
+ _make_crate(tmp_path)
+ edges, declared = oracle_rust_call_edges(tmp_path)
+
+ # (H) self.helper(), T::make(), free(), t.caller() are all first-party calls.
+ assert ("lib.rs", "helper") in edges
+ assert ("lib.rs", "make") in edges
+ assert ("lib.rs", "free") in edges
+ assert ("lib.rs", "caller") in edges
+ # (H) orphan is declared but never called -> never a call edge.
+ assert ("lib.rs", "orphan") not in edges
+ assert {"helper", "caller", "make", "free", "use_it", "orphan"} <= declared
+
+
+@needs_rust
+def test_cgr_matches_oracle_on_clean_rust_crate(tmp_path: Path) -> None:
+ _make_crate(tmp_path)
+ oracle, declared = oracle_rust_call_edges(tmp_path)
+ cgr = cgr_rust_call_edges(tmp_path, tmp_path.name, declared)
+ assert cgr == oracle
+
+
+def test_score_rust_retrieval_prf() -> None:
+ result = score_rust_retrieval(
+ {("a.rs", "f"), ("a.rs", "g")}, {("a.rs", "f"), ("b.rs", "h")}
+ )
+ row = next(r for r in result.rows if r["label"] == ec.RUST_RETRIEVAL_LABEL)
+ assert (row["tp"], row["fp"], row["fn"]) == (1, 1, 1)
diff --git a/codebase_rag/tests/test_rust_span_oracle.py b/codebase_rag/tests/test_rust_span_oracle.py
new file mode 100644
index 000000000..5bd9abb53
--- /dev/null
+++ b/codebase_rag/tests/test_rust_span_oracle.py
@@ -0,0 +1,83 @@
+# (H) Covers Rust node SPAN (end_line) validation: cgr's end_line for each node is
+# (H) graded against the syn oracle (which emits the whole-node span end), joined
+# (H) on (kind, file, start) endpoints. Exercises doc comments, multi-line
+# (H) attributes, a multi-line signature, a where-clause, and a multi-line closure
+# (H) so the span is not trivially the start line.
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+
+from codebase_rag import constants as cs
+from codebase_rag.parser_loader import load_parsers
+from evals import constants as ec
+from evals.cgr_graph import extract_cgr_rust_graph
+from evals.oracles import run_rust_oracle, rust_available
+from evals.score import score_span
+
+RS_SRC = """\
+/// A documented struct
+/// spanning several doc lines.
+#[derive(Debug, Clone)]
+pub struct Widget {
+ name: String,
+ size: u32,
+}
+
+impl Widget {
+ pub fn area(
+ &self,
+ scale: u32,
+ ) -> u32 {
+ self.size * scale
+ }
+}
+
+pub trait Drawable {
+ fn draw(&self) -> String {
+ String::from("x")
+ }
+}
+
+pub fn standalone()
+where
+ u32: Sized,
+{
+ let cb = |v: u32| {
+ v + 1
+ };
+ let _ = cb(2);
+}
+"""
+
+
+def _require_rust() -> None:
+ if not rust_available():
+ pytest.skip("cargo toolchain not available")
+ if cs.SupportedLanguage.RUST not in load_parsers()[0]:
+ pytest.skip("rust parser not available")
+
+
+def test_cgr_matches_syn_oracle_on_node_spans(tmp_path: Path) -> None:
+ _require_rust()
+ project = tmp_path / "rs_span"
+ (project / "src").mkdir(parents=True)
+ (project / "Cargo.toml").write_text(
+ encoding="utf-8", data='[package]\nname = "rs_span"\nversion = "0.1.0"\n'
+ )
+ (project / "src" / "lib.rs").write_text(RS_SRC, encoding="utf-8")
+
+ cgr = extract_cgr_rust_graph(project, project.name)
+ oracle = run_rust_oracle(project)
+
+ result = score_span(cgr, oracle, ec.RS_SCORED_NODE_KINDS)
+ by_label = {row["label"]: row for row in result.rows}
+ aggregate = by_label.get(ec.AGGREGATE_LABEL)
+ assert aggregate is not None, (by_label, result.diff)
+ assert aggregate["precision"] == 1.0 and aggregate["recall"] == 1.0, (
+ aggregate,
+ result.diff,
+ )
+ # (H) Guard the sample actually exercises multi-line spans (else it is vacuous).
+ assert aggregate["tp"] >= 5, aggregate
diff --git a/codebase_rag/tests/test_rust_structure_oracle.py b/codebase_rag/tests/test_rust_structure_oracle.py
new file mode 100644
index 000000000..f9e9e9fa8
--- /dev/null
+++ b/codebase_rag/tests/test_rust_structure_oracle.py
@@ -0,0 +1,68 @@
+# (H) Covers the Rust structure oracle harness (evals/oracles/rs_oracle +
+# (H) evals/rust_l1.py): the syn-based oracle is authoritative ground truth, and
+# (H) cgr's captured Rust nodes are graded against it on (kind, file, start_line).
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+
+from codebase_rag import constants as cs
+from codebase_rag.parser_loader import load_parsers
+from evals import constants as ec
+from evals.cgr_graph import extract_cgr_rust_nodes
+from evals.oracles import run_rust_oracle, rust_available
+from evals.score import score_node_kinds
+from evals.types_defs import GraphData
+
+RS_SRC = """\
+pub struct Point { pub x: i32, pub y: i32 }
+pub enum Direction { North, South }
+pub trait Shape { fn area(&self) -> f64; }
+pub type Meters = f64;
+
+pub fn free_fn(a: i32) -> i32 { a + 1 }
+
+impl Point {
+ pub fn new(x: i32, y: i32) -> Self { Point { x, y } }
+}
+
+impl Shape for Point {
+ fn area(&self) -> f64 { 0.0 }
+}
+"""
+
+
+def _require_rust() -> None:
+ if not rust_available():
+ pytest.skip("cargo toolchain not available")
+ if cs.SupportedLanguage.RUST not in load_parsers()[0]:
+ pytest.skip("rust parser not available")
+
+
+def _project(tmp_path: Path) -> Path:
+ project = tmp_path / "rs_oracle_test"
+ (project / "src").mkdir(parents=True)
+ (project / "Cargo.toml").write_text(
+ encoding="utf-8", data='[package]\nname = "rs_oracle_test"\nversion = "0.1.0"\n'
+ )
+ (project / "src" / "lib.rs").write_text(RS_SRC, encoding="utf-8")
+ return project
+
+
+def test_cgr_matches_syn_oracle_on_rust_structure(tmp_path: Path) -> None:
+ _require_rust()
+ project = _project(tmp_path)
+ cgr = GraphData(
+ nodes=extract_cgr_rust_nodes(project, project.name),
+ edges=set(),
+ name_edges=set(),
+ )
+ oracle = run_rust_oracle(project)
+
+ result = score_node_kinds(cgr, oracle, ec.RS_SCORED_NODE_KINDS)
+ by_label = {row["label"]: row for row in result.rows}
+ for label in ("Class", "Interface", "Enum", "Type", "Function", "Method"):
+ row = by_label.get(label)
+ assert row is not None, (label, by_label)
+ assert row["precision"] == 1.0 and row["recall"] == 1.0, (label, row)
diff --git a/codebase_rag/tests/test_rust_trait_method_containment.py b/codebase_rag/tests/test_rust_trait_method_containment.py
new file mode 100644
index 000000000..26db7c491
--- /dev/null
+++ b/codebase_rag/tests/test_rust_trait_method_containment.py
@@ -0,0 +1,43 @@
+# (H) Regression: a DEFINES_METHOD relationship is matched in the graph by the
+# (H) parent's LABEL and qualified_name, so a method on a non-Class container
+# (H) (a Rust trait -> Interface node) must be emitted with the parent's real
+# (H) label. It was hardcoded to Class, so MATCH (a:Class {qn: trait}) found
+# (H) nothing and the trait -> method containment edge was silently dropped.
+from __future__ import annotations
+
+from pathlib import Path
+from unittest.mock import MagicMock
+
+from codebase_rag.constants import NodeLabel, RelationshipType
+from codebase_rag.tests.conftest import create_and_run_updater, get_relationships
+
+
+def test_rust_trait_method_defined_by_interface_node(
+ temp_repo: Path, mock_ingestor: MagicMock
+) -> None:
+ project = temp_repo / "rs_trait"
+ (project / "src").mkdir(parents=True)
+ (project / "Cargo.toml").write_text(
+ encoding="utf-8", data='[package]\nname = "rs_trait"\nversion = "0.1.0"\n'
+ )
+ (project / "src" / "lib.rs").write_text(
+ encoding="utf-8",
+ data="""pub trait Shape {
+ fn area(&self) -> f64 { 0.0 }
+}
+""",
+ )
+ create_and_run_updater(project, mock_ingestor, skip_if_missing="rust")
+
+ defines_method = get_relationships(
+ mock_ingestor, RelationshipType.DEFINES_METHOD.value
+ )
+ # (H) (parent_label, parent_qn) pairs for the trait's method.
+ parents = {
+ (call[0][0][0], call[0][0][2])
+ for call in defines_method
+ if str(call[0][2][2]).endswith(".Shape.area")
+ }
+ assert (NodeLabel.INTERFACE.value, "rs_trait.src.lib.Shape") in parents, parents
+ # (H) The wrong Class-labelled parent must not be emitted.
+ assert (NodeLabel.CLASS.value, "rs_trait.src.lib.Shape") not in parents, parents
diff --git a/codebase_rag/tests/test_rust_trait_object_receivers.py b/codebase_rag/tests/test_rust_trait_object_receivers.py
new file mode 100644
index 000000000..7582f95ea
--- /dev/null
+++ b/codebase_rag/tests/test_rust_trait_object_receivers.py
@@ -0,0 +1,136 @@
+# (H) A trait-object/impl-Trait parameter (`s: &dyn Svc`) never entered the
+# (H) local variable type map: tree-sitter wraps the trait in dynamic_type /
+# (H) abstract_type / bounded_type nodes, none of which the Rust bare-type
+# (H) walker descends through, so the receiver stayed untyped and `s.run()`
+# (H) fell to the name-only trie fallback whose lexicographic tie-break binds
+# (H) an ARBITRARY same-named method (an impl or the trait method, depending
+# (H) on how the type names happen to sort). The static callee must be the
+# (H) trait method, mirroring the Java interface-receiver design; OVERRIDES
+# (H) expansion then keeps every impl alive for dead-code.
+from __future__ import annotations
+
+from pathlib import Path
+from unittest.mock import MagicMock
+
+import pytest
+from tree_sitter import Language, Parser
+
+from codebase_rag import constants as cs
+from codebase_rag.parsers.rs import type_inference as rs_ti
+from codebase_rag.parsers.rs import utils as rs_utils
+from codebase_rag.tests.conftest import run_updater
+from evals.dead_code import cgr_dead_code, default_dead_code_config
+
+try:
+ import tree_sitter_rust as tsrust
+
+ RUST_AVAILABLE = True
+except ImportError:
+ RUST_AVAILABLE = False
+
+# (H) Impl names sort BEFORE "Svc" so the pre-fix lexicographic tie-break
+# (H) picks an impl, not the trait method; the fix must not depend on luck.
+TRAIT_OBJECT_CALLERS = (
+ "trait Svc { fn run(&self) -> i32; }\n"
+ "struct Alpha;\n"
+ "impl Svc for Alpha { fn run(&self) -> i32 { 1 } }\n"
+ "struct Beta;\n"
+ "impl Svc for Beta { fn run(&self) -> i32 { 2 } }\n"
+ "pub fn use_ref(s: &dyn Svc) -> i32 { s.run() }\n"
+ "pub fn use_mut(s: &mut dyn Svc) -> i32 { s.run() }\n"
+ "pub fn use_boxed(s: Box) -> i32 { s.run() }\n"
+ "pub fn use_impl(s: &impl Svc) -> i32 { s.run() }\n"
+ "pub fn use_bounded(s: impl Svc + Clone) -> i32 { s.run() }\n"
+ "pub fn use_paren(s: &(dyn Svc + Send)) -> i32 { s.run() }\n"
+)
+
+
+def _run_calls(temp_repo: Path, mock_ingestor: MagicMock) -> set[tuple[str, str]]:
+ (temp_repo / "m.rs").write_text(TRAIT_OBJECT_CALLERS, encoding="utf-8")
+ run_updater(temp_repo, mock_ingestor, skip_if_missing="rust")
+ return {
+ (c.args[0][2], c.args[2][2])
+ for c in mock_ingestor.ensure_relationship_batch.call_args_list
+ if c.args[1] == cs.RelationshipType.CALLS and c.args[2][2].endswith(".run")
+ }
+
+
+@pytest.mark.parametrize(
+ "caller",
+ ["use_ref", "use_mut", "use_boxed", "use_impl", "use_bounded", "use_paren"],
+)
+def test_trait_typed_receiver_binds_to_trait_method(
+ temp_repo: Path, mock_ingestor: MagicMock, caller: str
+) -> None:
+ calls = _run_calls(temp_repo, mock_ingestor)
+ bound = {callee for c, callee in calls if c.endswith(f".{caller}")}
+ assert bound == {f"{temp_repo.name}.m.Svc.run"}, sorted(calls)
+
+
+def test_boxed_dyn_return_type_binds_to_trait_method(
+ temp_repo: Path, mock_ingestor: MagicMock
+) -> None:
+ # (H) The same walker gap on the RETURN side: a factory returning
+ # (H) `Box` typed its result as `Box`, so a call on the bound
+ # (H) local never reached the trait method either. (An associated-fn
+ # (H) factory: only impl-method return types are recorded, a free fn's
+ # (H) `let s = make()` chain is a separate, unrelated gap.)
+ (temp_repo / "m.rs").write_text(
+ "trait Svc { fn run(&self) -> i32; }\n"
+ "struct Alpha;\n"
+ "impl Svc for Alpha { fn run(&self) -> i32 { 1 } }\n"
+ "struct Beta;\n"
+ "impl Svc for Beta { fn run(&self) -> i32 { 2 } }\n"
+ "struct Maker;\n"
+ "impl Maker { fn make() -> Box { Box::new(Alpha) } }\n"
+ "pub fn use_made() -> i32 { let s = Maker::make(); s.run() }\n",
+ encoding="utf-8",
+ )
+ run_updater(temp_repo, mock_ingestor, skip_if_missing="rust")
+ bound = {
+ c.args[2][2]
+ for c in mock_ingestor.ensure_relationship_batch.call_args_list
+ if c.args[1] == cs.RelationshipType.CALLS
+ and c.args[0][2].endswith(".use_made")
+ and c.args[2][2].endswith(".run")
+ }
+ assert bound == {f"{temp_repo.name}.m.Svc.run"}, sorted(bound)
+
+
+def test_multi_impl_trait_nothing_dead_regardless_of_names(tmp_path: Path) -> None:
+ # (H) With impls sorting before the trait, the pre-fix arbitrary binding
+ # (H) lands on one impl, leaving the trait method and the other impl dead.
+ # (H) Typed to the trait, the call plus OVERRIDES expansion keeps all
+ # (H) three alive.
+ root = tmp_path / "rdyn"
+ root.mkdir()
+ (root / "m.rs").write_text(
+ "trait Svc { fn run(&self) -> i32; }\n"
+ "struct Alpha;\n"
+ "impl Svc for Alpha { fn run(&self) -> i32 { 1 } }\n"
+ "struct Beta;\n"
+ "impl Svc for Beta { fn run(&self) -> i32 { 2 } }\n"
+ "pub fn use_it(s: &dyn Svc) -> i32 { s.run() }\n",
+ encoding="utf-8",
+ )
+ dead = cgr_dead_code(root, "proj", default_dead_code_config(False, False))
+ assert not [d for d in dead if d.endswith(".run")], sorted(dead)
+
+
+@pytest.mark.skipif(not RUST_AVAILABLE, reason="tree-sitter-rust not installed")
+def test_one_ary_tuple_is_not_grouping_parens() -> None:
+ # (H) Rust writes a 1-ary tuple `(T,)` with a trailing comma; it also has
+ # (H) exactly one typed child, so a child-count-only grouping check would
+ # (H) type the value as its element. Only comma-free parens are grouping.
+ parser = Parser(Language(tsrust.language()))
+ src = (
+ b"fn a(s: (Alpha,)) -> (Beta,) { s.run() }\n"
+ b"fn b(s: (Alpha)) -> (Beta) { s.run() }\n"
+ )
+ fn_tuple, fn_grouped = parser.parse(src).root_node.children
+ engine = rs_ti.RustTypeInferenceEngine()
+ assert "s" not in engine.build_local_variable_type_map(fn_tuple, "proj.m")
+ assert rs_utils.extract_return_type_name(fn_tuple, None) is None
+ grouped = engine.build_local_variable_type_map(fn_grouped, "proj.m")
+ assert grouped.get("s") == "Alpha"
+ assert rs_utils.extract_return_type_name(fn_grouped, None) == "Beta"
diff --git a/codebase_rag/tests/test_scala_retrieval_eval.py b/codebase_rag/tests/test_scala_retrieval_eval.py
new file mode 100644
index 000000000..46f0c2066
--- /dev/null
+++ b/codebase_rag/tests/test_scala_retrieval_eval.py
@@ -0,0 +1,93 @@
+from pathlib import Path
+
+import pytest
+
+from evals import constants as ec
+from evals.oracles import scala_available
+from evals.scala_retrieval import (
+ cgr_scala_call_edges,
+ oracle_scala_call_edges,
+ score_scala_retrieval,
+)
+
+needs_scala = pytest.mark.skipif(
+ not scala_available(), reason="scala-cli toolchain not installed"
+)
+
+
+def _make_project(root: Path) -> None:
+ root.mkdir(parents=True, exist_ok=True)
+ (root / "Util.scala").write_text(
+ "object Util {\n def free(): Int = 2\n}\n",
+ encoding="utf-8",
+ )
+ (root / "T.scala").write_text(
+ "class T {\n"
+ " def helper(): Int = 1\n"
+ " def caller(): Int = this.helper()\n"
+ " def orphan(): Int = 9\n"
+ " def ~>(o: T): T = o\n"
+ " def done: Boolean = true\n"
+ "}\n"
+ "object T {\n"
+ " def make(): T = new T()\n"
+ "}\n",
+ encoding="utf-8",
+ )
+ (root / "Use.scala").write_text(
+ "object Use {\n"
+ " def useIt(): Int = {\n"
+ " val t = T.make()\n"
+ " val u = t ~> T.make()\n"
+ " val d = u.done\n"
+ " Util.free() + t.caller()\n"
+ " }\n"
+ "}\n",
+ encoding="utf-8",
+ )
+
+
+@needs_scala
+def test_oracle_captures_first_party_scala_calls(tmp_path: Path) -> None:
+ _make_project(tmp_path)
+ edges, declared, covered = oracle_scala_call_edges(tmp_path)
+
+ # (H) this.helper(), T.make(), Util.free(), t.caller() are first-party calls.
+ assert ("T.scala", "helper") in edges
+ assert ("Use.scala", "make") in edges
+ assert ("Use.scala", "free") in edges
+ assert ("Use.scala", "caller") in edges
+ # (H) An infix operator call (t ~> ...) is unambiguously a method call.
+ assert ("Use.scala", "~>") in edges
+ # (H) A bare paren-less select (u.done) is NOT graded: uniform access makes a
+ # (H) nullary call and a field read identical, so it is scoped out on both sides.
+ assert ("Use.scala", "done") not in edges
+ # (H) orphan is declared but never called -> never a call edge.
+ assert ("T.scala", "orphan") not in edges
+ assert {
+ "helper",
+ "caller",
+ "make",
+ "free",
+ "orphan",
+ "useIt",
+ "~>",
+ "done",
+ } <= declared
+ assert {"Util.scala", "T.scala", "Use.scala"} <= covered
+
+
+@needs_scala
+def test_cgr_matches_oracle_on_clean_scala_project(tmp_path: Path) -> None:
+ _make_project(tmp_path)
+ oracle, declared, covered = oracle_scala_call_edges(tmp_path)
+ cgr = cgr_scala_call_edges(tmp_path, tmp_path.name, declared, covered)
+ assert cgr == oracle
+
+
+def test_score_scala_retrieval_prf() -> None:
+ result = score_scala_retrieval(
+ {("A.scala", "f"), ("A.scala", "g")}, {("A.scala", "f"), ("B.scala", "h")}
+ )
+ row = next(r for r in result.rows if r["label"] == ec.SCALA_RETRIEVAL_LABEL)
+ assert (row["tp"], row["fp"], row["fn"]) == (1, 1, 1)
diff --git a/codebase_rag/tests/test_semantic_search.py b/codebase_rag/tests/test_semantic_search.py
index 759145d73..eb6c99dcd 100644
--- a/codebase_rag/tests/test_semantic_search.py
+++ b/codebase_rag/tests/test_semantic_search.py
@@ -4,6 +4,7 @@
import pytest
+from codebase_rag import constants as cs
from codebase_rag.utils.dependencies import has_semantic_dependencies
@@ -24,9 +25,7 @@ def mock_search_embeddings() -> MagicMock:
@pytest.fixture
def mock_ingestor() -> MagicMock:
mock = MagicMock()
- mock.__enter__ = MagicMock(return_value=mock)
- mock.__exit__ = MagicMock(return_value=False)
- mock._execute_query.return_value = [
+ mock.fetch_all.return_value = [
{
"node_id": 1,
"qualified_name": "project.module.func1",
@@ -55,10 +54,71 @@ def test_semantic_code_search_returns_empty_without_dependencies() -> None:
from codebase_rag.tools.semantic_search import semantic_code_search
- results = semantic_code_search("find error handlers")
+ results = semantic_code_search(MagicMock(), "find error handlers")
assert results == []
+@patch(
+ "codebase_rag.tools.semantic_search.has_semantic_dependencies", return_value=True
+)
+@patch("codebase_rag.vector_store.search_embeddings")
+@patch("codebase_rag.embedder.embed_code")
+def test_semantic_code_search_reuses_injected_ingestor(
+ mock_embed: MagicMock, mock_search: MagicMock, _deps: MagicMock
+) -> None:
+ from codebase_rag.tools.semantic_search import semantic_code_search
+
+ mock_embed.return_value = [0.0]
+ mock_search.return_value = [(1, 0.99), (2, 0.42)]
+
+ ingestor = MagicMock()
+ ingestor.fetch_all.return_value = [
+ {
+ "node_id": 1,
+ "qualified_name": "pkg.mod.foo",
+ "name": "foo",
+ "type": ["Function"],
+ },
+ {
+ "node_id": 2,
+ "qualified_name": "pkg.mod.Bar",
+ "name": "Bar",
+ "type": ["Class"],
+ },
+ ]
+
+ results = semantic_code_search(ingestor, "find the foo function", top_k=2)
+
+ ingestor.fetch_all.assert_called_once()
+ ingestor._execute_query.assert_not_called()
+ assert [r["qualified_name"] for r in results] == ["pkg.mod.foo", "pkg.mod.Bar"]
+ assert results[0]["score"] == 0.99
+
+
+@patch(
+ "codebase_rag.tools.semantic_search.has_semantic_dependencies", return_value=True
+)
+@patch("codebase_rag.vector_store.search_embeddings")
+@patch("codebase_rag.embedder.embed_code")
+def test_semantic_code_search_tolerates_missing_result_fields(
+ mock_embed: MagicMock, mock_search: MagicMock, _deps: MagicMock
+) -> None:
+ from codebase_rag.tools.semantic_search import semantic_code_search
+
+ mock_embed.return_value = [0.0]
+ mock_search.return_value = [(1, 0.99)]
+
+ ingestor = MagicMock()
+ ingestor.fetch_all.return_value = [{"node_id": 1}]
+
+ results = semantic_code_search(ingestor, "find foo", top_k=1)
+
+ assert len(results) == 1
+ assert results[0]["qualified_name"] == ""
+ assert results[0]["name"] == ""
+ assert results[0]["type"] == cs.SEMANTIC_TYPE_UNKNOWN
+
+
@pytest.mark.skipif(
not has_semantic_dependencies(), reason="semantic dependencies not installed"
)
@@ -72,12 +132,10 @@ def test_semantic_code_search_returns_formatted_results(
with (
patch("codebase_rag.embedder.embed_code", mock_embed_code),
patch("codebase_rag.vector_store.search_embeddings", mock_search_embeddings),
- patch(
- "codebase_rag.services.graph_service.MemgraphIngestor",
- return_value=mock_ingestor,
- ),
):
- results = semantic_code_search("find authentication code", top_k=3)
+ results = semantic_code_search(
+ mock_ingestor, "find authentication code", top_k=3
+ )
assert len(results) == 3
assert results[0]["node_id"] == 1
@@ -99,12 +157,8 @@ def test_semantic_code_search_calls_embed_code_with_query(
with (
patch("codebase_rag.embedder.embed_code", mock_embed_code),
patch("codebase_rag.vector_store.search_embeddings", mock_search_embeddings),
- patch(
- "codebase_rag.services.graph_service.MemgraphIngestor",
- return_value=mock_ingestor,
- ),
):
- semantic_code_search("database operations")
+ semantic_code_search(mock_ingestor, "database operations")
mock_embed_code.assert_called_once_with("database operations")
@@ -122,12 +176,8 @@ def test_semantic_code_search_passes_top_k_to_search(
with (
patch("codebase_rag.embedder.embed_code", mock_embed_code),
patch("codebase_rag.vector_store.search_embeddings", mock_search_embeddings),
- patch(
- "codebase_rag.services.graph_service.MemgraphIngestor",
- return_value=mock_ingestor,
- ),
):
- semantic_code_search("file handling", top_k=10)
+ semantic_code_search(mock_ingestor, "file handling", top_k=10)
mock_search_embeddings.assert_called_once_with([0.1] * 768, top_k=10)
@@ -146,7 +196,7 @@ def test_semantic_code_search_returns_empty_when_no_matches(
patch("codebase_rag.embedder.embed_code", mock_embed_code),
patch("codebase_rag.vector_store.search_embeddings", mock_search_empty),
):
- results = semantic_code_search("nonexistent functionality")
+ results = semantic_code_search(MagicMock(), "nonexistent functionality")
assert results == []
@@ -160,7 +210,7 @@ def test_semantic_code_search_handles_exception(mock_embed_code: MagicMock) -> N
mock_embed_code.side_effect = Exception("Embedding failed")
with patch("codebase_rag.embedder.embed_code", mock_embed_code):
- results = semantic_code_search("some query")
+ results = semantic_code_search(MagicMock(), "some query")
assert results == []
@@ -179,12 +229,8 @@ def test_semantic_code_search_preserves_score_order(
with (
patch("codebase_rag.embedder.embed_code", mock_embed_code),
patch("codebase_rag.vector_store.search_embeddings", mock_search),
- patch(
- "codebase_rag.services.graph_service.MemgraphIngestor",
- return_value=mock_ingestor,
- ),
):
- results = semantic_code_search("test query")
+ results = semantic_code_search(mock_ingestor, "test query")
assert results[0]["node_id"] == 3
assert results[0]["score"] == 0.99
@@ -198,7 +244,7 @@ def test_semantic_code_search_preserves_score_order(
def test_get_function_source_code_returns_source(mock_ingestor: MagicMock) -> None:
from codebase_rag.tools.semantic_search import get_function_source_code
- mock_ingestor._execute_query.return_value = [
+ mock_ingestor.fetch_all.return_value = [
{
"qualified_name": "project.module.func",
"start_line": 10,
@@ -211,10 +257,6 @@ def test_get_function_source_code_returns_source(mock_ingestor: MagicMock) -> No
mock_extract = MagicMock(return_value="def func():\n return 42")
with (
- patch(
- "codebase_rag.services.graph_service.MemgraphIngestor",
- return_value=mock_ingestor,
- ),
patch(
"codebase_rag.utils.source_extraction.validate_source_location",
mock_validate,
@@ -223,7 +265,7 @@ def test_get_function_source_code_returns_source(mock_ingestor: MagicMock) -> No
"codebase_rag.utils.source_extraction.extract_source_lines", mock_extract
),
):
- result = get_function_source_code(123)
+ result = get_function_source_code(mock_ingestor, 123)
assert result == "def func():\n return 42"
@@ -236,13 +278,9 @@ def test_get_function_source_code_returns_none_when_not_found(
) -> None:
from codebase_rag.tools.semantic_search import get_function_source_code
- mock_ingestor._execute_query.return_value = []
+ mock_ingestor.fetch_all.return_value = []
- with patch(
- "codebase_rag.services.graph_service.MemgraphIngestor",
- return_value=mock_ingestor,
- ):
- result = get_function_source_code(999)
+ result = get_function_source_code(mock_ingestor, 999)
assert result is None
@@ -255,7 +293,7 @@ def test_get_function_source_code_returns_none_on_invalid_location(
) -> None:
from codebase_rag.tools.semantic_search import get_function_source_code
- mock_ingestor._execute_query.return_value = [
+ mock_ingestor.fetch_all.return_value = [
{
"qualified_name": "project.module.func",
"start_line": None,
@@ -266,17 +304,11 @@ def test_get_function_source_code_returns_none_on_invalid_location(
mock_validate = MagicMock(return_value=(False, None))
- with (
- patch(
- "codebase_rag.services.graph_service.MemgraphIngestor",
- return_value=mock_ingestor,
- ),
- patch(
- "codebase_rag.utils.source_extraction.validate_source_location",
- mock_validate,
- ),
+ with patch(
+ "codebase_rag.utils.source_extraction.validate_source_location",
+ mock_validate,
):
- result = get_function_source_code(123)
+ result = get_function_source_code(mock_ingestor, 123)
assert result is None
@@ -287,13 +319,9 @@ def test_get_function_source_code_returns_none_on_invalid_location(
def test_get_function_source_code_handles_exception(mock_ingestor: MagicMock) -> None:
from codebase_rag.tools.semantic_search import get_function_source_code
- mock_ingestor._execute_query.side_effect = Exception("Database error")
+ mock_ingestor.fetch_all.side_effect = Exception("Database error")
- with patch(
- "codebase_rag.services.graph_service.MemgraphIngestor",
- return_value=mock_ingestor,
- ):
- result = get_function_source_code(123)
+ result = get_function_source_code(mock_ingestor, 123)
assert result is None
@@ -301,13 +329,13 @@ def test_get_function_source_code_handles_exception(mock_ingestor: MagicMock) ->
@pytest.mark.skipif(
not has_semantic_dependencies(), reason="semantic dependencies not installed"
)
-def test_create_semantic_search_tool_returns_tool() -> None:
+def test_create_semantic_search_tool_returns_tool(mock_ingestor: MagicMock) -> None:
from pydantic_ai import Tool
from codebase_rag.tools.semantic_search import create_semantic_search_tool
from codebase_rag.tools.tool_descriptions import AgenticToolName
- tool = create_semantic_search_tool()
+ tool = create_semantic_search_tool(mock_ingestor)
assert isinstance(tool, Tool)
assert tool.name == AgenticToolName.SEMANTIC_SEARCH
@@ -316,13 +344,13 @@ def test_create_semantic_search_tool_returns_tool() -> None:
@pytest.mark.skipif(
not has_semantic_dependencies(), reason="semantic dependencies not installed"
)
-def test_create_get_function_source_tool_returns_tool() -> None:
+def test_create_get_function_source_tool_returns_tool(mock_ingestor: MagicMock) -> None:
from pydantic_ai import Tool
from codebase_rag.tools.semantic_search import create_get_function_source_tool
from codebase_rag.tools.tool_descriptions import AgenticToolName
- tool = create_get_function_source_tool()
+ tool = create_get_function_source_tool(mock_ingestor)
assert isinstance(tool, Tool)
assert tool.name == AgenticToolName.GET_FUNCTION_SOURCE
@@ -339,15 +367,11 @@ async def test_semantic_search_tool_formats_results(
) -> None:
from codebase_rag.tools.semantic_search import create_semantic_search_tool
- tool = create_semantic_search_tool()
+ tool = create_semantic_search_tool(mock_ingestor)
with (
patch("codebase_rag.embedder.embed_code", mock_embed_code),
patch("codebase_rag.vector_store.search_embeddings", mock_search_embeddings),
- patch(
- "codebase_rag.services.graph_service.MemgraphIngestor",
- return_value=mock_ingestor,
- ),
):
result = await tool.function("find handlers")
@@ -367,7 +391,7 @@ async def test_semantic_search_tool_handles_no_results(
from codebase_rag.tools.semantic_search import create_semantic_search_tool
mock_search_empty = MagicMock(return_value=[])
- tool = create_semantic_search_tool()
+ tool = create_semantic_search_tool(MagicMock())
with (
patch("codebase_rag.embedder.embed_code", mock_embed_code),
@@ -387,7 +411,7 @@ async def test_get_function_source_tool_returns_source(
) -> None:
from codebase_rag.tools.semantic_search import create_get_function_source_tool
- mock_ingestor._execute_query.return_value = [
+ mock_ingestor.fetch_all.return_value = [
{
"qualified_name": "project.func",
"start_line": 1,
@@ -399,13 +423,9 @@ async def test_get_function_source_tool_returns_source(
mock_validate = MagicMock(return_value=(True, MagicMock()))
mock_extract = MagicMock(return_value="def func(): pass")
- tool = create_get_function_source_tool()
+ tool = create_get_function_source_tool(mock_ingestor)
with (
- patch(
- "codebase_rag.services.graph_service.MemgraphIngestor",
- return_value=mock_ingestor,
- ),
patch(
"codebase_rag.utils.source_extraction.validate_source_location",
mock_validate,
@@ -429,14 +449,10 @@ async def test_get_function_source_tool_handles_not_found(
) -> None:
from codebase_rag.tools.semantic_search import create_get_function_source_tool
- mock_ingestor._execute_query.return_value = []
+ mock_ingestor.fetch_all.return_value = []
- tool = create_get_function_source_tool()
+ tool = create_get_function_source_tool(mock_ingestor)
- with patch(
- "codebase_rag.services.graph_service.MemgraphIngestor",
- return_value=mock_ingestor,
- ):
- result = await tool.function(999)
+ result = await tool.function(999)
assert "Could not retrieve source code" in result
diff --git a/codebase_rag/tests/test_semantic_search_eval.py b/codebase_rag/tests/test_semantic_search_eval.py
new file mode 100644
index 000000000..69afba62d
--- /dev/null
+++ b/codebase_rag/tests/test_semantic_search_eval.py
@@ -0,0 +1,76 @@
+from pathlib import Path
+
+import pytest
+
+from codebase_rag.utils.dependencies import has_semantic_dependencies
+from evals import constants as ec
+from evals.semantic_search import (
+ SemanticCase,
+ cgr_semantic_ranking,
+ function_snippets,
+ score_semantic,
+)
+
+needs_semantic = pytest.mark.skipif(
+ not has_semantic_dependencies(), reason="semantic extra not installed"
+)
+
+
+def _make_repo(root: Path) -> None:
+ root.mkdir(parents=True, exist_ok=True)
+ (root / "__init__.py").write_text("", encoding="utf-8")
+ (root / "ops.py").write_text(
+ "import json\n\n\n"
+ "def load_json_file(path):\n"
+ " with open(path) as handle:\n"
+ " return json.load(handle)\n\n\n"
+ "def send_email(recipient, body):\n"
+ " server = connect_smtp()\n"
+ " server.sendmail(recipient, body)\n\n\n"
+ "def compute_sales_tax(amount, rate):\n"
+ " return amount * rate\n\n\n"
+ "def connect_smtp():\n"
+ " return object()\n",
+ encoding="utf-8",
+ )
+
+
+_CASES = [
+ SemanticCase("read and parse a json file from disk", "proj.ops.load_json_file"),
+ SemanticCase("send an email message to a recipient", "proj.ops.send_email"),
+ SemanticCase("calculate tax on a purchase amount", "proj.ops.compute_sales_tax"),
+]
+
+
+@needs_semantic
+def test_function_snippets_extracted_from_graph(tmp_path: Path) -> None:
+ src = tmp_path / "proj"
+ _make_repo(src)
+ snippets = function_snippets(src, "proj")
+ assert "proj.ops.load_json_file" in snippets
+ assert "json.load" in snippets["proj.ops.load_json_file"]
+
+
+@needs_semantic
+def test_cgr_semantic_search_retrieves_expected_function(tmp_path: Path) -> None:
+ src = tmp_path / "proj"
+ _make_repo(src)
+ queries = [case.query for case in _CASES]
+ ranking = cgr_semantic_ranking(src, "proj", queries, ec.SEMANTIC_TOP_K)
+ result = score_semantic(_CASES, ranking)
+ row = next(r for r in result.rows if r["label"] == ec.SEMANTIC_LABEL)
+ # (H) Each query's clearly-relevant function should rank in the top k.
+ assert row["recall"] == 1.0
+ assert row["fn"] == 0
+
+
+def test_score_semantic_counts_misses() -> None:
+ cases = [
+ SemanticCase("q1", "proj.a"),
+ SemanticCase("q2", "proj.b"),
+ ]
+ ranking = {"q1": ["proj.a", "proj.x"], "q2": ["proj.y"]}
+ result = score_semantic(cases, ranking)
+ row = next(r for r in result.rows if r["label"] == ec.SEMANTIC_LABEL)
+ assert (row["tp"], row["fn"]) == (1, 1)
+ assert row["recall"] == 0.5
diff --git a/codebase_rag/tests/test_shell_command.py b/codebase_rag/tests/test_shell_command.py
index f745b2e30..cf57396d1 100644
--- a/codebase_rag/tests/test_shell_command.py
+++ b/codebase_rag/tests/test_shell_command.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import sys
from pathlib import Path
from unittest.mock import MagicMock
@@ -274,6 +275,64 @@ def test_empty_segment(self) -> None:
available = ", ".join(sorted(settings.SHELL_COMMAND_ALLOWLIST))
assert _validate_segment("", available) is None
+ def test_bypass_allowlist_skips_allowlist_error(self) -> None:
+ available = ", ".join(sorted(settings.SHELL_COMMAND_ALLOWLIST))
+ assert (
+ _validate_segment(
+ "curl http://example.com", available, bypass_allowlist=True
+ )
+ is None
+ )
+
+ def test_bypass_allowlist_still_blocks_dangerous_rm(self) -> None:
+ available = ", ".join(sorted(settings.SHELL_COMMAND_ALLOWLIST))
+ error = _validate_segment("rm -rf /", available, bypass_allowlist=True)
+ assert error is not None
+ assert "dangerous" in error.lower()
+
+
+class TestYoloMode:
+ async def test_yolo_skips_approval_for_write_command(
+ self, temp_project_root: Path
+ ) -> None:
+ test_file = temp_project_root / "yolo_target.txt"
+ test_file.write_text("bye", encoding="utf-8")
+ commander = ShellCommander(
+ str(temp_project_root), timeout=5, is_yolo=lambda: True
+ )
+ tool = create_shell_command_tool(commander)
+ mock_ctx = MagicMock()
+ mock_ctx.tool_call_approved = False
+ result = await tool.function(mock_ctx, "rm yolo_target.txt")
+ assert result.return_code == 0
+ assert not test_file.exists()
+
+ async def test_yolo_runs_non_allowlist_command(
+ self, temp_project_root: Path
+ ) -> None:
+ commander = ShellCommander(
+ str(temp_project_root), timeout=5, is_yolo=lambda: True
+ )
+ tool = create_shell_command_tool(commander)
+ mock_ctx = MagicMock()
+ mock_ctx.tool_call_approved = False
+ assert "printf" not in settings.SHELL_COMMAND_ALLOWLIST
+ result = await tool.function(mock_ctx, "printf hello")
+ assert "not in the allowlist" not in result.stderr
+
+ async def test_yolo_still_blocks_dangerous_rm_rf(
+ self, temp_project_root: Path
+ ) -> None:
+ commander = ShellCommander(
+ str(temp_project_root), timeout=5, is_yolo=lambda: True
+ )
+ tool = create_shell_command_tool(commander)
+ mock_ctx = MagicMock()
+ mock_ctx.tool_call_approved = False
+ result = await tool.function(mock_ctx, "rm -rf /")
+ assert result.return_code != 0
+ assert "dangerous" in result.stderr.lower()
+
class TestHasRedirectOperators:
def test_output_redirect(self) -> None:
@@ -386,6 +445,9 @@ async def test_simple_pipe(
assert result.return_code == 0
assert "5" in result.stdout
+ @pytest.mark.skipif(
+ sys.platform == "win32", reason="Unix find not available on Windows"
+ )
async def test_find_with_wc(
self, shell_commander: ShellCommander, temp_project_root: Path
) -> None:
@@ -398,6 +460,10 @@ async def test_find_with_wc(
async def test_rg_in_pipeline(
self, shell_commander: ShellCommander, temp_project_root: Path
) -> None:
+ import shutil
+
+ if not shutil.which("rg"):
+ pytest.skip("rg (ripgrep) not installed")
(temp_project_root / "data.txt").write_text("foo\nbar\nbaz\n", encoding="utf-8")
result = await shell_commander.execute("cat data.txt | rg bar")
assert result.return_code == 0
@@ -630,11 +696,11 @@ def test_path_outside_project(self, tmp_path: Path) -> None:
["rm", "-rf", "../other"], project_root
)
assert is_dangerous
- assert "outside project" in reason
+ assert "outside project" in reason or "system directory" in reason
def test_safe_path_inside_project(self, tmp_path: Path) -> None:
- project_root = tmp_path / "project"
- project_root.mkdir()
+ project_root = (tmp_path / "project").resolve()
+ project_root.mkdir(exist_ok=True)
is_dangerous, _ = _is_dangerous_rm_path(
["rm", "-rf", "subdir/file.txt"], project_root
)
@@ -741,7 +807,8 @@ async def test_rm_outside_project_blocked(
) -> None:
result = await shell_commander.execute("rm ../outside_project")
assert result.return_code == -1
- assert "outside project" in result.stderr.lower()
+ stderr_lower = result.stderr.lower()
+ assert "outside project" in stderr_lower or "system directory" in stderr_lower
class TestAwkSedXargsPatterns:
diff --git a/codebase_rag/tests/test_sibling_mixin_resolution.py b/codebase_rag/tests/test_sibling_mixin_resolution.py
new file mode 100644
index 000000000..48bb15156
--- /dev/null
+++ b/codebase_rag/tests/test_sibling_mixin_resolution.py
@@ -0,0 +1,97 @@
+# (H) L3 finding from the evals/ harness: PythonAstAnalyzerMixin._traverse_single_pass
+# (H) calls self._infer_instance_variable_types_from_assignments(...), a method defined
+# (H) on the sibling PythonVariableAnalyzerMixin. Neither is the other's base; both are
+# (H) combined into the concrete PythonTypeInferenceEngine. A same-named stub in another
+# (H) class makes the bare-name trie fallback ambiguous, so resolution must go through
+# (H) the concrete subclass's MRO to land on the real sibling method.
+from __future__ import annotations
+
+from pathlib import Path
+
+from codebase_rag import constants as cs
+from codebase_rag.graph_updater import GraphUpdater
+from codebase_rag.parser_loader import load_parsers
+from codebase_rag.types_defs import PropertyDict, PropertyValue, ResultRow
+
+PROJECT = "proj"
+
+FILES = {
+ "pkg/__init__.py": "",
+ # (H) A decoy class declaring the same method name (mirrors a TYPE_CHECKING stub)
+ # (H) so the trie fallback alone cannot pick the right target.
+ "pkg/decoy.py": ("class Deps:\n def infer_vars(self):\n return None\n"),
+ "pkg/mixin_a.py": (
+ "class AMixin:\n def traverse(self):\n return self.infer_vars()\n"
+ ),
+ "pkg/mixin_b.py": ("class BMixin:\n def infer_vars(self):\n return {}\n"),
+ "pkg/engine.py": (
+ "from .mixin_a import AMixin\n"
+ "from .mixin_b import BMixin\n\n\n"
+ "class Engine(AMixin, BMixin):\n"
+ " def other(self):\n"
+ " return None\n"
+ ),
+}
+
+
+class _Capture:
+ def __init__(self) -> None:
+ self.rels: list[tuple[PropertyValue, str, PropertyValue]] = []
+
+ def ensure_node_batch(self, label: str, properties: PropertyDict) -> None:
+ return None
+
+ def ensure_relationship_batch(
+ self,
+ from_spec: tuple[str, str, PropertyValue],
+ rel_type: str,
+ to_spec: tuple[str, str, PropertyValue],
+ properties: PropertyDict | None = None,
+ ) -> None:
+ self.rels.append((from_spec[2], str(rel_type), to_spec[2]))
+
+ def flush_all(self) -> None:
+ return None
+
+ def fetch_all(
+ self, query: str, params: PropertyDict | None = None
+ ) -> list[ResultRow]:
+ return []
+
+ def execute_write(self, query: str, params: PropertyDict | None = None) -> None:
+ return None
+
+
+def _calls(tmp_path: Path) -> set[tuple[PropertyValue, PropertyValue]]:
+ for rel, content in FILES.items():
+ p = tmp_path / rel
+ p.parent.mkdir(parents=True, exist_ok=True)
+ p.write_text(content)
+ parsers, queries = load_parsers()
+ cap = _Capture()
+ GraphUpdater(
+ ingestor=cap,
+ repo_path=tmp_path,
+ parsers=parsers,
+ queries=queries,
+ project_name=PROJECT,
+ ).run(force=True)
+ return {
+ (frm, to) for (frm, rel, to) in cap.rels if rel == cs.RelationshipType.CALLS
+ }
+
+
+class TestSiblingMixinResolution:
+ def test_self_call_resolves_to_sibling_mixin_method(self, tmp_path: Path) -> None:
+ calls = _calls(tmp_path)
+ assert (
+ "proj.pkg.mixin_a.AMixin.traverse",
+ "proj.pkg.mixin_b.BMixin.infer_vars",
+ ) in calls, calls
+
+ def test_does_not_resolve_to_decoy_class(self, tmp_path: Path) -> None:
+ calls = _calls(tmp_path)
+ assert (
+ "proj.pkg.mixin_a.AMixin.traverse",
+ "proj.pkg.decoy.Deps.infer_vars",
+ ) not in calls, calls
diff --git a/codebase_rag/tests/test_single_file_repo_path.py b/codebase_rag/tests/test_single_file_repo_path.py
new file mode 100644
index 000000000..71d4a28a7
--- /dev/null
+++ b/codebase_rag/tests/test_single_file_repo_path.py
@@ -0,0 +1,138 @@
+from pathlib import Path
+from unittest.mock import MagicMock
+
+import pytest
+
+from codebase_rag.tests.conftest import (
+ get_node_names,
+ get_relationships,
+ run_updater,
+)
+
+
+@pytest.fixture
+def cpp_single_file(temp_repo: Path) -> Path:
+ test_file = temp_repo / "cmGlobalFastbuildGenerator.cxx"
+ test_file.write_text(
+ encoding="utf-8",
+ data="""
+#include