Files
CLIProxyAPI/internal/runtime/executor/helps/claude_diagnostics_test.go
Luis Pater 377c315fd7 fix(claude): anchor billing fingerprint to initial turn for cloaked cache stability
- Derive billing fingerprint message text from the first user message to prevent prompt cache invalidation across turns.
- Centralize continuity tag resolution to maintain stable prompt IDs and request references across requests.

Closes: #5730
2026-09-11 16:59:57 +08:00

444 lines
18 KiB
Go

package helps
import (
"fmt"
"net/http"
"testing"
"time"
"github.com/google/uuid"
)
func TestClaudeDiagnosticsTracksCompletedMessagePerCredentialSession(t *testing.T) {
resetClaudeDiagnosticsForTest()
defer resetClaudeDiagnosticsForTest()
key, sequence, previous := BeginClaudeDiagnostics("credential-a", "session-a")
if key == "" || sequence != 1 || previous != "" {
t.Fatalf("first begin = %q/%d/%q, want key/1/empty", key, sequence, previous)
}
CommitClaudeDiagnostics(key, sequence, "msg_first")
_, secondSequence, previous := BeginClaudeDiagnostics("credential-a", "session-a")
if secondSequence != 2 || previous != "msg_first" {
t.Fatalf("second begin = %d/%q, want 2/msg_first", secondSequence, previous)
}
_, _, otherSession := BeginClaudeDiagnostics("credential-a", "session-b")
_, _, otherCredential := BeginClaudeDiagnostics("credential-b", "session-a")
if otherSession != "" || otherCredential != "" {
t.Fatalf("diagnostics leaked across identity: session=%q credential=%q", otherSession, otherCredential)
}
}
func TestClaudeDiagnosticsRejectsExpiredGenerationCommit(t *testing.T) {
resetClaudeDiagnosticsForTest()
defer resetClaudeDiagnosticsForTest()
key, expiredSequence, _ := BeginClaudeDiagnostics("credential", "session")
claudeDiagnosticsState.Lock()
entry := claudeDiagnosticsState.entries[key]
entry.expiresAt = time.Now().Add(-time.Second)
claudeDiagnosticsState.entries[key] = entry
claudeDiagnosticsState.Unlock()
newKey, currentSequence, previous := BeginClaudeDiagnostics("credential", "session")
if newKey != key || currentSequence <= expiredSequence || previous != "" {
t.Fatalf("new generation = %q/%d/%q, want same key/new sequence/empty", newKey, currentSequence, previous)
}
CommitClaudeDiagnostics(newKey, currentSequence, "msg_current")
CommitClaudeDiagnostics(key, expiredSequence, "msg_expired")
_, _, previous = BeginClaudeDiagnostics("credential", "session")
if previous != "msg_current" {
t.Fatalf("previous message = %q, want current generation", previous)
}
}
func TestClaudeDiagnosticsCacheEvictsOldestEntriesWithinCapacity(t *testing.T) {
resetClaudeDiagnosticsForTest()
defer resetClaudeDiagnosticsForTest()
firstKey, firstSequence, _ := BeginClaudeDiagnostics("credential", "session-0")
var newestKey string
for index := 1; index <= claudeDiagnosticsMaxEntries; index++ {
newestKey, _, _ = BeginClaudeDiagnostics("credential", fmt.Sprintf("session-%d", index))
}
claudeDiagnosticsState.Lock()
entryCount := len(claudeDiagnosticsState.entries)
_, firstFound := claudeDiagnosticsState.entries[firstKey]
_, newestFound := claudeDiagnosticsState.entries[newestKey]
claudeDiagnosticsState.Unlock()
if entryCount > claudeDiagnosticsMaxEntries {
t.Fatalf("cache entries = %d, want at most %d", entryCount, claudeDiagnosticsMaxEntries)
}
if firstFound {
t.Fatal("oldest diagnostics entry was not evicted")
}
if !newestFound {
t.Fatal("newest diagnostics entry was evicted")
}
newKey, newSequence, _ := BeginClaudeDiagnostics("credential", "session-0")
if newKey != firstKey || newSequence <= firstSequence {
t.Fatalf("recreated generation = %q/%d, want same key after sequence %d", newKey, newSequence, firstSequence)
}
CommitClaudeDiagnostics(newKey, newSequence, "msg_recreated")
CommitClaudeDiagnostics(firstKey, firstSequence, "msg_evicted")
_, _, previous := BeginClaudeDiagnostics("credential", "session-0")
if previous != "msg_recreated" {
t.Fatalf("previous message = %q, want recreated generation", previous)
}
}
func TestClaudeDiagnosticsRejectsLateOlderCommit(t *testing.T) {
resetClaudeDiagnosticsForTest()
defer resetClaudeDiagnosticsForTest()
key, first, _ := BeginClaudeDiagnostics("credential", "session")
_, second, _ := BeginClaudeDiagnostics("credential", "session")
CommitClaudeDiagnostics(key, second, "msg_newer")
CommitClaudeDiagnostics(key, first, "msg_older")
_, _, previous := BeginClaudeDiagnostics("credential", "session")
if previous != "msg_newer" {
t.Fatalf("previous message = %q, want newer completed generation", previous)
}
}
func TestClaudeContinuityTracksRequestIDAndPromptID(t *testing.T) {
resetClaudeDiagnosticsForTest()
defer resetClaudeDiagnosticsForTest()
// Turn 1: New prompt turn
key, seq1, prevMsg, prevReq, prompt1 := BeginClaudeContinuity("cred-1", "sess-1", true, "")
if prevMsg != "" || prevReq != "" || prompt1 == "" {
t.Fatalf("turn 1 = prevMsg:%q prevReq:%q prompt:%q, want empty prevs and fresh prompt", prevMsg, prevReq, prompt1)
}
CommitClaudeContinuity(key, seq1, "msg_01aaa", "req_01bbb", prompt1)
// Turn 1.1: Tool continuation turn (not new prompt)
_, seq2, prevMsg, prevReq, prompt2 := BeginClaudeContinuity("cred-1", "sess-1", false, "")
if prevMsg != "msg_01aaa" || prevReq != "req_01bbb" {
t.Fatalf("turn 1.1 = prevMsg:%q prevReq:%q, want msg_01aaa / req_01bbb", prevMsg, prevReq)
}
if prompt2 != prompt1 {
t.Fatalf("turn 1.1 prompt = %q, want same prompt %q as turn 1", prompt2, prompt1)
}
CommitClaudeContinuity(key, seq2, "msg_01ccc", "req_01ddd", prompt2)
// Turn 2: New prompt turn
_, _, prevMsg, prevReq, prompt3 := BeginClaudeContinuity("cred-1", "sess-1", true, "")
if prevMsg != "msg_01ccc" || prevReq != "req_01ddd" {
t.Fatalf("turn 2 = prevMsg:%q prevReq:%q, want msg_01ccc / req_01ddd", prevMsg, prevReq)
}
if prompt3 == prompt1 {
t.Fatalf("turn 2 prompt = %q, want new prompt different from %q", prompt3, prompt1)
}
}
func TestClaudeContinuityHelperPredicates(t *testing.T) {
probeBody := []byte(`{"model":"claude-fable-5-1","max_tokens":1,"messages":[{"role":"user","content":[{"type":"text","text":"Hi","cache_control":{"type":"ephemeral"}}]}]}`)
if !IsClaudeProbeOrHelperRequest(probeBody) {
t.Fatal("IsClaudeProbeOrHelperRequest(fable probe) = false, want true")
}
quotaProbeBody := []byte(`{"model":"claude-haiku-4-5-20251001","max_tokens":1,"messages":[{"role":"user","content":"quota"}]}`)
if !IsClaudeProbeOrHelperRequest(quotaProbeBody) {
t.Fatal("IsClaudeProbeOrHelperRequest(quota probe) = false, want true")
}
ordinaryHiMaxTokens1 := []byte(`{"model":"claude-fable-5-1","max_tokens":1,"messages":[{"role":"user","content":"Hi"}]}`)
if IsClaudeProbeOrHelperRequest(ordinaryHiMaxTokens1) {
t.Fatal("IsClaudeProbeOrHelperRequest(ordinary Hi max_tokens=1) = true, want false")
}
multiTurnMaxTokens1 := []byte(`{"model":"claude-sonnet-5","max_tokens":1,"messages":[{"role":"user","content":"hello"},{"role":"assistant","content":"hi"},{"role":"user","content":"what is 1+1?"}]}`)
if IsClaudeProbeOrHelperRequest(multiTurnMaxTokens1) {
t.Fatal("IsClaudeProbeOrHelperRequest(multi-turn max_tokens=1) = true, want false")
}
withToolsMaxTokens1 := []byte(`{"model":"claude-sonnet-5","max_tokens":1,"messages":[{"role":"user","content":"Hi"}],"tools":[{"name":"t1","description":"tool"}]}`)
if IsClaudeProbeOrHelperRequest(withToolsMaxTokens1) {
t.Fatal("IsClaudeProbeOrHelperRequest(tools max_tokens=1) = true, want false")
}
titleBody := []byte(`{"model":"claude-haiku-4-5-20251001","output_config":{"format":{"schema":{"properties":{"title":{"type":"string"}}}}},"messages":[{"role":"user","content":"Return a short title summarizing this conversation"}]}`)
if !IsClaudeProbeOrHelperRequest(titleBody) {
t.Fatal("IsClaudeProbeOrHelperRequest(title helper) = false, want true")
}
ordinaryTitleSchema := []byte(`{"model":"claude-haiku-4-5-20251001","output_config":{"format":{"schema":{"properties":{"title":{"type":"string"}}}}},"messages":[{"role":"user","content":"what is the title of the book?"}]}`)
if IsClaudeProbeOrHelperRequest(ordinaryTitleSchema) {
t.Fatal("IsClaudeProbeOrHelperRequest(ordinary title schema) = true, want false")
}
relocatedTitleBody := []byte(`{"model":"claude-sonnet-5","system":[{"type":"text","text":"cli-identity"}],"messages":[{"role":"system","content":"Return a short title summarizing this conversation"}]}`)
if !IsClaudeProbeOrHelperRequest(relocatedTitleBody) {
t.Fatal("IsClaudeProbeOrHelperRequest(relocated title system in messages) = false, want true")
}
toolResultBody := []byte(`{"model":"claude-sonnet-5","messages":[{"role":"user","content":"hi"},{"role":"assistant","content":[{"type":"tool_use","id":"t1"}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":"ok"}]}]}`)
if IsClaudeNewPromptTurn(toolResultBody) {
t.Fatal("IsClaudeNewPromptTurn(tool_result) = true, want false")
}
newPromptBody := []byte(`{"model":"claude-sonnet-5","messages":[{"role":"user","content":"explain sorting"}]}`)
if !IsClaudeNewPromptTurn(newPromptBody) {
t.Fatal("IsClaudeNewPromptTurn(user text) = false, want true")
}
subagentHeader := http.Header{"X-Claude-Code-Agent-Id": []string{"sub-1"}}
if !IsClaudeSubagentRequest(subagentHeader, []byte(`{}`)) {
t.Fatal("IsClaudeSubagentRequest(agent header) = false, want true")
}
subagentMetaBody := []byte(`{"metadata":{"user_id":"{\"device_id\":\"dev\",\"session_id\":\"sess\",\"parent_session_id\":\"parent-1\"}"}}`)
if !IsClaudeSubagentRequest(nil, subagentMetaBody) {
t.Fatal("IsClaudeSubagentRequest(parent_session_id) = false, want true")
}
billingSystemBody := []byte(`{"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.258.1e2; cc_entrypoint=cli; cch=00000; cc_prev_req=req_01abc; cc_prompt_id=3c6489dc-badc-42b2-bd28-49f8ebabfedd;"}]}`)
prevReq, promptID := ExtractClaudeBillingTags(billingSystemBody)
if prevReq != "req_01abc" || promptID != "3c6489dc-badc-42b2-bd28-49f8ebabfedd" {
t.Fatalf("ExtractClaudeBillingTags = %q, %q; want req_01abc, 3c6489dc-badc-42b2-bd28-49f8ebabfedd", prevReq, promptID)
}
}
func TestIsValidClaudePromptID(t *testing.T) {
v4 := uuid.NewString()
if !IsValidClaudePromptID(v4) {
t.Fatalf("IsValidClaudePromptID(%q) = false, want true", v4)
}
// UUIDv1 should be rejected
v1 := "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
if IsValidClaudePromptID(v1) {
t.Fatalf("IsValidClaudePromptID(v1 %q) = true, want false", v1)
}
// UUIDv4 with non-RFC4122 variant (variant bits 0xxx instead of 10xx, e.g. '0' instead of '8','9','a','b')
nonRFC4122 := "3c6489dc-badc-42b2-0d28-49f8ebabfedd"
if IsValidClaudePromptID(nonRFC4122) {
t.Fatalf("IsValidClaudePromptID(non-RFC4122 variant %q) = true, want false", nonRFC4122)
}
// All zeros UUID should be rejected (version 0)
allZeros := "00000000-0000-0000-0000-000000000000"
if IsValidClaudePromptID(allZeros) {
t.Fatalf("IsValidClaudePromptID(all zeros %q) = true, want false", allZeros)
}
// Invalid format strings
for _, invalid := range []string{"", "not-a-uuid", "12345", "3c6489dc-badc-42b2-bd28-49f8ebabfedd-extra"} {
if IsValidClaudePromptID(invalid) {
t.Fatalf("IsValidClaudePromptID(%q) = true, want false", invalid)
}
}
}
func TestClaudeContinuityConcurrentOverlappingPromptID(t *testing.T) {
resetClaudeDiagnosticsForTest()
defer resetClaudeDiagnosticsForTest()
// Turn 1 starts (in-flight, not committed)
key, seq1, _, _, prompt1 := BeginClaudeContinuity("cred-1", "sess-1", true, "")
// Turn 2 starts concurrently before Turn 1 commits
_, seq2, _, _, prompt2 := BeginClaudeContinuity("cred-1", "sess-1", true, "")
if prompt1 == prompt2 {
t.Fatalf("turn 1 and turn 2 got same prompt %q, want distinct", prompt1)
}
// Turn 1 completes upstream and commits
CommitClaudeContinuity(key, seq1, "msg_01aaa", "req_01bbb", prompt1)
// Turn 1.1 tool continuation starts (not new turn)
_, _, prevMsg, prevReq, prompt11 := BeginClaudeContinuity("cred-1", "sess-1", false, "")
if prevMsg != "msg_01aaa" || prevReq != "req_01bbb" {
t.Fatalf("turn 1.1 prevMsg=%q prevReq=%q, want msg_01aaa / req_01bbb", prevMsg, prevReq)
}
if prompt11 != prompt1 {
t.Fatalf("turn 1.1 prompt=%q, want committed turn 1 prompt %q, not overwritten by uncommitted turn 2 (%q)", prompt11, prompt1, prompt2)
}
// Turn 2 completes upstream and commits
CommitClaudeContinuity(key, seq2, "msg_01ccc", "req_01ddd", prompt2)
// Turn 2.1 tool continuation starts
_, _, prevMsg2, prevReq2, prompt21 := BeginClaudeContinuity("cred-1", "sess-1", false, "")
if prevMsg2 != "msg_01ccc" || prevReq2 != "req_01ddd" {
t.Fatalf("turn 2.1 prevMsg=%q prevReq=%q, want msg_01ccc / req_01ddd", prevMsg2, prevReq2)
}
if prompt21 != prompt2 {
t.Fatalf("turn 2.1 prompt=%q, want committed turn 2 prompt %q", prompt21, prompt2)
}
}
func TestClaudeContinuityClearsStaleRequestID(t *testing.T) {
resetClaudeDiagnosticsForTest()
defer resetClaudeDiagnosticsForTest()
// Turn 1: has valid request-id
key, seq1, _, _, p1 := BeginClaudeContinuity("cred-1", "sess-1", true, "")
CommitClaudeContinuity(key, seq1, "msg_01aaa", "req_01bbb", p1)
// Turn 1.1: verify prevReq is req_01bbb
_, seq2, _, prevReq1, p2 := BeginClaudeContinuity("cred-1", "sess-1", false, "")
if prevReq1 != "req_01bbb" {
t.Fatalf("turn 1.1 prevReq = %q, want req_01bbb", prevReq1)
}
// Turn 1.1 finishes, but upstream returned no request-id
CommitClaudeContinuity(key, seq2, "msg_01ccc", "", p2)
// Turn 2: verify prevReq is empty, NOT stale req_01bbb
_, _, prevMsg, prevReq2, _ := BeginClaudeContinuity("cred-1", "sess-1", true, "")
if prevMsg != "msg_01ccc" {
t.Fatalf("turn 2 prevMsg = %q, want msg_01ccc", prevMsg)
}
if prevReq2 != "" {
t.Fatalf("turn 2 prevReq = %q, want empty (must not retain stale request-id from prior turn)", prevReq2)
}
}
func TestClaudeContinuity_ExpiredEntryDoesNotInheritPromptID(t *testing.T) {
resetClaudeDiagnosticsForTest()
defer resetClaudeDiagnosticsForTest()
key, _, _, _, p1 := BeginClaudeContinuity("cred-expire", "sess-expire", true, "")
if p1 == "" {
t.Fatal("expected non-empty promptID")
}
// Expire entry manually
claudeDiagnosticsState.Lock()
entry := claudeDiagnosticsState.entries[key]
entry.expiresAt = time.Now().Add(-time.Second)
claudeDiagnosticsState.entries[key] = entry
claudeDiagnosticsState.Unlock()
// Tool continuation call (isNewPromptTurn = false) without explicit prompt ID on expired entry
_, _, _, _, p2 := BeginClaudeContinuity("cred-expire", "sess-expire", false, "")
if p2 == "" {
t.Fatal("new generation must generate non-empty prompt ID")
}
if p2 == p1 {
t.Fatalf("new generation inherited expired prompt ID: %s", p1)
}
}
func TestIsClaudeProbeRequest_MultiContentBlocksNotAProbe(t *testing.T) {
// A request with max_tokens: 1 and multiple content blocks where one happens to be "quota"
// but another is a regular prompt must NOT be treated as a probe.
payload := []byte(`{
"model": "claude-sonnet-5",
"max_tokens": 1,
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "quota"},
{"type": "text", "text": "Please write an analysis of the codebase."}
]
}]
}`)
if IsClaudeProbeOrHelperRequest(payload) {
t.Fatal("multi-content user prompt with non-quota text must not be classified as a probe")
}
}
func TestIsClaudeProbeRequest_SingleContentBlockWithReminderIsProbe(t *testing.T) {
// A probe request with a system-reminder block and a single quota block IS a probe.
payload := []byte(`{
"model": "claude-sonnet-5",
"max_tokens": 1,
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "<system-reminder>some reminder</system-reminder>"},
{"type": "text", "text": "quota"}
]
}]
}`)
if !IsClaudeProbeOrHelperRequest(payload) {
t.Fatal("probe with system reminder and single quota block must be classified as a probe")
}
}
func TestClaudePayloadHas1hTTL(t *testing.T) {
tests := []struct {
name string
payload []byte
want bool
}{
{
name: "empty payload",
payload: []byte(""),
want: false,
},
{
name: "invalid json",
payload: []byte("not-json"),
want: false,
},
{
name: "no cache control",
payload: []byte(`{"messages":[{"role":"user","content":"hello"}]}`),
want: false,
},
{
name: "default 5m ephemeral",
payload: []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hello","cache_control":{"type":"ephemeral"}}]}]}`),
want: false,
},
{
name: "message content with 1h ttl",
payload: []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hello","cache_control":{"type":"ephemeral","ttl":"1h"}}]}]}`),
want: true,
},
{
name: "system block with 1h ttl",
payload: []byte(`{"system":[{"type":"text","text":"sys","cache_control":{"type":"ephemeral","ttl":"1h"}}]}`),
want: true,
},
{
name: "tool block with 1h ttl",
payload: []byte(`{"tools":[{"name":"tool1","cache_control":{"type":"ephemeral","ttl":"1h"}}]}`),
want: true,
},
{
name: "message text mentions ttl 1h without cache control",
payload: []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"can you set ttl: 1h"}]}]}`),
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ClaudePayloadHas1hTTL(tt.payload); got != tt.want {
t.Fatalf("ClaudePayloadHas1hTTL() = %v, want %v", got, tt.want)
}
})
}
}
func TestClaudeSubagentRequests1h(t *testing.T) {
// 1. Neither header nor payload has 1h
headers := http.Header{}
payload := []byte(`{"messages":[{"role":"user","content":"task"}]}`)
if ClaudeSubagentRequests1h(headers, payload) {
t.Fatal("ClaudeSubagentRequests1h() = true, want false when no 1h requested")
}
// 2. Payload has 1h
payloadWith1h := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"task","cache_control":{"type":"ephemeral","ttl":"1h"}}]}]}`)
if !ClaudeSubagentRequests1h(headers, payloadWith1h) {
t.Fatal("ClaudeSubagentRequests1h() = false, want true when payload has 1h")
}
// 3. Header has extended-cache-ttl beta
headersWithBeta := http.Header{}
headersWithBeta.Set("Anthropic-Beta", "claude-code-20250219,extended-cache-ttl-2025-04-11")
if !ClaudeSubagentRequests1h(headersWithBeta, payload) {
t.Fatal("ClaudeSubagentRequests1h() = false, want true when header has extended-cache-ttl beta")
}
}