Files
CLIProxyAPI/internal/runtime/executor/claude_executor_diagnostics_test.go
sususu f63a925d15 fix(claude): replay measured OAuth wire, Fast and diagnostic profiles
Align the remaining measured OAuth wire profiles, including the ordered
connection writer in internal/httpwire that reproduces the observed header
sequence, and the refresh/profile response shapes in internal/auth/claude.

Replay the measured Fast path and keep diagnostic continuity across cloaked and
native requests.

Preserve the native direct token-counting shape so a caller that reaches
count_tokens itself is not reshaped into the cloaked form.

Scope cloak dates to the credential's timezone rather than the host's, so
currentDate matches what the real client would have sent for that account.
2026-08-03 14:47:26 +08:00

93 lines
4.4 KiB
Go

package executor
import (
"bytes"
"context"
"io"
"net/http"
"strings"
"testing"
claudeauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
"github.com/tidwall/gjson"
)
func TestInjectClaudeDiagnosticsMatchesNativeFieldOrderAndContinuity(t *testing.T) {
t.Parallel()
body := []byte(`{"context_management":{"edits":[{"type":"clear_thinking_20251015","keep":"all"}]},"max_tokens":1,"messages":[]}`)
first, state := injectClaudeDiagnostics(body, "credential-diagnostics-order", "session-diagnostics-order")
wantOrder := `"context_management":{"edits":[{"type":"clear_thinking_20251015","keep":"all"}]},"diagnostics":{"previous_message_id":null},"max_tokens"`
if !bytes.Contains(first, []byte(wantOrder)) {
t.Fatalf("diagnostics field order differs from native: %s", first)
}
if got := gjson.GetBytes(first, "diagnostics.previous_message_id"); got.Type != gjson.Null {
t.Fatalf("first previous_message_id = %s, want null", got.Raw)
}
commitClaudeDiagnostics(state, "msg_01ABCDEF0123456789ABCDEFG")
second, _ := injectClaudeDiagnostics(body, "credential-diagnostics-order", "session-diagnostics-order")
if got := gjson.GetBytes(second, "diagnostics.previous_message_id").String(); got != "msg_01ABCDEF0123456789ABCDEFG" {
t.Fatalf("second previous_message_id = %q, want committed upstream ID", got)
}
}
func TestClaudeExecutorDiagnosticsAdvancesAfterSuccessfulResponse(t *testing.T) {
var previousValues []gjson.Result
call := 0
transport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
body, errRead := io.ReadAll(req.Body)
if errRead != nil {
t.Fatal(errRead)
}
previousValues = append(previousValues, gjson.GetBytes(body, "diagnostics.previous_message_id"))
call++
response := `{"id":"msg_diagnostics_` + string(rune('0'+call)) + `","type":"message","model":"claude-opus-5","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`
return &http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(response)), Request: req}, nil
})
ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", http.RoundTripper(transport))
deviceIDs := []string{"0000000000000000000000000000000000000000000000000000000000000000"}
auth := &cliproxyauth.Auth{
ID: "diagnostics-live-path",
Attributes: map[string]string{"api_key": "sk-ant-oat-diagnostics-live-path"},
Metadata: map[string]any{
"account_uuid": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
claudeauth.ClaudeDeviceIDsMetadataKey: deviceIDs,
},
}
executor := NewClaudeExecutor(&config.Config{})
request := cliproxyexecutor.Request{Model: "claude-opus-5", Payload: []byte(`{"model":"claude-opus-5","messages":[{"role":"user","content":"x"}],"max_tokens":16}`)}
options := cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatClaude,
Metadata: map[string]any{cliproxyexecutor.ExecutionSessionMetadataKey: "diagnostics-conversation"},
}
for range 2 {
if _, errExecute := executor.Execute(ctx, auth, request, options); errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
}
if len(previousValues) != 2 || previousValues[0].Type != gjson.Null || previousValues[0].Raw != "null" {
t.Fatalf("first diagnostics value = %#v, want explicit null", previousValues)
}
if got := previousValues[1].String(); got != "msg_diagnostics_1" {
t.Fatalf("second diagnostics previous_message_id = %q, want first upstream response ID", got)
}
}
func TestClaudeMessageIDFromSSECommitsOnlyCompletedMessage(t *testing.T) {
t.Parallel()
complete := []byte("event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_complete\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n")
if got := claudeMessageIDFromSSE(complete); got != "msg_complete" {
t.Fatalf("completed SSE message ID = %q, want msg_complete", got)
}
incomplete := []byte(strings.Replace(string(complete), "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n", "", 1))
if got := claudeMessageIDFromSSE(incomplete); got != "" {
t.Fatalf("incomplete SSE message ID = %q, want empty", got)
}
}