phase: 118_summary_seed_context
**Phase 118 final verification pass — complete.** All criteria verified; 4 pre-existing defects found and fixed.
- **Verified:** summary-seed wiring (`select_suggested` top-5 no-floor → summary blocks, no full text in HIGH prompt), all-doc markdown summaries + NULL backfill (`summary_backfilled`, no `sources_meta` bump), `read` adds full text with `read_docs`-only dedupe, `done.sources` = suggested+read / durable record = suggested+related+read + `suggested=N` log line (seen live in E2E), byte-locked PERSONA/LOW/TOOLS_SECTION, battery gate PASS recorded in `TOOL_CALLING_TESTING.md` §10 (turbo 2026-09-16: 1/2/4 GREEN, cond-3 reported 9/10 per A7, contract 21/21, caps 0).
- **Defects fixed (all pre-existing, none phase-118):** ① `ChatMessage` schema missing the phase-113 `related` key → `extra="forbid"` 422'd every done-time auto-save of grounded turns with a related tier, leaving `message_count=1` (root cause of `test_share_chat` 3F; browser-level instrumentation proved the PUT 422) — added the field + unit/integration pins; ② `test_theme_semantic_completion` pins stale vs phase-117 debox (border/chip removed) — re-targeted to assert border/chip *absence*; ③ `test_header_consistency` `<26`px pin red on 26.125px native date-input line — bound relaxed to `<34` (wrap-detection intent kept); ④ `test_navbar_refresh` bor.chat.v1 key set updated for `related`.
- **Test/lint/coverage:** `uv run pytest --cov=app --cov-report=term-missing` → **2506 passed, app/ 99%** (>90%); `uv run ruff check . && uv run pyright` → clean, 0 errors.
- **E2E:** new story suite in isolation → **2 passed**; full 103-suite matrix sweep (each isolated) → **all 103 green** after the fixes; `test_share_chat` 4 passed, `test_theme_semantic_completion` 8 passed, `test_header_consistency` 3 passed, `test_navbar_refresh` 7 passed.
- **Deviations:** none from LOCKED decisions. Note: orphaned diagnostic uvicorn processes briefly made E2E sessions exercise stale code — killed and re-verified; a sweep-regenerated tracked screenshot was restored. No commits made (harness commits).
- **Completion criteria:** all 7 ✅ (commit/phase-move is the harness's step).
- **Next pending phase:** none — `todo/` holds only this phase's overview pending the harness move.
This commit is contained in:
@@ -282,17 +282,28 @@ def test_chat_streams_deltas_then_done_with_sources(client, db, seeded_kb: FakeR
|
||||
assert done[0]["suggestions"] == []
|
||||
sources = done[0]["sources"]
|
||||
assert sources, "done must carry the cited sources"
|
||||
# Phase 118 (A4): the citation surface is the suggested tier (top-5,
|
||||
# no floor) + the agent's reads (none on this turn) — deduped.
|
||||
assert len(sources) == get_settings().suggested_docs
|
||||
assert sources[0]["path"] == "homelab/kubernetes.md"
|
||||
assert sources[0]["source"] == "docs"
|
||||
assert sources[0]["title"] == "Kubernetes Homelab Cluster"
|
||||
|
||||
# The LLM received the locked HIGH prompt with the FULL document text.
|
||||
# The LLM received the locked HIGH prompt — the ``<documents>`` block
|
||||
# seeds the document's stored SUMMARY (phase 118, LOCKED A6: summary
|
||||
# seeding re-revises the pre-phase full-text contract; the full text
|
||||
# reaches the context only through the capped ``read`` tool). The
|
||||
# summarizer's code-appended pointer line proves the summary block is
|
||||
# present; the doc's full body is no longer seeded.
|
||||
(system, user) = seeded_kb.seen_messages[0][0], seeded_kb.seen_messages[0][1]
|
||||
assert user["content"] == QUESTION
|
||||
assert "<relevance>HIGH</relevance>" in system["content"]
|
||||
assert "DEFLECT_MODE" not in system["content"]
|
||||
assert "<documents>" in system["content"]
|
||||
assert "Talos Linux" in system["content"] # full doc, not just the chunk
|
||||
section = system["content"].split("<documents>", 1)[1].split("</documents>", 1)[0]
|
||||
assert "Summary of" in section # the fake lite model's summary text
|
||||
assert "Source: docs/homelab/kubernetes.md" in section # code-appended pointer
|
||||
assert "Talos Linux" not in section # full doc no longer seeded (A6)
|
||||
assert "HONESTY GATE" in system["content"]
|
||||
|
||||
|
||||
@@ -382,7 +393,19 @@ def test_chat_writes_query_log_row(client, db, seeded_kb: FakeRagLLM) -> None:
|
||||
assert 1 <= row.chunk_hits <= total_chunks
|
||||
assert row.top_score > 0.0 # genuine token-overlap cosine, best hit
|
||||
assert row.top_score <= 1.0
|
||||
assert "docs/homelab/kubernetes.md" in row.sources
|
||||
# Phase 118 (LOCKED A3): the durable record is the FULL retrieval —
|
||||
# the suggested tier (ranks 1–5) + the related tier (ranks 6–7) +
|
||||
# the agent's reads (none on this turn), for this question.
|
||||
for path in (
|
||||
"docs/homelab/kubernetes.md", # rank 1
|
||||
"docs/homelab/templates/deploy.j2", # rank 2
|
||||
"docs/homelab/ssh/ssh_aliases.txt", # rank 3
|
||||
"docs/homelab/container_gitlab/gitlab.md", # rank 4
|
||||
"docs/deployments/new-service.md", # rank 5
|
||||
"docs/homelab/quadlet/cache.volume", # rank 6 (related)
|
||||
"docs/homelab/quadlet/compose.container", # rank 7 (related)
|
||||
):
|
||||
assert path in row.sources
|
||||
assert row.latency_ms >= 0
|
||||
# Why the gate answered (A8 revised): cosine over the threshold OR a
|
||||
# lexical hit. The mock-calibrated threshold (0.30, see tests/conftest.py)
|
||||
@@ -446,10 +469,12 @@ def test_off_topic_question_deflects_honestly(client, db, seeded_kb: FakeRagLLM)
|
||||
def test_done_frame_carries_related_tier_on_grounded_turn(
|
||||
client, db, seeded_kb: FakeRagLLM
|
||||
) -> None:
|
||||
"""Phase 113 (LOCKED A4): a grounded turn's done frame carries the
|
||||
related tier — the ranked docs beyond the cited ceiling, capped at
|
||||
``related_max_docs`` (2), disjoint from the cited list. The durable
|
||||
record keeps the FULL retrieval (cited + related, LOCKED A3)."""
|
||||
"""Phase 118 (LOCKED A3/A4): a grounded turn's done frame carries the
|
||||
suggested tier in ``sources`` (top-5, no floor) and the related
|
||||
tier — the ranked docs from rank 6+ after the suggested set, capped
|
||||
at ``related_max_docs`` (2) — in ``related``, disjoint from the
|
||||
citation surface. The durable record keeps the FULL retrieval
|
||||
(suggested + related + read, LOCKED A3)."""
|
||||
fastapi_app.dependency_overrides[chat_api.get_llm] = lambda: seeded_kb
|
||||
try:
|
||||
_, _, frames = _stream_chat(client, QUESTION)
|
||||
@@ -458,49 +483,54 @@ def test_done_frame_carries_related_tier_on_grounded_turn(
|
||||
|
||||
done = frames[-1]
|
||||
assert done["deflected"] is False
|
||||
sources = {(s["source"], s["path"]) for s in done["sources"]}
|
||||
# A4: the citation surface is the suggested tier (5, no read on this
|
||||
# turn) — ranks 1–5 for the Kubernetes question.
|
||||
sources = [(s["source"], s["path"]) for s in done["sources"]]
|
||||
assert len(sources) == get_settings().suggested_docs
|
||||
assert sources[0] == ("docs", "homelab/kubernetes.md")
|
||||
related = done["related"]
|
||||
assert related, "the 2nd-and-lower scored docs ride the related tier"
|
||||
# Rank 6–7 for the Kubernetes question (after the top-5 suggested
|
||||
# set), capped at related_max_docs.
|
||||
assert [(s["source"], s["path"]) for s in related] == [
|
||||
("docs", "homelab/quadlet/cache.volume"),
|
||||
("docs", "homelab/quadlet/compose.container"),
|
||||
]
|
||||
assert len(related) <= get_settings().related_max_docs
|
||||
# The related tier never overlaps the cited list (the dedupe is by
|
||||
# (source, path) — the same pattern as the cited docs).
|
||||
# The related tier never overlaps the citation surface (the dedupe is
|
||||
# by (source, path) — the same pattern as the cited docs).
|
||||
related_keys = {(s["source"], s["path"]) for s in related}
|
||||
assert sources.isdisjoint(related_keys)
|
||||
# Rank order: the cited top-2 are the kubernetes doc and the template;
|
||||
# the next ranked doc is the ssh aliases file.
|
||||
assert related[0]["path"] == "homelab/ssh/ssh_aliases.txt"
|
||||
assert set(sources).isdisjoint(related_keys)
|
||||
# Every ref carries the chip identity fields (the UI row reuses them).
|
||||
assert all(s["title"] for s in related)
|
||||
|
||||
# Durable record: the full retrieval (cited + related) is logged.
|
||||
# Durable record: the full retrieval (suggested + related) is logged.
|
||||
row = db.scalars(select(QueryLog)).one()
|
||||
assert "docs/homelab/ssh/ssh_aliases.txt" in row.sources
|
||||
assert "docs/homelab/quadlet/cache.volume" in row.sources
|
||||
assert "docs/homelab/kubernetes.md" in row.sources
|
||||
|
||||
|
||||
def test_deflected_done_frame_carries_weak_hits_in_related(
|
||||
client, db, seeded_kb: FakeRagLLM, monkeypatch: pytest.MonkeyPatch
|
||||
client, db, seeded_kb: FakeRagLLM
|
||||
) -> None:
|
||||
"""Phase 113: on a deflected turn nothing clears the bar — the cited
|
||||
tier is empty (done.sources stays [], the phase-112 contract) and the
|
||||
weak hits fall to the related tier (their visibility home). The
|
||||
durable record still carries the retrieval (LOCKED A3)."""
|
||||
monkeypatch.setenv("BOR_SOURCE_USEFULNESS_FLOOR", "0.20")
|
||||
get_settings.cache_clear()
|
||||
"""Phase 118: on a deflected turn done.sources stays [] (the
|
||||
phase-112 contract — a deflected answer cites nothing) and
|
||||
done.related carries rank 6+ after the suggested set (capped at
|
||||
``related_max_docs``) — the weak hits' visibility home; the weak
|
||||
hits themselves are the suggested tier (no floor, A3). The durable
|
||||
record still carries the retrieval (LOCKED A3)."""
|
||||
fastapi_app.dependency_overrides[chat_api.get_llm] = lambda: seeded_kb
|
||||
try:
|
||||
_, _, frames = _stream_chat(client, OFF_TOPIC)
|
||||
done = frames[-1]
|
||||
assert done["deflected"] is True
|
||||
assert done["sources"] == [] # a deflected answer cites nothing
|
||||
# The weak hits (the sourdough question's best mock cosines are
|
||||
# ~0.11/0.04 — both below the 0.20 bar) ride the related tier, in
|
||||
# rank order, capped at related_max_docs.
|
||||
# Rank 6–7 for the sourdough question (after the top-5 suggested
|
||||
# set), capped at related_max_docs.
|
||||
related = done["related"]
|
||||
assert len(related) <= get_settings().related_max_docs
|
||||
assert [s["path"] for s in related][:2] == [
|
||||
"deployments/new-service.md",
|
||||
"homelab/quadlet/lan.network",
|
||||
assert [s["path"] for s in related] == [
|
||||
"homelab/backups.md",
|
||||
"homelab/container_gitlab/gitlab-compose.yaml",
|
||||
]
|
||||
assert all(s["title"] for s in related)
|
||||
assert done["suggestions"] # the "Maybe try" chips are unchanged
|
||||
@@ -510,19 +540,16 @@ def test_deflected_done_frame_carries_weak_hits_in_related(
|
||||
assert row.deflected is True
|
||||
assert row.sources # the weak-hit paths, for threshold tuning
|
||||
finally:
|
||||
# The cache clear is LAST — an assertion that calls get_settings()
|
||||
# after the clear would re-populate the lru_cache with the
|
||||
# monkeypatched value and leak it into the next test.
|
||||
fastapi_app.dependency_overrides.clear()
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_related_doc_read_by_agent_is_cited_not_related(
|
||||
client, db, seeded_kb: FakeRagLLM
|
||||
) -> None:
|
||||
"""Phase 113 × phase 37: an agent-read doc is a citation by definition
|
||||
— when the agent ``read``s a doc that would otherwise ride the related
|
||||
tier, it joins done.sources (deduped, last) and is EXCLUDED from
|
||||
"""Phase 118 × phase 37: an agent-read doc is a citation by definition
|
||||
(LOCKED A4) — when the agent ``read``s a rank-6+ doc (the related
|
||||
tier, "nearby docs"), it joins done.sources (deduped, last — it was
|
||||
not suggested, so the read appends it) and is EXCLUDED from
|
||||
done.related (a "nearby doc" that was actually used must not read as
|
||||
nearby)."""
|
||||
scripted = FakeRagLLM(
|
||||
@@ -531,7 +558,7 @@ def test_related_doc_read_by_agent_is_cited_not_related(
|
||||
ToolCallPiece(
|
||||
id="call_1",
|
||||
name="read",
|
||||
arguments={"path": "docs/homelab/ssh/ssh_aliases.txt"},
|
||||
arguments={"path": "docs/homelab/quadlet/cache.volume"},
|
||||
)
|
||||
]
|
||||
]
|
||||
@@ -545,12 +572,14 @@ def test_related_doc_read_by_agent_is_cited_not_related(
|
||||
done = frames[-1]
|
||||
assert done["deflected"] is False
|
||||
sources = [(s["source"], s["path"]) for s in done["sources"]]
|
||||
assert sources[-1] == ("docs", "homelab/ssh/ssh_aliases.txt") # read ⇒ cited
|
||||
# A4: suggested (5) + the read doc (last).
|
||||
assert len(sources) == get_settings().suggested_docs + 1
|
||||
assert sources[-1] == ("docs", "homelab/quadlet/cache.volume") # read ⇒ cited
|
||||
related = [(s["source"], s["path"]) for s in done["related"]]
|
||||
assert ("docs", "homelab/ssh/ssh_aliases.txt") not in related
|
||||
assert ("docs", "homelab/quadlet/cache.volume") not in related
|
||||
assert set(sources).isdisjoint(set(related))
|
||||
# The OTHER related-tier doc (gitlab, rank 4) stays in the tier.
|
||||
assert ("docs", "homelab/container_gitlab/gitlab.md") in related
|
||||
# The OTHER related-tier doc (compose.container, rank 7) stays in the tier.
|
||||
assert ("docs", "homelab/quadlet/compose.container") in related
|
||||
|
||||
|
||||
def test_keyword_question_grounded_by_lexical_hit_despite_weak_cosine(
|
||||
@@ -1341,6 +1370,11 @@ def test_deflected_turn_stays_byte_identical_without_tools(
|
||||
direct-``chat_stream`` output even for a fake scripted to call tools
|
||||
(its script is never consumed). The LLM was called once, without a
|
||||
``tools`` key."""
|
||||
# The scripted read targets a doc OUTSIDE the OFF_TOPIC retrieval
|
||||
# top-7 (tables.md ranks 11th — not suggested, not rank 6+ related),
|
||||
# so "never read" stays distinguishable from "retrieved" in the
|
||||
# durable record below (phase 118: backups.md — the pre-phase read
|
||||
# target — now rides the rank-6+ related tier, durably recorded).
|
||||
scripted = FakeRagLLM(
|
||||
tool_script=[
|
||||
[ToolCallPiece(id="call_1", name="ls", arguments={})],
|
||||
@@ -1348,7 +1382,7 @@ def test_deflected_turn_stays_byte_identical_without_tools(
|
||||
ToolCallPiece(
|
||||
id="call_2",
|
||||
name="read",
|
||||
arguments={"path": "docs/homelab/backups.md"},
|
||||
arguments={"path": "docs/homelab/tables.md"},
|
||||
)
|
||||
],
|
||||
[StreamPiece("content", "never used — the agent never runs")],
|
||||
@@ -1380,7 +1414,7 @@ def test_deflected_turn_stays_byte_identical_without_tools(
|
||||
if r.question == OFF_TOPIC
|
||||
][-1:]
|
||||
assert row.deflected is True
|
||||
assert "backups.md" not in row.sources
|
||||
assert "tables.md" not in row.sources
|
||||
|
||||
|
||||
def test_zero_max_rounds_reproduce_pre_phase_single_request(
|
||||
|
||||
@@ -63,6 +63,16 @@ FULL_BRAIN: dict[str, Any] = {
|
||||
"sources": [
|
||||
{"source": "Homelab", "path": "kubernetes.md", "title": "Kubernetes Cluster"}
|
||||
],
|
||||
# Phase 113's related-doc tier — the UI persists it with every
|
||||
# grounded brain record (the restore path re-renders the row from
|
||||
# it). It must be an ACCEPTED key: the phase-113 omission (the key
|
||||
# missing from ChatMessage) made the extra="forbid" boundary 422
|
||||
# every done-time auto-save carrying it, so grounded turns' brain
|
||||
# messages never persisted (the A2 quiet failure swallowed the
|
||||
# 422). This round-trip is the regression pin.
|
||||
"related": [
|
||||
{"source": "Homelab", "path": "traefik.md", "title": "Traefik Notes"}
|
||||
],
|
||||
"deflected": False,
|
||||
"suggestions": ["What ports does Traefik expose?"],
|
||||
"thinking": "The kubernetes doc covers the cluster layout…",
|
||||
@@ -175,6 +185,7 @@ def _expect(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"who": m["who"],
|
||||
"text": m["text"],
|
||||
"sources": m.get("sources"),
|
||||
"related": m.get("related"),
|
||||
"deflected": m.get("deflected"),
|
||||
"suggestions": m.get("suggestions"),
|
||||
"thinking": m.get("thinking"),
|
||||
@@ -411,8 +422,8 @@ def test_create_round_trips_full_brain_record(admin_client: TestClient) -> None:
|
||||
)
|
||||
assert r.status_code == 201
|
||||
# The bor.chat.v1-shaped payload round-trips losslessly: every
|
||||
# optional key (sources/deflected/suggestions/thinking/tools/
|
||||
# stopped) survives identical.
|
||||
# optional key (sources/related/deflected/suggestions/thinking/
|
||||
# tools/stopped) survives identical.
|
||||
assert r.json()["messages"][1] == FULL_BRAIN
|
||||
|
||||
|
||||
@@ -516,9 +527,9 @@ def test_get_returns_full_payload_round_trip(admin_client: TestClient) -> None:
|
||||
assert body["id"] == created["id"]
|
||||
assert body["title"] == EXPLICIT_TITLE
|
||||
assert body["message_count"] == 2
|
||||
# Byte-identical payload: the brain record with sources/thinking/
|
||||
# tools/stopped (incl. the `argument: null` tool) survives the trip
|
||||
# to Postgres and back.
|
||||
# Byte-identical payload: the brain record with sources/related/
|
||||
# thinking/tools/stopped (incl. the `argument: null` tool) survives
|
||||
# the trip to Postgres and back.
|
||||
assert body["messages"] == _expect([_user(FIRST_QUESTION), FULL_BRAIN])
|
||||
|
||||
|
||||
|
||||
@@ -30,8 +30,10 @@ ends with the folder-summary stats —
|
||||
(this fixture's 2-doc source holds exactly ONE qualifying subtree: the
|
||||
source root) or ``folder_summaries=skipped`` otherwise — so the line
|
||||
pinned here gains that token, and a KB-changing run burns exactly ONE
|
||||
extra ``lite`` call (the source-root folder summary, markdown files
|
||||
never get a document summary).
|
||||
extra ``lite`` call beyond the phase-118 document summaries (the
|
||||
source-root folder summary; markdown files get document summaries too
|
||||
since phase 118, A2 — so this 2-doc markdown source burns TWO doc-summary
|
||||
calls on a fresh import).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -97,7 +99,8 @@ def _run_main(
|
||||
|
||||
@pytest.fixture()
|
||||
def src(tmp_path: Path) -> Path:
|
||||
"""A source dir with two markdown docs (md → no summary chat calls)."""
|
||||
"""A source dir with two markdown docs (phase 118, A2: both get
|
||||
document summaries — two extra ``chat`` calls over pre-118)."""
|
||||
root = tmp_path / "MyDocs"
|
||||
root.mkdir()
|
||||
(root / "alpha.md").write_text("# Alpha\n\nFirst document.\n", encoding="utf-8")
|
||||
@@ -164,16 +167,18 @@ def test_changed_import_writes_overview_row(
|
||||
"overview=updated sources_version=1 folder_summaries=1/0/0"
|
||||
)
|
||||
assert _version(db) == 1 # phase 53: a changed import bumps exactly once
|
||||
# Exactly two lite calls — the overview + the source-root folder
|
||||
# summary (markdown files never get a document summary, so nothing
|
||||
# else may touch ``chat``).
|
||||
assert len(llm.chat_calls) == 2
|
||||
by_role = {m["role"]: m["content"] for m in llm.chat_calls[0]}
|
||||
# Exactly four lite calls — the two phase-118 document summaries
|
||||
# (markdown included) + the overview + the source-root folder summary
|
||||
# (nothing else may touch ``chat``).
|
||||
assert len(llm.chat_calls) == 4
|
||||
by_role = {m["role"]: m["content"] for m in llm.chat_calls[2]}
|
||||
assert "KB_OVERVIEW_MODE" in by_role["system"]
|
||||
# One line per doc: source — path — title (no summary for markdown).
|
||||
assert "MyDocs — alpha.md — Alpha" in by_role["user"]
|
||||
assert "MyDocs — beta.md — Beta" in by_role["user"]
|
||||
by_role = {m["role"]: m["content"] for m in llm.chat_calls[1]}
|
||||
# One line per doc: source — path — title — first summary line
|
||||
# (phase 118: the markdown docs are summarized too — the fake's
|
||||
# deterministic digest for each).
|
||||
assert "MyDocs — alpha.md — Alpha — Summary of #" in by_role["user"]
|
||||
assert "MyDocs — beta.md — Beta — Summary of #" in by_role["user"]
|
||||
by_role = {m["role"]: m["content"] for m in llm.chat_calls[3]}
|
||||
assert "FOLDER_SUMMARY_MODE" in by_role["system"]
|
||||
assert by_role["user"].splitlines()[0] == "Folder: MyDocs"
|
||||
# The model's outline lands in the single row.
|
||||
@@ -197,17 +202,19 @@ def test_unchanged_reimport_does_not_call_lite(
|
||||
assert out.rstrip().endswith(
|
||||
"overview=updated sources_version=1 folder_summaries=1/0/0"
|
||||
)
|
||||
assert len(llm.chat_calls) == 2 # overview + source-root folder summary
|
||||
# 2 doc summaries (phase 118) + overview + source-root folder summary.
|
||||
assert len(llm.chat_calls) == 4
|
||||
assert _row(db) is not None
|
||||
|
||||
# Same hashes → no KB change → no lite call, previous outline kept.
|
||||
# Same hashes → no KB change → no lite call, previous outline kept —
|
||||
# and nothing to backfill (both summaries are already stored).
|
||||
rc, out = _run_main(monkeypatch, llm, ["--source", str(src)], capsys)
|
||||
assert rc == 0
|
||||
assert "unchanged=2" in out
|
||||
assert out.rstrip().endswith(
|
||||
"overview=skipped sources_version=skipped folder_summaries=skipped"
|
||||
)
|
||||
assert len(llm.chat_calls) == 2 # no new lite call
|
||||
assert len(llm.chat_calls) == 4 # no new lite call
|
||||
row = _row(db)
|
||||
assert row is not None and row.content == "Summary of MyDocs"
|
||||
assert _version(db) == 1 # phase 53: an unchanged re-run never bumps
|
||||
@@ -241,7 +248,10 @@ def test_lite_failure_is_fail_soft(
|
||||
assert out.rstrip().endswith(
|
||||
"overview=failed sources_version=2 folder_summaries=0/1/0"
|
||||
)
|
||||
assert len(bad.chat_calls) == 2 # the (failed) attempts were made
|
||||
# The three (failed) attempts: the changed doc's summary (phase 118),
|
||||
# the overview, and the folder summary — the unchanged, already-
|
||||
# summarized doc burns no backfill.
|
||||
assert len(bad.chat_calls) == 3
|
||||
row = _row(db)
|
||||
assert row is not None
|
||||
assert row.content == previous_content # previous row untouched
|
||||
@@ -262,7 +272,7 @@ def test_limit_run_skips_overview(
|
||||
assert out.rstrip().endswith(
|
||||
"overview=updated sources_version=1 folder_summaries=1/0/0"
|
||||
)
|
||||
assert len(llm.chat_calls) == 2
|
||||
assert len(llm.chat_calls) == 4 # 2 doc summaries + overview + folder
|
||||
|
||||
# An incomplete walk must not rewrite the outline (mirrors the
|
||||
# --prune-with---limit guard) — and must not advance the version.
|
||||
@@ -273,7 +283,9 @@ def test_limit_run_skips_overview(
|
||||
assert out.rstrip().endswith(
|
||||
"overview=skipped sources_version=skipped folder_summaries=skipped"
|
||||
)
|
||||
assert len(llm.chat_calls) == 2 # --limit never burns a lite call
|
||||
# --limit walks only alpha.md: its changed summary is the sole new
|
||||
# lite call; the overview + folder gates skip under --limit.
|
||||
assert len(llm.chat_calls) == 5
|
||||
row = _row(db)
|
||||
assert row is not None and row.content == "Summary of MyDocs"
|
||||
assert _version(db) == 1 # phase 53: --limit debug runs never bump
|
||||
@@ -323,6 +335,7 @@ def test_prune_only_run_bumps_sources_version(
|
||||
|
||||
# Delete one file; a --prune run drops exactly it: no add/update,
|
||||
# but pruned=1 → the version still bumps while the overview skips.
|
||||
assert len(llm.chat_calls) == 4 # 2 doc summaries + overview + folder
|
||||
(src / "alpha.md").unlink()
|
||||
rc, out = _run_main(monkeypatch, llm, ["--source", str(src), "--prune"], capsys)
|
||||
assert rc == 0
|
||||
|
||||
@@ -16,8 +16,8 @@ fixture of phase 56 stays pinned by its own suite):
|
||||
* positive — ``md,dockerfile,containerfile`` walks ``Dockerfile``,
|
||||
``Containerfile`` and ``notes.md`` (the ``Makefile`` negative control
|
||||
stays out): rows + embedded chunks + the deterministic mock
|
||||
``SUMMARY_MODE`` digest for both build files (non-markdown → the
|
||||
phase-30 ``lite`` path), ``formats == {"dockerfile": 1,
|
||||
``SUMMARY_MODE`` digest for EVERY doc (phase 118, A2: markdown
|
||||
included — the phase-30 ``lite`` path), ``formats == {"dockerfile": 1,
|
||||
"containerfile": 1, "md": 1}`` with NO ``unknown`` key;
|
||||
* case — an on-disk ``DOCKERFILE`` imports under the ``dockerfile``
|
||||
token, its row keeps the on-disk case;
|
||||
@@ -146,9 +146,9 @@ def test_name_token_files_import_end_to_end(mock_llm_port: int, db: Session) ->
|
||||
# D2: extensionless files count under their matched token.
|
||||
assert summary.formats == {"dockerfile": 1, "containerfile": 1, "md": 1}
|
||||
assert "unknown" not in summary.formats
|
||||
# Phase 30: both build files are non-markdown → lite summaries;
|
||||
# the markdown control never is.
|
||||
assert (summary.summaries, summary.summary_errors) == (2, 0)
|
||||
# Phase 118 (A2): EVERY doc gets a lite summary — both build
|
||||
# files AND the markdown control.
|
||||
assert (summary.summaries, summary.summary_errors) == (3, 0)
|
||||
|
||||
for rel, sentinel, tokens in (
|
||||
(DOCKER_REL, DOCKER_SENTINEL, DOCKER_SENTINEL_TOKENS),
|
||||
@@ -179,13 +179,18 @@ def test_name_token_files_import_end_to_end(mock_llm_port: int, db: Session) ->
|
||||
assert len(schunks) == 1 and schunks[0].position == -1
|
||||
assert schunks[0].embedding is not None
|
||||
|
||||
# The markdown control doc imported too — but markdown never
|
||||
# gets a summary (phase 30).
|
||||
# The markdown control doc imported too — AND got the mock
|
||||
# ``SUMMARY_MODE`` digest (phase 118, A2: markdown summarized).
|
||||
note = db.scalar(
|
||||
select(Document).where(Document.source == SOURCE, Document.path == NOTES_REL)
|
||||
)
|
||||
assert note is not None
|
||||
assert note.summary is None
|
||||
assert note.summary is not None
|
||||
assert note.summary.startswith("This document covers extensionless fixture notes")
|
||||
assert f"Source: {SOURCE}/{NOTES_REL}" in note.summary
|
||||
note_schunks = [c for c in note.chunks if c.is_summary]
|
||||
assert len(note_schunks) == 1 and note_schunks[0].position == -1
|
||||
assert note_schunks[0].embedding is not None
|
||||
assert [c for c in note.chunks if not c.is_summary]
|
||||
|
||||
# The negative control: no token names Makefile exactly — never
|
||||
@@ -243,7 +248,8 @@ def test_without_tokens_the_extensionless_files_stay_out(
|
||||
summary.files, summary.added, summary.unchanged, summary.updated, summary.errors
|
||||
) == (1, 1, 0, 0, 0)
|
||||
assert summary.formats == {"md": 1}
|
||||
assert summary.summaries == 0
|
||||
# Phase 118 (A2): the md-only run still summarizes the note.
|
||||
assert summary.summaries == 1
|
||||
for rel in (DOCKER_REL, CONTAINER_REL, "Makefile"):
|
||||
assert (
|
||||
db.scalar(
|
||||
|
||||
@@ -117,8 +117,9 @@ def test_novel_extension_imports_end_to_end(mock_llm_port: int, db: Session) ->
|
||||
summary.files, summary.added, summary.unchanged, summary.updated, summary.errors
|
||||
) == (2, 2, 0, 0, 0)
|
||||
assert summary.formats == {"sh": 1, "md": 1}
|
||||
# The .sh file is non-markdown → exactly one lite summary (phase 30).
|
||||
assert (summary.summaries, summary.summary_errors) == (1, 0)
|
||||
# Phase 118 (A2): EVERY doc gets a lite summary — the novel
|
||||
# ``.sh`` file AND the markdown control.
|
||||
assert (summary.summaries, summary.summary_errors) == (2, 0)
|
||||
|
||||
sh = db.scalar(
|
||||
select(Document).where(
|
||||
@@ -148,15 +149,20 @@ def test_novel_extension_imports_end_to_end(mock_llm_port: int, db: Session) ->
|
||||
assert len(schunks) == 1 and schunks[0].position == -1
|
||||
assert schunks[0].embedding is not None
|
||||
|
||||
# The markdown control doc imported too — but markdown never
|
||||
# gets a summary (phase 30).
|
||||
# The markdown control doc imported too — AND got the mock
|
||||
# ``SUMMARY_MODE`` digest (phase 118, A2: markdown summarized).
|
||||
note = db.scalar(
|
||||
select(Document).where(
|
||||
Document.source == SOURCE, Document.path == MD_REL
|
||||
)
|
||||
)
|
||||
assert note is not None
|
||||
assert note.summary is None
|
||||
assert note.summary is not None
|
||||
assert note.summary.startswith("This document covers extension fixture note")
|
||||
assert f"Source: {SOURCE}/{MD_REL}" in note.summary
|
||||
note_schunks = [c for c in note.chunks if c.is_summary]
|
||||
assert len(note_schunks) == 1 and note_schunks[0].position == -1
|
||||
assert note_schunks[0].embedding is not None
|
||||
assert [c for c in note.chunks if not c.is_summary]
|
||||
finally:
|
||||
_cleanup_source(db, SOURCE)
|
||||
@@ -175,7 +181,8 @@ def test_narrowing_to_md_still_excludes_the_novel_extension(
|
||||
summary.files, summary.added, summary.unchanged, summary.updated, summary.errors
|
||||
) == (1, 1, 0, 0, 0)
|
||||
assert summary.formats == {"md": 1}
|
||||
assert summary.summaries == 0
|
||||
# Phase 118 (A2): the md-only run still summarizes the note.
|
||||
assert summary.summaries == 1
|
||||
assert db.scalar(
|
||||
select(Document).where(
|
||||
Document.source == SOURCE, Document.path == SH_REL
|
||||
|
||||
@@ -78,8 +78,9 @@ def test_import_fixtures_end_to_end(admin_client, db) -> None:
|
||||
k8s = next(d for d in docs if d.path == "homelab/kubernetes.md")
|
||||
assert "Talos Linux" in k8s.content and k8s.content_hash
|
||||
|
||||
# Phase 30: the four non-markdown fixtures each gained one embedded
|
||||
# ``is_summary`` chunk, so the DB holds content + summary chunks.
|
||||
# Phase 30; phase 118 (A2): EVERY fixture gained one embedded
|
||||
# ``is_summary`` chunk (markdown included), so the DB holds content +
|
||||
# summary chunks.
|
||||
n_chunks = db.scalar(select(func.count()).select_from(Chunk))
|
||||
assert n_chunks == summary.chunks + summary.summaries
|
||||
for c in db.scalars(select(Chunk)).all():
|
||||
@@ -87,21 +88,15 @@ def test_import_fixtures_end_to_end(admin_client, db) -> None:
|
||||
|
||||
assert summary.summary_errors == 0
|
||||
for d in docs:
|
||||
non_md = Path(d.path).suffix.lower() not in (".md", ".markdown")
|
||||
# Phase 118 (A2): EVERY document — markdown included — carries a
|
||||
# stored summary + exactly one embedded summary chunk (−1).
|
||||
schunks = [c for c in d.chunks if c.is_summary]
|
||||
if non_md:
|
||||
# Lite summary stored + exactly one embedded summary chunk (−1).
|
||||
assert d.summary is not None, f"{d.path} should have a summary"
|
||||
assert len(schunks) == 1
|
||||
assert schunks[0].position == -1
|
||||
assert schunks[0].content == d.summary
|
||||
assert schunks[0].embedding is not None
|
||||
else:
|
||||
# Markdown docs never get a summary (phase 30 scope).
|
||||
assert d.summary is None and not schunks
|
||||
assert summary.summaries == sum(
|
||||
1 for d in docs if Path(d.path).suffix.lower() not in (".md", ".markdown")
|
||||
)
|
||||
assert d.summary is not None, f"{d.path} should have a summary"
|
||||
assert len(schunks) == 1
|
||||
assert schunks[0].position == -1
|
||||
assert schunks[0].content == d.summary
|
||||
assert schunks[0].embedding is not None
|
||||
assert summary.summaries == len(docs) # one per doc, no exceptions
|
||||
|
||||
# The Sources page consumes exactly this shape.
|
||||
r = admin_client.get("/api/docs") # phase 16: the catalog is admin-only
|
||||
|
||||
@@ -60,8 +60,9 @@ def test_ignored_files_never_indexed(db: Session, tmp_path: Path) -> None:
|
||||
# invisible to the pipeline.
|
||||
assert summary.files == 2
|
||||
assert summary.errors == 0
|
||||
# The kept non-markdown file IS summarized; the ignored .txt is not.
|
||||
assert summary.summaries == 1
|
||||
# Both kept files ARE summarized (phase 118, A2: markdown too);
|
||||
# the ignored files are never walked, hence never summarized.
|
||||
assert summary.summaries == 2
|
||||
assert summary.summary_errors == 0
|
||||
|
||||
docs = db.scalars(select(Document)).all()
|
||||
@@ -78,12 +79,17 @@ def test_ignored_files_never_indexed(db: Session, tmp_path: Path) -> None:
|
||||
assert not any(
|
||||
"SECRET-CONTENT" in t or "IGNORED-TEXT-CONTENT" in t for t in texts
|
||||
)
|
||||
# Exactly one summary call (top.txt) — the ignored files never reached
|
||||
# the lite model.
|
||||
assert len(llm.chat_calls) == 1
|
||||
user = next(m["content"] for m in llm.chat_calls[0] if m["role"] == "user")
|
||||
assert "TOP-TEXT-CONTENT" in user
|
||||
assert "SECRET-CONTENT" not in user and "IGNORED-TEXT-CONTENT" not in user
|
||||
# Exactly one summary call per kept file (keep.md + top.txt) — the
|
||||
# ignored files never reached the lite model.
|
||||
assert len(llm.chat_calls) == 2
|
||||
users = [
|
||||
next(m["content"] for m in msgs if m["role"] == "user")
|
||||
for msgs in llm.chat_calls
|
||||
]
|
||||
assert any("TOP-TEXT-CONTENT" in u for u in users)
|
||||
assert any("kept body" in u for u in users)
|
||||
for u in users:
|
||||
assert "SECRET-CONTENT" not in u and "IGNORED-TEXT-CONTENT" not in u
|
||||
top = next(d for d in docs if d.path == "top.txt")
|
||||
assert top.summary is not None
|
||||
_reset(db)
|
||||
|
||||
@@ -62,7 +62,8 @@ def test_hidden_paths_not_indexed_by_default(db: Session, tmp_path: Path) -> Non
|
||||
assert summary.files == 1
|
||||
assert summary.added == 1
|
||||
assert summary.errors == 0
|
||||
assert summary.summaries == 0
|
||||
# Phase 118 (A2): the one visible md IS summarized.
|
||||
assert summary.summaries == 1
|
||||
assert summary.summary_errors == 0
|
||||
|
||||
docs = db.scalars(select(Document)).all()
|
||||
@@ -73,8 +74,9 @@ def test_hidden_paths_not_indexed_by_default(db: Session, tmp_path: Path) -> Non
|
||||
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
|
||||
# Only the visible md reached the lite model (phase 118, A2) — the
|
||||
# hidden files were never walked.
|
||||
assert len(llm.chat_calls) == 1
|
||||
_reset(db)
|
||||
|
||||
|
||||
@@ -105,14 +107,14 @@ def test_hidden_paths_indexed_when_flag_on(db: Session, tmp_path: Path) -> None:
|
||||
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).
|
||||
# The hidden md was embedded like any visible md — AND summarized
|
||||
# like any other doc (phase 118, A2: markdown included).
|
||||
note = db.scalar(select(Document).where(Document.path == ".hidden/note.md"))
|
||||
assert note is not None and note.summary is None
|
||||
assert note is not None and note.summary is not 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.
|
||||
# The hidden yaml went through the full doc 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 = [
|
||||
@@ -121,11 +123,16 @@ def test_hidden_paths_indexed_when_flag_on(db: Session, tmp_path: Path) -> None:
|
||||
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
|
||||
# EVERY doc reached the lite model (phase 118, A2: markdown too).
|
||||
assert summary.summaries == 3
|
||||
assert len(llm.chat_calls) == 3
|
||||
users = [
|
||||
next(m["content"] for m in msgs if m["role"] == "user")
|
||||
for msgs in llm.chat_calls
|
||||
]
|
||||
assert any("HIDDEN-YAML-VALUE" in u for u in users)
|
||||
assert any("HIDDEN-MD-CONTENT" in u for u in users)
|
||||
assert any("visible body" in u for u in users)
|
||||
_reset(db)
|
||||
|
||||
|
||||
|
||||
@@ -579,7 +579,8 @@ def test_limit_run_skips_folder_generation(
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
"""A ``--limit`` debug run (an incomplete walk) never generates —
|
||||
no ``lite`` call at all, no rows, the line says ``skipped``."""
|
||||
no overview/folder ``lite`` call, no rows, the line says
|
||||
``skipped``."""
|
||||
llm = FakeEmbedder()
|
||||
rc, out = _run_main(monkeypatch, llm, ["--source", str(src), "--limit", "2"], capsys)
|
||||
assert rc == 0
|
||||
@@ -587,7 +588,10 @@ def test_limit_run_skips_folder_generation(
|
||||
assert out.rstrip().endswith(
|
||||
"overview=skipped sources_version=skipped folder_summaries=skipped"
|
||||
)
|
||||
assert llm.chat_calls == [] # no lite call, any mode
|
||||
# Phase 118 (A2): the two walked docs still burn their document
|
||||
# summaries — but the folder generation never fires (and neither
|
||||
# does the overview).
|
||||
assert len(llm.chat_calls) == 2 # doc summaries only, any other mode absent
|
||||
assert _rows(db) == {} # an incomplete walk never writes rows
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user