mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
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).
193 lines
6.2 KiB
Python
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)
|