"""Tests for markitect.llm.claude_code.""" import subprocess from unittest import mock import pytest from markitect.llm.claude_code import ClaudeCodeAdapter from markitect.llm.exceptions import LLMSubprocessError, LLMTimeoutError from markitect.prompts.execution.models import RunConfig, LLMResponse class TestClaudeCodeAdapter: @mock.patch("markitect.llm.claude_code.subprocess.run") def test_execute_prompt_success(self, mock_run): mock_run.return_value = subprocess.CompletedProcess( args=["claude", "--print"], returncode=0, stdout="Generated output text", stderr="", ) adapter = ClaudeCodeAdapter() config = RunConfig() resp = adapter.execute_prompt("Write something", config) assert isinstance(resp, LLMResponse) assert resp.content == "Generated output text" assert resp.model == "claude-code-cli" assert resp.usage["prompt_tokens"] > 0 assert resp.usage["completion_tokens"] > 0 assert resp.metadata["provider"] == "claude-code" @mock.patch("markitect.llm.claude_code.subprocess.run") def test_prompt_passed_via_stdin(self, mock_run): mock_run.return_value = subprocess.CompletedProcess( args=["claude", "--print"], returncode=0, stdout="ok", stderr="", ) adapter = ClaudeCodeAdapter() config = RunConfig() adapter.execute_prompt("My big prompt", config) call_kwargs = mock_run.call_args[1] assert call_kwargs["input"] == "My big prompt" assert call_kwargs["text"] is True @mock.patch("markitect.llm.claude_code.subprocess.run") def test_model_flag_passed(self, mock_run): mock_run.return_value = subprocess.CompletedProcess( args=["claude", "--print", "--model", "opus"], returncode=0, stdout="ok", stderr="", ) adapter = ClaudeCodeAdapter(model="opus") config = RunConfig() adapter.execute_prompt("Test", config) cmd = mock_run.call_args[0][0] assert "--model" in cmd assert "opus" in cmd @mock.patch("markitect.llm.claude_code.subprocess.run") def test_nonzero_exit_raises(self, mock_run): mock_run.return_value = subprocess.CompletedProcess( args=["claude", "--print"], returncode=1, stdout="", stderr="Error: something went wrong", ) adapter = ClaudeCodeAdapter() config = RunConfig() with pytest.raises(LLMSubprocessError) as exc_info: adapter.execute_prompt("Test", config) assert exc_info.value.return_code == 1 assert "something went wrong" in exc_info.value.stderr @mock.patch("markitect.llm.claude_code.subprocess.run") def test_timeout_raises(self, mock_run): mock_run.side_effect = subprocess.TimeoutExpired(cmd="claude", timeout=300) adapter = ClaudeCodeAdapter() config = RunConfig(timeout_seconds=300) with pytest.raises(LLMTimeoutError): adapter.execute_prompt("Test", config) @mock.patch("markitect.llm.claude_code.subprocess.run") def test_validate_config_success(self, mock_run): mock_run.return_value = subprocess.CompletedProcess( args=["claude", "--version"], returncode=0, stdout="1.0.0", stderr="", ) adapter = ClaudeCodeAdapter() assert adapter.validate_config(RunConfig()) is True @mock.patch("markitect.llm.claude_code.subprocess.run") def test_validate_config_missing_cli(self, mock_run): mock_run.side_effect = FileNotFoundError("claude not found") adapter = ClaudeCodeAdapter() assert adapter.validate_config(RunConfig()) is False @mock.patch("markitect.llm.claude_code.subprocess.run") def test_token_estimation(self, mock_run): # 20 chars -> ~5 tokens, 40 chars -> ~10 tokens mock_run.return_value = subprocess.CompletedProcess( args=["claude", "--print"], returncode=0, stdout="a" * 40, stderr="", ) adapter = ClaudeCodeAdapter() config = RunConfig() resp = adapter.execute_prompt("b" * 20, config) assert resp.usage["prompt_tokens"] == 5 assert resp.usage["completion_tokens"] == 10