"""The sidebar's Chat/Agent switch: what it stores, and what it hides.""" from __future__ import annotations from fastapi.testclient import TestClient from sqlalchemy import select from lembas.db.models import Chat, Connection, Folder, Model, User from lembas.services import settings_store from lembas.services.crypto import encrypt def _enable_agents(db) -> None: """The split only applies when agent chats are possible. With them off the sidebar deliberately goes back to showing everything, so a test that did not do this would be asserting against the un-split behaviour and passing for the wrong reason. """ settings_store.update(db, {"enabled": True}, key=settings_store.AGENTS) 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 _chat(db, *, title: str, kind: str = "chat", folder: Folder | None = None) -> Chat: user = db.scalars(select(User).order_by(User.created_at)).first() chat = Chat( user_id=user.id, title=title, kind=kind, model_id="test-model", folder_id=folder.id if folder else None, ) db.add(chat) db.commit() return chat def _folder(db, name: str, parent: Folder | None = None) -> 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) db.add(folder) db.commit() return folder # --- What the switch stores --------------------------------------------------- def test_the_switch_stores_the_choice_and_returns_the_tree(client: TestClient, db, registered): response = client.post("/api/preferences/sidebar-kind", data={"kind": "agent"}) assert response.status_code == 200 # The fragment, not a redirect and not a full page: the folder open/closed # state must survive a flick of the switch. assert 'id="sidebar-tree"' in response.text assert "" not in response.text.lower() user = db.scalars(select(User).order_by(User.created_at)).first() db.refresh(user) assert user.settings_json["sidebar_kind"] == "agent" def test_an_unknown_kind_is_refused_rather_than_stored(client: TestClient, db, registered): """`sidebar_kind` reads anything unrecognised back as "chat", so storing it would be a preference that silently does nothing.""" assert client.post("/api/preferences/sidebar-kind", data={"kind": "wizard"}).status_code == 400 user = db.scalars(select(User).order_by(User.created_at)).first() db.refresh(user) assert "sidebar_kind" not in (user.settings_json or {}) def test_the_new_chat_button_follows_the_switch(client: TestClient, db, registered): """It sits above the scroll area rather than inside the tree, so a swap of the tree alone left it saying "New chat" while agent chats were listed underneath -- saying one thing and doing another, which is the shape of failure the switch itself was arranged to avoid. Asserted on the *fragment*, not on a page load: a page load re-renders the button anyway, which is exactly why this went unnoticed. """ _add_connection(db) _enable_agents(db) response = client.post("/api/preferences/sidebar-kind", data={"kind": "agent"}) assert 'id="sidebar-actions"' in response.text assert 'hx-swap-oob="true"' in response.text assert "New agent chat" in response.text assert 'href="/chat?kind=agent"' in response.text response = client.post("/api/preferences/sidebar-kind", data={"kind": "chat"}) assert "New agent chat" not in response.text assert 'href="/chat"' in response.text def test_a_page_load_carries_no_stray_out_of_band_row( client: TestClient, db, registered ): """`hx-swap-oob` on a full page load would be a duplicate element sitting in the tree, waiting to be swapped by the next unrelated request.""" _add_connection(db) _enable_agents(db) page = client.get("/chat").text assert page.count('id="sidebar-actions"') == 1 assert 'id="sidebar-actions"\n hx-swap-oob' not in page assert "hx-swap-oob" not in page.split('id="sidebar-tree"')[1][:2000] def test_the_switch_survives_a_page_load(client: TestClient, db, registered): _add_connection(db) _enable_agents(db) client.post("/api/preferences/sidebar-kind", data={"kind": "agent"}) page = client.get("/chat").text assert 'id="sidebar-kind-agent"' in page # The Agent side is what "New chat" opens on, so the fork is already picked. assert 'href="/chat?kind=agent"' in page # --- What each side shows ------------------------------------------------------ def test_each_side_shows_only_its_own_kind(client: TestClient, db, registered): _add_connection(db) _enable_agents(db) _chat(db, title="An ordinary question") _chat(db, title="A machine errand", kind="agent") page = client.get("/chat").text assert "An ordinary question" in page assert "A machine errand" not in page client.post("/api/preferences/sidebar-kind", data={"kind": "agent"}) page = client.get("/chat").text assert "A machine errand" in page assert "An ordinary question" not in page def test_a_folder_of_the_other_kind_is_hidden(client: TestClient, db, registered): _add_connection(db) _enable_agents(db) folder = _folder(db, "Errands") _chat(db, title="A machine errand", kind="agent", folder=folder) # Chat side: the folder holds something, but nothing of this kind. assert "Errands" not in client.get("/chat").text client.post("/api/preferences/sidebar-kind", data={"kind": "agent"}) page = client.get("/chat").text assert "Errands" in page assert "A machine errand" in page def test_a_folder_matching_three_levels_down_is_shown(client: TestClient, db, registered): """Judging a folder on its own contents alone would bury it.""" _add_connection(db) _enable_agents(db) top = _folder(db, "Top") middle = _folder(db, "Middle", parent=top) bottom = _folder(db, "Bottom", parent=middle) _chat(db, title="A machine errand", kind="agent", folder=bottom) client.post("/api/preferences/sidebar-kind", data={"kind": "agent"}) page = client.get("/chat").text assert "Top" in page assert "Middle" in page assert "Bottom" in page assert "A machine errand" in page def test_an_empty_folder_shows_on_both_sides(client: TestClient, db, registered): """Two reasons a folder can look empty, and only one is a reason to hide it. A folder the filter emptied is noise. A folder that was empty to begin with is a container somebody just made -- hiding that one means it can never be found again, let alone filed into. """ _add_connection(db) _enable_agents(db) _folder(db, "Waiting") assert "Waiting" in client.get("/chat").text client.post("/api/preferences/sidebar-kind", data={"kind": "agent"}) assert "Waiting" in client.get("/chat").text def test_a_folder_holding_both_kinds_shows_the_right_half(client: TestClient, db, registered): _add_connection(db) _enable_agents(db) folder = _folder(db, "Mixed") _chat(db, title="An ordinary question", folder=folder) _chat(db, title="A machine errand", kind="agent", folder=folder) page = client.get("/chat").text assert "Mixed" in page assert "An ordinary question" in page assert "A machine errand" not in page client.post("/api/preferences/sidebar-kind", data={"kind": "agent"}) page = client.get("/chat").text assert "Mixed" in page assert "A machine errand" in page assert "An ordinary question" not in page # --- Whether the switch appears at all ----------------------------------------- def test_the_switch_is_absent_when_agent_chats_are_off(client: TestClient, db, registered): """A two-way switch with one useful side is worse than no switch: it offers a view that is empty by construction and cannot be made otherwise.""" _add_connection(db) settings_store.update(db, {"enabled": False}, key=settings_store.AGENTS) page = client.get("/chat").text assert 'id="sidebar-kind-agent"' not in page def test_the_switch_is_present_when_agent_chats_are_on(client: TestClient, db, registered): _add_connection(db) settings_store.update(db, {"enabled": True}, key=settings_store.AGENTS) page = client.get("/chat").text assert 'id="sidebar-kind-agent"' in page assert 'id="sidebar-kind-chat"' in page # --- The row itself ------------------------------------------------------------ def test_an_agent_chat_is_marked_in_the_row(client: TestClient, db, registered): """Legible with the switch off too: a chat that can run commands should not look like one that cannot.""" _add_connection(db) _enable_agents(db) _chat(db, title="A machine errand", kind="agent") client.post("/api/preferences/sidebar-kind", data={"kind": "agent"}) page = client.get("/chat").text assert "#i-terminal" in page # --- The verb goes where the event does ---------------------------------------- def test_the_switch_posts_at_a_route_that_serves_post(client: TestClient, db, registered): """Asserted as a refusal as well as a success. A control wired to a method its route does not serve fails silently -- htmx surfaces nothing, so the interface looks exactly like one that works.""" assert client.get("/api/preferences/sidebar-kind").status_code == 405 assert client.post("/api/preferences/sidebar-kind", data={"kind": "chat"}).status_code == 200