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:
@@ -57,6 +57,7 @@ def test_suggestions_honors_bor_suggestions_env_override(monkeypatch) -> None:
|
||||
("/sources.html", "Knowledge base"),
|
||||
("/document.html", "Brain of Reese"), # phase 10: viewer page
|
||||
("/login.html", "Sign in"), # phase 16: admin sign-in page
|
||||
("/tuning.html", "Global Tuning"), # phase 27: global tuning page
|
||||
],
|
||||
)
|
||||
def test_html_pages_served_locally_no_cdn(client, path: str, marker: str) -> None:
|
||||
@@ -78,6 +79,7 @@ def test_styles_and_js_served(client) -> None:
|
||||
assert client.get("/assets/document.js").status_code == 200 # phase 10: viewer page
|
||||
assert client.get("/assets/login.js").status_code == 200 # phase 16: login page
|
||||
assert client.get("/assets/document-modal.js").status_code == 200 # phase 26: modal module
|
||||
assert client.get("/assets/tuning.js").status_code == 200 # phase 27: tuning page
|
||||
|
||||
|
||||
# Emoji code points banned from UI chrome (phase 08): the pictograph
|
||||
@@ -108,6 +110,7 @@ def _find_emoji(text: str) -> list[str]:
|
||||
"/sources.html",
|
||||
"/document.html",
|
||||
"/login.html", # phase 16
|
||||
"/tuning.html", # phase 27
|
||||
"/assets/app.js",
|
||||
"/assets/sources.js",
|
||||
"/assets/markdown.js",
|
||||
|
||||
@@ -22,9 +22,11 @@ from sqlalchemy import select, text
|
||||
from test_chat_api import FakeRagLLM, _stream_chat
|
||||
|
||||
from app.api import chat as chat_api
|
||||
from app.api.steering import load_steering_notes
|
||||
from app.main import app as fastapi_app
|
||||
from app.models import SteeringNote
|
||||
from app.rag.importer import import_sources
|
||||
from app.rag.prompts import build_steering_section
|
||||
|
||||
FIXTURES = Path(__file__).resolve().parents[1] / "fixtures" / "docs"
|
||||
QUESTION = "How is my Kubernetes cluster set up?"
|
||||
@@ -131,6 +133,92 @@ def test_create_enforces_2000_char_limit(admin_client: TestClient) -> None:
|
||||
assert len(r.json()["note"]) == 2000
|
||||
|
||||
|
||||
# ---------- update (PUT) — phase 27: edit a note in place ----------
|
||||
|
||||
|
||||
def test_update_note_returns_200_and_replaces_text(admin_client: TestClient, db) -> None:
|
||||
created = admin_client.post("/api/steering", json={"note": NOTE}).json()
|
||||
|
||||
r = admin_client.put(f"/api/steering/{created['id']}", json={"note": f" {NOTE}-v2 "})
|
||||
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["id"] == created["id"]
|
||||
assert body["note"] == f"{NOTE}-v2" # trimmed, full replacement
|
||||
# Editing does not redate the note.
|
||||
assert datetime.fromisoformat(body["created_at"]) == datetime.fromisoformat(
|
||||
created["created_at"]
|
||||
)
|
||||
row = db.get(SteeringNote, uuid.UUID(created["id"]))
|
||||
assert row is not None
|
||||
assert row.note == f"{NOTE}-v2"
|
||||
|
||||
|
||||
def test_update_is_reflected_in_list_load_and_prompt(admin_client: TestClient, db) -> None:
|
||||
base = datetime.now(UTC)
|
||||
db.add_all(
|
||||
[
|
||||
SteeringNote(note="oldest note", created_at=base),
|
||||
SteeringNote(note="newest note", created_at=base + timedelta(hours=1)),
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
oldest = db.scalars(select(SteeringNote).order_by(SteeringNote.created_at.asc())).first()
|
||||
assert oldest is not None
|
||||
updated = "oldest note — updated"
|
||||
assert admin_client.put(f"/api/steering/{oldest.id}", json={"note": updated}).status_code == 200
|
||||
|
||||
# The GET list keeps its newest-first order and carries the new text.
|
||||
body = admin_client.get("/api/steering").json()
|
||||
assert [n["note"] for n in body["notes"]] == ["newest note", updated]
|
||||
|
||||
# The chat path reads the updated note back, oldest first… (expire the
|
||||
# test session's identity map so the fresh DB values are loaded).
|
||||
db.expire_all()
|
||||
notes = load_steering_notes(db)
|
||||
assert notes == [updated, "newest note"]
|
||||
|
||||
# …and it appears, in order, in the <tuning> prompt section.
|
||||
section = build_steering_section(notes)
|
||||
assert f"1. {updated}" in section
|
||||
assert "2. newest note" in section
|
||||
assert section.index("1. ") < section.index("2. ")
|
||||
|
||||
|
||||
def test_update_unknown_note_returns_404(admin_client: TestClient) -> None:
|
||||
r = admin_client.put(f"/api/steering/{uuid.uuid4()}", json={"note": "x"})
|
||||
assert r.status_code == 404
|
||||
assert r.json() == {"detail": "steering note not found"}
|
||||
|
||||
|
||||
def test_update_rejects_invalid_bodies(admin_client: TestClient) -> None:
|
||||
created = admin_client.post("/api/steering", json={"note": NOTE}).json()
|
||||
note_id = created["id"]
|
||||
assert admin_client.put(f"/api/steering/{note_id}", json={"note": ""}).status_code == 422
|
||||
assert admin_client.put(f"/api/steering/{note_id}", json={"note": " "}).status_code == 422
|
||||
assert (
|
||||
admin_client.put(f"/api/steering/{note_id}", json={"note": "x" * 2001}).status_code == 422
|
||||
)
|
||||
# The 2000-char boundary passes and is stored as-is.
|
||||
r = admin_client.put(f"/api/steering/{note_id}", json={"note": "y" * 2000})
|
||||
assert r.status_code == 200
|
||||
assert len(r.json()["note"]) == 2000
|
||||
|
||||
|
||||
def test_update_anonymous_returns_403(admin_client: TestClient, db) -> None:
|
||||
created = admin_client.post("/api/steering", json={"note": NOTE}).json()
|
||||
anon = TestClient(fastapi_app) # fresh jar: truly anonymous
|
||||
|
||||
r = anon.put(f"/api/steering/{created['id']}", json={"note": "anonymous edit"})
|
||||
|
||||
assert r.status_code == 403
|
||||
assert r.json() == {"detail": "admin only"}
|
||||
# The text is untouched, in both the API and the chat path.
|
||||
body = admin_client.get("/api/steering").json()
|
||||
assert [n["note"] for n in body["notes"]] == [NOTE]
|
||||
assert load_steering_notes(db) == [NOTE]
|
||||
|
||||
|
||||
# ---------- chat turn: note reaches the system prompt ----------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user