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).
130 lines
4.8 KiB
Python
130 lines
4.8 KiB
Python
"""Tests for file permissions hardening on sensitive files."""
|
|
|
|
import os
|
|
import stat
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
|
|
class TestCronFilePermissions(unittest.TestCase):
|
|
"""Verify cron files get secure permissions."""
|
|
|
|
def setUp(self):
|
|
self.tmpdir = tempfile.mkdtemp()
|
|
self.cron_dir = Path(self.tmpdir) / "cron"
|
|
self.output_dir = self.cron_dir / "output"
|
|
|
|
def tearDown(self):
|
|
import shutil
|
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
|
|
|
@patch("cron.jobs.CRON_DIR")
|
|
@patch("cron.jobs.OUTPUT_DIR")
|
|
@patch("cron.jobs.JOBS_FILE")
|
|
def test_ensure_dirs_sets_0700(self, mock_jobs_file, mock_output, mock_cron):
|
|
mock_cron.__class__ = Path
|
|
# Use real paths
|
|
cron_dir = Path(self.tmpdir) / "cron"
|
|
output_dir = cron_dir / "output"
|
|
|
|
with patch("cron.jobs.CRON_DIR", cron_dir), \
|
|
patch("cron.jobs.OUTPUT_DIR", output_dir):
|
|
from cron.jobs import ensure_dirs
|
|
ensure_dirs()
|
|
|
|
cron_mode = stat.S_IMODE(os.stat(cron_dir).st_mode)
|
|
output_mode = stat.S_IMODE(os.stat(output_dir).st_mode)
|
|
self.assertEqual(cron_mode, 0o700)
|
|
self.assertEqual(output_mode, 0o700)
|
|
|
|
@patch("cron.jobs.CRON_DIR")
|
|
@patch("cron.jobs.OUTPUT_DIR")
|
|
@patch("cron.jobs.JOBS_FILE")
|
|
def test_save_jobs_sets_0600(self, mock_jobs_file, mock_output, mock_cron):
|
|
cron_dir = Path(self.tmpdir) / "cron"
|
|
output_dir = cron_dir / "output"
|
|
jobs_file = cron_dir / "jobs.json"
|
|
|
|
with patch("cron.jobs.CRON_DIR", cron_dir), \
|
|
patch("cron.jobs.OUTPUT_DIR", output_dir), \
|
|
patch("cron.jobs.JOBS_FILE", jobs_file):
|
|
from cron.jobs import save_jobs
|
|
save_jobs([{"id": "test", "prompt": "hello"}])
|
|
|
|
file_mode = stat.S_IMODE(os.stat(jobs_file).st_mode)
|
|
self.assertEqual(file_mode, 0o600)
|
|
|
|
def test_save_job_output_sets_0600(self):
|
|
output_dir = Path(self.tmpdir) / "output"
|
|
with patch("cron.jobs.OUTPUT_DIR", output_dir), \
|
|
patch("cron.jobs.CRON_DIR", Path(self.tmpdir)), \
|
|
patch("cron.jobs.ensure_dirs"):
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
from cron.jobs import save_job_output
|
|
output_file = save_job_output("test-job", "test output content")
|
|
|
|
file_mode = stat.S_IMODE(os.stat(output_file).st_mode)
|
|
self.assertEqual(file_mode, 0o600)
|
|
|
|
# Job output dir should also be 0700
|
|
job_dir = output_dir / "test-job"
|
|
dir_mode = stat.S_IMODE(os.stat(job_dir).st_mode)
|
|
self.assertEqual(dir_mode, 0o700)
|
|
|
|
|
|
class TestConfigFilePermissions(unittest.TestCase):
|
|
"""Verify config files get secure permissions."""
|
|
|
|
def setUp(self):
|
|
self.tmpdir = tempfile.mkdtemp()
|
|
|
|
def tearDown(self):
|
|
import shutil
|
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
|
|
|
def test_save_config_sets_0600(self):
|
|
config_path = Path(self.tmpdir) / "config.yaml"
|
|
with patch("hermes_cli.config.get_config_path", return_value=config_path), \
|
|
patch("hermes_cli.config.ensure_hermes_home"):
|
|
from hermes_cli.config import save_config
|
|
save_config({"model": "test/model"})
|
|
|
|
file_mode = stat.S_IMODE(os.stat(config_path).st_mode)
|
|
self.assertEqual(file_mode, 0o600)
|
|
|
|
def test_save_env_value_sets_0600(self):
|
|
env_path = Path(self.tmpdir) / ".env"
|
|
with patch("hermes_cli.config.get_env_path", return_value=env_path), \
|
|
patch("hermes_cli.config.ensure_hermes_home"):
|
|
from hermes_cli.config import save_env_value
|
|
save_env_value("TEST_KEY", "test_value")
|
|
|
|
file_mode = stat.S_IMODE(os.stat(env_path).st_mode)
|
|
self.assertEqual(file_mode, 0o600)
|
|
|
|
def test_ensure_hermes_home_sets_0700(self):
|
|
home = Path(self.tmpdir) / ".hermes"
|
|
with patch("hermes_cli.config.get_hermes_home", return_value=home):
|
|
from hermes_cli.config import ensure_hermes_home
|
|
ensure_hermes_home()
|
|
|
|
home_mode = stat.S_IMODE(os.stat(home).st_mode)
|
|
self.assertEqual(home_mode, 0o700)
|
|
|
|
for subdir in ("cron", "sessions", "logs", "memories"):
|
|
subdir_mode = stat.S_IMODE(os.stat(home / subdir).st_mode)
|
|
self.assertEqual(subdir_mode, 0o700, f"{subdir} should be 0700")
|
|
|
|
|
|
class TestSecureHelpers(unittest.TestCase):
|
|
"""Test the _secure_file and _secure_dir helpers."""
|
|
|
|
def test_secure_file_nonexistent_no_error(self):
|
|
from cron.jobs import _secure_file
|
|
_secure_file(Path("/nonexistent/path/file.json")) # Should not raise
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|