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:
@@ -18,6 +18,15 @@ T0 = "2026-09-01T08:00:00+00:00"
|
||||
T1 = "2026-09-02T08:00:00+00:00"
|
||||
T2 = "2026-09-03T08:00:00+00:00"
|
||||
|
||||
# Phase 106 (task 05): the 6th catalogue element — the document's
|
||||
# creation date (``created_at``, ISO-8601). Deliberately DISTINCT from
|
||||
# the ``indexed_at`` stamps so a test that confuses the two columns
|
||||
# fails loudly.
|
||||
C0 = "2020-01-01T00:00:00+00:00"
|
||||
C1 = "2021-06-15T12:00:00+00:00"
|
||||
C2 = "2022-03-01T06:00:00+00:00"
|
||||
C3 = "2023-11-30T23:59:59+00:00"
|
||||
|
||||
|
||||
def _folder_nodes(node) -> list:
|
||||
"""The folder-kind children of a source/folder node, in order."""
|
||||
@@ -34,10 +43,10 @@ def test_multi_source_registry_order_leads_and_indexed_only_appended() -> None:
|
||||
indexed-only sources in alphabetical order (the superset rule)."""
|
||||
names = ["beta", "alpha", "empty"] # registry order — NOT alphabetical
|
||||
doc_rows: list[TreeDocRow] = [
|
||||
("beta", "b.md", "B", 1, T0),
|
||||
("alpha", "a.md", "A", 1, T0),
|
||||
("gamma", "g.md", "G", 1, T0), # indexed-only → appended
|
||||
("delta", "d.md", "D", 1, T0), # indexed-only → appended
|
||||
("beta", "b.md", "B", 1, T0, C0),
|
||||
("alpha", "a.md", "A", 1, T0, C0),
|
||||
("gamma", "g.md", "G", 1, T0, C0), # indexed-only → appended
|
||||
("delta", "d.md", "D", 1, T0, C0), # indexed-only → appended
|
||||
]
|
||||
tree = build_kb_tree(names, doc_rows, {})
|
||||
assert [s.name for s in tree] == ["beta", "alpha", "empty", "delta", "gamma"]
|
||||
@@ -59,6 +68,8 @@ def test_registered_zero_document_source_lists_empty() -> None:
|
||||
assert tree[0].documents == 0
|
||||
assert tree[0].children == []
|
||||
assert tree[0].summary is None
|
||||
# Phase 106 (D9): a 0-document source has no dates at all.
|
||||
assert tree[0].updated_at is None
|
||||
|
||||
|
||||
def test_nested_document_counts_into_source_ancestors_and_own_folder() -> None:
|
||||
@@ -66,10 +77,10 @@ def test_nested_document_counts_into_source_ancestors_and_own_folder() -> None:
|
||||
to ``a/b``, and to ``a/b/c`` (the recursive subtree, the phase-94
|
||||
``ls`` count rule)."""
|
||||
rows = [
|
||||
("S", "a/b/c/deep.md", "Deep", 1, T0),
|
||||
("S", "a/b/shallow.md", "Shallow", 1, T0),
|
||||
("S", "a/top.md", "Top", 1, T0),
|
||||
("S", "root.md", "Root", 1, T0),
|
||||
("S", "a/b/c/deep.md", "Deep", 1, T0, C0),
|
||||
("S", "a/b/shallow.md", "Shallow", 1, T0, C0),
|
||||
("S", "a/top.md", "Top", 1, T0, C0),
|
||||
("S", "root.md", "Root", 1, T0, C0),
|
||||
]
|
||||
(source,) = build_kb_tree(["S"], rows, {})
|
||||
assert source.documents == 4
|
||||
@@ -91,8 +102,8 @@ def test_existence_rule_a_file_path_is_never_a_folder() -> None:
|
||||
starts with ``folder + "/"``); a document's own path — even one
|
||||
with dots — never creates a folder."""
|
||||
rows = [
|
||||
("S", "x.md", "X", 1, T0),
|
||||
("S", "x.y/z.md", "Z", 1, T0),
|
||||
("S", "x.md", "X", 1, T0, C0),
|
||||
("S", "x.y/z.md", "Z", 1, T0, C1),
|
||||
]
|
||||
(source,) = build_kb_tree(["S"], rows, {})
|
||||
folders = _folder_nodes(source)
|
||||
@@ -109,8 +120,8 @@ def test_file_folder_name_collision_both_appear() -> None:
|
||||
colliding file counts into the folder's subtree (the ``ls`` count
|
||||
rule's ``path == folder`` arm)."""
|
||||
rows = [
|
||||
("S", "a", "File A", 1, T0), # a file wearing the folder's name
|
||||
("S", "a/b.md", "B", 1, T0), # makes ``a`` a folder
|
||||
("S", "a", "File A", 1, T0, C0), # a file wearing the folder's name
|
||||
("S", "a/b.md", "B", 1, T0, C1), # makes ``a`` a folder
|
||||
]
|
||||
(source,) = build_kb_tree(["S"], rows, {})
|
||||
folders = _folder_nodes(source)
|
||||
@@ -127,12 +138,12 @@ def test_subfolder_path_order_and_file_catalog_order() -> None:
|
||||
the input (catalog — ``GET /api/docs``) order, independent of the
|
||||
subfolder ordering."""
|
||||
rows = [
|
||||
("S", "zeta/z1.md", "Z1", 1, T0),
|
||||
("S", "alpha/a1.md", "A1", 1, T0),
|
||||
("S", "mike/m1.md", "M1", 1, T0),
|
||||
("S", "beta/b1.md", "B1", 1, T0),
|
||||
("S", "z-file.md", "Z", 1, T0), # file AFTER the folders in input
|
||||
("S", "a-file.md", "A", 1, T0), # file before it in input
|
||||
("S", "zeta/z1.md", "Z1", 1, T0, C0),
|
||||
("S", "alpha/a1.md", "A1", 1, T0, C0),
|
||||
("S", "mike/m1.md", "M1", 1, T0, C0),
|
||||
("S", "beta/b1.md", "B1", 1, T0, C0),
|
||||
("S", "z-file.md", "Z", 1, T0, C0), # file AFTER the folders in input
|
||||
("S", "a-file.md", "A", 1, T0, C0), # file before it in input
|
||||
]
|
||||
(source,) = build_kb_tree(["S"], rows, {})
|
||||
assert [f.path for f in _folder_nodes(source)] == ["alpha", "beta", "mike", "zeta"]
|
||||
@@ -147,9 +158,9 @@ def test_summaries_present_and_absent() -> None:
|
||||
path) or null when absent — any row (AI or manual is indistinguishable
|
||||
here; the builder carries whatever is stored)."""
|
||||
rows = [
|
||||
("S", "one/a.md", "A", 1, T0),
|
||||
("S", "one/b.md", "B", 1, T0),
|
||||
("S", "two/c.md", "C", 1, T0),
|
||||
("S", "one/a.md", "A", 1, T0, C0),
|
||||
("S", "one/b.md", "B", 1, T0, C0),
|
||||
("S", "two/c.md", "C", 1, T0, C0),
|
||||
]
|
||||
summaries = {("S", ""): "Source desc.", ("S", "one"): "One desc."}
|
||||
# ("S", "two") is NOT stored → null.
|
||||
@@ -158,21 +169,28 @@ def test_summaries_present_and_absent() -> None:
|
||||
one, two = _folder_nodes(source)
|
||||
assert one.summary == "One desc."
|
||||
assert two.summary is None
|
||||
# File nodes carry no summary key at all (the 00_phase.md shape).
|
||||
# File nodes carry no summary key at all (the 00_phase.md shape);
|
||||
# since phase 106 they DO carry the creation date (``created_at``
|
||||
# — the RAG view's ``Created`` column).
|
||||
file = _file_nodes(one)[0]
|
||||
assert set(file.model_dump()) == {"kind", "path", "title", "chunks", "indexed_at"}
|
||||
assert set(file.model_dump()) == {
|
||||
"kind", "path", "title", "chunks", "created_at", "indexed_at"
|
||||
}
|
||||
assert "summary" not in file.__class__.model_fields
|
||||
|
||||
|
||||
def test_file_metadata_unchanged_in_tree() -> None:
|
||||
"""File ``title`` / ``chunks`` / ``indexed_at`` ride into the tree
|
||||
verbatim from the catalogue row (no reformatting)."""
|
||||
rows = [("S", "deep/x/y.md", "The Title", 7, T2)]
|
||||
"""File ``title`` / ``chunks`` / ``created_at`` (phase 106) /
|
||||
``indexed_at`` ride into the tree verbatim from the catalogue row
|
||||
(no reformatting) — and the two stamps stay distinct (the
|
||||
``created_at`` date is not confused with the ``indexed_at`` stamp)."""
|
||||
rows = [("S", "deep/x/y.md", "The Title", 7, T2, C2)]
|
||||
(source,) = build_kb_tree(["S"], rows, {})
|
||||
file = _file_nodes(_folder_nodes(_folder_nodes(source)[0])[0])[0]
|
||||
assert file.path == "deep/x/y.md"
|
||||
assert file.title == "The Title"
|
||||
assert file.chunks == 7
|
||||
assert file.created_at == C2 # phase 106: verbatim from the catalogue row
|
||||
assert file.indexed_at == T2
|
||||
|
||||
|
||||
@@ -188,10 +206,10 @@ def test_indexed_document_under_unlisted_source_is_impossible() -> None:
|
||||
is no input where a document is dropped."""
|
||||
names = ["reg-b", "reg-a"]
|
||||
rows: list[TreeDocRow] = [
|
||||
("reg-b", "b.md", "B", 1, T0),
|
||||
("zzz", "z.md", "Z", 1, T0),
|
||||
("aaa", "a.md", "A", 1, T0),
|
||||
("reg-a", "a.md", "A2", 1, T0),
|
||||
("reg-b", "b.md", "B", 1, T0, C0),
|
||||
("zzz", "z.md", "Z", 1, T0, C1),
|
||||
("aaa", "a.md", "A", 1, T0, C2),
|
||||
("reg-a", "a.md", "A2", 1, T0, C3),
|
||||
]
|
||||
tree = build_kb_tree(names, rows, {})
|
||||
listed = [s.name for s in tree]
|
||||
@@ -214,14 +232,14 @@ def test_indexed_document_under_unlisted_source_is_impossible() -> None:
|
||||
#: summary-stored folder and an unstored one. Paths are in (source,
|
||||
#: path) catalog order; titles map 1:1 to paths.
|
||||
CROSS_ROWS: list[TreeDocRow] = [
|
||||
("S", "note", "Note", 1, T0),
|
||||
("S", "one/a.md", "A", 2, T1),
|
||||
("S", "one/b.md", "B", 0, T1),
|
||||
("S", "one/two/c.md", "C", 3, T1),
|
||||
("S", "one/two/d.md", "D", 1, T1),
|
||||
("S", "root.md", "Root", 4, T0),
|
||||
("S", "zz/e.md", "E", 2, T2),
|
||||
("S", "zz/f.md", "F", 2, T2),
|
||||
("S", "note", "Note", 1, T0, C0),
|
||||
("S", "one/a.md", "A", 2, T1, C1),
|
||||
("S", "one/b.md", "B", 0, T1, C1),
|
||||
("S", "one/two/c.md", "C", 3, T1, C2),
|
||||
("S", "one/two/d.md", "D", 1, T1, C2),
|
||||
("S", "root.md", "Root", 4, T0, C3),
|
||||
("S", "zz/e.md", "E", 2, T2, C0),
|
||||
("S", "zz/f.md", "F", 2, T2, C0),
|
||||
]
|
||||
|
||||
CROSS_SUMMARIES = {
|
||||
@@ -236,9 +254,17 @@ def _cross_check(folder: str, builder_node) -> None:
|
||||
"""Assert the builder's level *folder* equals
|
||||
``group_folder_listing("S", folder, ...)`` — same subfolder
|
||||
``(path, count, summary)`` triples in order AND same file
|
||||
``(path, title)`` pairs in order (uncapped — the dataset is well
|
||||
under the ``ls`` 50-line cap, so the cap is inert)."""
|
||||
rows = [(path, title) for _source, path, title, _chunks, _stamp in CROSS_ROWS]
|
||||
``(source, path, title, date)`` 4-tuples in order (uncapped — the
|
||||
dataset is well under the ``ls`` 50-line cap, so the cap is inert).
|
||||
Phase 106 (task 06, D5): the cross-check compares the builder's
|
||||
OUTPUT node projections against the agent's extended file shape —
|
||||
the node's ``created_at`` DATE PART (the same ``YYYY-MM-DD`` the
|
||||
agent's ``ls`` line renders) joins the comparison.
|
||||
"""
|
||||
rows = [
|
||||
(path, title, created[:10])
|
||||
for _source, path, title, _chunks, _stamp, created in CROSS_ROWS
|
||||
]
|
||||
source_summaries = {
|
||||
folder_path: summary
|
||||
for (source, folder_path), summary in CROSS_SUMMARIES.items()
|
||||
@@ -246,8 +272,8 @@ def _cross_check(folder: str, builder_node) -> None:
|
||||
}
|
||||
subs, files, _total = group_folder_listing("S", folder, rows, source_summaries)
|
||||
assert [(f.path, f.documents, f.summary) for f in _folder_nodes(builder_node)] == subs
|
||||
assert [(f.path, f.title) for f in _file_nodes(builder_node)] == [
|
||||
(path, title) for _source, path, title in files
|
||||
assert [(f.path, f.title, f.created_at[:10]) for f in _file_nodes(builder_node)] == [
|
||||
(path, title, date) for _source, path, title, date in files
|
||||
]
|
||||
|
||||
|
||||
@@ -302,8 +328,8 @@ def test_folder_two_docs_no_stored_row_is_pending() -> None:
|
||||
(the marker's "waiting to generate" semantics); file nodes carry
|
||||
no flag at all (the file table has no description column)."""
|
||||
rows = [
|
||||
("S", "one/a.md", "A", 1, T0),
|
||||
("S", "one/b.md", "B", 1, T0),
|
||||
("S", "one/a.md", "A", 1, T0, C0),
|
||||
("S", "one/b.md", "B", 1, T0, C0),
|
||||
]
|
||||
(source,) = build_kb_tree(["S"], rows, {})
|
||||
(one,) = _folder_nodes(source)
|
||||
@@ -321,8 +347,8 @@ def test_folder_with_stored_row_is_not_pending() -> None:
|
||||
A row on the folder does not cover the source root: with no
|
||||
``(source, "")`` row the SOURCE node stays pending."""
|
||||
rows = [
|
||||
("S", "one/a.md", "A", 1, T0),
|
||||
("S", "one/b.md", "B", 1, T0),
|
||||
("S", "one/a.md", "A", 1, T0, C0),
|
||||
("S", "one/b.md", "B", 1, T0, C0),
|
||||
]
|
||||
(source,) = build_kb_tree(["S"], rows, {("S", "one"): "Manual."})
|
||||
(one,) = _folder_nodes(source)
|
||||
@@ -338,8 +364,8 @@ def test_single_document_folder_never_pending() -> None:
|
||||
— its one file line IS its description), even with no stored row
|
||||
— while its ≥ 2-doc source root (no root row) still is."""
|
||||
rows = [
|
||||
("S", "solo/only.md", "Only", 1, T0), # 1-doc folder
|
||||
("S", "top.md", "Top", 1, T0), # source total = 2
|
||||
("S", "solo/only.md", "Only", 1, T0, C0), # 1-doc folder
|
||||
("S", "top.md", "Top", 1, T0, C1), # source total = 2
|
||||
]
|
||||
(source,) = build_kb_tree(["S"], rows, {})
|
||||
(solo,) = _folder_nodes(source)
|
||||
@@ -357,8 +383,8 @@ def test_name_collision_pending_follows_recursive_count() -> None:
|
||||
the number of direct children. A stored row on the NESTED folder
|
||||
alone clears only that marker (the rule is per node)."""
|
||||
rows = [
|
||||
("S", "one/a", "File A", 1, T0), # a file wearing the folder's name
|
||||
("S", "one/a/b.md", "B", 1, T0), # makes ``one/a`` a folder
|
||||
("S", "one/a", "File A", 1, T0, C0), # a file wearing the folder's name
|
||||
("S", "one/a/b.md", "B", 1, T0, C1), # makes ``one/a`` a folder
|
||||
]
|
||||
(source,) = build_kb_tree(["S"], rows, {})
|
||||
one = _folder_nodes(source)[0]
|
||||
@@ -386,8 +412,8 @@ def test_source_root_pending_and_zero_document_source_never() -> None:
|
||||
A registered 0-document source is NEVER pending (0 < the minimum —
|
||||
there is nothing to summarize), with or without a manual row."""
|
||||
rows = [
|
||||
("Full", "x/1.md", "1", 1, T0),
|
||||
("Full", "y.md", "Y", 1, T0),
|
||||
("Full", "x/1.md", "1", 1, T0, C0),
|
||||
("Full", "y.md", "Y", 1, T0, C1),
|
||||
]
|
||||
full, empty = build_kb_tree(["Full", "Empty"], rows, {})
|
||||
assert full.documents == 2
|
||||
@@ -407,11 +433,115 @@ def test_two_sources_pending_independently() -> None:
|
||||
row and the other not, only the rowless source's node is pending —
|
||||
the markers never leak across sources."""
|
||||
rows = [
|
||||
("A", "a1.md", "A1", 1, T0),
|
||||
("A", "a2.md", "A2", 1, T0),
|
||||
("B", "b1.md", "B1", 1, T0),
|
||||
("B", "b2.md", "B2", 1, T0),
|
||||
("A", "a1.md", "A1", 1, T0, C0),
|
||||
("A", "a2.md", "A2", 1, T0, C1),
|
||||
("B", "b1.md", "B1", 1, T0, C2),
|
||||
("B", "b2.md", "B2", 1, T0, C3),
|
||||
]
|
||||
a, b = build_kb_tree(["A", "B"], rows, {("A", ""): "A root."})
|
||||
assert (a.summary, a.summary_pending) == ("A root.", False)
|
||||
assert (b.summary, b.summary_pending) == (None, True)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Document dates (phase 106, task 05, D8/D9) — the pure builder's date
|
||||
# threading: file ``created_at`` verbatim; folder/source ``updated_at``
|
||||
# = the subtree's MAX document ``created_at`` (derived as the builder
|
||||
# recurses, never stored; ``None`` for a node with no documents).
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_updated_at_deep_file_beats_shallow_sibling() -> None:
|
||||
"""D9: a folder's ``updated_at`` is the MAX over its WHOLE subtree —
|
||||
a deeper file's date wins over a shallower sibling's (the max
|
||||
recurses through every level, not just the direct files)."""
|
||||
rows = [
|
||||
("S", "root.md", "Root", 1, T0, C0),
|
||||
("S", "a/top.md", "Top", 1, T0, C1),
|
||||
("S", "a/b/shallow.md", "Shallow", 1, T0, C2),
|
||||
("S", "a/b/c/deep.md", "Deep", 1, T0, C3), # the overall max
|
||||
]
|
||||
(source,) = build_kb_tree(["S"], rows, {})
|
||||
a = _folder_nodes(source)[0]
|
||||
a_b = _folder_nodes(a)[0]
|
||||
a_b_c = _folder_nodes(a_b)[0]
|
||||
# The deepest folder: its one file's date.
|
||||
assert a_b_c.updated_at == C3
|
||||
# a/b: its own file (C2) vs its child's subtree max (C3) → C3.
|
||||
assert a_b.updated_at == C3
|
||||
# a: its direct file (C1) vs the deeper subtree (C3) → the DEEPER
|
||||
# file's date wins over the shallow sibling's.
|
||||
assert a.updated_at == C3
|
||||
# The source root: max over root.md (C0) + a's subtree (C3).
|
||||
assert source.updated_at == C3
|
||||
# File nodes carry their own date verbatim — no ``updated_at`` key.
|
||||
deep = _file_nodes(a_b_c)[0]
|
||||
assert deep.created_at == C3
|
||||
assert "updated_at" not in deep.model_dump()
|
||||
|
||||
|
||||
def test_updated_at_direct_file_wins_when_it_is_the_max() -> None:
|
||||
"""The inverse: when a folder's OWN direct file holds the newest
|
||||
date, the max stays at the direct level (the recursion takes the
|
||||
max, it does not prefer depth)."""
|
||||
rows = [
|
||||
("S", "root.md", "Root", 1, T0, C0),
|
||||
("S", "a/top.md", "Top", 1, T0, C3), # the overall max, DIRECT
|
||||
("S", "a/b/shallow.md", "Shallow", 1, T0, C1),
|
||||
("S", "a/b/c/deep.md", "Deep", 1, T0, C2),
|
||||
]
|
||||
(source,) = build_kb_tree(["S"], rows, {})
|
||||
a = _folder_nodes(source)[0]
|
||||
a_b = _folder_nodes(a)[0]
|
||||
a_b_c = _folder_nodes(a_b)[0]
|
||||
assert a_b_c.updated_at == C2 # its own file
|
||||
assert a_b.updated_at == C2 # max(C1, child C2)
|
||||
assert a.updated_at == C3 # the direct file (C3) beats the subtree (C2)
|
||||
assert source.updated_at == C3 # max(C0, a's C3)
|
||||
|
||||
|
||||
def test_updated_at_threads_through_folder_only_subtree() -> None:
|
||||
"""A folder with NO direct files (only subfolders) still carries the
|
||||
date threaded up from its child subfolders — the max is over the
|
||||
children (files AND folders), so a pure directory chain never loses
|
||||
the dates below it."""
|
||||
rows = [("S", "a/b/c/x.md", "X", 1, T0, C2)]
|
||||
(source,) = build_kb_tree(["S"], rows, {})
|
||||
a = _folder_nodes(source)[0] # no direct files — only subfolder a/b
|
||||
a_b = _folder_nodes(a)[0] # no direct files — only subfolder a/b/c
|
||||
a_b_c = _folder_nodes(a_b)[0]
|
||||
assert a_b_c.updated_at == C2
|
||||
assert a_b.updated_at == C2
|
||||
assert a.updated_at == C2
|
||||
assert source.updated_at == C2
|
||||
|
||||
|
||||
def test_updated_at_is_none_only_for_nodes_without_documents() -> None:
|
||||
"""``None`` is reserved for nodes with NO documents at all — a
|
||||
registered 0-document source; every node that has ≥ 1 document in
|
||||
its subtree carries a date (the 6th catalogue element is always
|
||||
present — ``created_at`` is NOT NULL, D1)."""
|
||||
full, empty = build_kb_tree(
|
||||
["Full", "Empty"],
|
||||
[("Full", "only.md", "Only", 1, T0, C1)],
|
||||
{},
|
||||
)
|
||||
assert full.documents == 1
|
||||
assert full.updated_at == C1
|
||||
assert empty.documents == 0
|
||||
assert empty.updated_at is None
|
||||
assert empty.children == []
|
||||
|
||||
|
||||
def test_updated_at_does_not_leak_across_sources() -> None:
|
||||
"""The max is per source subtree: one source's newest document never
|
||||
lifts another source's ``updated_at`` (the dates are computed
|
||||
inside :func:`build_kb_tree`'s per-source node, like ``documents``
|
||||
and ``summary_pending``)."""
|
||||
rows = [
|
||||
("A", "a1.md", "A1", 1, T0, C0),
|
||||
("B", "b1.md", "B1", 1, T0, C3), # B's date is the global max
|
||||
]
|
||||
a, b = build_kb_tree(["A", "B"], rows, {})
|
||||
assert a.updated_at == C0
|
||||
assert b.updated_at == C3
|
||||
|
||||
Reference in New Issue
Block a user