"""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 == []