"""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.""" env_loader._SECRET_SOURCES.clear() yield env_loader._SECRET_SOURCES.clear() 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