from __future__ import annotations import json 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 from chat.llm.mock import MockLLMClient CANNED_PARSE = { "container_name": "office", "container_type": "workplace", "container_properties": { "public": True, "moving": False, "audible_range": "normal", }, "you_activity": { "posture": "sitting", "action_verb": "working late", "action_interruptible": True, "action_required_attention": "medium", "action_expected_duration": "an hour", "attention": "the screen", "holding": [], }, "bot_activity": { "posture": "sitting", "action_verb": "writing email", "action_interruptible": True, "action_required_attention": "medium", "action_expected_duration": "a few minutes", "attention": "her keyboard", "holding": [], }, "initial_time_iso": "2026-04-26T20:00:00+00:00", "edge_seed_summary": "BotA is your coworker.", "edge_seed_knowledge_facts": [ "coworker", "they sometimes stay late together", ], } @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")) # Import after env is set so dependency lookup uses MockLLMClient. from chat.web.kickoff import get_llm_client mock = MockLLMClient(canned=[json.dumps(CANNED_PARSE)]) app.dependency_overrides[get_llm_client] = lambda: mock with TestClient(app) as c: c.mock_llm = mock # type: ignore[attr-defined] yield c app.dependency_overrides.clear() def _author_bot(db_path: Path, bot_id: str = "bot_a") -> None: from chat.db.connection import open_db with open_db(db_path) as conn: 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", }, ) project(conn) def test_get_kickoff_404_when_bot_missing(client): response = client.get("/bots/no_such_bot/kickoff") assert response.status_code == 404 def test_get_kickoff_renders_parsed_form(client, tmp_path): _author_bot(tmp_path / "test.db", "bot_a") response = client.get("/bots/bot_a/kickoff") assert response.status_code == 200 body = response.text assert "office" in body assert "sitting" in body assert "working late" in body # Mock was consumed once. assert len(client.mock_llm._canned) == 0 def test_post_kickoff_creates_chat_and_redirects(client, tmp_path): _author_bot(tmp_path / "test.db", "bot_a") form_data = { "container_name": "office", "container_type": "workplace", "container_properties": json.dumps(CANNED_PARSE["container_properties"]), "initial_time_iso": "2026-04-26T20:00:00+00:00", "you_activity_posture": "sitting", "you_activity_action_verb": "working late", "you_activity_action_interruptible": "on", "you_activity_action_required_attention": "medium", "you_activity_action_expected_duration": "an hour", "you_activity_attention": "the screen", "you_activity_holding": "", "bot_activity_posture": "sitting", "bot_activity_action_verb": "writing email", "bot_activity_action_interruptible": "on", "bot_activity_action_required_attention": "medium", "bot_activity_action_expected_duration": "a few minutes", "bot_activity_attention": "her keyboard", "bot_activity_holding": "", "edge_seed_summary": "BotA is your coworker.", "edge_seed_knowledge_facts": "coworker\nthey sometimes stay late together", } response = client.post( "/bots/bot_a/kickoff", data=form_data, follow_redirects=False, ) assert response.status_code == 303 assert response.headers["location"] == "/chats/chat_bot_a" from chat.db.connection import open_db from chat.state.world import ( active_scene, find_container, get_activity, get_chat, ) from chat.state.edges import get_edge with open_db(tmp_path / "test.db") as conn: chat = get_chat(conn, "chat_bot_a") assert chat is not None assert chat["host_bot_id"] == "bot_a" assert chat["time"] == "2026-04-26T20:00:00+00:00" container = find_container(conn, "chat_bot_a", "office") assert container is not None assert container["type"] == "workplace" you_act = get_activity(conn, "you") assert you_act is not None assert you_act["posture"] == "sitting" assert you_act["action"]["verb"] == "working late" bot_act = get_activity(conn, "bot_a") assert bot_act is not None assert bot_act["posture"] == "sitting" assert bot_act["action"]["verb"] == "writing email" scene = active_scene(conn, "chat_bot_a") assert scene is not None assert scene["ended_at"] is None assert "you" in scene["participants"] assert "bot_a" in scene["participants"] edge = get_edge(conn, "bot_a", "you") assert edge is not None knowledge = edge["knowledge"] assert "coworker" in knowledge assert "they sometimes stay late together" in knowledge # The seed summary should appear somewhere in knowledge as a v1 compromise. assert any("BotA is your coworker" in k for k in knowledge) def test_post_kickoff_404_when_bot_missing(client): response = client.post( "/bots/no_such/kickoff", data={ "container_name": "office", "container_type": "workplace", "container_properties": "{}", "initial_time_iso": "2026-04-26T20:00:00+00:00", "you_activity_posture": "", "you_activity_action_verb": "", "you_activity_action_interruptible": "on", "you_activity_action_required_attention": "low", "you_activity_action_expected_duration": "", "you_activity_attention": "", "you_activity_holding": "", "bot_activity_posture": "", "bot_activity_action_verb": "", "bot_activity_action_interruptible": "on", "bot_activity_action_required_attention": "low", "bot_activity_action_expected_duration": "", "bot_activity_attention": "", "bot_activity_holding": "", "edge_seed_summary": "", "edge_seed_knowledge_facts": "", }, follow_redirects=False, ) assert response.status_code == 404