feat: bot reset with hard confirm and event-driven purge
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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:
|
||||
if hasattr(app.state, "background_worker"):
|
||||
app.state.background_worker.enabled = False
|
||||
yield c
|
||||
|
||||
|
||||
def _seed_bot_with_state(db: Path) -> None:
|
||||
"""Seed a bot plus a chat, container, scene, edge, memory, and activity row."""
|
||||
with open_db(db) as conn:
|
||||
append_event(
|
||||
conn,
|
||||
kind="bot_authored",
|
||||
payload={
|
||||
"id": "bot_a",
|
||||
"name": "BotA",
|
||||
"persona": "thoughtful, observant",
|
||||
"voice_samples": [],
|
||||
"traits": ["shy"],
|
||||
"backstory": "",
|
||||
"initial_relationship_to_you": "coworker",
|
||||
"kickoff_prose": "you stay late",
|
||||
},
|
||||
)
|
||||
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": "",
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="container_created",
|
||||
payload={
|
||||
"chat_id": "chat_bot_a",
|
||||
"name": "office",
|
||||
"type": "workplace",
|
||||
"properties": {},
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="scene_opened",
|
||||
payload={
|
||||
"chat_id": "chat_bot_a",
|
||||
"container_id": 1,
|
||||
"started_at": "2026-04-26T20:00:00+00:00",
|
||||
"participants": ["you", "bot_a"],
|
||||
},
|
||||
)
|
||||
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,
|
||||
},
|
||||
)
|
||||
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,
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="activity_change",
|
||||
payload={
|
||||
"entity_id": "bot_a",
|
||||
"posture": "sitting",
|
||||
"action": {"verb": "writing"},
|
||||
},
|
||||
)
|
||||
project(conn)
|
||||
|
||||
|
||||
def test_reset_purges_state_but_preserves_identity(client, tmp_path):
|
||||
_seed_bot_with_state(tmp_path / "test.db")
|
||||
response = client.post(
|
||||
"/bots/bot_a/reset",
|
||||
data={"confirm_name": "BotA"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert response.status_code == 303
|
||||
assert response.headers["location"] == "/bots"
|
||||
|
||||
with open_db(tmp_path / "test.db") as conn:
|
||||
# Identity preserved.
|
||||
bot = conn.execute(
|
||||
"SELECT id, name, kickoff_prose, initial_relationship_to_you "
|
||||
"FROM bots WHERE id = 'bot_a'"
|
||||
).fetchone()
|
||||
assert bot is not None
|
||||
assert bot[1] == "BotA"
|
||||
assert bot[2] == "you stay late"
|
||||
assert bot[3] == "coworker"
|
||||
|
||||
# State purged.
|
||||
assert conn.execute(
|
||||
"SELECT COUNT(*) FROM chats WHERE host_bot_id = 'bot_a'"
|
||||
).fetchone()[0] == 0
|
||||
assert conn.execute(
|
||||
"SELECT COUNT(*) FROM scenes WHERE chat_id = 'chat_bot_a'"
|
||||
).fetchone()[0] == 0
|
||||
assert conn.execute(
|
||||
"SELECT COUNT(*) FROM containers WHERE chat_id = 'chat_bot_a'"
|
||||
).fetchone()[0] == 0
|
||||
assert conn.execute(
|
||||
"SELECT COUNT(*) FROM chat_state WHERE chat_id = 'chat_bot_a'"
|
||||
).fetchone()[0] == 0
|
||||
assert conn.execute(
|
||||
"SELECT COUNT(*) FROM memories WHERE owner_id = 'bot_a'"
|
||||
).fetchone()[0] == 0
|
||||
assert conn.execute(
|
||||
"SELECT COUNT(*) FROM edges WHERE source_id = 'bot_a' OR target_id = 'bot_a'"
|
||||
).fetchone()[0] == 0
|
||||
assert conn.execute(
|
||||
"SELECT COUNT(*) FROM activity WHERE entity_id = 'bot_a'"
|
||||
).fetchone()[0] == 0
|
||||
|
||||
# Event log records the bot_reset event.
|
||||
assert conn.execute(
|
||||
"SELECT COUNT(*) FROM event_log WHERE kind = 'bot_reset'"
|
||||
).fetchone()[0] == 1
|
||||
|
||||
|
||||
def test_reset_400_when_confirm_name_mismatch(client, tmp_path):
|
||||
_seed_bot_with_state(tmp_path / "test.db")
|
||||
response = client.post(
|
||||
"/bots/bot_a/reset",
|
||||
data={"confirm_name": "WrongName"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_reset_404_when_bot_missing(client):
|
||||
response = client.post(
|
||||
"/bots/no_such/reset",
|
||||
data={"confirm_name": "Anything"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_bot_list_renders_reset_form(client, tmp_path):
|
||||
_seed_bot_with_state(tmp_path / "test.db")
|
||||
response = client.get("/bots")
|
||||
assert response.status_code == 200
|
||||
assert "Reset" in response.text
|
||||
assert "confirm_name" in response.text
|
||||
Reference in New Issue
Block a user