Files
hermes-agent/tests/hermes_state/test_state_db_holders.py
ethernet 8eb20a50ae Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	agent/learning_graph.py
#	hermes_cli/backup.py
#	tests/hermes_cli/test_backup.py
#	tests/hermes_cli/test_doctor_journal_modes.py
#	tests/hermes_cli/test_sessions_set_journal_mode.py
2026-09-24 08:51:52 -04:00

114 lines
3.8 KiB
Python

"""Behavioral tests for the state-holder and repair-admission authority."""
import os
from types import SimpleNamespace
import pytest
import hermes_state_holders
@pytest.mark.platforms("linux")
@pytest.mark.parametrize("identity", ["alias", "different-inode", "different-device"])
def test_foreign_holder_uses_device_and_inode_not_path(
tmp_path, monkeypatch, identity
):
"""Descriptor identity is authoritative even when /proc spells another path."""
db_path = tmp_path / "state.db"
db_path.touch()
alias_path = tmp_path / "namespace-alias" / "state.db" if identity == "alias" else db_path
proc_root = tmp_path / "proc"
for pid in (111, 222):
(proc_root / str(pid) / "fd").mkdir(parents=True)
os.symlink(db_path, proc_root / "222" / "fd" / "3")
# Keep the /proc projection local: patching os globally also intercepts
# pytest's home guard and pathlib while they inspect these same symlinks.
projected_os = SimpleNamespace(**vars(os))
projected_os.getpid = lambda: 111
monkeypatch.setattr(hermes_state_holders, "os", projected_os)
real_listdir = os.listdir
def _listdir(path):
if isinstance(path, str):
path = path.replace("/proc", str(proc_root))
return real_listdir(path)
monkeypatch.setattr(hermes_state_holders.os, "listdir", _listdir)
def _readlink(path):
if path == "/proc/222/fd/3":
return str(alias_path)
return os.readlink(path.replace("/proc", str(proc_root)))
monkeypatch.setattr(hermes_state_holders.os, "readlink", _readlink)
real_stat = os.stat
def _stat(path, *args, **kwargs):
mapped = str(path).replace("/proc", str(proc_root))
result = real_stat(mapped, *args, **kwargs)
if str(path) == "/proc/222/fd/3" and identity != "alias":
values = list(result)
values[1 if identity == "different-inode" else 2] += 1
return os.stat_result(values)
return result
monkeypatch.setattr(hermes_state_holders.os, "stat", _stat)
assert hermes_state_holders.foreign_state_db_holders(db_path) == (
[(222, str(alias_path))] if identity == "alias" else []
)
def test_windows_restart_manager_scan_sizes_then_excludes_self(monkeypatch, tmp_path):
"""The rstrtmgr lane sizes on ERROR_MORE_DATA, drops our own pid, and fails closed."""
import ctypes
db_path = tmp_path / "state.db"
db_path.touch()
(tmp_path / "state.db-wal").touch()
calls = []
class _Fn:
def __init__(self, impl):
self.impl = impl
def __call__(self, *args):
return self.impl(*args)
def start(session, _flags, _key):
session._obj.value = 7
return 0
def register(_session, count, names, *_rest):
calls.append(("register", count, sorted(names)))
return 0
def get_list(_session, needed, count, apps, _reasons):
needed._obj.value = 2
if apps is None:
return 234 # ERROR_MORE_DATA: sizing call
apps[0].process.pid = os.getpid()
apps[1].process.pid = 4242
count._obj.value = 2
return 0
class _Api:
RmStartSession = _Fn(start)
RmRegisterResources = _Fn(register)
RmGetList = _Fn(get_list)
RmEndSession = _Fn(lambda _session: calls.append("end") or 0)
monkeypatch.setattr(ctypes, "WinDLL", lambda *a, **k: _Api(), raising=False)
holders = hermes_state_holders._windows_restart_manager_holders(db_path)
assert [pid for pid, _ in holders] == [4242]
assert calls[0] == ("register", 2, sorted(str(p) for p in (db_path, tmp_path / "state.db-wal")))
assert calls[-1] == "end"
_Api.RmStartSession = _Fn(lambda *_args: 5)
with pytest.raises(OSError):
hermes_state_holders._windows_restart_manager_holders(db_path)