"""Integration: the phase-106 D5 date surfaces against REAL Postgres rows (task 06). The two tool surfaces the model reads carry the document's creation date: the ``read`` result's SECOND line (``date: YYYY-MM-DD`` — the FIRST line stays the byte-identical ``Document {source}/{path}:`` header the E2E mock's ``_READ_RESULT_PREFIX`` contract keys on) and the ``ls`` FILE line's APPENDED `` | date: YYYY-MM-DD`` field (the mock's ``_CATALOG_LINE_RE`` ``title: .+$`` tail absorbs it). The rows carry DISTINCT fixed ``created_at`` values, so the pins prove the date is the ROW's date (per row), not a constant. 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.db import SessionLocal from app.models import Document, GitSource from app.rag.agent import AgentHolder, run_agent from app.rag.llm import LLMClient, RetryPiece, StreamPiece, ToolCallPiece, ToolResultPiece if TYPE_CHECKING: from app.rag.scaffolding import ScaffoldingFilter #: DISTINCT fixed creation dates — the per-row date pins (task 06): #: each document renders ITS OWN row's UTC date part. D1 = datetime(2019, 6, 15, 3, 4, 6, tzinfo=UTC) # "2019-06-15" D2 = datetime(2020, 1, 2, 5, 0, 0, tzinfo=UTC) # "2020-01-02" D3 = datetime(2024, 6, 15, 23, 59, 59, tzinfo=UTC) # "2024-06-15" (late UTC instant) D1_STR, D2_STR, D3_STR = "2019-06-15", "2020-01-02", "2024-06-15" def _doc( db: Session, source: str, path: str, title: str, content: str, created_at: datetime, ) -> 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=created_at, # D1: explicit — the pins prove per-row dates ) db.add(doc) return doc @pytest.fixture() def kb(db) -> Iterator[None]: """Fresh documents table (chunks first — the FK).""" db.execute(text("TRUNCATE chunks, documents")) db.commit() yield db.execute(text("TRUNCATE chunks, documents")) db.commit() @pytest.fixture() def src(db) -> Iterator[GitSource]: """One registered git source — the scoped ``ls`` source-name check reads the real registry (``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() 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, ) -> 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] 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]: """SEC-14-04: uses a short-lived session per tool call.""" out: list[StreamPiece | ToolCallPiece | RetryPiece | ToolResultPiece] = [] async for piece in run_agent( llm, lambda: SessionLocal(), # SEC-14-04: session factory (short-lived sessions) system_prompt="SYSTEM_PROMPT", user_message="QUESTION", seed_docs=[], settings=_settings(), holder=holder, ): out.append(piece) return out # -------------------------------------------------------------------- # read — the date is the stored row's date, on the SECOND line # -------------------------------------------------------------------- def test_read_result_second_line_is_stored_date(kb, db) -> None: """A real row (distinct ``created_at``): the ``read`` result's SECOND line is the stored date (the UTC date part), the FIRST line stays the byte-identical header, and the content follows whole.""" created = _doc( db, "Alpha", "deep/nested/doc.md", "The Doc", "FULL-TEXT", D2 ) db.commit() holder, llm = _run_call(db, "read", {"path": "Alpha/deep/nested/doc.md"}) content = llm.requests[1][0][3]["content"] lines = content.splitlines() assert lines[0] == "Document Alpha/deep/nested/doc.md:" # byte-identical header assert lines[1] == f"date: {D2_STR}" # the STORED date (row's UTC date part) assert lines[2:] == ["FULL-TEXT"] # SEC-14-04: short-lived session loads fresh copies assert [d.id for d in holder.read_docs] == [created.id] assert holder.tool_calls == 1 def test_read_result_date_is_the_row_date_not_a_constant(kb, db) -> None: """Two rows with DISTINCT dates: each ``read`` renders its OWN row's date (a late-UTC instant renders its date part, no time).""" a = _doc(db, "Alpha", "a.md", "A", "A-TEXT", D1) b = _doc(db, "Alpha", "b.md", "B", "B-TEXT", D3) db.commit() holder_a, llm_a = _run_call(db, "read", {"path": "Alpha/a.md"}) assert llm_a.requests[1][0][3]["content"] == ( f"Document Alpha/a.md:\ndate: {D1_STR}\nA-TEXT" ) holder_b, llm_b = _run_call(db, "read", {"path": "Alpha/b.md"}) assert llm_b.requests[1][0][3]["content"] == ( f"Document Alpha/b.md:\ndate: {D3_STR}\nB-TEXT" ) # SEC-14-04: short-lived sessions assert [d.id for d in holder_a.read_docs] == [a.id] assert [d.id for d in holder_b.read_docs] == [b.id] # -------------------------------------------------------------------- # ls — every FILE line carries its date in the appended field # -------------------------------------------------------------------- def test_ls_drill_file_lines_carry_their_dates(kb, src, db) -> None: """A source drill against real rows: EVERY file line ends with the appended `` | date: YYYY-MM-DD`` field — each row's OWN stored date — while the header and subfolder lines stay date-free.""" _doc(db, "Homelab", "backups/cron.md", "Cron", "CRON", D1) _doc(db, "Homelab", "backups/restic.md", "Restic", "RESTIC", D2) _doc(db, "Homelab", "networking/lan.md", "LAN", "LAN", D3) _doc(db, "Homelab", "readme.md", "Readme", "README", D1) db.commit() holder, llm = _run_call(db, "ls", {"path": "Homelab"}) content = llm.requests[1][0][3]["content"] lines = content.splitlines() # The root level: one direct file (readme.md — D1) + the two # subfolder lines (date-free) + the date-free header. assert lines[0] == "Homelab — 1 documents, 2 folders:" assert lines[2] == " backups/ — 2 documents" # subfolder: no date assert lines[3] == " networking/ — 1 documents" # subfolder: no date assert lines[5] == ( f"source: Homelab | path: readme.md | title: Readme | date: {D1_STR}" ) assert holder.tool_calls == 1 # Drill into backups: BOTH files list, each with its OWN date. holder2, llm2 = _run_call(db, "ls", {"path": "Homelab/backups"}) lines2 = llm2.requests[1][0][3]["content"].splitlines() assert lines2[0] == "Homelab/backups — 2 documents, 0 folders:" assert lines2[2] == ( f"source: Homelab | path: backups/cron.md | title: Cron | date: {D1_STR}" ) assert lines2[3] == ( f"source: Homelab | path: backups/restic.md | title: Restic | date: {D2_STR}" ) assert holder2.tool_calls == 1 def test_ls_top_level_source_lines_carry_no_date(kb, db) -> None: """The top level (source lines) is UNCHANGED in shape — sources are not documents, so no date rides them (only FILE lines do). The registry is FRESH (truncated + the one source re-registered), so the top level is exactly the one source block.""" db.execute(text("TRUNCATE git_sources")) db.commit() db.add(GitSource(url="https://github.com/reese/Homelab.git", kind="git")) db.commit() try: _doc(db, "Homelab", "a.md", "A", "A-TEXT", D1) db.commit() holder, llm = _run_call(db, "ls", {}) content = llm.requests[1][0][3]["content"] assert content == "1 sources:\n\nHomelab — 1 documents" assert "date" not in content assert holder.tool_calls == 1 finally: db.execute(text("TRUNCATE git_sources")) db.commit()