fix: bot_reset cascades to guest references in other chats
This commit is contained in:
@@ -66,6 +66,13 @@ def _apply_bot_reset(conn: Connection, e: Event) -> None:
|
||||
"DELETE FROM edges WHERE source_id = ? OR target_id = ?",
|
||||
(bot_id, bot_id),
|
||||
)
|
||||
|
||||
# Phase 2 cascade: clear guest references in other bots' chats so the host
|
||||
# doesn't see a stale guest_bot_id pointing at this (now-purged) bot.
|
||||
conn.execute(
|
||||
"UPDATE chats SET guest_bot_id = NULL WHERE guest_bot_id = ?",
|
||||
(bot_id,),
|
||||
)
|
||||
# NOTE: bots row itself is preserved (identity, kickoff_prose intact).
|
||||
# NOTE: "you" activity (entity_id="you") may linger from a deleted chat;
|
||||
# acceptable for v1 — Phase 1.5 cleanup if needed.
|
||||
|
||||
@@ -183,3 +183,167 @@ def test_bot_list_renders_reset_form(client, tmp_path):
|
||||
assert response.status_code == 200
|
||||
assert "Reset" in response.text
|
||||
assert "confirm_name" in response.text
|
||||
|
||||
|
||||
def _seed_two_bots_with_guest_link(
|
||||
db: Path, *, extra_events: list[dict] | None = None
|
||||
) -> None:
|
||||
"""Seed bot_a + bot_b, each hosting their own chat, with bot_b a guest in chat_bot_a.
|
||||
|
||||
``extra_events`` is appended after the guest_added event and projected
|
||||
together with the rest of the seed (so handlers run only once per event).
|
||||
"""
|
||||
with open_db(db) as conn:
|
||||
# bot_a + its chat
|
||||
append_event(
|
||||
conn,
|
||||
kind="bot_authored",
|
||||
payload={
|
||||
"id": "bot_a",
|
||||
"name": "BotA",
|
||||
"persona": "thoughtful",
|
||||
"voice_samples": [],
|
||||
"traits": [],
|
||||
"backstory": "",
|
||||
"initial_relationship_to_you": "coworker",
|
||||
"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": "",
|
||||
},
|
||||
)
|
||||
# bot_b + its own chat
|
||||
append_event(
|
||||
conn,
|
||||
kind="bot_authored",
|
||||
payload={
|
||||
"id": "bot_b",
|
||||
"name": "BotB",
|
||||
"persona": "curious",
|
||||
"voice_samples": [],
|
||||
"traits": [],
|
||||
"backstory": "",
|
||||
"initial_relationship_to_you": "friend",
|
||||
"kickoff_prose": "",
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="chat_created",
|
||||
payload={
|
||||
"id": "chat_bot_b",
|
||||
"host_bot_id": "bot_b",
|
||||
"initial_time": "2026-04-26T20:00:00+00:00",
|
||||
"narrative_anchor": "Day 1",
|
||||
"weather": "",
|
||||
},
|
||||
)
|
||||
# bot_b joins chat_bot_a as a guest.
|
||||
append_event(
|
||||
conn,
|
||||
kind="guest_added",
|
||||
payload={
|
||||
"chat_id": "chat_bot_a",
|
||||
"guest_bot_id": "bot_b",
|
||||
},
|
||||
)
|
||||
for ev in extra_events or []:
|
||||
append_event(conn, kind=ev["kind"], payload=ev["payload"])
|
||||
project(conn)
|
||||
|
||||
|
||||
def test_reset_clears_guest_reference_in_other_chats(client, tmp_path):
|
||||
db = tmp_path / "test.db"
|
||||
_seed_two_bots_with_guest_link(db)
|
||||
|
||||
# Sanity-check the seed: bot_b is the guest in bot_a's chat.
|
||||
from chat.state.world import get_chat
|
||||
with open_db(db) as conn:
|
||||
assert get_chat(conn, "chat_bot_a")["guest_bot_id"] == "bot_b"
|
||||
assert get_chat(conn, "chat_bot_b") is not None
|
||||
|
||||
response = client.post(
|
||||
"/bots/bot_b/reset",
|
||||
data={"confirm_name": "BotB"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert response.status_code == 303
|
||||
|
||||
with open_db(db) as conn:
|
||||
# The guest reference in bot_a's chat is cleared.
|
||||
chat_a = get_chat(conn, "chat_bot_a")
|
||||
assert chat_a is not None
|
||||
assert chat_a["guest_bot_id"] is None
|
||||
|
||||
# bot_b's own chat is gone (Phase 1 host purge behavior).
|
||||
assert get_chat(conn, "chat_bot_b") is None
|
||||
|
||||
# bot_a is untouched.
|
||||
assert conn.execute(
|
||||
"SELECT COUNT(*) FROM bots WHERE id = 'bot_a'"
|
||||
).fetchone()[0] == 1
|
||||
|
||||
|
||||
def test_reset_purges_guest_memories_from_other_chats(client, tmp_path):
|
||||
db = tmp_path / "test.db"
|
||||
_seed_two_bots_with_guest_link(
|
||||
db,
|
||||
extra_events=[
|
||||
# bot_b is a guest in chat_bot_a and remembers things from there.
|
||||
{
|
||||
"kind": "memory_written",
|
||||
"payload": {
|
||||
"owner_id": "bot_b",
|
||||
"chat_id": "chat_bot_a",
|
||||
"pov_summary": "Met BotA; she was tense.",
|
||||
"witness_you": 1,
|
||||
"witness_host": 1,
|
||||
"witness_guest": 1,
|
||||
"significance": 3,
|
||||
},
|
||||
},
|
||||
# And a memory from bot_b's own chat for good measure.
|
||||
{
|
||||
"kind": "memory_written",
|
||||
"payload": {
|
||||
"owner_id": "bot_b",
|
||||
"chat_id": "chat_bot_b",
|
||||
"pov_summary": "A quiet evening at home.",
|
||||
"witness_you": 1,
|
||||
"witness_host": 1,
|
||||
"witness_guest": 0,
|
||||
"significance": 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
with open_db(db) as conn:
|
||||
# Sanity: bot_b owns 2 memories pre-reset, one in each chat.
|
||||
assert conn.execute(
|
||||
"SELECT COUNT(*) FROM memories WHERE owner_id = 'bot_b'"
|
||||
).fetchone()[0] == 2
|
||||
|
||||
response = client.post(
|
||||
"/bots/bot_b/reset",
|
||||
data={"confirm_name": "BotB"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert response.status_code == 303
|
||||
|
||||
with open_db(db) as conn:
|
||||
# ALL of bot_b's memories are gone, including the cross-chat one in chat_bot_a.
|
||||
assert conn.execute(
|
||||
"SELECT COUNT(*) FROM memories WHERE owner_id = 'bot_b'"
|
||||
).fetchone()[0] == 0
|
||||
assert conn.execute(
|
||||
"SELECT COUNT(*) FROM memories WHERE owner_id = 'bot_b' AND chat_id = 'chat_bot_a'"
|
||||
).fetchone()[0] == 0
|
||||
|
||||
Reference in New Issue
Block a user