Files
LLeMbas/tests/test_suggestions.py
T
Homer c4aff999ba Files that outlived the chats that held them, and a page that led with its footnotes
The second audit pass. Four things, and the first two were reported.

The Prompts page put a screen of variables and a screen of preview above
the editor, so the tabs began two screens down and switching one had to
drag the whole page to be any use -- and on a short tab it could not drag
far enough, leaving the panel stranded above a screenful of nothing.
Editor first, reference after, bar sticky. Custom themes were three fixed
slots: fifty-seven empty colour boxes on a fresh instance and no way to
make a fourth theme. One block per theme plus a blank one, colours behind
a disclosure. Both measured rather than argued about -- rendered through
TestClient and driven under headless Chromium, where the tab bar moved
385->642px before and does not move now, and the themes page went from
5495px to 2820px.

Asking where generated images go found the other two. Deleting a chat
cascades to the attachment rows and leaves every file on disk; the helper
written for exactly that was called from one place, and it was not the
delete button, a schedule's chat, a helper's chat or deleting an account.
Underneath it, `claim` bound message_id and never chat_id, so anything
picked before a chat existed kept an empty chat_id forever -- which six
readers filter on, so those files were also unnamed in the prompt,
unopenable in the canvas, and invisible to the one caller the cleanup had.

And folders nest now. The route has handled parent_id since folders
existed, with a cycle guard and a depth cap the move path never applied;
the sidebar has always drawn a tree. Nothing could ask for one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:20:59 +02:00

194 lines
7.4 KiB
Python

"""Prompt suggestions: seeding, administration, and the cards."""
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import select
from lembas.db.models import Connection, Model, Suggestion, User
from lembas.services import settings_store
from lembas.services import suggestions as suggestions_service
from lembas.services.crypto import encrypt
@pytest.fixture
def clean(client: TestClient, db, registered):
"""Start from no suggestions.
The app's lifespan seeds the three defaults, so any test that goes through
`client` already has them. Tests about seeding want that; tests about the
admin screen want to count their own rows.
"""
for suggestion in suggestions_service.all_of_them(db):
db.delete(suggestion)
db.commit()
@pytest.fixture
def plain_user(client: TestClient, db, registered) -> User:
client.post("/auth/logout", follow_redirects=False)
client.post(
"/auth/register",
data={"name": "Sam", "email": "sam@shire.test", "password": "potatoes-po-ta-toes"},
follow_redirects=False,
)
return db.scalar(select(User).where(User.email == "sam@shire.test"))
def _connection(db) -> None:
connection = Connection(
name="Test", base_url="http://127.0.0.1:1", api_key_encrypted=encrypt("")
)
db.add(connection)
db.commit()
db.add(Model(connection_id=connection.id, model_id="test-model"))
db.commit()
# --- Seeding ------------------------------------------------------------------
def test_seeding_writes_the_defaults_once(db):
assert suggestions_service.seed_defaults(db) == len(suggestions_service.DEFAULTS)
assert suggestions_service.seed_defaults(db) == 0
assert len(suggestions_service.all_of_them(db)) == len(suggestions_service.DEFAULTS)
def test_deleting_every_suggestion_does_not_bring_them_back(db):
"""The guard is a flag, not "is the table empty". Otherwise an administrator
who decided against them gets them back on every restart."""
suggestions_service.seed_defaults(db)
for suggestion in suggestions_service.all_of_them(db):
db.delete(suggestion)
db.commit()
assert suggestions_service.seed_defaults(db) == 0
assert suggestions_service.all_of_them(db) == []
def test_the_seed_flag_lives_in_settings(db):
suggestions_service.seed_defaults(db)
assert settings_store.get(db, suggestions_service.SEEDED_KEY) is True
def test_every_default_prompt_stands_on_its_own():
"""A card is sent the instant it is clicked, with nothing added. One that
trails off waiting for material the person has not given is a prompt the
model has to guess at."""
for name, _description, prompt in suggestions_service.DEFAULTS:
assert prompt == prompt.strip(), f"{name} has stray whitespace"
# Any terminal punctuation, not a full stop. The point is that the
# prompt finishes rather than trails off, and a card that asks the model
# a direct question ends with "?" -- which this asserted was malformed.
assert prompt[-1] in ".?!", f"{name} does not end as a complete sentence"
# --- What is shown ------------------------------------------------------------
def test_only_enabled_ones_are_shown(db):
suggestions_service.create(db, name="Shown", prompt="a")
hidden = suggestions_service.create(db, name="Hidden", prompt="b")
hidden.enabled = False
db.commit()
assert [s.name for s in suggestions_service.visible(db)] == ["Shown"]
def test_position_decides_the_order(db):
first = suggestions_service.create(db, name="Zebra", prompt="a")
second = suggestions_service.create(db, name="Antelope", prompt="b")
first.position, second.position = 5, 1
db.commit()
assert [s.name for s in suggestions_service.visible(db)] == ["Antelope", "Zebra"]
def test_no_more_than_the_cap_is_shown(db):
for index in range(suggestions_service.MAX_SHOWN + 3):
suggestions_service.create(db, name=f"One {index}", prompt="x")
assert len(suggestions_service.visible(db)) == suggestions_service.MAX_SHOWN
def test_new_rows_land_at_the_end(db):
suggestions_service.create(db, name="First", prompt="a")
assert suggestions_service.create(db, name="Second", prompt="b").position == 1
# --- The cards ----------------------------------------------------------------
def test_the_cards_appear_on_the_new_chat_screen(client: TestClient, db, registered):
_connection(db)
suggestions_service.create(db, name="Explain this", description="Plain language.", prompt="Go ")
page = client.get("/chat").text
assert "Explain this" in page
assert "Plain language." in page
assert 'data-suggestion="Go "' in page
def test_the_cards_do_not_appear_in_an_existing_empty_chat(
client: TestClient, db, registered, make_chat
):
"""A chat someone opened on purpose already has a model and a prompt
chosen; the cards are for the screen where nothing has been decided."""
_connection(db)
suggestions_service.create(db, name="Explain this", prompt="Go ")
chat_id = make_chat()
assert "data-suggestion" not in client.get(f"/chat/{chat_id}").text
def test_a_prompt_with_quotes_is_escaped_in_the_attribute(client: TestClient, db, registered):
_connection(db)
suggestions_service.create(db, name="Tricky", prompt='say "<script>alert(1)</script>"')
page = client.get("/chat").text
assert "<script>alert(1)</script>" not in page
assert "&lt;script&gt;" in page
# --- Administration -----------------------------------------------------------
def test_the_admin_page_is_refused_to_a_plain_user(client: TestClient, plain_user):
assert client.get("/admin/suggestions").status_code == 403
assert client.post("/admin/suggestions", data={"name": "x"}).status_code == 403
def test_an_admin_can_create_edit_and_delete(client: TestClient, db, registered, clean):
client.post(
"/admin/suggestions",
data={"name": "Explain this", "description": "Plain language.", "prompt": "Go "},
follow_redirects=False,
)
suggestion = db.scalar(select(Suggestion))
assert suggestion.name == "Explain this"
assert suggestion.enabled is True
client.post(
f"/admin/suggestions/{suggestion.id}",
data={"name": "Renamed", "description": "", "prompt": "New ", "position": "3"},
follow_redirects=False,
)
db.refresh(suggestion)
assert suggestion.name == "Renamed"
assert suggestion.prompt == "New "
# An unticked checkbox is simply absent from the post, which is the signal.
assert suggestion.enabled is False
assert suggestion.position == 2
client.post(f"/admin/suggestions/{suggestion.id}/delete", follow_redirects=False)
assert db.scalar(select(Suggestion)) is None
def test_a_nameless_suggestion_is_refused(client: TestClient, db, registered, clean):
client.post("/admin/suggestions", data={"name": " "}, follow_redirects=False)
assert db.scalar(select(Suggestion)) is None
def test_the_limit_is_enforced(client: TestClient, db, registered, clean):
for index in range(suggestions_service.MAX_SUGGESTIONS):
suggestions_service.create(db, name=f"One {index}", prompt="x")
client.post("/admin/suggestions", data={"name": "One too many"}, follow_redirects=False)
assert len(suggestions_service.all_of_them(db)) == suggestions_service.MAX_SUGGESTIONS
def test_editing_something_that_is_gone_is_a_404(client: TestClient, registered):
assert client.post("/admin/suggestions/nope", data={"name": "x"}).status_code == 404