Files
brain-of-reese/tests/unit/test_prompts_dates.py
T
ducoterra 9820c361b0
Build and Push Containers / build-and-push-app (push) Successful in 2m2s
Build and Push Containers / build-and-push-db (push) Successful in 14s
phase: 118_summary_seed_context
**Phase 118 final verification pass — complete.** All criteria verified; 4 pre-existing defects found and fixed.

- **Verified:** summary-seed wiring (`select_suggested` top-5 no-floor → summary blocks, no full text in HIGH prompt), all-doc markdown summaries + NULL backfill (`summary_backfilled`, no `sources_meta` bump), `read` adds full text with `read_docs`-only dedupe, `done.sources` = suggested+read / durable record = suggested+related+read + `suggested=N` log line (seen live in E2E), byte-locked PERSONA/LOW/TOOLS_SECTION, battery gate PASS recorded in `TOOL_CALLING_TESTING.md` §10 (turbo 2026-09-16: 1/2/4 GREEN, cond-3 reported 9/10 per A7, contract 21/21, caps 0).
- **Defects fixed (all pre-existing, none phase-118):** ① `ChatMessage` schema missing the phase-113 `related` key → `extra="forbid"` 422'd every done-time auto-save of grounded turns with a related tier, leaving `message_count=1` (root cause of `test_share_chat` 3F; browser-level instrumentation proved the PUT 422) — added the field + unit/integration pins; ② `test_theme_semantic_completion` pins stale vs phase-117 debox (border/chip removed) — re-targeted to assert border/chip *absence*; ③ `test_header_consistency` `<26`px pin red on 26.125px native date-input line — bound relaxed to `<34` (wrap-detection intent kept); ④ `test_navbar_refresh` bor.chat.v1 key set updated for `related`.
- **Test/lint/coverage:** `uv run pytest --cov=app --cov-report=term-missing` → **2506 passed, app/ 99%** (>90%); `uv run ruff check . && uv run pyright` → clean, 0 errors.
- **E2E:** new story suite in isolation → **2 passed**; full 103-suite matrix sweep (each isolated) → **all 103 green** after the fixes; `test_share_chat` 4 passed, `test_theme_semantic_completion` 8 passed, `test_header_consistency` 3 passed, `test_navbar_refresh` 7 passed.
- **Deviations:** none from LOCKED decisions. Note: orphaned diagnostic uvicorn processes briefly made E2E sessions exercise stale code — killed and re-verified; a sweep-regenerated tracked screenshot was restored. No commits made (harness commits).
- **Completion criteria:** all 7 ✅ (commit/phase-move is the harness's step).
- **Next pending phase:** none — `todo/` holds only this phase's overview pending the harness move.
2026-09-16 06:57:49 -04:00

376 lines
15 KiB
Python

"""Unit: the phase-106 D5 date surfaces (task 06) — the date rides every
document the model sees.
Three surfaces, pinned here against pure inputs (no database):
* the HIGH prompt's ``<document>`` block carries
``date="YYYY-MM-DD"`` — the row's ``created_at`` UTC date part,
APPENDED after ``title`` (the only position; always present,
``created_at`` is NOT NULL) — while the DEFLECTION prompt stays
byte-identical to the pre-phase text (the A8 contract — titles only,
no documents, no dates);
* the ``read`` tool result carries ``date: YYYY-MM-DD`` as its SECOND
line — the FIRST line stays ``Document {source}/{path}:``
BYTE-IDENTICAL (the E2E mock's ``_READ_RESULT_PREFIX`` header
contract) — in BOTH the plain and the truncated shapes (marker +
notice still follow the cut content);
* the ``ls`` FILE line ENDS with the appended `` | date: YYYY-MM-DD``
field (never inserted before ``title``) and the 50-line cap note is
unchanged; source/folder lines carry no date.
"""
from __future__ import annotations
import asyncio
import uuid
from collections.abc import AsyncIterator
from datetime import UTC, datetime
from typing import Any, cast
from unittest.mock import MagicMock
import pytest
from sqlalchemy.orm import Session
from app.config import Settings
from app.models import Document
from app.rag import agent
from app.rag.agent import READ_TRUNCATION_NOTICE, AgentHolder, run_agent
from app.rag.llm import (
LLMClient,
StreamPiece,
ToolCallPiece,
)
from app.rag.prompts import (
_base,
build_deflect_prompt,
build_high_prompt,
build_steering_section,
)
from app.rag.retriever import TRUNCATION_MARKER
#: A fixed creation date; the UTC date part is ``2024-06-15``.
CREATED_AT = datetime(2024, 6, 15, 12, 30, 45, tzinfo=UTC)
DATE = "2024-06-15"
def _doc(
source: str = "S",
path: str = "P",
title: str = "T",
content: str = "CONTENT",
created_at: datetime = CREATED_AT,
summary: str | None = None,
) -> Document:
return Document(
id=uuid.uuid4(),
source=source,
path=path,
full_path=f"/tmp/{path}",
title=title,
content=content,
content_hash="0" * 64,
created_at=created_at,
summary=summary,
)
def _settings(**kwargs: Any) -> Settings:
kwargs.setdefault("_env_file", None)
return Settings(**kwargs) # pyright: ignore[reportCallIssue]
class ScriptedLLM:
"""Canned stream sequences; records every ``chat_stream`` request."""
def __init__(self, *streams: list[StreamPiece | ToolCallPiece]) -> None:
self.streams: list[list[StreamPiece | ToolCallPiece]] = list(streams)
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: object = None,
) -> AsyncIterator[StreamPiece | ToolCallPiece]:
self.requests.append((messages, tools))
if not self.streams:
return
for piece in self.streams.pop(0):
yield piece
async def _run(
llm: ScriptedLLM,
holder: AgentHolder,
settings: Settings,
) -> None:
async for _piece in run_agent(
cast("LLMClient", llm),
lambda: cast("Session", MagicMock()), # SEC-14-04: session factory
system_prompt="SYSTEM_PROMPT",
user_message="QUESTION",
seed_docs=[],
settings=settings,
holder=holder,
):
pass
# --------------------------------------------------------------------
# The HIGH prompt's <document> block — date="YYYY-MM-DD" after title
# --------------------------------------------------------------------
def test_high_block_carries_date_attribute_after_title() -> None:
"""The block's identity attributes are ``source`` / ``path`` /
``title`` — and, phase 106 (D5), ``date`` APPENDED after ``title``
(the only position), always present."""
doc = _doc(source="S", path="P", title="T", content="THE BODY")
prompt = build_high_prompt([doc])
block = (
f'<document source="S" path="P" title="T" date="{DATE}">\n'
"THE BODY\n"
"</document>"
)
assert block in prompt
# The attribute order is pinned: date directly after title, then
# the closing bracket (nothing may land between title and date or
# after date before the block opens).
assert f'title="T" date="{DATE}">' in prompt
assert '<document source="S" path="P" title="T">\n' not in prompt # pre-phase shape is gone
def test_high_block_date_is_the_utc_date_part() -> None:
"""The date is the row's ``created_at`` UTC DATE part: a late UTC
instant (23:59:59 on 2024-06-15) renders ``2024-06-15`` — and the
time-of-day is never shown (full precision is stored; only the
date part rides the block). Stored rows are always UTC-aware
(``timestamptz`` read-back + :func:`normalize_doc_date`), so the
UTC date part is the stored date part."""
doc = _doc(created_at=datetime(2024, 6, 15, 23, 59, 59, tzinfo=UTC))
prompt = build_high_prompt([doc])
assert f'title="T" date="{DATE}">' in prompt
assert "2024-06-16" not in prompt
# The time-of-day is never rendered on the block.
assert "23:59" not in prompt and "12:30" not in prompt
def test_high_block_date_always_present_for_every_document() -> None:
"""Every document block carries the date (``created_at`` is NOT
NULL — the attribute is never omitted), each with its OWN row's
date."""
a = _doc(source="A", path="a.md", title="A", content="CA", created_at=CREATED_AT)
b = _doc(
source="B",
path="b.md",
title="B",
content="CB",
created_at=datetime(2019, 6, 15, 5, 0, 0, tzinfo=UTC),
)
prompt = build_high_prompt([a, b])
assert '<document source="A" path="a.md" title="A" date="2024-06-15">' in prompt
assert '<document source="B" path="b.md" title="B" date="2019-06-15">' in prompt
# No date-less block anywhere.
assert prompt.count("<document ") == prompt.count(' date="')
def test_high_block_date_survives_the_summary_body_change() -> None:
"""Phase 118 (task 03): the block BODY became the document's summary
(never the full content) — the D5 identity attributes, including
``date`` after ``title``, survive byte-identical around the new
body (the E2E mock's block parser keys off exactly these)."""
doc = _doc(
source="S",
path="P",
title="T",
content="FULL_CONTENT_SENTINEL_42",
created_at=CREATED_AT,
summary="The stored summary.",
)
prompt = build_high_prompt([doc])
block = (
f'<document source="S" path="P" title="T" date="{DATE}">\n'
"The stored summary.\n"
"</document>"
)
assert block in prompt
# Attribute order pinned: date directly after title, body after.
assert f'title="T" date="{DATE}">' in prompt
# The full content stays out (A6) — only the summary rides the block.
assert "FULL_CONTENT_SENTINEL_42" not in prompt
# --------------------------------------------------------------------
# The deflection prompt — byte-identical to the pre-phase text (A8)
# --------------------------------------------------------------------
def test_deflect_prompt_is_byte_identical_to_pre_phase() -> None:
"""The deflection path is titles-only (no documents involved): the
A8 byte-identity contract holds — same inputs, pre-phase bytes,
with or without steering notes."""
# No notes: the pre-phase LOW build verbatim.
expected_plain = (
_base("LOW")
+ "\n"
+ "DEFLECT_MODE: retrieval was weak — the titles below are the closest "
"your notes come to the question. They are titles only; do not pretend "
"they answer it. Use them to propose 2-3 alternative questions.\n"
+ "Reply in plain text only — you have no tools in this mode.\n"
+ "- T1\n- T2"
)
assert build_deflect_prompt(["T1", "T2"]) == expected_plain
# With steering notes: the pre-phase text + the <tuning> section
# (phase 15) — still no date anywhere.
expected_steered = (
_base("LOW")
+ "\n"
+ build_steering_section(["be concise"])
+ "\n"
+ "DEFLECT_MODE: retrieval was weak — the titles below are the closest "
"your notes come to the question. They are titles only; do not pretend "
"they answer it. Use them to propose 2-3 alternative questions.\n"
+ "Reply in plain text only — you have no tools in this mode.\n"
+ "- T1\n- T2"
)
assert build_deflect_prompt(["T1", "T2"], notes=["be concise"]) == expected_steered
for prompt in (expected_plain, expected_steered):
assert "<document" not in prompt
assert 'date="' not in prompt
# --------------------------------------------------------------------
# The read result — the date is the SECOND line (first line
# byte-identical — the mock's _READ_RESULT_PREFIX header contract)
# --------------------------------------------------------------------
def test_read_result_plain_carries_date_second_line(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""At or under the cap: ``Document {source}/{path}:`` (byte-
identical FIRST line) + ``date: YYYY-MM-DD`` (the D5 SECOND line)
+ the full content — nothing else."""
doc = _doc(source="S", path="a.md", title="A", content="A-CONTENT")
monkeypatch.setattr(agent, "find_document", lambda db, source, path: doc)
holder = AgentHolder()
llm = ScriptedLLM(
[ToolCallPiece(id="call_1", name="read", arguments={"path": "S/a.md"})],
[StreamPiece("content", "ans")],
)
asyncio.run(_run(llm, holder, _settings()))
assert holder.read_docs == [doc] and holder.tool_calls == 1
content = llm.requests[1][0][3]["content"]
lines = content.splitlines()
assert lines[0] == "Document S/a.md:" # byte-identical first line
assert lines[1] == f"date: {DATE}" # the D5 second line
assert lines[2:] == ["A-CONTENT"]
def test_read_result_truncated_carries_date_then_marker_and_notice(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Over the cap: the SAME two header lines, then the first ``cap``
chars, the shared marker, and the pinned notice — the truncation
contract (phase 95) is unchanged by the date line."""
cap = 10
content_body = "x" * 25
doc = _doc(source="S", path="big.md", title="Big", content=content_body)
monkeypatch.setattr(agent, "find_document", lambda db, source, path: doc)
holder = AgentHolder()
llm = ScriptedLLM(
[ToolCallPiece(id="call_1", name="read", arguments={"path": "S/big.md"})],
[StreamPiece("content", "ans")],
)
asyncio.run(_run(llm, holder, _settings(read_max_chars=cap)))
expected = (
"Document S/big.md:\n"
f"date: {DATE}\n"
+ content_body[:cap]
+ "\n"
+ TRUNCATION_MARKER
+ "\n"
+ READ_TRUNCATION_NOTICE.format(shown=cap, total=len(content_body))
)
assert llm.requests[1][0][3]["content"] == expected
# The truncation still records on the holder (the ToolResultPiece
# signal) — the date line adds nothing to the counters.
assert holder.read_truncations == [("S/big.md", cap, len(content_body))]
assert holder.tool_calls == 1
# --------------------------------------------------------------------
# The ls FILE line — the appended " | date: YYYY-MM-DD" field
# --------------------------------------------------------------------
def test_ls_file_line_ends_with_appended_date() -> None:
"""The ``ls`` FILE line is ``source: X | path: Y | title: Z`` with
the phase-106 D5 field APPENDED at the END (never inserted before
``title`` — the mock's non-greedy ``path`` capture would swallow an
inserted field)."""
rows = [
("a.md", "A", "2019-06-15"),
("b.md", "B", "2024-06-15"),
]
subfolders, files, total = agent.group_folder_listing("S", "", rows, {})
rendered = agent.render_folder_listing("S", subfolders, files, total)
assert total == 2
lines = rendered.splitlines()
assert lines[0] == "S — 2 documents, 0 folders:"
# The file lines end with the appended date field — per-row dates.
assert lines[2] == "source: S | path: a.md | title: A | date: 2019-06-15"
assert lines[3] == "source: S | path: b.md | title: B | date: 2024-06-15"
# The date field is the LAST field (appended, never inserted before
# ``title`` — where the mock's non-greedy path capture would
# swallow it).
for line in (lines[2], lines[3]):
assert line.index("date:") > line.index("title:")
assert line.rsplit(" | ", 1)[-1].startswith("date: ") # date is final
def test_ls_folder_and_source_lines_carry_no_date() -> None:
"""Only FILE lines are documents: the folder header, the indented
subfolder lines, and the top-level source lines carry NO date."""
rows = [
("a/x.md", "X", "2019-06-15"),
("a/y.md", "Y", "2020-01-02"),
("top.md", "Top", "2024-06-15"),
]
subfolders, files, total = agent.group_folder_listing("S", "", rows, {"a": "A stuff."})
rendered = agent.render_folder_listing("S", subfolders, files, total)
lines = rendered.splitlines()
assert lines[0] == "S — 1 documents, 1 folders:" # header: no date
assert lines[2] == " a/ — 2 documents: A stuff." # subfolder: no date
# Only the DIRECT root file lists (a/ is a subfolder line — the
# drill-down is one level per call), and it alone carries a date.
file_lines = [line for line in lines if line.startswith("source: ")]
assert file_lines == [
"source: S | path: top.md | title: Top | date: 2024-06-15"
]
# The top level (source lines) is byte-identical to the pre-phase
# template — no date on source lines.
assert agent.render_ls_top([("S", 3, "Source stuff.")]) == (
"1 sources:\n\nS — 3 documents\n Source stuff."
)
assert "date" not in agent.render_ls_top([("S", 3, None)])
def test_ls_cap_note_unchanged_past_fifty() -> None:
"""The 50-line cap note is UNCHANGED by the date field: 51 files →
50 lines (each with its appended date) + the exact pre-phase note;
the line COUNT is unaffected by the wider lines."""
rows = [(f"f{i:03d}.md", f"T{i}", "2024-06-15") for i in range(51)]
subfolders, files, total = agent.group_folder_listing("S", "", rows, {})
rendered = agent.render_folder_listing("S", subfolders, files, total)
lines = rendered.splitlines()
assert total == 51 and len(lines) == 1 + 1 + 50 + 1 # header, blank, 50, note
assert lines[-1] == (
"…and 1 more documents in this folder — use grep (pattern) to "
"find a specific one."
)
assert lines[1] == "" # the blank after the header is untouched
assert all(
line.endswith(" | date: 2024-06-15")
for line in lines[2:52]
)