CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m2s
CI & Build / Python tests (push) Successful in 2m17s
CI & Build / Build & push image (push) Successful in 56s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
215 lines
9.0 KiB
Python
215 lines
9.0 KiB
Python
"""The operator's half of the tuning surface (#4102).
|
|
|
|
WHY THIS EXISTS
|
|
|
|
The model moves these dials — that is the operator's decision for milestone
|
|
416 — so the browser's job is no longer "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 scribe.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 scribe.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 scribe.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",
|
|
}
|
|
|
|
|
|
def test_the_browser_and_the_agent_call_the_same_service():
|
|
"""Rule 33 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 scribe.mcp.tools import retrieval_tuning as tool
|
|
from scribe.routes import retrieval as routes
|
|
from scribe.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 scribe.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 scribe.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 scribe.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 scribe.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 scribe.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 (milestone 458 step 6) ─────────────────────────────
|
|
|
|
async def _moments_call(handler_name, path, method="GET", **kw):
|
|
from types import SimpleNamespace
|
|
|
|
from quart import Quart, g
|
|
|
|
from scribe.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 scribe.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 scribe.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 scribe.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
|