Files
brain-of-reese/tests/unit/test_prompt_lock.py
T
ducoterra b34b8ca672
Build and Push Containers / build-and-push-app (push) Successful in 1m46s
Build and Push Containers / build-and-push-db (push) Successful in 12s
phase: 182_verbatim_show_text
**Phase 182 — final verification pass (all 8 tasks already complete; this was verification + docs + one defect fix).**

- Verified: `app/rag/verbatim.py` (locator grammar, `splitlines()` slice, line-boundary cap, `resolve_answer_blocks`), `SHOW_TEXT_TOOL` + `shown_texts`, `SHOW_TEXT_CLAUSE`, `done.answer_text` (9 fields, key dropped when empty) + `ChatMessageRecord.answer_text` (≤50, 600 000-char cap counted in the 8 M budget), `verbatim.js` + 4 `{verbatim:true}` renderers, `Containerfile` minify step, `.env.example` 3 vars.
- `uv run python -c "…answer_text, show_text_max_chars, show_text_max_blocks"` → `True 12000 5`; each negative refuses startup naming its field; `0` accepted as inert ✅
- `bash .agents/validate.sh` → **validation OK**: 4320 unit+integration tests green, `TOTAL 6849 17 → 99%` (gate app/ >90% OK), `ruff check .` "All checks passed!", `pyright` 0 errors (re-run after my edits, 08:28).
- E2E, each in isolation `--no-cov`: `test_verbatim_show_text.py` 13 passed (panel bytes == `documents.content`, server caption, clipboard == server string, `/document.html?source=…&path=…`, honest unresolved state, reload+share, `show_image` regression, flag-off app) ✅; green: `test_answer_images` `test_harness_aligned_tools` `test_agent_document_tools` `test_markdown_tables` `test_markdown_inline_links_images` `test_svg_document_preview` `test_smoke` `test_document_viewer` `test_markdown_blockquotes` `test_chat_persistence` `test_thinking_timeline` `test_chat_rag` `test_settings_mobile_value_width`.
- **Defect found & fixed (pre-existing, not phase 182):** `tests/e2e/test_admin_settings_panel.py::test_admin_sees_groups_and_source_badges` failed — its whole-page `assert "reese" not in page.content()` tripped on the shell's onboarding chips, which follow the newest saved chats in the one shared E2E DB (a leftover `reeselink.json` question from a tool suite). Scoped the strict sweep to `.settings-shell` and kept a whole-page `reese:reese` credential needle → 12 passed with the leftovers still present.
- Docs: README fixed 2 inconsistencies — Quick Start omitted the **required** `BOR_ADMIN_PASSWORD`/`BOR_SESSION_SECRET` (the app fail-loud refuses boot) and the broken anchor `#chat--` → `#chat-`; full re-read of README (1816 lines) + AGENTS confirmed the phase-182 feature/settings/log/UI-contract text matches the code, and every `.env.example` var + every `Settings` field is documented.
- Notable: kept `resolve_answer_blocks` synchronous on the SSE terminal path (bounded indexed SELECTs, matching the existing `query_log` precedent and the phase's documented fence-lookup bound); no live-infra changes made.
- Next pending phase: none — `todo/` holds only 182 (its 8 task files are in `complete/`; the harness commits + moves `00_phase.md`).
2026-10-15 08:33:12 -04:00

376 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 (
ORIGIN_RULE,
PERSONA,
SEARCH_DISCIPLINE_RULE,
SHOW_IMAGE_CLAUSE,
SHOW_TEXT_CLAUSE,
SUGGEST_INTRO,
TOOLS_SECTION,
_base,
build_deflect_prompt,
build_high_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``. RE-CUT again for phase 145 (task 02): the
#: ``read`` clause gains the original-source rule — :data:`ORIGIN_RULE`,
#: the one sentence-group appended at the ``read`` clause's end (the
#: truncation workflow sentence) and BEFORE the ``grep`` clause begins
#: (the ``origin:`` line is a read-result fact; the rule binds the
#: ANSWER that uses what the read added). Only that sentence-group was
#: inserted — 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-145; the pre-phase-145 sha/length are
#: kept below for the chained diff-style pin. RE-CUT again for phase 162
#: (task 03, LOCKED A3+A4): the ``grep`` sentence's ``(case-insensitive)``
#: parenthetical widened to the folded contract ``(case- and
#: accent-insensitive — accents fold: besta matches BESTÅ)``, and the
#: LOCKED search-discipline sentence (:data:`SEARCH_DISCIPLINE_RULE`) was
#: inserted immediately after the grep sentence (before the ``For a normal
#: search pass`` clause). Only those two byte-groups moved — the prefix
#: (the ``ls``-clause opening) and the suffix (the discipline-rules
#: ending) survive byte-identical; the pre-phase-162 sha/length are kept
#: below for the chained diff-style pin.
TOOLS_SECTION_SHA256 = "988642f9dc6bb2a4a1d44a83b5368ff4cdb9df4e181b2fe3dc2fb241b1857595"
TOOLS_SECTION_LEN = 3818
# The pre-phase-162 anchors — the section BEFORE phase 162's grep-fold +
# search-discipline insertion. Kept for the chained diff-style pin.
TOOLS_SECTION_PRE_PHASE_162_SHA256 = (
"41a84885be98d4e15b95205694d59d364296a30e27758d39ae737881d5bacaa7"
)
TOOLS_SECTION_PRE_PHASE_162_LEN = 3279
# The grep contract parenthetical exactly as it changed in phase 162
# (LOCKED A3): the pre-phase ``(case-insensitive)`` vs the folded one now
# embedded in ``TOOLS_SECTION``. Used to revert phase 162 for the diff pin.
_GREP_CONTRACT_PRE_PHASE_162 = "(case-insensitive)"
_GREP_CONTRACT_PHASE_162 = (
"(case- and accent-insensitive — accents fold: `besta` matches `BESTÅ`)"
)
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>"
)
#: The pre-phase-145 anchors — the section BEFORE :data:`ORIGIN_RULE` was
#: inserted. Kept for the diff-style pin: the ONLY byte change is the
#: rule's insertion, so removing the rule reproduces these exact bytes.
TOOLS_SECTION_PRE_PHASE_145_SHA256 = (
"3671680bbf4e04efe1186690c18d24869e4974ceb29485cedc3e34a9286bf3b2"
)
TOOLS_SECTION_PRE_PHASE_145_LEN = 2656
#: Phase 145 (task 02) — the original-source rule the ``read`` teaching
#: carries is :data:`app.rag.prompts.ORIGIN_RULE` (imported above, LOCKED
#: verbatim). :data:`TOOLS_SECTION` embeds that exact sentence-group at
#: the ``read`` clause's end (the truncation workflow sentence) and
#: BEFORE the ``grep`` clause begins. A read result for a git-sourced
#: document carries an ``origin:`` line (and a ``branch:`` line when a
#: branch is pinned) — the read contract's fact — so when the answer uses
#: such a document it includes the original source in the answer: a link
#: to the repository (the ``origin`` line) with the document's path (the
#: ``Document …`` header) named and the pinned branch when one is shown,
#: as the repository's file-view URL where the model can tell it
#: (``<repo-url>/blob/<branch>/<path>``), or the repository URL plus the
#: path as plain text otherwise. The rule is CONDITIONAL on the ``origin``
#: line's presence — a local-sourced document's read result carries none,
#: so the rule adds nothing to those answers (the model has no URL to
#: cite; it must not invent one — the sentence's conditioning does that
#: job).
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)
def test_tools_section_origin_rule_verbatim_at_its_position() -> None:
"""Phase 145 (task 02): the original-source rule is LOCKED verbatim
and sits at its EXACT position — the ``read`` clause's end (the
truncation workflow sentence) and BEFORE the ``grep`` clause begins.
The sentence-group is pinned byte-for-byte, and the surrounding
boundary (the truncation sentence's tail → the rule → the ``grep``
clause's opening) is pinned as one contiguous substring, so a move
or re-spelling of ANY byte (the rule's or the boundary's) fails."""
# Verbatim presence.
assert ORIGIN_RULE in TOOLS_SECTION
assert TOOLS_SECTION.count(ORIGIN_RULE) == 1 # exactly once
# Exact position: after the truncation sentence, before the grep
# clause — one contiguous span, no drift at either seam.
assert (
"it searches the whole document. "
+ ORIGIN_RULE
+ " `grep` locates an exact string"
) in TOOLS_SECTION
def test_tools_section_search_discipline_rule_verbatim_at_its_position() -> None:
"""Phase 162 (task 03, LOCKED A4): the search-discipline rule is
LOCKED verbatim and sits at its EXACT position — immediately after
the grep sentence (the ``…retry with the plain text you expect to
see.`` clause) and BEFORE the ``For a normal search pass`` clause.
The rule + its exact boundaries are pinned as one contiguous span, so
a move or re-spelling of ANY byte fails."""
assert SEARCH_DISCIPLINE_RULE in TOOLS_SECTION
assert TOOLS_SECTION.count(SEARCH_DISCIPLINE_RULE) == 1 # exactly once
assert (
"retry with the plain text you expect to see. "
+ SEARCH_DISCIPLINE_RULE
+ " For a normal search pass only `pattern`"
) in TOOLS_SECTION
def test_tools_section_grep_fold_contract_present() -> None:
"""Phase 162 (task 03, LOCKED A3): the grep sentence now states the
FOLDED contract — the ``(case-insensitive)`` parenthetical is replaced
byte-for-byte by the folded one, and the old one is gone."""
assert _GREP_CONTRACT_PHASE_162 in TOOLS_SECTION
assert _GREP_CONTRACT_PRE_PHASE_162 not in TOOLS_SECTION
def test_tools_section_phase162_and_145_are_the_only_byte_changes() -> None:
"""Chained diff-style pin: the ONLY byte changes are phase 162's (the
folded grep contract + :data:`SEARCH_DISCIPLINE_RULE`) and phase 145's
(:data:`ORIGIN_RULE`). Reverting phase 162 (drop the rule + its
separating space, restore the pre-phase grep parenthetical) reproduces
the pre-phase-162 section byte-for-byte; reverting the origin rule on
top of that reproduces the pre-phase-145 section byte-for-byte."""
# Revert phase 162 -> pre-phase-162.
pre_162 = TOOLS_SECTION.replace(SEARCH_DISCIPLINE_RULE + " ", "")
pre_162 = pre_162.replace(_GREP_CONTRACT_PHASE_162, _GREP_CONTRACT_PRE_PHASE_162)
assert len(pre_162) == TOOLS_SECTION_PRE_PHASE_162_LEN
assert _sha256(pre_162) == TOOLS_SECTION_PRE_PHASE_162_SHA256
# Revert phase 145 -> pre-phase-145.
pre_145 = pre_162.replace(ORIGIN_RULE + " ", "")
assert len(pre_145) == TOOLS_SECTION_PRE_PHASE_145_LEN
assert _sha256(pre_145) == TOOLS_SECTION_PRE_PHASE_145_SHA256
# ---------- 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
# ---------- SHOW_TEXT_CLAUSE (phase 182, task 04: verbatim display) ----------
#: Anchors for ``SHOW_TEXT_CLAUSE`` — the ``show_text`` / ```bor``` locator
#: teaching appended INSIDE ``<tools>`` when ``BOR_ANSWER_TEXT`` is on
#: (phase 182, LOCKED A1–A3/A6). Pinned the way ``TOOLS_SECTION`` is
#: (sha256 + exact prefix + total length) because this clause IS the fix
#: for the retype bug the phase exists for: a silent wording drift here
#: re-opens it (the tool alone never teaches that the user copies from
#: what is DISPLAYED, nor the fence syntax).
SHOW_TEXT_CLAUSE_SHA256 = "e03a8153ca6cde61c9437f3b66b4fa07cd2a3f9f2e24e3bb8c37ca1d7c7e9e3b"
SHOW_TEXT_CLAUSE_LEN = 1179
SHOW_TEXT_CLAUSE_PREFIX = (
"`show_text` displays part of a document to the user EXACTLY as stored"
)
def test_show_text_clause_byte_locked() -> None:
"""The clause is LOCKED copy: sha256 + exact prefix + total length,
plus the two sentences the phase's contract depends on verbatim (the
copy-from-the-display rule and the server-owns-the-numbers rule)."""
assert len(SHOW_TEXT_CLAUSE) == SHOW_TEXT_CLAUSE_LEN
assert _sha256(SHOW_TEXT_CLAUSE) == SHOW_TEXT_CLAUSE_SHA256
assert SHOW_TEXT_CLAUSE.startswith(SHOW_TEXT_CLAUSE_PREFIX)
assert SHOW_TEXT_CLAUSE.endswith("For an image use `show_image` instead.")
assert "the user copies from what is displayed, not from your prose" in SHOW_TEXT_CLAUSE
assert "the numbers you display are the server's, never yours" in SHOW_TEXT_CLAUSE
def test_high_prompt_display_clause_composition_is_byte_locked() -> None:
"""One mechanism, four combinations (phase 182, task 04): neither
flag reproduces the untouched :data:`TOOLS_SECTION` tail,
``show_image`` alone reproduces the phase-124 bytes exactly,
``show_text`` alone appends only the text clause, and both append
the image clause, a blank line, the text clause, then the closing
tag — ``TOOLS_SECTION`` itself is never edited (its own sha pin
above holds)."""
off = build_high_prompt([])
assert off.endswith(TOOLS_SECTION)
head = off[: -len("</tools>")]
assert build_high_prompt([], show_image=True) == head + SHOW_IMAGE_CLAUSE + "\n</tools>"
assert build_high_prompt([], show_text=True) == head + SHOW_TEXT_CLAUSE + "\n</tools>"
assert (
build_high_prompt([], show_image=True, show_text=True)
== head + SHOW_IMAGE_CLAUSE + "\n\n" + SHOW_TEXT_CLAUSE + "\n</tools>"
)