213 lines
7.1 KiB
Python
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
|