Hermes-agent
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
"""Tests for the secret-source tracking in ``hermes_cli.env_loader``.
|
||||
|
||||
These cover the small public surface that lets `hermes model` / `hermes setup`
|
||||
label detected credentials with their origin ("from Bitwarden") so users
|
||||
don't see an unexplained "credentials ✓" line when their .env is empty.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from hermes_cli import env_loader # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_sources():
|
||||
"""Each test starts with a clean source map and applied-home guard."""
|
||||
env_loader._SECRET_SOURCES.clear()
|
||||
env_loader.reset_secret_source_cache()
|
||||
yield
|
||||
env_loader._SECRET_SOURCES.clear()
|
||||
env_loader.reset_secret_source_cache()
|
||||
|
||||
|
||||
def test_get_secret_source_returns_none_for_untracked_var():
|
||||
assert env_loader.get_secret_source("ANTHROPIC_API_KEY") is None
|
||||
|
||||
|
||||
def test_get_secret_source_returns_label_for_tracked_var():
|
||||
env_loader._SECRET_SOURCES["ANTHROPIC_API_KEY"] = "bitwarden"
|
||||
assert env_loader.get_secret_source("ANTHROPIC_API_KEY") == "bitwarden"
|
||||
|
||||
|
||||
def test_format_secret_source_suffix_empty_for_untracked():
|
||||
# Credentials from .env or the shell shouldn't add noise — the
|
||||
# implicit case stays unlabeled.
|
||||
assert env_loader.format_secret_source_suffix("ANTHROPIC_API_KEY") == ""
|
||||
|
||||
|
||||
def test_format_secret_source_suffix_bitwarden_uses_proper_name():
|
||||
env_loader._SECRET_SOURCES["ANTHROPIC_API_KEY"] = "bitwarden"
|
||||
assert (
|
||||
env_loader.format_secret_source_suffix("ANTHROPIC_API_KEY")
|
||||
== " (from Bitwarden)"
|
||||
)
|
||||
|
||||
|
||||
def test_format_secret_source_suffix_generic_label_for_future_sources():
|
||||
# Future-proofing: a new secret source (e.g. "vault") should still
|
||||
# produce a sensible label without needing to edit every call site.
|
||||
env_loader._SECRET_SOURCES["OPENAI_API_KEY"] = "vault"
|
||||
assert (
|
||||
env_loader.format_secret_source_suffix("OPENAI_API_KEY")
|
||||
== " (from vault)"
|
||||
)
|
||||
|
||||
|
||||
def test_apply_external_secret_sources_records_bitwarden_origin(tmp_path, monkeypatch):
|
||||
"""End-to-end: when ``apply_bitwarden_secrets`` returns applied keys,
|
||||
they end up in ``_SECRET_SOURCES`` so the UI can label them."""
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
config_path = tmp_path / "config.yaml"
|
||||
config_path.write_text(
|
||||
"secrets:\n"
|
||||
" bitwarden:\n"
|
||||
" enabled: true\n"
|
||||
" project_id: test-project\n"
|
||||
" access_token_env: BWS_ACCESS_TOKEN\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
# Stub apply_bitwarden_secrets to return a synthetic FetchResult.
|
||||
from agent.secret_sources.bitwarden import FetchResult
|
||||
|
||||
fake_result = FetchResult(
|
||||
secrets={"ANTHROPIC_API_KEY": "sk-ant-test"},
|
||||
applied=["ANTHROPIC_API_KEY"],
|
||||
)
|
||||
|
||||
def _fake_apply(**_kwargs):
|
||||
return fake_result
|
||||
|
||||
# The import inside _apply_external_secret_sources is lazy, so we
|
||||
# patch the *module attribute* it will pull in.
|
||||
import agent.secret_sources.bitwarden as bw_module
|
||||
|
||||
monkeypatch.setattr(bw_module, "apply_bitwarden_secrets", _fake_apply)
|
||||
|
||||
env_loader._apply_external_secret_sources(tmp_path)
|
||||
|
||||
assert env_loader.get_secret_source("ANTHROPIC_API_KEY") == "bitwarden"
|
||||
assert (
|
||||
env_loader.format_secret_source_suffix("ANTHROPIC_API_KEY")
|
||||
== " (from Bitwarden)"
|
||||
)
|
||||
|
||||
|
||||
def test_apply_external_secret_sources_noop_when_disabled(tmp_path, monkeypatch):
|
||||
"""Disabled Bitwarden config must not touch the source map."""
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
config_path = tmp_path / "config.yaml"
|
||||
config_path.write_text(
|
||||
"secrets:\n"
|
||||
" bitwarden:\n"
|
||||
" enabled: false\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
env_loader._apply_external_secret_sources(tmp_path)
|
||||
|
||||
assert env_loader.get_secret_source("ANTHROPIC_API_KEY") is None
|
||||
|
||||
|
||||
def test_apply_external_secret_sources_dedupes_within_process(tmp_path, monkeypatch):
|
||||
"""``load_hermes_dotenv()`` is called at module-import time from several
|
||||
hot modules (cli.py, hermes_cli/main.py, run_agent.py, ...). The
|
||||
Bitwarden status line previously printed once per call — 3-5x per
|
||||
startup. The applied-home guard must short-circuit subsequent calls
|
||||
so the heavy work (config re-parse, Bitwarden lookup, status print)
|
||||
runs exactly once per HERMES_HOME per process.
|
||||
"""
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
config_path = tmp_path / "config.yaml"
|
||||
config_path.write_text(
|
||||
"secrets:\n"
|
||||
" bitwarden:\n"
|
||||
" enabled: true\n"
|
||||
" project_id: test-project\n"
|
||||
" access_token_env: BWS_ACCESS_TOKEN\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
from agent.secret_sources.bitwarden import FetchResult
|
||||
|
||||
call_count = {"n": 0}
|
||||
|
||||
def _fake_apply(**_kwargs):
|
||||
call_count["n"] += 1
|
||||
return FetchResult(
|
||||
secrets={"ANTHROPIC_API_KEY": "sk-ant-test"},
|
||||
applied=["ANTHROPIC_API_KEY"],
|
||||
)
|
||||
|
||||
import agent.secret_sources.bitwarden as bw_module
|
||||
monkeypatch.setattr(bw_module, "apply_bitwarden_secrets", _fake_apply)
|
||||
|
||||
# Five calls in a row, simulating module-import-time invocations from
|
||||
# cli.py, hermes_cli/main.py, run_agent.py, trajectory_compressor.py,
|
||||
# gateway/run.py. Only the first should actually call the backend.
|
||||
for _ in range(5):
|
||||
env_loader._apply_external_secret_sources(tmp_path)
|
||||
|
||||
assert call_count["n"] == 1, (
|
||||
"Bitwarden backend was called {} time(s); expected exactly 1 — "
|
||||
"the applied-home guard is broken.".format(call_count["n"])
|
||||
)
|
||||
|
||||
# Source tracking still works after dedup.
|
||||
assert env_loader.get_secret_source("ANTHROPIC_API_KEY") == "bitwarden"
|
||||
|
||||
# reset_secret_source_cache() forces a fresh pull on the next call.
|
||||
env_loader.reset_secret_source_cache()
|
||||
env_loader._apply_external_secret_sources(tmp_path)
|
||||
assert call_count["n"] == 2
|
||||
Reference in New Issue
Block a user