diff --git a/changelog.d/internal.remove-test-code-from-prod.md b/changelog.d/internal.remove-test-code-from-prod.md new file mode 100644 index 000000000..682183cda --- /dev/null +++ b/changelog.d/internal.remove-test-code-from-prod.md @@ -0,0 +1,3 @@ +Moved the test-only `PolicyContext.for_testing()` factory out of the production +`policy_core` module into a test fixture (`make_policy_context()` in +`tests/luthien_proxy/fixtures/policy_context.py`). No user-facing behavior change. diff --git a/src/luthien_proxy/policy_core/policy_context.py b/src/luthien_proxy/policy_core/policy_context.py index 5f55aef3d..cb9db45f8 100644 --- a/src/luthien_proxy/policy_core/policy_context.py +++ b/src/luthien_proxy/policy_core/policy_context.py @@ -316,50 +316,5 @@ def __deepcopy__(self, memo: dict[int, Any]) -> "PolicyContext": return new_ctx - @classmethod - def for_testing( - cls, - transaction_id: str = "test-txn", - request: Any | None = None, - raw_http_request: RawHttpRequest | None = None, - session_id: str | None = None, - user_id: str | None = None, - user_credential: Credential | None = None, - credential_manager: "CredentialManager | None" = None, - inference_provider_registry: "InferenceProviderRegistry | None" = None, - policy_cache_factory: "PolicyCacheFactory | None" = None, - ) -> "PolicyContext": - """Create a PolicyContext suitable for unit tests. - - Uses NullEventEmitter so no external dependencies are required. - - Args: - transaction_id: Transaction ID (defaults to "test-txn") - request: Optional request object - raw_http_request: Optional raw HTTP request data - session_id: Optional session ID - user_id: Optional user identity for tests exercising user-aware behavior - user_credential: Optional credential for tests exercising auth - credential_manager: Optional manager for tests exercising auth providers - inference_provider_registry: Optional provider registry for tests - exercising named-provider dispatch - policy_cache_factory: Optional cache factory for tests exercising caching - - Returns: - PolicyContext with null implementations for external services - """ - return cls( - transaction_id=transaction_id, - request=request, - emitter=NullEventEmitter(), - raw_http_request=raw_http_request, - session_id=session_id, - user_id=user_id, - user_credential=user_credential, - credential_manager=credential_manager, - inference_provider_registry=inference_provider_registry, - policy_cache_factory=policy_cache_factory, - ) - __all__ = ["PolicyContext"] diff --git a/tests/luthien_proxy/fixtures/policy_context.py b/tests/luthien_proxy/fixtures/policy_context.py new file mode 100644 index 000000000..d533d3a72 --- /dev/null +++ b/tests/luthien_proxy/fixtures/policy_context.py @@ -0,0 +1,65 @@ +"""Test helper for constructing PolicyContext instances. + +Previously this lived on the production class as ``PolicyContext.for_testing``. +It is test-only scaffolding, so it lives in the test tree instead. Import it as:: + + from tests.luthien_proxy.fixtures.policy_context import make_policy_context +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from luthien_proxy.observability.emitter import NullEventEmitter +from luthien_proxy.policy_core.policy_context import PolicyContext +from luthien_proxy.types import RawHttpRequest + +if TYPE_CHECKING: + from luthien_proxy.credential_manager import CredentialManager + from luthien_proxy.credentials.credential import Credential + from luthien_proxy.inference.registry import InferenceProviderRegistry + from luthien_proxy.utils.policy_cache import PolicyCacheFactory + + +def make_policy_context( + transaction_id: str = "test-txn", + request: Any | None = None, + raw_http_request: RawHttpRequest | None = None, + session_id: str | None = None, + user_id: str | None = None, + user_credential: "Credential | None" = None, + credential_manager: "CredentialManager | None" = None, + inference_provider_registry: "InferenceProviderRegistry | None" = None, + policy_cache_factory: "PolicyCacheFactory | None" = None, +) -> PolicyContext: + """Create a PolicyContext suitable for unit tests. + + Uses NullEventEmitter so no external dependencies are required. + + Args: + transaction_id: Transaction ID (defaults to "test-txn") + request: Optional request object + raw_http_request: Optional raw HTTP request data + session_id: Optional session ID + user_id: Optional user identity for tests exercising user-aware behavior + user_credential: Optional credential for tests exercising auth + credential_manager: Optional manager for tests exercising auth providers + inference_provider_registry: Optional provider registry for tests + exercising named-provider dispatch + policy_cache_factory: Optional cache factory for tests exercising caching + + Returns: + PolicyContext with null implementations for external services + """ + return PolicyContext( + transaction_id=transaction_id, + request=request, + emitter=NullEventEmitter(), + raw_http_request=raw_http_request, + session_id=session_id, + user_id=user_id, + user_credential=user_credential, + credential_manager=credential_manager, + inference_provider_registry=inference_provider_registry, + policy_cache_factory=policy_cache_factory, + ) diff --git a/tests/luthien_proxy/unit_tests/credentials/test_credential_manager_resolve.py b/tests/luthien_proxy/unit_tests/credentials/test_credential_manager_resolve.py index e3d5a07d8..909bf8cd6 100644 --- a/tests/luthien_proxy/unit_tests/credentials/test_credential_manager_resolve.py +++ b/tests/luthien_proxy/unit_tests/credentials/test_credential_manager_resolve.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock import pytest +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from luthien_proxy.credential_manager import CredentialError, CredentialManager from luthien_proxy.credentials.auth_provider import ( @@ -11,7 +12,6 @@ UserThenServer, ) from luthien_proxy.credentials.credential import Credential, CredentialType -from luthien_proxy.policy_core.policy_context import PolicyContext class TestResolveUserCredentials: @@ -22,7 +22,7 @@ async def test_returns_user_credential_when_set(self): """resolve(UserCredentials(), context) returns context.user_credential when set.""" manager = CredentialManager(db_pool=None, cache=None) cred = Credential(value="sk-ant-test", credential_type=CredentialType.API_KEY) - context = PolicyContext.for_testing(user_credential=cred) + context = make_policy_context(user_credential=cred) result = await manager.resolve(UserCredentials(), context) @@ -32,7 +32,7 @@ async def test_returns_user_credential_when_set(self): async def test_raises_when_user_credential_missing(self): """resolve(UserCredentials(), context) raises CredentialError when user_credential is None.""" manager = CredentialManager(db_pool=None, cache=None) - context = PolicyContext.for_testing(user_credential=None) + context = make_policy_context(user_credential=None) with pytest.raises(CredentialError, match="No user credential on request context"): await manager.resolve(UserCredentials(), context) @@ -51,7 +51,7 @@ async def test_calls_store_for_server_key(self): manager = CredentialManager(db_pool=None, cache=None) manager._store = mock_store - context = PolicyContext.for_testing() + context = make_policy_context() result = await manager.resolve(ServerKey("test_key"), context) mock_store.get.assert_called_once_with("test_key") @@ -61,7 +61,7 @@ async def test_calls_store_for_server_key(self): async def test_raises_when_no_store(self): """resolve(ServerKey("name"), context) raises CredentialError when store is None.""" manager = CredentialManager(db_pool=None, cache=None) - context = PolicyContext.for_testing() + context = make_policy_context() with pytest.raises(CredentialError, match="No credential store configured"): await manager.resolve(ServerKey("test_key"), context) @@ -75,7 +75,7 @@ async def test_raises_when_key_not_found(self): manager = CredentialManager(db_pool=None, cache=None) manager._store = mock_store - context = PolicyContext.for_testing() + context = make_policy_context() with pytest.raises(CredentialError, match="Server key 'missing_key' not found"): await manager.resolve(ServerKey("missing_key"), context) @@ -89,7 +89,7 @@ async def test_returns_user_credential_when_available(self): """resolve(UserThenServer("name"), context) returns user credential when available.""" manager = CredentialManager(db_pool=None, cache=None) user_cred = Credential(value="sk-ant-user", credential_type=CredentialType.API_KEY) - context = PolicyContext.for_testing(user_credential=user_cred) + context = make_policy_context(user_credential=user_cred) result = await manager.resolve(UserThenServer("fallback_key"), context) @@ -105,7 +105,7 @@ async def test_falls_back_with_warn_when_user_missing(self): manager = CredentialManager(db_pool=None, cache=None) manager._store = mock_store - context = PolicyContext.for_testing(user_credential=None) + context = make_policy_context(user_credential=None) result = await manager.resolve(UserThenServer("fallback_key", on_fallback="warn"), context) @@ -124,7 +124,7 @@ async def test_falls_back_with_fallback_when_user_missing(self): manager = CredentialManager(db_pool=None, cache=None) manager._store = mock_store - context = PolicyContext.for_testing(user_credential=None) + context = make_policy_context(user_credential=None) result = await manager.resolve(UserThenServer("fallback_key", on_fallback="fallback"), context) assert result == server_cred @@ -133,7 +133,7 @@ async def test_falls_back_with_fallback_when_user_missing(self): async def test_raises_with_fail_when_user_missing(self): """resolve(UserThenServer("name", on_fallback="fail"), context) raises CredentialError when user credential is None.""" manager = CredentialManager(db_pool=None, cache=None) - context = PolicyContext.for_testing(user_credential=None) + context = make_policy_context(user_credential=None) with pytest.raises(CredentialError, match="No user credential on request context"): await manager.resolve(UserThenServer("fallback_key", on_fallback="fail"), context) @@ -145,7 +145,7 @@ async def test_fall_back_to_server_key_when_user_missing_with_fail(self): manager = CredentialManager(db_pool=None, cache=None) manager._store = mock_store - context = PolicyContext.for_testing(user_credential=None) + context = make_policy_context(user_credential=None) with pytest.raises(CredentialError): await manager.resolve(UserThenServer("fallback_key", on_fallback="fail"), context) @@ -161,7 +161,7 @@ class TestResolveUnknownProvider: async def test_raises_for_unknown_provider_type(self): """resolve() raises CredentialError for unknown auth provider type.""" manager = CredentialManager(db_pool=None, cache=None) - context = PolicyContext.for_testing() + context = make_policy_context() # Create a fake provider that doesn't match any known type class UnknownProvider: diff --git a/tests/luthien_proxy/unit_tests/credentials/test_policy_context_credentials.py b/tests/luthien_proxy/unit_tests/credentials/test_policy_context_credentials.py index ac6a75779..fc396b9f5 100644 --- a/tests/luthien_proxy/unit_tests/credentials/test_policy_context_credentials.py +++ b/tests/luthien_proxy/unit_tests/credentials/test_policy_context_credentials.py @@ -3,10 +3,10 @@ import copy import pytest +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from luthien_proxy.credential_manager import CredentialManager from luthien_proxy.credentials.credential import Credential, CredentialError, CredentialType -from luthien_proxy.policy_core.policy_context import PolicyContext class TestPolicyContextUserCredential: @@ -15,13 +15,13 @@ class TestPolicyContextUserCredential: def test_user_credential_is_accessible(self): """user_credential property is set and accessible.""" cred = Credential(value="sk-ant-test", credential_type=CredentialType.API_KEY) - context = PolicyContext.for_testing(user_credential=cred) + context = make_policy_context(user_credential=cred) assert context.user_credential == cred def test_user_credential_defaults_to_none(self): """user_credential defaults to None.""" - context = PolicyContext.for_testing() + context = make_policy_context() assert context.user_credential is None @@ -32,13 +32,13 @@ class TestPolicyContextCredentialManager: def test_credential_manager_returns_manager_when_set(self): """credential_manager property returns manager when set.""" manager = CredentialManager(db_pool=None, cache=None) - context = PolicyContext.for_testing(credential_manager=manager) + context = make_policy_context(credential_manager=manager) assert context.credential_manager is manager def test_credential_manager_raises_when_none(self): """credential_manager property raises CredentialError when None.""" - context = PolicyContext.for_testing(credential_manager=None) + context = make_policy_context(credential_manager=None) with pytest.raises(CredentialError, match="No credential manager configured"): _ = context.credential_manager @@ -50,7 +50,7 @@ class TestPolicyContextDeepCopy: def test_deepcopy_shares_user_credential(self): """__deepcopy__ shares user_credential with the copy.""" cred = Credential(value="sk-ant-test", credential_type=CredentialType.API_KEY) - context = PolicyContext.for_testing(user_credential=cred) + context = make_policy_context(user_credential=cred) context_copy = copy.deepcopy(context) @@ -60,7 +60,7 @@ def test_deepcopy_shares_user_credential(self): def test_deepcopy_shares_credential_manager(self): """__deepcopy__ shares _credential_manager with the copy.""" manager = CredentialManager(db_pool=None, cache=None) - context = PolicyContext.for_testing(credential_manager=manager) + context = make_policy_context(credential_manager=manager) context_copy = copy.deepcopy(context) @@ -70,7 +70,7 @@ def test_deepcopy_shares_credential_manager(self): def test_deepcopy_preserves_credential_manager_property(self): """__deepcopy__ copy can access credential_manager property.""" manager = CredentialManager(db_pool=None, cache=None) - context = PolicyContext.for_testing(credential_manager=manager) + context = make_policy_context(credential_manager=manager) context_copy = copy.deepcopy(context) @@ -78,14 +78,14 @@ def test_deepcopy_preserves_credential_manager_property(self): class TestPolicyContextForTesting: - """Test PolicyContext.for_testing() constructor.""" + """Test make_policy_context() constructor.""" def test_for_testing_accepts_credential_params(self): """for_testing() accepts credential parameters.""" cred = Credential(value="sk-ant-test", credential_type=CredentialType.API_KEY) manager = CredentialManager(db_pool=None, cache=None) - context = PolicyContext.for_testing( + context = make_policy_context( user_credential=cred, credential_manager=manager, ) @@ -93,15 +93,15 @@ def test_for_testing_accepts_credential_params(self): assert context.user_credential == cred assert context.credential_manager is manager - def test_for_testing_defaults_to_no_credentials(self): - """for_testing() with no params sets credentials to None.""" - context = PolicyContext.for_testing() + def test_make_policy_context_defaults_to_no_credentials(self): + """make_policy_context() with no params sets credentials to None.""" + context = make_policy_context() assert context.user_credential is None assert context._credential_manager is None - def test_for_testing_with_custom_transaction_id(self): - """for_testing() accepts custom transaction_id.""" - context = PolicyContext.for_testing(transaction_id="custom-txn-123") + def test_make_policy_context_with_custom_transaction_id(self): + """make_policy_context() accepts custom transaction_id.""" + context = make_policy_context(transaction_id="custom-txn-123") assert context.transaction_id == "custom-txn-123" diff --git a/tests/luthien_proxy/unit_tests/inference/test_dispatch.py b/tests/luthien_proxy/unit_tests/inference/test_dispatch.py index 2bc4efebf..857e5e634 100644 --- a/tests/luthien_proxy/unit_tests/inference/test_dispatch.py +++ b/tests/luthien_proxy/unit_tests/inference/test_dispatch.py @@ -11,6 +11,7 @@ from unittest.mock import AsyncMock, MagicMock import pytest +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from luthien_proxy.credentials import ( Credential, @@ -22,7 +23,6 @@ ) from luthien_proxy.inference.base import InferenceProvider from luthien_proxy.inference.dispatch import resolve_inference_provider -from luthien_proxy.policy_core.policy_context import PolicyContext def _user_cred() -> Credential: @@ -46,7 +46,7 @@ class TestUserCredentials: @pytest.mark.asyncio async def test_happy_path_returns_passthrough_and_user_cred(self): - context = PolicyContext.for_testing(user_credential=_user_cred()) + context = make_policy_context(user_credential=_user_cred()) result = await resolve_inference_provider( UserCredentials(), context, @@ -58,7 +58,7 @@ async def test_happy_path_returns_passthrough_and_user_cred(self): @pytest.mark.asyncio async def test_missing_user_cred_raises(self): - context = PolicyContext.for_testing(user_credential=None) + context = make_policy_context(user_credential=None) with pytest.raises(CredentialError, match="no user credential"): await resolve_inference_provider( UserCredentials(), @@ -73,7 +73,7 @@ class TestProvider: @pytest.mark.asyncio async def test_looks_up_registry_and_returns_no_override(self): - context = PolicyContext.for_testing(user_credential=None) + context = make_policy_context(user_credential=None) registered = _fake_registry_provider() registry = _mock_registry(registered) result = await resolve_inference_provider( @@ -88,7 +88,7 @@ async def test_looks_up_registry_and_returns_no_override(self): @pytest.mark.asyncio async def test_missing_registry_raises(self): - context = PolicyContext.for_testing(user_credential=None) + context = make_policy_context(user_credential=None) with pytest.raises(RuntimeError, match="no InferenceProviderRegistry is configured"): await resolve_inference_provider( Provider(name="my-judge"), @@ -103,7 +103,7 @@ class TestUserThenProvider: @pytest.mark.asyncio async def test_user_cred_present_uses_passthrough(self): - context = PolicyContext.for_testing(user_credential=_user_cred()) + context = make_policy_context(user_credential=_user_cred()) registry = _mock_registry() result = await resolve_inference_provider( UserThenProvider(name="my-judge", on_fallback="warn"), @@ -116,7 +116,7 @@ async def test_user_cred_present_uses_passthrough(self): @pytest.mark.asyncio async def test_fail_mode_raises_when_no_user_cred(self): - context = PolicyContext.for_testing(user_credential=None) + context = make_policy_context(user_credential=None) registry = _mock_registry() with pytest.raises(CredentialError, match="on_fallback='fail'"): await resolve_inference_provider( @@ -129,7 +129,7 @@ async def test_fail_mode_raises_when_no_user_cred(self): @pytest.mark.asyncio async def test_warn_mode_falls_back_and_logs(self, caplog): - context = PolicyContext.for_testing(user_credential=None) + context = make_policy_context(user_credential=None) registered = _fake_registry_provider() registry = _mock_registry(registered) with caplog.at_level(logging.WARNING): @@ -145,7 +145,7 @@ async def test_warn_mode_falls_back_and_logs(self, caplog): @pytest.mark.asyncio async def test_fallback_mode_silent(self, caplog): - context = PolicyContext.for_testing(user_credential=None) + context = make_policy_context(user_credential=None) registered = _fake_registry_provider() registry = _mock_registry(registered) with caplog.at_level(logging.WARNING): @@ -161,7 +161,7 @@ async def test_fallback_mode_silent(self, caplog): @pytest.mark.asyncio async def test_fallback_requires_registry_when_no_user_cred(self): - context = PolicyContext.for_testing(user_credential=None) + context = make_policy_context(user_credential=None) with pytest.raises(RuntimeError, match="no InferenceProviderRegistry is configured"): await resolve_inference_provider( UserThenProvider(name="my-judge", on_fallback="fallback"), diff --git a/tests/luthien_proxy/unit_tests/pipeline/test_anthropic_processor.py b/tests/luthien_proxy/unit_tests/pipeline/test_anthropic_processor.py index c2e4ce65e..4df5a24bf 100644 --- a/tests/luthien_proxy/unit_tests/pipeline/test_anthropic_processor.py +++ b/tests/luthien_proxy/unit_tests/pipeline/test_anthropic_processor.py @@ -24,6 +24,7 @@ from httpx import Request as HttpxRequest from httpx import Response as HttpxResponse from tests.constants import DEFAULT_TEST_MODEL +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from luthien_proxy.exceptions import BackendAPIError from luthien_proxy.llm.types.anthropic import AnthropicRequest, AnthropicResponse, build_usage @@ -1878,7 +1879,7 @@ async def test_non_streaming_calls_request_and_response_hooks(self): "usage": {"input_tokens": 1, "output_tokens": 1}, } io = _StubIO(request=request, response=response) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() emissions = [] async for emission in _run_policy_hooks(NoOpPolicy(), io, ctx): @@ -1913,7 +1914,7 @@ async def test_streaming_calls_stream_event_and_complete_hooks(self): RawMessageStopEvent(type="message_stop"), ] io = _StubIO(request=request, stream_events=stream_events) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() emissions = [] async for emission in _run_policy_hooks(NoOpPolicy(), io, ctx): @@ -1962,7 +1963,7 @@ async def on_anthropic_stream_complete(self, context: PolicyContext) -> list[Ant "usage": {"input_tokens": 1, "output_tokens": 1}, } io = _StubIO(request=request, response=response) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() async for _ in _run_policy_hooks(_AddMaxTokensPolicy(), io, ctx): pass @@ -2012,7 +2013,7 @@ async def on_anthropic_stream_complete(self, context: PolicyContext) -> list[Ant ), ] io = _StubIO(request=request, stream_events=stream_events) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() emissions = [] async for emission in _run_policy_hooks(_AppendPolicy(), io, ctx): @@ -2055,7 +2056,7 @@ async def test_multi_serial_policy_ordering_through_hooks(self): "usage": {"input_tokens": 1, "output_tokens": 2}, } io = _StubIO(request=request, response=response) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() emissions = [] async for emission in _run_policy_hooks(pipeline, io, ctx): diff --git a/tests/luthien_proxy/unit_tests/policies/test_conversation_link_policy.py b/tests/luthien_proxy/unit_tests/policies/test_conversation_link_policy.py index 9bddb7f75..df0ac3362 100644 --- a/tests/luthien_proxy/unit_tests/policies/test_conversation_link_policy.py +++ b/tests/luthien_proxy/unit_tests/policies/test_conversation_link_policy.py @@ -1,6 +1,7 @@ """Tests for ConversationLinkPolicy.""" import pytest +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from luthien_proxy.policies.conversation_link_policy import ConversationLinkPolicy from luthien_proxy.policy_core.policy_context import PolicyContext @@ -34,7 +35,7 @@ def _subsequent_turn_request(): class TestConversationLinkPolicy: def _make_context(self, session_id: str | None = "test-session") -> PolicyContext: - return PolicyContext.for_testing(session_id=session_id) + return make_policy_context(session_id=session_id) async def _run_first_turn( self, policy: ConversationLinkPolicy, ctx: PolicyContext, content: str = "Hello world" @@ -170,7 +171,7 @@ class TestConversationLinkPolicyIntegration: """Test through the real SimplePolicy entry point (on_anthropic_response).""" def _make_context(self, session_id: str = "test-session") -> PolicyContext: - return PolicyContext.for_testing(session_id=session_id) + return make_policy_context(session_id=session_id) @pytest.mark.asyncio async def test_on_anthropic_response_injects_into_first_text_block(self): diff --git a/tests/luthien_proxy/unit_tests/policies/test_debug_logging_policy.py b/tests/luthien_proxy/unit_tests/policies/test_debug_logging_policy.py index b5253cb01..baa5222e9 100644 --- a/tests/luthien_proxy/unit_tests/policies/test_debug_logging_policy.py +++ b/tests/luthien_proxy/unit_tests/policies/test_debug_logging_policy.py @@ -22,6 +22,7 @@ TextDelta, ) from tests.constants import DEFAULT_TEST_MODEL +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from luthien_proxy.llm.types.anthropic import ( AnthropicRequest, @@ -81,7 +82,7 @@ class TestDebugLoggingPolicyAnthropicRequest: async def test_on_anthropic_request_returns_same_request(self): """on_anthropic_request returns the exact same request object unchanged.""" policy = DebugLoggingPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() request: AnthropicRequest = { "model": DEFAULT_TEST_MODEL, @@ -97,7 +98,7 @@ async def test_on_anthropic_request_returns_same_request(self): async def test_on_anthropic_request_preserves_all_fields(self): """on_anthropic_request preserves all fields in a complex request.""" policy = DebugLoggingPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() request: AnthropicRequest = { "model": DEFAULT_TEST_MODEL, @@ -150,7 +151,7 @@ class TestDebugLoggingPolicyAnthropicResponse: async def test_on_anthropic_response_returns_same_response(self): """on_anthropic_response returns the exact same response object unchanged.""" policy = DebugLoggingPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() response: AnthropicResponse = { "id": "msg_123", @@ -170,7 +171,7 @@ async def test_on_anthropic_response_returns_same_response(self): async def test_on_anthropic_response_preserves_content(self): """on_anthropic_response preserves content blocks exactly.""" policy = DebugLoggingPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_block: AnthropicTextBlock = {"type": "text", "text": "Complex response text"} response: AnthropicResponse = { @@ -223,7 +224,7 @@ class TestDebugLoggingPolicyAnthropicStreamEvent: async def test_on_anthropic_stream_event_returns_same_event(self): """on_anthropic_stream_event returns the exact same event object unchanged.""" policy = DebugLoggingPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() event = RawContentBlockDeltaEvent.model_construct( type="content_block_delta", @@ -239,7 +240,7 @@ async def test_on_anthropic_stream_event_returns_same_event(self): async def test_on_anthropic_stream_event_never_returns_empty_list(self): """on_anthropic_stream_event never filters out events (returns empty list).""" policy = DebugLoggingPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() events = [ RawMessageStartEvent.model_construct( @@ -304,7 +305,7 @@ async def test_on_anthropic_stream_event_records_event(self): async def test_on_anthropic_stream_event_passes_through_all_event_types(self): """on_anthropic_stream_event handles all Anthropic stream event types.""" policy = DebugLoggingPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() message_start = RawMessageStartEvent.model_construct( type="message_start", @@ -369,7 +370,7 @@ class TestDebugLoggingPolicyLogging: async def test_on_anthropic_request_logs_at_info_level(self, caplog): """on_anthropic_request logs request data at INFO level.""" policy = DebugLoggingPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() request: AnthropicRequest = { "model": DEFAULT_TEST_MODEL, @@ -387,7 +388,7 @@ async def test_on_anthropic_request_logs_at_info_level(self, caplog): async def test_on_anthropic_response_logs_at_info_level(self, caplog): """on_anthropic_response logs response data at INFO level.""" policy = DebugLoggingPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() response: AnthropicResponse = { "id": "msg_123", @@ -409,7 +410,7 @@ async def test_on_anthropic_response_logs_at_info_level(self, caplog): async def test_on_anthropic_stream_event_logs_at_info_level(self, caplog): """on_anthropic_stream_event logs event data at INFO level.""" policy = DebugLoggingPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() event = RawContentBlockDeltaEvent.model_construct( type="content_block_delta", diff --git a/tests/luthien_proxy/unit_tests/policies/test_demo_force_bash_rmrf_policy.py b/tests/luthien_proxy/unit_tests/policies/test_demo_force_bash_rmrf_policy.py index c878975a6..004bcecb3 100644 --- a/tests/luthien_proxy/unit_tests/policies/test_demo_force_bash_rmrf_policy.py +++ b/tests/luthien_proxy/unit_tests/policies/test_demo_force_bash_rmrf_policy.py @@ -19,10 +19,10 @@ RawMessageStopEvent, ) from tests.constants import DEFAULT_TEST_MODEL +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from luthien_proxy.policies.demo_force_bash_rmrf_policy import DemoForceBashRmRfPolicy from luthien_proxy.policy_core import AnthropicHookPolicy, BasePolicy -from luthien_proxy.policy_core.policy_context import PolicyContext def _upstream_response() -> dict[str, Any]: @@ -73,9 +73,7 @@ class TestNonStreaming: @pytest.mark.asyncio async def test_replaces_upstream_with_tool_use(self): policy = DemoForceBashRmRfPolicy(target_path="/tmp/luthien-demo/x", tool_name="Bash") - result = cast( - dict[str, Any], await policy.on_anthropic_response(_upstream_response(), PolicyContext.for_testing()) - ) # type: ignore[arg-type] + result = cast(dict[str, Any], await policy.on_anthropic_response(_upstream_response(), make_policy_context())) # type: ignore[arg-type] assert result["stop_reason"] == "tool_use" assert result["role"] == "assistant" @@ -91,9 +89,7 @@ async def test_replaces_upstream_with_tool_use(self): @pytest.mark.asyncio async def test_respects_configured_tool_name(self, tool_name: str): policy = DemoForceBashRmRfPolicy(tool_name=tool_name) - result = cast( - dict[str, Any], await policy.on_anthropic_response(_upstream_response(), PolicyContext.for_testing()) - ) # type: ignore[arg-type] + result = cast(dict[str, Any], await policy.on_anthropic_response(_upstream_response(), make_policy_context())) # type: ignore[arg-type] assert result["content"][0]["name"] == tool_name @@ -103,14 +99,14 @@ async def test_swallows_upstream_events(self): policy = DemoForceBashRmRfPolicy() result = await policy.on_anthropic_stream_event( cast(Any, RawMessageStopEvent.model_construct(type="message_stop")), - PolicyContext.for_testing(), + make_policy_context(), ) assert result == [] @pytest.mark.asyncio async def test_complete_emits_six_events_in_protocol_order(self): policy = DemoForceBashRmRfPolicy(target_path="/tmp/luthien-demo/x", tool_name="Bash") - events = await policy.on_anthropic_stream_complete(PolicyContext.for_testing()) + events = await policy.on_anthropic_stream_complete(make_policy_context()) assert [type(ev) for ev in events] == [ RawMessageStartEvent, @@ -124,7 +120,7 @@ async def test_complete_emits_six_events_in_protocol_order(self): @pytest.mark.asyncio async def test_complete_tool_use_block_has_configured_name_and_command(self): policy = DemoForceBashRmRfPolicy(target_path="/tmp/luthien-demo/x", tool_name="mcp__workspace__bash") - events = await policy.on_anthropic_stream_complete(PolicyContext.for_testing()) + events = await policy.on_anthropic_stream_complete(make_policy_context()) # Compare via model_dump to sidestep typed-vs-dict access for fields # that the policy constructs with `model_construct` (validation-skipped). @@ -140,7 +136,7 @@ async def test_complete_tool_use_block_has_configured_name_and_command(self): @pytest.mark.asyncio async def test_complete_message_delta_signals_tool_use_stop(self): policy = DemoForceBashRmRfPolicy() - events = await policy.on_anthropic_stream_complete(PolicyContext.for_testing()) + events = await policy.on_anthropic_stream_complete(make_policy_context()) message_delta = cast(Any, events[4]).model_dump() assert message_delta["delta"]["stop_reason"] == "tool_use" diff --git a/tests/luthien_proxy/unit_tests/policies/test_dogfood_safety_policy.py b/tests/luthien_proxy/unit_tests/policies/test_dogfood_safety_policy.py index b43b5cc93..3d0062ece 100644 --- a/tests/luthien_proxy/unit_tests/policies/test_dogfood_safety_policy.py +++ b/tests/luthien_proxy/unit_tests/policies/test_dogfood_safety_policy.py @@ -15,6 +15,7 @@ RawMessageDeltaEvent, ToolUseBlock, ) +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from tests.luthien_proxy.unit_tests.policies.anthropic_event_builders import message_delta from luthien_proxy.policies.dogfood_safety_policy import ( @@ -39,7 +40,7 @@ def _make_policy( def _make_context(transaction_id: str = "test-txn") -> PolicyContext: - return PolicyContext.for_testing(transaction_id=transaction_id) + return make_policy_context(transaction_id=transaction_id) # ============================================================================ diff --git a/tests/luthien_proxy/unit_tests/policies/test_hackathon_onboarding_policy.py b/tests/luthien_proxy/unit_tests/policies/test_hackathon_onboarding_policy.py index 853550c98..215a1363e 100644 --- a/tests/luthien_proxy/unit_tests/policies/test_hackathon_onboarding_policy.py +++ b/tests/luthien_proxy/unit_tests/policies/test_hackathon_onboarding_policy.py @@ -3,6 +3,7 @@ from __future__ import annotations import pytest +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from luthien_proxy.policies.hackathon_onboarding_policy import ( HackathonOnboardingPolicy, @@ -13,7 +14,6 @@ BasePolicy, TextModifierPolicy, ) -from luthien_proxy.policy_core.policy_context import PolicyContext @pytest.fixture @@ -23,7 +23,7 @@ def policy(): @pytest.fixture def context(): - return PolicyContext.for_testing() + return make_policy_context() # ============================================================================= diff --git a/tests/luthien_proxy/unit_tests/policies/test_hackathon_policy_template.py b/tests/luthien_proxy/unit_tests/policies/test_hackathon_policy_template.py index 736356652..a968894d4 100644 --- a/tests/luthien_proxy/unit_tests/policies/test_hackathon_policy_template.py +++ b/tests/luthien_proxy/unit_tests/policies/test_hackathon_policy_template.py @@ -3,10 +3,10 @@ from __future__ import annotations import pytest +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from luthien_proxy.policies.hackathon_policy_template import HackathonPolicy from luthien_proxy.policies.simple_policy import SimplePolicy -from luthien_proxy.policy_core.policy_context import PolicyContext @pytest.fixture @@ -16,7 +16,7 @@ def policy(): @pytest.fixture def context(): - return PolicyContext.for_testing() + return make_policy_context() class TestTemplate: diff --git a/tests/luthien_proxy/unit_tests/policies/test_multi_serial_policy.py b/tests/luthien_proxy/unit_tests/policies/test_multi_serial_policy.py index 36d4007d5..f4db1ea34 100644 --- a/tests/luthien_proxy/unit_tests/policies/test_multi_serial_policy.py +++ b/tests/luthien_proxy/unit_tests/policies/test_multi_serial_policy.py @@ -14,6 +14,7 @@ TextDelta, ) from tests.constants import DEFAULT_TEST_MODEL +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from tests.luthien_proxy.unit_tests.policies.multi_policy_helpers import ( AnthropicOnlyPolicy, OpenAIOnlyPolicy, @@ -83,7 +84,7 @@ class TestMultiSerialAnthropicRequest: @pytest.mark.asyncio async def test_passes_through_with_noop(self): policy = MultiSerialPolicy(policies=[noop_config()]) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() request: AnthropicRequest = { "model": DEFAULT_TEST_MODEL, "messages": [{"role": "user", "content": "Hello"}], @@ -104,7 +105,7 @@ class TestMultiSerialAnthropicResponse: @pytest.mark.asyncio async def test_allcaps_transforms_text(self): policy = MultiSerialPolicy(policies=[allcaps_config()]) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() response = make_anthropic_response("hello world") result = await policy.on_anthropic_response(response, ctx) @@ -121,7 +122,7 @@ async def test_chaining_two_transformations(self): allcaps_config(), ] ) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() response = make_anthropic_response("hello world") result = await policy.on_anthropic_response(response, ctx) @@ -139,7 +140,7 @@ class TestMultiSerialAnthropicStreamEvent: @pytest.mark.asyncio async def test_text_delta_chained_through_allcaps(self): policy = MultiSerialPolicy(policies=[allcaps_config()]) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_delta = TextDelta.model_construct(type="text_delta", text="hello") event = RawContentBlockDeltaEvent.model_construct(type="content_block_delta", index=0, delta=text_delta) @@ -153,7 +154,7 @@ async def test_text_delta_chained_through_allcaps(self): @pytest.mark.asyncio async def test_non_text_events_pass_through(self): policy = MultiSerialPolicy(policies=[allcaps_config()]) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() event = RawMessageStartEvent.model_construct( type="message_start", message={ @@ -175,7 +176,7 @@ async def test_non_text_events_pass_through(self): @pytest.mark.asyncio async def test_empty_policy_list_passes_events_through(self): policy = MultiSerialPolicy(policies=[]) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_delta = TextDelta.model_construct(type="text_delta", text="hello") event = RawContentBlockDeltaEvent.model_construct(type="content_block_delta", index=0, delta=text_delta) @@ -205,7 +206,7 @@ async def on_anthropic_stream_complete(self, context: PolicyContext) -> None: policy = MultiSerialPolicy(policies=[]) policy._sub_policies = [tracking] - await policy.on_anthropic_stream_complete(PolicyContext.for_testing()) + await policy.on_anthropic_stream_complete(make_policy_context()) assert tracking.complete_calls == 1 @@ -223,7 +224,7 @@ async def on_anthropic_streaming_policy_complete(self, context: PolicyContext) - policy = MultiSerialPolicy(policies=[]) policy._sub_policies = [tracking] - await policy.on_anthropic_streaming_policy_complete(PolicyContext.for_testing()) + await policy.on_anthropic_streaming_policy_complete(make_policy_context()) assert tracking.cleanup_calls == 1 @@ -240,7 +241,7 @@ async def test_stream_complete_events_chain_through_remaining_policies(self): onboarding = OnboardingPolicy({"gateway_url": "http://localhost:9999"}) allcaps = AllCapsPolicy() serial = MultiSerialPolicy.from_instances([onboarding, allcaps]) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() # Call on_anthropic_request so OnboardingPolicy stashes the first-turn request. first_turn_request: AnthropicRequest = { @@ -288,7 +289,7 @@ async def test_anthropic_request_raises_for_incompatible_policy(self): """Anthropic call raises TypeError when a sub-policy lacks AnthropicExecutionInterface.""" policy = MultiSerialPolicy(policies=[noop_config()]) policy._sub_policies = (*policy._sub_policies, OpenAIOnlyPolicy()) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() request: AnthropicRequest = { "model": DEFAULT_TEST_MODEL, "messages": [{"role": "user", "content": "Hello"}], @@ -303,7 +304,7 @@ async def test_anthropic_response_raises_for_incompatible_policy(self): """Anthropic response call raises TypeError for incompatible sub-policy.""" policy = MultiSerialPolicy(policies=[noop_config()]) policy._sub_policies = (*policy._sub_policies, OpenAIOnlyPolicy()) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() response = make_anthropic_response("hello") with pytest.raises(TypeError, match="OpenAIOnly.*does not implement AnthropicExecutionInterface"): @@ -314,7 +315,7 @@ async def test_anthropic_stream_event_raises_for_incompatible_policy(self): """Anthropic stream event raises TypeError for incompatible sub-policy.""" policy = MultiSerialPolicy(policies=[noop_config()]) policy._sub_policies = (*policy._sub_policies, OpenAIOnlyPolicy()) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_delta = TextDelta.model_construct(type="text_delta", text="hello") event = RawContentBlockDeltaEvent.model_construct(type="content_block_delta", index=0, delta=text_delta) @@ -325,7 +326,7 @@ async def test_anthropic_stream_event_raises_for_incompatible_policy(self): async def test_all_compatible_policies_pass_validation(self): """No error when all sub-policies implement the required interface.""" policy = MultiSerialPolicy(policies=[noop_config(), allcaps_config()]) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() response = make_anthropic_response("hello") result = await policy.on_anthropic_response(response, ctx) diff --git a/tests/luthien_proxy/unit_tests/policies/test_noop_policy.py b/tests/luthien_proxy/unit_tests/policies/test_noop_policy.py index 0cdd2200c..eaf7b50a9 100644 --- a/tests/luthien_proxy/unit_tests/policies/test_noop_policy.py +++ b/tests/luthien_proxy/unit_tests/policies/test_noop_policy.py @@ -21,13 +21,13 @@ TextDelta, ) from tests.constants import DEFAULT_TEST_MODEL +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from luthien_proxy.policies.noop_policy import NoOpPolicy from luthien_proxy.policy_core import ( AnthropicExecutionInterface, BasePolicy, ) -from luthien_proxy.policy_core.policy_context import PolicyContext # ============================================================================= # Protocol and inheritance tests @@ -70,7 +70,7 @@ class TestNoOpPolicyAnthropicRequest: async def test_on_anthropic_request_returns_same_request(self): """on_anthropic_request returns the exact same request object.""" policy = NoOpPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() request = { "model": DEFAULT_TEST_MODEL, @@ -86,7 +86,7 @@ async def test_on_anthropic_request_returns_same_request(self): async def test_on_anthropic_request_preserves_all_fields(self): """on_anthropic_request preserves all fields in a complex request.""" policy = NoOpPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() request = { "model": DEFAULT_TEST_MODEL, @@ -116,7 +116,7 @@ class TestNoOpPolicyAnthropicResponse: async def test_on_anthropic_response_returns_same_response(self): """on_anthropic_response returns the exact same response object.""" policy = NoOpPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() response = Message.model_construct( id="msg_123", @@ -136,7 +136,7 @@ async def test_on_anthropic_response_returns_same_response(self): async def test_on_anthropic_response_preserves_content(self): """on_anthropic_response preserves content blocks exactly.""" policy = NoOpPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() response = Message.model_construct( id="msg_456", @@ -162,7 +162,7 @@ class TestNoOpPolicyAnthropicStreaming: async def test_on_anthropic_stream_event_returns_same_event(self): """on_anthropic_stream_event returns the exact same event object.""" policy = NoOpPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() event = RawContentBlockDeltaEvent.model_construct( type="content_block_delta", @@ -178,7 +178,7 @@ async def test_on_anthropic_stream_event_returns_same_event(self): async def test_on_anthropic_stream_event_never_returns_empty_list(self): """on_anthropic_stream_event never filters out events.""" policy = NoOpPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() events = [ RawMessageStartEvent.model_construct( @@ -224,7 +224,7 @@ async def test_on_anthropic_stream_event_never_returns_empty_list(self): async def test_on_anthropic_stream_event_handles_all_event_types(self): """on_anthropic_stream_event handles all Anthropic stream event types.""" policy = NoOpPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() message_start = RawMessageStartEvent.model_construct( type="message_start", diff --git a/tests/luthien_proxy/unit_tests/policies/test_onboarding_policy.py b/tests/luthien_proxy/unit_tests/policies/test_onboarding_policy.py index c329a99bb..e27cd7ec1 100644 --- a/tests/luthien_proxy/unit_tests/policies/test_onboarding_policy.py +++ b/tests/luthien_proxy/unit_tests/policies/test_onboarding_policy.py @@ -16,6 +16,7 @@ ToolUseBlock, ) from anthropic.types.raw_message_delta_event import Delta +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from luthien_proxy.policies.onboarding_policy import ( OnboardingPolicy, @@ -26,7 +27,6 @@ BasePolicy, TextModifierPolicy, ) -from luthien_proxy.policy_core.policy_context import PolicyContext @pytest.fixture @@ -36,7 +36,7 @@ def policy(): @pytest.fixture def context(): - return PolicyContext.for_testing() + return make_policy_context() # ============================================================================= diff --git a/tests/luthien_proxy/unit_tests/policies/test_simple_llm_policy.py b/tests/luthien_proxy/unit_tests/policies/test_simple_llm_policy.py index c3a579a63..f03bb1e51 100644 --- a/tests/luthien_proxy/unit_tests/policies/test_simple_llm_policy.py +++ b/tests/luthien_proxy/unit_tests/policies/test_simple_llm_policy.py @@ -22,6 +22,7 @@ ToolUseBlock, ) from tests.luthien_proxy.fixtures.anthropic_stream_validator import validate_anthropic_event_ordering +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from tests.luthien_proxy.unit_tests.policies.anthropic_event_builders import ( block_stop, event_types, @@ -60,7 +61,7 @@ def _make_policy(on_error: str = "block") -> SimpleLLMPolicy: def _make_context() -> PolicyContext: - return PolicyContext.for_testing(transaction_id="test-txn") + return make_policy_context(transaction_id="test-txn") def test_init_preserves_all_config_fields(): diff --git a/tests/luthien_proxy/unit_tests/policies/test_simple_policy.py b/tests/luthien_proxy/unit_tests/policies/test_simple_policy.py index 11502c8dc..d8b4504b8 100644 --- a/tests/luthien_proxy/unit_tests/policies/test_simple_policy.py +++ b/tests/luthien_proxy/unit_tests/policies/test_simple_policy.py @@ -27,6 +27,7 @@ ToolUseBlock, ) from tests.constants import DEFAULT_TEST_MODEL +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from luthien_proxy.llm.types.anthropic import ( AnthropicRequest, @@ -97,7 +98,7 @@ class TestSimplePolicyAnthropicRequest: async def test_on_request_passthrough_by_default(self): """Base class on_anthropic_request passes through text unchanged.""" policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() request: AnthropicRequest = { "model": DEFAULT_TEST_MODEL, @@ -113,7 +114,7 @@ async def test_on_request_passthrough_by_default(self): async def test_on_request_transforms_string_content(self): """Subclass simple_on_request transforms string message content.""" policy = AnthropicUppercasePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() request: AnthropicRequest = { "model": DEFAULT_TEST_MODEL, @@ -129,7 +130,7 @@ async def test_on_request_transforms_string_content(self): async def test_on_request_transforms_text_block_content(self): """Subclass simple_on_request transforms text blocks in message content list.""" policy = AnthropicUppercasePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_block: AnthropicTextBlock = {"type": "text", "text": "hello world"} request: AnthropicRequest = { @@ -149,7 +150,7 @@ async def test_on_request_transforms_text_block_content(self): async def test_on_request_ignores_tool_use_blocks(self): """on_anthropic_request does not transform tool_use blocks in messages.""" policy = AnthropicUppercasePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() tool_block: AnthropicToolUseBlock = { "type": "tool_use", @@ -181,7 +182,7 @@ class TestSimplePolicyAnthropicResponse: async def test_on_response_passthrough_by_default(self): """Base class on_anthropic_response passes through response unchanged.""" policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() response: AnthropicResponse = { "id": "msg_123", @@ -203,7 +204,7 @@ async def test_on_response_passthrough_by_default(self): async def test_on_response_transforms_text_content(self): """Subclass simple_on_response_content transforms text blocks.""" policy = AnthropicUppercasePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() response: AnthropicResponse = { "id": "msg_123", @@ -224,7 +225,7 @@ async def test_on_response_transforms_text_content(self): async def test_on_response_transforms_multiple_text_blocks(self): """on_anthropic_response transforms all text blocks in content.""" policy = AnthropicUppercasePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() response: AnthropicResponse = { "id": "msg_123", @@ -250,7 +251,7 @@ async def test_on_response_transforms_multiple_text_blocks(self): async def test_on_response_transforms_tool_use_blocks(self): """Subclass simple_on_anthropic_tool_call transforms tool_use blocks.""" policy = AnthropicPrefixToolNamePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() tool_block: AnthropicToolUseBlock = { "type": "tool_use", @@ -277,7 +278,7 @@ async def test_on_response_transforms_tool_use_blocks(self): async def test_on_response_mixed_content_blocks(self): """on_anthropic_response transforms text and tool_use blocks correctly.""" policy = AnthropicPrefixToolNamePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_block: AnthropicTextBlock = {"type": "text", "text": "calling tool"} tool_block: AnthropicToolUseBlock = { @@ -314,7 +315,7 @@ class TestSimplePolicyAnthropicStreamEventBasic: async def test_message_start_passes_through(self): """on_anthropic_stream_event passes message_start unchanged.""" policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() event = RawMessageStartEvent.model_construct( type="message_start", @@ -337,7 +338,7 @@ async def test_message_start_passes_through(self): async def test_content_block_start_initializes_buffer(self): """on_anthropic_stream_event initializes text buffer for content_block_start.""" policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() event = RawContentBlockStartEvent.model_construct( type="content_block_start", @@ -355,7 +356,7 @@ async def test_content_block_start_initializes_buffer(self): async def test_content_block_stop_passes_through(self): """on_anthropic_stream_event passes through content_block_stop event.""" policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() # Start block start_event = RawContentBlockStartEvent.model_construct( @@ -387,7 +388,7 @@ async def test_content_block_stop_passes_through(self): async def test_message_delta_passes_through(self): """on_anthropic_stream_event passes message_delta unchanged.""" policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() event = RawMessageDeltaEvent.model_construct( type="message_delta", @@ -403,7 +404,7 @@ async def test_message_delta_passes_through(self): async def test_message_stop_passes_through(self): """on_anthropic_stream_event passes message_stop unchanged.""" policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() event = RawMessageStopEvent.model_construct(type="message_stop") @@ -419,7 +420,7 @@ class TestSimplePolicyAnthropicStreamEventText: async def test_text_delta_buffers_content(self): """on_anthropic_stream_event buffers TextDelta without emitting.""" policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() # Start start_event = RawContentBlockStartEvent.model_construct( @@ -445,7 +446,7 @@ async def test_text_delta_buffers_content(self): async def test_text_delta_accumulates(self): """Multiple TextDeltas accumulate in the buffer.""" policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() # Start start_event = RawContentBlockStartEvent.model_construct( @@ -476,7 +477,7 @@ async def test_text_delta_accumulates(self): async def test_thinking_delta_passes_through(self): """ThinkingDelta passes through unchanged (not buffered like TextDelta).""" policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() # Start start_event = RawContentBlockStartEvent.model_construct( @@ -506,7 +507,7 @@ class TestSimplePolicyAnthropicStreamEventToolUse: async def test_tool_use_start_initializes_buffer(self): """on_anthropic_stream_event initializes tool buffer for tool_use block.""" policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() event = RawContentBlockStartEvent.model_construct( type="content_block_start", @@ -527,7 +528,7 @@ async def test_tool_use_start_initializes_buffer(self): async def test_json_delta_buffers_incrementally(self): """InputJSONDelta accumulates in tool buffer.""" policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() # Start tool_use start_event = RawContentBlockStartEvent.model_construct( @@ -563,7 +564,7 @@ async def test_json_delta_buffers_incrementally(self): async def test_tool_use_stop_completes_and_transforms(self): """on_anthropic_stream_event completes tool_use and calls transform.""" policy = AnthropicPrefixToolNamePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() # Start start_event = RawContentBlockStartEvent.model_construct( @@ -608,8 +609,8 @@ class TestSimplePolicyAnthropicBufferManagement: async def test_separate_contexts_have_separate_buffers(self): """Different PolicyContexts maintain independent buffers.""" policy = SimplePolicy() - ctx_a = PolicyContext.for_testing(transaction_id="txn-a") - ctx_b = PolicyContext.for_testing(transaction_id="txn-b") + ctx_a = make_policy_context(transaction_id="txn-a") + ctx_b = make_policy_context(transaction_id="txn-b") # Start block in ctx_a start_a = RawContentBlockStartEvent.model_construct( @@ -628,8 +629,8 @@ async def test_separate_contexts_have_separate_buffers(self): async def test_on_anthropic_streaming_policy_complete_cleans_state(self): """on_anthropic_streaming_policy_complete removes per-request state.""" policy = SimplePolicy() - ctx_a = PolicyContext.for_testing(transaction_id="txn-a") - ctx_b = PolicyContext.for_testing(transaction_id="txn-b") + ctx_a = make_policy_context(transaction_id="txn-a") + ctx_b = make_policy_context(transaction_id="txn-b") start_a = RawContentBlockStartEvent.model_construct( type="content_block_start", @@ -660,7 +661,7 @@ class TestSimplePolicyErrorHandling: async def test_on_stream_event_raises_on_text_delta_without_buffer(self): """on_anthropic_stream_event raises RuntimeError when TextDelta received without buffer.""" policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() # Send text delta WITHOUT starting a block first text_delta = TextDelta.model_construct(type="text_delta", text="orphan delta") @@ -681,7 +682,7 @@ async def test_on_stream_event_raises_on_text_delta_without_buffer(self): async def test_on_stream_event_raises_on_json_delta_without_buffer(self): """on_anthropic_stream_event raises RuntimeError when InputJSONDelta received without buffer.""" policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() # Send JSON delta WITHOUT starting a tool block first json_delta = InputJSONDelta.model_construct(type="input_json_delta", partial_json='{"key":') @@ -704,7 +705,7 @@ async def test_on_stream_event_raises_on_malformed_json(self): import json as json_module policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() # Start tool_use block start_event = RawContentBlockStartEvent.model_construct( @@ -741,7 +742,7 @@ async def test_on_stream_event_raises_on_malformed_json(self): async def test_on_anthropic_response_raises_on_missing_tool_use_id(self): """on_anthropic_response raises ValueError when tool_use block is missing id.""" policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() response: AnthropicResponse = { "id": "msg_123", @@ -769,7 +770,7 @@ async def test_on_anthropic_response_raises_on_missing_tool_use_id(self): async def test_on_anthropic_response_raises_on_missing_tool_use_name(self): """on_anthropic_response raises ValueError when tool_use block is missing name.""" policy = SimplePolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() response: AnthropicResponse = { "id": "msg_123", diff --git a/tests/luthien_proxy/unit_tests/policies/test_string_replacement_policy.py b/tests/luthien_proxy/unit_tests/policies/test_string_replacement_policy.py index df9651da5..b9ffc1702 100644 --- a/tests/luthien_proxy/unit_tests/policies/test_string_replacement_policy.py +++ b/tests/luthien_proxy/unit_tests/policies/test_string_replacement_policy.py @@ -21,6 +21,7 @@ ) from pydantic import ValidationError from tests.constants import DEFAULT_TEST_MODEL +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from luthien_proxy.llm.types.anthropic import ( AnthropicRequest, @@ -211,7 +212,7 @@ class TestAnthropicRequest: async def test_on_anthropic_request_returns_same_request(self): """on_anthropic_request returns the exact same request object unchanged.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["foo", "bar"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() request: AnthropicRequest = { "model": DEFAULT_TEST_MODEL, @@ -231,7 +232,7 @@ class TestAnthropicResponse: async def test_on_anthropic_response_applies_replacement(self): """on_anthropic_response applies string replacements to text content blocks.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["foo", "bar"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_block: AnthropicTextBlock = {"type": "text", "text": "Hello foo world!"} response: AnthropicResponse = { @@ -255,7 +256,7 @@ async def test_on_anthropic_response_applies_multiple_replacements(self): policy = StringReplacementPolicy( config=StringReplacementConfig(replacements=[["foo", "bar"], ["hello", "goodbye"]]) ) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_block: AnthropicTextBlock = {"type": "text", "text": "hello foo world"} response: AnthropicResponse = { @@ -277,7 +278,7 @@ async def test_on_anthropic_response_applies_multiple_replacements(self): async def test_on_anthropic_response_leaves_tool_use_unchanged(self): """on_anthropic_response does not modify tool_use content blocks.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["weather", "climate"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() tool_use_block: AnthropicToolUseBlock = { "type": "tool_use", @@ -305,7 +306,7 @@ async def test_on_anthropic_response_leaves_tool_use_unchanged(self): async def test_on_anthropic_response_mixed_content_blocks(self): """on_anthropic_response transforms text but leaves tool_use unchanged in mixed content.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["weather", "climate"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_block: AnthropicTextBlock = {"type": "text", "text": "Let me check the weather"} tool_use_block: AnthropicToolUseBlock = { @@ -345,7 +346,7 @@ async def test_on_anthropic_response_match_capitalization_lowercase(self): match_capitalization=True, ) ) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_block: AnthropicTextBlock = {"type": "text", "text": "hello world"} response: AnthropicResponse = { @@ -372,7 +373,7 @@ async def test_on_anthropic_response_match_capitalization_uppercase(self): match_capitalization=True, ) ) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_block: AnthropicTextBlock = {"type": "text", "text": "HELLO world"} response: AnthropicResponse = { @@ -399,7 +400,7 @@ async def test_on_anthropic_response_match_capitalization_multiple_occurrences(s match_capitalization=True, ) ) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_block: AnthropicTextBlock = {"type": "text", "text": "hello HELLO Hello"} response: AnthropicResponse = { @@ -447,7 +448,7 @@ class TestAnthropicStreamEvent: async def test_on_anthropic_stream_event_transforms_text_delta(self): """on_anthropic_stream_event applies replacement to text_delta text, flushing on block stop.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["foo", "bar"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_delta = TextDelta.model_construct(type="text_delta", text="hello foo world") delta_event = RawContentBlockDeltaEvent.model_construct( @@ -465,7 +466,7 @@ async def test_on_anthropic_stream_event_transforms_text_delta(self): async def test_on_anthropic_stream_event_does_not_mutate_original(self): """on_anthropic_stream_event creates new event instead of mutating original.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["foo", "bar"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() original_text = "hello foo world" text_delta = TextDelta.model_construct(type="text_delta", text=original_text) @@ -492,7 +493,7 @@ async def test_on_anthropic_stream_event_match_capitalization(self): match_capitalization=True, ) ) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_delta = TextDelta.model_construct(type="text_delta", text="HELLO world") delta_event = RawContentBlockDeltaEvent.model_construct( @@ -510,7 +511,7 @@ async def test_on_anthropic_stream_event_match_capitalization(self): async def test_on_anthropic_stream_event_leaves_thinking_delta_unchanged(self): """on_anthropic_stream_event does not modify thinking_delta events.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["consider", "think"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() thinking_delta = ThinkingDelta.model_construct(type="thinking_delta", thinking="Let me consider...") event = RawContentBlockDeltaEvent.model_construct( @@ -530,7 +531,7 @@ async def test_on_anthropic_stream_event_leaves_thinking_delta_unchanged(self): async def test_on_anthropic_stream_event_leaves_input_json_delta_unchanged(self): """on_anthropic_stream_event does not modify input_json_delta events.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["loc", "location"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() json_delta = InputJSONDelta.model_construct(type="input_json_delta", partial_json='{"loc') event = RawContentBlockDeltaEvent.model_construct( @@ -550,7 +551,7 @@ async def test_on_anthropic_stream_event_leaves_input_json_delta_unchanged(self) async def test_on_anthropic_stream_event_passes_through_message_start(self): """on_anthropic_stream_event passes through message_start events unchanged.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["test", "demo"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() event = RawMessageStartEvent.model_construct( type="message_start", @@ -573,7 +574,7 @@ async def test_on_anthropic_stream_event_passes_through_message_start(self): async def test_on_anthropic_stream_event_passes_through_content_block_start(self): """on_anthropic_stream_event passes through content_block_start events unchanged.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["test", "demo"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() event = RawContentBlockStartEvent.model_construct( type="content_block_start", @@ -589,7 +590,7 @@ async def test_on_anthropic_stream_event_passes_through_content_block_start(self async def test_on_anthropic_stream_event_passes_through_content_block_stop(self): """content_block_stop passes through when the buffer is empty (no prior text deltas).""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["test", "demo"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() event = RawContentBlockStopEvent.model_construct( type="content_block_stop", @@ -604,7 +605,7 @@ async def test_on_anthropic_stream_event_passes_through_content_block_stop(self) async def test_on_anthropic_stream_event_passes_through_message_delta(self): """on_anthropic_stream_event passes through message_delta events unchanged.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["test", "demo"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() event = RawMessageDeltaEvent.model_construct( type="message_delta", @@ -620,7 +621,7 @@ async def test_on_anthropic_stream_event_passes_through_message_delta(self): async def test_on_anthropic_stream_event_passes_through_message_stop(self): """on_anthropic_stream_event passes through message_stop events unchanged.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["test", "demo"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() event = RawMessageStopEvent.model_construct(type="message_stop") @@ -636,7 +637,7 @@ class TestAnthropicEdgeCases: async def test_empty_replacements_list(self): """Policy with empty replacements list leaves content unchanged.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_block: AnthropicTextBlock = {"type": "text", "text": "Hello world!"} response: AnthropicResponse = { @@ -658,7 +659,7 @@ async def test_empty_replacements_list(self): async def test_none_replacements(self): """Policy with None replacements leaves content unchanged.""" policy = StringReplacementPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_block: AnthropicTextBlock = {"type": "text", "text": "Hello world!"} response: AnthropicResponse = { @@ -680,7 +681,7 @@ async def test_none_replacements(self): async def test_empty_content_list(self): """Policy handles response with empty content list.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["foo", "bar"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() response: AnthropicResponse = { "id": "msg_123", @@ -700,7 +701,7 @@ async def test_empty_content_list(self): async def test_special_regex_characters_in_replacement(self): """Policy handles special regex characters in replacement strings.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["[test]", "check"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text_block: AnthropicTextBlock = {"type": "text", "text": "Hello [test] world!"} response: AnthropicResponse = { @@ -726,7 +727,7 @@ class TestStreamingBufferBehavior: async def test_replacement_spanning_two_chunks(self): """Replacement target split across two chunks is correctly replaced.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["hello", "goodbye"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() events = [ RawContentBlockDeltaEvent.model_construct( @@ -754,7 +755,7 @@ async def test_one_char_at_a_time(self): match_capitalization=True, ) ) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() text = "say Hello!" events: list[Any] = [ @@ -779,7 +780,7 @@ async def test_multiple_replacements_across_chunks(self): match_capitalization=True, ) ) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() # "apple" split as "app" + "le", "grape" split as "gra" + "pe" events = [ @@ -808,7 +809,7 @@ async def test_multiple_replacements_across_chunks(self): async def test_no_buffering_for_single_char_replacements(self): """Single-char replacements don't use buffering (no chunk boundary issue).""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["a", "x"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() # buffer_size should be 0 for single-char source assert policy._buffer_size == 0 @@ -830,7 +831,7 @@ async def test_no_buffering_for_single_char_replacements(self): async def test_buffer_flushed_on_stream_complete(self): """on_anthropic_stream_complete flushes any remaining buffer.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["hello", "hi"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() # Send text without a content_block_stop text_delta = TextDelta.model_construct(type="text_delta", text="hello") @@ -858,7 +859,7 @@ async def test_buffer_flushed_on_stream_complete(self): async def test_empty_buffer_on_stream_complete(self): """on_anthropic_stream_complete returns empty list when no buffer remains.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["foo", "bar"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() result = await policy.on_anthropic_stream_complete(ctx) assert result == [] @@ -867,7 +868,7 @@ async def test_empty_buffer_on_stream_complete(self): async def test_double_flush_does_not_double_emit(self): """content_block_stop flush followed by stream_complete does not emit twice.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["hello", "hi"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() # Send text + content_block_stop (flushes buffer) events: list[Any] = [ @@ -889,7 +890,7 @@ async def test_double_flush_does_not_double_emit(self): async def test_replacement_target_at_exact_end_of_stream(self): """Replacement target as the final chunk is correctly replaced on flush.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["hello", "goodbye"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() events: list[Any] = [ RawContentBlockDeltaEvent.model_construct( @@ -912,7 +913,7 @@ async def test_replacement_target_at_exact_end_of_stream(self): async def test_buffer_resets_between_content_blocks(self): """Buffer flushes on content_block_stop so second block starts clean.""" policy = StringReplacementPolicy(config=StringReplacementConfig(replacements=[["hello", "hi"]])) - ctx = PolicyContext.for_testing() + ctx = make_policy_context() events: list[Any] = [ # Block 0 diff --git a/tests/luthien_proxy/unit_tests/policies/test_tool_call_judge_policy.py b/tests/luthien_proxy/unit_tests/policies/test_tool_call_judge_policy.py index 9ef1e3992..13954a5f2 100644 --- a/tests/luthien_proxy/unit_tests/policies/test_tool_call_judge_policy.py +++ b/tests/luthien_proxy/unit_tests/policies/test_tool_call_judge_policy.py @@ -21,6 +21,7 @@ TextDelta, ToolUseBlock, ) +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from tests.luthien_proxy.unit_tests.policies.anthropic_event_builders import ( block_stop, event_types, @@ -53,7 +54,7 @@ def _make_policy(**overrides) -> ToolCallJudgePolicy: def _make_context() -> PolicyContext: """Create a fresh PolicyContext for testing.""" - return PolicyContext.for_testing(transaction_id="test-txn") + return make_policy_context(transaction_id="test-txn") # ============================================================================ diff --git a/tests/luthien_proxy/unit_tests/policy_core/test_anthropic_execution_interface.py b/tests/luthien_proxy/unit_tests/policy_core/test_anthropic_execution_interface.py index 8653421fb..1bfe1c0a7 100644 --- a/tests/luthien_proxy/unit_tests/policy_core/test_anthropic_execution_interface.py +++ b/tests/luthien_proxy/unit_tests/policy_core/test_anthropic_execution_interface.py @@ -6,6 +6,7 @@ from anthropic.lib.streaming import MessageStreamEvent from anthropic.types import RawContentBlockStartEvent from tests.constants import DEFAULT_TEST_MODEL +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from luthien_proxy.llm.types.anthropic import AnthropicRequest, AnthropicResponse from luthien_proxy.policy_core.anthropic_execution_interface import ( @@ -78,7 +79,7 @@ async def on_anthropic_stream_complete(self, context: PolicyContext) -> list[Ant policy = cast(AnthropicExecutionInterface, TestPolicy()) request: AnthropicRequest = {"model": DEFAULT_TEST_MODEL, "messages": [], "max_tokens": 1} - result = await policy.on_anthropic_request(request, PolicyContext.for_testing()) + result = await policy.on_anthropic_request(request, make_policy_context()) assert result.get("modified") is True @pytest.mark.asyncio @@ -111,7 +112,7 @@ async def on_anthropic_stream_complete(self, context: PolicyContext) -> list[Ant "usage": {"input_tokens": 1, "output_tokens": 1}, } - result = await policy.on_anthropic_response(response, PolicyContext.for_testing()) + result = await policy.on_anthropic_response(response, make_policy_context()) assert result["type"] == "message" @pytest.mark.asyncio @@ -138,7 +139,7 @@ async def on_anthropic_stream_complete(self, context: PolicyContext) -> list[Ant type="content_block_start", index=0, content_block={"type": "text", "text": ""} ) - result = await policy.on_anthropic_stream_event(event, PolicyContext.for_testing()) + result = await policy.on_anthropic_stream_event(event, make_policy_context()) assert len(result) == 2 assert result[0].type == "content_block_start" @@ -162,5 +163,5 @@ async def on_anthropic_stream_complete(self, context: PolicyContext) -> list[Ant return [] policy = cast(AnthropicExecutionInterface, TestPolicy()) - result = await policy.on_anthropic_stream_complete(PolicyContext.for_testing()) + result = await policy.on_anthropic_stream_complete(make_policy_context()) assert result == [] diff --git a/tests/luthien_proxy/unit_tests/policy_core/test_anthropic_hook_policy.py b/tests/luthien_proxy/unit_tests/policy_core/test_anthropic_hook_policy.py index efee802da..5e90ad0c9 100644 --- a/tests/luthien_proxy/unit_tests/policy_core/test_anthropic_hook_policy.py +++ b/tests/luthien_proxy/unit_tests/policy_core/test_anthropic_hook_policy.py @@ -3,9 +3,9 @@ from __future__ import annotations import pytest +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from luthien_proxy.policy_core.anthropic_hook_policy import AnthropicHookPolicy -from luthien_proxy.policy_core.policy_context import PolicyContext class TestAnthropicHookPolicyDefaults: @@ -15,7 +15,7 @@ class TestAnthropicHookPolicyDefaults: async def test_on_anthropic_request_default_passthrough(self): """Default on_anthropic_request returns request unchanged.""" policy = AnthropicHookPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() request = {"model": "claude-3-5-sonnet", "messages": [], "max_tokens": 100} result = await policy.on_anthropic_request(request, ctx) @@ -26,7 +26,7 @@ async def test_on_anthropic_request_default_passthrough(self): async def test_on_anthropic_response_default_passthrough(self): """Default on_anthropic_response returns response unchanged.""" policy = AnthropicHookPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() response = { "id": "msg_test", "type": "message", @@ -47,7 +47,7 @@ async def test_on_anthropic_stream_event_default_returns_single_event(self): from anthropic.types import RawContentBlockDeltaEvent, TextDelta policy = AnthropicHookPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() event = RawContentBlockDeltaEvent( type="content_block_delta", index=0, @@ -62,7 +62,7 @@ async def test_on_anthropic_stream_event_default_returns_single_event(self): async def test_on_anthropic_stream_complete_default_returns_empty(self): """Default on_anthropic_stream_complete returns empty list.""" policy = AnthropicHookPolicy() - ctx = PolicyContext.for_testing() + ctx = make_policy_context() result = await policy.on_anthropic_stream_complete(ctx) diff --git a/tests/luthien_proxy/unit_tests/policy_core/test_policy_context.py b/tests/luthien_proxy/unit_tests/policy_core/test_policy_context.py index 2da15ee08..d9c8d4307 100644 --- a/tests/luthien_proxy/unit_tests/policy_core/test_policy_context.py +++ b/tests/luthien_proxy/unit_tests/policy_core/test_policy_context.py @@ -5,6 +5,7 @@ import pytest from opentelemetry import trace +from tests.luthien_proxy.fixtures.policy_context import make_policy_context from luthien_proxy.policy_core.policy_context import PolicyContext @@ -14,7 +15,7 @@ class TestPolicyContextSpan: def test_span_creates_child_span_with_policy_prefix(self): """Span names are prefixed with 'policy.' automatically.""" - ctx = PolicyContext.for_testing(transaction_id="test-123") + ctx = make_policy_context(transaction_id="test-123") mock_span = MagicMock() mock_span.__enter__ = MagicMock(return_value=mock_span) @@ -30,7 +31,7 @@ def test_span_creates_child_span_with_policy_prefix(self): def test_span_does_not_double_prefix(self): """If name already has policy. prefix, don't add it again.""" - ctx = PolicyContext.for_testing(transaction_id="test-123") + ctx = make_policy_context(transaction_id="test-123") mock_span = MagicMock() mock_span.__enter__ = MagicMock(return_value=mock_span) @@ -46,7 +47,7 @@ def test_span_does_not_double_prefix(self): def test_span_includes_transaction_id(self): """Spans include the transaction_id attribute.""" - ctx = PolicyContext.for_testing(transaction_id="txn-456") + ctx = make_policy_context(transaction_id="txn-456") mock_span = MagicMock() mock_span.__enter__ = MagicMock(return_value=mock_span) @@ -62,7 +63,7 @@ def test_span_includes_transaction_id(self): def test_span_accepts_custom_attributes(self): """Custom attributes are set on the span.""" - ctx = PolicyContext.for_testing(transaction_id="test-123") + ctx = make_policy_context(transaction_id="test-123") mock_span = MagicMock() mock_span.__enter__ = MagicMock(return_value=mock_span) @@ -80,7 +81,7 @@ def test_span_accepts_custom_attributes(self): def test_span_yields_span_for_further_customization(self): """The yielded span can be used to add events and attributes.""" - ctx = PolicyContext.for_testing(transaction_id="test-123") + ctx = make_policy_context(transaction_id="test-123") mock_span = MagicMock() mock_span.__enter__ = MagicMock(return_value=mock_span) @@ -102,7 +103,7 @@ class TestPolicyContextAddSpanEvent: def test_add_span_event_adds_to_current_span(self): """Events are added to the current span.""" - ctx = PolicyContext.for_testing(transaction_id="test-123") + ctx = make_policy_context(transaction_id="test-123") mock_span = MagicMock() mock_span.is_recording.return_value = True @@ -114,7 +115,7 @@ def test_add_span_event_adds_to_current_span(self): def test_add_span_event_with_attributes(self): """Events can include attributes.""" - ctx = PolicyContext.for_testing(transaction_id="test-123") + ctx = make_policy_context(transaction_id="test-123") mock_span = MagicMock() mock_span.is_recording.return_value = True @@ -128,7 +129,7 @@ def test_add_span_event_with_attributes(self): def test_add_span_event_no_op_when_not_recording(self): """Adding event when span is not recording is a no-op.""" - ctx = PolicyContext.for_testing(transaction_id="test-123") + ctx = make_policy_context(transaction_id="test-123") mock_span = MagicMock() mock_span.is_recording.return_value = False @@ -140,18 +141,18 @@ def test_add_span_event_no_op_when_not_recording(self): class TestPolicyContextForTesting: - """Tests for PolicyContext.for_testing() factory.""" + """Tests for make_policy_context() factory.""" def test_for_testing_creates_valid_context(self): """for_testing() creates a usable PolicyContext.""" - ctx = PolicyContext.for_testing() + ctx = make_policy_context() assert ctx.transaction_id == "test-txn" assert ctx.request is None assert ctx.session_id is None - def test_for_testing_accepts_custom_values(self): - """for_testing() accepts custom transaction_id and session_id.""" - ctx = PolicyContext.for_testing(transaction_id="custom-txn", session_id="sess-123") + def test_make_policy_context_accepts_custom_values(self): + """make_policy_context() accepts custom transaction_id and session_id.""" + ctx = make_policy_context(transaction_id="custom-txn", session_id="sess-123") assert ctx.transaction_id == "custom-txn" assert ctx.session_id == "sess-123" @@ -200,14 +201,14 @@ class TestPolicyContextScratchpad: def test_scratchpad_is_mutable_dict(self): """scratchpad is a mutable dictionary.""" - ctx = PolicyContext.for_testing() + ctx = make_policy_context() ctx.scratchpad["key"] = "value" assert ctx.scratchpad["key"] == "value" def test_scratchpad_persists_across_accesses(self): """scratchpad retains values across multiple accesses.""" - ctx = PolicyContext.for_testing() + ctx = make_policy_context() ctx.scratchpad["counter"] = 0 ctx.scratchpad["counter"] += 1 @@ -217,8 +218,8 @@ def test_scratchpad_persists_across_accesses(self): def test_scratchpad_is_isolated_per_context(self): """Each context has its own scratchpad.""" - ctx1 = PolicyContext.for_testing(transaction_id="ctx1") - ctx2 = PolicyContext.for_testing(transaction_id="ctx2") + ctx1 = make_policy_context(transaction_id="ctx1") + ctx2 = make_policy_context(transaction_id="ctx2") ctx1.scratchpad["value"] = "from ctx1" ctx2.scratchpad["value"] = "from ctx2" @@ -232,26 +233,26 @@ class TestPolicyContextSummaries: def test_summaries_default_to_none(self): """Summary fields default to None.""" - ctx = PolicyContext.for_testing() + ctx = make_policy_context() assert ctx.request_summary is None assert ctx.response_summary is None def test_request_summary_can_be_set(self): """Policies can set request_summary.""" - ctx = PolicyContext.for_testing() + ctx = make_policy_context() ctx.request_summary = "pass_through" assert ctx.request_summary == "pass_through" def test_response_summary_can_be_set(self): """Policies can set response_summary.""" - ctx = PolicyContext.for_testing() + ctx = make_policy_context() ctx.response_summary = "blocked rm -rf: high risk score (0.92)" assert ctx.response_summary == "blocked rm -rf: high risk score (0.92)" def test_summaries_are_isolated_per_context(self): """Each context has its own summaries.""" - ctx1 = PolicyContext.for_testing(transaction_id="ctx1") - ctx2 = PolicyContext.for_testing(transaction_id="ctx2") + ctx1 = make_policy_context(transaction_id="ctx1") + ctx2 = make_policy_context(transaction_id="ctx2") ctx1.request_summary = "modified" ctx2.request_summary = "pass_through" @@ -261,7 +262,7 @@ def test_summaries_are_isolated_per_context(self): def test_summaries_can_be_any_string(self): """Summaries are free-form text fields.""" - ctx = PolicyContext.for_testing() + ctx = make_policy_context() ctx.request_summary = "Added safety prefix to system prompt" ctx.response_summary = "Redacted 3 PII patterns (email, phone, SSN)" @@ -274,25 +275,25 @@ class TestPolicyContextPolicyCache: def test_policy_cache_raises_without_factory(self): """Accessing policy_cache() without a configured factory raises RuntimeError.""" - ctx = PolicyContext.for_testing() + ctx = make_policy_context() with pytest.raises(RuntimeError, match="PolicyCache not available"): ctx.policy_cache("SomePolicy") def test_has_policy_cache_false_by_default(self): """has_policy_cache defaults to False when no factory is configured.""" - ctx = PolicyContext.for_testing() + ctx = make_policy_context() assert ctx.has_policy_cache is False def test_has_policy_cache_true_with_factory(self): """has_policy_cache is True when a factory is supplied.""" sentinel = MagicMock(name="PolicyCacheInstance") - ctx = PolicyContext.for_testing(policy_cache_factory=lambda name: sentinel) + ctx = make_policy_context(policy_cache_factory=lambda name: sentinel) assert ctx.has_policy_cache is True def test_policy_cache_forwards_name_to_factory(self): """policy_cache() invokes the factory with the supplied policy name.""" factory = MagicMock(return_value=MagicMock(name="PolicyCacheInstance")) - ctx = PolicyContext.for_testing(policy_cache_factory=factory) + ctx = make_policy_context(policy_cache_factory=factory) result = ctx.policy_cache("MyPolicy") @@ -300,15 +301,15 @@ def test_policy_cache_forwards_name_to_factory(self): assert result is factory.return_value def test_for_testing_forwards_policy_cache_factory(self): - """PolicyContext.for_testing() propagates the factory through to the instance.""" + """make_policy_context() propagates the factory through to the instance.""" factory = lambda name: MagicMock(name=f"cache-{name}") # noqa: E731 - ctx = PolicyContext.for_testing(policy_cache_factory=factory) + ctx = make_policy_context(policy_cache_factory=factory) assert ctx._policy_cache_factory is factory def test_deepcopy_preserves_factory_identity(self): """deepcopy shares the factory reference so sub-policies hit the same infra.""" factory = MagicMock(return_value=MagicMock()) - ctx = PolicyContext.for_testing(policy_cache_factory=factory) + ctx = make_policy_context(policy_cache_factory=factory) copied = copy.deepcopy(ctx) diff --git a/tests/luthien_proxy/unit_tests/policy_core/test_policy_context_policy_state.py b/tests/luthien_proxy/unit_tests/policy_core/test_policy_context_policy_state.py index f4da66b32..88ed1bd79 100644 --- a/tests/luthien_proxy/unit_tests/policy_core/test_policy_context_policy_state.py +++ b/tests/luthien_proxy/unit_tests/policy_core/test_policy_context_policy_state.py @@ -4,8 +4,7 @@ from typing import Any, cast import pytest - -from luthien_proxy.policy_core import PolicyContext +from tests.luthien_proxy.fixtures.policy_context import make_policy_context @dataclass @@ -19,7 +18,7 @@ class _Owner: class TestPolicyContextRequestState: def test_get_request_state_creates_and_reuses_by_owner_and_type(self): - ctx = PolicyContext.for_testing() + ctx = make_policy_context() owner = _Owner() state_a = ctx.get_request_state(owner, _State, _State) @@ -30,7 +29,7 @@ def test_get_request_state_creates_and_reuses_by_owner_and_type(self): assert state_b.values[1] == "hello" def test_get_request_state_isolated_between_owners(self): - ctx = PolicyContext.for_testing() + ctx = make_policy_context() owner_a = _Owner() owner_b = _Owner() @@ -42,7 +41,7 @@ def test_get_request_state_isolated_between_owners(self): assert state_b.values == {} def test_pop_request_state_removes_owned_state(self): - ctx = PolicyContext.for_testing() + ctx = make_policy_context() owner = _Owner() ctx.get_request_state(owner, _State, _State).values[1] = "hello" @@ -52,7 +51,7 @@ def test_pop_request_state_removes_owned_state(self): assert ctx.pop_request_state(owner, _State) is None def test_get_request_state_raises_if_stored_type_mismatch(self): - ctx = PolicyContext.for_testing() + ctx = make_policy_context() owner = _Owner() cast(Any, ctx)._request_state[(id(owner), _State)] = {"unexpected": "dict"} @@ -60,7 +59,7 @@ def test_get_request_state_raises_if_stored_type_mismatch(self): ctx.get_request_state(owner, _State, _State) def test_get_request_state_raises_if_factory_returns_wrong_type(self): - ctx = PolicyContext.for_testing() + ctx = make_policy_context() owner = _Owner() with pytest.raises(TypeError, match="returned dict, expected _State"):