hermes-agent/tests/agent/test_file_safety_cross_profile.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

184 lines
7.4 KiB
Python

"""Tests for the cross-Hermes-profile write guard in agent/file_safety.
The guard fires when a tool tries to write into another Hermes profile's
skills/plugins/cron/memories directory. It's a soft guard — defense in
depth, NOT a security boundary — but it prevents the agent from silently
corrupting a profile that belongs to a different session.
Reference: May 2026 incident — a hermes-security profile session
accidentally edited skills under both ~/.hermes/profiles/hermes-security/skills/
AND ~/.hermes/skills/ (the default profile's skills), realizing only
afterwards that the second path belonged to a different profile.
"""
from __future__ import annotations
from pathlib import Path
import pytest
# ---------------------------------------------------------------------------
# Helpers — set up a fake Hermes root with two profiles, monkeypatch the
# resolver helpers so the classifier sees the test layout.
# ---------------------------------------------------------------------------
@pytest.fixture
def fake_hermes(tmp_path, monkeypatch):
"""Build a fake Hermes layout:
<tmp>/
skills/foo/SKILL.md # default profile
plugins/foo/__init__.py
cron/<state>
memories/MEMORY.md
profiles/
hermes-security/
skills/foo/SKILL.md # named profile
plugins/...
coder/
skills/foo/SKILL.md # another named profile
"""
root = tmp_path / "fake-hermes"
(root / "skills" / "foo").mkdir(parents=True)
(root / "skills" / "foo" / "SKILL.md").write_text("# default skill\n")
(root / "plugins" / "foo").mkdir(parents=True)
(root / "memories").mkdir(parents=True)
(root / "cron").mkdir(parents=True)
sec_home = root / "profiles" / "hermes-security"
(sec_home / "skills" / "foo").mkdir(parents=True)
(sec_home / "skills" / "foo" / "SKILL.md").write_text("# sec skill\n")
(sec_home / "plugins").mkdir(parents=True)
coder_home = root / "profiles" / "coder"
(coder_home / "skills" / "foo").mkdir(parents=True)
(coder_home / "skills" / "foo" / "SKILL.md").write_text("# coder skill\n")
# Monkeypatch the resolver functions used by file_safety so each test
# can choose which profile is "active".
import hermes_constants
monkeypatch.setattr(hermes_constants, "get_default_hermes_root", lambda: root)
# The reloads below ensure get_cross_profile_warning/classify see the patched root.
import agent.file_safety as fs
monkeypatch.setattr(fs, "_hermes_root_path", lambda: root)
return {
"root": root,
"default_home": root,
"security_home": sec_home,
"coder_home": coder_home,
}
def _set_active_home(monkeypatch, hermes_home: Path):
"""Point file_safety._hermes_home_path at a specific profile dir."""
import agent.file_safety as fs
monkeypatch.setattr(fs, "_hermes_home_path", lambda: hermes_home)
# ---------------------------------------------------------------------------
# _resolve_active_profile_name
# ---------------------------------------------------------------------------
class TestResolveActiveProfileName:
def test_default_when_home_is_root(self, fake_hermes, monkeypatch):
_set_active_home(monkeypatch, fake_hermes["default_home"])
from agent.file_safety import _resolve_active_profile_name
assert _resolve_active_profile_name() == "default"
def test_falls_back_to_default_on_resolution_failure(self, fake_hermes, monkeypatch):
"""If HERMES_HOME resolution raises, return 'default' rather than crashing the tool."""
import agent.file_safety as fs
def _boom():
raise RuntimeError("simulated")
monkeypatch.setattr(fs, "_hermes_home_path", _boom)
# Should not raise — falls back to "default"
assert fs._resolve_active_profile_name() == "default"
# ---------------------------------------------------------------------------
# classify_cross_profile_target
# ---------------------------------------------------------------------------
class TestClassifyCrossProfileTarget:
def test_security_writing_default_skill(self, fake_hermes, monkeypatch):
"""The exact incident from May 2026."""
_set_active_home(monkeypatch, fake_hermes["security_home"])
from agent.file_safety import classify_cross_profile_target
result = classify_cross_profile_target(
str(fake_hermes["default_home"] / "skills" / "foo" / "SKILL.md")
)
assert result is not None
assert result["active_profile"] == "hermes-security"
assert result["target_profile"] == "default"
assert result["area"] == "skills"
def test_default_writing_security_skill(self, fake_hermes, monkeypatch):
"""Inverse direction — default-profile session reaching into a named profile."""
_set_active_home(monkeypatch, fake_hermes["default_home"])
from agent.file_safety import classify_cross_profile_target
result = classify_cross_profile_target(
str(fake_hermes["security_home"] / "skills" / "foo" / "SKILL.md")
)
assert result is not None
assert result["active_profile"] == "default"
assert result["target_profile"] == "hermes-security"
@pytest.mark.parametrize("area", ["skills", "plugins", "cron", "memories"])
def test_all_profile_scoped_areas_classified(self, fake_hermes, monkeypatch, area):
_set_active_home(monkeypatch, fake_hermes["security_home"])
from agent.file_safety import classify_cross_profile_target
target = fake_hermes["default_home"] / area / "foo.txt"
result = classify_cross_profile_target(str(target))
assert result is not None
assert result["area"] == area
# ---------------------------------------------------------------------------
# get_cross_profile_warning
# ---------------------------------------------------------------------------
class TestGetCrossProfileWarning:
def test_in_profile_returns_none(self, fake_hermes, monkeypatch):
_set_active_home(monkeypatch, fake_hermes["security_home"])
from agent.file_safety import get_cross_profile_warning
assert get_cross_profile_warning(
str(fake_hermes["security_home"] / "skills" / "foo" / "SKILL.md")
) is None
def test_cross_profile_warning_names_both_profiles(self, fake_hermes, monkeypatch):
_set_active_home(monkeypatch, fake_hermes["security_home"])
from agent.file_safety import get_cross_profile_warning
warn = get_cross_profile_warning(
str(fake_hermes["default_home"] / "skills" / "foo" / "SKILL.md")
)
assert warn is not None
# Must name BOTH profiles so the model knows which is which.
assert "default" in warn
assert "hermes-security" in warn
# Must name the bypass kwarg.
assert "cross_profile=True" in warn
# Must reference the area.
assert "skills" in warn
def test_warning_is_defense_in_depth_not_boundary(self, fake_hermes, monkeypatch):
_set_active_home(monkeypatch, fake_hermes["security_home"])
from agent.file_safety import get_cross_profile_warning
warn = get_cross_profile_warning(
str(fake_hermes["default_home"] / "skills" / "foo" / "SKILL.md")
)
# Must self-document as defense-in-depth so future reviewers
# don't promote it to a hard block.
assert "not a security boundary" in warn.lower()