Files
brain-of-reese/tests/integration/test_agent_tools.py
T
ducoterra ee3efb28c9
Build and Push Containers / build-and-push-app (push) Successful in 4m35s
Build and Push Containers / build-and-push-db (push) Successful in 14s
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.
2026-09-13 19:28:05 -04:00

706 lines
26 KiB
Python

"""Integration: the agent DB accessors against real Postgres (phase 37;
the harness-aligned ``ls``/``read``/``grep`` surface, phase 70; the
drill-down tree ``ls``, phase 94).
``_source_document_rows`` must order a source's rows by ``path`` (the
file lines' catalog order), ``list_source_names`` must resolve the
registered source names (the registry join), and ``find_document``
must resolve a hit to the full document row (content included, for the
never-truncated read) and return ``None`` for unknown pairs. Phase 70:
the ``ls``/``read``/``grep`` tools are pinned here too — the locked
parameter shape in ``AGENT_TOOLS``, and scripted ``ToolCallPiece``s
executed through ``run_agent`` against the real DB. Phase 94: the
drill-down ``ls`` against the REAL tables — ``ls()`` lists the
registered sources (registry order, recursive counts, stored
source-root summaries from ``folder_summaries``), ``ls(source)`` /
``ls(source/folder)`` list one folder level (the SQL prefix logic:
subfolders = slash-boundary prefixes, counts = the recursive subtree,
file lines in catalog order, capped at 50 + the grep-pointer note),
and the refusals (unknown source segment → the no-source refusal; an
unknown folder → NOT-A_FOLDER with the parent's subfolders).
``read`` runs on the canonical combined ``source/path`` form
(first-slash split; a bare source name and an unknown identity get the
no-document refusal), and ``grep`` (``all_documents`` for a whole-KB
search, ``find_document`` for a scoped one) — both byte-identical
across the phase-94 change. Phase 106 (D5): the ``ls`` FILE line ends
with the appended `` | date: YYYY-MM-DD`` field and the ``read``
result carries the ``date: YYYY-MM-DD`` second line (first line
byte-identical) — the fixture documents carry a fixed ``created_at``
so the pins stay deterministic.
Requires: podman compose up -d db
"""
from __future__ import annotations
import asyncio
import uuid
from collections.abc import AsyncIterator, Iterator
from copy import deepcopy
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any, cast
import pytest
from sqlalchemy import delete, text
from sqlalchemy.orm import Session
from app.config import Settings
from app.models import Document, FolderSummary, GitSource
from app.rag import agent
from app.rag.agent import AGENT_TOOLS, AgentHolder, run_agent
from app.rag.llm import (
LLMClient,
RetryPiece,
StreamPiece,
ToolCallPiece,
ToolResultPiece,
)
if TYPE_CHECKING:
from app.rag.scaffolding import ScaffoldingFilter
#: The fixture documents' fixed creation date (phase 106, D5) — the
#: ``ls`` file line and the ``read`` second line format its UTC date
#: part; a fixed value keeps the pins deterministic.
_FIXTURE_CREATED_AT = datetime(2024, 6, 15, 12, 0, 0, tzinfo=UTC)
def _doc(db: Session, source: str, path: str, title: str, content: str) -> Document:
doc = Document(
id=uuid.uuid4(),
source=source,
path=path,
full_path=f"/tmp/{source}/{path}",
title=title,
content=content,
content_hash="0" * 64,
created_at=_FIXTURE_CREATED_AT,
)
db.add(doc)
return doc
@pytest.fixture()
def kb(db) -> Iterator[None]:
"""Fresh documents + folder_summaries tables (chunks first — the FK)
for these accessors (phase 94: the drill-down ``ls`` reads the
stored summaries too)."""
db.execute(text("TRUNCATE chunks, documents, folder_summaries"))
db.commit()
yield
db.execute(text("TRUNCATE chunks, documents, folder_summaries"))
db.commit()
@pytest.fixture()
def registry(db) -> Iterator[None]:
"""A FRESH two-source registry (phase 94): the drill-down ``ls``
top level IS the registry, so the table is truncated and re-seeded
around the tests in a controlled ``(added_at, id)`` order —
``Deployments`` before ``Homelab`` (the top-level listing order)."""
db.execute(text("TRUNCATE git_sources"))
db.commit()
db.add(GitSource(url="https://github.com/reese/Deployments.git", kind="git"))
db.commit()
db.add(GitSource(url="https://github.com/reese/Homelab.git", kind="git"))
db.commit()
yield
db.execute(text("TRUNCATE git_sources"))
db.commit()
@pytest.fixture()
def src(db) -> Iterator[GitSource]:
"""One registered git source — the scoped ``ls`` source-name check
reads the real registry, so the row is inserted and deleted around
the tests (``repo_name`` resolves the URL to ``Homelab``)."""
row = GitSource(url="https://github.com/reese/Homelab.git", kind="git")
db.add(row)
db.commit()
yield row
db.execute(delete(GitSource).where(GitSource.id == row.id))
db.commit()
def test_source_document_rows_order_by_path_within_the_source(kb, db) -> None:
"""Phase 94: the file lines' order — the source's rows in ``path``
order (the old ``list_catalog``'s per-source ordering, now the
``ls`` folder-level accessor's contract; a different source's rows
never leak in)."""
_doc(db, "Zeta", "b/second.md", "Zeta B", "ZB")
_doc(db, "Zeta", "a/first.md", "Zeta A", "ZA")
_doc(db, "Alpha", "c/third.md", "Alpha C", "AC")
db.commit()
assert agent._source_document_rows(db, "Zeta") == [
("a/first.md", "Zeta A", "2024-06-15"),
("b/second.md", "Zeta B", "2024-06-15"),
]
def test_source_document_rows_is_empty_without_rows(kb, db) -> None:
assert agent._source_document_rows(db, "Zeta") == []
def test_list_source_names_resolves_registry_rows(db) -> None:
"""The real registry: git names resolve through the import pipeline's
``repo_name`` (trailing ``.git`` stripped); a second row resolving to
the same name (the phase-69 sibling case) is listed once."""
a = GitSource(url="https://github.com/reese/Homelab.git", kind="git")
b = GitSource(url="https://github.com/reese/Homelab", kind="git") # sibling
c = GitSource(url="https://e.com/deployments", kind="git")
db.add_all([a, b, c])
db.commit()
try:
assert agent.list_source_names(db).count("Homelab") == 1 # deduped
assert "deployments" in agent.list_source_names(db)
finally:
db.execute(delete(GitSource).where(GitSource.id.in_([a.id, b.id, c.id])))
db.commit()
def test_find_document_hit_returns_full_row(kb, db) -> None:
created = _doc(db, "Alpha", "deep/nested/doc.md", "The Doc", "FULL-TEXT")
db.commit()
found = agent.find_document(db, "Alpha", "deep/nested/doc.md")
assert found is not None
assert found.id == created.id
assert found.source == "Alpha"
assert found.path == "deep/nested/doc.md"
assert found.title == "The Doc"
assert found.content == "FULL-TEXT" # the read tool feeds this, untruncated
def test_find_document_none_for_unknown_pairs(kb, db) -> None:
_doc(db, "Alpha", "x.md", "X", "X-CONTENT")
db.commit()
assert agent.find_document(db, "Alpha", "nope.md") is None # wrong path
assert agent.find_document(db, "Beta", "x.md") is None # wrong source
assert agent.find_document(db, "nope", "nope.md") is None # nothing at all
# ---------- AGENT_TOOLS surface (phase 70: ls / read / grep) ----------
def test_agent_tools_offers_the_harness_aligned_surface() -> None:
by_name = {t["function"]["name"]: t for t in AGENT_TOOLS}
assert list(by_name) == [ # the harness order, phase 70
"ls",
"read",
"grep",
]
ls = by_name["ls"]["function"]["parameters"]
assert ls["type"] == "object"
assert ls["required"] == [] # path is optional
assert set(ls["properties"]) == {"path"}
read = by_name["read"]["function"]["parameters"]
assert read["type"] == "object"
assert read["required"] == ["path"]
assert set(read["properties"]) == {"path"}
grep = by_name["grep"]["function"]["parameters"]
assert grep["type"] == "object"
assert grep["required"] == ["pattern"]
assert set(grep["properties"]) == {"pattern", "path"}
assert all(p["type"] == "string" for p in grep["properties"].values())
class ScriptedToolLLM:
"""One scripted tool-call stream, then one canned answer stream.
Records every ``chat_stream`` request's messages and tools."""
def __init__(self, call: ToolCallPiece) -> None:
self.call = call
self.requests: list[
tuple[list[dict[str, Any]], list[dict[str, Any]] | None]
] = []
async def chat_stream(
self,
messages: list[dict[str, str]],
tools: list[dict[str, Any]] | None = None,
scaffolding: ScaffoldingFilter | None = None, # phase 71 pass-through
) -> AsyncIterator[StreamPiece | ToolCallPiece]:
self.requests.append((deepcopy(messages), deepcopy(tools)))
if len(self.requests) == 1:
yield self.call
else:
yield StreamPiece("content", "ans")
def _settings(**kwargs: Any) -> Settings:
kwargs.setdefault("_env_file", None)
return Settings(**kwargs) # pyright: ignore[reportCallIssue]
class ScriptedToolCallsLLM:
"""N scripted tool-call rounds (one ``ToolCallPiece`` each), then one
canned answer; records every request's messages and tools (the
phase-72 task-02 two-round self-correction cases: the refusal round,
then the corrected call)."""
def __init__(self, calls: list[ToolCallPiece]) -> None:
self.calls = calls
self.requests: list[
tuple[list[dict[str, Any]], list[dict[str, Any]] | None]
] = []
async def chat_stream(
self,
messages: list[dict[str, str]],
tools: list[dict[str, Any]] | None = None,
scaffolding: ScaffoldingFilter | None = None,
) -> AsyncIterator[StreamPiece | ToolCallPiece]:
self.requests.append((deepcopy(messages), deepcopy(tools)))
index = len(self.requests) - 1
if index < len(self.calls):
yield self.calls[index]
else:
yield StreamPiece("content", "ans")
def _run_call(
db: Session, name: str, arguments: dict[str, Any]
) -> tuple[AgentHolder, ScriptedToolLLM]:
"""Drive one scripted tool call through ``run_agent``."""
holder = AgentHolder()
llm = ScriptedToolLLM(ToolCallPiece(id="call_1", name=name, arguments=arguments))
asyncio.run(_consume(cast("LLMClient", llm), db, holder))
return holder, llm
async def _consume(
llm: LLMClient, db: Session, holder: AgentHolder
) -> list[StreamPiece | ToolCallPiece | RetryPiece | ToolResultPiece]:
out: list[StreamPiece | ToolCallPiece | RetryPiece | ToolResultPiece] = []
async for piece in run_agent(
llm,
db,
system_prompt="SYSTEM_PROMPT",
user_message="QUESTION",
seed_docs=[],
settings=_settings(),
holder=holder,
):
out.append(piece)
return out
# ---------- ls (the drill-down tree, phase 94 — the real registry + DB) ----------
def test_ls_top_level_lists_registered_sources_through_run_agent(
kb, registry, db
) -> None:
"""No path: the TOP level against the real tables — registry order
(``(added_at, id)`` — Deployments before Homelab), recursive counts
(all of a source's documents), the stored source-root summary
(``folder_path = ''``) shown only when stored."""
_doc(db, "Deployments", "a/one.md", "A1", "A1-CONTENT")
_doc(db, "Homelab", "x.md", "X", "X-CONTENT")
_doc(db, "Homelab", "y/z.md", "Z", "Z-CONTENT")
db.add(FolderSummary(source="Homelab", folder_path="", summary="Home lab notes."))
db.commit()
holder, llm = _run_call(db, "ls", {})
assert llm.requests[0][1] == AGENT_TOOLS
assert llm.requests[1][0][3]["content"] == (
"2 sources:\n"
"\n"
"Deployments — 1 documents\n"
"Homelab — 2 documents\n"
" Home lab notes."
)
assert holder.tool_calls == 1
assert holder.read_docs == []
def test_ls_top_level_empty_registry_through_run_agent(
kb, db, monkeypatch: pytest.MonkeyPatch
) -> None:
"""No registered sources: the top level is the header line alone
(``0 sources:`` — the old ``0 documents:`` behavior preserved in
spirit), still counted. The env fallback (``BOR_GIT_SOURCES`` — the
operator's ``.env`` may name sources) is emptied for the test, so
the registry is genuinely empty."""
import app.rag.git_sources as git_sources_mod
db.execute(text("TRUNCATE git_sources"))
db.commit()
monkeypatch.setattr(
git_sources_mod,
"get_settings",
lambda: _settings(git_sources=""),
)
_doc(db, "Orphan", "a.md", "A", "A-CONTENT") # indexed but unregistered
db.commit()
holder, llm = _run_call(db, "ls", {})
assert llm.requests[1][0][3]["content"] == "0 sources:"
assert holder.tool_calls == 1
def test_ls_source_scope_lists_root_folder_through_run_agent(kb, registry, db) -> None:
"""A registered source name: the source's ROOT folder — the direct
subfolders (path order, recursive counts, stored summaries attached)
+ the root's own file lines in catalog order — against the real
tables; a registered source with no documents lists its header
line alone."""
_doc(db, "Homelab", "backups/cron.md", "Cron", "CRON")
_doc(db, "Homelab", "backups/restic.md", "Restic", "RESTIC")
_doc(db, "Homelab", "networking/lan.md", "LAN", "LAN")
_doc(db, "Homelab", "readme.md", "Readme", "README")
db.add(
FolderSummary(
source="Homelab", folder_path="backups", summary="Backup notes."
)
)
db.commit()
holder, llm = _run_call(db, "ls", {"path": "Homelab"})
assert llm.requests[1][0][3]["content"] == (
"Homelab — 1 documents, 2 folders:\n"
"\n"
" backups/ — 2 documents: Backup notes.\n"
" networking/ — 1 documents\n"
"\n"
"source: Homelab | path: readme.md | title: Readme | date: 2024-06-15"
)
assert holder.tool_calls == 1
assert holder.read_docs == []
# A registered source with no documents: the header line alone.
holder0, llm0 = _run_call(db, "ls", {"path": "Deployments"})
assert llm0.requests[1][0][3]["content"] == "Deployments — 0 documents, 0 folders:"
assert holder0.tool_calls == 1
def test_ls_nested_folder_scope_drills_one_level_through_run_agent(
kb, registry, db
) -> None:
"""A ``source/folder`` path: that folder's subfolders + own file
lines (identity = ``source/folder``) — the drill-down against the
real tables."""
_doc(db, "Homelab", "networking/lan/a.md", "A", "A")
_doc(db, "Homelab", "networking/lan/b.md", "B", "B")
_doc(db, "Homelab", "networking/vpn/c.md", "C", "C")
db.commit()
holder, llm = _run_call(db, "ls", {"path": "Homelab/networking"})
assert llm.requests[1][0][3]["content"] == (
"Homelab/networking — 0 documents, 2 folders:\n"
"\n"
" networking/lan/ — 2 documents\n"
" networking/vpn/ — 1 documents"
)
assert holder.tool_calls == 1
assert holder.read_docs == []
# One level deeper.
holder2, llm2 = _run_call(db, "ls", {"path": "Homelab/networking/lan"})
assert llm2.requests[1][0][3]["content"] == (
"Homelab/networking/lan — 2 documents, 0 folders:\n"
"\n"
"source: Homelab | path: networking/lan/a.md | title: A | date: 2024-06-15\n"
"source: Homelab | path: networking/lan/b.md | title: B | date: 2024-06-15"
)
assert holder2.tool_calls == 1
def test_ls_folder_file_cap_through_run_agent(kb, registry, db) -> None:
"""The cap end-to-end: 51 direct files in one folder cost 50 file
lines + the deterministic grep-pointer note, never 51."""
for i in range(51):
_doc(db, "Homelab", f"big/f{i:03d}.md", f"T{i}", "BODY")
db.commit()
holder, llm = _run_call(db, "ls", {"path": "Homelab/big"})
content = llm.requests[1][0][3]["content"]
lines = content.splitlines()
assert lines[0] == "Homelab/big — 51 documents, 0 folders:"
assert lines[2] == (
"source: Homelab | path: big/f000.md | title: T0 | date: 2024-06-15"
)
assert lines[51] == (
"source: Homelab | path: big/f049.md | title: T49 | date: 2024-06-15"
)
assert lines[52] == (
"…and 1 more documents in this folder — use grep (pattern) to "
"find a specific one."
)
assert len(lines) == 53
assert holder.tool_calls == 1
def test_ls_scoped_unknown_source_refused_through_run_agent(kb, src, db) -> None:
"""A ``path`` without ``/`` matching no source name is a refusal —
the extended line with the teaching parenthetical (phase 72, the
prefix byte-identical to the pre-phase-72 line); still not counted."""
_doc(db, "Homelab", "a.md", "A", "A-CONTENT")
db.commit()
holder, llm = _run_call(db, "ls", {"path": "Ghost"})
assert (
llm.requests[1][0][3]["content"]
== agent.NO_SOURCE_NOT_A_DIRECTORY.format(scope="Ghost")
)
assert holder.tool_calls == 0 and holder.read_docs == []
def test_ls_path_like_scope_unknown_source_gets_no_source_refusal(kb, src, db) -> None:
"""Phase 94: a ``/`` now names a folder — the phase-72 document-path
teaching is DELETED; a ``source/…`` argument whose FIRST segment
names no registered source gets the no-source refusal (the segment
echoed), not counted, the tools stay offered."""
_doc(db, "Homelab", "a.md", "A", "A-CONTENT")
db.commit()
holder, llm = _run_call(db, "ls", {"path": "app/rag/importer.py"})
assert (
llm.requests[1][0][3]["content"]
== agent.NO_SOURCE_NOT_A_DIRECTORY.format(scope="app")
)
assert holder.tool_calls == 0 and holder.read_docs == []
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
def test_ls_unknown_folder_gets_not_a_folder_with_parents_subfolders(
kb, src, db,
) -> None:
"""Phase 94: a folder segment matching no indexed prefix gets the
NOT-A_FOLDER teaching — the argument echoed, the source named, its
DIRECT subfolders listed (the self-correction list), not counted,
the tools stay offered."""
_doc(db, "Homelab", "backups/cron.md", "Cron", "CRON")
_doc(db, "Homelab", "containers/caddy.md", "Caddy", "CADDY")
_doc(db, "Homelab", "networking/lan.md", "LAN", "LAN")
_doc(db, "Homelab", "readme.md", "Readme", "README")
db.commit()
holder, llm = _run_call(db, "ls", {"path": "Homelab/netwoking"})
assert llm.requests[1][0][3]["content"] == (
"'Homelab/netwoking' is not a folder — Homelab has: "
"backups/ containers/ networking/"
)
assert holder.tool_calls == 0 and holder.read_docs == []
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
# ---------- read (the canonical combined source/path form) ----------
def test_read_combined_path_through_run_agent(kb, db) -> None:
"""The combined ``source/path`` identity resolves at the FIRST slash
against the REAL table (a path with further slashes included): the
read executes, the holder records the row, the result header carries
the true source/path."""
created = _doc(db, "Alpha", "deep/nested/doc.md", "The Doc", "FULL-TEXT")
db.commit()
holder, llm = _run_call(db, "read", {"path": "Alpha/deep/nested/doc.md"})
# Phase 106 (D5): the date rides every read — the SECOND line (the
# first line stays the byte-identical header).
assert llm.requests[1][0][3]["content"] == (
"Document Alpha/deep/nested/doc.md:\n"
"date: 2024-06-15\n"
"FULL-TEXT"
)
assert holder.tool_calls == 1
assert holder.read_docs == [created]
def test_read_bare_source_name_refused_through_run_agent(kb, db) -> None:
"""A bare source name (no '/') can never be a document — the
no-document refusal echoing the argument as passed; the old
split-teaching refusal is gone (phase 70)."""
_doc(db, "Alpha", "deep/nested/doc.md", "The Doc", "FULL-TEXT")
db.commit()
holder, llm = _run_call(db, "read", {"path": "Alpha"})
assert (
llm.requests[1][0][3]["content"] == "No document at 'Alpha' — check the ls output."
)
assert holder.tool_calls == 0 and holder.read_docs == []
def test_read_unknown_combined_path_refused_through_run_agent(kb, db) -> None:
"""A combined identity that matches NOTHING — not a document and not
any indexed document's ``path`` (zero candidates) — gets today's
no-document refusal byte-identical (the argument echoed as passed —
the model sees its own form)."""
_doc(db, "Alpha", "x.md", "X", "X-CONTENT")
db.commit()
holder, llm = _run_call(db, "read", {"path": "Alpha/nope/deep.md"})
assert (
llm.requests[1][0][3]["content"]
== "No document at 'Alpha/nope/deep.md' — check the ls output."
)
assert holder.tool_calls == 0 and holder.read_docs == []
def test_read_bare_path_single_source_suggestion_then_corrected_read(kb, db) -> None:
"""Phase 72, task 02: a bare path under ONE source (exact ``path``
match, the source prefix missing) → the single-identity suggestion
(a refusal — not counted); the scripted corrected call (round 2, the
suggested combined identity) then succeeds against real Postgres —
the two-round self-correction."""
created = _doc(db, "Alpha", "deep/nested/doc.md", "The Doc", "FULL-TEXT")
db.commit()
llm = ScriptedToolCallsLLM(
[
ToolCallPiece(
id="call_1", name="read", arguments={"path": "deep/nested/doc.md"}
),
ToolCallPiece(
id="call_2",
name="read",
arguments={"path": "Alpha/deep/nested/doc.md"},
),
]
)
holder = AgentHolder()
asyncio.run(_consume(cast("LLMClient", llm), db, holder))
# Round 1: the bare path resolves to no combined identity, but it IS
# the indexed document's path — the refusal names the one combined
# identity to use (not counted, the tools stay offered).
assert llm.requests[1][0][3]["content"] == (
"No document at 'deep/nested/doc.md' — "
"did you mean 'Alpha/deep/nested/doc.md'?"
)
assert llm.requests[1][1] == AGENT_TOOLS
# Round 2: the corrected combined identity succeeds — the full
# content (plus the phase-106 D5 date line), the holder records the
# row, and it counts.
assert llm.requests[2][0][5]["content"] == (
"Document Alpha/deep/nested/doc.md:\n"
"date: 2024-06-15\n"
"FULL-TEXT"
)
assert llm.requests[2][1] == AGENT_TOOLS
assert holder.read_docs == [created]
assert holder.tool_calls == 1 # only the corrected read executed
def test_read_bare_path_two_sources_one_of_suggestion_then_corrected_read(
kb, db,
) -> None:
"""Phase 72, task 02: the same bare path under TWO sources → the
``one of`` line (up to ``SUGGESTION_LIMIT`` identities, catalog
order — Alpha before Beta); the scripted corrected call (round 2,
the first suggested identity) then succeeds."""
a = _doc(db, "Alpha", "shared/x.md", "Alpha X", "A-TEXT")
_doc(db, "Beta", "shared/x.md", "Beta X", "B-TEXT")
db.commit()
llm = ScriptedToolCallsLLM(
[
ToolCallPiece(id="call_1", name="read", arguments={"path": "shared/x.md"}),
ToolCallPiece(
id="call_2", name="read", arguments={"path": "Alpha/shared/x.md"}
),
]
)
holder = AgentHolder()
asyncio.run(_consume(cast("LLMClient", llm), db, holder))
assert llm.requests[1][0][3]["content"] == (
"No document at 'shared/x.md' — did you mean one of: "
"'Alpha/shared/x.md', 'Beta/shared/x.md'?"
)
assert llm.requests[2][0][5]["content"] == (
"Document Alpha/shared/x.md:\ndate: 2024-06-15\nA-TEXT"
)
assert holder.read_docs == [a]
assert holder.tool_calls == 1 # only the corrected read executed
# ---------- grep (the phase-68 A5 contract under the new name) ----------
def test_all_documents_orders_by_source_then_path(kb, db) -> None:
_doc(db, "Zeta", "b/second.md", "Zeta B", "ZB")
_doc(db, "Zeta", "a/first.md", "Zeta A", "ZA")
_doc(db, "Alpha", "c/third.md", "Alpha C", "AC")
db.commit()
docs = agent.all_documents(db)
assert [(d.source, d.path) for d in docs] == [
("Alpha", "c/third.md"),
("Zeta", "a/first.md"),
("Zeta", "b/second.md"),
]
assert [d.content for d in docs] == ["AC", "ZA", "ZB"] # full rows
def test_grep_whole_kb_through_run_agent(kb, db) -> None:
_doc(db, "Beta", "b/two.md", "Two", "no hit\nNEEDLE in two\nlast")
_doc(db, "Alpha", "a/one.md", "One", "first\nneedle in one\nthird")
db.commit()
holder, llm = _run_call(db, "grep", {"pattern": "needle"})
# Offered: the first request carries AGENT_TOOLS (the 3-tool list).
assert llm.requests[0][1] == AGENT_TOOLS
# Executed against the real DB: catalog order, grep-style lines.
assert llm.requests[1][0][3]["content"] == (
"Alpha/a/one.md:2: needle in one\n"
"Beta/b/two.md:2: NEEDLE in two"
)
assert holder.tool_calls == 1
assert holder.read_docs == [] # locked A5: grep adds no context
def test_grep_scoped_through_run_agent(kb, db) -> None:
_doc(db, "Alpha", "a/one.md", "One", "first\nNeedle here\nthird")
_doc(db, "Beta", "b/two.md", "Two", "NEEDLE too")
db.commit()
holder, llm = _run_call(db, "grep", {"pattern": "needle", "path": "Alpha/a/one.md"})
# Only the named document is searched — the other one's hit is absent.
assert llm.requests[1][0][3]["content"] == "Alpha/a/one.md:2: Needle here"
assert holder.tool_calls == 1
assert holder.read_docs == []
def test_grep_scoped_missing_doc_refused_through_run_agent(kb, db) -> None:
"""A scoped ``grep`` miss that matches no indexed document's ``path``
(zero candidates) keeps today's line byte-identical — a refusal,
not counted."""
_doc(db, "Alpha", "a/one.md", "One", "nothing")
db.commit()
holder, llm = _run_call(db, "grep", {"pattern": "needle", "path": "Alpha/ghost.md"})
assert (
llm.requests[1][0][3]["content"]
== "No document at 'Alpha/ghost.md' — check the ls output."
)
assert holder.tool_calls == 0 and holder.read_docs == []
def test_grep_no_matches_through_run_agent(kb, db) -> None:
_doc(db, "Alpha", "a/one.md", "One", "nothing matching")
db.commit()
holder, llm = _run_call(db, "grep", {"pattern": "zebra"})
assert llm.requests[1][0][3]["content"] == (
"No matches for 'zebra' in the knowledge base."
)
assert holder.tool_calls == 1 # an executed grep with zero hits
assert holder.read_docs == []