Files
FabledGrimoire/tests/test_matched_passage_surfaces.py
bvandeusenandClaude Opus 5.5 0f5ef0be63
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 44s
rename(547): the server is grimoire — package, image, DB defaults, backups
Step 2 of the Scribe -> Grimoire rename.

- src/scribe -> src/grimoire, and every import, patch target, module path,
  hypercorn entry point and `python -m grimoire.restore` with it; pyproject,
  uv.lock and the frontend package are named grimoire.
- Image git.fabledsword.com/bvandeusen/fabledgrimoire; CI's Postgres service
  runs as grimoire / grimoire_test.
- Compose files default the DB user, database and (prod) volume to grimoire,
  each overridable from .env, so an install created as Scribe keeps its data
  by setting POSTGRES_USER, POSTGRES_DB and PGDATA_VOLUME.
- Backups are written as grimoire-<ts>.tar with format grimoire-backup-tar.
  Archives named scribe-* with the scribe-backup-tar manifest still restore
  and are still counted at the destination (folder and S3), with tests.
- Exports, the MCP auth realm, the ASGI scope key and the persistent logger
  carry the new name.

Kept on purpose: the secret-sealing key label stays "scribe.secrets.v1",
because every stored secret was derived under it.

Plugin identity (plugin name, hooks, env vars, .scribe marker, skills) is
step 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 10:50:45 -04:00

167 lines
6.7 KiB
Python

"""Every semantic search hands on the passage that matched.
Three searches collapse chunk rows to the best one per record, so each of them
KNOWS which passage earned the hit. A search that drops it leaves every door to
preview the head of the document instead — a span the search has already scored
lower, with nothing saying so.
These pin the mechanism (`report["best_chunk"]` from all three searches) and
each surface that reads it, because the failure mode is silent: a door that
quietly reverts to the body's opening still returns a plausible-looking string
and no test that only checks "a preview exists" would notice.
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from grimoire.services import embeddings as emb
from grimoire.services.text import (
BODY_OPENING,
MATCHED_PASSAGE,
excerpt_fields,
matched_excerpt,
)
from tests.helpers import make_mock_session
def _searching(rows):
"""A patched session whose one query returns `rows` — the shared
make_mock_session rather than a third local copy of the
__aenter__/__aexit__ dance."""
session = make_mock_session()
result = MagicMock()
result.all.return_value = rows
session.execute = AsyncMock(return_value=result)
return session
# ---------------------------------------------------------------------------
# The shared choice of span
# ---------------------------------------------------------------------------
def test_the_matched_passage_wins_over_the_opening():
body = "An opening about nothing much. " * 20 + " THE ANSWER."
text, kind, _cut = matched_excerpt(body, {"index": 4, "text": "THE ANSWER."}, 1000)
assert text == "THE ANSWER."
assert kind == MATCHED_PASSAGE
def test_without_a_chunk_the_opening_is_named_as_the_opening():
"""The fallback is legitimate — a plain listing matched nothing — but it
must not pass for the passage that matched."""
text, kind, _cut = matched_excerpt("just a body", None, 1000)
assert kind == BODY_OPENING
def test_an_empty_chunk_is_not_mistaken_for_a_passage():
"""A record embedded from its title alone stores an empty body chunk;
rendering that as "the passage that matched" would be a blank line
presented as evidence."""
_t, kind, _c = matched_excerpt("real body", {"index": 0, "text": " "}, 1000)
assert kind == BODY_OPENING
def test_the_field_names_travel_together():
"""A door renames the text field to keep its consumers working; the label
has to follow it, or a row carries an excerpt under one name and its
meaning under another."""
out = excerpt_fields("b" * 500, {"index": 1, "text": "hit"}, 100, key="snippet")
assert out["snippet"] == "hit"
assert out["snippet_is"] == MATCHED_PASSAGE
assert out["body_length"] == 500
assert "read_full" in out
def test_a_record_shown_whole_advertises_nothing_further():
out = excerpt_fields("short", None, 1000)
assert out["excerpt"] == "short"
assert "read_full" not in out
# ---------------------------------------------------------------------------
# All three searches publish the winning chunk
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_rule_search_reports_the_clause_that_matched():
"""A rule's `why` and `how_to_apply` run long. A caller shown only the head
cannot see the clause the query actually hit."""
r1, r2 = MagicMock(id=1), MagicMock(id=2)
session = _searching([
(r1, 0.10, 2, "the clause that matched"),
(r2, 0.20, 0, "r2 best"),
(r1, 0.40, 9, "a worse clause of r1"),
])
report: dict = {}
with (
patch.object(emb, "async_session", return_value=session),
patch.object(emb, "get_embedding", AsyncMock(return_value=[0.0] * 384)),
patch.object(emb, "can_read_project", AsyncMock(return_value=True)),
):
out = await emb.semantic_search_rules(1, "q", limit=5, threshold=0.0,
report=report)
assert [r.id for _s, r in out] == [1, 2]
assert report["best_chunk"][1] == {"index": 2, "text": "the clause that matched"}
@pytest.mark.asyncio
async def test_milestone_search_reports_the_passage_of_the_plan_that_matched():
"""A milestone's body IS the plan and search shows its short description,
which need not mention the part the query was about."""
m1 = MagicMock(id=7)
session = _searching([(m1, 0.15, 5, "step 6 — the acceptance case")])
report: dict = {}
with (
patch.object(emb, "async_session", return_value=session),
patch.object(emb, "get_embedding", AsyncMock(return_value=[0.0] * 384)),
patch.object(emb, "can_read_project", AsyncMock(return_value=True)),
):
out = await emb.semantic_search_milestones(
1, "acceptance", limit=5, threshold=0.0, report=report,
)
assert [m.id for _s, m in out] == [7]
assert report["best_chunk"][7]["text"] == "step 6 — the acceptance case"
@pytest.mark.asyncio
async def test_a_caller_that_passes_no_report_still_works():
"""Every one of these searches fails open by design — a recall aid must
never break the call it serves — and that includes the chunk channel."""
session = _searching([(MagicMock(id=1), 0.1, 0, "text")])
with (
patch.object(emb, "async_session", return_value=session),
patch.object(emb, "get_embedding", AsyncMock(return_value=[0.0] * 384)),
patch.object(emb, "can_read_project", AsyncMock(return_value=True)),
):
out = await emb.semantic_search_milestones(1, "q", limit=5, threshold=0.0)
assert len(out) == 1
def test_record_best_chunk_on_no_report_is_a_no_op():
emb.record_best_chunk(None, {1: {"index": 0, "text": "x"}}) # must not raise
# ---------------------------------------------------------------------------
# Only what survived the bar is published
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_chunks_are_published_only_for_results_that_came_back():
"""Keyed off the returned rows, so a caller can look up every result it has
and never holds passages for records it was not shown."""
keep, drop = MagicMock(id=1), MagicMock(id=2)
session = _searching([
(keep, 0.10, 0, "kept"),
(drop, 0.95, 0, "below the bar"),
])
report: dict = {}
with (
patch.object(emb, "async_session", return_value=session),
patch.object(emb, "get_embedding", AsyncMock(return_value=[0.0] * 384)),
patch.object(emb, "can_read_project", AsyncMock(return_value=True)),
):
await emb.semantic_search_milestones(
1, "q", limit=5, threshold=0.5, report=report,
)
assert set(report["best_chunk"]) == {1}