"""Choosing the approval mode: after a chat exists, and before one does. The regression test at the top is the one that was missing. The mode select in the topbar posted with `hx-post` against a route that only answers `PATCH`, so every change returned 405 and the mode never moved -- and htmx surfaces nothing on a failed request, so the control looked like it had worked. A control wired to a method the route does not serve fails exactly this quietly, which is why the assertion below reads the row rather than the response. """ from __future__ import annotations import pytest from fastapi.testclient import TestClient from sqlalchemy import select from lembas.db.models import KIND_AGENT, Chat, Connection, Model, SshProfile, User from lembas.services import settings_store from lembas.services.agent import policy from .conftest import control_named def _agent_chat(db, *, mode: str = policy.MODE_MANUAL) -> Chat: """An agent chat pointed at a profile that is never actually connected to. Nothing here opens a connection: changing the mode is a database write, and a real sshd would only make the test slower and flakier. A model is needed even though nothing generates: with none, `index.html` renders the "no models available" screen *instead of* the thread and the composer, so a test asserting on composer markup would pass against a page that does not contain a composer at all. That is not hypothetical -- it is how the first version of the assertion below came to be vacuous. """ settings_store.update(db, {"enabled": True}, key=settings_store.AGENTS) user = db.scalars(select(User)).first() assert user is not None connection = Connection(name="c", base_url="http://127.0.0.1:1", api_key_encrypted="") db.add(connection) db.commit() db.add(Model(connection_id=connection.id, model_id="m")) db.commit() profile = SshProfile( owner_id=user.id, name="Test box", host="127.0.0.1", port=22, username="tester", host_key="host key", host_fingerprint="SHA256:x", default_dir="/project", ) db.add(profile) db.commit() chat = Chat( user_id=user.id, model_id="m", connection_id=connection.id, kind=KIND_AGENT, ssh_profile_id=profile.id, project_dir="/project", agent_mode=mode, ) db.add(chat) db.commit() return chat def test_patching_the_mode_changes_it(client: TestClient, db, registered): chat = _agent_chat(db) response = client.patch(f"/api/chats/{chat.id}", data={"agent_mode": policy.MODE_PLAN}) assert response.status_code == 204, response.text db.refresh(chat) assert chat.agent_mode == policy.MODE_PLAN def test_the_mode_form_uses_a_method_the_route_serves(client: TestClient, db, registered): """The bug itself, stated as a test rather than as a comment. POST is not merely unhandled here, it is *silently* unhandled: htmx swallows the 405 and the select keeps showing whatever was clicked. Asserting the method is refused is what stops somebody reintroducing `hx-post` and finding the mode unchangeable again with nothing in the logs. """ chat = _agent_chat(db) refused = client.post(f"/api/chats/{chat.id}", data={"agent_mode": policy.MODE_AUTO}) assert refused.status_code == 405 db.refresh(chat) assert chat.agent_mode == policy.MODE_MANUAL def test_the_mode_select_carries_its_own_verb(client: TestClient, db, registered): """The request must hang off the element the event fires on. This is the invariant, and the previous version of this test did not check it. `hx-patch` lived on an empty sibling `