Files
brain-of-reese/tests/integration/test_importer_include_hidden.py
T
ducoterra d731169b8b
Build and Push Containers / build-and-push-app (push) Successful in 1m44s
Build and Push Containers / build-and-push-db (push) Successful in 13s
phase: 105_hidden_folders_toggle
All completion criteria verified. Everything is green.

**Phase 105 final verification pass — all criteria verified**

- Verified the full implementation in the working tree: `git_sources.include_hidden` column + alembic `0019` (dev DB at head, column present), `iter_importable_files`/`import_sources` flag support with `str(root)`-keyed map used by both walk and progress pre-walk, `GitSourcePatchIn` rename with optional fields, sync/CLI pipeline wiring (OR-collision), and the per-row "Hidden" checkbox + tag + error line on the Sources page
- Unit + integration: `uv run pytest` → exit 0 (2148 tests collected, all pass; this sandbox occasionally swallows pytest's final status line — exit codes verified)
- Coverage: `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (3879 stmts, 15 miss) — >90% gate ✓
- Dedicated E2E: `uv run pytest tests/e2e/test_hidden_folders_toggle.py -v --no-cov` → **6 passed in 23.20s** (DB up, in isolation)
- Regression E2E in isolation: `test_source_ignore_paths` 6 passed, `test_git_sources_admin` 6 passed, `test_local_directory_sources` 3 passed, `test_sync_button` 3 passed, `test_smoke` 3 passed
- Lint/types: `uv run ruff check .` + `uv run pyright` → clean (0 errors/warnings)

**Completion criteria:** (1) checkbox persists via PATCH 200 → "hidden on" tag + GET round-trips `include_hidden: true`; failure path reverts box + `role="alert"` canned message ✓; (2) flag OFF byte-identical (only `visible.md` indexed), ON indexes `.hidden/note.md` into the KB catalog, `EXCLUDED_DIRS` excluded both states ✓; (3) A2: flag OFF → `detail.pruned==1`, doc gone from catalog ✓; (4) PATCH bool-only/list-only/both/neither no-op, phase-89 fixed 422s unchanged, 404, anonymous 403 (incl. bool-only body) ✓; (5) env-fallback rows render no checkbox, WCAG-clean (aria-label, keyboard focus, visible label, text tag) ✓; (6) full gate green ✓; (7) commit left to the harness per instructions (no `git add`/`commit` run; phase files untouched).

**Deviations:** none — no defects found; no code changes were needed on this pass.

**Next pending phase:** `.agents/phases/todo/98_sync_summary_visibility`.
2026-09-12 21:26:30 -04:00

242 lines
8.5 KiB
Python

"""Integration: the phase-105 hidden-folders flag through the real import
pipeline.
Phase 105 (TODO.md L3 — "…a toggle per input … to allow indexing hidden
.folders."): ``import_sources`` gains ``include_hidden_by_root`` (same
``str(root)`` keying as phase 89's ``ignore_by_root``). A1
(owner-confirmed 2026-09-14): flag ON admits dot-prefixed components —
hidden files are indexed, embedded, and summarized exactly like visible
files — while ``EXCLUDED_DIRS`` stay excluded in both states. A2: a file
indexed with the flag ON leaves the KB on the next ``prune=True`` run
with the flag OFF (the untouched ``seen`` set does the work). A4: no map
→ byte-identical to pre-phase-105. Mirrors the fixture-tree + mock-LLM
pattern of ``test_importer_ignore.py``: a ``tmp_path`` source dir run
through the real ``import_sources`` into the compose Postgres, with
:class:`tests.fakes.FakeEmbedder` as the deterministic LLM stand-in.
"""
from __future__ import annotations
import asyncio
from pathlib import Path
from sqlalchemy import select, text
from sqlalchemy.orm import Session
from app.models import Chunk, Document
from app.rag.importer import import_sources
from tests.fakes import FakeEmbedder
NAME = "HiddenFix"
def _write(root: Path, rel: str, content: str) -> None:
path = root / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
def _tree(tmp_path: Path, name: str = NAME) -> Path:
"""One visible md, a hidden dir (md + non-markdown yaml), an excluded dir."""
root = tmp_path / name
_write(root, "visible.md", "# Visible\n\nvisible body\n")
_write(root, ".hidden/note.md", "# Note\n\nHIDDEN-MD-CONTENT\n")
_write(root, ".hidden/data.yaml", "key: HIDDEN-YAML-VALUE\n")
_write(root, ".venv/junk.md", "# Junk\n\nEXCLUDED-CONTENT\n")
return root
def _reset(db: Session) -> None:
# House cleanup pattern (tests/integration/test_importer_ignore.py).
db.execute(text("TRUNCATE chunks, documents, query_log"))
db.commit()
def test_hidden_paths_not_indexed_by_default(db: Session, tmp_path: Path) -> None:
# A4: no map → today's behavior, byte-identical — the hidden files
# never produce a Document/Chunk row and are never embedded.
_reset(db)
root = _tree(tmp_path)
llm = FakeEmbedder()
summary = asyncio.run(import_sources([root], llm, session=db))
assert summary.files == 1
assert summary.added == 1
assert summary.errors == 0
assert summary.summaries == 0
assert summary.summary_errors == 0
docs = db.scalars(select(Document)).all()
assert {(d.source, d.path) for d in docs} == {(NAME, "visible.md")}
for rel in (".hidden/note.md", ".hidden/data.yaml", ".venv/junk.md"):
assert not any(d.path == rel for d in docs)
for texts in llm.calls: # every embed batch
for t in texts:
assert "HIDDEN-MD-CONTENT" not in t and "HIDDEN-YAML-VALUE" not in t
assert "EXCLUDED-CONTENT" not in t
# The hidden yaml never reached the lite model.
assert not llm.chat_calls
_reset(db)
def test_hidden_paths_indexed_when_flag_on(db: Session, tmp_path: Path) -> None:
# A1: with the map, hidden files are indexed, embedded, and
# summarized exactly like visible files — EXCLUDED_DIRS stay out.
_reset(db)
root = _tree(tmp_path)
llm = FakeEmbedder()
summary = asyncio.run(
import_sources(
[root], llm, session=db, include_hidden_by_root={str(root): True}
)
)
assert summary.files == 3
assert summary.added == 3
assert summary.errors == 0
assert summary.summary_errors == 0
docs = db.scalars(select(Document)).all()
assert {(d.source, d.path) for d in docs} == {
(NAME, "visible.md"),
(NAME, ".hidden/note.md"),
(NAME, ".hidden/data.yaml"),
}
# EXCLUDED_DIRS is excluded in BOTH states (A1).
assert not any(d.path == ".venv/junk.md" for d in docs)
chunks = db.scalars(select(Chunk)).all()
assert not any("EXCLUDED-CONTENT" in c.content for c in chunks)
# The hidden md was embedded like any visible md (no summary — the
# markdown path skips the lite model).
note = db.scalar(select(Document).where(Document.path == ".hidden/note.md"))
assert note is not None and note.summary is None
assert any("HIDDEN-MD-CONTENT" in c.content for c in chunks)
# The hidden yaml went through the FULL non-markdown path (phase 30):
# a stored summary plus one embedded is_summary chunk on top of the
# content chunks.
yaml_doc = db.scalar(select(Document).where(Document.path == ".hidden/data.yaml"))
assert yaml_doc is not None and yaml_doc.summary is not None
yaml_chunks = [
c for c in chunks if c.document_id == yaml_doc.id
]
assert any(c.is_summary for c in yaml_chunks)
assert any(not c.is_summary for c in yaml_chunks)
assert any("HIDDEN-YAML-VALUE" in c.content for c in yaml_chunks)
# Only the yaml reached the lite model.
assert summary.summaries == 1
assert len(llm.chat_calls) == 1
user = next(m["content"] for m in llm.chat_calls[0] if m["role"] == "user")
assert "HIDDEN-YAML-VALUE" in user
_reset(db)
def test_flag_off_re_run_prunes_hidden_docs(db: Session, tmp_path: Path) -> None:
# A2: the owner flips the flag off — the next prune run walks with
# the default rules, the hidden files never enter ``seen``, and their
# rows (summary chunk included) leave the index.
_reset(db)
root = _tree(tmp_path)
llm = FakeEmbedder()
s1 = asyncio.run(
import_sources(
[root], llm, session=db, include_hidden_by_root={str(root): True}
)
)
assert (s1.files, s1.added) == (3, 3)
assert (
db.scalar(select(Document).where(Document.path == ".hidden/note.md"))
is not None
)
s2 = asyncio.run(import_sources([root], llm, session=db, prune=True))
assert s2.files == 1
assert s2.unchanged == 1 # visible.md untouched
assert s2.pruned == 2 # both hidden documents
assert (
db.scalar(select(Document).where(Document.path == ".hidden/note.md"))
is None
)
assert (
db.scalar(select(Document).where(Document.path == ".hidden/data.yaml"))
is None
)
assert (
db.scalar(select(Document).where(Document.path == "visible.md")) is not None
)
# The summary chunk rows went with their documents (cascade).
assert not any(
"HIDDEN-YAML-VALUE" in c.content for c in db.scalars(select(Chunk)).all()
)
_reset(db)
def test_progress_total_agrees_with_walk_in_both_states(
db: Session, tmp_path: Path
) -> None:
# The phase-64 pre-walk uses the same per-root flag as the loop, so
# ``total`` agrees with the walk in both states.
_reset(db)
root = _tree(tmp_path)
llm = FakeEmbedder()
on_totals: set[int] = set()
def on_progress(source: str, rel: str, done: int, total: int) -> None:
on_totals.add(total)
s_on = asyncio.run(
import_sources(
[root],
llm,
session=db,
progress=on_progress,
include_hidden_by_root={str(root): True},
)
)
assert s_on.files == 3
assert on_totals == {3} # hidden files counted in the denominator
_reset(db)
llm2 = FakeEmbedder()
off_totals: set[int] = set()
def off_progress(source: str, rel: str, done: int, total: int) -> None:
off_totals.add(total)
s_off = asyncio.run(
import_sources([root], llm2, session=db, progress=off_progress)
)
assert s_off.files == 1
assert off_totals == {1} # visible only, the pre-phase-105 count
_reset(db)
def test_unlisted_root_stays_hidden(db: Session, tmp_path: Path) -> None:
# The map is per-root, not global: listing one root as True leaves
# the other root exactly as pre-phase-105.
_reset(db)
root_a = _tree(tmp_path, name="HiddenFixA")
root_b = _tree(tmp_path, name="HiddenFixB")
llm = FakeEmbedder()
summary = asyncio.run(
import_sources(
[root_a, root_b],
llm,
session=db,
include_hidden_by_root={str(root_a): True},
)
)
# A: 3 (flag on) · B: 1 (unlisted → False)
assert summary.files == 4
docs = db.scalars(select(Document)).all()
assert {(d.source, d.path) for d in docs} == {
("HiddenFixA", "visible.md"),
("HiddenFixA", ".hidden/note.md"),
("HiddenFixA", ".hidden/data.yaml"),
("HiddenFixB", "visible.md"),
}
assert not any(
d.source == "HiddenFixB" and d.path == ".hidden/note.md" for d in docs
)
_reset(db)