feat: error banners and first-run navigation flow
This commit is contained in:
+35
-4
@@ -20,11 +20,29 @@ def client(tmp_path, monkeypatch):
|
||||
yield c
|
||||
|
||||
|
||||
def _author_bot_and_chat(db_path: Path, bot_id: str = "bot_a") -> None:
|
||||
"""Insert a bot and a chat directly via the event log (skip kickoff route)."""
|
||||
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",
|
||||
@@ -53,13 +71,26 @@ def _author_bot_and_chat(db_path: Path, bot_id: str = "bot_a") -> None:
|
||||
project(conn)
|
||||
|
||||
|
||||
def test_root_redirects_to_chats(client):
|
||||
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):
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from chat.app import app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(tmp_path, monkeypatch):
|
||||
cfg = tmp_path / "config.toml"
|
||||
cfg.write_text('featherless_api_key = "test"\n')
|
||||
monkeypatch.setenv("CHAT_CONFIG_PATH", str(cfg))
|
||||
db = tmp_path / "test.db"
|
||||
monkeypatch.setenv("CHAT_DB_PATH", str(db))
|
||||
with TestClient(app) as c:
|
||||
if hasattr(app.state, "background_worker"):
|
||||
app.state.background_worker.enabled = False
|
||||
yield c
|
||||
|
||||
|
||||
def _setup_minimal_state(db_path):
|
||||
"""Set up enough state so the first-run middleware doesn't redirect."""
|
||||
from chat.db.connection import open_db
|
||||
from chat.eventlog.log import append_event
|
||||
from chat.eventlog.projector import project
|
||||
|
||||
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_a",
|
||||
"name": "BotA",
|
||||
"persona": "",
|
||||
"voice_samples": [],
|
||||
"traits": [],
|
||||
"backstory": "",
|
||||
"initial_relationship_to_you": "",
|
||||
"kickoff_prose": "",
|
||||
},
|
||||
)
|
||||
project(conn)
|
||||
|
||||
|
||||
def test_404_renders_friendly_page_for_html(client, tmp_path):
|
||||
_setup_minimal_state(tmp_path / "test.db")
|
||||
response = client.get("/chats/no_such_chat")
|
||||
assert response.status_code == 404
|
||||
body = response.text
|
||||
assert "404" in body
|
||||
assert "back to" in body.lower()
|
||||
@@ -0,0 +1,146 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from chat.app import app
|
||||
from chat.db.connection import open_db
|
||||
from chat.eventlog.log import append_event
|
||||
from chat.eventlog.projector import project
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(tmp_path, monkeypatch):
|
||||
cfg = tmp_path / "config.toml"
|
||||
cfg.write_text('featherless_api_key = "test"\n')
|
||||
monkeypatch.setenv("CHAT_CONFIG_PATH", str(cfg))
|
||||
db = tmp_path / "test.db"
|
||||
monkeypatch.setenv("CHAT_DB_PATH", str(db))
|
||||
with TestClient(app) as c:
|
||||
if hasattr(app.state, "background_worker"):
|
||||
app.state.background_worker.enabled = False
|
||||
yield c
|
||||
|
||||
|
||||
def test_root_redirects_to_settings_when_no_you(client):
|
||||
response = client.get("/", follow_redirects=False)
|
||||
assert response.status_code == 303
|
||||
assert response.headers["location"] == "/settings"
|
||||
|
||||
|
||||
def test_chats_redirects_to_settings_when_no_you(client):
|
||||
response = client.get("/chats", follow_redirects=False)
|
||||
assert response.status_code == 303
|
||||
assert response.headers["location"] == "/settings"
|
||||
|
||||
|
||||
def test_redirects_to_bots_new_when_you_exists_but_no_bots(client, tmp_path):
|
||||
with open_db(tmp_path / "test.db") as conn:
|
||||
append_event(
|
||||
conn,
|
||||
kind="you_authored",
|
||||
payload={
|
||||
"name": "Me",
|
||||
"pronouns": "they/them",
|
||||
"persona": "engineer",
|
||||
},
|
||||
)
|
||||
project(conn)
|
||||
response = client.get("/chats", follow_redirects=False)
|
||||
assert response.status_code == 303
|
||||
assert response.headers["location"] == "/bots/new"
|
||||
|
||||
|
||||
def test_root_redirects_to_bots_new_when_you_exists_but_no_bots(client, tmp_path):
|
||||
with open_db(tmp_path / "test.db") as conn:
|
||||
append_event(
|
||||
conn,
|
||||
kind="you_authored",
|
||||
payload={
|
||||
"name": "Me",
|
||||
"pronouns": "they/them",
|
||||
"persona": "engineer",
|
||||
},
|
||||
)
|
||||
project(conn)
|
||||
response = client.get("/", follow_redirects=False)
|
||||
assert response.status_code == 303
|
||||
assert response.headers["location"] == "/bots/new"
|
||||
|
||||
|
||||
def test_no_redirect_when_setup_complete(client, tmp_path):
|
||||
with open_db(tmp_path / "test.db") as conn:
|
||||
append_event(
|
||||
conn,
|
||||
kind="you_authored",
|
||||
payload={
|
||||
"name": "Me",
|
||||
"pronouns": "they/them",
|
||||
"persona": "engineer",
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="bot_authored",
|
||||
payload={
|
||||
"id": "bot_a",
|
||||
"name": "BotA",
|
||||
"persona": "...",
|
||||
"voice_samples": [],
|
||||
"traits": [],
|
||||
"backstory": "",
|
||||
"initial_relationship_to_you": "",
|
||||
"kickoff_prose": "",
|
||||
},
|
||||
)
|
||||
project(conn)
|
||||
response = client.get("/chats", follow_redirects=False)
|
||||
# /chats page renders normally (200) instead of redirecting.
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_settings_page_accessible_without_you(client):
|
||||
"""Don't redirect FROM /settings — user needs to fill it out."""
|
||||
response = client.get("/settings", follow_redirects=False)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_bots_new_accessible_without_redirect(client, tmp_path):
|
||||
with open_db(tmp_path / "test.db") as conn:
|
||||
append_event(
|
||||
conn,
|
||||
kind="you_authored",
|
||||
payload={"name": "Me", "pronouns": "", "persona": ""},
|
||||
)
|
||||
project(conn)
|
||||
response = client.get("/bots/new", follow_redirects=False)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_bots_list_accessible_without_redirect_when_empty(client, tmp_path):
|
||||
"""The bot list page itself should never redirect — even when empty."""
|
||||
with open_db(tmp_path / "test.db") as conn:
|
||||
append_event(
|
||||
conn,
|
||||
kind="you_authored",
|
||||
payload={"name": "Me", "pronouns": "", "persona": ""},
|
||||
)
|
||||
project(conn)
|
||||
response = client.get("/bots", follow_redirects=False)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_post_to_settings_not_redirected(client):
|
||||
"""POST should bypass middleware — it's a write, not a landing nav."""
|
||||
response = client.post(
|
||||
"/settings",
|
||||
data={"name": "Me", "pronouns": "", "persona": ""},
|
||||
follow_redirects=False,
|
||||
)
|
||||
# Settings POST returns 200 with the saved page (no HTTPException raised).
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_health_endpoint_not_redirected(client):
|
||||
response = client.get("/health", follow_redirects=False)
|
||||
assert response.status_code == 200
|
||||
Reference in New Issue
Block a user