from __future__ import annotations from pathlib import Path import pytest from fastapi.testclient import TestClient from chat.app import app from chat.eventlog.log import append_event from chat.eventlog.projector import project @pytest.fixture def client(tmp_path, monkeypatch): config_path = tmp_path / "config.toml" config_path.write_text('featherless_api_key = "test"\n') monkeypatch.setenv("CHAT_CONFIG_PATH", str(config_path)) monkeypatch.setenv("CHAT_DB_PATH", str(tmp_path / "test.db")) with TestClient(app) as c: yield c def _author_you(db_path: Path) -> None: """Author a ``you_entity`` so the first-run middleware doesn't redirect.""" from chat.db.connection import open_db with open_db(db_path) as conn: append_event( conn, kind="you_authored", payload={"name": "Me", "pronouns": "", "persona": ""}, ) project(conn) def _author_bot_and_chat(db_path: Path, bot_id: str = "bot_a") -> None: """Insert a you_entity, bot, and chat via the event log (skip kickoff route).""" from chat.db.connection import open_db with open_db(db_path) as conn: append_event( conn, kind="you_authored", payload={"name": "Me", "pronouns": "", "persona": ""}, ) append_event( conn, kind="bot_authored", payload={ "id": bot_id, "name": "BotA", "persona": "thoughtful, observant", "voice_samples": [], "traits": ["shy"], "backstory": "", "initial_relationship_to_you": "coworker", "kickoff_prose": "you stay late at the office; she's there too", }, ) append_event( conn, kind="chat_created", payload={ "id": f"chat_{bot_id}", "host_bot_id": bot_id, "initial_time": "2026-04-26T20:00:00+00:00", "narrative_anchor": "Day 1", "weather": "", }, ) project(conn) def test_root_redirects_to_chats_when_setup_complete(client, tmp_path): # With both you_entity and a bot present, the first-run middleware # passes through and the nav router sends "/" → "/chats". _author_bot_and_chat(tmp_path / "test.db", "bot_a") response = client.get("/", follow_redirects=False) assert response.status_code == 303 assert response.headers["location"] == "/chats" def test_chats_list_empty_state(client, tmp_path): # Author you + a bot but NO chats — should render the empty-state # chats list, not redirect. _author_bot_and_chat(tmp_path / "test.db", "bot_a") # Drop the chat row so we hit the empty-state branch (the helper # creates a chat — undo it via a fresh seed without chat_created). from chat.db.connection import open_db with open_db(tmp_path / "test.db") as conn: conn.execute("DELETE FROM chats") conn.commit() response = client.get("/chats") assert response.status_code == 200 body = response.text.lower() # Empty state should mention there are no chats yet. assert "no chats yet" in body def test_chats_list_renders_existing_chats(client, tmp_path): _author_bot_and_chat(tmp_path / "test.db", "bot_a") response = client.get("/chats") assert response.status_code == 200 body = response.text # The bot's display name should appear in the chat row. assert "BotA" in body # The chat's in-fiction time should appear in the meta. assert "2026-04-26T20:00:00+00:00" in body def test_existing_template_routes_still_work_with_new_layout(client): # Smoke test the layout reshuffle didn't break the existing pages. for path in ("/bots", "/bots/new", "/settings"): response = client.get(path) assert response.status_code == 200, f"{path} returned {response.status_code}" body = response.text # Each page should now show the persistent left-rail brand link. assert 'class="rail"' in body or "rail-brand" in body