|
| 1 | +"""Secret resolver framework tests. |
| 2 | +
|
| 3 | +Covers entry-point discovery and the builtins < entry points < programmatic |
| 4 | +precedence chain (mirroring tests/run/test_sinks.py), the built-in env resolver, |
| 5 | +the per-run ContextVar carrier, and resolve_credential_refs. |
| 6 | +""" |
| 7 | + |
| 8 | +from __future__ import annotations |
| 9 | + |
| 10 | +import logging |
| 11 | +from unittest.mock import MagicMock, patch |
| 12 | + |
| 13 | +import pytest |
| 14 | + |
| 15 | +from agentevals.resolvers import ( |
| 16 | + EnvSecretResolver, |
| 17 | + build_resolver, |
| 18 | + clear_resolver_plugin_registry, |
| 19 | + create_env_resolver, |
| 20 | + get_resolved_credential, |
| 21 | + register_resolver_factory, |
| 22 | + registered_resolver_kinds, |
| 23 | + reset_resolved_credentials, |
| 24 | + resolve_credential_refs, |
| 25 | + set_resolved_credentials, |
| 26 | +) |
| 27 | + |
| 28 | + |
| 29 | +@pytest.fixture |
| 30 | +def isolated_resolver_plugins(): |
| 31 | + """``register_resolver_factory`` is process-global; reset around plugin tests.""" |
| 32 | + clear_resolver_plugin_registry() |
| 33 | + yield |
| 34 | + clear_resolver_plugin_registry() |
| 35 | + |
| 36 | + |
| 37 | +class TestEnvResolver: |
| 38 | + def test_env_resolver_discovered_via_entry_point(self): |
| 39 | + assert "env" in registered_resolver_kinds() |
| 40 | + |
| 41 | + def test_create_env_resolver_returns_env_resolver(self): |
| 42 | + assert isinstance(create_env_resolver({"kind": "env"}), EnvSecretResolver) |
| 43 | + |
| 44 | + async def test_resolves_from_environment(self, monkeypatch): |
| 45 | + monkeypatch.setenv("AE_TEST_SECRET", "sk-from-env") |
| 46 | + ref = {"kind": "env", "name": "AE_TEST_SECRET"} |
| 47 | + assert await build_resolver(ref).resolve(ref) == "sk-from-env" |
| 48 | + |
| 49 | + async def test_missing_name_field_raises(self): |
| 50 | + with pytest.raises(ValueError, match="name"): |
| 51 | + await EnvSecretResolver().resolve({"kind": "env"}) |
| 52 | + |
| 53 | + async def test_unset_variable_raises(self, monkeypatch): |
| 54 | + monkeypatch.delenv("AE_TEST_MISSING", raising=False) |
| 55 | + with pytest.raises(ValueError, match="not set"): |
| 56 | + await EnvSecretResolver().resolve({"kind": "env", "name": "AE_TEST_MISSING"}) |
| 57 | + |
| 58 | + |
| 59 | +class TestBuildResolver: |
| 60 | + def test_missing_kind_raises(self): |
| 61 | + with pytest.raises(ValueError, match="missing a 'kind'"): |
| 62 | + build_resolver({"name": "X"}) |
| 63 | + |
| 64 | + def test_unknown_kind_raises_and_lists_available(self): |
| 65 | + with pytest.raises(ValueError, match="unknown secret resolver kind 'nope'") as exc: |
| 66 | + build_resolver({"kind": "nope"}) |
| 67 | + assert "env" in str(exc.value) |
| 68 | + |
| 69 | + |
| 70 | +class TestPluginPrecedence: |
| 71 | + async def test_programmatic_registration_overrides_entry_point(self, isolated_resolver_plugins): |
| 72 | + class Sentinel: |
| 73 | + async def resolve(self, ref): |
| 74 | + return "programmatic" |
| 75 | + |
| 76 | + register_resolver_factory("env", lambda _ref: Sentinel()) |
| 77 | + resolver = build_resolver({"kind": "env"}) |
| 78 | + assert isinstance(resolver, Sentinel) |
| 79 | + assert await resolver.resolve({"kind": "env"}) == "programmatic" |
| 80 | + |
| 81 | + def test_register_adds_new_kind(self, isolated_resolver_plugins): |
| 82 | + register_resolver_factory("custom", lambda _ref: MagicMock()) |
| 83 | + assert "custom" in registered_resolver_kinds() |
| 84 | + |
| 85 | + |
| 86 | +class TestEntryPointDiscovery: |
| 87 | + """Entry-point discovery without relying on packages installed in the test venv.""" |
| 88 | + |
| 89 | + async def test_entry_point_resolver_resolves_kind(self, isolated_resolver_plugins): |
| 90 | + class FromEp: |
| 91 | + async def resolve(self, ref): |
| 92 | + return "from-entry-point" |
| 93 | + |
| 94 | + def factory(_ref): |
| 95 | + return FromEp() |
| 96 | + |
| 97 | + ep = MagicMock() |
| 98 | + ep.name = "from_ep" |
| 99 | + ep.load.return_value = factory |
| 100 | + |
| 101 | + with patch("agentevals.resolvers.entry_points", return_value=[ep]): |
| 102 | + resolver = build_resolver({"kind": "from_ep"}) |
| 103 | + ep.load.assert_called_once_with() |
| 104 | + assert await resolver.resolve({"kind": "from_ep"}) == "from-entry-point" |
| 105 | + |
| 106 | + def test_entry_point_load_failure_skipped(self, isolated_resolver_plugins, caplog): |
| 107 | + ep = MagicMock() |
| 108 | + ep.name = "broken_pkg_resolver" |
| 109 | + ep.load.side_effect = ImportError("dist not installed") |
| 110 | + |
| 111 | + with patch("agentevals.resolvers.entry_points", return_value=[ep]), caplog.at_level(logging.ERROR): |
| 112 | + kinds = registered_resolver_kinds() |
| 113 | + assert "broken_pkg_resolver" not in kinds |
| 114 | + assert any("failed to load resolver entry point" in r.getMessage() for r in caplog.records) |
| 115 | + |
| 116 | + def test_non_callable_entry_point_skipped(self, isolated_resolver_plugins, caplog): |
| 117 | + ep = MagicMock() |
| 118 | + ep.name = "bad_export" |
| 119 | + ep.load.return_value = "not_callable" |
| 120 | + |
| 121 | + with patch("agentevals.resolvers.entry_points", return_value=[ep]), caplog.at_level(logging.WARNING): |
| 122 | + kinds = registered_resolver_kinds() |
| 123 | + assert "bad_export" not in kinds |
| 124 | + assert any("not callable" in r.getMessage() for r in caplog.records) |
| 125 | + |
| 126 | + |
| 127 | +class TestResolvedCredentialContext: |
| 128 | + def test_get_returns_none_for_absent_key(self): |
| 129 | + token = set_resolved_credentials({"present": "v"}) |
| 130 | + try: |
| 131 | + assert get_resolved_credential("present") == "v" |
| 132 | + assert get_resolved_credential("absent") is None |
| 133 | + finally: |
| 134 | + reset_resolved_credentials(token) |
| 135 | + |
| 136 | + async def test_resolve_credential_refs_maps_logical_names_to_values(self, monkeypatch): |
| 137 | + monkeypatch.setenv("AE_TEST_A", "val-a") |
| 138 | + monkeypatch.setenv("AE_TEST_B", "val-b") |
| 139 | + resolved = await resolve_credential_refs( |
| 140 | + { |
| 141 | + "a": {"kind": "env", "name": "AE_TEST_A"}, |
| 142 | + "b": {"kind": "env", "name": "AE_TEST_B"}, |
| 143 | + } |
| 144 | + ) |
| 145 | + assert resolved == {"a": "val-a", "b": "val-b"} |
0 commit comments