phase: 96_oneshot_resilience
Build and Push Containers / build-and-push-app (push) Successful in 1m34s
Build and Push Containers / build-and-push-db (push) Successful in 10s

All checks complete. Final report:

**Phase 96 (one-shot resilience) — final verification pass, all green** (all 4 task files already in `complete/`; verified the working-tree implementation against the design)

- `LLMClient.chat()` empty-content retry (D1–D3) via `_chat_once` + `_EmptyContentError` (carries `finish_reason`), under `BOR_LLM_RETRIES`/`BOR_LLM_RETRY_DELAY` — verified in diff
- `missing_folder_summaries()` + `generate_folder_summaries(only_missing=…)` — verified; `folder_summary_table_empty` deleted, both sync gates switched to the gap probe
- `.env.example` comments updated (chat-turn stream + one-shot summary calls)

**Test / lint / coverage results**
- `uv run pytest --cov=app --cov-report=term-missing` → **1988 passed**, coverage **99%** (gate >90%)
- `uv run pytest tests/e2e/test_oneshot_llm_retry.py -v --no-cov` → **2 passed** (isolated)
- Regressions, each isolated: `test_ls_tree_drilldown` 3 passed, `test_sync_button` 3 passed, `test_local_directory_sources` 3 passed, `test_llm_retry` 4 passed
- `uv run ruff check . && uv run pyright` → clean (0 errors)

**Completion criteria:** retry-then-recover unit-pinned ✓ · exhaustion + `BOR_LLM_RETRIES=0` byte-identical ✓ · streaming path untouched ✓ · gap-fill both sync paths, other rows byte-identical incl. `updated_at` ✓ · no-gap zero-burn ✓ · phase E2E green ✓ · regression E2Es green ✓ · full suite + >90% + lint/types ✓ · no completed-phase behavior change (full suite green) ✓. Commit left to the harness per executor rules (working tree, 16 files).

**Deviations:** none. **Next pending phase:** `97_kb_tree_catalog`.
This commit is contained in:
2026-09-11 13:16:20 -04:00
parent bcaef800c5
commit a49be80b8e
42 changed files with 2893 additions and 143 deletions
+192 -10
View File
@@ -31,9 +31,9 @@ from app.rag.folder_summaries import (
SYSTEM_PROMPT,
build_folder_summary_prompt,
folder_of,
folder_summary_table_empty,
generate_folder_summaries,
group_by_folder,
missing_folder_summaries,
summarize_folder,
)
from app.rag.llm import LLMError
@@ -613,16 +613,198 @@ def test_generate_only_flushes_caller_commits(db: Session, clean_tables) -> None
assert MIN_DOCS_PER_FOLDER == 2 # the ≥ 2 scope rule, pinned by name
def test_folder_summary_table_empty_gate(db: Session, clean_tables) -> None:
"""The sync-path gate probe (phase 94, task 02): empty → True
(the first full sync after migration 0017 must still generate),
one row → False (a populated table waits for a KB change)."""
assert folder_summary_table_empty(db) is True # the truncated table
_add_doc(db, "FSU", "a/one.md", "One")
_add_doc(db, "FSU", "a/two.md", "Two")
# ---------- missing_folder_summaries (phase 96, task 02) ----------
def test_missing_fresh_table_is_exactly_the_candidate_set(
db: Session, clean_tables
) -> None:
"""No stored rows → every candidate folder is a gap, sorted by
``(source, folder_path)``; the single-doc FSU-solo root is not a
candidate and can never be a gap."""
_seed_catalogue(db)
assert missing_folder_summaries(db) == [
("FSU", ""),
("FSU", "a"),
("FSU", "a/b"),
]
assert ("FSU-solo", "") not in missing_folder_summaries(db)
def test_missing_fully_populated_table_is_empty(db: Session, clean_tables) -> None:
"""Every candidate row present → no gap (the zero-burn gate case)."""
_seed_catalogue(db)
asyncio.run(generate_folder_summaries(db, _FakeLLM()))
db.commit()
assert folder_summary_table_empty(db) is False # rows landed
assert missing_folder_summaries(db) == []
def test_missing_one_deleted_row_is_that_folder(db: Session, clean_tables) -> None:
_seed_catalogue(db)
asyncio.run(generate_folder_summaries(db, _FakeLLM()))
db.commit()
db.execute(
text(
"DELETE FROM folder_summaries "
"WHERE source = 'FSU' AND folder_path = 'a'"
)
)
db.commit()
assert missing_folder_summaries(db) == [("FSU", "a")]
def test_missing_empty_kb_empty_table_is_no_gap(db: Session, clean_tables) -> None:
"""No catalogue → no candidates → ``[]`` — an empty table over an
empty KB is not a gap (there is nothing to fill)."""
assert missing_folder_summaries(db) == []
def test_missing_single_doc_folder_is_never_listed(db: Session, clean_tables) -> None:
"""A below-minimum folder without a row is NOT a gap — it is not a
candidate (its one file line IS its summary)."""
_add_doc(db, "FSU", "solo/one.md", "One")
assert missing_folder_summaries(db) == []
def test_missing_stale_row_is_not_a_gap(db: Session, clean_tables) -> None:
"""A stored row for a folder that dropped below 2 docs is stale,
not missing — the prune pass owns it, the gap detector ignores it."""
_seed_catalogue(db)
asyncio.run(generate_folder_summaries(db, _FakeLLM()))
db.commit()
db.add(FolderSummary(source="FSU", folder_path="gone/old", summary="stale"))
db.commit()
assert missing_folder_summaries(db) == []
# ---------- generate_folder_summaries(only_missing=…) (phase 96, 02) ----------
def _updated_at(db: Session, source: str, folder_path: str) -> object:
"""The stored row's ``updated_at`` (raw SQL — bypasses the ORM
identity map, so the before/after byte-identity comparison is
honest)."""
return db.execute(
text(
"SELECT updated_at FROM folder_summaries "
"WHERE source = :s AND folder_path = :f"
),
{"s": source, "f": folder_path},
).scalar_one()
def test_only_missing_fills_exactly_the_missing_keys(
db: Session, clean_tables
) -> None:
"""Two missing + two present → exactly the missing keys are
generated (sorted order, one lite call each); the present rows are
byte-identical after (text AND ``updated_at``); stats right."""
_seed_catalogue(db)
asyncio.run(generate_folder_summaries(db, _FakeLLM()))
db.commit()
full = _rows(db)
a_stamp = _updated_at(db, "FSU", "a")
db.execute(
text(
"DELETE FROM folder_summaries "
"WHERE (source, folder_path) IN (('FSU', ''), ('FSU', 'a/b'))"
)
)
db.commit()
assert missing_folder_summaries(db) == [("FSU", ""), ("FSU", "a/b")]
llm = _FakeLLM()
stats = asyncio.run(generate_folder_summaries(db, llm, only_missing=True))
assert stats == {"generated": 2, "failed": 0, "pruned": 0}
assert llm.calls == 2, "one call per MISSING key — zero for present rows"
assert [user.splitlines()[0] for _s, user in llm.requests] == [
"Folder: FSU",
"Folder: FSU/a/b",
], "the missing keys in sorted (source, folder_path) order"
assert _rows(db) == full, "the fill restores exactly the full candidate set"
assert _updated_at(db, "FSU", "a") == a_stamp, (
"the present row is byte-identical — never re-stamped by the fill"
)
def test_only_missing_no_gap_burns_zero_calls(db: Session, clean_tables) -> None:
"""No gap → zero lite calls, zero rows touched, zero stats (the
zero-burn invariant the unchanged-sync gate relies on)."""
_seed_catalogue(db)
asyncio.run(generate_folder_summaries(db, _FakeLLM()))
db.commit()
before = _rows(db)
stamps = {f: _updated_at(db, "FSU", f) for f in ("", "a", "a/b")}
llm = _FakeLLM()
stats = asyncio.run(generate_folder_summaries(db, llm, only_missing=True))
assert stats == {"generated": 0, "failed": 0, "pruned": 0}
assert llm.calls == 0, "zero-burn: no gap, no lite call"
assert _rows(db) == before
for folder, stamp in stamps.items():
assert _updated_at(db, "FSU", folder) == stamp, "no row re-stamped"
def test_only_missing_still_prunes_stale_rows(db: Session, clean_tables) -> None:
"""The prune pass runs in BOTH modes: the manually seeded stale row
(folder gone from the catalogue) is pruned while the genuine
missing folders are filled, and the present row stays untouched."""
_seed_catalogue(db)
db.add(FolderSummary(source="FSU", folder_path="a", summary="keep me"))
db.add(FolderSummary(source="FSU", folder_path="gone/old", summary="stale"))
db.commit()
a_stamp = _updated_at(db, "FSU", "a")
assert missing_folder_summaries(db) == [("FSU", ""), ("FSU", "a/b")]
llm = _FakeLLM()
stats = asyncio.run(generate_folder_summaries(db, llm, only_missing=True))
assert stats == {"generated": 2, "failed": 0, "pruned": 1}
assert llm.calls == 2
stored = _rows(db)
assert ("FSU", "gone/old") not in stored, (
"the stale row is pruned even under only_missing"
)
assert stored[("FSU", "a")] == "keep me"
assert _updated_at(db, "FSU", "a") == a_stamp
assert stored[("FSU", "")] == REPLY and stored[("FSU", "a/b")] == REPLY
def test_only_missing_fail_soft_keeps_prior_and_lands_others(
db: Session, clean_tables
) -> None:
"""Per-folder fail-soft applies under ``only_missing`` too: the
failing missing folder is counted and stays absent; the other
missing folders still land; the present row is untouched."""
_seed_catalogue(db)
db.add(FolderSummary(source="FSU", folder_path="a", summary="keep me"))
db.commit()
llm = _FakeLLM(fail_folders=("FSU/a/b",))
stats = asyncio.run(generate_folder_summaries(db, llm, only_missing=True))
assert stats == {"generated": 1, "failed": 1, "pruned": 0}
assert llm.calls == 2 # both missing folders were attempted
stored = _rows(db)
assert stored[("FSU", "")] == REPLY, "the other missing folder still lands"
assert ("FSU", "a/b") not in stored, "the failed folder stays absent"
assert stored[("FSU", "a")] == "keep me", "the present row is untouched"
def test_gap_probe_subsumes_the_table_empty_gate(db: Session, clean_tables) -> None:
"""The deleted phase-94 table-empty gate probe, re-expressed through
``missing_folder_summaries`` (phase 96, task 03 — the probe's unit
coverage moved here): an empty table over a populated catalogue
means EVERY candidate is missing (the targeted fill over all
candidates IS a full generation — the first full sync after
migration 0017 must still generate), a populated table means no
gap (a populated table waits for a KB change or a gap)."""
assert missing_folder_summaries(db) == [] # the truncated table, empty KB
_add_doc(db, "FSU", "a/one.md", "One")
_add_doc(db, "FSU", "a/two.md", "Two")
assert missing_folder_summaries(db) == [("FSU", ""), ("FSU", "a")]
asyncio.run(generate_folder_summaries(db, _FakeLLM()))
db.commit()
assert missing_folder_summaries(db) == [] # rows landed → no gap
db.execute(text("DELETE FROM folder_summaries"))
db.commit()
assert folder_summary_table_empty(db) is True # emptied again
assert missing_folder_summaries(db) == [("FSU", ""), ("FSU", "a")] # emptied again