Files
hermes-agent/tests/hermes_state/test_delete_session_write_guards.py
kshitijk4poor f34a71d3c5 fix(state): delete_session_if_empty returns False for a guarded row
Its contract is a boolean "deleted or not" (callers like the CLI /new and
/clear hygiene path treat False as "keep it"), and every other non-candidate
(messages, title, children) returns False. Raising SessionActiveWriteGuardError
here forced every caller to grow a try/except for what is just another reason
the row is not empty-and-idle.
2026-10-03 16:32:15 +05:30

111 lines
5.1 KiB
Python

"""Invariant tests for entry-side deletion refusal on active write guards (#123583)."""
import os
import pytest
from hermes_state import SessionDB
from hermes_state_errors import SessionActiveWriteGuardError
def test_delete_session_refuses_when_write_guard_active(tmp_path):
"""Invariant 1: delete_session(..., exclude_active_write_guards=True) refuses inside
the transaction while an active turn lease or compression lock protects the row."""
path = tmp_path / "state.db"
db = SessionDB(path)
db.create_session("sess-lease", source="test")
db.create_session("sess-cmp", source="test")
turn_holder = f"pid={os.getpid()}:turn=1"
cmp_holder = f"pid={os.getpid()}:cmp=1"
assert db.try_acquire_session_turn_lease("sess-lease", turn_holder, ttl_seconds=300.0) is True
assert db.try_acquire_compression_lock("sess-cmp", cmp_holder, ttl_seconds=300.0) is True
# 1. Active turn lease -> raises SessionActiveWriteGuardError and row survives
with pytest.raises(SessionActiveWriteGuardError):
db.delete_session("sess-lease", exclude_active_write_guards=True)
assert db.get_session("sess-lease") is not None
# 2. Active compression lock -> raises SessionActiveWriteGuardError and row survives
with pytest.raises(SessionActiveWriteGuardError):
db.delete_session("sess-cmp", exclude_active_write_guards=True)
assert db.get_session("sess-cmp") is not None
# 3. Released guards -> deletion succeeds
db.release_session_turn_lease("sess-lease", turn_holder)
db.release_compression_lock("sess-cmp", cmp_holder)
assert db.delete_session("sess-lease", exclude_active_write_guards=True) is True
assert db.get_session("sess-lease") is None
assert db.delete_session("sess-cmp", exclude_active_write_guards=True) is True
assert db.get_session("sess-cmp") is None
# 4. An idle compression-ended row is a closed parent, not a live write: delete must succeed
# (no CompressionSessionClosedError leaking out of the guard check).
db.create_session("sess-compressed", source="test")
db.end_session("sess-compressed", "compression")
assert db.delete_session("sess-compressed", exclude_active_write_guards=True) is True
assert db.get_session("sess-compressed") is None
db.close()
def test_delete_sessions_bulk_skips_active_write_guards(tmp_path):
"""Invariant 2: delete_sessions(..., exclude_active_write_guards=True) atomically
skips rows with active guards and removes only the idle ones."""
path = tmp_path / "state.db"
db = SessionDB(path)
db.create_session("bulk-active", source="test")
db.create_session("bulk-idle", source="test")
turn_holder = f"pid={os.getpid()}:turn=bulk"
assert db.try_acquire_session_turn_lease("bulk-active", turn_holder, ttl_seconds=300.0) is True
skipped: list[str] = []
deleted_count = db.delete_sessions(
["bulk-active", "bulk-idle"], exclude_active_write_guards=True, skipped_ids=skipped)
assert deleted_count == 1
assert skipped == ["bulk-active"] # reported to the caller, not silently dropped
# Protected row survived; idle row was deleted
assert db.get_session("bulk-active") is not None
assert db.get_session("bulk-idle") is None
db.release_session_turn_lease("bulk-active", turn_holder)
# Delegate children cascade with their root, so a guarded child protects the root too:
# single delete refuses, bulk delete skips the root and never cascades the guarded child away.
db.create_session("deleg-root", source="test")
db.create_session(
"deleg-child", source="test", parent_session_id="deleg-root",
model_config={"_delegate_from": "deleg-root"},
)
child_holder = f"pid={os.getpid()}:turn=child"
assert db.try_acquire_session_turn_lease("deleg-child", child_holder, ttl_seconds=300.0) is True
with pytest.raises(SessionActiveWriteGuardError):
db.delete_session("deleg-root", exclude_active_write_guards=True)
skipped = []
assert db.delete_sessions(["deleg-root"], exclude_active_write_guards=True, skipped_ids=skipped) == 0
assert skipped == ["deleg-root"]
assert db.get_session("deleg-root") is not None and db.get_session("deleg-child") is not None
db.release_session_turn_lease("deleg-child", child_holder)
db.close()
def test_delete_session_if_empty_refuses_when_write_guard_active(tmp_path):
"""delete_session_if_empty shares the guarded-delete refusal: the emptiness predicate
reads committed state, so a row whose first turn is leased but not yet flushed would
be deleted mid-turn (#123583)."""
path = tmp_path / "state.db"
db = SessionDB(path)
db.create_session("empty-live", source="test")
turn_holder = f"pid={os.getpid()}:turn=1"
assert db.try_acquire_session_turn_lease("empty-live", turn_holder, ttl_seconds=300.0) is True
assert db.delete_session_if_empty("empty-live") is False
assert db.get_session("empty-live") is not None
db.release_session_turn_lease("empty-live", turn_holder)
assert db.delete_session_if_empty("empty-live") is True
assert db.get_session("empty-live") is None
db.close()