package runner import ( "strings" "testing" ) func TestBuildCacheAffinityKeyUsesExplicitKeyFirst(t *testing.T) { body := map[string]any{ "cacheAffinityKey": "session-a", "sessionId": "session-b", "messages": []any{map[string]any{"role": "user", "content": "hello"}}, } got := buildCacheAffinityKey("chat.completions", "text_generate", body) if !strings.HasPrefix(got, "explicit:") { t.Fatalf("expected explicit cache affinity key, got %q", got) } } func TestBuildCacheAffinityKeyUsesSessionContinuity(t *testing.T) { got := buildCacheAffinityKey("chat.completions", "text_generate", map[string]any{ "session_id": "conversation-1", }) if !strings.HasPrefix(got, "session_id:") { t.Fatalf("expected session cache affinity key, got %q", got) } } func TestBuildCacheAffinityKeyFallsBackToPromptHash(t *testing.T) { body := map[string]any{ "messages": []any{ map[string]any{"role": "system", "content": "stable"}, map[string]any{"role": "user", "content": "hello"}, }, "tools": []any{map[string]any{"type": "function", "function": map[string]any{"name": "lookup"}}}, } first := buildCacheAffinityKey("chat.completions", "text_generate", body) second := buildCacheAffinityKey("chat.completions", "text_generate", body) if first == "" || first != second || !strings.HasPrefix(first, "prompt_lcp_v2:") { t.Fatalf("expected stable prompt LCP cache affinity key, first=%q second=%q", first, second) } } func TestBuildCacheAffinityKeysUsesMessagePrefixLCPKeys(t *testing.T) { first := buildCacheAffinityKeys("chat.completions", "text_generate", map[string]any{ "messageHashes": []any{"system-hash", "user-hash"}, "tools": []any{map[string]any{"type": "function", "function": map[string]any{"name": "lookup"}}}, }) next := buildCacheAffinityKeys("chat.completions", "text_generate", map[string]any{ "messageHashes": []any{"system-hash", "user-hash", "assistant-hash", "tool-hash"}, "tools": []any{map[string]any{"type": "function", "function": map[string]any{"name": "lookup"}}}, }) if !strings.HasPrefix(first.Primary, "prompt_lcp_v2:") { t.Fatalf("expected LCP prompt key, got %+v", first) } if len(first.Record) != 1 || len(next.Record) != 3 || len(next.Lookup) != 6 { t.Fatalf("expected per-message prefix keys, first=%+v next=%+v", first, next) } found := false for _, key := range next.Lookup { if key == first.Primary { found = true break } } if !found { t.Fatalf("next lookup keys should include first request prefix key, first=%+v next=%+v", first, next) } } func TestBuildCacheAffinityKeysRequiresTwoStableMessages(t *testing.T) { systemOnly := buildCacheAffinityKeys("chat.completions", "text_generate", map[string]any{ "messages": []any{ map[string]any{"role": "system", "content": "shared system prompt"}, }, }) firstConversation := buildCacheAffinityKeys("chat.completions", "text_generate", map[string]any{ "messages": []any{ map[string]any{"role": "system", "content": "shared system prompt"}, map[string]any{"role": "user", "content": "conversation A"}, }, }) secondConversation := buildCacheAffinityKeys("chat.completions", "text_generate", map[string]any{ "messages": []any{ map[string]any{"role": "system", "content": "shared system prompt"}, map[string]any{"role": "user", "content": "conversation B"}, }, }) if systemOnly.Primary != "" || len(systemOnly.Lookup) != 0 || len(systemOnly.Record) != 0 { t.Fatalf("shared system prompt alone must not create affinity, got %+v", systemOnly) } if firstConversation.Primary == "" || secondConversation.Primary == "" { t.Fatalf("two stable messages should create affinity, first=%+v second=%+v", firstConversation, secondConversation) } for _, firstKey := range firstConversation.Lookup { for _, secondKey := range secondConversation.Lookup { if firstKey == secondKey { t.Fatalf("different conversations sharing only a system prompt must not intersect: key=%s", firstKey) } } } } func TestBuildCacheAffinityKeysNormalizesResponsesToolHistory(t *testing.T) { tools := []any{map[string]any{ "type": "function", "function": map[string]any{ "name": "lookup_weather", }, }} chat := buildCacheAffinityKeys("chat.completions", "text_generate", map[string]any{ "tools": tools, "messages": []any{ map[string]any{"role": "system", "content": "Use tools when helpful."}, map[string]any{"role": "user", "content": "Weather in Hangzhou?"}, }, }) responses := buildCacheAffinityKeys("responses", "text_generate", map[string]any{ "tools": tools, "instructions": "Use tools when helpful.", "input": "Weather in Hangzhou?", }) continued := buildCacheAffinityKeys("responses", "text_generate", map[string]any{ "tools": tools, "instructions": "Use tools when helpful.", "input": []any{ map[string]any{"role": "user", "content": "Weather in Hangzhou?"}, map[string]any{"type": "function_call", "call_id": "call_weather", "name": "lookup_weather", "arguments": `{"city":"Hangzhou"}`}, map[string]any{"type": "function_call_output", "call_id": "call_weather", "output": `{"temperature":31}`}, }, }) if chat.Primary == "" || chat.Primary != responses.Primary { t.Fatalf("chat and responses should share the V2 protocol-independent prefix, chat=%+v responses=%+v", chat, responses) } found := false for _, key := range continued.Lookup { if key == responses.Primary { found = true break } } if !found { t.Fatalf("responses tool continuation should retain its stable prefix, initial=%+v continued=%+v", responses, continued) } }