c4aff999ba
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>
194 lines
7.4 KiB
Python
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 "<script>" 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
|