phase: 118_summary_seed_context
Build and Push Containers / build-and-push-app (push) Successful in 2m2s
Build and Push Containers / build-and-push-db (push) Successful in 14s

**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:
2026-09-16 06:57:49 -04:00
parent 21aad84a6d
commit 9820c361b0
80 changed files with 4690 additions and 1302 deletions
+78 -44
View File
@@ -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(
+16 -5
View File
@@ -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])
+31 -18
View File
@@ -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
+15 -9
View File
@@ -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
+11 -16
View File
@@ -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
+14 -8
View File
@@ -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