Files
FabledGrimoire/tests/test_routes_retrieval_tuning.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

215 lines
9.0 KiB
Python

"""The operator's half of the tuning surface.
WHY THIS EXISTS
The model moves these dials, so the browser's job is not "set the number". It is to show what
was set, by whom, and on what argument, and to let the operator disagree.
The load-bearing guard here is the last one. `/api/settings` is a generic
key-value endpoint that accepts any key at all, and every retrieval floor IS an
ordinary settings key. A floor written straight through it would land correctly
and record nothing — a tuning history with holes in it, which is worse than no
history because it reads as complete. So the generic endpoint routes those keys
through `set_dial`, and that routing is asserted rather than remembered.
"""
import inspect
import pytest
def test_retrieval_blueprint_registered():
from grimoire.routes.retrieval import retrieval_bp
assert retrieval_bp.name == "retrieval"
assert retrieval_bp.url_prefix == "/api/retrieval"
def test_retrieval_blueprint_registered_in_app():
from grimoire.app import create_app
app = create_app()
assert "retrieval" in app.blueprints
def test_every_endpoint_is_reachable_on_the_app():
"""Handlers existing is not the same as them being routed."""
from grimoire.app import create_app
app = create_app()
rules = {
str(r.rule) for r in app.url_map.iter_rules()
if r.endpoint.startswith("retrieval.")
}
assert rules == {
"/api/retrieval/surfaces",
"/api/retrieval/surfaces/<surface>",
"/api/retrieval/tuning-history",
"/api/retrieval/moments",
"/api/retrieval/moments/mappings",
"/api/retrieval/moments/proposals",
"/api/retrieval/moments/proposals/judge",
"/api/retrieval/reply-shapes",
}
def test_the_browser_and_the_agent_call_the_same_service():
"""Parity. Two callers, one service — or the two surfaces drift and
the guard that a reason is required exists on only one of them."""
from grimoire.mcp.tools import retrieval_tuning as tool
from grimoire.routes import retrieval as routes
from grimoire.services import retrieval_tuning as svc
assert routes.set_dial is svc.set_dial
assert tool.tuning_svc is svc
def test_the_route_does_not_take_the_actor_from_the_caller():
"""`actor` is the one field a reviewer leans on to answer "did I do this,
or did the session?". A payload-supplied actor would let a model claim to
be the operator, which turns the column into decoration."""
from grimoire.routes import retrieval as routes
src = inspect.getsource(routes.tune_surface_route)
assert 'actor="human"' in src
assert 'data.get("actor"' not in src and 'data["actor"]' not in src
# ── the hole the generic settings endpoint would otherwise leave ────────────
def test_every_registry_key_is_recognised_as_a_dial():
"""Derived from the registry, both directions. A seventh surface added
without a row here is a floor that can be written silently again."""
from grimoire.services.retrieval_surfaces import SURFACES, dial_for_key
for name, surface in SURFACES.items():
assert dial_for_key(surface.floor_key) == (name, "floor")
assert dial_for_key(surface.budget_key) == (name, "budget")
def test_an_ordinary_setting_is_not_mistaken_for_a_dial():
"""The interception must be narrow. A false positive here would send an
unrelated setting through a service that parses it as a float and rejects
the save."""
from grimoire.services.retrieval_surfaces import dial_for_key
for key in ("smtp_password", "kb_autoinject_enabled", "theme",
"kb_planmatch_threshold", ""):
assert dial_for_key(key) is None, key
def test_the_settings_endpoint_routes_a_dial_through_the_recorder():
"""THE GUARD. Asserted on the source because the alternative is a full
request-context round trip for a branch whose whole content is "which
function gets called" — and because what must not regress is precisely
that this module reaches for `set_dial` at all."""
from grimoire.routes import settings as routes
src = inspect.getsource(routes.update_settings_route)
assert "dial_for_key" in src, (
"the generic settings endpoint no longer recognises retrieval dials — "
"a floor written through it now moves with no event recorded, and the "
"tuning history will say nothing happened"
)
assert "set_dial" in src
assert 'actor="human"' in src
@pytest.mark.asyncio
async def test_setting_a_dial_to_the_value_it_already_has_records_nothing():
"""The Settings form re-sends every field on every save. Without this, one
press of Save writes six rows saying the operator set six dials to the
numbers they were already on — and a history nobody can skim is one nobody
reads."""
from unittest.mock import AsyncMock, MagicMock, patch
from grimoire.services import retrieval_tuning as rt
from tests.helpers import make_mock_session
session = make_mock_session()
with patch.object(rt, "async_session", MagicMock(return_value=session)), \
patch.object(rt, "set_setting", AsyncMock()) as setter, \
patch.object(rt, "floor_for", AsyncMock(return_value=0.72)), \
patch.object(rt, "budget_for", AsyncMock(return_value=3)):
out = await rt.set_dial(1, "prompt_rule", "floor", 0.72,
reason="re-saved the settings form untouched")
assert out["unchanged"] is True
session.add.assert_not_called()
# And the setting is left alone too: rewriting the same value would bump
# whatever timestamp the row carries for no reason.
setter.assert_not_called()
# ── the moments view ────────────────────────────────────────────────────
async def _moments_call(handler_name, path, method="GET", **kw):
from types import SimpleNamespace
from quart import Quart, g
from grimoire.routes import retrieval as routes
app = Quart(__name__)
async with app.test_request_context(path, method=method, **kw):
g.user = SimpleNamespace(id=7)
resp = await getattr(routes, handler_name).__wrapped__()
if isinstance(resp, tuple):
return await resp[0].get_json(), resp[1]
return await resp.get_json(), 200
async def test_the_view_gets_the_catalog_with_counts_beside_it():
from unittest.mock import AsyncMock, patch
from grimoire.routes import retrieval as routes
usage = AsyncMock(return_value={"by_moment": {"work.finish": {"delivered": 2}}})
with patch.object(routes.moment_actions_svc, "actions_by_moment",
AsyncMock(return_value={"actions": {}, "removed_defaults": []})), \
patch.object(routes.rulebooks_svc, "mount_counts", AsyncMock(return_value={"work.finish": 1})), \
patch.object(routes, "moment_usage", usage):
body, status = await _moments_call("moments_route", "/api/retrieval/moments",
query_string={"days": "7"})
assert status == 200
assert body["mounted"] == {"work.finish": 1}
assert body["usage"]["days"] == 7
assert body["usage"]["by_moment"]["work.finish"]["delivered"] == 2
assert {m["name"] for m in body["moments"]} >= {"work.finish", "reply.report"}
async def test_a_failed_mount_count_is_flagged_not_shown_as_nothing():
from unittest.mock import AsyncMock, patch
from grimoire.routes import retrieval as routes
with patch.object(routes.moment_actions_svc, "actions_by_moment",
AsyncMock(return_value={"actions": {}, "removed_defaults": []})), \
patch.object(routes.rulebooks_svc, "mount_counts", AsyncMock(side_effect=RuntimeError("down"))), \
patch.object(routes, "moment_usage", AsyncMock(return_value={"by_moment": {}})):
body, status = await _moments_call("moments_route", "/api/retrieval/moments")
assert status == 200
assert body["mounted"] == {} and body["mounted_failed"] is True
assert body["usage"]["days"] == 30
async def test_a_mapping_removal_can_come_as_query_parameters():
"""The browser's DELETE carries no body (api/client.apiDelete), so the
mapping it removes arrives in the query string."""
from unittest.mock import AsyncMock, patch
from grimoire.routes import retrieval as routes
unmap = AsyncMock(return_value={"change": "removed this install's mapping"})
with patch.object(routes.moment_actions_svc, "unmap_action", unmap):
body, status = await _moments_call(
"unmap_action_route", "/api/retrieval/moments/mappings", method="DELETE",
query_string={"tool": "Bash", "match": "make ship", "moment": "work.deliver"},
)
assert status == 200
assert unmap.await_args.args == (7, "Bash", "make ship", "work.deliver")
assert unmap.await_args.kwargs["actor"] == "human"
async def test_a_mapping_with_neither_body_nor_parameters_is_refused():
body, status = await _moments_call(
"unmap_action_route", "/api/retrieval/moments/mappings", method="DELETE",
)
assert status == 400