"""Live integration tests for the Claude SDK step. These tests make REAL API calls to the Anthropic API. They require: - ``ANTHROPIC_API_KEY`` environment variable to be set - Network access to ``api.anthropic.com`` Run with:: ANTHROPIC_API_KEY=sk-... uv run pytest tests/test_claude_sdk_live.py -v -m integration """ from __future__ import annotations import os import pytest from ace.core.context import ACEStepContext, SkillbookView from ace.core.skillbook import Skillbook from ace.integrations.claude_sdk import ( ClaudeSDKExecuteStep, ClaudeSDKResult, ClaudeSDKToTrace, ) pytestmark = [ pytest.mark.integration, pytest.mark.requires_api, pytest.mark.skipif( not os.environ.get("ANTHROPIC_API_KEY"), reason="ANTHROPIC_API_KEY not set", ), ] pytest.importorskip("anthropic") MODEL = "claude-sonnet-4-20250514" # ------------------------------------------------------------------ # # 1. Basic text generation # ------------------------------------------------------------------ # class TestBasicGeneration: def test_simple_question(self): """Verify a basic question returns a successful result with tokens.""" step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=100) ctx = ACEStepContext( sample="What is 2+2? Answer with just the number.", skillbook=None ) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] assert r.success is True assert r.error is None assert "4" in r.output assert r.model == MODEL assert r.stop_reason in ("end_turn", "max_tokens") assert r.input_tokens > 0 assert r.output_tokens > 0 assert r.total_tokens == r.input_tokens + r.output_tokens assert r.latency_seconds > 0 assert r.raw_response is not None def test_longer_response(self): """Verify the step handles multi-sentence responses.""" step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=300) ctx = ACEStepContext( sample="Explain in 2-3 sentences why the sky is blue.", skillbook=None, ) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] assert r.success is True assert len(r.output) > 50 assert r.output_tokens > 10 # ------------------------------------------------------------------ # # 2. System prompt # ------------------------------------------------------------------ # class TestSystemPrompt: def test_system_prompt_affects_output(self): """A system prompt instructing a specific format should be followed.""" step = ClaudeSDKExecuteStep( model=MODEL, max_tokens=50, system_prompt="You are a calculator. Only output numbers, nothing else.", ) ctx = ACEStepContext(sample="What is 15 * 3?", skillbook=None) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] assert r.success is True assert "45" in r.output # ------------------------------------------------------------------ # # 3. Skillbook injection # ------------------------------------------------------------------ # class TestSkillbookInjection: def test_skillbook_injected_into_system(self): """When a skillbook has skills, they're injected and the model can reference them.""" sb = Skillbook() sb.add_skill("math", "Always show step-by-step work for math problems") sb.add_skill("format", "End every answer with 'QED'") step = ClaudeSDKExecuteStep( model=MODEL, max_tokens=200, inject_skillbook=True, ) ctx = ACEStepContext( sample="What is 7 * 8? Follow the strategies you've been given.", skillbook=SkillbookView(sb), ) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] assert r.success is True assert "56" in r.output def test_skillbook_injection_disabled(self): """With inject_skillbook=False, skills are NOT sent to the model.""" sb = Skillbook() sb.add_skill("format", "End every answer with the word BANANA") step = ClaudeSDKExecuteStep( model=MODEL, max_tokens=50, inject_skillbook=False, ) ctx = ACEStepContext( sample="What is 1+1? Just answer the number.", skillbook=SkillbookView(sb), ) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] assert r.success is True assert "BANANA" not in r.output # ------------------------------------------------------------------ # # 4. Tool use # ------------------------------------------------------------------ # class TestToolUse: def test_tool_call_returned(self): """When given a tool, the model should call it and we capture the call.""" tools = [ { "name": "get_weather", "description": "Get the current weather for a city.", "input_schema": { "type": "object", "properties": { "city": {"type": "string", "description": "City name"}, }, "required": ["city"], }, } ] step = ClaudeSDKExecuteStep( model=MODEL, max_tokens=200, tools=tools, ) ctx = ACEStepContext( sample="What's the weather in Paris right now?", skillbook=None, ) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] assert r.success is True assert r.stop_reason in ("tool_use", "end_turn") if r.stop_reason == "tool_use": assert len(r.tool_calls) >= 1 tc = r.tool_calls[0] assert tc.name == "get_weather" assert "city" in tc.input assert tc.id.startswith("toolu_") # ------------------------------------------------------------------ # # 5. Temperature # ------------------------------------------------------------------ # class TestTemperature: def test_zero_temperature_deterministic(self): """Temperature 0 should produce near-identical outputs.""" step = ClaudeSDKExecuteStep( model=MODEL, max_tokens=20, temperature=0.0, ) ctx = ACEStepContext( sample="Complete this: 1, 2, 3, 4, ", skillbook=None, ) r1: ClaudeSDKResult = step(ctx).trace # type: ignore[assignment] r2: ClaudeSDKResult = step(ctx).trace # type: ignore[assignment] assert r1.success and r2.success # With temp=0 outputs should be identical or very similar assert r1.output[:10] == r2.output[:10] # ------------------------------------------------------------------ # # 6. Error handling # ------------------------------------------------------------------ # class TestErrorHandling: def test_invalid_model_returns_error(self): """An invalid model name should return a failed result, not crash.""" step = ClaudeSDKExecuteStep( model="claude-nonexistent-99", max_tokens=10, ) ctx = ACEStepContext(sample="hello", skillbook=None) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] assert r.success is False assert r.error is not None assert r.latency_seconds >= 0 def test_max_tokens_respected(self): """A very low max_tokens should truncate the response.""" step = ClaudeSDKExecuteStep( model=MODEL, max_tokens=5, ) ctx = ACEStepContext( sample="Write a 500-word essay about the history of computing.", skillbook=None, ) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] assert r.success is True assert r.stop_reason == "max_tokens" assert r.output_tokens <= 10 # small buffer around max_tokens # ------------------------------------------------------------------ # # 7. ToTrace conversion with real data # ------------------------------------------------------------------ # class TestToTraceWithRealData: def test_full_pipeline_execute_then_convert(self): """Run execute step then ToTrace step — end-to-end pipeline flow.""" execute = ClaudeSDKExecuteStep(model=MODEL, max_tokens=100) to_trace = ClaudeSDKToTrace() ctx = ACEStepContext(sample="What is the capital of France?", skillbook=None) # Execute ctx = execute(ctx) r: ClaudeSDKResult = ctx.trace # type: ignore[assignment] assert r.success is True assert "Paris" in r.output # Convert ctx = to_trace(ctx) trace = ctx.trace assert isinstance(trace, dict) assert trace["question"] == "What is the capital of France?" assert "Paris" in trace["answer"] assert "succeeded" in trace["reasoning"] assert str(r.input_tokens) in trace["reasoning"] assert "succeeded" in trace["feedback"] assert trace["ground_truth"] is None # ------------------------------------------------------------------ # # 8. ACESample input (structured sample) # ------------------------------------------------------------------ # class TestACESampleInput: def test_structured_sample(self): """Step should extract question+context from a structured sample.""" from dataclasses import dataclass @dataclass class Sample: question: str = "What color is a ripe banana?" context: str = "We are discussing fruits." ground_truth: str = "yellow" metadata: dict = None # type: ignore[assignment] def __post_init__(self): self.metadata = self.metadata or {} step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=30) ctx = ACEStepContext(sample=Sample(), skillbook=None) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] assert r.success is True assert r.task # task was extracted assert "banana" in r.task.lower() or "color" in r.task.lower() # ------------------------------------------------------------------ # # 9. Observability data quality # ------------------------------------------------------------------ # class TestObservabilityData: def test_token_counts_realistic(self): """Token counts should be reasonable for a simple prompt.""" step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=50) ctx = ACEStepContext(sample="Say 'hello world'", skillbook=None) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] assert r.success is True # Input: system overhead + short prompt, should be < 100 assert 1 < r.input_tokens < 100 # Output: short response assert 1 < r.output_tokens <= 50 # Total is sum assert r.total_tokens == r.input_tokens + r.output_tokens # Latency > 0 and reasonable (< 30s for a short request) assert 0 < r.latency_seconds < 30 def test_model_field_matches_request(self): """The model field in the result should match what we requested.""" step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=10) ctx = ACEStepContext(sample="hi", skillbook=None) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] assert r.model == MODEL # ------------------------------------------------------------------ # # 10. Pydantic validation on real results # ------------------------------------------------------------------ # class TestPydanticValidation: def test_result_is_pydantic_model(self): """ClaudeSDKResult should be a Pydantic BaseModel.""" from pydantic import BaseModel step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=10) ctx = ACEStepContext(sample="hi", skillbook=None) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] assert isinstance(r, BaseModel) def test_model_dump(self): """Real result should serialise cleanly, with raw_response excluded.""" step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=20) ctx = ACEStepContext(sample="Say hello", skillbook=None) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] d = r.model_dump() assert isinstance(d, dict) assert d["success"] is True assert d["input_tokens"] > 0 assert d["output_tokens"] > 0 assert d["total_tokens"] == d["input_tokens"] + d["output_tokens"] assert "raw_response" not in d def test_model_dump_json(self): """Result should serialise to JSON string.""" import json step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=10) ctx = ACEStepContext(sample="hi", skillbook=None) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] j = r.model_dump_json() parsed = json.loads(j) assert parsed["task"] == "hi" assert parsed["success"] is True def test_total_tokens_auto_computed(self): """total_tokens should equal input + output on a real response.""" step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=20) ctx = ACEStepContext(sample="Count to 3", skillbook=None) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] assert r.total_tokens == r.input_tokens + r.output_tokens assert r.total_tokens > 0 def test_tool_calls_are_pydantic_models(self): """Tool calls in the result should be validated ToolCall models.""" from ace.integrations.claude_sdk import ToolCall tools = [ { "name": "lookup", "description": "Look up a value.", "input_schema": { "type": "object", "properties": {"key": {"type": "string"}}, "required": ["key"], }, } ] step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=100, tools=tools) ctx = ACEStepContext(sample="Look up the value of 'pi'", skillbook=None) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] assert r.success is True if r.tool_calls: tc = r.tool_calls[0] assert isinstance(tc, ToolCall) assert isinstance(tc.id, str) assert isinstance(tc.name, str) assert isinstance(tc.input, dict) # ------------------------------------------------------------------ # # 11. Logfire integration (real) # ------------------------------------------------------------------ # class TestLogfireIntegration: def test_logfire_configure_and_instrument(self): """Configure Logfire, create a step, make a call — verify spans are sent.""" from ace.observability import configure_logfire, is_configured configured = configure_logfire() if not configured: pytest.skip("Logfire not configured (missing LOGFIRE_TOKEN?)") assert is_configured() step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=20) ctx = ACEStepContext(sample="Say 'logfire test'", skillbook=None) result_ctx = step(ctx) r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] assert r.success is True assert r.output_tokens > 0