Files
FabledScribe/tests/test_settings_defaults_agree.py
T
bvandeusenandClaude Opus 5 59407728e6
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 1m10s
CI & Build / TypeScript typecheck (push) Successful in 1m15s
CI & Build / Python tests (push) Failing after 1m22s
CI & Build / Build & push image (push) Skipped
feat(planning): start_planning hands back the active plan that already covers the work (#4079)
Step 4 of milestone 415 "An existing plan is found before a new one is made".
A session that could not see an existing plan made a second one beside it.
start_planning and create_milestone now ask first: an ACTIVE milestone in the
project with the same title, or one that reads as the same plan (title, design
and steps against milestone embeddings), is returned with its progress and a
pointer to create_records(milestone_id=...). Nothing is created; force=true
bypasses.

- dedup.find_matching_plan / plan_gate / plan_match_response; access-checked
  before either arm (rule 78), fail-open like the other gates.
- Done milestones never block; the semantic arm needs 200+ chars of candidate.
- kb_plan_match_threshold (default 0.90) is a setting, in the Settings view,
  and pinned against the Python default by test_settings_defaults_agree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
2026-09-15 13:43:18 -04:00

93 lines
4.1 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.
Both sides are read from SOURCE rather than imported. The Vue file cannot be
imported at all, and reading the Python constant through an import would tie
this to module-load side effects it has no interest in.
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"
# (services module, python constant, vue ref). Hand-written because the
# pairing is an editorial fact — the names do not share a convention either
# side could derive — but every entry is asserted to EXIST on both sides, so a
# rename fails loudly here rather than silently dropping that threshold from
# the check.
_PAIRS = (
("plugin_context.py", "AUTOINJECT_DEFAULT_THRESHOLD", "kbInjectThreshold"),
("plugin_context.py", "WRITEPATH_DEFAULT_THRESHOLD", "kbWritePathThreshold"),
("plugin_context.py", "RULEHINT_DEFAULT_THRESHOLD", "kbRuleHintThreshold"),
("plugin_context.py", "TOOLRULE_DEFAULT_THRESHOLD", "kbToolRuleThreshold"),
("plugin_context.py", "PROMPTRULE_DEFAULT_THRESHOLD", "kbPromptRuleThreshold"),
# 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.
("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))
@pytest.mark.parametrize(("module", "constant", "ref_name"), _PAIRS,
ids=[p[1] for p in _PAIRS])
def test_the_form_shows_the_default_the_server_uses(module, constant, ref_name):
"""An untouched control must render the bar actually in force."""
server, form = _python_default(module, constant), _vue_default(ref_name)
assert form == server, (
f"SettingsView shows {form} for {ref_name} while the server defaults "
f"to {server} ({constant}). An operator who has never set this reads "
f"the form as the value in force, so the two must move together — "
f"retune both, or neither."
)