219 lines · 8.0 KB · Go
RawDownload
package executor import (	"bufio"	"bytes"	"net/http"	"net/http/httptest"	"os"	"sort"	"strings"	"testing" 	cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth") // claudeCode2_1_220WireHeaderOrder is the header name sequence captured from a// real Claude Code 2.1.220 OAuth POST /v1/messages over HTTP/1.1, minus the four// names the Node HTTP layer appends after the sorted block (Connection, Host,// Accept-Encoding, Content-Length) and minus User-Agent. Go hardcodes Host,// User-Agent and Content-Length ahead of the sorted block, so those four// positions cannot be matched without replacing the request serialiser; the real// client carries User-Agent inside the sorted block at index 3.var claudeCode2_1_220WireHeaderOrder = []string{	"Accept",	"Authorization",	"Content-Type",	"X-Claude-Code-Session-Id",	"X-Stainless-Arch",	"X-Stainless-Lang",	"X-Stainless-OS",	"X-Stainless-Package-Version",	"X-Stainless-Retry-Count",	"X-Stainless-Runtime",	"X-Stainless-Runtime-Version",	"X-Stainless-Timeout",	"anthropic-beta",	"anthropic-dangerous-direct-browser-access",	"anthropic-version",	"x-app",	"x-client-request-id",} func newClaudeWireProbeRequest(t *testing.T, rawURL string) *http.Request {	t.Helper()	auth := &cliproxyauth.Auth{ID: "wire", Metadata: map[string]any{"access_token": "sk-ant-oat01-wire"}}	req := httptest.NewRequest(http.MethodPost, rawURL, strings.NewReader("{}"))	req.Header = http.Header{}	body := []byte(`{"model":"claude-opus-5","messages":[{"role":"user","content":"hi"}]}`)	if err := applyClaudeHeaders(req, auth, "sk-ant-oat01-wire", false, nil, body, nil, nil, false); err != nil {		t.Fatalf("applyClaudeHeaders: %v", err)	}	// Mirror the production sequence: the casing pass runs at the send boundary,	// not inside applyClaudeHeaders, so Header.Get keeps working everywhere else.	applyClaudeWireHeaderCasing(req)	return req} // The casing pass must stay at the send boundary. Running it inside// applyClaudeHeaders would make these headers invisible to Header.Get for the// rest of the pipeline, which is how the first attempt broke ten other tests.func TestApplyClaudeHeaders_LeavesHeadersCanonicalForThePipeline(t *testing.T) {	auth := &cliproxyauth.Auth{ID: "wire", Metadata: map[string]any{"access_token": "sk-ant-oat01-wire"}}	req := httptest.NewRequest(http.MethodPost, "https://api.anthropic.com/v1/messages?beta=true", strings.NewReader("{}"))	req.Header = http.Header{}	body := []byte(`{"model":"claude-opus-5","messages":[{"role":"user","content":"hi"}]}`)	if err := applyClaudeHeaders(req, auth, "sk-ant-oat01-wire", false, nil, body, nil, nil, false); err != nil {		t.Fatalf("applyClaudeHeaders: %v", err)	}	for canonical := range claudeWireHeaderCasing {		if req.Header.Get(canonical) == "" {			t.Fatalf("%s is unreadable through Header.Get right after applyClaudeHeaders", canonical)		}	}} // serializedHeaderNames reads the names off the actual serialized request, which// is the only representation the server ever sees.func serializedHeaderNames(t *testing.T, req *http.Request) []string {	t.Helper()	var buf bytes.Buffer	if err := req.Write(&buf); err != nil {		t.Fatalf("write request: %v", err)	}	var names []string	scanner := bufio.NewScanner(&buf)	scanner.Scan() // request line	for scanner.Scan() {		line := scanner.Text()		if line == "" {			break		}		name, _, found := strings.Cut(line, ":")		if !found {			t.Fatalf("malformed header line %q", line)		}		names = append(names, name)	}	return names} // The wire casing is a fingerprint in its own right: CPA negotiates ALPN// http/1.1, so names are not lowercased by HPACK and reach Anthropic verbatim.func TestApplyClaudeHeaders_WireCasingMatchesRealClient(t *testing.T) {	req := newClaudeWireProbeRequest(t, "https://api.anthropic.com/v1/messages?beta=true")	got := serializedHeaderNames(t, req) 	transportOwned := map[string]bool{		"Host": true, "Content-Length": true, "Connection": true, "Accept-Encoding": true,		// Go writes User-Agent before the sorted block; the real client keeps it		// inside it. Tracked separately below.		"User-Agent": true,	}	var sdkNames []string	for _, name := range got {		if !transportOwned[name] {			sdkNames = append(sdkNames, name)		}	} 	want := claudeCode2_1_220WireHeaderOrder	if len(sdkNames) != len(want) {		t.Fatalf("header count = %d, want %d\n got %v", len(sdkNames), len(want), sdkNames)	}	for i := range want {		if sdkNames[i] != want[i] {			t.Fatalf("wire header %d = %q, want %q\n got  %v\n want %v", i, sdkNames[i], want[i], sdkNames, want)		}	}} // Documents the one ordering gap the casing fix cannot close. If Go ever stops// hoisting User-Agent, or the serialiser is replaced, this test fails and the// name can move back into claudeCode2_1_220WireHeaderOrder.func TestApplyClaudeHeaders_UserAgentStillHoistedByGo(t *testing.T) {	req := newClaudeWireProbeRequest(t, "https://api.anthropic.com/v1/messages?beta=true")	names := serializedHeaderNames(t, req)	uaIndex, acceptIndex := -1, -1	for i, name := range names {		switch name {		case "User-Agent":			uaIndex = i		case "Accept":			acceptIndex = i		}	}	if uaIndex == -1 || acceptIndex == -1 {		t.Fatalf("missing User-Agent or Accept: %v", names)	}	if uaIndex > acceptIndex {		t.Fatal("User-Agent now sorts with the block: fold it back into the expected wire order")	}	if got := req.Header.Get("User-Agent"); !strings.HasPrefix(got, "claude-cli/") {		t.Fatalf("User-Agent = %q, want the Claude Code identity", got)	}} // Guards the property that makes the casing fix sufficient: the real client's// order is a plain bytewise sort, which is also what Go emits.func TestClaudeWireHeaderOrderIsBytewiseSorted(t *testing.T) {	sorted := append([]string(nil), claudeCode2_1_220WireHeaderOrder...)	sort.Strings(sorted)	for i := range sorted {		if sorted[i] != claudeCode2_1_220WireHeaderOrder[i] {			t.Fatalf("captured order is not a bytewise sort at %d: %q vs %q", i, claudeCode2_1_220WireHeaderOrder[i], sorted[i])		}	}} // Every fingerprint rule is keyed on the upstream host, never on the caller.func TestApplyClaudeHeaders_WireCasingIsAnthropicOnly(t *testing.T) {	req := newClaudeWireProbeRequest(t, "https://api.moonshot.cn/v1/messages")	for _, name := range serializedHeaderNames(t, req) {		if name == "anthropic-beta" || name == "x-app" || name == "X-Stainless-OS" {			t.Fatalf("Anthropic wire casing leaked to a third-party gateway: %q", name)		}	}	if req.Header.Get("Anthropic-Version") == "" {		t.Fatal("third-party gateway lost its canonical headers")	}} // The rewritten keys are unreachable through Header.Get, so the pass has to run// after every other mutation. This pins that the values survived the rewrite.func TestApplyClaudeHeaders_WireCasingPreservesValues(t *testing.T) {	req := newClaudeWireProbeRequest(t, "https://api.anthropic.com/v1/messages?beta=true")	for canonical, wire := range claudeWireHeaderCasing {		if _, stillCanonical := req.Header[canonical]; stillCanonical {			t.Fatalf("%s was not rewritten to %s", canonical, wire)		}		if len(req.Header[wire]) == 0 || req.Header[wire][0] == "" {			t.Fatalf("%s lost its value during the rewrite", wire)		}	}} // The three Claude request paths must all leave through doClaudeUpstreamRequest.// A direct client.Do would skip the wire-casing pass silently, and no behavioural// test can catch that for a path it does not exercise, so the invariant is// checked structurally.func TestClaudeExecutorHasSingleUpstreamSendBoundary(t *testing.T) {	paths := []string{		"claude_executor_execute.go",		"claude_executor_stream.go",		"claude_executor_tokens.go",	}	for _, name := range paths {		src, err := os.ReadFile(name)		if err != nil {			t.Fatalf("read %s: %v", name, err)		}		text := string(src)		if strings.Contains(text, "httpClient.Do(") {			t.Errorf("%s bypasses the send boundary with a direct httpClient.Do", name)		}		if !strings.Contains(text, "doClaudeUpstreamRequest(") {			t.Errorf("%s does not route through doClaudeUpstreamRequest", name)		}	}}