package common import ( "testing" ) func TestDeriveClaudeUserID_SameConversationIsStable(t *testing.T) { raw := []byte(`{"model":"claude-test","messages":[{"role":"user","content":"hello"}]}`) first := DeriveClaudeUserID(raw) second := DeriveClaudeUserID(raw) if first == "" { t.Fatal("expected non-empty user_id") } if first != second { t.Fatalf("same conversation produced different user_id: %q vs %q", first, second) } } func TestDeriveClaudeUserID_PreservesCallerSuppliedMetadataUserID(t *testing.T) { testCases := []struct { name string rawJSON string expected string }{ { name: "plain string", rawJSON: `{"model":"claude-test","metadata":{"user_id":"caller-123"},"messages":[{"role":"user","content":"hello"}]}`, expected: "caller-123", }, { name: "whitespace preserved", rawJSON: `{"model":"claude-test","metadata":{"user_id":" caller-spaces "},"messages":[{"role":"user","content":"hello"}]}`, expected: " caller-spaces ", }, { name: "special characters", rawJSON: `{"model":"claude-test","metadata":{"user_id":"foo\"bar\nbaz\\qux"},"messages":[{"role":"user","content":"hello"}]}`, expected: "foo\"bar\nbaz\\qux", }, { name: "claude code json string", rawJSON: `{"model":"claude-test","metadata":{"user_id":"{\"device_id\":\"dev-1\",\"session_id\":\"sess-1\"}"},"messages":[{"role":"user","content":"hello"}]}`, expected: `{"device_id":"dev-1","session_id":"sess-1"}`, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { if got := DeriveClaudeUserID([]byte(tc.rawJSON)); got != tc.expected { t.Fatalf("caller-supplied metadata.user_id not preserved, got %q want %q", got, tc.expected) } }) } } func TestDeriveClaudeUserID_PreservesOpenAIUserField(t *testing.T) { raw := []byte(`{"model":"claude-test","user":"openai-user-456","messages":[{"role":"user","content":"hello"}]}`) if got := DeriveClaudeUserID(raw); got != "openai-user-456" { t.Fatalf("caller-supplied user not preserved, got %q", got) } } func TestDeriveClaudeUserID_MetadataUserIDTakesPriorityOverUserField(t *testing.T) { raw := []byte(`{"model":"claude-test","metadata":{"user_id":"meta-user-1"},"user":"openai-user-2","messages":[{"role":"user","content":"hello"}]}`) if got := DeriveClaudeUserID(raw); got != "meta-user-1" { t.Fatalf("metadata.user_id should take priority over user field, got %q", got) } } func TestDeriveClaudeUserID_CaseInsensitiveUserRole(t *testing.T) { rawA := []byte(`{"model":"claude-test","messages":[{"role":"User","content":"message A"}]}`) rawB := []byte(`{"model":"claude-test","messages":[{"role":"USER","content":"message B"}]}`) idA := DeriveClaudeUserID(rawA) idB := DeriveClaudeUserID(rawB) if idA == "" || idB == "" || idA == "unknown" || idB == "unknown" { t.Fatalf("expected valid derived user_id for uppercase User role, got idA=%q idB=%q", idA, idB) } if idA == idB { t.Fatalf("different messages with User role produced same user_id: %q", idA) } } func TestDeriveClaudeUserID_IgnoresNonStringMetadataUserIDOrUser(t *testing.T) { raw := []byte(`{"model":"claude-test","metadata":{"user_id":12345},"user":true,"messages":[{"role":"user","content":"hello"}]}`) got := DeriveClaudeUserID(raw) if got == "" || got == "12345" || got == "true" { t.Fatalf("non-string user_id should be ignored and derived, got %q", got) } } func TestDeriveClaudeUserID_DifferentSessionsAreDifferent(t *testing.T) { a := []byte(`{"model":"claude-test","prompt_cache_key":"session-a","messages":[{"role":"user","content":"hello"}]}`) b := []byte(`{"model":"claude-test","prompt_cache_key":"session-b","messages":[{"role":"user","content":"hello"}]}`) idA := DeriveClaudeUserID(a) idB := DeriveClaudeUserID(b) if idA == idB { t.Fatalf("different prompt_cache_key produced same user_id: %q", idA) } } func TestDeriveClaudeUserID_SessionIDVariants(t *testing.T) { a := []byte(`{"model":"claude-test","session_id":"sess-a","messages":[{"role":"user","content":"hello"}]}`) b := []byte(`{"model":"claude-test","sessionId":"sess-b","messages":[{"role":"user","content":"hello"}]}`) idA := DeriveClaudeUserID(a) idB := DeriveClaudeUserID(b) if idA == "" || idB == "" { t.Fatal("expected non-empty user_id for session_id/sessionId") } if idA == idB { t.Fatalf("different session ids produced same user_id: %q", idA) } } func TestDeriveClaudeUserID_ConversationIDVariants(t *testing.T) { cObj := []byte(`{"model":"claude-test","conversation":{"id":"conv-1"},"messages":[{"role":"user","content":"hello"}]}`) cStr := []byte(`{"model":"claude-test","conversation":"conv-2","messages":[{"role":"user","content":"hello"}]}`) cFlat := []byte(`{"model":"claude-test","conversation_id":"conv-3","messages":[{"role":"user","content":"hello"}]}`) idObj := DeriveClaudeUserID(cObj) idStr := DeriveClaudeUserID(cStr) idFlat := DeriveClaudeUserID(cFlat) if idObj == "" || idStr == "" || idFlat == "" { t.Fatal("expected non-empty user_id for conversation variants") } if idObj == idStr || idObj == idFlat || idStr == idFlat { t.Fatalf("different conversation ids produced identical user_ids: obj=%q str=%q flat=%q", idObj, idStr, idFlat) } } func TestDeriveClaudeUserID_TurnGrowthKeepsSameUserID(t *testing.T) { first := []byte(`{"model":"claude-test","prompt_cache_key":"session-1","messages":[{"role":"user","content":"hello"}]}`) second := []byte(`{"model":"claude-test","prompt_cache_key":"session-1","messages":[{"role":"user","content":"hello"},{"role":"assistant","content":"hi"},{"role":"user","content":"follow up"}]}`) idFirst := DeriveClaudeUserID(first) idSecond := DeriveClaudeUserID(second) if idFirst != idSecond { t.Fatalf("conversation turn growth changed user_id: %q vs %q", idFirst, idSecond) } } func TestDeriveClaudeUserID_TurnGrowthWithoutSessionKeyKeepsSameUserID(t *testing.T) { first := []byte(`{"model":"claude-test","messages":[{"role":"user","content":"first prompt"}]}`) second := []byte(`{"model":"claude-test","messages":[{"role":"user","content":"first prompt"},{"role":"assistant","content":"hi"},{"role":"user","content":"second prompt"}]}`) idFirst := DeriveClaudeUserID(first) idSecond := DeriveClaudeUserID(second) if idFirst == "" || idFirst == "unknown" { t.Fatalf("expected valid derived user_id, got %q", idFirst) } if idFirst != idSecond { t.Fatalf("conversation turn growth without session key changed user_id: %q vs %q", idFirst, idSecond) } } func TestDeriveClaudeUserID_GeminiTurnGrowthWithoutSessionKeyKeepsSameUserID(t *testing.T) { first := []byte(`{"contents":[{"role":"user","parts":[{"text":"first gemini prompt"}]}]}`) second := []byte(`{"contents":[{"role":"user","parts":[{"text":"first gemini prompt"}]},{"role":"model","parts":[{"text":"answer"}]},{"role":"user","parts":[{"text":"second prompt"}]}]}`) idFirst := DeriveClaudeUserID(first) idSecond := DeriveClaudeUserID(second) if idFirst == "" || idFirst == "unknown" { t.Fatalf("expected valid derived user_id, got %q", idFirst) } if idFirst != idSecond { t.Fatalf("gemini turn growth without session key changed user_id: %q vs %q", idFirst, idSecond) } } func TestDeriveClaudeUserID_FirstMessageFallback(t *testing.T) { rawA := []byte(`{"model":"claude-test","messages":[{"role":"user","content":"message A"}]}`) rawB := []byte(`{"model":"claude-test","messages":[{"role":"user","content":"message B"}]}`) idA := DeriveClaudeUserID(rawA) idB := DeriveClaudeUserID(rawB) if idA == "" || idB == "" || idA == "unknown" || idB == "unknown" { t.Fatalf("expected valid derived user_id, got idA=%q idB=%q", idA, idB) } if idA == idB { t.Fatalf("different first messages produced same user_id: %q", idA) } } func TestDeriveClaudeUserID_ResponsesInputString(t *testing.T) { rawA := []byte(`{"model":"claude-test","input":"hello world A"}`) rawB := []byte(`{"model":"claude-test","input":"hello world B"}`) idA := DeriveClaudeUserID(rawA) idB := DeriveClaudeUserID(rawB) if idA == "" || idB == "" || idA == "unknown" || idB == "unknown" { t.Fatalf("expected valid derived user_id for input string, got idA=%q idB=%q", idA, idB) } if idA == idB { t.Fatalf("different input strings produced same user_id: %q", idA) } } func TestDeriveClaudeUserID_ResponsesInputArraySkipsSystemLevelItems(t *testing.T) { rawA := []byte(`{ "model": "claude-test", "input": [ {"type": "message", "role": "system", "content": "system prompt"}, {"type": "message", "role": "developer", "content": "dev prompt"}, {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "user message A"}]} ] }`) rawB := []byte(`{ "model": "claude-test", "input": [ {"type": "message", "role": "system", "content": "system prompt"}, {"type": "message", "role": "developer", "content": "dev prompt"}, {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "user message B"}]} ] }`) idA := DeriveClaudeUserID(rawA) idB := DeriveClaudeUserID(rawB) if idA == "" || idB == "" || idA == "unknown" || idB == "unknown" { t.Fatalf("expected valid derived user_id, got idA=%q idB=%q", idA, idB) } if idA == idB { t.Fatalf("different user messages with same system prompt produced identical user_id: %q", idA) } } func TestDeriveClaudeUserID_GeminiContentsDefaultRole(t *testing.T) { rawA := []byte(`{"contents":[{"parts":[{"text":"gemini message A"}]}]}`) rawB := []byte(`{"contents":[{"parts":[{"text":"gemini message B"}]}]}`) idA := DeriveClaudeUserID(rawA) idB := DeriveClaudeUserID(rawB) if idA == "" || idB == "" || idA == "unknown" || idB == "unknown" { t.Fatalf("expected valid derived user_id for gemini without explicit role, got idA=%q idB=%q", idA, idB) } if idA == idB { t.Fatalf("different gemini messages produced same user_id: %q", idA) } } func TestDeriveClaudeUserID_GeminiContentsMultipleTextParts(t *testing.T) { rawA := []byte(`{"contents":[{"role":"user","parts":[{"text":"Prefix"},{"text":"Question A"}]}]}`) rawB := []byte(`{"contents":[{"role":"user","parts":[{"text":"Prefix"},{"text":"Question B"}]}]}`) idA := DeriveClaudeUserID(rawA) idB := DeriveClaudeUserID(rawB) if idA == "" || idB == "" || idA == "unknown" || idB == "unknown" { t.Fatalf("expected valid derived user_id for gemini multiple parts, got idA=%q idB=%q", idA, idB) } if idA == idB { t.Fatalf("different second parts produced same user_id: %q", idA) } } func TestDeriveClaudeUserID_GeminiContentsSkipsThoughtParts(t *testing.T) { raw := []byte(`{ "contents": [ { "role": "user", "parts": [ {"thought": true, "text": "internal thought"}, {"text": "visible content"} ] } ] }`) rawOnlyVisible := []byte(`{ "contents": [ { "role": "user", "parts": [ {"text": "visible content"} ] } ] }`) id1 := DeriveClaudeUserID(raw) id2 := DeriveClaudeUserID(rawOnlyVisible) if id1 != id2 { t.Fatalf("thought part changed derived user_id: %q vs %q", id1, id2) } } func TestDeriveClaudeUserID_GeminiSystemInstruction(t *testing.T) { rawCamel := []byte(`{"systemInstruction":{"parts":[{"text":"system rule A"}]}}`) rawSnake := []byte(`{"system_instruction":{"parts":[{"text":"system rule B"}]}}`) idCamel := DeriveClaudeUserID(rawCamel) idSnake := DeriveClaudeUserID(rawSnake) if idCamel == "" || idSnake == "" || idCamel == "unknown" || idSnake == "unknown" { t.Fatalf("expected valid derived user_id for systemInstruction, got camel=%q snake=%q", idCamel, idSnake) } if idCamel == idSnake { t.Fatalf("different system instructions produced same user_id: %q", idCamel) } }