Files
LLeMbas/tests/test_folders.py
T
Jaroslav Beneš 20040f53a8 Three things that said one thing and did another
All three shipped in the last two commits, and all three are the same kind of
mistake: an interface that looks right and is not.

The folder settings page could not be scrolled. `.main` is a flex column with
`min-height: 0`, so a `.page` dropped straight into it overflows the viewport
with nothing to scroll -- Save and Back end up below the bottom of the window,
reachable by zooming out or by dragging the prompt textarea up out of the way.
Every other page of this shape already wraps its content in `.admin-scroll`;
this one did not. The two class names that scroll are one rule in admin.css
precisely so this is a wrapper somebody forgot rather than a value they got
wrong, and now it is noted.

The project directory was a text box, on the one screen that asks for an
absolute path on another machine. It is the same button-and-hidden-field the
new-chat screen uses, wired by `[data-dir-field]` in ui.js -- scoped to that
attribute so this and the composer's own handler cannot both answer one click
and open two dialogs. The composer keeps its own because it does more: it
follows the selected profile's default directory until somebody picks their
own, which only means something while a chat is being created. With no
connection chosen it says so rather than opening onto nothing, and Clear is
always there, because browsing somewhere and changing your mind before saving
needs a way back to "no opinion" as much as clearing a saved one does.

And "New chat" did not follow the Chat/Agent switch. The button sits above the
scroll area rather than inside the tree the switch swaps, so it went on saying
"New chat" over a list of agent chats. It moves to its own partial and arrives
out of band, the way the chat title already does. Renaming it to something
neutral would have hidden the bug rather than fixed it, and would have cost the
`?kind=agent` preselection the label is there to explain.

The tests that existed asserted a page load, which re-renders the button
anyway -- which is exactly why nobody saw it. The new ones assert the fragment.
The directory field was driven under a DOM stub first.

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

364 lines
14 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_can_be_scrolled(client: TestClient, db, registered):
"""`.main` is a flex column with `min-height: 0`, so a `.page` dropped
straight into it overflows the viewport with nothing to scroll and Save ends
up below the bottom of the window. Every other page of this shape wraps its
content in the scrolling container; this one did not."""
folder = _folder(db, "Errands")
page = client.get(f"/folders/{folder.id}").text
assert 'class="admin-scroll"' in page
assert page.index('class="admin-scroll"') < page.index('class="page"')
def test_the_project_directory_is_chosen_rather_than_typed(
client: TestClient, db, registered
):
"""The same control the new-chat screen uses, and for the reason it gives
there: a path on another machine is something you would rather find than
spell."""
_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", project_dir="/srv/project")
page = client.get(f"/folders/{folder.id}").text
assert "data-dir-field" in page
assert "data-dir-browse" in page
# The hidden field is what the form actually submits, so it must carry the
# stored value -- the button beside it only shows it.
assert 'name="project_dir"' in page
assert 'value="/srv/project"' in page
assert 'type="hidden"' in page
def test_the_directory_can_still_be_cleared(client: TestClient, db, registered):
"""A hidden input always submits, so an empty one means "no opinion" rather
than "leave it alone" -- which is the whole reason `update_folder` reads the
raw form."""
_add_connection(db)
folder = _folder(db, "Errands", project_dir="/srv/project")
client.patch(f"/api/folders/{folder.id}", data={"project_dir": ""})
db.refresh(folder)
assert folder.project_dir == ""
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