Files

1 line
54 KiB
JSON

{"repo": "bishalops/tfrev", "files": {"tests/test_client.py": {"d8dcac736b3cf3acf4b5cd5e563dfb60e1edb2ff": {"t": 1785747150.9610174, "n": 10810, "h": "fad4f11648b4a44d", "c": "\"\"\"Tests for tfrev.client.\"\"\"\n\nfrom __future__ import annotations\n\nfrom unittest.mock import MagicMock, patch\n\nimport anthropic\nimport pytest\n\nfrom tfrev.client import APIResponse, ReviewClient\nfrom tfrev.config import TfrevConfig\n\n\n@pytest.fixture\ndef config():\n return TfrevConfig()\n\n\n@pytest.fixture\ndef mock_anthropic_response():\n block = MagicMock()\n block.text = '{\"verdict\": \"PASS\"}'\n response = MagicMock()\n response.content = [block]\n response.model = \"claude-sonnet-4-6\"\n response.usage.input_tokens = 100\n response.usage.output_tokens = 50\n response.stop_reason = \"end_turn\"\n return response\n\n\nclass TestReviewClientInit:\n def test_missing_api_key_raises(self, config, monkeypatch):\n monkeypatch.delenv(\"ANTHROPIC_API_KEY\", raising=False)\n with pytest.raises(RuntimeError, match=\"ANTHROPIC_API_KEY\"):\n ReviewClient(config)\n\n def test_initializes_with_api_key(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n client = ReviewClient(config)\n assert client._client.api_key == \"sk-ant-test\"\n\n def test_bedrock_claude_model_uses_anthropic_bedrock(self):\n bedrock_config = TfrevConfig(provider=\"aws-bedrock\", model=\"anthropic.claude-sonnet-4-5\")\n with patch(\"tfrev.client.anthropic.AnthropicBedrock\") as mock_cls:\n with patch.dict(\"sys.modules\", {\"boto3\": MagicMock()}):\n client = ReviewClient(bedrock_config)\n mock_cls.assert_called_once()\n assert client._boto3_bedrock_client is None\n\n def test_bedrock_non_claude_model_uses_boto3(self, monkeypatch):\n bedrock_config = TfrevConfig(provider=\"aws-bedrock\", model=\"deepseek.deepseek-r1-v1:0\")\n mock_boto3 = MagicMock()\n with patch.dict(\"sys.modules\", {\"boto3\": mock_boto3}):\n with patch(\"tfrev.client.anthropic.AnthropicBedrock\") as mock_bedrock_cls:\n client = ReviewClient(bedrock_config)\n mock_bedrock_cls.assert_not_called()\n mock_boto3.client.assert_called_once_with(\n \"bedrock-runtime\", config=mock_boto3.session.Config.return_value\n )\n assert client._boto3_bedrock_client is not None\n assert client._client is None\n\n\nclass TestReviewClientReview:\n def test_successful_call(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = mock_anthropic_response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert isinstance(result, APIResponse)\n assert result.content == '{\"verdict\": \"PASS\"}'\n assert result.model == \"claude-sonnet-4-6\"\n assert result.input_tokens == 100\n assert result.output_tokens == 50\n assert result.stop_reason == \"end_turn\"\n\n def test_rate_limit_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.RateLimitError(\n message=\"rate limited\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"Rate limit exceeded\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_internal_server_error_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.InternalServerError(\n message=\"overloaded\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"server error\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_auth_error_raises_immediately(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-bad\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.side_effect = anthropic.AuthenticationError(\n message=\"invalid key\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"Invalid ANTHROPIC_API_KEY\"):\n client.review(\"system\", \"user\")\n\n # No retries for auth errors\n assert mock_cls.return_value.messages.create.call_count == 1\n\n def test_api_error_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.APIError(\n message=\"something went wrong\", request=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"failed after\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_rate_limit_succeeds_on_retry(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = [\n anthropic.RateLimitError(message=\"rate limited\", response=MagicMock(), body={}),\n mock_anthropic_response,\n ]\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.content == '{\"verdict\": \"PASS\"}'\n assert mock_cls.return_value.messages.create.call_count == 2\n\n def test_multiple_content_blocks_concatenated(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n block1 = MagicMock()\n block1.text = \"hello \"\n block2 = MagicMock()\n block2.text = \"world\"\n response = MagicMock()\n response.content = [block1, block2]\n response.model = \"claude-sonnet-4-6\"\n response.usage.input_tokens = 10\n response.usage.output_tokens = 5\n response.stop_reason = \"end_turn\"\n\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.content == \"hello world\"\n\n def test_stop_reason_none_becomes_unknown(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n mock_anthropic_response.stop_reason = None\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = mock_anthropic_response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.stop_reason == \"unknown\"\n\n\nclass TestBedrockConverseReview:\n \"\"\"Tests for the boto3 converse path used by non-Claude Bedrock models.\"\"\"\n\n @pytest.fixture\n def bedrock_config(self):\n return TfrevConfig(provider=\"aws-bedrock\", model=\"deepseek.deepseek-r1-v1:0\")\n\n @pytest.fixture\n def mock_converse_response(self):\n return {\n \"output\": {\"message\": {\"content\": [{\"text\": '{\"verdict\": \"PASS\"}'}]}},\n \"usage\": {\"inputTokens\": 200, \"outputTokens\": 80},\n \"stopReason\": \"end_turn\",\n }\n\n def _make_client(self, bedrock_config, mock_boto3_client):\n mock_boto3 = MagicMock()\n mock_boto3.client.return_value = mock_boto3_client\n with patch.dict(\"sys.modules\", {\"boto3\": mock_boto3}):\n return ReviewClient(bedrock_config)\n\n def test_successful_converse_call(self, bedrock_config, mock_converse_response):\n mock_boto3_client = MagicMock()\n mock_boto3_client.converse.return_value = mock_converse_response\n client = self._make_client(bedrock_config, mock_boto3_client)\n\n result = client.review(\"system\", \"user\")\n\n assert isinstance(result, APIResponse)\n assert result.content == '{\"verdict\": \"PASS\"}'\n assert result.model == \"deepseek.deepseek-r1-v1:0\"\n assert result.input_tokens == 200\n assert result.output_tokens == 80\n assert result.stop_reason == \"end_turn\"\n\n def test_converse_passes_correct_params(self, bedrock_config, mock_converse_response):\n mock_boto3_client = MagicMock()\n mock_boto3_client.converse.return_value = mock_converse_response\n client = self._make_client(bedrock_config, mock_boto3_client)\n\n client.review(\"my system prompt\", \"my user prompt\")\n\n mock_boto3_client.converse.assert_called_once_with(\n modelId=\"deepseek.deepseek-r1-v1:0\",\n system=[{\"text\": \"my system prompt\"}],\n messages=[{\"role\": \"user\", \"content\": [{\"text\": \"my user prompt\"}]}],\n inferenceConfig={\"maxTokens\": bedrock_config.max_tokens},\n )\n\n def test_converse_stop_reason_none_becomes_unknown(\n self, bedrock_config, mock_converse_response\n ):\n mock_converse_response[\"stopReason\"] = None\n mock_boto3_client = MagicMock()\n mock_boto3_client.converse.return_value = mock_converse_response\n client = self._make_client(bedrock_config, mock_boto3_client)\n\n result = client.review(\"system\", \"user\")\n\n assert result.stop_reason == \"unknown\"\n\n def test_converse_multiple_content_blocks_concatenated(self, bedrock_config):\n mock_boto3_client = MagicMock()\n mock_boto3_client.converse.return_value = {\n \"output\": {\"message\": {\"content\": [{\"text\": \"hello \"}, {\"text\": \"world\"}]}},\n \"usage\": {\"inputTokens\": 10, \"outputTokens\": 5},\n \"stopReason\": \"end_turn\",\n }\n client = self._make_client(bedrock_config, mock_boto3_client)\n\n result = client.review(\"system\", \"user\")\n\n assert result.content == \"hello world\"\n"}, "a43a3d9baa7612179c57506312350586f5178fe2": {"t": 1785770720.7948601, "n": 10810, "h": "fad4f11648b4a44d", "c": "\"\"\"Tests for tfrev.client.\"\"\"\n\nfrom __future__ import annotations\n\nfrom unittest.mock import MagicMock, patch\n\nimport anthropic\nimport pytest\n\nfrom tfrev.client import APIResponse, ReviewClient\nfrom tfrev.config import TfrevConfig\n\n\n@pytest.fixture\ndef config():\n return TfrevConfig()\n\n\n@pytest.fixture\ndef mock_anthropic_response():\n block = MagicMock()\n block.text = '{\"verdict\": \"PASS\"}'\n response = MagicMock()\n response.content = [block]\n response.model = \"claude-sonnet-4-6\"\n response.usage.input_tokens = 100\n response.usage.output_tokens = 50\n response.stop_reason = \"end_turn\"\n return response\n\n\nclass TestReviewClientInit:\n def test_missing_api_key_raises(self, config, monkeypatch):\n monkeypatch.delenv(\"ANTHROPIC_API_KEY\", raising=False)\n with pytest.raises(RuntimeError, match=\"ANTHROPIC_API_KEY\"):\n ReviewClient(config)\n\n def test_initializes_with_api_key(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n client = ReviewClient(config)\n assert client._client.api_key == \"sk-ant-test\"\n\n def test_bedrock_claude_model_uses_anthropic_bedrock(self):\n bedrock_config = TfrevConfig(provider=\"aws-bedrock\", model=\"anthropic.claude-sonnet-4-5\")\n with patch(\"tfrev.client.anthropic.AnthropicBedrock\") as mock_cls:\n with patch.dict(\"sys.modules\", {\"boto3\": MagicMock()}):\n client = ReviewClient(bedrock_config)\n mock_cls.assert_called_once()\n assert client._boto3_bedrock_client is None\n\n def test_bedrock_non_claude_model_uses_boto3(self, monkeypatch):\n bedrock_config = TfrevConfig(provider=\"aws-bedrock\", model=\"deepseek.deepseek-r1-v1:0\")\n mock_boto3 = MagicMock()\n with patch.dict(\"sys.modules\", {\"boto3\": mock_boto3}):\n with patch(\"tfrev.client.anthropic.AnthropicBedrock\") as mock_bedrock_cls:\n client = ReviewClient(bedrock_config)\n mock_bedrock_cls.assert_not_called()\n mock_boto3.client.assert_called_once_with(\n \"bedrock-runtime\", config=mock_boto3.session.Config.return_value\n )\n assert client._boto3_bedrock_client is not None\n assert client._client is None\n\n\nclass TestReviewClientReview:\n def test_successful_call(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = mock_anthropic_response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert isinstance(result, APIResponse)\n assert result.content == '{\"verdict\": \"PASS\"}'\n assert result.model == \"claude-sonnet-4-6\"\n assert result.input_tokens == 100\n assert result.output_tokens == 50\n assert result.stop_reason == \"end_turn\"\n\n def test_rate_limit_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.RateLimitError(\n message=\"rate limited\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"Rate limit exceeded\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_internal_server_error_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.InternalServerError(\n message=\"overloaded\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"server error\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_auth_error_raises_immediately(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-bad\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.side_effect = anthropic.AuthenticationError(\n message=\"invalid key\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"Invalid ANTHROPIC_API_KEY\"):\n client.review(\"system\", \"user\")\n\n # No retries for auth errors\n assert mock_cls.return_value.messages.create.call_count == 1\n\n def test_api_error_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.APIError(\n message=\"something went wrong\", request=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"failed after\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_rate_limit_succeeds_on_retry(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = [\n anthropic.RateLimitError(message=\"rate limited\", response=MagicMock(), body={}),\n mock_anthropic_response,\n ]\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.content == '{\"verdict\": \"PASS\"}'\n assert mock_cls.return_value.messages.create.call_count == 2\n\n def test_multiple_content_blocks_concatenated(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n block1 = MagicMock()\n block1.text = \"hello \"\n block2 = MagicMock()\n block2.text = \"world\"\n response = MagicMock()\n response.content = [block1, block2]\n response.model = \"claude-sonnet-4-6\"\n response.usage.input_tokens = 10\n response.usage.output_tokens = 5\n response.stop_reason = \"end_turn\"\n\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.content == \"hello world\"\n\n def test_stop_reason_none_becomes_unknown(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n mock_anthropic_response.stop_reason = None\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = mock_anthropic_response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.stop_reason == \"unknown\"\n\n\nclass TestBedrockConverseReview:\n \"\"\"Tests for the boto3 converse path used by non-Claude Bedrock models.\"\"\"\n\n @pytest.fixture\n def bedrock_config(self):\n return TfrevConfig(provider=\"aws-bedrock\", model=\"deepseek.deepseek-r1-v1:0\")\n\n @pytest.fixture\n def mock_converse_response(self):\n return {\n \"output\": {\"message\": {\"content\": [{\"text\": '{\"verdict\": \"PASS\"}'}]}},\n \"usage\": {\"inputTokens\": 200, \"outputTokens\": 80},\n \"stopReason\": \"end_turn\",\n }\n\n def _make_client(self, bedrock_config, mock_boto3_client):\n mock_boto3 = MagicMock()\n mock_boto3.client.return_value = mock_boto3_client\n with patch.dict(\"sys.modules\", {\"boto3\": mock_boto3}):\n return ReviewClient(bedrock_config)\n\n def test_successful_converse_call(self, bedrock_config, mock_converse_response):\n mock_boto3_client = MagicMock()\n mock_boto3_client.converse.return_value = mock_converse_response\n client = self._make_client(bedrock_config, mock_boto3_client)\n\n result = client.review(\"system\", \"user\")\n\n assert isinstance(result, APIResponse)\n assert result.content == '{\"verdict\": \"PASS\"}'\n assert result.model == \"deepseek.deepseek-r1-v1:0\"\n assert result.input_tokens == 200\n assert result.output_tokens == 80\n assert result.stop_reason == \"end_turn\"\n\n def test_converse_passes_correct_params(self, bedrock_config, mock_converse_response):\n mock_boto3_client = MagicMock()\n mock_boto3_client.converse.return_value = mock_converse_response\n client = self._make_client(bedrock_config, mock_boto3_client)\n\n client.review(\"my system prompt\", \"my user prompt\")\n\n mock_boto3_client.converse.assert_called_once_with(\n modelId=\"deepseek.deepseek-r1-v1:0\",\n system=[{\"text\": \"my system prompt\"}],\n messages=[{\"role\": \"user\", \"content\": [{\"text\": \"my user prompt\"}]}],\n inferenceConfig={\"maxTokens\": bedrock_config.max_tokens},\n )\n\n def test_converse_stop_reason_none_becomes_unknown(\n self, bedrock_config, mock_converse_response\n ):\n mock_converse_response[\"stopReason\"] = None\n mock_boto3_client = MagicMock()\n mock_boto3_client.converse.return_value = mock_converse_response\n client = self._make_client(bedrock_config, mock_boto3_client)\n\n result = client.review(\"system\", \"user\")\n\n assert result.stop_reason == \"unknown\"\n\n def test_converse_multiple_content_blocks_concatenated(self, bedrock_config):\n mock_boto3_client = MagicMock()\n mock_boto3_client.converse.return_value = {\n \"output\": {\"message\": {\"content\": [{\"text\": \"hello \"}, {\"text\": \"world\"}]}},\n \"usage\": {\"inputTokens\": 10, \"outputTokens\": 5},\n \"stopReason\": \"end_turn\",\n }\n client = self._make_client(bedrock_config, mock_boto3_client)\n\n result = client.review(\"system\", \"user\")\n\n assert result.content == \"hello world\"\n"}, "6fb77922f5fb5df5d3f73fcc02825514ed0b5ce2": {"t": 1785770724.4731755, "n": 10796, "h": "2b140202a5a6bb30", "c": "\"\"\"Tests for tfrev.client.\"\"\"\n\nfrom __future__ import annotations\n\nfrom unittest.mock import MagicMock, patch\n\nimport anthropic\nimport pytest\n\nfrom tfrev.client import APIResponse, ReviewClient\nfrom tfrev.config import TfrevConfig\n\n\n@pytest.fixture\ndef config():\n return TfrevConfig()\n\n\n@pytest.fixture\ndef mock_anthropic_response():\n block = MagicMock()\n block.text = '{\"verdict\": \"PASS\"}'\n response = MagicMock()\n response.content = [block]\n response.model = \"claude-sonnet-4-6\"\n response.usage.input_tokens = 100\n response.usage.output_tokens = 50\n response.stop_reason = \"end_turn\"\n return response\n\n\nclass TestReviewClientInit:\n def test_missing_api_key_raises(self, config, monkeypatch):\n monkeypatch.delenv(\"ANTHROPIC_API_KEY\", raising=False)\n with pytest.raises(RuntimeError, match=\"ANTHROPIC_API_KEY\"):\n ReviewClient(config)\n\n def test_initializes_with_api_key(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n client = ReviewClient(config)\n assert client._client.api_key == \"sk-ant-test\"\n\n def test_bedrock_claude_model_uses_anthropic_bedrock(self):\n bedrock_config = TfrevConfig(provider=\"aws-bedrock\", model=\"anthropic.claude-sonnet-4-5\")\n with patch(\"tfrev.client.anthropic.AnthropicBedrock\") as mock_cls:\n with patch.dict(\"sys.modules\", {\"boto3\": MagicMock()}):\n client = ReviewClient(bedrock_config)\n mock_cls.assert_called_once()\n assert client._boto3_bedrock_client is None\n\n def test_bedrock_non_claude_model_uses_boto3(self, monkeypatch):\n bedrock_config = TfrevConfig(provider=\"aws-bedrock\", model=\"deepseek.deepseek-r1-v1:0\")\n mock_boto3 = MagicMock()\n with patch.dict(\"sys.modules\", {\"boto3\": mock_boto3}):\n with patch(\"tfrev.client.anthropic.AnthropicBedrock\") as mock_bedrock_cls:\n client = ReviewClient(bedrock_config)\n mock_bedrock_cls.assert_not_called()\n mock_boto3.client.assert_called_once_with(\n \"bedrock-runtime\", config=mock_boto3.session.Config.return_value\n )\n assert client._boto3_bedrock_client is not None\n assert client._client is None\n\n\nclass TestReviewClientReview:\n def test_successful_call(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = mock_anthropic_response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert isinstance(result, APIResponse)\n assert result.content == '{\"verdict\": \"PASS\"}'\n assert result.model == \"claude-sonnet-4-6\"\n assert result.input_tokens == 100\n assert result.output_tokens == 50\n assert result.stop_reason == \"end_turn\"\n\n def test_rate_limit_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.RateLimitError(\n message=\"rate limited\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"Rate limit exceeded\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_internal_server_error_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.InternalServerError(\n message=\"overloaded\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"server error\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_auth_error_raises_immediately(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-bad\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.side_effect = anthropic.AuthenticationError(\n message=\"invalid key\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"Invalid ANTHROPIC_API_KEY\"):\n client.review(\"system\", \"user\")\n\n # No retries for auth errors\n assert mock_cls.return_value.messages.create.call_count == 1\n\n def test_api_error_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.APIError(\n message=\"something went wrong\", request=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"failed after\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_rate_limit_succeeds_on_retry(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = [\n anthropic.RateLimitError(message=\"rate limited\", response=MagicMock(), body={}),\n mock_anthropic_response,\n ]\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.content == '{\"verdict\": \"PASS\"}'\n assert mock_cls.return_value.messages.create.call_count == 2\n\n def test_multiple_content_blocks_concatenated(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n block1 = MagicMock()\n block1.text = \"hello \"\n block2 = MagicMock()\n block2.text = \"world\"\n response = MagicMock()\n response.content = [block1, block2]\n response.model = \"claude-sonnet-4-6\"\n response.usage.input_tokens = 10\n response.usage.output_tokens = 5\n response.stop_reason = \"end_turn\"\n\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.content == \"hello world\"\n\n def test_stop_reason_none_becomes_unknown(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n mock_anthropic_response.stop_reason = None\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = mock_anthropic_response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.stop_reason == \"unknown\"\n\n\nclass TestBedrockConverseReview:\n \"\"\"Tests for the boto3 converse path used by non-Claude Bedrock models.\"\"\"\n\n @pytest.fixture\n def bedrock_config(self):\n return TfrevConfig(provider=\"aws-bedrock\", model=\"deepseek.deepseek-r1-v1:0\")\n\n @pytest.fixture\n def mock_converse_response(self):\n return {\n \"output\": {\"message\": {\"content\": [{\"text\": '{\"verdict\": \"PASS\"}'}]}},\n \"usage\": {\"inputTokens\": 200, \"outputTokens\": 80},\n \"stopReason\": \"end_turn\",\n }\n\n def _make_client(self, bedrock_config, mock_boto3_client):\n mock_boto3 = MagicMock()\n mock_boto3.client.return_value = mock_boto3_client\n with patch.dict(\"sys.modules\", {\"boto3\": mock_boto3}):\n return ReviewClient(bedrock_config)\n\n def test_successful_converse_call(self, bedrock_config, mock_converse_response):\n mock_boto3_client = MagicMock()\n mock_boto3_client.converse.return_value = mock_converse_response\n client = self._make_client(bedrock_config, mock_boto3_client)\n\n result = client.review(\"system\", \"user\")\n\n assert isinstance(result, APIResponse)\n assert result.content == '{\"verdict\": \"PASS\"}'\n assert result.model == \"deepseek.deepseek-r1-v1:0\"\n assert result.input_tokens == 200\n assert result.output_tokens == 80\n assert result.stop_reason == \"end_turn\"\n\n def test_converse_passes_correct_params(self, bedrock_config, mock_converse_response):\n mock_boto3_client = MagicMock()\n mock_boto3_client.converse.return_value = mock_converse_response\n client = self._make_client(bedrock_config, mock_boto3_client)\n\n client.review(\"my system prompt\", \"my user prompt\")\n\n mock_boto3_client.converse.assert_called_once_with(\n modelId=\"deepseek.deepseek-r1-v1:0\",\n system=[{\"text\": \"my system prompt\"}],\n messages=[{\"role\": \"user\", \"content\": [{\"text\": \"my user prompt\"}]}],\n inferenceConfig={\"maxTokens\": bedrock_config.max_tokens},\n )\n\n def test_converse_stop_reason_none_becomes_unknown(self, bedrock_config, mock_converse_response):\n mock_converse_response[\"stopReason\"] = None\n mock_boto3_client = MagicMock()\n mock_boto3_client.converse.return_value = mock_converse_response\n client = self._make_client(bedrock_config, mock_boto3_client)\n\n result = client.review(\"system\", \"user\")\n\n assert result.stop_reason == \"unknown\"\n\n def test_converse_multiple_content_blocks_concatenated(self, bedrock_config):\n mock_boto3_client = MagicMock()\n mock_boto3_client.converse.return_value = {\n \"output\": {\"message\": {\"content\": [{\"text\": \"hello \"}, {\"text\": \"world\"}]}},\n \"usage\": {\"inputTokens\": 10, \"outputTokens\": 5},\n \"stopReason\": \"end_turn\",\n }\n client = self._make_client(bedrock_config, mock_boto3_client)\n\n result = client.review(\"system\", \"user\")\n\n assert result.content == \"hello world\"\n"}, "727cbd9061bf3fa50c860eb2f97f07c2ba9887b2": {"t": 1785770725.9670205, "n": 6629, "h": "326a8469cfe7b429", "c": "\"\"\"Tests for tfrev.client.\"\"\"\n\nfrom __future__ import annotations\n\nfrom unittest.mock import MagicMock, patch\n\nimport anthropic\nimport pytest\n\nfrom tfrev.client import APIResponse, ReviewClient\nfrom tfrev.config import TfrevConfig\n\n\n@pytest.fixture\ndef config():\n return TfrevConfig()\n\n\n@pytest.fixture\ndef mock_anthropic_response():\n block = MagicMock()\n block.text = '{\"verdict\": \"PASS\"}'\n response = MagicMock()\n response.content = [block]\n response.model = \"claude-sonnet-4-6\"\n response.usage.input_tokens = 100\n response.usage.output_tokens = 50\n response.stop_reason = \"end_turn\"\n return response\n\n\nclass TestReviewClientInit:\n def test_missing_api_key_raises(self, config, monkeypatch):\n monkeypatch.delenv(\"ANTHROPIC_API_KEY\", raising=False)\n with pytest.raises(RuntimeError, match=\"ANTHROPIC_API_KEY\"):\n ReviewClient(config)\n\n def test_initializes_with_api_key(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n client = ReviewClient(config)\n assert client._client.api_key == \"sk-ant-test\"\n\n\nclass TestReviewClientReview:\n def test_successful_call(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = mock_anthropic_response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert isinstance(result, APIResponse)\n assert result.content == '{\"verdict\": \"PASS\"}'\n assert result.model == \"claude-sonnet-4-6\"\n assert result.input_tokens == 100\n assert result.output_tokens == 50\n assert result.stop_reason == \"end_turn\"\n\n def test_rate_limit_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.RateLimitError(\n message=\"rate limited\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"Rate limit exceeded\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_internal_server_error_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.InternalServerError(\n message=\"overloaded\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"server error\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_auth_error_raises_immediately(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-bad\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.side_effect = anthropic.AuthenticationError(\n message=\"invalid key\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"Invalid ANTHROPIC_API_KEY\"):\n client.review(\"system\", \"user\")\n\n # No retries for auth errors\n assert mock_cls.return_value.messages.create.call_count == 1\n\n def test_api_error_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.APIError(\n message=\"something went wrong\", request=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"failed after\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_rate_limit_succeeds_on_retry(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = [\n anthropic.RateLimitError(message=\"rate limited\", response=MagicMock(), body={}),\n mock_anthropic_response,\n ]\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.content == '{\"verdict\": \"PASS\"}'\n assert mock_cls.return_value.messages.create.call_count == 2\n\n def test_multiple_content_blocks_concatenated(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n block1 = MagicMock()\n block1.text = \"hello \"\n block2 = MagicMock()\n block2.text = \"world\"\n response = MagicMock()\n response.content = [block1, block2]\n response.model = \"claude-sonnet-4-6\"\n response.usage.input_tokens = 10\n response.usage.output_tokens = 5\n response.stop_reason = \"end_turn\"\n\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.content == \"hello world\"\n\n def test_stop_reason_none_becomes_unknown(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n mock_anthropic_response.stop_reason = None\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = mock_anthropic_response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.stop_reason == \"unknown\"\n"}, "9a222e3fa5a2772df26afcb8bc2534c4eded8fad": {"t": 1785770728.9308805, "n": 6637, "h": "df7021aadcd86e84", "c": "\"\"\"Tests for tfrev.client.\"\"\"\n\nfrom __future__ import annotations\n\nfrom unittest.mock import MagicMock, patch\n\nimport anthropic\nimport pytest\n\nfrom tfrev.client import APIResponse, ReviewClient\nfrom tfrev.config import TfrevConfig\n\n\n@pytest.fixture\ndef config():\n return TfrevConfig()\n\n\n@pytest.fixture\ndef mock_anthropic_response():\n block = MagicMock()\n block.text = '{\"verdict\": \"PASS\"}'\n response = MagicMock()\n response.content = [block]\n response.model = \"claude-sonnet-4-6\"\n response.usage.input_tokens = 100\n response.usage.output_tokens = 50\n response.stop_reason = \"end_turn\"\n return response\n\n\nclass TestReviewClientInit:\n def test_missing_api_key_raises(self, config, monkeypatch):\n monkeypatch.delenv(\"ANTHROPIC_API_KEY\", raising=False)\n with pytest.raises(RuntimeError, match=\"ANTHROPIC_API_KEY\"):\n ReviewClient(config)\n\n def test_initializes_with_api_key(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n client = ReviewClient(config)\n assert isinstance(client._client, anthropic.Anthropic)\n\n\nclass TestReviewClientReview:\n def test_successful_call(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = mock_anthropic_response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert isinstance(result, APIResponse)\n assert result.content == '{\"verdict\": \"PASS\"}'\n assert result.model == \"claude-sonnet-4-6\"\n assert result.input_tokens == 100\n assert result.output_tokens == 50\n assert result.stop_reason == \"end_turn\"\n\n def test_rate_limit_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.RateLimitError(\n message=\"rate limited\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"Rate limit exceeded\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_internal_server_error_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.InternalServerError(\n message=\"overloaded\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"server error\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_auth_error_raises_immediately(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-bad\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.side_effect = anthropic.AuthenticationError(\n message=\"invalid key\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"Invalid ANTHROPIC_API_KEY\"):\n client.review(\"system\", \"user\")\n\n # No retries for auth errors\n assert mock_cls.return_value.messages.create.call_count == 1\n\n def test_api_error_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.APIError(\n message=\"something went wrong\", request=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"failed after\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_rate_limit_succeeds_on_retry(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = [\n anthropic.RateLimitError(message=\"rate limited\", response=MagicMock(), body={}),\n mock_anthropic_response,\n ]\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.content == '{\"verdict\": \"PASS\"}'\n assert mock_cls.return_value.messages.create.call_count == 2\n\n def test_multiple_content_blocks_concatenated(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n block1 = MagicMock()\n block1.text = \"hello \"\n block2 = MagicMock()\n block2.text = \"world\"\n response = MagicMock()\n response.content = [block1, block2]\n response.model = \"claude-sonnet-4-6\"\n response.usage.input_tokens = 10\n response.usage.output_tokens = 5\n response.stop_reason = \"end_turn\"\n\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.content == \"hello world\"\n\n def test_stop_reason_none_becomes_unknown(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n mock_anthropic_response.stop_reason = None\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = mock_anthropic_response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.stop_reason == \"unknown\"\n"}, "aaf0299ae74321567369af0f899e8cb80d25a098": {"t": 1785770730.39676, "n": 6622, "h": "cda98cc7c0fbddcb", "c": "\"\"\"Tests for tfrev.client.\"\"\"\n\nfrom __future__ import annotations\n\nfrom unittest.mock import MagicMock, patch\n\nimport anthropic\nimport pytest\n\nfrom tfrev.client import APIResponse, ReviewClient\nfrom tfrev.config import TfrevConfig\n\n\n@pytest.fixture\ndef config():\n return TfrevConfig()\n\n\n@pytest.fixture\ndef mock_anthropic_response():\n block = MagicMock()\n block.text = '{\"verdict\": \"PASS\"}'\n response = MagicMock()\n response.content = [block]\n response.model = \"claude-sonnet-4-6\"\n response.usage.input_tokens = 100\n response.usage.output_tokens = 50\n response.stop_reason = \"end_turn\"\n return response\n\n\nclass TestReviewClientInit:\n def test_missing_api_key_raises(self, config, monkeypatch):\n monkeypatch.delenv(\"ANTHROPIC_API_KEY\", raising=False)\n with pytest.raises(RuntimeError, match=\"ANTHROPIC_API_KEY\"):\n ReviewClient(config)\n\n def test_initializes_with_api_key(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n client = ReviewClient(config)\n assert client._api_key == \"sk-ant-test\"\n\n\nclass TestReviewClientReview:\n def test_successful_call(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = mock_anthropic_response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert isinstance(result, APIResponse)\n assert result.content == '{\"verdict\": \"PASS\"}'\n assert result.model == \"claude-sonnet-4-6\"\n assert result.input_tokens == 100\n assert result.output_tokens == 50\n assert result.stop_reason == \"end_turn\"\n\n def test_rate_limit_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.RateLimitError(\n message=\"rate limited\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"Rate limit exceeded\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_internal_server_error_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.InternalServerError(\n message=\"overloaded\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"server error\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_auth_error_raises_immediately(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-bad\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.side_effect = anthropic.AuthenticationError(\n message=\"invalid key\", response=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"Invalid ANTHROPIC_API_KEY\"):\n client.review(\"system\", \"user\")\n\n # No retries for auth errors\n assert mock_cls.return_value.messages.create.call_count == 1\n\n def test_api_error_retries_then_raises(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = anthropic.APIError(\n message=\"something went wrong\", request=MagicMock(), body={}\n )\n client = ReviewClient(config)\n with pytest.raises(RuntimeError, match=\"failed after\"):\n client.review(\"system\", \"user\")\n\n assert mock_cls.return_value.messages.create.call_count == 3\n\n def test_rate_limit_succeeds_on_retry(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n with patch(\"tfrev.client.time.sleep\"):\n mock_cls.return_value.messages.create.side_effect = [\n anthropic.RateLimitError(message=\"rate limited\", response=MagicMock(), body={}),\n mock_anthropic_response,\n ]\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.content == '{\"verdict\": \"PASS\"}'\n assert mock_cls.return_value.messages.create.call_count == 2\n\n def test_multiple_content_blocks_concatenated(self, config, monkeypatch):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n block1 = MagicMock()\n block1.text = \"hello \"\n block2 = MagicMock()\n block2.text = \"world\"\n response = MagicMock()\n response.content = [block1, block2]\n response.model = \"claude-sonnet-4-6\"\n response.usage.input_tokens = 10\n response.usage.output_tokens = 5\n response.stop_reason = \"end_turn\"\n\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.content == \"hello world\"\n\n def test_stop_reason_none_becomes_unknown(self, config, monkeypatch, mock_anthropic_response):\n monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-test\")\n mock_anthropic_response.stop_reason = None\n with patch(\"tfrev.client.anthropic.Anthropic\") as mock_cls:\n mock_cls.return_value.messages.create.return_value = mock_anthropic_response\n client = ReviewClient(config)\n result = client.review(\"system\", \"user\")\n\n assert result.stop_reason == \"unknown\"\n"}}}}