phase: 106_document_dates
Everything is verified green. Final report: **Phase 106 — Document dates (final verification pass; all 10 tasks already complete)** - Verified all phase artifacts: alembic `0020` (dev DB at `0020`), `doc_dates.py`, git `file_commit_dates`, importer `doc_dates_by_root`/`dates_updated`, both entry-point wirings, date APIs + tree `created_at`/`updated_at`, LLM surfaces (prompt block, `read` line 2, appended `ls` field), `apply_recency_boost` in `retrieve()`, UI columns/badge, admin editor, mock-LLM regex — all present and correct; no defects found, no fixes needed. - `uv run pytest --cov=app --cov-report=term-missing` → **2299 passed, TOTAL 99%** (>90% ✓) - `uv run pytest tests/e2e/test_document_dates.py -v --no-cov` → **6/6 passed** in isolation (DB up) - 12 regression E2E suites (retrieval_quality, whole_document_context, agent_document_tools, ls_tree_drilldown, read_truncation_cap, kb_tree, kb_tree_nav, document_viewer, edit_summaries, import_documents, sync_button, hidden_folders_toggle, smoke) → **all green in isolation** - `uv run ruff check .` → clean; `uv run pyright` → **0 errors, 0 warnings** **Completion criteria:** 1) non-null `created_at` + 0020 upgrade/downgrade on dev DB ✓ (real-Alembic integration tests) 2) sync refresh/older/manual-persists/content-reset/no sources_meta bump ✓ 3) zip/tar mtime + future→today ✓ 4) LLM date surfaces + cross-check ✓ 5) UI Created/Updated/badge positions ✓ 6) admin editor set+revert round-trip ✓ 7) old-correct-beats-new-similar (defaults & boost-off) + near-tie + `BOR_RECENCY_BOOST=0` byte-identical ✓ 8) full gate ✓ 9) commit/phase-move — left to harness per instructions. - **Notable:** recency default tuned 0.001 → **0.0007** (task 07 step 5 explicitly permits; measured margins recorded in `test_recency_boost.py` docstring). - **Next pending phase:** none — `todo/` holds only this phase.
This commit is contained in:
+85
-51
@@ -37,6 +37,7 @@ import logging
|
||||
import uuid
|
||||
from collections.abc import AsyncGenerator, AsyncIterator, Sequence
|
||||
from copy import deepcopy
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import pytest
|
||||
@@ -72,6 +73,13 @@ def _settings(**kwargs: Any) -> Settings:
|
||||
return Settings(**kwargs) # pyright: ignore[reportCallIssue]
|
||||
|
||||
|
||||
#: The fixture documents' fixed creation date (phase 106, D5): the
|
||||
#: agent formats ``doc.created_at`` on the ``read`` result's second
|
||||
#: line — the detached fixture rows carry it exactly as the NOT NULL
|
||||
#: DB column guarantees it for real rows.
|
||||
_FIXTURE_CREATED_AT = datetime(2024, 6, 15, 12, 0, 0, tzinfo=UTC)
|
||||
|
||||
|
||||
def _doc(source: str, path: str, title: str = "Title", content: str = "CONTENT") -> Document:
|
||||
return Document(
|
||||
id=uuid.uuid4(),
|
||||
@@ -81,6 +89,7 @@ def _doc(source: str, path: str, title: str = "Title", content: str = "CONTENT")
|
||||
title=title,
|
||||
content=content,
|
||||
content_hash="0" * 64,
|
||||
created_at=_FIXTURE_CREATED_AT,
|
||||
)
|
||||
|
||||
|
||||
@@ -438,7 +447,9 @@ def test_ls_then_read_then_answer(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# lookup, no self-correction candidates (phase 70).
|
||||
assert calls == [("Homelab", "aws-route53.md")]
|
||||
|
||||
# The follow-up request carries the assistant tool-call + tool result.
|
||||
# The follow-up request carries the assistant tool-call + tool
|
||||
# result (phase 106, D5: the ``date:`` second line rides every
|
||||
# read — the first line is byte-identical).
|
||||
msgs = llm.requests[1][0]
|
||||
assert msgs[0] == {"role": "system", "content": "SYSTEM_PROMPT"}
|
||||
assert msgs[1] == {"role": "user", "content": "QUESTION"}
|
||||
@@ -468,7 +479,11 @@ def test_ls_then_read_then_answer(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
assert msgs[5] == {
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_2",
|
||||
"content": "Document Homelab/aws-route53.md:\nR53-CONTENT", # full text, no cap
|
||||
"content": (
|
||||
"Document Homelab/aws-route53.md:\n"
|
||||
"date: 2024-06-15\n"
|
||||
"R53-CONTENT"
|
||||
), # full text, no cap (date: the D5 second line)
|
||||
}
|
||||
|
||||
|
||||
@@ -641,14 +656,14 @@ def test_ls_source_scope_lists_root_folder(monkeypatch: pytest.MonkeyPatch) -> N
|
||||
"""A registered source name (no ``/``): the source's ROOT folder —
|
||||
subfolders (2-space-indented, path order, ``: {summary}`` only when
|
||||
stored) + the root's own file lines in EXACTLY the
|
||||
``source: X | path: Y | title: Z`` format — the pinned template,
|
||||
counted."""
|
||||
``source: X | path: Y | title: Z | date: YYYY-MM-DD`` format (the
|
||||
phase-106 D5 appended date field) — the pinned template, counted."""
|
||||
monkeypatch.setattr(
|
||||
agent,
|
||||
"ls_folder",
|
||||
lambda db, source, folder: (
|
||||
[("backups", 2, "Backup notes."), ("networking", 1, None)],
|
||||
[("Homelab", "readme.md", "Readme")],
|
||||
[("Homelab", "readme.md", "Readme", "2024-06-15")],
|
||||
1,
|
||||
),
|
||||
)
|
||||
@@ -665,7 +680,7 @@ def test_ls_source_scope_lists_root_folder(monkeypatch: pytest.MonkeyPatch) -> N
|
||||
" backups/ — 2 documents: Backup notes.\n"
|
||||
" networking/ — 1 documents\n"
|
||||
"\n"
|
||||
"source: Homelab | path: readme.md | title: Readme"
|
||||
"source: Homelab | path: readme.md | title: Readme | date: 2024-06-15"
|
||||
)
|
||||
assert holder.tool_calls == 1
|
||||
|
||||
@@ -677,11 +692,11 @@ def test_ls_nested_folder_scope_lists_one_level_deeper(
|
||||
lines, identity = ``source/folder`` (the same template as the
|
||||
root), counted; the fetchers are the source-scoped ones."""
|
||||
|
||||
def _rows(db: Any, source: str) -> list[tuple[str, str]]:
|
||||
def _rows(db: Any, source: str) -> list[tuple[str, str, str]]:
|
||||
assert (source, db) == ("Homelab", None)
|
||||
return [
|
||||
("networking/lan.md", "LAN"),
|
||||
("networking/vpn.md", "VPN"),
|
||||
("networking/lan.md", "LAN", "2024-06-15"),
|
||||
("networking/vpn.md", "VPN", "2024-06-15"),
|
||||
]
|
||||
|
||||
monkeypatch.setattr(agent, "_source_document_rows", _rows)
|
||||
@@ -702,8 +717,8 @@ def test_ls_nested_folder_scope_lists_one_level_deeper(
|
||||
assert llm.requests[1][0][3]["content"] == (
|
||||
"Homelab/networking — 2 documents, 0 folders:\n"
|
||||
"\n"
|
||||
"source: Homelab | path: networking/lan.md | title: LAN\n"
|
||||
"source: Homelab | path: networking/vpn.md | title: VPN"
|
||||
"source: Homelab | path: networking/lan.md | title: LAN | date: 2024-06-15\n"
|
||||
"source: Homelab | path: networking/vpn.md | title: VPN | date: 2024-06-15"
|
||||
)
|
||||
assert holder.tool_calls == 1
|
||||
|
||||
@@ -812,9 +827,9 @@ def test_ls_unknown_top_level_folder_gets_not_a_folder_teaching(
|
||||
agent,
|
||||
"_source_document_rows",
|
||||
lambda db, source: [
|
||||
("backups/cron.md", "Cron"),
|
||||
("containers/caddy.md", "Caddy"),
|
||||
("networking/lan.md", "LAN"),
|
||||
("backups/cron.md", "Cron", "2024-06-15"),
|
||||
("containers/caddy.md", "Caddy", "2024-06-15"),
|
||||
("networking/lan.md", "LAN", "2024-06-15"),
|
||||
],
|
||||
)
|
||||
monkeypatch.setattr(agent, "_source_folder_summaries", lambda db, source: {})
|
||||
@@ -847,9 +862,9 @@ def test_ls_unknown_nested_folder_gets_not_a_folder_with_nested_parent(
|
||||
agent,
|
||||
"_source_document_rows",
|
||||
lambda db, source: [
|
||||
("networking/lan/a.md", "A"),
|
||||
("networking/vpn/b.md", "B"),
|
||||
("readme.md", "Readme"),
|
||||
("networking/lan/a.md", "A", "2024-06-15"),
|
||||
("networking/vpn/b.md", "B", "2024-06-15"),
|
||||
("readme.md", "Readme", "2024-06-15"),
|
||||
],
|
||||
)
|
||||
monkeypatch.setattr(agent, "_source_folder_summaries", lambda db, source: {})
|
||||
@@ -878,7 +893,10 @@ def test_ls_file_path_scope_gets_not_a_folder(monkeypatch: pytest.MonkeyPatch) -
|
||||
monkeypatch.setattr(
|
||||
agent,
|
||||
"_source_document_rows",
|
||||
lambda db, source: [("notes.md", "Notes"), ("a/b.md", "B")],
|
||||
lambda db, source: [
|
||||
("notes.md", "Notes", "2024-06-15"),
|
||||
("a/b.md", "B", "2024-06-15"),
|
||||
],
|
||||
)
|
||||
monkeypatch.setattr(agent, "_source_folder_summaries", lambda db, source: {})
|
||||
monkeypatch.setattr(agent, "list_source_names", lambda db: ["S"])
|
||||
@@ -937,9 +955,9 @@ def test_ls_folder_composes_the_fetchers(monkeypatch: pytest.MonkeyPatch) -> Non
|
||||
monkeypatch surface)."""
|
||||
seen: list[tuple[str, str, str]] = []
|
||||
|
||||
def _rows(db: Any, source: str) -> list[tuple[str, str]]:
|
||||
def _rows(db: Any, source: str) -> list[tuple[str, str, str]]:
|
||||
seen.append(("rows", source, ""))
|
||||
return [("a/b.md", "B"), ("a.md", "A")]
|
||||
return [("a/b.md", "B", "2024-06-15"), ("a.md", "A", "2024-06-15")]
|
||||
|
||||
def _summaries(db: Any, source: str) -> dict[str, str]:
|
||||
seen.append(("summaries", source, ""))
|
||||
@@ -949,7 +967,7 @@ def test_ls_folder_composes_the_fetchers(monkeypatch: pytest.MonkeyPatch) -> Non
|
||||
monkeypatch.setattr(agent, "_source_folder_summaries", _summaries)
|
||||
assert agent.ls_folder(cast("Session", object()), "S", "") == (
|
||||
[("a", 1, "A stuff.")],
|
||||
[("S", "a.md", "A")],
|
||||
[("S", "a.md", "A", "2024-06-15")],
|
||||
1,
|
||||
)
|
||||
assert seen == [("rows", "S", ""), ("summaries", "S", "")]
|
||||
@@ -961,10 +979,10 @@ def test_group_folder_listing_subfolder_recursion_and_counts() -> None:
|
||||
counts for BOTH ``a`` and ``a/b``), path order, the stored summary
|
||||
attached or ``None``."""
|
||||
rows = [
|
||||
("a/b/c.md", "C"),
|
||||
("a/b/d.md", "D"),
|
||||
("a/e.md", "E"),
|
||||
("f.md", "F"),
|
||||
("a/b/c.md", "C", "2024-06-15"),
|
||||
("a/b/d.md", "D", "2024-06-15"),
|
||||
("a/e.md", "E", "2024-06-15"),
|
||||
("f.md", "F", "2024-06-15"),
|
||||
]
|
||||
sub, files, total = agent.group_folder_listing(
|
||||
"S", "", rows, {"a": "A subtree.", "a/b": "B subtree."}
|
||||
@@ -974,7 +992,7 @@ def test_group_folder_listing_subfolder_recursion_and_counts() -> None:
|
||||
# recursive subtree (a/e.md + a/b/c.md + a/b/d.md), the stored
|
||||
# summary attached.
|
||||
assert sub == [("a", 3, "A subtree.")]
|
||||
assert files == [("S", "f.md", "F")]
|
||||
assert files == [("S", "f.md", "F", "2024-06-15")]
|
||||
assert total == 1
|
||||
# One level down: a/b is a's direct subfolder with its own count.
|
||||
sub2, _files2, _total2 = agent.group_folder_listing("S", "a", rows, {"a/b": "B subtree."})
|
||||
@@ -987,13 +1005,13 @@ def test_group_folder_listing_nested_level_counts_and_membership() -> None:
|
||||
file of ``a``) — membership is the folder_of rule, order is path
|
||||
order."""
|
||||
rows = [
|
||||
("a/b/c.md", "C"),
|
||||
("a/b/d.md", "D"),
|
||||
("a/e.md", "E"),
|
||||
("a/b/c.md", "C", "2024-06-15"),
|
||||
("a/b/d.md", "D", "2024-06-15"),
|
||||
("a/e.md", "E", "2024-06-15"),
|
||||
]
|
||||
sub, files, total = agent.group_folder_listing("S", "a", rows, {})
|
||||
assert sub == [("a/b", 2, None)]
|
||||
assert files == [("S", "a/e.md", "E")]
|
||||
assert files == [("S", "a/e.md", "E", "2024-06-15")]
|
||||
assert total == 1
|
||||
|
||||
|
||||
@@ -1004,18 +1022,18 @@ def test_group_folder_listing_file_path_is_not_a_folder() -> None:
|
||||
sharing a real folder's name counts for that folder, the existence
|
||||
rule intact)."""
|
||||
rows = [
|
||||
("a.md", "A"), # a file at the root, and a folder name? NO —
|
||||
("b/x.md", "X"), # nothing starts with "a.md/"
|
||||
("a.md", "A", "2024-06-15"), # a file at the root, and a folder name? NO —
|
||||
("b/x.md", "X", "2024-06-15"), # nothing starts with "a.md/"
|
||||
]
|
||||
sub, files, total = agent.group_folder_listing("S", "", rows, {})
|
||||
assert sub == [("b", 1, None)] # "a.md" is NOT a subfolder
|
||||
assert files == [("S", "a.md", "A")] # b/x.md is NOT a direct root file
|
||||
assert files == [("S", "a.md", "A", "2024-06-15")] # b/x.md is NOT a direct root file
|
||||
assert total == 1
|
||||
# The path == folder arm: a doc named "a" under a real folder "a/".
|
||||
rows2 = [("a", "FileA"), ("a/c.md", "C")]
|
||||
rows2 = [("a", "FileA", "2024-06-15"), ("a/c.md", "C", "2024-06-15")]
|
||||
sub2, files2, total2 = agent.group_folder_listing("S", "", rows2, {})
|
||||
assert sub2 == [("a", 2, None)] # the file "a" counts for folder "a"
|
||||
assert files2 == [("S", "a", "FileA")] # …and is a direct ROOT file
|
||||
assert files2 == [("S", "a", "FileA", "2024-06-15")] # …and is a direct ROOT file
|
||||
assert total2 == 1
|
||||
|
||||
|
||||
@@ -1023,14 +1041,14 @@ def test_group_folder_listing_caps_files_at_fifty_keeps_the_total() -> None:
|
||||
"""The cap: 51 direct files → 50 file lines + the PRE-cap total (51)
|
||||
for the renderer's note; 50 files → 50 lines, no note material.
|
||||
A 500-file folder costs 50 lines, never 500."""
|
||||
rows51 = [(f"big/f{i:03d}.md", f"T{i}") for i in range(51)]
|
||||
rows51 = [(f"big/f{i:03d}.md", f"T{i}", "2024-06-15") for i in range(51)]
|
||||
sub, files, total = agent.group_folder_listing("S", "big", rows51, {})
|
||||
assert sub == []
|
||||
assert total == 51
|
||||
assert len(files) == 50
|
||||
assert files[0] == ("S", "big/f000.md", "T0")
|
||||
assert files[-1] == ("S", "big/f049.md", "T49")
|
||||
rows50 = [(f"big/f{i:03d}.md", f"T{i}") for i in range(50)]
|
||||
assert files[0] == ("S", "big/f000.md", "T0", "2024-06-15")
|
||||
assert files[-1] == ("S", "big/f049.md", "T49", "2024-06-15")
|
||||
rows50 = [(f"big/f{i:03d}.md", f"T{i}", "2024-06-15") for i in range(50)]
|
||||
_sub, files50, total50 = agent.group_folder_listing("S", "big", rows50, {})
|
||||
assert total50 == 50 and len(files50) == 50
|
||||
|
||||
@@ -1056,7 +1074,7 @@ def test_render_folder_listing_root_template() -> None:
|
||||
agent.render_folder_listing(
|
||||
"Homelab",
|
||||
[("backups", 2, "Backup notes."), ("networking", 1, None)],
|
||||
[("Homelab", "readme.md", "Readme")],
|
||||
[("Homelab", "readme.md", "Readme", "2024-06-15")],
|
||||
1,
|
||||
)
|
||||
== "Homelab — 1 documents, 2 folders:\n"
|
||||
@@ -1064,7 +1082,7 @@ def test_render_folder_listing_root_template() -> None:
|
||||
" backups/ — 2 documents: Backup notes.\n"
|
||||
" networking/ — 1 documents\n"
|
||||
"\n"
|
||||
"source: Homelab | path: readme.md | title: Readme"
|
||||
"source: Homelab | path: readme.md | title: Readme | date: 2024-06-15"
|
||||
)
|
||||
|
||||
|
||||
@@ -1090,7 +1108,7 @@ def test_render_folder_listing_cap_note_only_past_fifty() -> None:
|
||||
cap: 51 → 50 lines + the deterministic grep-pointer note (the
|
||||
``…and 1 more…`` shape — unpluralized, the house pin); 50 → no
|
||||
note."""
|
||||
files51 = [("S", f"f{i:03d}.md", f"T{i}") for i in range(51)]
|
||||
files51 = [("S", f"f{i:03d}.md", f"T{i}", "2024-06-15") for i in range(51)]
|
||||
capped = files51[:50]
|
||||
rendered = agent.render_folder_listing("S/big", [], capped, 51)
|
||||
lines = rendered.splitlines()
|
||||
@@ -1100,9 +1118,11 @@ def test_render_folder_listing_cap_note_only_past_fifty() -> None:
|
||||
"…and 1 more documents in this folder — use grep (pattern) to "
|
||||
"find a specific one."
|
||||
)
|
||||
files50 = [("S", f"f{i:03d}.md", f"T{i}") for i in range(50)]
|
||||
files50 = [("S", f"f{i:03d}.md", f"T{i}", "2024-06-15") for i in range(50)]
|
||||
rendered50 = agent.render_folder_listing("S/big", [], files50, 50)
|
||||
assert rendered50.splitlines()[-1] == "source: S | path: f049.md | title: T49"
|
||||
assert rendered50.splitlines()[-1] == (
|
||||
"source: S | path: f049.md | title: T49 | date: 2024-06-15"
|
||||
)
|
||||
assert "more documents" not in rendered50
|
||||
|
||||
|
||||
@@ -1145,7 +1165,9 @@ def test_read_combined_path_resolves_and_returns_full_content(
|
||||
assert holder.read_docs == [doc]
|
||||
assert holder.tool_calls == 1
|
||||
assert llm.requests[1][0][3]["content"] == (
|
||||
"Document Homelab/active/container_caddy/caddy.md:\nCADDY-CONTENT"
|
||||
"Document Homelab/active/container_caddy/caddy.md:\n"
|
||||
"date: 2024-06-15\n"
|
||||
"CADDY-CONTENT"
|
||||
)
|
||||
|
||||
|
||||
@@ -1533,7 +1555,9 @@ def test_reading_an_already_read_doc_is_deduped(monkeypatch: pytest.MonkeyPatch)
|
||||
asyncio.run(_run(llm, holder, _settings()))
|
||||
assert holder.read_docs == [doc] # appended exactly once
|
||||
assert holder.tool_calls == 1 # the re-read counts nothing
|
||||
assert llm.requests[1][0][3]["content"] == "Document S/a.md:\nA-CONTENT"
|
||||
assert llm.requests[1][0][3]["content"] == (
|
||||
"Document S/a.md:\ndate: 2024-06-15\nA-CONTENT"
|
||||
)
|
||||
assert llm.requests[2][0][5]["content"] == agent.ALREADY_IN_CONTEXT
|
||||
# Rejected → the tools are still offered on the next request…
|
||||
assert llm.requests[2][1] == AGENT_TOOLS
|
||||
@@ -1558,8 +1582,11 @@ def test_read_exactly_at_cap_is_byte_identical_and_untruncated(
|
||||
[StreamPiece("content", "ans")],
|
||||
)
|
||||
out = asyncio.run(_run(llm, holder, _settings(read_max_chars=cap)))
|
||||
# Byte-identical to today's read result (no marker, no notice).
|
||||
assert llm.requests[1][0][3]["content"] == "Document S/big.md:\n" + content
|
||||
# The read result plus the phase-106 D5 date line (no marker, no
|
||||
# notice).
|
||||
assert llm.requests[1][0][3]["content"] == (
|
||||
"Document S/big.md:\ndate: 2024-06-15\n" + content
|
||||
)
|
||||
assert TRUNCATION_MARKER not in llm.requests[1][0][3]["content"]
|
||||
# No truncation recorded, none surfaced to the loop.
|
||||
assert holder.read_truncations == []
|
||||
@@ -1589,6 +1616,7 @@ def test_read_at_cap_plus_one_truncates_with_marker_and_notice(
|
||||
asyncio.run(_run(llm, holder, _settings(read_max_chars=cap)))
|
||||
expected = (
|
||||
"Document S/big.md:\n"
|
||||
"date: 2024-06-15\n"
|
||||
+ content[:cap]
|
||||
+ "\n"
|
||||
+ TRUNCATION_MARKER
|
||||
@@ -2247,7 +2275,9 @@ def test_grep_counts_but_never_adds_context(monkeypatch: pytest.MonkeyPatch) ->
|
||||
assert holder.tool_calls == 2 # grep + read, both executed
|
||||
assert holder.read_docs == [doc] # only the read added context (A5)
|
||||
assert llm.requests[1][0][3]["content"] == "S/a.md:1: needle here"
|
||||
assert llm.requests[2][0][5]["content"] == "Document S/a.md:\nneedle here"
|
||||
assert llm.requests[2][0][5]["content"] == (
|
||||
"Document S/a.md:\ndate: 2024-06-15\nneedle here"
|
||||
)
|
||||
|
||||
|
||||
# ---------- unlimited calls: re-lists and multi-reads (phase 45) ----------
|
||||
@@ -2292,8 +2322,12 @@ def test_multi_read_executes_without_budgets(monkeypatch: pytest.MonkeyPatch) ->
|
||||
asyncio.run(_run(llm, holder, _settings()))
|
||||
assert holder.read_docs == [a, b] # both reads appended, in order
|
||||
assert holder.tool_calls == 2
|
||||
assert llm.requests[1][0][3]["content"] == "Document S/a.md:\nA-CONTENT"
|
||||
assert llm.requests[2][0][5]["content"] == "Document S/b.md:\nB-CONTENT"
|
||||
assert llm.requests[1][0][3]["content"] == (
|
||||
"Document S/a.md:\ndate: 2024-06-15\nA-CONTENT"
|
||||
)
|
||||
assert llm.requests[2][0][5]["content"] == (
|
||||
"Document S/b.md:\ndate: 2024-06-15\nB-CONTENT"
|
||||
)
|
||||
assert llm.requests[2][1] == AGENT_TOOLS # the second read was still offered
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user