Files
chat/tests/test_kickoff_confirm.py
T
2026-04-26 12:28:05 -04:00

213 lines
7.1 KiB
Python

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