"""Integration test: ``import_docs`` git-source resolution (phase 28, task 03). Drives ``scripts.import_docs`` end to end with a fake ``clone_or_pull`` (no real git, no network) and a recording fake ``import_sources`` (no real DB), covering: - ``BOR_GIT_SOURCES`` set → each URL is cloned/pulled into ``BOR_SOURCES_DIR//`` and exactly those dirs are imported. - ``--source`` still wins over ``BOR_GIT_SOURCES`` (no git at all). - No git sources + no ``--source`` → the legacy ``DEFAULT_SOURCES``. - A failing git sync → exit code 1, an error naming the failing repo on stderr, and **zero** import attempts. """ from __future__ import annotations from pathlib import Path import pytest from app.config import Settings from app.rag.importer import ImportSummary from scripts import import_docs from scripts.git_sync import GitSyncError def _settings(git_sources: str = "", sources_dir: str = "~/bor-sources") -> Settings: """Fresh settings (no .env file); explicit kwargs beat any env leaks.""" return Settings(_env_file=None, git_sources=git_sources, sources_dir=sources_dir) # pyright: ignore[reportCallIssue] class FakeImportSources: """Records every ``import_sources`` call instead of touching a DB.""" def __init__(self) -> None: self.calls: list[dict] = [] async def __call__( self, sources: list[Path], llm: object, *, prune: bool = False, limit: int | None = None, ) -> ImportSummary: self.calls.append({"sources": list(sources), "prune": prune, "limit": limit}) return ImportSummary(files=1, added=1) def _fake_clone_factory() -> tuple[list[tuple[str, Path]], object]: """A ``clone_or_pull`` that materialises a checkout with one .md file.""" calls: list[tuple[str, Path]] = [] def fake_clone_or_pull(url: str, dest: Path | str) -> Path: dest = Path(dest) dest.mkdir(parents=True, exist_ok=True) (dest / "notes.md").write_text(f"# {dest.name}\ncontent for the KB\n", encoding="utf-8") calls.append((url, dest)) return dest return calls, fake_clone_or_pull # --- repo_name ------------------------------------------------------------- @pytest.mark.parametrize( ("url", "name"), [ ("https://github.com/user/homelab.git", "homelab"), ("https://github.com/user/homelab", "homelab"), ("git@github.com:user/homelab.git", "homelab"), ("git@github.com:homelab.git", "homelab"), ("ssh://git@host:2222/group/deployments.git", "deployments"), ("https://git.reeseapps.com:8443/proj/notes", "notes"), ], ) def test_repo_name(url: str, name: str) -> None: assert import_docs.repo_name(url) == name def test_repo_name_slug_fallback() -> None: # No usable basename (path ends in the .git suffix itself) → slug. assert import_docs.repo_name("https://host/.git") == "https-host" # --- _resolve_sources ------------------------------------------------------ def test_resolve_sources_git_urls_cloned_into_sources_dir( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: calls, fake = _fake_clone_factory() monkeypatch.setattr(import_docs, "clone_or_pull", fake) settings = _settings( git_sources="https://host/a/homelab.git, git@host:user/deploy.git ,", sources_dir=str(tmp_path / "bor"), ) sources = import_docs._resolve_sources(None, settings) assert sources == [tmp_path / "bor" / "homelab", tmp_path / "bor" / "deploy"] assert calls == [ ("https://host/a/homelab.git", tmp_path / "bor" / "homelab"), ("git@host:user/deploy.git", tmp_path / "bor" / "deploy"), ] def test_resolve_sources_cli_source_wins( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: calls, fake = _fake_clone_factory() monkeypatch.setattr(import_docs, "clone_or_pull", fake) settings = _settings(git_sources="https://host/a/repo.git") manual = tmp_path / "Manual" sources = import_docs._resolve_sources([manual], settings) assert sources == [manual] assert calls == [] # git is never touched when --source is given def test_resolve_sources_defaults_when_nothing_configured() -> None: sources = import_docs._resolve_sources(None, _settings()) assert sources == [p.expanduser() for p in import_docs.DEFAULT_SOURCES] # --- main() ---------------------------------------------------------------- def test_main_git_sources_clone_then_import( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str], ) -> None: settings = _settings( git_sources="https://host/a/homelab.git,https://host/a/deploy.git", sources_dir=str(tmp_path / "bor"), ) monkeypatch.setattr(import_docs, "get_settings", lambda: settings) calls, fake = _fake_clone_factory() monkeypatch.setattr(import_docs, "clone_or_pull", fake) fake_import = FakeImportSources() monkeypatch.setattr(import_docs, "import_sources", fake_import) rc = import_docs.main([]) assert rc == 0 assert [(url, dest) for url, dest in calls] == [ ("https://host/a/homelab.git", tmp_path / "bor" / "homelab"), ("https://host/a/deploy.git", tmp_path / "bor" / "deploy"), ] assert len(fake_import.calls) == 1 # The cloned checkouts are exactly what gets imported. assert fake_import.calls[0]["sources"] == [ tmp_path / "bor" / "homelab", tmp_path / "bor" / "deploy", ] for dest in (tmp_path / "bor" / "homelab", tmp_path / "bor" / "deploy"): assert (dest / "notes.md").is_file() # The final summary print reflects the import (added > 0). assert "added=1" in capsys.readouterr().out def test_main_cli_source_still_imports_manual_dir( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: manual = tmp_path / "manual" manual.mkdir() (manual / "a.md").write_text("# A\nhi\n", encoding="utf-8") # BOR_GIT_SOURCES is set but must be ignored — --source always wins. settings = _settings(git_sources="https://host/a/repo.git") monkeypatch.setattr(import_docs, "get_settings", lambda: settings) calls, fake = _fake_clone_factory() monkeypatch.setattr(import_docs, "clone_or_pull", fake) fake_import = FakeImportSources() monkeypatch.setattr(import_docs, "import_sources", fake_import) rc = import_docs.main(["--source", str(manual)]) assert rc == 0 assert calls == [] assert fake_import.calls[0]["sources"] == [manual] assert fake_import.calls[0]["prune"] is False def test_main_git_failure_aborts_before_import( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str] ) -> None: settings = _settings( git_sources="https://host/a/bad.git", sources_dir=str(tmp_path / "bor"), ) monkeypatch.setattr(import_docs, "get_settings", lambda: settings) def failing_clone(url: str, dest: Path | str) -> Path: raise GitSyncError( f"git clone --depth 1 {url} failed (exit 128): " "fatal: repository not found" ) monkeypatch.setattr(import_docs, "clone_or_pull", failing_clone) fake_import = FakeImportSources() monkeypatch.setattr(import_docs, "import_sources", fake_import) rc = import_docs.main([]) assert rc == 1 err = capsys.readouterr().err assert "import_docs: git sync failed" in err assert "bad.git" in err # the failing repo is named assert fake_import.calls == [] # no partial import assert not (tmp_path / "bor").exists()