feat: read-only drawer with scene, activity, edges, memories
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
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:
|
||||
# Disable background worker (we won't drive turns).
|
||||
if hasattr(app.state, "background_worker"):
|
||||
app.state.background_worker.enabled = False
|
||||
yield c
|
||||
|
||||
|
||||
def _seed(db: Path) -> None:
|
||||
with open_db(db) as conn:
|
||||
append_event(
|
||||
conn,
|
||||
kind="bot_authored",
|
||||
payload={
|
||||
"id": "bot_a",
|
||||
"name": "BotA",
|
||||
"persona": "...",
|
||||
"voice_samples": [],
|
||||
"traits": ["shy"],
|
||||
"backstory": "",
|
||||
"initial_relationship_to_you": "",
|
||||
"kickoff_prose": "",
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="you_authored",
|
||||
payload={
|
||||
"name": "Me",
|
||||
"pronouns": "they/them",
|
||||
"persona": "engineer",
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="chat_created",
|
||||
payload={
|
||||
"id": "chat_bot_a",
|
||||
"host_bot_id": "bot_a",
|
||||
"initial_time": "2026-04-26T20:00:00+00:00",
|
||||
"narrative_anchor": "Day 1",
|
||||
"weather": "",
|
||||
},
|
||||
)
|
||||
# Activity for both you and host bot.
|
||||
append_event(
|
||||
conn,
|
||||
kind="activity_change",
|
||||
payload={
|
||||
"entity_id": "you",
|
||||
"posture": "sitting",
|
||||
"action": {"verb": "thinking"},
|
||||
"attention": "the screen",
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="activity_change",
|
||||
payload={
|
||||
"entity_id": "bot_a",
|
||||
"posture": "standing",
|
||||
"action": {"verb": "looking out the window"},
|
||||
},
|
||||
)
|
||||
# Edge host -> you.
|
||||
append_event(
|
||||
conn,
|
||||
kind="edge_update",
|
||||
payload={
|
||||
"source_id": "bot_a",
|
||||
"target_id": "you",
|
||||
"chat_id": "chat_bot_a",
|
||||
"affinity_delta": 5,
|
||||
"trust_delta": 2,
|
||||
"knowledge_facts": ["Me likes coffee"],
|
||||
},
|
||||
)
|
||||
# A regular memory.
|
||||
append_event(
|
||||
conn,
|
||||
kind="memory_written",
|
||||
payload={
|
||||
"owner_id": "bot_a",
|
||||
"chat_id": "chat_bot_a",
|
||||
"pov_summary": "Talked about her sister",
|
||||
"witness_you": 1,
|
||||
"witness_host": 1,
|
||||
"witness_guest": 0,
|
||||
"significance": 2,
|
||||
},
|
||||
)
|
||||
# A pinned memory with significance 3 (★★).
|
||||
append_event(
|
||||
conn,
|
||||
kind="memory_written",
|
||||
payload={
|
||||
"owner_id": "bot_a",
|
||||
"chat_id": "chat_bot_a",
|
||||
"pov_summary": "First kiss",
|
||||
"witness_you": 1,
|
||||
"witness_host": 1,
|
||||
"witness_guest": 0,
|
||||
"significance": 3,
|
||||
"pinned": 1,
|
||||
"auto_pinned": 1,
|
||||
},
|
||||
)
|
||||
project(conn)
|
||||
|
||||
|
||||
def test_drawer_404_when_chat_missing(client):
|
||||
response = client.get("/chats/no_such/drawer")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_drawer_renders_scene_and_activity(client, tmp_path):
|
||||
_seed(tmp_path / "test.db")
|
||||
response = client.get("/chats/chat_bot_a/drawer")
|
||||
assert response.status_code == 200
|
||||
body = response.text
|
||||
# Scene/time anchor.
|
||||
assert "2026-04-26" in body
|
||||
# Activity verbs from both entities.
|
||||
assert "thinking" in body
|
||||
assert "looking out the window" in body
|
||||
# Activity attention.
|
||||
assert "the screen" in body
|
||||
|
||||
|
||||
def test_drawer_renders_edges(client, tmp_path):
|
||||
_seed(tmp_path / "test.db")
|
||||
response = client.get("/chats/chat_bot_a/drawer")
|
||||
assert response.status_code == 200
|
||||
body = response.text
|
||||
assert "BotA" in body
|
||||
assert "you" in body
|
||||
# Default affinity 50 + delta 5 = 55.
|
||||
assert "55" in body
|
||||
# Knowledge fact appears.
|
||||
assert "Me likes coffee" in body
|
||||
|
||||
|
||||
def test_drawer_renders_memories_with_significance_markers(client, tmp_path):
|
||||
_seed(tmp_path / "test.db")
|
||||
response = client.get("/chats/chat_bot_a/drawer")
|
||||
assert response.status_code == 200
|
||||
body = response.text
|
||||
assert "Talked about her sister" in body
|
||||
assert "First kiss" in body
|
||||
# Pinned counter shows 1 / 8 (or 1/8).
|
||||
assert "1 / 8" in body or "1/8" in body
|
||||
# Significance star marker for the pinned, score-3 memory.
|
||||
assert "★" in body
|
||||
|
||||
|
||||
def test_drawer_handles_no_state_gracefully(client, tmp_path):
|
||||
db = tmp_path / "test.db"
|
||||
with open_db(db) as conn:
|
||||
# Just enough state for the chat to exist.
|
||||
append_event(
|
||||
conn,
|
||||
kind="bot_authored",
|
||||
payload={
|
||||
"id": "bot_a",
|
||||
"name": "BotA",
|
||||
"persona": "...",
|
||||
"voice_samples": [],
|
||||
"traits": [],
|
||||
"backstory": "",
|
||||
"initial_relationship_to_you": "",
|
||||
"kickoff_prose": "",
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="chat_created",
|
||||
payload={
|
||||
"id": "chat_bot_a",
|
||||
"host_bot_id": "bot_a",
|
||||
"initial_time": "2026-04-26T20:00:00+00:00",
|
||||
"narrative_anchor": "Day 1",
|
||||
"weather": "",
|
||||
},
|
||||
)
|
||||
project(conn)
|
||||
response = client.get("/chats/chat_bot_a/drawer")
|
||||
assert response.status_code == 200
|
||||
body = response.text
|
||||
# Drawer renders gracefully with empty placeholders.
|
||||
assert "No active container" in body or "Container:" not in body
|
||||
assert "No edges yet" in body or "Edges" in body
|
||||
Reference in New Issue
Block a user