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

523 lines
15 KiB
Python

"""Hermetic tests for the Bitwarden Secrets Manager integration.
We never hit GitHub or Bitwarden in tests — subprocess + urllib are
mocked so the suite stays fast and offline-safe. The "live" pull and
binary download are exercised manually by `hermes secrets bitwarden
setup` outside of pytest.
"""
from __future__ import annotations
import hashlib
import io
import json
import os
import stat
import subprocess
import sys
import time
import zipfile
from pathlib import Path
from unittest import mock
import pytest
# Make the worktree importable without depending on the installed wheel.
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from agent.secret_sources import bitwarden as bw # noqa: E402
@pytest.fixture(autouse=True)
def _reset_caches():
bw._reset_cache_for_tests()
yield
bw._reset_cache_for_tests()
@pytest.fixture
def hermes_home(tmp_path, monkeypatch):
"""Point Hermes at an isolated home directory."""
home = tmp_path / ".hermes"
home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(home))
# Some modules cache get_hermes_home; clear if needed.
import hermes_constants
if hasattr(hermes_constants, "_HERMES_HOME_CACHE"):
hermes_constants._HERMES_HOME_CACHE = None # type: ignore[attr-defined]
return home
# ---------------------------------------------------------------------------
# _platform_asset_name
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"system,machine,libc_text,expected",
[
("Darwin", "x86_64", "",
f"bws-macos-universal-{bw._BWS_VERSION}.zip"),
("Darwin", "arm64", "",
f"bws-macos-universal-{bw._BWS_VERSION}.zip"),
("Linux", "x86_64", "glibc",
f"bws-x86_64-unknown-linux-gnu-{bw._BWS_VERSION}.zip"),
("Linux", "x86_64", "musl libc",
f"bws-x86_64-unknown-linux-musl-{bw._BWS_VERSION}.zip"),
("Linux", "aarch64", "",
f"bws-aarch64-unknown-linux-gnu-{bw._BWS_VERSION}.zip"),
("Windows", "AMD64", "",
f"bws-x86_64-pc-windows-msvc-{bw._BWS_VERSION}.zip"),
("Windows", "ARM64", "",
f"bws-aarch64-pc-windows-msvc-{bw._BWS_VERSION}.zip"),
],
)
def test_platform_asset_name(system, machine, libc_text, expected):
with mock.patch.object(bw.platform, "system", return_value=system), \
mock.patch.object(bw.platform, "machine", return_value=machine), \
mock.patch.object(
bw.subprocess,
"run",
return_value=mock.Mock(stdout=libc_text, stderr=libc_text),
):
assert bw._platform_asset_name() == expected
# ---------------------------------------------------------------------------
# install_bws — fully mocked HTTP
# ---------------------------------------------------------------------------
def _make_fake_zip(binary_bytes: bytes) -> bytes:
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr("bws", binary_bytes)
return buf.getvalue()
# ---------------------------------------------------------------------------
# _safe_extract_member — zip-slip containment
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"evil_name",
[
"../escape",
"../../escape",
"sub/../../escape",
],
)
def test_safe_extract_member_rejects_traversal(tmp_path, evil_name):
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr(evil_name, b"pwned")
buf.seek(0)
dest = tmp_path / "extract"
dest.mkdir()
outside = tmp_path / "escape"
with zipfile.ZipFile(buf) as zf:
with pytest.raises(RuntimeError, match="unsafe archive member"):
bw._safe_extract_member(zf, evil_name, dest)
# The traversal target must not have been written.
assert not outside.exists()
def test_install_bws_happy_path(hermes_home, monkeypatch):
fake_binary = b"#!/bin/sh\necho 'bws fake 2.0.0'\n"
zip_bytes = _make_fake_zip(fake_binary)
asset_name = bw._platform_asset_name()
checksum_text = (
f"{hashlib.sha256(zip_bytes).hexdigest()} {asset_name}\n"
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff other-file\n"
)
def fake_download(url, dest):
if url.endswith(".zip"):
Path(dest).write_bytes(zip_bytes)
elif url.endswith(".txt"):
Path(dest).write_text(checksum_text)
else:
raise AssertionError(f"unexpected download url: {url}")
monkeypatch.setattr(bw, "_http_download", fake_download)
path = bw.install_bws()
assert path.exists()
assert path.read_bytes() == fake_binary
# Executable bit set
assert path.stat().st_mode & stat.S_IXUSR
# ---------------------------------------------------------------------------
# fetch_bitwarden_secrets
# ---------------------------------------------------------------------------
def _fake_bws_payload(items):
return json.dumps(items)
def test_fetch_server_url_sets_env(monkeypatch, tmp_path):
"""server_url must be plumbed into the subprocess as BWS_SERVER_URL."""
fake_binary = tmp_path / "bws"
fake_binary.write_text("")
payload = _fake_bws_payload([{"key": "K", "value": "v"}])
captured_env = {}
def fake_run(cmd, **kwargs):
captured_env.update(kwargs["env"])
return mock.Mock(returncode=0, stdout=payload, stderr="")
monkeypatch.setattr(bw.subprocess, "run", fake_run)
bw.fetch_bitwarden_secrets(
access_token="0.t",
project_id="p",
binary=fake_binary,
use_cache=False,
server_url="https://vault.bitwarden.eu",
)
assert captured_env.get("BWS_SERVER_URL") == "https://vault.bitwarden.eu"
# ---------------------------------------------------------------------------
# apply_bitwarden_secrets — the public entry point used by env_loader
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# env_loader integration
# ---------------------------------------------------------------------------
def test_env_loader_calls_bsm_when_enabled(tmp_path, monkeypatch):
home = tmp_path / ".hermes"
home.mkdir()
(home / "config.yaml").write_text(
"secrets:\n"
" bitwarden:\n"
" enabled: true\n"
" project_id: 'proj-1'\n"
" access_token_env: 'BWS_ACCESS_TOKEN'\n"
" cache_ttl_seconds: 0\n"
" override_existing: false\n"
" auto_install: false\n"
)
monkeypatch.setenv("HERMES_HOME", str(home))
monkeypatch.setenv("BWS_ACCESS_TOKEN", "0.t")
monkeypatch.delenv("MY_BSM_KEY", raising=False)
called = {"n": 0}
def fake_fetch(**kwargs):
called["n"] += 1
assert kwargs["project_id"] == "proj-1"
return {"MY_BSM_KEY": "from-bsm"}, []
monkeypatch.setattr(
"agent.secret_sources.bitwarden.find_bws",
lambda **_kw: Path("/fake/bws"),
)
monkeypatch.setattr(
"agent.secret_sources.bitwarden.fetch_bitwarden_secrets",
fake_fetch,
)
from agent.secret_sources import registry as reg_module
reg_module._reset_registry_for_tests()
from hermes_cli.env_loader import _apply_external_secret_sources
_apply_external_secret_sources(home)
assert called["n"] == 1
assert os.environ.get("MY_BSM_KEY") == "from-bsm"
# ---------------------------------------------------------------------------
# Disk-persisted cache (cross-process — speeds up back-to-back CLI invocations)
# ---------------------------------------------------------------------------
def test_disk_cache_key_mismatch_triggers_refetch(monkeypatch, tmp_path):
"""Disk cache entry written by a different token/project is ignored."""
home = tmp_path / ".hermes"
home.mkdir()
fake_binary = tmp_path / "bws"
fake_binary.write_text("")
payload = _fake_bws_payload([{"key": "K1", "value": "v1"}])
call_count = {"n": 0}
def fake_run(*a, **kw):
call_count["n"] += 1
return mock.Mock(returncode=0, stdout=payload, stderr="")
monkeypatch.setattr(bw.subprocess, "run", fake_run)
bw._reset_cache_for_tests(home)
# Write a cache entry for a DIFFERENT token/project pair
cache_path = bw._disk_cache_path(home)
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.write_text(json.dumps({
"key": "deadbeef00000000|other-project|",
"secrets": {"OTHER": "should-not-leak"},
"fetched_at": time.time(),
}))
secrets, _ = bw.fetch_bitwarden_secrets(
access_token="0.t", project_id="proj-1", binary=fake_binary,
cache_ttl_seconds=300, home_path=home,
)
# We must NOT have used the foreign cache entry
assert secrets == {"K1": "v1"}
assert "OTHER" not in secrets
assert call_count["n"] == 1
def test_encrypted_cache_writes_without_plaintext(monkeypatch, tmp_path):
"""Encrypted cache stores last-good secrets without raw values on disk."""
home = tmp_path / ".hermes"
home.mkdir()
fake_binary = tmp_path / "bws"
fake_binary.write_text("")
payload = _fake_bws_payload([{"key": "K1", "value": "secret-value"}])
monkeypatch.setattr(
bw.subprocess,
"run",
lambda *a, **kw: mock.Mock(returncode=0, stdout=payload, stderr=""),
)
bw._reset_cache_for_tests(home)
# A successful encrypted write must remove a pre-existing legacy plaintext
# cache from the migration path.
legacy_key = (bw._token_fingerprint("0.t"), "proj-1", "")
bw._DISK_CACHE.write(
legacy_key,
bw._CachedFetch(secrets={"K1": "legacy"}, fetched_at=time.time()),
300,
home,
)
assert bw._disk_cache_path(home).exists()
secrets, warnings = bw.fetch_bitwarden_secrets(
access_token="0.t", project_id="proj-1", binary=fake_binary,
cache_ttl_seconds=0, encrypted_cache_enabled=True,
encrypted_cache_max_stale_seconds=604800, home_path=home,
)
assert secrets == {"K1": "secret-value"}
assert warnings == []
assert not bw._disk_cache_path(home).exists()
cache_path = bw._encrypted_disk_cache_path(home)
assert cache_path.exists()
mode = stat.S_IMODE(os.stat(cache_path).st_mode)
assert mode == 0o600, f"expected 0o600, got 0o{mode:o}"
text = cache_path.read_text()
assert "secret-value" not in text
assert "0.t" not in text
payload_disk = json.loads(text)
assert set(payload_disk.keys()) == {
"version", "key", "salt", "nonce", "ciphertext",
}
assert not bw._disk_cache_path(home).exists()
def test_encrypted_cache_falls_back_on_network_error(monkeypatch, tmp_path):
"""A fresh-enough encrypted cache is used when BWS is unreachable."""
home = tmp_path / ".hermes"
home.mkdir()
fake_binary = tmp_path / "bws"
fake_binary.write_text("")
calls = {"n": 0}
def fake_run(*a, **kw):
calls["n"] += 1
if calls["n"] == 1:
return mock.Mock(
returncode=0,
stdout=_fake_bws_payload([{"key": "K1", "value": "cached"}]),
stderr="",
)
return mock.Mock(
returncode=1,
stdout="",
stderr="Error: network is unreachable",
)
monkeypatch.setattr(bw.subprocess, "run", fake_run)
bw._reset_cache_for_tests(home)
first, _ = bw.fetch_bitwarden_secrets(
access_token="0.t", project_id="proj-1", binary=fake_binary,
cache_ttl_seconds=0, encrypted_cache_enabled=True,
encrypted_cache_max_stale_seconds=604800, home_path=home,
)
assert first == {"K1": "cached"}
bw._CACHE.clear()
second, warnings = bw.fetch_bitwarden_secrets(
access_token="0.t", project_id="proj-1", binary=fake_binary,
cache_ttl_seconds=0, encrypted_cache_enabled=True,
encrypted_cache_max_stale_seconds=604800, home_path=home,
)
assert second == {"K1": "cached"}
assert calls["n"] == 2
assert len(warnings) == 1
assert "stale ENCRYPTED disk cache" in warnings[0]
assert "bws live fetch failed" in warnings[0]
# ---------------------------------------------------------------------------
# Stale disk cache fallback when live bws fetch fails
# ---------------------------------------------------------------------------
def _seed_stale_disk_cache(home, *, secrets, age_seconds, project_id="proj-1",
access_token="0.t", server_url=""):
"""Populate the disk cache as if a successful fetch happened `age_seconds`
ago. Writes the JSON payload directly (same shape the shared DiskCache
reads/writes) rather than going through DiskCache.write, since that
would honor cache_ttl_seconds and refuse to persist an already-"stale"
entry — this needs to land on disk regardless of TTL."""
cache_key = (
bw._token_fingerprint(access_token), project_id, server_url,
)
cache_path = bw._disk_cache_path(home)
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.write_text(json.dumps({
"key": bw._cache_key_str(cache_key),
"secrets": secrets,
"fetched_at": time.time() - age_seconds,
}))
def test_stale_disk_cache_returned_when_bws_fails(monkeypatch, tmp_path):
"""When bws fails and the disk cache is stale, return the stale secrets
with a warning rather than raising."""
home = tmp_path / ".hermes"
home.mkdir()
fake_binary = tmp_path / "bws"
fake_binary.write_text("")
bw._reset_cache_for_tests(home)
# Seed a stale (older than TTL) disk cache from a previous successful fetch
_seed_stale_disk_cache(home, secrets={"OPENAI_API_KEY": "sk-old"},
age_seconds=3600)
# Now simulate a BWS network failure
def fail_run(*a, **kw):
return mock.Mock(returncode=1, stdout="",
stderr="Error: dns resolution failed")
monkeypatch.setattr(bw.subprocess, "run", fail_run)
secrets, warnings = bw.fetch_bitwarden_secrets(
access_token="0.t", project_id="proj-1", binary=fake_binary,
cache_ttl_seconds=300, home_path=home,
)
assert secrets == {"OPENAI_API_KEY": "sk-old"}
assert len(warnings) == 1
assert "stale disk cache" in warnings[0]
assert "dns resolution failed" in warnings[0]
def test_stale_fallback_skipped_on_auth_failure(monkeypatch, tmp_path):
"""An AUTH_FAILED bws error must raise, not serve stale secrets — a bad
access token indicates a real credential problem the caller needs to
see, not a transient outage worth papering over."""
home = tmp_path / ".hermes"
home.mkdir()
fake_binary = tmp_path / "bws"
fake_binary.write_text("")
bw._reset_cache_for_tests(home)
_seed_stale_disk_cache(home, secrets={"K1": "v1"}, age_seconds=3600)
monkeypatch.setattr(
bw.subprocess, "run",
lambda *a, **kw: mock.Mock(returncode=1, stdout="",
stderr="Error: unauthorized (401)"),
)
with pytest.raises(RuntimeError, match="unauthorized"):
bw.fetch_bitwarden_secrets(
access_token="0.t", project_id="proj-1", binary=fake_binary,
cache_ttl_seconds=300, home_path=home,
)