hermes-agent/tests/test_hermes_state_readonly_preflight.py
Teknium 6b81590c55
test: prune low-value tests suite-wide (wave 1) — 46,820 → 28,106 test functions
Systematic prune per AGENTS.md test policy, one pass over every major
test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli,
cron, tui_gateway, honcho/openviking, root-level):

- DELETE: source-reading tests (read_text/getsource on prod files),
  change-detector tests (exact catalog counts, model-name snapshots,
  config version literals), mock-echo tests (assert a mock returns what
  it was told), assertion-free/trivial tests, near-duplicate
  parametrizations (boundaries + one representative kept), async/sync
  twin duplicates, cosmetic within-file variations.
- KEEP (mandatory): security/redaction/approval guards, message-role
  alternation invariants, prompt-caching/deterministic-call-id
  invariants, issue-number regression tests (deduped), E2E tests.
- 6 test files deleted outright (script-style/no-assert or fully
  redundant); conftest.py, fakes/, fixtures/ untouched.
- tests/acp/conftest.py added: autouse fixture stubs the live
  models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server
  tests performed on every session create — test_server.py 147s → 3.4s,
  and the tests are now genuinely hermetic.
- Sleep-based slowness shrunk where safe (codex_ttfb_watchdog,
  compression_concurrent_fork, etc.); no wall-clock assertion tightened.

Verification: full hermetic suite via scripts/run_tests.sh —
2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall
(baseline: 583s wall, 13,564s subprocess CPU).
2026-07-29 13:10:23 -07:00

193 lines
6.2 KiB
Python

"""Tests for the read-only DB preflight (port of Kilo-Org/kilocode#12508).
A stray read-only ``state.db`` / ``-wal`` / ``-shm`` (sudo run, restored
backup, copied dotfiles) used to surface as an opaque
``sqlite3.OperationalError: attempt to write a readonly database`` raised
from deep inside ``_init_schema`` — naming no file and no fix.
``preflight_db_writability`` now runs before the first connection:
- files inside the Hermes home tree are repaired with ``chmod u+rw``
(the safe scope — chmod fails on files the user doesn't own);
- anything else fails fast with an error naming the exact file and the
exact ``chmod`` command;
- WAL sidecars are never deleted, so committed frames survive repair.
"""
import os
import sqlite3
import stat
import sys
from pathlib import Path
import pytest
import hermes_state
from hermes_state import SessionDB, preflight_db_writability
pytestmark = [
pytest.mark.skipif(sys.platform == "win32", reason="POSIX chmod semantics"),
pytest.mark.skipif(
hasattr(os, "geteuid") and os.geteuid() == 0,
reason="root bypasses file permission checks",
),
]
@pytest.fixture()
def hermes_home(tmp_path, monkeypatch):
"""Isolated HERMES_HOME so the repair scope covers tmp DBs."""
home = tmp_path / ".hermes"
home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(home))
return home
def _make_db(path: Path) -> None:
conn = sqlite3.connect(str(path))
conn.execute("CREATE TABLE t (x)")
conn.execute("INSERT INTO t VALUES (1)")
conn.commit()
conn.close()
def _make_wal_db(path: Path) -> None:
"""Create a WAL-mode DB with committed-but-uncheckpointed frames."""
conn = sqlite3.connect(str(path))
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("CREATE TABLE t (x)")
conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
# Keep a READ-ONLY second connection open so neither close can
# checkpoint: the writer skips checkpoint-on-close because another
# connection exists, and the ro holder cannot checkpoint at all.
# The committed row therefore lives only in the -wal file.
holder = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
holder.execute("SELECT 1").fetchone()
conn.execute("INSERT INTO t VALUES (42)")
conn.commit()
conn.close()
holder.close()
assert path.with_name(path.name + "-wal").is_file(), (
"fixture precondition: -wal sidecar must survive with pending frames"
)
class TestRepairScope:
def test_repairs_readonly_db_inside_home(self, hermes_home):
db = hermes_home / "state.db"
_make_db(db)
os.chmod(db, 0o444)
preflight_db_writability(db, db_label="state.db")
assert os.access(db, os.W_OK)
def test_repairs_readonly_sidecars(self, hermes_home):
db = hermes_home / "state.db"
_make_wal_db(db)
wal = db.with_name(db.name + "-wal")
assert wal.is_file(), "fixture must leave a -wal behind"
os.chmod(db, 0o444)
os.chmod(wal, 0o444)
preflight_db_writability(db, db_label="state.db")
assert os.access(db, os.W_OK)
assert os.access(wal, os.W_OK)
def test_repairs_readonly_parent_directory(self, hermes_home):
sub = hermes_home / "kanban"
sub.mkdir()
db = sub / "kanban.db"
_make_db(db)
os.chmod(sub, 0o555)
try:
preflight_db_writability(db, db_label="kanban.db")
assert os.access(sub, os.W_OK)
finally:
os.chmod(sub, 0o755)
class TestRefusalOutsideScope:
def test_actionable_error_names_file_and_chmod(self, hermes_home, tmp_path):
outside = tmp_path / "elsewhere"
outside.mkdir()
db = outside / "custom.db"
_make_db(db)
os.chmod(db, 0o444)
try:
with pytest.raises(sqlite3.OperationalError) as exc_info:
preflight_db_writability(db, db_label="custom.db")
msg = str(exc_info.value)
assert str(db) in msg
assert "chmod u+rw" in msg
# Must NOT have silently chmod'd a file outside the home tree.
assert not os.access(db, os.W_OK)
finally:
os.chmod(db, 0o644)
def test_wal_error_warns_against_deletion(self, hermes_home, tmp_path):
outside = tmp_path / "elsewhere"
outside.mkdir()
db = outside / "custom.db"
_make_wal_db(db)
wal = db.with_name(db.name + "-wal")
os.chmod(wal, 0o444)
try:
with pytest.raises(sqlite3.OperationalError) as exc_info:
preflight_db_writability(db, db_label="custom.db")
msg = str(exc_info.value)
assert str(wal) in msg
assert "Do NOT delete" in msg
finally:
os.chmod(wal, 0o644)
class TestSkips:
def test_healthy_db_untouched(self, hermes_home):
db = hermes_home / "state.db"
_make_db(db)
before = stat.S_IMODE(db.stat().st_mode)
preflight_db_writability(db)
assert stat.S_IMODE(db.stat().st_mode) == before
class TestSessionDBIntegration:
def test_sessiondb_selfheals_readonly_db_in_home(self, hermes_home):
db_path = hermes_home / "state.db"
first = SessionDB(db_path)
first.close()
for suffix in ("", "-wal", "-shm"):
p = db_path.with_name(db_path.name + suffix)
if p.is_file():
os.chmod(p, 0o444)
db = SessionDB(db_path) # must not raise "readonly database"
try:
assert os.access(db_path, os.W_OK)
finally:
db.close()
def test_sessiondb_actionable_error_outside_home(
self, hermes_home, tmp_path
):
outside = tmp_path / "custom-loc"
outside.mkdir()
db_path = outside / "state.db"
first = SessionDB(db_path)
first.close()
os.chmod(db_path, 0o444)
hermes_state._set_last_init_error(None)
try:
with pytest.raises(sqlite3.OperationalError) as exc_info:
SessionDB(db_path)
msg = str(exc_info.value)
assert str(db_path) in msg
assert "chmod" in msg
finally:
os.chmod(db_path, 0o644)
hermes_state._set_last_init_error(None)