feat(rag): global tuning manager — /tuning.html + PUT /api/steering/{id}: create, edit, list, delete steering notes without a chat

This commit is contained in:
2026-08-25 13:46:32 -04:00
parent fcde1fd37b
commit 589e26dbe9
16 changed files with 1697 additions and 21 deletions
+29 -8
View File
@@ -20,12 +20,14 @@ APP_JS = ASSETS / "app.js"
SOURCES_JS = ASSETS / "sources.js"
DOCUMENT_JS = ASSETS / "document.js"
LOGIN_JS = ASSETS / "login.js"
TUNING_JS = ASSETS / "tuning.js"
STYLES_CSS = ASSETS / "styles.css"
INDEX_HTML = FRONTEND / "index.html"
SOURCES_HTML = FRONTEND / "sources.html"
DOCUMENT_HTML = FRONTEND / "document.html"
LOGIN_HTML = FRONTEND / "login.html"
TUNING_HTML = FRONTEND / "tuning.html"
def _text(path: Path) -> str:
@@ -74,7 +76,7 @@ def test_init_shared_header_toggles_only_elements_that_exist() -> None:
assert fn != -1
body = js[fn : js.find("\n}", fn)]
assert "await fetchIsAdmin()" in body
for selector in ('#sign-in-link', '#sign-out-btn', '#nav-sources'):
for selector in ("#sign-in-link", "#sign-out-btn", "#nav-sources", "#nav-tuning"):
assert f'querySelector("{selector}")' in body
assert "return admin" in body, "callers may reuse the flag"
@@ -111,13 +113,32 @@ def test_nav_sources_ships_hidden_on_every_nav_page() -> None:
nav link is hidden for anonymous — so it SHIPS with the hidden
attribute (anonymous-safe default) on every page that has a nav
(chat, sources, login)."""
for html in (INDEX_HTML, SOURCES_HTML, LOGIN_HTML):
for html in (INDEX_HTML, SOURCES_HTML, LOGIN_HTML, TUNING_HTML):
text = _text(html)
assert re.search(r'id="nav-sources"[^>]*\bhidden\b', text), (
f"{html.name}: #nav-sources must ship hidden"
)
def test_nav_tuning_ships_hidden_on_the_tuning_page() -> None:
"""Phase 27: the Global Tuning page reuses the shared header — the
"Tuning" nav link is admin-only, so it SHIPS hidden (revealed by
initSharedHeader once whoami says admin), is the page's active link
(is-active + aria-current), and the page loads markdown.js (classic)
+ the tuning.js module with NO direct header.js <script> tag
(single-evaluation design)."""
text = _text(TUNING_HTML)
tag = re.search(r'<a[^>]*id="nav-tuning"[^>]*>', text)
assert tag, "tuning.html must carry the #nav-tuning nav link"
assert 'class="nav-link is-active"' in tag.group(0), "the Tuning link is the active one"
assert 'aria-current="page"' in tag.group(0)
assert "hidden" in tag.group(0), "#nav-tuning must ship hidden (admin-only)"
srcs = _script_srcs(TUNING_HTML)
assert [s for s in srcs if "header.js" in s] == [], "no direct header.js <script> tag"
assert [s for s in srcs if "markdown.js" in s]
assert [s for s in srcs if "tuning.js" in s]
def test_nav_sources_is_absent_from_the_viewer() -> None:
"""The document viewer has no nav — no #nav-sources element there (the
module's missing-element no-op keeps it out)."""
@@ -229,12 +250,12 @@ def test_login_js_uses_the_shared_fetch_is_admin() -> None:
def test_non_chat_pages_bind_new_chat_to_the_chat_page() -> None:
"""On sources and the viewer, New Chat means "go to the chat,
fresh": the binding clears the phase-14 key (clearChatStorage) and
navigates to "/" — and both pages run initSharedHeader() at boot
on the shared cached whoami. Phase 23: the import is relative
(`./header.js`)."""
for js_file in (SOURCES_JS, DOCUMENT_JS):
"""On sources, the viewer, and the tuning page, New Chat means "go to
the chat, fresh": the binding clears the phase-14 key
(clearChatStorage) and navigates to "/" — and each page runs
initSharedHeader() at boot on the shared cached whoami. Phase 23:
the import is relative (`./header.js`)."""
for js_file in (SOURCES_JS, DOCUMENT_JS, TUNING_JS):
js = _text(js_file)
assert 'from "./header.js"' in js
assert "initSharedHeader()" in js
+139
View File
@@ -0,0 +1,139 @@
"""Unit: steering notes PUT endpoint (phase 27 — Global Tuning).
``PUT /api/steering/{note_id}`` is driven **via the router** with a
stubbed session (FastAPI dependency override — no Postgres required):
200 with the updated note (trimmed, ``created_at`` preserved), 404 for an
unknown id, 422 for empty/whitespace/over-2000 bodies, and the
router-level 403 for anonymous callers (the real phase-16
``require_admin`` gate — no auth surface added by the PUT route itself).
"""
from __future__ import annotations
import uuid
from datetime import UTC, datetime
from fastapi.testclient import TestClient
from app.config import get_settings
from app.db import get_db
from app.main import create_app
from app.models import SteeringNote
CREATED_AT = datetime(2026, 8, 24, 12, 0, 0, tzinfo=UTC)
class _FakeSession:
"""Just enough of a SQLAlchemy session for the PUT route."""
def __init__(self, rows: dict[uuid.UUID, SteeringNote]) -> None:
self.rows = rows
self.commits = 0
def get(self, _model: object, pk: object) -> SteeringNote | None:
if isinstance(pk, uuid.UUID):
return self.rows.get(pk)
return None
def commit(self) -> None:
self.commits += 1
def refresh(self, _row: object) -> None:
pass
def _note() -> SteeringNote:
return SteeringNote(id=uuid.uuid4(), note="be concise", created_at=CREATED_AT)
def _put_client(rows: dict[uuid.UUID, SteeringNote]) -> tuple[TestClient, _FakeSession]:
"""A fresh app whose ``get_db`` is a stub holding ``rows``."""
session = _FakeSession(rows)
app = create_app()
app.dependency_overrides[get_db] = lambda: session
return TestClient(app), session
def _sign_in(client: TestClient) -> None:
"""Real login route (no DB): sets the signed admin session cookie."""
r = client.post("/api/login", json={"password": get_settings().admin_password})
assert r.status_code == 204, f"admin login failed: {r.status_code} {r.text}"
# ---------- 200: update in place ----------
def test_update_note_returns_200_with_new_text() -> None:
note = _note()
client, session = _put_client({note.id: note})
_sign_in(client)
r = client.put(f"/api/steering/{note.id}", json={"note": " be MORE concise "})
assert r.status_code == 200
body = r.json()
assert body["note"] == "be MORE concise" # trimmed before storage
assert body["id"] == str(note.id)
assert datetime.fromisoformat(body["created_at"]) == CREATED_AT # preserved, not redated
assert note.note == "be MORE concise" # updated in place
assert session.commits == 1
def test_update_accepts_full_length_note() -> None:
note = _note()
client, _session = _put_client({note.id: note})
_sign_in(client)
r = client.put(f"/api/steering/{note.id}", json={"note": "x" * 2000})
assert r.status_code == 200
assert len(r.json()["note"]) == 2000
# ---------- 404 / 422 ----------
def test_update_unknown_id_returns_404() -> None:
note = _note()
client, _session = _put_client({note.id: note})
_sign_in(client)
r = client.put(f"/api/steering/{uuid.uuid4()}", json={"note": "anything"})
assert r.status_code == 404
assert r.json() == {"detail": "steering note not found"}
assert note.note == "be concise" # untouched
def test_update_invalid_id_returns_422() -> None:
note = _note()
client, _session = _put_client({note.id: note})
_sign_in(client)
assert client.put("/api/steering/not-a-uuid", json={"note": "x"}).status_code == 422
def test_update_rejects_empty_blank_and_overlong_bodies() -> None:
note = _note()
client, session = _put_client({note.id: note})
_sign_in(client)
assert client.put(f"/api/steering/{note.id}", json={"note": ""}).status_code == 422
assert client.put(f"/api/steering/{note.id}", json={"note": " \t\n "}).status_code == 422
assert client.put(f"/api/steering/{note.id}", json={"note": "x" * 2001}).status_code == 422
assert note.note == "be concise" # rejected bodies never touch the row
assert session.commits == 0
# ---------- 403: router-level require_admin (phase 16) ----------
def test_update_anonymous_returns_403_admin_only() -> None:
note = _note()
client, session = _put_client({note.id: note})
r = client.put(f"/api/steering/{note.id}", json={"note": "sneaky"})
assert r.status_code == 403
assert r.json() == {"detail": "admin only"} # the require_admin message
assert note.note == "be concise" # anonymous callers never mutate
assert session.commits == 0