Files
brain-of-reese/tests/unit/test_prompt_lock.py
T
ducoterra a8c0402ad2
Build and Push Containers / build-and-push-app (push) Successful in 2m37s
Build and Push Containers / build-and-push-db (push) Successful in 12s
phase: 141_read_range_dedupe_fix
All gates green and verified. Final report:

**Phase 141 final verification pass — complete**

- **Verified (tasks 01–04 already in `complete/`):** `AgentHolder.read_coverage` span recording (all 4 read outcomes, refusals record nothing); coverage dedupe matrix (range read of uncovered lines proceeds, fully-covered re-read → `ALREADY_IN_CONTEXT` with a now-true claim, partial plain re-read → `REREAD_PARTIAL` naming merged spans, `REREAD_TRUNCATED` retired, fast path retired per task 02); `TOOLS_SECTION` re-read sentence re-cut to coverage-true form (prompt lock re-pinned); owner-scenario integration repro (pure + truncated variants) and mock `read two ranges` flow + dedicated E2E — all match the task specs exactly; no code defects found.
- **Tests/lint/coverage:** `uv run pytest --cov=app --cov-report=term-missing` → exit 0, TOTAL 99% (5498 stmts, `app/rag/agent.py` 99%) — run twice, both green; `uv run pytest tests/e2e/test_read_range_dedupe.py -v --no-cov` → 2 passed in isolation; `uv run ruff check . && uv run pyright` → All checks passed / 0 errors.
- **Completion criteria:** (1) two different ranges both reach the model, re-covered re-read refused truthfully — ✅ (integration + E2E); (2) partial plain re-read gets the `offset`/`limit` teaching, false claim gone both directions — ✅; (3) plain-read + phase-131 result shapes byte-identical, existing pins pass unchanged — ✅; (4) all gate commands green, >90% — ✅; (5) no behavior change in completed phases (grep/ls, truncation notice, `read_docs`/`read_truncations` signals) — ✅ (whole project suite green).
- **Documentation:** README.md — fixed pre-existing phase-140 gap: `BOR_PDF_PICTORIAL` / `BOR_PDF_PICTORIAL_MIN_CHARS` were in `.env.example`/Settings but undocumented (README claims every `.env.example` var is covered); added the pictorial-PDF-pass bullet to the PDF section + two config-reference rows. AGENTS.md — verified consistent (rules, quick-ref commands, phase refs all check out) — no changes needed.
- **Notable:** overview's "fast path survives for holder-state rows" note is superseded by task 02's authoritative decision (every dedupe row resolves — "full" needs the true line count); implementation follows task 02. No phase files touched; changes left in the working tree for the harness commit.
- **Next pending phase:** `142_kb_overview_freshness`.
2026-10-01 01:27:46 -04:00

204 lines
9.2 KiB
Python

"""Prompt-lock pin (phase 112, task 03 — owner decision iii, 2026-09-14).
The persona + HONESTY GATE text is **locked verbatim** (PLAN §6): it
changes through the plan, never in code. This module byte-pins the
locked prompt constants against their pre-phase-112 anchor values —
sha256 + exact prefix/suffix + total length, so *any* byte change
(option (i)'s copy tightening, option (ii)'s plan amendment, or an
accidental edit) fails loudly until the anchors are re-cut as part of
the same plan revision. ``tests.unit.test_prompts`` pins the
assembled-prompt structure and the behavioral contracts on top of
these constants; this file pins the constants themselves.
"""
from __future__ import annotations
import hashlib
from app.rag.prompts import (
PERSONA,
SUGGEST_INTRO,
TOOLS_SECTION,
_base,
build_deflect_prompt,
)
def _sha256(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
# ---------- PERSONA (the locked base of BOTH the HIGH and LOW prompts) ----------
#: Pre-phase-112 anchors for ``PERSONA`` (the ``{relevance}`` placeholder
#: and the line wrapping included).
PERSONA_SHA256 = "e31792a73e64c53853097e0f7b6df8b96c5f2d05c286944c3edba16dde7777fe"
PERSONA_LEN = 706
PERSONA_PREFIX = (
'You are "Brain of Reese" — the digital brain of Reese, a self-hoster and\n'
"homelab tinkerer. Personality: chippy, upbeat, warm, and genuinely\n"
"optimistic about the user's ability to do things.\n"
"\n"
"Rules:\n"
)
PERSONA_SUFFIX = (
"4. Never invent facts, hosts, or steps that are not in the context.\n"
"5. Keep answers tight: short paragraphs, bullets where helpful.\n"
"\n"
"<relevance>{relevance}</relevance>"
)
def test_persona_byte_locked() -> None:
"""Any byte change to the locked persona (opening, any rule line, the
``<relevance>`` placeholder) fails on the sha256; the prefix/suffix
anchors name the damaged region for the diff."""
assert len(PERSONA) == PERSONA_LEN
assert _sha256(PERSONA) == PERSONA_SHA256
assert PERSONA.startswith(PERSONA_PREFIX)
assert PERSONA.endswith(PERSONA_SUFFIX)
def test_high_and_low_bases_byte_locked() -> None:
"""``_base`` only substitutes ``{relevance}`` — the per-mode base
lengths pin the substitution against a moved or re-spelled
placeholder in the locked text."""
assert len(_base("HIGH")) == 699 # PERSONA_LEN - 11 + 4
assert len(_base("LOW")) == 698 # PERSONA_LEN - 11 + 3
# ---------- TOOLS_SECTION (the HIGH prompt's locked ``<tools>`` copy) ----------
#: Anchors for ``TOOLS_SECTION`` — pre-phase-112 values, RE-CUT for
#: phase 118 (task 04, A6, owner directive 2026-09-15): the ``read``
#: clause was rewritten for the summary-seed mode (the ``<documents>``
#: section holds SUMMARIES — ``read`` adds the full text; only an
#: already-read document is refused). RE-CUT again for phase 141
#: (task 02): the re-read sentence was re-cut to the coverage-true
#: form — do not re-read a document read IN FULL (answer from it);
#: a PARTIAL document (a range read, or a truncated read) is not
#: complete in the context until read all — read the other parts with
#: ``offset``/``limit``. Only that sentence moved — the prefix (the
#: ``ls``-clause opening) and the suffix (the discipline-rules
#: ending) survived byte-identical, so they are the same anchors as
#: pre-phase-141.
TOOLS_SECTION_SHA256 = "3671680bbf4e04efe1186690c18d24869e4974ceb29485cedc3e34a9286bf3b2"
TOOLS_SECTION_LEN = 2656
TOOLS_SECTION_PREFIX = (
"<tools>\n"
"You may extend your context with three tools. `ls` lists the "
"knowledge base as a tree, one level at a time: "
)
TOOLS_SECTION_SUFFIX = (
"Never repeat a call that was refused or already succeeded — the refusal "
"already told you the correct form. Answer as soon as you have what "
"you need.\n</tools>"
)
def test_tools_section_byte_locked() -> None:
"""The ``<tools>`` teaching is LOCKED verbatim too (the E2E mock keys
on the ``<tools>`` marker's presence; the wording is the owner's):
sha256 + exact prefix/suffix + total length."""
assert len(TOOLS_SECTION) == TOOLS_SECTION_LEN
assert _sha256(TOOLS_SECTION) == TOOLS_SECTION_SHA256
assert TOOLS_SECTION.startswith(TOOLS_SECTION_PREFIX)
assert TOOLS_SECTION.endswith(TOOLS_SECTION_SUFFIX)
# ---------- the LOW prompt's locked DEFLECT_MODE body ----------
#: The ``DEFLECT_MODE`` body exactly as it was pre-phase-112 (the
#: phase-71 plain-text line included) — inline in
#: :func:`app.rag.prompts.build_deflect_prompt`, so it is pinned through
#: the built prompt rather than a module constant.
LOW_BODY_SHA256 = "c9868cfccdd0ff79d7c1de5df5f6dea0182d3726ca912c9ef609ab54b563a0a4"
LOW_BODY_LEN = 259
LOW_BODY = (
"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."
)
def test_deflect_body_byte_locked() -> None:
"""The LOW build = locked base + exactly the locked DEFLECT_MODE body
+ the weak-hit title list — byte for byte (the mock keys on the
``DEFLECT_MODE`` marker's presence; the body wording is locked)."""
assert len(LOW_BODY) == LOW_BODY_LEN
assert _sha256(LOW_BODY) == LOW_BODY_SHA256
assert build_deflect_prompt(["T1", "T2"]) == _base("LOW") + "\n" + LOW_BODY + "\n- T1\n- T2"
prompt = build_deflect_prompt(["T1", "T2"])
assert prompt.count(LOW_BODY) == 1
assert prompt.index("DEFLECT_MODE") < prompt.index(
"Reply in plain text only"
) # the marker precedes the plain-text line
#: The full LOW prompt build on the canonical fixture titles — sha-pinned
#: (phase 118, task 03): the deflection path is UNTOUCHED by the
#: summary-seed re-revision (LOCKED A8) — the same inputs must produce
#: the pre-phase bytes, so this anchor is a pre-phase-118 value.
LOW_PROMPT_SHA256 = "726eddb4eb3bcc26c840011f6f8635d6af55aa09d4d064bbb9e256caa9665837"
LOW_PROMPT_LEN = 968
def test_low_prompt_build_byte_identical_to_pre_phase() -> None:
"""Phase 118 contract: the LOW prompt output is byte-identical to
pre-phase for identical inputs — the summary seeding (and the new
``SUGGEST_INTRO`` line) never leaks into the deflection path."""
prompt = build_deflect_prompt(["T1", "T2"])
assert len(prompt) == LOW_PROMPT_LEN
assert _sha256(prompt) == LOW_PROMPT_SHA256
assert "SUGGEST_INTRO" not in prompt and "<documents>" not in prompt
# ---------- SUGGEST_INTRO (phase 118 framing, re-cut for phase 119, LOCKED A5) ----------
#: Anchors for ``SUGGEST_INTRO`` — the ``<documents>`` section's intro
#: line (the owner's "start here if these summaries seem right to you"
#: framing, TODO L3, phase 118). RE-CUT for phase 119 (task 04, LOCKED
#: A5, owner directive 2026-09-16): the final sentence was replaced
#: with the cite-discipline sentence (cite only the documents read — or
#: the suggested document answered from without reading — never a
#: document neither read nor used), closing the live confabulation in
#: which the model cited a file it never read; the phase-118 "cite the
#: document(s) you used, by path" sentence is retired. Only that
#: sentence moved — the start-here framing, the ``read`` pointer, and
#: the full-text clause survive byte-identical, so the prefix anchor is
#: the same as pre-phase-119. The E2E mock's ``_document_block`` parser
#: is regex-based over the block markup (which stays byte-stable around
#: the intro), so this constant is a prompt-copy lock, pinned the way
#: ``TOOLS_SECTION`` is: sha256 + prefix + total length. ``PERSONA``,
#: ``TOOLS_SECTION``, and the LOW body anchors above stay byte-identical
#: — this change touches no other constant.
SUGGEST_INTRO_SHA256 = "c6ffbb19975cbf184910a9a8e8fdea101c16f5d3eecb872fb07afeabd2763302"
SUGGEST_INTRO_LEN = 460
SUGGEST_INTRO_PREFIX = (
"The blocks below are the summaries of the top-ranked documents for "
"your question — start here if one seems right to you: "
)
def test_suggest_intro_byte_locked() -> None:
"""The start-here framing is LOCKED copy (phase 118; the final
sentence re-cut by phase 119, LOCKED A5): sha256 + exact prefix +
total length; the four contracts it must carry (summaries are the
starting points; ``read`` adds the full text, which is NOT in the
prompt until read; the phase-119 cite discipline — cite only what
was read, or the suggested document answered from without reading,
never a document neither read nor used) are pinned as substrings."""
assert len(SUGGEST_INTRO) == SUGGEST_INTRO_LEN
assert _sha256(SUGGEST_INTRO) == SUGGEST_INTRO_SHA256
assert SUGGEST_INTRO.startswith(SUGGEST_INTRO_PREFIX)
assert "call `read" in SUGGEST_INTRO
assert "combined `source/path`" in SUGGEST_INTRO
assert "its full text is not in the prompt until you read it" in SUGGEST_INTRO
assert (
"Cite only the document(s) you read — or, if you answered from a "
"suggested summary without reading it, cite that suggested "
"document — never a document you neither read nor used."
) in SUGGEST_INTRO
# The retired phase-118 final sentence is gone from the constant.
assert "Cite the document(s) you used, by path." not in SUGGEST_INTRO