Files
LLeMbas/tests/test_folders.py
T
Jaroslav Beneš b602657450 A folder that carries something, and a way to name one
A folder was a name and nothing else -- and not even that, since PATCH could
rename one and nothing in the interface ever called it. It now carries a
description, a system prompt, and seeds for the model, the kind and the agent
target, with a settings page behind the row.

The prompt is a fourth rung on the ladder, chat > folder > model > instance,
and it goes above the model deliberately: a model's prompt describes the model
wherever it is used, a folder's describes this piece of work whichever model
is pointed at it. It is read when a reply is built rather than copied when a
chat is made, so editing it reaches the chats already there, and the walk up
the parents is bounded and cycle-safe because it runs on the request path.
`api/pages.py` mirrors the ladder for the settings panel and had to gain the
same rung -- a panel naming the wrong source is worse than one naming none,
because it is believed.

The seeds fill in what the request left empty and nothing it filled in: the
folder says what this work usually needs, the screen in front of somebody says
what they want this time. `ssh_profile_id` is a plain string rather than a
foreign key, for the reason `compacted_through_id` is, so it is validated on
read.

Getting *into* a folder needed fixing too. `/api/chats/start` has accepted a
folder_id since folders existed and nothing ever sent one, so the only route in
was to make the chat elsewhere and move it. There is a New chat here on the row
now, and `?folder=` on the new-chat screen.

Naming is a themed dialog, and deliberately not htmx's hx-prompt: htmx calls
the browser's prompt() synchronously and only then fires htmx:prompt with the
answer already in hand, so intercepting the event cannot supply a different one
and the grey box appears anyway. `data-prompt` follows the data-confirm-button
shape instead -- swallow the click, ask, write the answer into hx-vals,
click again behind a guard. JSON.stringify rather than concatenation, or a
folder called `"` produces hx-vals that does not parse and the rename silently
does nothing. Driven under a DOM stub, and there is a test that no template
brings hx-prompt back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 08:30:49 +02:00

312 lines
12 KiB
Python

"""What a folder carries, and what the chats inside it inherit from it."""
from __future__ import annotations
from fastapi.testclient import TestClient
from sqlalchemy import select
from lembas.db.models import Chat, Connection, Folder, Model, SshProfile, User
from lembas.services import chat as chat_service
from lembas.services import settings_store
from lembas.services.crypto import encrypt
def _add_connection(db) -> Connection:
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()
return connection
def _folder(db, name: str, parent: Folder | None = None, **fields) -> Folder:
user = db.scalars(select(User).order_by(User.created_at)).first()
folder = Folder(
user_id=user.id, name=name, parent_id=parent.id if parent else None, **fields
)
db.add(folder)
db.commit()
return folder
# --- Naming and renaming -------------------------------------------------------
def test_a_folder_is_created_with_the_name_that_was_asked_for(
client: TestClient, db, registered
):
client.post("/api/folders", data={"name": "Isengard"})
assert db.scalar(select(Folder)).name == "Isengard"
def test_a_folder_with_no_name_still_gets_one(client: TestClient, db, registered):
"""The button asks first, but a request that arrives without one must not
produce a folder with a blank label nobody can click."""
client.post("/api/folders", data={})
assert db.scalar(select(Folder)).name == "New folder"
def test_renaming_a_folder(client: TestClient, db, registered):
"""PATCH has been able to do this since folders existed and nothing in the
interface called it, so a folder could not be renamed at all."""
folder = _folder(db, "Isengard")
assert client.patch(f"/api/folders/{folder.id}", data={"name": "Orthanc"}).status_code == 204
db.refresh(folder)
assert folder.name == "Orthanc"
def test_a_blank_rename_is_ignored(client: TestClient, db, registered):
"""A folder nobody can see the name of is one nobody can find."""
folder = _folder(db, "Isengard")
client.patch(f"/api/folders/{folder.id}", data={"name": " "})
db.refresh(folder)
assert folder.name == "Isengard"
# --- The system prompt ladder ---------------------------------------------------
def test_a_folder_prompt_reaches_a_chat_inside_it(client: TestClient, db, registered, make_chat):
_add_connection(db)
folder = _folder(db, "Errands", system_prompt="Answer in the fewest words.")
chat = db.get(Chat, make_chat())
chat.folder_id = folder.id
db.commit()
assert chat_service.effective_system_prompt(db, chat) == "Answer in the fewest words."
def test_a_nested_folder_inherits_its_parents_prompt(
client: TestClient, db, registered, make_chat
):
"""A project's prompt belongs on the project, not on each sub-folder of it."""
_add_connection(db)
top = _folder(db, "Project", system_prompt="Answer in the fewest words.")
inner = _folder(db, "Notes", parent=top)
chat = db.get(Chat, make_chat())
chat.folder_id = inner.id
db.commit()
assert chat_service.effective_system_prompt(db, chat) == "Answer in the fewest words."
def test_the_nearest_folder_prompt_wins(client: TestClient, db, registered, make_chat):
_add_connection(db)
top = _folder(db, "Project", system_prompt="From the top.")
inner = _folder(db, "Notes", parent=top, system_prompt="From the sub-folder.")
chat = db.get(Chat, make_chat())
chat.folder_id = inner.id
db.commit()
assert chat_service.effective_system_prompt(db, chat) == "From the sub-folder."
def test_the_chats_own_prompt_still_wins(client: TestClient, db, registered, make_chat):
_add_connection(db)
folder = _folder(db, "Errands", system_prompt="From the folder.")
chat = db.get(Chat, make_chat())
chat.folder_id = folder.id
chat.system_prompt = "From the chat."
db.commit()
assert chat_service.effective_system_prompt(db, chat) == "From the chat."
def test_a_folder_prompt_beats_the_models(client: TestClient, db, registered, make_chat):
"""The folder is the more specific statement: a model's prompt describes the
model wherever it is used, a folder's describes this piece of work."""
_add_connection(db)
model = db.scalar(select(Model))
model.system_prompt = "From the model."
folder = _folder(db, "Errands", system_prompt="From the folder.")
chat = db.get(Chat, make_chat())
chat.folder_id = folder.id
db.commit()
assert chat_service.effective_system_prompt(db, chat) == "From the folder."
def test_an_empty_folder_prompt_falls_through(client: TestClient, db, registered, make_chat):
_add_connection(db)
model = db.scalar(select(Model))
model.system_prompt = "From the model."
folder = _folder(db, "Errands")
chat = db.get(Chat, make_chat())
chat.folder_id = folder.id
db.commit()
assert chat_service.effective_system_prompt(db, chat) == "From the model."
def test_the_panel_names_the_folder_as_the_source(client: TestClient, db, registered, make_chat):
"""The settings panel mirrors the ladder and has to keep mirroring it. One
naming the wrong source is worse than one naming none, because it is
believed."""
_add_connection(db)
folder = _folder(db, "Errands", system_prompt="Answer in the fewest words.")
chat_id = make_chat()
chat = db.get(Chat, chat_id)
chat.folder_id = folder.id
db.commit()
page = client.get(f"/chat/{chat_id}").text
assert "folder prompt" in page or "the folder" in page
# --- Seeds ----------------------------------------------------------------------
def test_a_folder_seeds_a_new_chats_model(client: TestClient, db, registered):
_add_connection(db)
db.add(Model(connection_id=db.scalar(select(Connection)).id, model_id="other-model"))
db.commit()
folder = _folder(db, "Errands", model_id="other-model")
client.post("/api/chats/start", data={"content": "Hello", "folder_id": folder.id})
chat = db.scalar(select(Chat))
assert chat.model_id == "other-model"
def test_an_explicit_choice_beats_the_folders_seed(client: TestClient, db, registered):
"""The folder says what this work usually needs; the screen in front of
somebody says what they want this time."""
_add_connection(db)
db.add(Model(connection_id=db.scalar(select(Connection)).id, model_id="other-model"))
db.commit()
folder = _folder(db, "Errands", model_id="other-model")
client.post(
"/api/chats/start",
data={"content": "Hello", "folder_id": folder.id, "model_id": "test-model"},
)
assert db.scalar(select(Chat)).model_id == "test-model"
def test_a_folder_belonging_to_someone_else_seeds_nothing(client: TestClient, db, registered):
_add_connection(db)
other = User(name="Sam", email="sam@shire.test", password_hash="x")
db.add(other)
db.commit()
folder = Folder(user_id=other.id, name="Theirs", model_id="other-model")
db.add(folder)
db.commit()
client.post("/api/chats/start", data={"content": "Hello", "folder_id": folder.id})
chat = db.scalar(select(Chat))
assert chat.model_id == "test-model"
def test_a_deleted_connection_on_a_folder_does_not_raise(client: TestClient, db, registered):
"""`ssh_profile_id` is a plain string, not a foreign key, so it can outlive
the profile it names. It is validated on read instead."""
_add_connection(db)
settings_store.update(db, {"enabled": True}, key=settings_store.AGENTS)
folder = _folder(db, "Errands", kind="agent", ssh_profile_id="gone")
response = client.post(
"/api/chats/start", data={"content": "Hello", "folder_id": folder.id}
)
assert response.status_code == 204
# No profile means no agent chat: `_agent_target` refuses rather than
# creating one pointed at nothing.
assert db.scalar(select(Chat)).kind == "chat"
def test_the_seeds_are_saved_and_cleared_through_one_route(client: TestClient, db, registered):
"""Every field clearable, which is what reading the raw form buys: with
`Form(None)` an empty box and an absent one are the same request."""
folder = _folder(db, "Errands")
client.patch(
f"/api/folders/{folder.id}",
data={
"description": "Work on the tower.",
"system_prompt": "Answer in the fewest words.",
"model_id": "test-model",
"kind": "agent",
},
)
db.refresh(folder)
assert folder.description == "Work on the tower."
assert folder.system_prompt == "Answer in the fewest words."
assert folder.kind == "agent"
client.patch(f"/api/folders/{folder.id}", data={"system_prompt": "", "kind": ""})
db.refresh(folder)
assert folder.system_prompt == ""
assert folder.kind == ""
# Untouched keys are left alone rather than blanked.
assert folder.description == "Work on the tower."
def test_a_kind_that_is_not_a_kind_is_dropped(client: TestClient, db, registered):
"""A folder seeding a kind that is not a kind hands every chat a value
`_new_chat` then has to ignore anyway."""
folder = _folder(db, "Errands")
client.patch(f"/api/folders/{folder.id}", data={"kind": "wizard", "agent_mode": "reckless"})
db.refresh(folder)
assert folder.kind == ""
assert folder.agent_mode == ""
# --- Getting into a folder at all -----------------------------------------------
def test_new_chat_here_files_the_chat(client: TestClient, db, registered):
"""`/api/chats/start` has accepted a folder_id since folders existed and
nothing ever sent one."""
_add_connection(db)
folder = _folder(db, "Errands")
page = client.get(f"/chat?folder={folder.id}").text
assert f'name="folder_id" value="{folder.id}"' in page
client.post("/api/chats/start", data={"content": "Hello", "folder_id": folder.id})
assert db.scalar(select(Chat)).folder_id == folder.id
def test_a_folder_fixed_to_agent_opens_the_new_chat_screen_on_that_fork(
client: TestClient, db, registered
):
_add_connection(db)
settings_store.update(db, {"enabled": True}, key=settings_store.AGENTS)
db.add(
SshProfile(
owner_id=db.scalars(select(User).order_by(User.created_at)).first().id,
name="Box",
host="example.test",
username="root",
host_key="ssh-ed25519 AAAA",
)
)
db.commit()
folder = _folder(db, "Errands", kind="agent")
page = client.get(f"/chat?folder={folder.id}").text
assert 'name="kind" value="agent"' in page
# --- The settings page ----------------------------------------------------------
def test_the_settings_page_renders_what_is_stored(client: TestClient, db, registered):
_add_connection(db)
folder = _folder(db, "Errands", system_prompt="Answer in the fewest words.")
page = client.get(f"/folders/{folder.id}").text
assert "Answer in the fewest words." in page
assert f'hx-patch="/api/folders/{folder.id}"' in page
def test_the_settings_page_refuses_someone_elses_folder(client: TestClient, db, registered):
other = User(name="Sam", email="sam@shire.test", password_hash="x")
db.add(other)
db.commit()
folder = Folder(user_id=other.id, name="Theirs")
db.add(folder)
db.commit()
assert client.get(f"/folders/{folder.id}").status_code == 404
def test_the_settings_form_posts_at_a_route_that_serves_patch(
client: TestClient, db, registered
):
"""A control wired to a method its route does not serve fails silently --
htmx surfaces nothing, so it looks exactly like a control that works."""
folder = _folder(db, "Errands")
assert client.post(f"/api/folders/{folder.id}", data={"name": "x"}).status_code == 405
assert client.patch(f"/api/folders/{folder.id}", data={"name": "x"}).status_code == 204