feat: drawer bulk significance re-rate per chat (T110.4)
The drawer's Significance review panel previously only supported per-memory edits. Adds a bulk control: pick ``level_from`` and ``level_to``, and every memory in the chat at ``level_from`` is moved to ``level_to``. Implementation emits one ``manual_edit`` event per matching memory (not a single bulk event) so the §6.4 per-row audit trail stays intact — each affected memory carries its own ``prior_value -> new_value`` snapshot, so an inverse edit can restore an individual row without needing to inspect a bulk payload's member list. Reuses the existing ``memory_significance`` ``manual_edit`` projector branch (T25), so no state-layer changes are required. The route rejects no-op submissions (``level_from == level_to``) with 400 to avoid padding the event log with empty edits, and clamps both levels to 0..3 (matching ``edit_memory_significance``). UI: a small ``<details>`` block in the Significance review section with two number inputs and a submit button. Test: tests/test_drawer_phase4.py::test_bulk_significance_re_rate_emits_manual_edit_per_memory.
This commit is contained in:
@@ -523,6 +523,95 @@ def test_delete_impact_modal_escapes_user_controllable_strings(client, tmp_path)
|
||||
assert "<script>alert" in body
|
||||
|
||||
|
||||
def test_bulk_significance_re_rate_emits_manual_edit_per_memory(client, tmp_path):
|
||||
"""T110.4: bulk significance re-rate fans out into one
|
||||
``manual_edit`` event per matching memory — preserving the per-row
|
||||
audit trail (and reversibility) instead of collapsing everything
|
||||
into a single bulk event.
|
||||
|
||||
Seed five memories at significance 0, bulk re-rate 0 -> 2, and
|
||||
verify five new ``memory_significance`` ``manual_edit`` rows landed
|
||||
AND every memory now sits at significance 2.
|
||||
"""
|
||||
db = tmp_path / "test.db"
|
||||
_seed_chat(db)
|
||||
|
||||
# Five memories at significance 0.
|
||||
with open_db(db) as conn:
|
||||
for i in range(5):
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="memory_written",
|
||||
payload={
|
||||
"owner_id": "bot_a",
|
||||
"chat_id": "chat_bot_a",
|
||||
"pov_summary": f"low-sig memory {i}",
|
||||
"witness_you": 1,
|
||||
"witness_host": 1,
|
||||
"witness_guest": 0,
|
||||
"significance": 0,
|
||||
},
|
||||
)
|
||||
# Plus one memory at significance 1 to verify the re-rate is
|
||||
# scoped to ``level_from`` and doesn't sweep the whole chat.
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="memory_written",
|
||||
payload={
|
||||
"owner_id": "bot_a",
|
||||
"chat_id": "chat_bot_a",
|
||||
"pov_summary": "already-rated memory",
|
||||
"witness_you": 1,
|
||||
"witness_host": 1,
|
||||
"witness_guest": 0,
|
||||
"significance": 1,
|
||||
},
|
||||
)
|
||||
prior_manual_edits = conn.execute(
|
||||
"SELECT COUNT(*) FROM event_log WHERE kind = 'manual_edit'"
|
||||
).fetchone()[0]
|
||||
|
||||
response = client.post(
|
||||
"/chats/chat_bot_a/drawer/memory/significance/bulk",
|
||||
data={"level_from": "0", "level_to": "2"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
with open_db(db) as conn:
|
||||
# Five new manual_edit rows, one per matching memory.
|
||||
new_manual_edits = conn.execute(
|
||||
"SELECT COUNT(*) FROM event_log WHERE kind = 'manual_edit'"
|
||||
).fetchone()[0]
|
||||
assert new_manual_edits - prior_manual_edits == 5
|
||||
|
||||
# Every emitted edit is a memory_significance edit with prior=0
|
||||
# and new=2.
|
||||
import json as _json
|
||||
|
||||
rows = conn.execute(
|
||||
"SELECT payload_json FROM event_log "
|
||||
"WHERE kind = 'manual_edit' "
|
||||
"ORDER BY id DESC LIMIT 5"
|
||||
).fetchall()
|
||||
for r in rows:
|
||||
payload = _json.loads(r[0])
|
||||
assert payload["target_kind"] == "memory_significance"
|
||||
assert payload["prior_value"] == 0
|
||||
assert payload["new_value"] == 2
|
||||
|
||||
# Projection caught up — five memories at sig=2, the untouched
|
||||
# one stays at sig=1, none remain at sig=0.
|
||||
dist = dict(
|
||||
conn.execute(
|
||||
"SELECT significance, COUNT(*) FROM memories "
|
||||
"WHERE chat_id = 'chat_bot_a' GROUP BY significance"
|
||||
).fetchall()
|
||||
)
|
||||
assert dist.get(0, 0) == 0
|
||||
assert dist.get(1, 0) == 1
|
||||
assert dist.get(2, 0) == 5
|
||||
|
||||
|
||||
def test_delete_turn_with_event_id_zero_returns_400(client, tmp_path):
|
||||
"""T110.1: ``event_id <= 0`` is an obvious client error and must NOT
|
||||
silently rewind the entire log via ``after_event_id = -1``. The route
|
||||
|
||||
Reference in New Issue
Block a user