phase: 97_kb_tree_catalog
Build and Push Containers / build-and-push-app (push) Successful in 2m11s
Build and Push Containers / build-and-push-db (push) Successful in 11s

All completion criteria verified — everything is green, no defects found. Final report:

## Phase 97 final verification pass — ALL GREEN

**Verified (no code changes needed):**
- `GET /api/docs/tree` (admin), `build_kb_tree` pure builder, `PATCH /api/folders/summary`, migration 0018 (`manually_edited`, head confirmed), generator skip/keep + `kept_manual` stat, RAG tree UI + edit affordance in `sources.js`/`index.html`/`styles.css`
- `tests/e2e/test_kb_tree.py`: 8 passed — top level, drill source/folder, edit round-trip, clear, manual-desc-survives-sync, reload fallback, anonymous gate
- Integration: tree shape/order/403/empty/indexed-only + PATCH update/create/root/clear/404/403/no-LLM + stat-walk equivalence (in `test_docs_api.py`); 3-field `folder_summaries=` import token preserved

**Gates (exact commands):**
- `uv run pytest --cov=app --cov-report=term-missing` → **2053 passed**, TOTAL coverage **99%** (>90% ✓)
- `uv run ruff check . && uv run pyright` → **All checks passed / 0 errors**
- `uv run pytest tests/e2e/test_kb_tree.py -v --no-cov` → **8 passed** in isolation
- 30 story/RAG-view E2E suites run **one per process**: all passed, incl. `test_ls_tree_drilldown` (agent `ls` byte-identical ✓), `test_import_documents`, `test_edit_summaries`, `test_admin_auth`, `test_kb_overview`

**Completion criteria:** tree view ✓ · edit round-trip + clear ✓ · manual persists/clear resets ✓ · `ls` unchanged ✓ · pytest/coverage/lint ✓ · E2E isolation ✓ · commit — left to harness per protocol (working tree untouched, `git add/commit` not run)

**Deviations:** none. **Next pending phase:** none — `todo/` contains only 97 (96 already committed).
This commit is contained in:
2026-09-11 22:48:02 -04:00
parent a49be80b8e
commit ad7585d474
81 changed files with 6299 additions and 211 deletions
+41 -8
View File
@@ -177,13 +177,25 @@ def _open_menu(page: Page) -> None:
expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true")
def _drill_to_files(page: Page) -> None:
"""Phase 97: the catalog is the drill-down tree — the top level
lists the sources (the file table is per-level, hidden at the
top). The 640px ``min-width`` pin targets the FILE table, so both
RAG-view tests drill to a level that has files (the fixture
source + its ``homelab`` folder) before pinning."""
for name in ("docs", "homelab"):
page.click(f'#folders-tbody a.folder-link:text-is("{name}")')
def _wrap_handle(page: Page, view: str) -> JSHandle:
"""The table card (``.table-wrap``) of ``view``: the id'd cards for
history/tokens, the RAG card found through its ``.docs-table``
(that card is the only one without an id)."""
history/tokens, the RAG card found through its FILE table (phase 97:
the RAG view carries TWO cards — ``#folders-wrap`` at the top level
and the per-level file table's card; the 640px pin is the file
table's, and the tests drill there first)."""
if view == "sources":
return page.evaluate_handle(
"() => document.querySelector('.docs-table').closest('.table-wrap')"
"() => document.querySelector('#docs-table').closest('.table-wrap')"
)
return page.evaluate_handle(f"() => document.querySelector('#{view}-table-wrap')")
@@ -381,14 +393,25 @@ def test_rag_view_regression(
regress it — AND its table is still full-width inside the card:
the 640px ``min-width`` still engages, so the card keeps its
in-card scroll (the shared rule added a containing block, not a
width)."""
width).
Phase 97: the catalog is the drill-down tree — the top level lists
the sources, so the FILE table (the 640px pin's subject) is reached
by drilling ``docs`` → ``homelab`` first; the drill is the only
change to this pin."""
summary = _seed_kb(mock_llm)
assert summary is not None and summary.added == 13 # A9 formats (phase 47 added quadlet+j2)
page = _mobile_page(browser)
try:
login(page, app_url, next="/sources.html")
_wait_settled_admin(page)
expect(page.locator(".docs-table")).to_be_visible(timeout=15_000)
# Phase 97: the top level lists the sources (the source row is
# the catalog-rendered signal); the 640px in-card-scroll pin
# targets the FILE table, so drill to a level that has files
# first (the drill is the only change).
page.locator("#folders-tbody tr").first.wait_for(state="visible", timeout=15_000)
_drill_to_files(page)
expect(page.locator("#docs-tbody tr").first).to_be_visible(timeout=15_000)
_assert_viewport_width(page, "/sources.html (direct)")
_assert_in_card_scroll(page, _wrap_handle(page, "sources"), "/sources.html")
@@ -411,7 +434,10 @@ def test_desktop_unchanged(
than the mobile 346px card. The zero-offset
``position: relative`` changed no layout, so desktop is
byte-identical in behavior. The KB is seeded the house way for
the RAG view's table (it hides itself on an empty KB).
the RAG view's table (it hides itself on an empty KB). Phase 97:
the RAG view's table is the FILE table — the top level lists the
sources, so the sources pass drills ``docs`` → ``homelab`` first
(the drill is the only change).
Data state: the desktop pin is enforced on the EMPTY-table state
the module docstring declares ("the History/Tokens contract holds
@@ -443,8 +469,15 @@ def test_desktop_unchanged(
if i: # the first view is the login landing
page.goto(app_url + path)
_wait_settled_admin(page)
marker = ".docs-table" if view == "sources" else f"#{view}-table-wrap"
expect(page.locator(marker)).to_be_visible(timeout=15_000)
if view == "sources":
# Phase 97: the top level lists the sources — the
# container-width pin is the FILE table's (the 640px
# min-width's desktop counterpart), so drill to a level
# that has files first.
page.locator("#folders-tbody tr").first.wait_for(state="visible", timeout=15_000)
_drill_to_files(page)
marker = ("#docs-tbody tr" if view == "sources" else f"#{view}-table-wrap")
expect(page.locator(marker).first).to_be_visible(timeout=15_000)
_assert_viewport_width(page, f"{path} (desktop)")
report = _wrap_handle(page, view).evaluate(