191 lines
6.3 KiB
Python
191 lines
6.3 KiB
Python
"""T25: drawer edits with manual_edit event capture.
|
|
|
|
Each editable field on the drawer is exposed as a small POST endpoint.
|
|
Edits emit either a ``manual_edit`` event (snapshotting the prior value
|
|
for §6.4 reversibility) or, for pin toggles, a ``memory_pin_changed``
|
|
event with ``auto_pinned=0`` so manual pins survive auto-eviction.
|
|
|
|
Phase 1 narrowed scope: affinity slider, significance dropdown, pin
|
|
toggle. Other §6.4 fields (activity, edge_summary, edge_trust, pov_summary,
|
|
knowledge_facts list) are deferred to a Phase 1.5 follow-up.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
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(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": [],
|
|
"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": "",
|
|
},
|
|
)
|
|
# Edge bot_a -> you with affinity_delta=0 to materialise the row at
|
|
# default 50/50.
|
|
append_event(
|
|
conn,
|
|
kind="edge_update",
|
|
payload={
|
|
"source_id": "bot_a",
|
|
"target_id": "you",
|
|
"chat_id": "chat_bot_a",
|
|
"affinity_delta": 0,
|
|
},
|
|
)
|
|
append_event(
|
|
conn,
|
|
kind="memory_written",
|
|
payload={
|
|
"owner_id": "bot_a",
|
|
"chat_id": "chat_bot_a",
|
|
"pov_summary": "A memory",
|
|
"witness_you": 1,
|
|
"witness_host": 1,
|
|
"witness_guest": 0,
|
|
"significance": 1,
|
|
},
|
|
)
|
|
project(conn)
|
|
|
|
|
|
def test_edit_edge_affinity_emits_manual_edit_and_updates(client, tmp_path):
|
|
_seed(tmp_path / "test.db")
|
|
response = client.post(
|
|
"/chats/chat_bot_a/drawer/edge/bot_a/you/affinity",
|
|
data={"affinity": "75"},
|
|
)
|
|
assert response.status_code == 200 # returns refreshed drawer partial
|
|
# Refresh shows the new affinity value.
|
|
assert "75" in response.text
|
|
|
|
with open_db(tmp_path / "test.db") as conn:
|
|
cur = conn.execute(
|
|
"SELECT payload_json FROM event_log WHERE kind = 'manual_edit'"
|
|
).fetchall()
|
|
assert len(cur) == 1
|
|
payload = json.loads(cur[0][0])
|
|
assert payload["target_kind"] == "edge_affinity"
|
|
assert payload["prior_value"] == 50
|
|
assert payload["new_value"] == 75
|
|
assert payload["target_id"]["source_id"] == "bot_a"
|
|
assert payload["target_id"]["target_id"] == "you"
|
|
|
|
from chat.state.edges import get_edge
|
|
|
|
edge = get_edge(conn, "bot_a", "you")
|
|
assert edge["affinity"] == 75
|
|
|
|
|
|
def test_edit_memory_significance_emits_event(client, tmp_path):
|
|
_seed(tmp_path / "test.db")
|
|
with open_db(tmp_path / "test.db") as conn:
|
|
memory_id = conn.execute("SELECT id FROM memories LIMIT 1").fetchone()[0]
|
|
response = client.post(
|
|
f"/chats/chat_bot_a/drawer/memory/{memory_id}/significance",
|
|
data={"significance": "3"},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
with open_db(tmp_path / "test.db") as conn:
|
|
sig = conn.execute(
|
|
"SELECT significance FROM memories WHERE id = ?", (memory_id,)
|
|
).fetchone()[0]
|
|
assert sig == 3
|
|
cur = conn.execute(
|
|
"SELECT payload_json FROM event_log WHERE kind = 'manual_edit'"
|
|
).fetchall()
|
|
assert len(cur) == 1
|
|
payload = json.loads(cur[0][0])
|
|
assert payload["target_kind"] == "memory_significance"
|
|
assert payload["prior_value"] == 1
|
|
assert payload["new_value"] == 3
|
|
assert payload["target_id"] == memory_id
|
|
|
|
|
|
def test_toggle_memory_pin_manual_emits_event_with_auto_pinned_0(client, tmp_path):
|
|
_seed(tmp_path / "test.db")
|
|
with open_db(tmp_path / "test.db") as conn:
|
|
memory_id = conn.execute("SELECT id FROM memories LIMIT 1").fetchone()[0]
|
|
response = client.post(
|
|
f"/chats/chat_bot_a/drawer/memory/{memory_id}/pin",
|
|
data={"pinned": "1"},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
with open_db(tmp_path / "test.db") as conn:
|
|
row = conn.execute(
|
|
"SELECT pinned, auto_pinned FROM memories WHERE id = ?", (memory_id,)
|
|
).fetchone()
|
|
assert row[0] == 1
|
|
assert row[1] == 0 # NOT auto-pinned (manual pin survives auto-eviction)
|
|
# The pin toggle uses memory_pin_changed (not manual_edit).
|
|
cur = conn.execute(
|
|
"SELECT payload_json FROM event_log "
|
|
"WHERE kind = 'memory_pin_changed' ORDER BY id DESC LIMIT 1"
|
|
).fetchone()
|
|
payload = json.loads(cur[0])
|
|
assert payload["pinned"] == 1
|
|
assert payload["auto_pinned"] == 0
|
|
assert payload["memory_id"] == memory_id
|
|
|
|
|
|
def test_edit_404_when_chat_missing(client):
|
|
response = client.post(
|
|
"/chats/no_such/drawer/edge/bot_a/you/affinity",
|
|
data={"affinity": "75"},
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_edit_404_when_target_missing(client, tmp_path):
|
|
_seed(tmp_path / "test.db")
|
|
response = client.post(
|
|
"/chats/chat_bot_a/drawer/memory/99999/significance",
|
|
data={"significance": "2"},
|
|
)
|
|
assert response.status_code == 404
|