diff --git a/java_codebase_rag/config.py b/java_codebase_rag/config.py index 0b7a2de1..0e4e9480 100644 --- a/java_codebase_rag/config.py +++ b/java_codebase_rag/config.py @@ -202,20 +202,27 @@ def discover_project_root(start: Path) -> Path | None: First match wins (closest to start). Config file takes priority over index directory at the same level. Stops at $HOME inclusive — checks $HOME itself but does not walk past it. Returns None if no marker found. + + A bare ``.java-codebase-rag/`` index directory at ``$HOME`` is intentionally + NOT treated as an anchor (issue #357): a stray home-level index (e.g. an + accidental ``init`` run from home) would otherwise hijack resolution for any + command run from a ``$HOME`` subdir without its own marker, silently reading + and writing the home-level index. A config file at ``$HOME`` still anchors. """ start = start.resolve() home = Path.home().resolve() current = start while True: - # Config file is the primary anchor + # Config file is the primary anchor (valid at every level, including $HOME). if find_yaml_config_file(current) is not None: return current - # Index directory is the secondary anchor (supports indexes without config) - if _has_index_dir(current): + # Index directory is the secondary anchor (supports indexes without config), + # but NOT at $HOME — see the docstring for the cross-project hijack rationale. + if current != home and _has_index_dir(current): return current - # Stop if we've reached home (check home itself, but don't walk past it) + # Stop if we've reached home (config-file check above already handled home) if current == home: return None diff --git a/tests/test_config.py b/tests/test_config.py index 0d97a26c..2784a15b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -136,6 +136,48 @@ def test_discover_project_root_both_markers_same_level(self, tmp_path): result = discover_project_root(tmp_path) assert result == tmp_path + def test_discover_project_root_ignores_stray_index_dir_at_home(self, tmp_path, monkeypatch): + """A bare .java-codebase-rag/ index dir at $HOME must not anchor project + root (issue #357). Otherwise a command run from any $HOME subdir without + its own marker silently resolves to $HOME and reads/writes the home-level + index (cross-project resolution).""" + fake_home = tmp_path / "home" + fake_home.mkdir() + project_dir = fake_home / "project" + project_dir.mkdir() + # Stray index dir at $HOME (e.g. an accidental `init` run from home). + stray_idx = fake_home / ".java-codebase-rag" + stray_idx.mkdir() + (stray_idx / "code_graph.lbug").write_bytes(b"\x00" * 16) + + monkeypatch.setenv("HOME", str(fake_home)) + + result = discover_project_root(project_dir) + assert result is None, "stray ~/.java-codebase-rag/ must not anchor at $HOME (#357)" + + def test_discover_project_root_config_at_home_still_anchors(self, tmp_path, monkeypatch): + """A config file at $HOME still anchors even with a stray index dir beside + it — the #357 fix only demotes the bare index-dir signal at $HOME, not the + config-file anchor (a deliberate ~/.java-codebase-rag.yml is intentional).""" + fake_home = tmp_path / "home" + fake_home.mkdir() + project_dir = fake_home / "project" + project_dir.mkdir() + # Stray index dir at $HOME, NON-empty: a real accidental `init` leaves + # code_graph.lbug behind, so _has_index_dir (which requires non-empty) + # actually sees it. An empty mkdir() would be invisible to the index-anchor + # check and would not represent the documented "stray index dir beside it" + # scenario -- mirrors test_discover_project_root_ignores_stray_index_dir_at_home. + stray_idx = fake_home / ".java-codebase-rag" + stray_idx.mkdir() + (stray_idx / "code_graph.lbug").write_bytes(b"\x00" * 16) + (fake_home / YAML_CONFIG_FILENAMES[0]).write_text("# home config") + + monkeypatch.setenv("HOME", str(fake_home)) + + result = discover_project_root(project_dir) + assert result == fake_home + class TestSourceRootFromYaml: """Tests for source_root YAML field parsing and resolution."""