Files
brain-of-reese/tests/integration/test_import_docs_git.py
T
ducoterra ee3efb28c9
Build and Push Containers / build-and-push-app (push) Successful in 4m35s
Build and Push Containers / build-and-push-db (push) Successful in 14s
phase: 106_document_dates
Everything is verified green. Final report:

**Phase 106 — Document dates (final verification pass; all 10 tasks already complete)**

- Verified all phase artifacts: alembic `0020` (dev DB at `0020`), `doc_dates.py`, git `file_commit_dates`, importer `doc_dates_by_root`/`dates_updated`, both entry-point wirings, date APIs + tree `created_at`/`updated_at`, LLM surfaces (prompt block, `read` line 2, appended `ls` field), `apply_recency_boost` in `retrieve()`, UI columns/badge, admin editor, mock-LLM regex — all present and correct; no defects found, no fixes needed.
- `uv run pytest --cov=app --cov-report=term-missing` → **2299 passed, TOTAL 99%** (>90% ✓)
- `uv run pytest tests/e2e/test_document_dates.py -v --no-cov` → **6/6 passed** in isolation (DB up)
- 12 regression E2E suites (retrieval_quality, whole_document_context, agent_document_tools, ls_tree_drilldown, read_truncation_cap, kb_tree, kb_tree_nav, document_viewer, edit_summaries, import_documents, sync_button, hidden_folders_toggle, smoke) → **all green in isolation**
- `uv run ruff check .` → clean; `uv run pyright` → **0 errors, 0 warnings**

**Completion criteria:** 1) non-null `created_at` + 0020 upgrade/downgrade on dev DB ✓ (real-Alembic integration tests) 2) sync refresh/older/manual-persists/content-reset/no sources_meta bump ✓ 3) zip/tar mtime + future→today ✓ 4) LLM date surfaces + cross-check ✓ 5) UI Created/Updated/badge positions ✓ 6) admin editor set+revert round-trip ✓ 7) old-correct-beats-new-similar (defaults & boost-off) + near-tie + `BOR_RECENCY_BOOST=0` byte-identical ✓ 8) full gate ✓ 9) commit/phase-move — left to harness per instructions.

- **Notable:** recency default tuned 0.001 → **0.0007** (task 07 step 5 explicitly permits; measured margins recorded in `test_recency_boost.py` docstring).
- **Next pending phase:** none — `todo/` holds only this phase.
2026-09-13 19:28:05 -04:00

735 lines
29 KiB
Python

"""Integration test: ``import_docs`` source resolution (phase 28, task
03; phase 35 re-points at the shared resolver; phase 38 adds the local
kind).
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: the phase-53 version bump is stubbed in the
``main()`` tests the same way (the ``sources_version=`` summary token
is asserted against the canned value).
- Effective sources set (phase 35: the shared resolver — stubbed here,
keeping this file's no-real-DB style) → each git URL is cloned/pulled
into ``BOR_SOURCES_DIR/<repo-name>/``; local rows are their existing
directories, walked directly; exactly those dirs are imported.
- DB rows (both kinds) win over ``BOR_GIT_SOURCES`` (the resolver's
``db`` origin — the env list is ignored; the env fallback stays
git-only).
- ``--source`` still wins over the DB rows (no git at all, no resolver
call).
- No sources configured + 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.
- A missing local directory → the same pre-import fail-loud: exit code
1, ``local source missing: <path>`` on stderr, zero import attempts.
"""
from __future__ import annotations
import re
from collections.abc import Iterator
from datetime import datetime
from pathlib import Path
import pytest
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.config import Settings
from app.models import GitSource
from app.rag.importer import ImportSummary
from scripts import import_docs
from scripts.git_sync import GitSyncError
from tests.fakes import FakeEmbedder
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]
def _git_row(url: str, ignore_paths: list[str] | None = None) -> GitSource:
return GitSource(url=url, kind="git", ignore_paths=ignore_paths or [])
def _local_row(
path: str,
ignore_paths: list[str] | None = None,
include_hidden: bool = False,
) -> GitSource:
"""A local row as the phase-38 API stores it: the expanded path in
both ``path`` and the NOT-NULL ``url`` location column (plus the
phase-89 ignore list and the phase-105 hidden-folders flag, both
defaulting off — A4)."""
return GitSource(
url=path, kind="local", path=path,
ignore_paths=ignore_paths or [], include_hidden=include_hidden,
)
@pytest.fixture()
def clean_documents(db: Session) -> Iterator[None]:
"""Phase 105: the real-import CLI tests write ``documents``/
``chunks`` (the canonical KB state) — global, truncated around
every such test."""
db.execute(text("TRUNCATE chunks, documents"))
db.commit()
yield
db.execute(text("TRUNCATE chunks, documents"))
db.commit()
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,
ignore_by_root: dict[str, list[str]] | None = None, # phase 89
include_hidden_by_root: dict[str, bool] | None = None, # phase 105
doc_dates_by_root: dict[str, dict[str, datetime]] | None = None, # phase 106
) -> ImportSummary:
self.calls.append(
{"sources": list(sources), "prune": prune, "limit": limit,
"ignore_by_root": ignore_by_root,
"include_hidden_by_root": include_hidden_by_root,
"doc_dates_by_root": doc_dates_by_root}
)
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
def _stub_bump(monkeypatch: pytest.MonkeyPatch) -> list[None]:
"""Stub the phase-53 version bump (this file keeps its no-real-DB
style for the counter — the fake import already avoids the KB
tables). Returns the call record; the canned new version is 1."""
bumps: list[None] = []
def fake_bump(session: object) -> int:
bumps.append(None)
return 1
monkeypatch.setattr(import_docs, "bump_sources_version", fake_bump)
return bumps
def _stub_folder_summaries(monkeypatch: pytest.MonkeyPatch) -> list[dict]:
"""Stub the phase-94 folder-summary regeneration (this file keeps
its no-real-DB / no-network style — the real generator would read
the global ``documents`` table and burn ``lite`` calls). Returns
the call record; the canned stats are the zero dict."""
calls: list[dict] = []
async def fake_generate(
db: object, llm: object, *, skip: bool = False, only_missing: bool = False
) -> dict[str, int]:
calls.append({"skip": skip, "only_missing": only_missing})
return {"generated": 0, "failed": 0, "pruned": 0}
monkeypatch.setattr(import_docs, "generate_folder_summaries", fake_generate)
return calls
# --- 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)
# Phase 35: resolution goes through the shared resolver (stubbed —
# this file keeps its no-real-DB style); the rows are the env list,
# surfaced as synthetic git rows.
monkeypatch.setattr(
import_docs,
"effective_sources",
lambda db: (
[_git_row("https://host/a/homelab.git"), _git_row("git@host:user/deploy.git")],
"env",
),
)
settings = _settings(
git_sources="https://host/a/homelab.git, git@host:user/deploy.git ,",
sources_dir=str(tmp_path / "bor"),
)
sources, ignore_map, hidden_map, date_map = import_docs._resolve_sources(None, settings)
assert sources == [tmp_path / "bor" / "homelab", tmp_path / "bor" / "deploy"]
assert ignore_map == {} # phase 89: no row carries a list → empty map
# Phase 105: flag-off rows still contribute their root — with False
# (A4), keyed by the SAME root string the importer sees.
assert hidden_map == {
str(tmp_path / "bor" / "homelab"): False,
str(tmp_path / "bor" / "deploy"): False,
}
# Phase 106: git rows are listed with their checkout's date walk —
# the fake checkouts are not git repos, so the walk fails soft to
# ``{}`` (the importer would then take the mtime fallback).
assert date_map == {
str(tmp_path / "bor" / "homelab"): {},
str(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, ignore_map, hidden_map, date_map = import_docs._resolve_sources([manual], settings)
assert sources == [manual]
assert ignore_map == {} # phase 89: manual dirs have no rows → no ignore
assert hidden_map == {} # phase 105: manual dirs have no rows → hidden skipped
# Phase 106: manual dirs have no rows (no clone) → no date map
# entries (the importer's mtime fallback applies).
assert date_map == {}
assert calls == [] # git is never touched when --source is given
def test_resolve_sources_db_rows_win_over_env(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Phase 35: the resolver's ``db`` origin (table has rows) — the
``BOR_GIT_SOURCES`` list must be ignored; only the DB repo is cloned."""
calls, fake = _fake_clone_factory()
monkeypatch.setattr(import_docs, "clone_or_pull", fake)
monkeypatch.setattr(
import_docs,
"effective_sources",
lambda db: ([_git_row("https://db.example/only.git")], "db"),
)
settings = _settings(
git_sources="https://env.example/ignored.git",
sources_dir=str(tmp_path / "bor"),
)
sources, ignore_map, hidden_map, date_map = import_docs._resolve_sources(None, settings)
assert sources == [tmp_path / "bor" / "only"]
assert ignore_map == {} # phase 89: no row carries a list → empty map
# Phase 105: the default-flag row contributes its root with False (A4).
assert hidden_map == {str(tmp_path / "bor" / "only"): False}
# Phase 106: the git row's (fake, non-repo) checkout fails soft → {}.
assert date_map == {str(tmp_path / "bor" / "only"): {}}
assert calls == [("https://db.example/only.git", tmp_path / "bor" / "only")]
def test_resolve_sources_defaults_when_nothing_configured(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Both origins empty (the resolver's ``([], "env")``) → legacy dirs.
monkeypatch.setattr(import_docs, "effective_sources", lambda db: ([], "env"))
sources, ignore_map, hidden_map, date_map = import_docs._resolve_sources(None, _settings())
assert sources == [p.expanduser() for p in import_docs.DEFAULT_SOURCES]
assert ignore_map == {} # phase 89: the legacy fallback has no rows
assert hidden_map == {} # phase 105: the legacy fallback has no rows
# Phase 106: the legacy fallback has no rows (no clone) → mtime
# fallback for every file.
assert date_map == {}
def test_resolve_sources_rows_branch_builds_ignore_map(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Phase 89: the rows branch returns each row's ignore list keyed by
the resolved root string — the local row's directory, the git row's
checkout dir; a row without a list contributes nothing to the map."""
calls, fake = _fake_clone_factory()
monkeypatch.setattr(import_docs, "clone_or_pull", fake)
local_dir = tmp_path / "LocalDocs"
local_dir.mkdir()
monkeypatch.setattr(
import_docs,
"effective_sources",
lambda db: (
[
_git_row("https://db.example/only.git"),
_local_row(str(local_dir), ignore_paths=["ignore/"]),
],
"db",
),
)
settings = _settings(sources_dir=str(tmp_path / "bor"))
sources, ignore_map, hidden_map, date_map = import_docs._resolve_sources(None, settings)
assert sources == [tmp_path / "bor" / "only", local_dir]
# Keyed by the SAME string the importer sees (the root, not the name).
assert ignore_map == {str(local_dir): ["ignore/"]}
# Phase 105: both rows are flag-off → per-root False entries (A4).
assert hidden_map == {str(tmp_path / "bor" / "only"): False, str(local_dir): False}
# Phase 106: ONLY the git row is listed (local rows take the mtime
# fallback); the fake checkout's date walk fails soft to ``{}``.
assert date_map == {str(tmp_path / "bor" / "only"): {}}
def test_resolve_sources_two_rows_sharing_root_string_extend(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Phase 89 collision rule: two rows resolving to the SAME root
string (the sibling/repo-name edge — ``…/shared`` and
``…/shared.git``) get the UNION of their lists (extend, not
replace), in row order."""
calls, fake = _fake_clone_factory()
monkeypatch.setattr(import_docs, "clone_or_pull", fake)
monkeypatch.setattr(
import_docs,
"effective_sources",
lambda db: (
[
_git_row("https://a.example/shared", ignore_paths=["a/"]),
_git_row("https://a.example/shared.git", ignore_paths=["b"]),
],
"db",
),
)
settings = _settings(sources_dir=str(tmp_path / "bor"))
sources, ignore_map, hidden_map, date_map = import_docs._resolve_sources(None, settings)
shared = str(tmp_path / "bor" / "shared")
assert sources == [tmp_path / "bor" / "shared", tmp_path / "bor" / "shared"]
assert ignore_map == {shared: ["a/", "b"]} # union, row order
# Phase 105 collision: the shared root gets the OR of the flags —
# both rows off here, so one False entry for the one root string.
assert hidden_map == {shared: False}
# Phase 106 collision: both git rows resolve to the SAME root — one
# date walk for the one root string (last row's walk wins, both
# fail soft to ``{}`` for the fake checkout).
assert date_map == {shared: {}}
def test_main_rows_branch_passes_ignore_map_to_import(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
"""Phase 89: a ``kind=local`` row carrying ``ignore_paths`` →
``main`` passes the per-root map to ``import_sources`` (keyed by
the directory string, prune flag unchanged)."""
settings = _settings(sources_dir=str(tmp_path / "bor"))
monkeypatch.setattr(import_docs, "get_settings", lambda: settings)
local_dir = tmp_path / "LocalDocs"
local_dir.mkdir()
(local_dir / "keep.md").write_text("# Keep\nin scope\n", encoding="utf-8")
(local_dir / "ignore").mkdir()
(local_dir / "ignore" / "secret.md").write_text("# Secret\nignored\n",
encoding="utf-8")
monkeypatch.setattr(
import_docs,
"effective_sources",
lambda db: ([_local_row(str(local_dir), ignore_paths=["ignore/"])], "db"),
)
fake_import = FakeImportSources()
monkeypatch.setattr(import_docs, "import_sources", fake_import)
_stub_bump(monkeypatch)
_stub_folder_summaries(monkeypatch)
rc = import_docs.main([])
assert rc == 0
call = fake_import.calls[0]
assert call["sources"] == [local_dir]
assert call["ignore_by_root"] == {str(local_dir): ["ignore/"]}
# Phase 105: the default-flag row passes the per-root map too — a
# False entry, not an absent key (the importer reads it per root).
assert call["include_hidden_by_root"] == {str(local_dir): False}
# Phase 106: the local-only resolution contributes no date map
# (no clone — the importer's mtime fallback applies).
assert call["doc_dates_by_root"] == {}
assert call["prune"] is False # the CLI's no-prune default is unchanged
# --- phase 105: per-row hidden-folders flag ---------------------------------
def test_main_rows_branch_passes_include_hidden_map_to_import(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
"""Phase 105: a ``kind=local`` row with ``include_hidden=True`` →
``main`` passes the per-root flag map to ``import_sources`` (keyed
by the directory string — the map is built, not lost)."""
settings = _settings(sources_dir=str(tmp_path / "bor"))
monkeypatch.setattr(import_docs, "get_settings", lambda: settings)
local_dir = tmp_path / "LocalDocs"
local_dir.mkdir()
(local_dir / "visible.md").write_text("# Visible\nin scope\n", encoding="utf-8")
(local_dir / ".hidden").mkdir()
(local_dir / ".hidden" / "note.md").write_text("# Note\nhidden\n", encoding="utf-8")
monkeypatch.setattr(
import_docs,
"effective_sources",
lambda db: ([_local_row(str(local_dir), include_hidden=True)], "db"),
)
fake_import = FakeImportSources()
monkeypatch.setattr(import_docs, "import_sources", fake_import)
_stub_bump(monkeypatch)
_stub_folder_summaries(monkeypatch)
rc = import_docs.main([])
assert rc == 0
call = fake_import.calls[0]
assert call["sources"] == [local_dir]
assert call["include_hidden_by_root"] == {str(local_dir): True}
assert call["ignore_by_root"] == {} # the row carries no ignore list
assert call["prune"] is False # the CLI's no-prune default is unchanged
def _stub_overview(monkeypatch: pytest.MonkeyPatch) -> None:
"""Stub the phase-31 overview regeneration (the CLI's real-import
tests: the deterministic ``FakeEmbedder`` must not burn its canned
``chat`` on the KB outline — the import is what is under test)."""
async def fake_overview(llm: object, session: object = None) -> bool:
return False
monkeypatch.setattr(import_docs, "regenerate_overview", fake_overview)
def _kb_docs(db: Session) -> set[tuple[str, str]]:
"""The (source, path) pairs of the ``documents`` table."""
rows = db.execute(text("SELECT source, path FROM documents")).all()
return {(row.source, row.path) for row in rows}
def test_main_local_row_include_hidden_true_indexes_hidden_file(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
db: Session,
capsys: pytest.CaptureFixture[str],
clean_documents: None,
) -> None:
"""Phase 105 (A1): the CLI's DB-row path with the flag ON — the
file inside the hidden folder is indexed, embedded, and counted
like any visible file (real import over a host temp dir, the
deterministic ``FakeEmbedder``; the regression this phase most
plausibly breaks — the CLI's map built but lost — would leave it
out)."""
settings = _settings(sources_dir=str(tmp_path / "bor"))
monkeypatch.setattr(import_docs, "get_settings", lambda: settings)
local_dir = tmp_path / "LocalDocs"
local_dir.mkdir()
(local_dir / "visible.md").write_text("# Visible\nin scope\n", encoding="utf-8")
(local_dir / ".hidden").mkdir()
(local_dir / ".hidden" / "note.md").write_text("# Note\nhidden\n", encoding="utf-8")
monkeypatch.setattr(
import_docs,
"effective_sources",
lambda db: ([_local_row(str(local_dir), include_hidden=True)], "db"),
)
monkeypatch.setattr(import_docs, "LLMClient", lambda: FakeEmbedder())
_stub_overview(monkeypatch)
_stub_bump(monkeypatch)
_stub_folder_summaries(monkeypatch)
rc = import_docs.main([])
assert rc == 0
# The hidden file has a documents row next to the visible one.
assert _kb_docs(db) == {
("LocalDocs", "visible.md"),
("LocalDocs", ".hidden/note.md"),
}
out = capsys.readouterr().out
assert "added=2" in out # both files were imported
def test_main_local_row_include_hidden_false_skips_hidden_file(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
db: Session,
capsys: pytest.CaptureFixture[str],
clean_documents: None,
) -> None:
"""Phase 105 (A4): the same fixture with the default flag (off) —
the hidden-folder file never enters the KB (the byte-identical
pre-phase-105 walk): no documents row, and the summary line counts
only the visible file."""
settings = _settings(sources_dir=str(tmp_path / "bor"))
monkeypatch.setattr(import_docs, "get_settings", lambda: settings)
local_dir = tmp_path / "LocalDocs"
local_dir.mkdir()
(local_dir / "visible.md").write_text("# Visible\nin scope\n", encoding="utf-8")
(local_dir / ".hidden").mkdir()
(local_dir / ".hidden" / "note.md").write_text("# Note\nhidden\n", encoding="utf-8")
monkeypatch.setattr(
import_docs,
"effective_sources",
lambda db: ([_local_row(str(local_dir))], "db"), # flag defaults to False
)
monkeypatch.setattr(import_docs, "LLMClient", lambda: FakeEmbedder())
_stub_overview(monkeypatch)
_stub_bump(monkeypatch)
_stub_folder_summaries(monkeypatch)
rc = import_docs.main([])
assert rc == 0
assert _kb_docs(db) == {("LocalDocs", "visible.md")}
out = capsys.readouterr().out
assert "added=1" in out # the hidden file was never walked
# --- 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)
monkeypatch.setattr(
import_docs,
"effective_sources",
lambda db: (
[_git_row("https://host/a/homelab.git"), _git_row("https://host/a/deploy.git")],
"env",
),
)
calls, fake = _fake_clone_factory()
monkeypatch.setattr(import_docs, "clone_or_pull", fake)
fake_import = FakeImportSources()
monkeypatch.setattr(import_docs, "import_sources", fake_import)
bumps = _stub_bump(monkeypatch)
_stub_folder_summaries(monkeypatch)
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",
]
# Phase 106: the git rows' (fake, non-repo) checkouts fail soft to
# ``{}`` — but the roots ARE listed (the CLI feeds the map).
assert fake_import.calls[0]["doc_dates_by_root"] == {
str(tmp_path / "bor" / "homelab"): {},
str(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).
out = capsys.readouterr().out
assert "added=1" in out
# Phase 53: a KB-changing run bumps the sources version exactly
# once and reports it (stubbed — this file keeps its no-real-DB
# style for the counter, like the fake import above).
assert len(bumps) == 1
assert "sources_version=1" in out
def test_main_cli_source_still_imports_manual_dir(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> 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)
bumps = _stub_bump(monkeypatch)
_stub_folder_summaries(monkeypatch)
rc = import_docs.main(["--source", str(manual)])
assert rc == 0
assert calls == []
assert fake_import.calls[0]["sources"] == [manual]
# Phase 106: manual --source has no rows (no clone) → no date map
# (the importer's mtime fallback applies).
assert fake_import.calls[0]["doc_dates_by_root"] == {}
assert fake_import.calls[0]["prune"] is False
# Phase 53: a manual --source run that changes the KB bumps exactly
# once (the CLI is the other canonical sync path).
assert len(bumps) == 1
assert "sources_version=1" in capsys.readouterr().out
def test_resolve_sources_mixed_git_and_local(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Phase 38: DB rows of both kinds — the git row is cloned into
``BOR_SOURCES_DIR``, the local row is its existing directory itself
(no clone), in row order; the env list is ignored."""
calls, fake = _fake_clone_factory()
monkeypatch.setattr(import_docs, "clone_or_pull", fake)
local_dir = tmp_path / "LocalDocs"
local_dir.mkdir()
(local_dir / "a.md").write_text("# A\nlocal fixture\n", encoding="utf-8")
monkeypatch.setattr(
import_docs,
"effective_sources",
lambda db: (
[_git_row("https://db.example/only.git"), _local_row(str(local_dir))],
"db",
),
)
settings = _settings(
git_sources="https://env.example/ignored.git",
sources_dir=str(tmp_path / "bor"),
)
sources, ignore_map, hidden_map, date_map = import_docs._resolve_sources(None, settings)
assert sources == [tmp_path / "bor" / "only", local_dir]
assert ignore_map == {} # phase 89: neither row carries a list
# Phase 105: both rows default-flag → per-root False entries (A4).
assert hidden_map == {str(tmp_path / "bor" / "only"): False, str(local_dir): False}
# Phase 106: only the git row is listed (local takes the mtime
# fallback); the fake checkout's date walk fails soft to ``{}``.
assert date_map == {str(tmp_path / "bor" / "only"): {}}
assert calls == [("https://db.example/only.git", tmp_path / "bor" / "only")]
def test_resolve_sources_missing_local_dir_aborts(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Phase 38: a local row whose directory is gone at run time →
``GitSyncError`` naming the path, before any import (the same
pre-import fail-loud as a failing git clone)."""
monkeypatch.setattr(import_docs, "clone_or_pull", _fake_clone_factory()[1])
missing = tmp_path / "Gone"
monkeypatch.setattr(
import_docs,
"effective_sources",
lambda db: ([_local_row(str(missing))], "db"),
)
settings = _settings(sources_dir=str(tmp_path / "bor"))
with pytest.raises(GitSyncError, match=f"local source missing: {re.escape(str(missing))}"):
import_docs._resolve_sources(None, settings)
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)
monkeypatch.setattr(
import_docs,
"effective_sources",
lambda db: ([_git_row("https://host/a/bad.git")], "env"),
)
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: source 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()
def test_main_missing_local_dir_aborts_before_import(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Phase 38: a DB local row whose directory is missing → exit code
1, ``local source missing: <path>`` on stderr, zero import attempts
(no ``--source`` given, so the DB row is what should have been
imported)."""
settings = _settings(sources_dir=str(tmp_path / "bor"))
monkeypatch.setattr(import_docs, "get_settings", lambda: settings)
missing = tmp_path / "Gone"
monkeypatch.setattr(
import_docs,
"effective_sources",
lambda db: ([_local_row(str(missing))], "db"),
)
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: source sync failed" in err
assert f"local source missing: {missing}" in err # the path is named
assert fake_import.calls == [] # no partial import