"""Unit: the shared source resolver (phase 35, task 03; local kind, phase 38, task 03). ``effective_sources`` is driven with a stubbed session (no Postgres) and a fresh ``Settings(_env_file=None)`` env (monkeypatched into the resolver's module — the dev ``.env`` never leaks in, same pattern as the integration suites): DB rows of **both kinds** win in ``(added_at, id)`` order (env ignored), the ``BOR_GIT_SOURCES`` list is a git-only fallback while the table is empty (surfaced as synthetic ``kind='git'`` rows, phase-28 CSV parse reused), and both-empty yields ``([], "env")`` so the callers keep their fail-loud behavior. The phase-35 ``effective_git_sources`` alias is kept covered as well: it returns the repo URLs of the git rows only, same origin. """ from __future__ import annotations import re from typing import Any import pytest from app.config import Settings from app.models import GitSource from app.rag import git_sources as resolver class _FakeScalars: """The ``.scalars(stmt).all()`` tail of the resolver's query.""" def __init__(self, rows: list[GitSource]) -> None: self._rows = rows def all(self) -> list[GitSource]: return self._rows class _FakeSession: """Just enough of a SQLAlchemy session for the resolver (test_steering pattern). Records the statement so the ordering can be asserted.""" def __init__(self, rows: list[GitSource]) -> None: self._rows = rows self.statements: list[Any] = [] def scalars(self, stmt: Any) -> _FakeScalars: self.statements.append(stmt) return _FakeScalars(self._rows) def _stub_env(monkeypatch: pytest.MonkeyPatch, git_sources: str) -> None: """Point the resolver's env fallback at a fresh Settings (no .env).""" monkeypatch.setattr( resolver, "get_settings", lambda: Settings(_env_file=None, git_sources=git_sources), # pyright: ignore[reportCallIssue] ) def _git(url: str) -> GitSource: return GitSource(url=url, kind="git") def _local(path: str) -> GitSource: """A local row as phase-38 task 02 stores it: the expanded path in both ``path`` and the NOT-NULL ``url`` location column.""" return GitSource(url=path, kind="local", path=path) # --- the three branches ---------------------------------------------------- def test_db_rows_win_over_env(monkeypatch: pytest.MonkeyPatch) -> None: """Seeded table + env set → the DB rows, origin ``db`` (env ignored).""" _stub_env(monkeypatch, "https://env.example/ignored.git") session = _FakeSession( [GitSource(url="https://db.example/a.git"), GitSource(url="https://db.example/b.git")] ) rows, origin = resolver.effective_sources(session) # pyright: ignore[reportArgumentType] assert [row.url for row in rows] == ["https://db.example/a.git", "https://db.example/b.git"] assert origin == "db" def test_mixed_kinds_returned_in_row_order(monkeypatch: pytest.MonkeyPatch) -> None: """Phase 38: the table holds git **and** local rows — both come back, in ``(added_at, id)`` row order, kinds and paths intact, env ignored.""" _stub_env(monkeypatch, "https://env.example/ignored.git") local = "/abs/notes" session = _FakeSession( [_git("https://db.example/a.git"), _local(local), _git("git@db.example:b.git")] ) rows, origin = resolver.effective_sources(session) # pyright: ignore[reportArgumentType] assert origin == "db" assert [row.kind for row in rows] == ["git", "local", "git"] assert [row.url for row in rows] == [ "https://db.example/a.git", local, "git@db.example:b.git", ] assert [row.path for row in rows] == [None, local, None] def test_env_fallback_git_only_while_table_empty(monkeypatch: pytest.MonkeyPatch) -> None: """Empty table + env set → synthetic ``kind='git'`` rows, origin ``env``. The env fallback is git-only (no local rows can come from it); the CSV parse is ``Settings.git_source_list`` itself (phase 28): whitespace-trimmed, empty entries dropped, order preserved. """ _stub_env(monkeypatch, " https://env.example/one.git , ,git@env.example:two.git ") session = _FakeSession([]) rows, origin = resolver.effective_sources(session) # pyright: ignore[reportArgumentType] assert origin == "env" assert [row.url for row in rows] == ["https://env.example/one.git", "git@env.example:two.git"] assert all(row.kind == "git" for row in rows) assert all(row.path is None for row in rows) def test_both_empty_returns_empty_env(monkeypatch: pytest.MonkeyPatch) -> None: """Empty table + unset env → ``([], "env")`` — the callers fail loudly.""" _stub_env(monkeypatch, " , ") # whitespace-only is just as unconfigured as empty session = _FakeSession([]) rows, origin = resolver.effective_sources(session) # pyright: ignore[reportArgumentType] assert rows == [] assert origin == "env" # --- the ordering ---------------------------------------------------------- def test_db_rows_ordered_by_added_at_then_id(monkeypatch: pytest.MonkeyPatch) -> None: """The statement orders by ``(added_at, id)`` — oldest first, the id tie-break deciding same-timestamp inserts (matches the API's GET).""" _stub_env(monkeypatch, "") session = _FakeSession([_git("https://db.example/a.git")]) resolver.effective_sources(session) # pyright: ignore[reportArgumentType] assert len(session.statements) == 1 compiled = str(session.statements[0].compile(compile_kwargs={"literal_binds": True})) assert re.search( r"ORDER BY\s+git_sources\.added_at ASC,\s+git_sources\.id ASC", compiled ), compiled # --- the phase-35 back-compat alias ---------------------------------------- def test_alias_returns_git_urls_only(monkeypatch: pytest.MonkeyPatch) -> None: """``effective_git_sources`` (phase-35 name) — repo URLs of the git rows only; local rows are filtered out (they carry no clone URL), origin unchanged.""" _stub_env(monkeypatch, "") session = _FakeSession( [_git("https://db.example/a.git"), _local("/abs/notes"), _git("git@db.example:b.git")] ) urls, origin = resolver.effective_git_sources(session) # pyright: ignore[reportArgumentType] assert urls == ["https://db.example/a.git", "git@db.example:b.git"] assert origin == "db" def test_alias_env_fallback_unchanged(monkeypatch: pytest.MonkeyPatch) -> None: """The alias's env fallback is byte-identical to the phase-35 one (git-only CSV list, origin ``env``).""" _stub_env(monkeypatch, " https://env.example/one.git , ") session = _FakeSession([]) urls, origin = resolver.effective_git_sources(session) # pyright: ignore[reportArgumentType] assert urls == ["https://env.example/one.git"] assert origin == "env" # --- phase 145 (task 01): origin_for_source (the read result's origin) --- class _GitOnlySession: """The origin resolver's session (phase 145, task 01): emulates the resolver's ``kind = 'git'`` WHERE clause — only the git rows ever reach the caller, exactly like the real query; the seeded local rows prove they are never candidates.""" def __init__(self, rows: list[GitSource]) -> None: self._rows = [row for row in rows if row.kind == "git"] self.statements: list[Any] = [] def scalars(self, stmt: Any) -> _FakeScalars: self.statements.append(stmt) return _FakeScalars(self._rows) # --- the basename mapping -------------------------------------------------- def test_origin_basename_mapping() -> None: """The URL's path component — after the ``://``, or after the ``:`` for scp-style ``git@host:owner/repo`` — trailing ``/`` stripped, a single trailing ``.git`` suffix stripped, last segment taken. An ``@`` inside the PATH is not userinfo and stays (no split, no strip).""" cases = { "https://github.com/owner/myrepo.git": "myrepo", # the .git suffix "https://github.com/owner/myrepo": "myrepo", # no suffix, same dir "https://github.com/owner/myrepo/": "myrepo", # trailing slash "https://github.com/owner/myrepo.git/": "myrepo", # both "git@github.com:owner/myrepo.git": "myrepo", # scp-style + suffix "git@github.com:owner/myrepo": "myrepo", # scp-style, no suffix "ssh://git@host/owner/myrepo.git": "myrepo", # ssh scheme, @ in the host part "https://host/a@b/myrepo.git": "myrepo", # @ inside the PATH — not userinfo } for url, expected in cases.items(): assert resolver._url_basename(url) == expected, url # --- the match: sanitize + branch ------------------------------------------ def test_origin_exact_match_is_sanitized() -> None: """EXACTLY ONE match → the phase-121 masked URL + the branch — a legacy embedded-credential row never leaks its secret (the stored value is untouched; the OUTPUT mask does the work).""" session = _GitOnlySession( [GitSource(url="https://user:tok@host/x.git", kind="git", branch=None)] ) assert resolver.origin_for_source(session, "x") == ("https://host/x.git", None) # pyright: ignore[reportArgumentType] def test_origin_branch_passthrough() -> None: """The branch rides through as-is: pinned → the name (the read result's ``branch:`` line), NULL → ``None`` (the default branch — no ``branch:`` line).""" session = _GitOnlySession( [GitSource(url="https://host/x.git", kind="git", branch="release/1.2")] ) assert resolver.origin_for_source(session, "x") == ("https://host/x.git", "release/1.2") # pyright: ignore[reportArgumentType] def test_origin_local_rows_never_origins() -> None: """Local rows are never origins (nothing to link to from the user's browser): a source that exists ONLY as a local row gets no origin (zero git candidates); a local + git pair resolves to the git row.""" local = GitSource(url="/srv/repo", kind="local", path="/srv/repo") session = _GitOnlySession([local]) # the WHERE clause drops it assert resolver.origin_for_source(session, "repo") is None # pyright: ignore[reportArgumentType] session = _GitOnlySession( [local, GitSource(url="https://host/repo.git", kind="git", branch=None)] ) assert resolver.origin_for_source(session, "repo") == ("https://host/repo.git", None) # pyright: ignore[reportArgumentType] # --- the no-guess rows: None + WARNING -------------------------------------- def test_origin_zero_match_is_none_with_warning( caplog: pytest.LogCaptureFixture, ) -> None: """No git row's basename matches (a plain local source) → ``None`` + a WARNING naming the source — the read result stays byte-identical to pre-phase.""" session = _GitOnlySession([GitSource(url="https://host/other.git", kind="git")]) assert resolver.origin_for_source(session, "repo") is None # pyright: ignore[reportArgumentType] warnings = [record for record in caplog.records if record.levelname == "WARNING"] assert len(warnings) == 1 assert "repo" in warnings[0].message def test_origin_ambiguous_is_none_with_warning( caplog: pytest.LogCaptureFixture, ) -> None: """TWO-OR-MORE rows share the basename → ``None`` + a WARNING naming the sanitized candidates (never guess — a wrong origin would ride into every answer about the document). The candidates are logged SANITIZED: a legacy embedded credential stays out of the log too.""" session = _GitOnlySession( [ GitSource(url="https://user:tok@a.example/org/repo.git", kind="git"), GitSource(url="git@b.example:other/repo.git", kind="git"), ] ) assert resolver.origin_for_source(session, "repo") is None # pyright: ignore[reportArgumentType] warnings = [record for record in caplog.records if record.levelname == "WARNING"] assert len(warnings) == 1 assert "https://a.example/org/repo.git" in warnings[0].message assert "git@b.example:other/repo.git" in warnings[0].message assert "tok" not in warnings[0].message