Files
hermes-agent/tests/hermes_state/test_state_db_second_process_maintenance.py

86 lines
3.3 KiB
Python

"""#103339 item 2: structural maintenance from a second PROCESS must refuse while ANY other process holds
state.db, even where SQLite's own lock probe is blind (``journal_mode=DELETE`` reader; malformed file)."""
from __future__ import annotations
import select
import subprocess
import sys
import uuid
from pathlib import Path
import pytest
from hermes_state import SessionDB
from hermes_state_repair import repair_state_db_schema
_HOLDER = """
import sqlite3, sys
conn = sqlite3.connect(sys.argv[1])
conn.execute("SELECT count(*) FROM sessions").fetchall()
print("ready", flush=True)
sys.stdin.read(1)
conn.close()
"""
@pytest.fixture
def delete_mode_db(tmp_path, monkeypatch) -> Path:
import hermes_state_wal
monkeypatch.setattr(hermes_state_wal, "resolve_journal_mode", lambda: "delete")
db = tmp_path / "state.db"
handle = SessionDB(db_path=db)
sid = handle.create_session(session_id=str(uuid.uuid4()), source="cli")
handle.append_message(sid, role="user", content="seed")
handle.close()
assert db.read_bytes()[18] == 1, "fixture must be a rollback-journal DB"
return db
@pytest.mark.linux_only
def test_repair_refuses_delete_mode_db_held_open_by_another_process(delete_mode_db):
"""A held DELETE-mode reader takes only SHARED, so ``BEGIN IMMEDIATE`` succeeds and the lock probe sees
nothing; the holder scan must still refuse — REINDEX/VACUUM from a second process is the #103339 class."""
db = delete_mode_db
holder = subprocess.Popen([sys.executable, "-c", _HOLDER, str(db)], stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
try:
assert select.select([holder.stdout], [], [], 10)[0] and holder.stdout.readline().strip() == "ready"
with open(db, "r+b") as fh: # schema page garbage under the holder: the shape repair is invoked on
fh.seek(100)
fh.write(b"\xff" * (4096 - 100))
report = repair_state_db_schema(db, backup=False)
finally:
holder.stdin.write("x")
holder.stdin.close()
holder.wait(timeout=10)
assert report["repaired"] is False
assert "stop the gateway" in (report["error"] or "").lower()
def test_holder_scan_sees_through_a_symlinked_home(tmp_path):
"""psutil/libproc report the resolved pathname; a holder opened via the real path must be found
when the scan is asked about the alias, or maintenance proceeds under a live writer. The Linux
/proc leg already compares inodes; the textual psutil leg (macOS) is the one this pins."""
import sqlite3
from hermes_state_holders import foreign_state_db_holders
real = tmp_path / "real-home"
real.mkdir()
alias = tmp_path / "alias-home"
alias.symlink_to(real, target_is_directory=True)
db = real / "state.db"
sqlite3.connect(db).execute("CREATE TABLE t(x)").connection.close()
holder = subprocess.Popen(
[sys.executable, "-c",
f"import sqlite3, sys, time; c = sqlite3.connect({str(db)!r}); c.execute('BEGIN IMMEDIATE'); "
"print('held', flush=True); time.sleep(30)"],
stdout=subprocess.PIPE, text=True)
try:
assert holder.stdout.readline().strip() == "held"
assert any(pid == holder.pid for pid, _ in foreign_state_db_holders(alias / "state.db"))
finally:
holder.kill()
holder.wait()