Files
FabledScribe/tests/test_settings_defaults_agree.py
T
bvandeusenandClaude Opus 5 003bfd7a0a
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 43s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Failing after 1m2s
CI & Build / Build & push image (push) Skipped
fix(tests): two readers moved, and the settings guard now checks the registry (#4102)
CI on 09b4845 caught both, and the second is an improvement rather than a
repair.

`test_services_reply_preferences` patched `reply_preferences.get_setting`,
which the registry refactor removed — its floor and budget are resolved through
`retrieval_surfaces` now. Its key-independence test also asserted the arm asks
for exactly one key; it asks for two, because both of its numbers are its own
since this step, so the assertion names both and adds a check that the registry
and the module constant still agree about the floor key. They writing different
keys is the failure where the Settings form saves one string and the arm reads
another.

`test_settings_defaults_agree` parsed `plugin_context.py` for a bare
module-level float, and those constants now alias the registry. Rather than
teach the regex about aliases, the six retrieval floors are keyed on their
SURFACE NAME and read from the registry directly — which is strictly better for
this guard: a surface name is also its telemetry source, so a row names the same
arm the readout does, and a floor cannot be checked against a stale constant
that happened to keep its old value. `PLAN_MATCH_DEFAULT_THRESHOLD` is not a
push surface and keeps the older shape, with a note saying why.

Added while there: `test_every_tunable_surface_has_a_control`, derived from the
registry, so a seventh surface arrives as a failing test rather than as a number
only the model can reach (rules 25, 27).

Also lands the audit trail this step needs — `retrieval_tuning_events` (model +
migration 0103) and `services/retrieval_tuning.py`. The tool layer on top is
the next commit; the table is here because `reason` being REQUIRED is the whole
guardrail, and the schema is where that starts. A number moved silently leaves
nothing for the operator to review or disagree with, and the operator's decision
is that the model moves these "9 times out of 10".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
2026-09-17 11:20:47 -04:00

134 lines
5.9 KiB
Python

"""The Settings UI shows the default the server actually uses (#3927).
WHY THIS EXISTS
A retrieval threshold lives in two places by necessity: a Python constant the
arm reads when no row is stored, and a Vue `ref` the Settings form shows when
the operator has never touched it. Neither can import the other.
So the form's initial value is a CLAIM about the server's behaviour, and it is
the kind of claim that rots quietly. Retune the Python constant and the input
keeps rendering the old number — the operator reads it as the bar in force,
sees no reason to change anything, and the form has misinformed them about the
one fact it exists to convey. Nothing errors, nothing looks wrong, and the
value they are shown is simply not the value being applied.
WHAT THIS PINS
The RELATIONSHIP, never the number: for each threshold, the Vue ref's initial
string equals the Python default. Retuning either stays free as long as both
move — which is the point, because these are tuning values and #3853 moved one
of them the day this was written.
The Vue side is read from SOURCE, because that file cannot be imported at all.
The Python side is read from source for a loose constant and IMPORTED for a
registry surface — the registry is a plain table of frozen dataclasses with
nothing to trigger on import, and importing it means this guard checks the value
the server will actually resolve rather than a literal that happens to look
right.
It cannot check that the form WRITES the right key — that is behaviour, and
the keys are asserted where they are built. It catches the drift that has no
other alarm.
"""
from __future__ import annotations
import pathlib
import re
import pytest
ROOT = pathlib.Path(__file__).resolve().parents[1]
_SERVICES = ROOT / "src" / "scribe" / "services"
_VUE = ROOT / "frontend" / "src" / "views" / "SettingsView.vue"
# THE SIX RETRIEVAL FLOORS now live in one registry (#4102), so their side of
# the pairing is a SURFACE NAME rather than a module constant. That is strictly
# better for this guard: the surface name is also the telemetry source, so a row
# here names the same arm the readout does, and a floor that moved cannot be
# checked against a stale constant that happened to keep its old value.
_SURFACE_PAIRS = (
("auto_inject", "kbInjectThreshold"),
("write_path", "kbWritePathThreshold"),
("write_path_rule", "kbRuleHintThreshold"),
("pre_tool_rule", "kbToolRuleThreshold"),
("prompt_rule", "kbPromptRuleThreshold"),
("report_preference", "kbReportPrefThreshold"),
)
# (services module, python constant, vue ref) for the defaults that are NOT
# retrieval surfaces. Hand-written because the pairing is an editorial fact —
# the names share no convention either side could derive — and asserted to
# exist on both sides, so a rename fails loudly rather than silently dropping
# that threshold from the check.
_CONSTANT_PAIRS = (
# The plan gate (milestone 415): it blocks a create, so a form showing a
# looser bar than the one in force would be the more misleading drift. Not
# a push surface, so it has no registry entry and keeps the older shape.
("dedup.py", "PLAN_MATCH_DEFAULT_THRESHOLD", "kbPlanMatchThreshold"),
)
def _python_default(module: str, name: str) -> float:
m = re.search(rf"^{re.escape(name)}\s*=\s*([0-9.]+)\s*$",
(_SERVICES / module).read_text(), re.M)
assert m, (
f"{name} is no longer a bare module-level float in "
f"services/{module}. If it moved or was renamed, update "
f"_PAIRS; if it was retired, drop its row — leaving it here checks "
f"nothing while looking like coverage."
)
return float(m.group(1))
def _vue_default(ref_name: str) -> float:
m = re.search(rf"const {re.escape(ref_name)} = ref\(\"([0-9.]+)\"\)",
_VUE.read_text())
assert m, (
f"{ref_name} is no longer a `ref(\"<number>\")` in SettingsView.vue. "
f"If the control was renamed, update _PAIRS; if it was removed, the "
f"setting has lost its UI and that is the thing to fix (rule 25)."
)
return float(m.group(1))
def _assert_agrees(server: float, form: float, ref_name: str, what: str) -> None:
assert form == server, (
f"SettingsView shows {form} for {ref_name} while the server defaults "
f"to {server} ({what}). An operator who has never set this reads the "
f"form as the value in force, so the two must move together — retune "
f"both, or neither."
)
@pytest.mark.parametrize(("surface", "ref_name"), _SURFACE_PAIRS,
ids=[p[0] for p in _SURFACE_PAIRS])
def test_the_form_shows_the_floor_the_surface_uses(surface, ref_name):
"""An untouched control must render the bar actually in force."""
from scribe.services.retrieval_surfaces import get_surface
_assert_agrees(get_surface(surface).floor_default, _vue_default(ref_name),
ref_name, f"{surface} floor")
def test_every_tunable_surface_has_a_control():
"""Rule 25/27: a number an operator may need different has a UI or it is
not shipped. Derived from the registry, so a seventh surface arrives here
as a failure rather than as a setting only the model can reach."""
from scribe.services.retrieval_surfaces import surface_names
covered = {s for s, _ref in _SURFACE_PAIRS}
missing = [n for n in surface_names() if n not in covered]
assert not missing, (
f"these surfaces are tunable with no Settings control: {missing}. "
"Add the control and its row, or say in _SURFACE_PAIRS why it has none."
)
@pytest.mark.parametrize(("module", "constant", "ref_name"), _CONSTANT_PAIRS,
ids=[p[1] for p in _CONSTANT_PAIRS])
def test_the_form_shows_the_default_the_server_uses(module, constant, ref_name):
"""Same claim, for the defaults that are not retrieval surfaces."""
_assert_agrees(_python_default(module, constant), _vue_default(ref_name),
ref_name, constant)