Files
hermes-agent/tests/hermes_state/test_delete_session_write_guards.py
kshitijk4poor 1a6a9b66b9 fix(sessions): guard delegate children that a delete would cascade
delete_session/delete_sessions cascade-delete delegate children, but the
write-guard check only looked at the root. A guarded delegate child could be
removed out from under its live turn, and in bulk delete an active id that
was also another selected root's delegate child was reported in
skipped_active while the cascade deleted it anyway.

Check {root, *delegate children} via a small _guarded_ids helper: single
delete refuses, bulk delete skips the root, so the cascade never touches a
guarded row. Ports the delegate-protection idea from #124496.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
2026-09-27 20:49:04 +05:30

92 lines
4.2 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()