mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
Use database.journal_mode as the sole non-secret operator setting, preserve the vulnerable-SQLite safety gate and existing WAL databases, validate explicit DELETE results, document the active config path, and cover real SQLite openers with behavioral tests.
228 lines
7.7 KiB
Python
228 lines
7.7 KiB
Python
"""Behavioral coverage for #68545's centralized journal-mode setting."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
|
|
def _write_config(monkeypatch: pytest.MonkeyPatch, tmp_path, config: object) -> None:
|
|
home = tmp_path / "hermes-home"
|
|
home.mkdir(exist_ok=True)
|
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
(home / "config.yaml").write_text(
|
|
yaml.safe_dump(config),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def _configure_mode(monkeypatch: pytest.MonkeyPatch, tmp_path, mode: object) -> None:
|
|
_write_config(monkeypatch, tmp_path, {"database": {"journal_mode": mode}})
|
|
|
|
|
|
def _disable_vulnerable_gate(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(
|
|
"hermes_state.is_sqlite_wal_reset_vulnerable",
|
|
lambda **kwargs: False,
|
|
)
|
|
|
|
|
|
def test_database_journal_mode_has_a_canonical_default():
|
|
from hermes_cli.config import DEFAULT_CONFIG
|
|
|
|
assert DEFAULT_CONFIG["database"]["journal_mode"] == "wal"
|
|
|
|
|
|
def test_resolve_journal_mode_uses_real_database_config(monkeypatch, tmp_path):
|
|
from hermes_state import resolve_journal_mode
|
|
|
|
_configure_mode(monkeypatch, tmp_path, "DELETE")
|
|
assert resolve_journal_mode() == "delete"
|
|
|
|
|
|
def test_new_nonsecret_hermes_env_override_is_not_exposed(monkeypatch, tmp_path):
|
|
from hermes_state import resolve_journal_mode
|
|
|
|
_configure_mode(monkeypatch, tmp_path, "wal")
|
|
monkeypatch.setenv("HERMES_JOURNAL_MODE", "delete")
|
|
assert resolve_journal_mode() == "wal"
|
|
|
|
|
|
@pytest.mark.parametrize("value", ["bogus", "truncate", None, 42, {"bad": "shape"}])
|
|
def test_invalid_config_value_falls_back_to_wal(monkeypatch, tmp_path, value):
|
|
from hermes_state import resolve_journal_mode
|
|
|
|
_configure_mode(monkeypatch, tmp_path, value)
|
|
assert resolve_journal_mode() == "wal"
|
|
|
|
|
|
@pytest.mark.parametrize("database", [[], "delete", 42, None])
|
|
def test_malformed_database_section_falls_back_to_wal(
|
|
monkeypatch, tmp_path, database
|
|
):
|
|
from hermes_state import resolve_journal_mode
|
|
|
|
_write_config(monkeypatch, tmp_path, {"database": database})
|
|
assert resolve_journal_mode() == "wal"
|
|
|
|
|
|
def test_apply_wal_with_fallback_honors_delete_config(monkeypatch, tmp_path):
|
|
from hermes_state import apply_wal_with_fallback
|
|
|
|
_configure_mode(monkeypatch, tmp_path, "delete")
|
|
_disable_vulnerable_gate(monkeypatch)
|
|
conn = sqlite3.connect(tmp_path / "configured.db")
|
|
try:
|
|
assert apply_wal_with_fallback(conn, db_label="configured.db") == "delete"
|
|
assert conn.execute("PRAGMA journal_mode").fetchone()[0].lower() == "delete"
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_apply_wal_with_fallback_defaults_to_wal(monkeypatch, tmp_path):
|
|
from hermes_state import apply_wal_with_fallback
|
|
|
|
_configure_mode(monkeypatch, tmp_path, "wal")
|
|
_disable_vulnerable_gate(monkeypatch)
|
|
conn = sqlite3.connect(tmp_path / "default.db")
|
|
try:
|
|
assert apply_wal_with_fallback(conn, db_label="default.db") == "wal"
|
|
assert conn.execute("PRAGMA journal_mode").fetchone()[0].lower() == "wal"
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_configured_delete_validates_vulnerable_sqlite_result(monkeypatch, tmp_path):
|
|
"""The safety gate must not report DELETE when SQLite returns MEMORY."""
|
|
from hermes_state import apply_wal_with_fallback
|
|
|
|
_configure_mode(monkeypatch, tmp_path, "delete")
|
|
monkeypatch.setattr(
|
|
"hermes_state.is_sqlite_wal_reset_vulnerable",
|
|
lambda **kwargs: True,
|
|
)
|
|
conn = sqlite3.connect(":memory:")
|
|
try:
|
|
with pytest.raises(sqlite3.OperationalError, match="configured.*delete"):
|
|
apply_wal_with_fallback(conn, db_label="memory-configured.db")
|
|
assert conn.execute("PRAGMA journal_mode").fetchone()[0].lower() == "memory"
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_configured_delete_never_live_downgrades_existing_wal(monkeypatch, tmp_path):
|
|
from hermes_state import apply_wal_with_fallback
|
|
|
|
_configure_mode(monkeypatch, tmp_path, "delete")
|
|
db_path = tmp_path / "existing-wal.db"
|
|
conn = sqlite3.connect(db_path)
|
|
try:
|
|
assert conn.execute("PRAGMA journal_mode=WAL").fetchone()[0].lower() == "wal"
|
|
monkeypatch.setattr(
|
|
"hermes_state.is_sqlite_wal_reset_vulnerable",
|
|
lambda **kwargs: True,
|
|
)
|
|
assert apply_wal_with_fallback(conn, db_label="existing-wal.db") == "wal"
|
|
assert conn.execute("PRAGMA journal_mode").fetchone()[0].lower() == "wal"
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_real_db_openers_honor_configured_delete(monkeypatch, tmp_path):
|
|
"""All helper-routed file-backed openers must behaviorally use DELETE."""
|
|
_configure_mode(monkeypatch, tmp_path, "delete")
|
|
_disable_vulnerable_gate(monkeypatch)
|
|
|
|
from agent import verification_evidence
|
|
from cron import executions
|
|
from gateway import delivery_ledger
|
|
from gateway.platforms.api_server import ResponseStore
|
|
from hermes_cli import kanban_db, projects_db
|
|
from hermes_state import SessionDB
|
|
from plugins.memory.holographic.store import MemoryStore
|
|
from plugins.platforms.discord.recovery import DiscordRecoveryStore
|
|
from tools import async_delegation
|
|
|
|
observed: dict[str, str] = {}
|
|
|
|
for name, connect in (
|
|
("async_delegation", async_delegation._connect),
|
|
("delivery_ledger", delivery_ledger._connect),
|
|
("verification_evidence", verification_evidence._connect),
|
|
):
|
|
conn = connect()
|
|
try:
|
|
observed[name] = conn.execute("PRAGMA journal_mode").fetchone()[0].lower()
|
|
finally:
|
|
conn.close()
|
|
|
|
monkeypatch.setattr(executions, "EXECUTIONS_FILE", tmp_path / "cron" / "executions.db")
|
|
executions.EXECUTIONS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
cron_conn = executions._connect()
|
|
try:
|
|
executions._initialize_schema(cron_conn)
|
|
observed["cron_executions"] = cron_conn.execute(
|
|
"PRAGMA journal_mode"
|
|
).fetchone()[0].lower()
|
|
finally:
|
|
cron_conn.close()
|
|
|
|
discord = DiscordRecoveryStore(hermes_home=tmp_path)
|
|
observed["discord_recovery"] = discord.call(
|
|
lambda conn: conn.execute("PRAGMA journal_mode").fetchone()[0].lower()
|
|
)
|
|
|
|
session_db = SessionDB(db_path=tmp_path / "state.db")
|
|
try:
|
|
observed["session_db"] = session_db._conn.execute(
|
|
"PRAGMA journal_mode"
|
|
).fetchone()[0].lower()
|
|
finally:
|
|
session_db.close()
|
|
|
|
kanban_conn = kanban_db.connect(db_path=tmp_path / "kanban.db")
|
|
try:
|
|
observed["kanban"] = kanban_conn.execute(
|
|
"PRAGMA journal_mode"
|
|
).fetchone()[0].lower()
|
|
finally:
|
|
kanban_conn.close()
|
|
|
|
projects_conn = projects_db.connect(db_path=tmp_path / "projects.db")
|
|
try:
|
|
observed["projects"] = projects_conn.execute(
|
|
"PRAGMA journal_mode"
|
|
).fetchone()[0].lower()
|
|
finally:
|
|
projects_conn.close()
|
|
|
|
holographic = MemoryStore(db_path=tmp_path / "memory_store.db")
|
|
try:
|
|
observed["holographic"] = holographic._conn.execute(
|
|
"PRAGMA journal_mode"
|
|
).fetchone()[0].lower()
|
|
finally:
|
|
holographic.close()
|
|
|
|
response_store = ResponseStore(db_path=str(tmp_path / "response_store.db"))
|
|
try:
|
|
observed["response_store"] = response_store._conn.execute(
|
|
"PRAGMA journal_mode"
|
|
).fetchone()[0].lower()
|
|
finally:
|
|
response_store.close()
|
|
|
|
assert observed == {
|
|
"async_delegation": "delete",
|
|
"delivery_ledger": "delete",
|
|
"verification_evidence": "delete",
|
|
"cron_executions": "delete",
|
|
"discord_recovery": "delete",
|
|
"session_db": "delete",
|
|
"kanban": "delete",
|
|
"projects": "delete",
|
|
"holographic": "delete",
|
|
"response_store": "delete",
|
|
}
|