test_prompt_caching.py
1 """Tests for agent/prompt_caching.py — Anthropic cache control injection.""" 2 3 import copy 4 import pytest 5 6 from agent.prompt_caching import ( 7 _apply_cache_marker, 8 apply_anthropic_cache_control, 9 ) 10 11 12 MARKER = {"type": "ephemeral"} 13 14 15 class TestApplyCacheMarker: 16 def test_tool_message_gets_top_level_marker_on_native_anthropic(self): 17 """Native Anthropic path: cache_control injected top-level (adapter moves it inside tool_result).""" 18 msg = {"role": "tool", "content": "result"} 19 _apply_cache_marker(msg, MARKER, native_anthropic=True) 20 assert msg["cache_control"] == MARKER 21 22 def test_tool_message_skips_marker_on_openrouter(self): 23 """OpenRouter path: top-level cache_control on role:tool is invalid and causes silent hang.""" 24 msg = {"role": "tool", "content": "result"} 25 _apply_cache_marker(msg, MARKER, native_anthropic=False) 26 assert "cache_control" not in msg 27 28 def test_none_content_gets_top_level_marker(self): 29 msg = {"role": "assistant", "content": None} 30 _apply_cache_marker(msg, MARKER) 31 assert msg["cache_control"] == MARKER 32 33 def test_empty_string_content_gets_top_level_marker(self): 34 """Empty text blocks cannot have cache_control (Anthropic rejects them).""" 35 msg = {"role": "assistant", "content": ""} 36 _apply_cache_marker(msg, MARKER) 37 assert msg["cache_control"] == MARKER 38 # Must NOT wrap into [{"type": "text", "text": "", "cache_control": ...}] 39 assert msg["content"] == "" 40 41 def test_string_content_wrapped_in_list(self): 42 msg = {"role": "user", "content": "Hello"} 43 _apply_cache_marker(msg, MARKER) 44 assert isinstance(msg["content"], list) 45 assert len(msg["content"]) == 1 46 assert msg["content"][0]["type"] == "text" 47 assert msg["content"][0]["text"] == "Hello" 48 assert msg["content"][0]["cache_control"] == MARKER 49 50 def test_list_content_last_item_gets_marker(self): 51 msg = { 52 "role": "user", 53 "content": [ 54 {"type": "text", "text": "First"}, 55 {"type": "text", "text": "Second"}, 56 ], 57 } 58 _apply_cache_marker(msg, MARKER) 59 assert "cache_control" not in msg["content"][0] 60 assert msg["content"][1]["cache_control"] == MARKER 61 62 def test_empty_list_content_no_crash(self): 63 msg = {"role": "user", "content": []} 64 # Should not crash on empty list 65 _apply_cache_marker(msg, MARKER) 66 67 68 class TestApplyAnthropicCacheControl: 69 def test_empty_messages(self): 70 result = apply_anthropic_cache_control([]) 71 assert result == [] 72 73 def test_returns_deep_copy(self): 74 msgs = [{"role": "user", "content": "Hello"}] 75 result = apply_anthropic_cache_control(msgs) 76 assert result is not msgs 77 assert result[0] is not msgs[0] 78 # Original should be unmodified 79 assert "cache_control" not in msgs[0].get("content", "") 80 81 def test_system_message_gets_marker(self): 82 msgs = [ 83 {"role": "system", "content": "You are helpful"}, 84 {"role": "user", "content": "Hi"}, 85 ] 86 result = apply_anthropic_cache_control(msgs) 87 # System message should have cache_control 88 sys_content = result[0]["content"] 89 assert isinstance(sys_content, list) 90 assert sys_content[0]["cache_control"]["type"] == "ephemeral" 91 92 def test_last_3_non_system_get_markers(self): 93 msgs = [ 94 {"role": "system", "content": "System"}, 95 {"role": "user", "content": "msg1"}, 96 {"role": "assistant", "content": "msg2"}, 97 {"role": "user", "content": "msg3"}, 98 {"role": "assistant", "content": "msg4"}, 99 ] 100 result = apply_anthropic_cache_control(msgs) 101 # System (index 0) + last 3 non-system (indices 2, 3, 4) = 4 breakpoints 102 # Index 1 (msg1) should NOT have marker 103 content_1 = result[1]["content"] 104 if isinstance(content_1, str): 105 assert True # No marker applied (still a string) 106 else: 107 assert "cache_control" not in content_1[0] 108 109 def test_no_system_message(self): 110 msgs = [ 111 {"role": "user", "content": "Hello"}, 112 {"role": "assistant", "content": "Hi"}, 113 ] 114 result = apply_anthropic_cache_control(msgs) 115 # Both should get markers (4 slots available, only 2 messages) 116 assert len(result) == 2 117 118 def test_1h_ttl(self): 119 msgs = [{"role": "system", "content": "System prompt"}] 120 result = apply_anthropic_cache_control(msgs, cache_ttl="1h") 121 sys_content = result[0]["content"] 122 assert isinstance(sys_content, list) 123 assert sys_content[0]["cache_control"]["ttl"] == "1h" 124 125 def test_max_4_breakpoints(self): 126 msgs = [ 127 {"role": "system", "content": "System"}, 128 ] + [ 129 {"role": "user" if i % 2 == 0 else "assistant", "content": f"msg{i}"} 130 for i in range(10) 131 ] 132 result = apply_anthropic_cache_control(msgs) 133 # Count how many messages have cache_control 134 count = 0 135 for msg in result: 136 content = msg.get("content") 137 if isinstance(content, list): 138 for item in content: 139 if isinstance(item, dict) and "cache_control" in item: 140 count += 1 141 elif "cache_control" in msg: 142 count += 1 143 assert count <= 4