Files
brain-of-reese/tests/unit/test_regen_folder_summaries.py
T
ducoterra 742eef955b
Build and Push Containers / build-and-push-app (push) Successful in 2m25s
Build and Push Containers / build-and-push-db (push) Successful in 13s
phase: 155_folder_summary_voice
All verification is complete — the whole project is 100% green. Final report:

**Phase 155 final verification pass — complete.**

**Verified (all tasks 01–04 done in `complete/`):**
- Instruction nudge in `app/rag/folder_summaries.py` (forbidden openers as the pin, 4 constraints preserved, docstring cites the phase)
- `scripts/regen_folder_summaries.py` (import_docs idiom, full mode, cron-safe summary line, fail-loud boot)
- Unit pins, integration suite, mock voice update, dedicated E2E suite

**Test / lint / coverage:**
- `uv run pytest --cov=app -q` → 100% passed (exit 0, zero failures); coverage **99%** on `app/` (>90% ✓)
- `uv run pytest tests/e2e/test_folder_summary_voice.py -v --no-cov` → **3 passed** in isolation
- `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings

**Completion criteria:**
1. Marker + header prefix byte-identical — ✅ (git vs HEAD + unit pins verbatim, green)
2. Regen script full regeneration — ✅ (live smoke: exit 0, `folders=… limit=7` summary line; misconfig boot → named stderr, exit 1; `updated_at`-advance/no-prune/canned-text semantics proven by the integration suite, green)
3. Sync path untouched — ✅ (0 diffs to `app/api/sync.py` / `app/rag/importer.py`; phase-127 pins green)
4. E2E stored + rendered without forbidden openers — ✅ (3/3, exact canned-string assertions + phase-127 regression guard)
5. Full suite green, E2E isolated green, ruff + pyright clean, coverage >90% — ✅
6. Atomic commit — deferred to the harness (working tree left intact per protocol)

**Documentation:** README.md and AGENTS.md reviewed in full — verified consistent, no changes needed (README already documents the new script in the update workflow; all `.env.example` vars documented; pages table matches routes; 17 colors = 9+8; no stale mock-voice references).

**Decisions:** No live-infrastructure changes (dev-DB read + a 0-row script smoke only). The `TODO.md` working-tree edit is the owner's own, left untouched.

**Next pending phase:** `156_table_ux_polish`.
2026-10-04 16:32:50 -04:00

296 lines
11 KiB
Python

"""Unit tests: ``scripts/regen_folder_summaries.py`` wiring (phase 155, task 02).
The one-shot full-regeneration entrypoint is pinned by wiring, not by a
live LLM or DB: a fake ``LLMClient``, a fake short-lived session, and a
stubbed ``generate_folder_summaries`` that records its call. The pins:
* the FULL-mode call contract — the stub is called with NO kwargs
(``changed_docs=None`` / ``only_missing=False`` / ``skip=False`` are
the generator's defaults; the phase-127 change-gated sync path is
untouched, the script is a consumer of the full mode);
* the stats dict surfaces on the single cron-safe summary line
(``regen_folder_summaries: folders=… failed=… pruned=…
kept_manual=… limit=…``) on the module logger;
* the ``--limit`` debug passthrough (parity with ``import_docs``'s
parser) is named on the summary line without disturbing the
generator's call contract;
* the settings/boot failure exits non-zero with the NAMED stderr line
— the app validator idiom (``ensure_admin_configured`` names the
missing ``BOR_`` variable; a settings validation error names the
field); the LLM/session/generator never run on a boot failure;
* a DB/commit outage exits non-zero with the named stderr line (never
a silent partial — the session still closes, no summary line);
* ``main()`` calls ``configure_debugging()`` (the phase-8 idiom — the
``DEBUGPY=1`` listener contract itself is the phase-8 precedent,
``tests/unit/test_debugging.py``).
"""
from __future__ import annotations
import logging
import pytest
from app.config import Settings
from scripts import regen_folder_summaries as regen
STATS = {"generated": 3, "failed": 1, "pruned": 2, "kept_manual": 1}
class _FakeLLM:
"""A duck-typed ``LLMClient`` stand-in (records construction)."""
class _FakeSession:
"""A short-lived session stub (phase-53 convention: the script's
own session commits; the generator only flushes)."""
def __init__(self, commit_error: Exception | None = None) -> None:
self.commit_error = commit_error
self.committed = False
self.closed = False
def commit(self) -> None:
if self.commit_error is not None:
raise self.commit_error
self.committed = True
def close(self) -> None:
self.closed = True
class _GenerateStub:
"""The stubbed ``generate_folder_summaries``: records the call's
positional args and kwargs and returns the fixed stats dict."""
def __init__(self) -> None:
self.calls: list[tuple[tuple[object, ...], dict[str, object]]] = []
self.raised: Exception | None = None
async def __call__(
self, session: object, llm: object, **kwargs: object
) -> dict[str, int]:
self.calls.append(((session, llm), dict(kwargs)))
if self.raised is not None:
raise self.raised
return dict(STATS)
def _wire(
monkeypatch: pytest.MonkeyPatch,
) -> tuple[_FakeSession, _GenerateStub, list[logging.LogRecord]]:
"""Boot the wiring with a fake LLM, fake session, and generator
stub; ``configure_logging``/``configure_debugging`` are spied (the
phase-8 idiom is pinned by the call, the listener contract by the
phase-8 precedent file)."""
session = _FakeSession()
stub = _GenerateStub()
llm = _FakeLLM()
monkeypatch.setattr(
regen,
"get_settings",
lambda: Settings(_env_file=None), # pyright: ignore[reportCallIssue]
)
monkeypatch.setattr(regen, "configure_logging", lambda level: None)
debugging_calls: list[int] = []
monkeypatch.setattr(
regen,
"configure_debugging",
lambda: debugging_calls.append(1) or False,
)
monkeypatch.setattr(regen, "LLMClient", lambda: llm)
monkeypatch.setattr(regen, "SessionLocal", lambda: session)
monkeypatch.setattr(regen, "generate_folder_summaries", stub)
records: list[logging.LogRecord] = []
class _Sink(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
records.append(record)
sink = _Sink()
regen_logger = logging.getLogger("scripts.regen_folder_summaries")
regen_logger.addHandler(sink)
regen_logger.setLevel(logging.INFO)
return session, stub, records
def _summary_lines(records: list[logging.LogRecord]) -> list[str]:
return [r.getMessage() for r in records if r.getMessage().startswith(
"regen_folder_summaries:"
)]
# ---------- the parser (the import_docs parity flag) ----------
def test_parser_defaults_to_no_limit() -> None:
args = regen.build_parser().parse_args([])
assert args.limit is None
def test_parser_limit_parity_flag() -> None:
args = regen.build_parser().parse_args(["--limit", "5"])
assert args.limit == 5
# ---------- the success path: the FULL-mode call contract ----------
def test_main_full_mode_defaults_and_stats_on_summary_line(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
session, stub, records = _wire(monkeypatch)
rc = regen.main([])
out = capsys.readouterr()
assert rc == 0
assert out.err == "" # success: no stderr noise
# The FULL-mode call contract: exactly one call, (session, llm)
# positional, NO kwargs — changed_docs=None / only_missing=False /
# skip=False are the generator's defaults (the phase-127 change-
# gated sync path is untouched — the script is a consumer of the
# default mode, not of changed_docs).
assert len(stub.calls) == 1
(pos, kwargs), = stub.calls
assert kwargs == {}
assert isinstance(pos[0], _FakeSession) # the short-lived session
assert isinstance(pos[1], _FakeLLM) # the LLMClient stand-in
# The phase-53 convention: the script's own session commits + closes.
assert session.committed is True
assert session.closed is True
# The stats dict surfaces on the single cron-safe summary line.
lines = _summary_lines(records)
assert len(lines) == 1
assert lines[0] == (
"regen_folder_summaries: folders=3 failed=1 pruned=2 "
"kept_manual=1 limit=none"
)
def test_main_debug_parsing_runs_full_mode_and_names_limit(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""The --limit debug passthrough (parity with import_docs): named
on the summary line WITHOUT disturbing the generator's call
contract — the full-mode defaults are still the no-kwargs call."""
_session, stub, records = _wire(monkeypatch)
rc = regen.main(["--limit", "7"])
assert rc == 0
assert len(stub.calls) == 1
assert stub.calls[0][1] == {} # the full-mode defaults, untouched
assert _summary_lines(records) == [
"regen_folder_summaries: folders=3 failed=1 pruned=2 "
"kept_manual=1 limit=7"
]
def test_main_calls_configure_debugging(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""The phase-8 wiring pin: main() calls configure_debugging()
(DEBUGPY=1 honored, default off — the listener contract itself is
the phase-8 precedent, tests/unit/test_debugging.py)."""
called: list[int] = []
session, _stub, _records = _wire(monkeypatch)
monkeypatch.setattr(
regen, "configure_debugging", lambda: called.append(1) or False
)
assert regen.main([]) == 0
assert called == [1]
assert session.committed is True
# ---------- the fail-loud boot (the app validator idiom) ----------
def test_main_boot_failure_missing_admin_secret_is_named(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""A missing BOR_ADMIN_PASSWORD: the app's fail-loud gate
(ensure_admin_configured) names the variable, main exits non-zero
with the named stderr line, and the LLM/session/generator NEVER
run (never a silent partial)."""
monkeypatch.setattr(
regen,
"get_settings",
lambda: Settings(_env_file=None, admin_password=""), # pyright: ignore[reportCallIssue]
)
monkeypatch.setattr(regen, "configure_logging", lambda level: None)
monkeypatch.setattr(regen, "configure_debugging", lambda: False)
llm_calls: list[int] = []
monkeypatch.setattr(
regen, "LLMClient", lambda: llm_calls.append(1) or _FakeLLM()
)
session = _FakeSession()
monkeypatch.setattr(regen, "SessionLocal", lambda: session)
stub = _GenerateStub()
monkeypatch.setattr(regen, "generate_folder_summaries", stub)
rc = regen.main([])
err = capsys.readouterr().err
assert rc == 1
assert "regen_folder_summaries: boot failed:" in err
assert "BOR_ADMIN_PASSWORD" in err # the gate names the variable
assert llm_calls == [] # the LLM never constructed
assert stub.calls == [] # the generator never ran
assert session.committed is False and session.closed is False
def test_main_settings_validation_error_is_named(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""A settings validation error (a bad env value — the settings
validator naming the offending field) names the failure the same
way: non-zero exit, the named line, nothing runs."""
def _bad_settings() -> Settings:
# The Settings validator rejects a negative recency_boost —
# the pydantic ValidationError names the field.
return Settings(_env_file=None, recency_boost=-1) # pyright: ignore[reportCallIssue]
monkeypatch.setattr(regen, "get_settings", _bad_settings)
monkeypatch.setattr(regen, "configure_logging", lambda level: None)
llm_calls: list[int] = []
monkeypatch.setattr(
regen, "LLMClient", lambda: llm_calls.append(1) or _FakeLLM()
)
stub = _GenerateStub()
monkeypatch.setattr(regen, "generate_folder_summaries", stub)
rc = regen.main([])
err = capsys.readouterr().err
assert rc == 1
assert "regen_folder_summaries: boot failed:" in err
assert "recency_boost" in err
assert llm_calls == []
assert stub.calls == []
# ---------- the fail-loud run (never a silent partial) ----------
def test_main_commit_outage_fails_loud(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""A DB/commit outage: the failure is NAMED on stderr, the exit
code is non-zero, the session still closes (the uncommitted
regeneration rolls back with it — never a silent partial), and NO
summary line is logged (a failed run reports nothing generated)."""
session, stub, records = _wire(monkeypatch)
session.commit_error = RuntimeError("db down (test sentinel)")
rc = regen.main([])
err = capsys.readouterr().err
assert rc == 1
assert "regen_folder_summaries: regeneration failed:" in err
assert "db down (test sentinel)" in err
assert len(stub.calls) == 1 # the generator ran…
assert session.committed is False # …but the commit failed
assert session.closed is True # …and the session closed (rollback)
assert _summary_lines(records) == [] # no success line
assert "regen_folder_summaries: folders=" not in err