vibe-proxy/backend/internal/translator/common/claude_user_id_test.go
2026-08-24 00:10:41 +02:00

286 lines
11 KiB
Go

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)
}
}