phase: 98_sync_summary_visibility
All verification complete. Final report: **Phase 98 — Sync summary visibility: final verification pass** (all 5 tasks already complete; implementation verified against the design, no defects found, no code changes needed) - **Implementation checked:** `SyncStatus` phase machine (4 new keys, terminal-keep counts), `on_progress` hook in `generate_folder_summaries`, `summary_pending` on `KbTreeSource`/`KbTreeFolder` + D3 rule in `build_kb_tree`, phase-aware sync labels + pending UI in `sources.js`, `.kb-summary-pending` CSS — all match decisions D1–D5. - **Unit + integration:** `uv run pytest` → 2184 tests, 0 failed/errors (exit 0) - **Coverage:** `uv run pytest --cov=app --cov-report=term-missing` → **99%** on `app/` (criterion >90% ✓; `app/api/sync.py` and `app/rag/folder_summaries.py` at 100%) - **Lint/types:** `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings - **Phase E2E (isolation):** `uv run pytest tests/e2e/test_sync_summary_visibility.py -v --no-cov` → **3 passed** (phase machine, live label, pending markers + gap-fill self-heal) - **Regression suites (each isolated, `--no-cov`):** test_kb_tree ✓, test_ls_tree_drilldown 3 ✓, test_sync_button 3 ✓, test_sync_upload_progress 4 ✓, test_oneshot_llm_retry 2 ✓, test_local_directory_sources 3 ✓ - **Completion criteria:** all 7 verified green — status phase fields + terminal semantics; `Writing KB overview…`/`Summarizing folders… (n/m)` labels (title + aria-live); pending set == `missing_folder_summaries` (integration cross-check pinned at `test_docs_api.py:428`); CLI/`ls` byte-identity (no changes to those paths, pins green); suite/coverage/lint gates; dedicated + regression E2E. Commit left to the harness per protocol (no `git add`/`commit` run). - **Decisions/deviations:** none — no fixes were required this pass. - **Next pending phase:** `99_kb_tree_table_and_back_nav`.
This commit is contained in:
@@ -283,3 +283,135 @@ def test_cross_check_nested_level_matches_group_folder_listing() -> None:
|
||||
("one/a.md", "A"),
|
||||
("one/b.md", "B"),
|
||||
]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# summary_pending (phase 98, task 03) — the D3 rule (ONE concept):
|
||||
# a SOURCE or FOLDER node is pending iff its recursive document count
|
||||
# ≥ MIN_DOCS_PER_FOLDER (2) AND it has NO stored folder_summaries row
|
||||
# (AI or manual — any row). That is exactly
|
||||
# ``app.rag.folder_summaries.missing_folder_summaries``'s candidate
|
||||
# set — the marker never drifts from the gap-fill (the integration
|
||||
# cross-check in ``tests/integration/test_docs_api.py`` pins it
|
||||
# end to end). FILE nodes carry no flag.
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_folder_two_docs_no_stored_row_is_pending() -> None:
|
||||
"""A ≥ 2-doc folder with NO stored row → ``summary_pending`` true
|
||||
(the marker's "waiting to generate" semantics); file nodes carry
|
||||
no flag at all (the file table has no description column)."""
|
||||
rows = [
|
||||
("S", "one/a.md", "A", 1, T0),
|
||||
("S", "one/b.md", "B", 1, T0),
|
||||
]
|
||||
(source,) = build_kb_tree(["S"], rows, {})
|
||||
(one,) = _folder_nodes(source)
|
||||
assert one.documents == 2
|
||||
assert one.summary is None
|
||||
assert one.summary_pending is True
|
||||
file = _file_nodes(one)[0]
|
||||
assert "summary_pending" not in file.__class__.model_fields
|
||||
assert "summary_pending" not in file.model_dump()
|
||||
|
||||
|
||||
def test_folder_with_stored_row_is_not_pending() -> None:
|
||||
"""The same ≥ 2-doc folder WITH a stored row — ANY row, the builder
|
||||
cannot tell AI from manual — is NOT pending (a description exists).
|
||||
A row on the folder does not cover the source root: with no
|
||||
``(source, "")`` row the SOURCE node stays pending."""
|
||||
rows = [
|
||||
("S", "one/a.md", "A", 1, T0),
|
||||
("S", "one/b.md", "B", 1, T0),
|
||||
]
|
||||
(source,) = build_kb_tree(["S"], rows, {("S", "one"): "Manual."})
|
||||
(one,) = _folder_nodes(source)
|
||||
assert one.summary == "Manual."
|
||||
assert one.summary_pending is False
|
||||
# The source root (2 docs) has no (source, "") row of its own.
|
||||
assert source.summary is None
|
||||
assert source.summary_pending is True
|
||||
|
||||
|
||||
def test_single_document_folder_never_pending() -> None:
|
||||
"""A < 2-document folder is NEVER pending (it never gets a summary
|
||||
— its one file line IS its description), even with no stored row
|
||||
— while its ≥ 2-doc source root (no root row) still is."""
|
||||
rows = [
|
||||
("S", "solo/only.md", "Only", 1, T0), # 1-doc folder
|
||||
("S", "top.md", "Top", 1, T0), # source total = 2
|
||||
]
|
||||
(source,) = build_kb_tree(["S"], rows, {})
|
||||
(solo,) = _folder_nodes(source)
|
||||
assert solo.documents == 1
|
||||
assert solo.summary_pending is False
|
||||
assert source.summary_pending is True
|
||||
|
||||
|
||||
def test_name_collision_pending_follows_recursive_count() -> None:
|
||||
"""The phase-94 count-rule edge: documents ``one/a`` AND ``one/a/b``
|
||||
→ folder ``one/a`` exists and its recursive count is 2 (the document
|
||||
whose path EQUALS the folder name counts — the ``path == folder``
|
||||
arm) → pending true with no stored row, even though the folder has
|
||||
only ONE direct file — pending follows the RECURSIVE count, not
|
||||
the number of direct children. A stored row on the NESTED folder
|
||||
alone clears only that marker (the rule is per node)."""
|
||||
rows = [
|
||||
("S", "one/a", "File A", 1, T0), # a file wearing the folder's name
|
||||
("S", "one/a/b.md", "B", 1, T0), # makes ``one/a`` a folder
|
||||
]
|
||||
(source,) = build_kb_tree(["S"], rows, {})
|
||||
one = _folder_nodes(source)[0]
|
||||
assert one.path == "one"
|
||||
assert one.documents == 2
|
||||
assert one.summary_pending is True
|
||||
a = _folder_nodes(one)[0]
|
||||
assert a.path == "one/a"
|
||||
assert a.documents == 2 # "one/a" itself + "one/a/b.md"
|
||||
assert len(_file_nodes(a)) == 1 # ONE direct file — the count is not that
|
||||
assert a.summary_pending is True
|
||||
# Stored row on the nested folder only: it clears that node, and
|
||||
# only that node.
|
||||
(nested,) = build_kb_tree(["S"], rows, {("S", "one/a"): "Nested."})
|
||||
one2 = _folder_nodes(nested)[0]
|
||||
a2 = _folder_nodes(one2)[0]
|
||||
assert a2.summary == "Nested."
|
||||
assert a2.summary_pending is False
|
||||
assert one2.summary_pending is True
|
||||
|
||||
|
||||
def test_source_root_pending_and_zero_document_source_never() -> None:
|
||||
"""The source root: a source with ≥ 2 docs and NO ``(source, "")``
|
||||
row → the SOURCE node is pending; the stored root row clears it.
|
||||
A registered 0-document source is NEVER pending (0 < the minimum —
|
||||
there is nothing to summarize), with or without a manual row."""
|
||||
rows = [
|
||||
("Full", "x/1.md", "1", 1, T0),
|
||||
("Full", "y.md", "Y", 1, T0),
|
||||
]
|
||||
full, empty = build_kb_tree(["Full", "Empty"], rows, {})
|
||||
assert full.documents == 2
|
||||
assert full.summary_pending is True
|
||||
assert empty.documents == 0
|
||||
assert empty.summary_pending is False
|
||||
full2, empty2 = build_kb_tree(
|
||||
["Full", "Empty"], rows, {("Full", ""): "Root desc."}
|
||||
)
|
||||
assert full2.summary == "Root desc."
|
||||
assert full2.summary_pending is False
|
||||
assert empty2.summary_pending is False
|
||||
|
||||
|
||||
def test_two_sources_pending_independently() -> None:
|
||||
"""Pending is computed per source: with one source holding a root
|
||||
row and the other not, only the rowless source's node is pending —
|
||||
the markers never leak across sources."""
|
||||
rows = [
|
||||
("A", "a1.md", "A1", 1, T0),
|
||||
("A", "a2.md", "A2", 1, T0),
|
||||
("B", "b1.md", "B1", 1, T0),
|
||||
("B", "b2.md", "B2", 1, T0),
|
||||
]
|
||||
a, b = build_kb_tree(["A", "B"], rows, {("A", ""): "A root."})
|
||||
assert (a.summary, a.summary_pending) == ("A root.", False)
|
||||
assert (b.summary, b.summary_pending) == (None, True)
|
||||
|
||||
Reference in New Issue
Block a user