package upstream import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/linguo2625469/workbuddy2api-panel/internal/auth" ) const ( ccIdentity = "You are Claude Code, Anthropic's official CLI for Claude." ccBranch = "Main branch (you will usually use this for PRs)" ccHeader = "x-anthropic-billing-header: cc_version=1.0; cc_entrypoint=cli;" // Codex instructions 首段(上游逐字精确指纹,三句缺一不可)。 codexInstructions = "You are a coding agent running in the Codex CLI, a terminal-based coding assistant. Codex CLI is an open source project led by OpenAI. You are expected to be precise, safe, and helpful." ) func TestIdentityRewritten(t *testing.T) { out := sanitizeText(ccIdentity) if !strings.Contains(out, "official CLI tool for Claude.") { t.Errorf("identity not rewritten: %q", out) } if strings.Contains(out, ccIdentity) { t.Errorf("original identity still present: %q", out) } } // 桌面版(claude-desktop-3p / Agent SDK)的身份句以逗号接后继内容,结尾不是句号。 // 回归用例:匹配串曾带结尾句号,导致该形态漏网、指纹原样发上游 → 400 code=11128。 func TestIdentityDesktopVariantRewritten(t *testing.T) { in := "You are Claude Code, Anthropic's official CLI for Claude, running within the Claude Agent SDK." out := sanitizeText(in) if strings.Contains(out, "official CLI for Claude") { t.Errorf("desktop identity not rewritten: %q", out) } if !strings.Contains(out, "official CLI tool for Claude, running within the Claude Agent SDK.") { t.Errorf("desktop identity suffix not preserved: %q", out) } } func TestBranchRewritten(t *testing.T) { out := sanitizeText(ccBranch) if !strings.Contains(out, "Default branch (you will usually use this for PRs)") { t.Errorf("branch not rewritten: %q", out) } if strings.Contains(out, "Main branch") { t.Errorf("original branch still present: %q", out) } } // 反馈句带 Anthropic 仓库链接,上游按整句拦截(实测只留链接或只留半边均不拦)。 // 回归用例:give→provide 一词之差即可绕过。 func TestFeedbackSentenceRewritten(t *testing.T) { in := "To give feedback, users should report the issue at https://github.com/anthropics/claude-code/issues" out := sanitizeText(in) if strings.Contains(out, "To give feedback") { t.Errorf("feedback sentence not rewritten: %q", out) } if !strings.Contains(out, "To provide feedback, users should report the issue at https://github.com/anthropics/claude-code/issues") { t.Errorf("feedback sentence not rewritten as expected: %q", out) } } // 上游反探测:请求体里出现裸数字 11128 即整单拦截(与上下文无关)。 // 回归用例:该串会被改写为 11-128 以打断精确匹配。 func TestUpstreamErrorCodeRewritten(t *testing.T) { in := "upstream returned code=11128 for this request" out := sanitizeText(in) if strings.Contains(out, "11128") { t.Errorf("error code not rewritten: %q", out) } if !strings.Contains(out, "11-128") { t.Errorf("error code not rewritten as expected: %q", out) } } // 回归:工具调用消息的 content 常为 null,而旧版 sanitizeMessages 在 content 缺失时 // 直接 continue,整条消息连 tool_calls 一起被跳过 → arguments 里的被拦字符串原样漏出。 func TestToolCallArgumentsSanitized(t *testing.T) { msgs := []any{ map[string]any{"role": "user", "content": "run"}, map[string]any{"role": "assistant", "content": nil, "tool_calls": []any{ map[string]any{"id": "c1", "type": "function", "function": map[string]any{ "name": "Bash", "arguments": `{"command":"echo 11128"}`, }}, }}, } if !sanitizeMessages(msgs) { t.Fatal("sanitizeMessages 未报告任何改动,tool_calls 被跳过") } fn := msgs[1].(map[string]any)["tool_calls"].([]any)[0].(map[string]any)["function"].(map[string]any) got := fn["arguments"].(string) if strings.Contains(got, "11128") { t.Errorf("tool_call arguments 未被净化: %q", got) } } func TestBillingHeaderStrippedValueIrrelevant(t *testing.T) { out := sanitizeText(ccHeader) if strings.Contains(out, "x-anthropic-billing-header") { t.Errorf("header not stripped: %q", out) } } // 附加验证:正常对话里出现 github.com/anthropics/ 链接(但不是反馈句整句) // 时,预检特征命中(进入净化),但改写层只动精确匹配的整句——普通链接文本 // 不该被改写。同理,既不含 11128 也不含反馈整句的文本原样返回。 func TestNormalAnthropicLinkNotRewritten(t *testing.T) { in := "see https://github.com/anthropics/anthropic-cookbook for examples" if out := sanitizeText(in); out != in { t.Errorf("normal anthropic link should be untouched: %q -> %q", in, out) } } // Codex instructions 首段:命中预告且整句改写,逐字指纹被破坏、语义保留。 func TestCodexInstructionsRewritten(t *testing.T) { out := sanitizeText(codexInstructions) if strings.Contains(out, codexInstructions) { t.Errorf("codex fingerprint still present: %q", out) } if !strings.Contains(out, "You are a coding agent running in the Codex CLI tool, a terminal-based coding assistant.") { t.Errorf("codex first sentence not rewritten: %q", out) } // 其余两句原样保留,语义不变。 if !strings.Contains(out, "Codex CLI is an open source project led by OpenAI.") || !strings.Contains(out, "You are expected to be precise, safe, and helpful.") { t.Errorf("codex remaining sentences altered: %q", out) } } // 预检必须能认出 Codex 特征(此前只含 Claude Code,导致提前放行)。 func TestCodexFingerprintDetected(t *testing.T) { if !hasFingerprint(codexInstructions) { t.Error("codex fingerprint not detected by precheck") } } // 非精确变体不应被改写:仅去掉其中一个词即视为已破坏,无需改动。 func TestCodexVariantNotTouched(t *testing.T) { in := "You are a coding agent running in a CLI, a terminal-based coding assistant." if out := sanitizeText(in); out != in { t.Errorf("already-broken variant should be untouched: %q -> %q", in, out) } } func TestBillingHeaderCaseInsensitive(t *testing.T) { alt := "X-Anthropic-Billing-Header: cc_version=1.0;" out := strings.ToLower(sanitizeText(alt)) if strings.Contains(out, "billing") { t.Errorf("case-insensitive header not stripped: %q", out) } } func TestTrailingKVStripped(t *testing.T) { out := sanitizeText("...; cc_version=2.0; cc_entrypoint=cli;") if strings.Contains(out, "cc_version") || strings.Contains(out, "cc_entrypoint") { t.Errorf("trailing kv not stripped: %q", out) } } func TestExactMatchOnlyVariantNotTouched(t *testing.T) { in := "...official CLI for Claude!" if out := sanitizeText(in); out != in { t.Errorf("variant should be untouched: %q -> %q", in, out) } } func TestUserFreeTextNotTouched(t *testing.T) { in := "please use main branch for this repo" if out := sanitizeText(in); out != in { t.Errorf("free text should be untouched: %q -> %q", in, out) } } func TestNoFeatureReturnsSameString(t *testing.T) { in := "ordinary user message" if out := sanitizeText(in); out != in { t.Errorf("no-feature text should pass through unchanged: %q -> %q", in, out) } } func TestMultimodalTextPartOnly(t *testing.T) { imgPart := map[string]any{"type": "image", "source": map[string]any{"type": "base64", "data": "..."}} content := []any{ map[string]any{"type": "text", "text": ccIdentity}, imgPart, } out, changed := sanitizeContent(content) if !changed { t.Fatal("expected change") } parts := out.([]any) txt, _ := parts[0].(map[string]any)["text"].(string) if !strings.Contains(txt, "CLI tool") { t.Errorf("text part not sanitized: %q", txt) } img, _ := parts[1].(map[string]any) if img["type"] != "image" || img["source"].(map[string]any)["data"] != "..." { t.Error("image part modified") } } // 集成:完整请求体经 PrepareBodyOpt 净化后无残留指纹,且 stream/tool_choice 行为不受影响。 func TestPrepareBodyOptSanitizesSystem(t *testing.T) { body := []byte(`{"model":"glm-5.2","messages":[` + `{"role":"system","content":"` + ccIdentity + ` ` + ccHeader + `"},` + `{"role":"user","content":"hi"}]}`) out := PrepareBodyOpt(body, true) var obj map[string]any if err := json.Unmarshal(out, &obj); err != nil { t.Fatal(err) } if obj["stream"] != true { t.Error("stream not forced") } msgs := obj["messages"].([]any) sys, _ := msgs[0].(map[string]any)["content"].(string) if strings.Contains(sys, "x-anthropic-billing-header") || strings.Contains(sys, ccIdentity) || strings.Contains(sys, ccBranch) { t.Errorf("fingerprints remain: %q", sys) } if !strings.Contains(sys, "CLI tool") { t.Errorf("rewrite missing: %q", sys) } } func TestPrepareBodyOptDisabledPreservesFingerprints(t *testing.T) { body := []byte(`{"model":"glm-5.2","messages":[{"role":"system","content":"` + ccIdentity + `"}]}`) out := PrepareBodyOpt(body, false) if !strings.Contains(string(out), ccIdentity) { t.Error("sanitize=false should preserve fingerprints") } // 但 stream 仍强制 var obj map[string]any _ = json.Unmarshal(out, &obj) if obj["stream"] != true { t.Error("stream should still be forced") } } // PrepareBody 默认行为 = 开启脱敏(保持向后兼容)。 func TestPrepareBodyDefaultSanitizes(t *testing.T) { body := []byte(`{"model":"glm-5.2","messages":[{"role":"system","content":"` + ccIdentity + `"}]}`) out := PrepareBodyOpt(body, true) if strings.Contains(string(out), ccIdentity) { t.Error("PrepareBody default should sanitize") } } // 出站边界集成:ChatStream 发往上游的 wire body 必须无残留指纹。 func TestChatStreamWireBodySanitized(t *testing.T) { var gotBody []byte ts := newTestUpstream(t, func(w http.ResponseWriter, r *http.Request) { gotBody, _ = io.ReadAll(r.Body) w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}]}\n\ndata: [DONE]\n\n")) }) defer ts.Close() c := New() c.SanitizeFingerprints.Store(true) c.ChatBaseCN = ts.URL acct := &auth.Auth{AccessToken: "test-token", Domain: "copilot.tencent.com", UID: "u1"} body := []byte(`{"model":"glm-5.2","messages":[` + `{"role":"system","content":"` + ccIdentity + ` ` + ccHeader + `"},` + `{"role":"user","content":"hi"}]}`) rc, status, respBody, err := c.ChatStream(acct, body, "", ChatMeta{}) if err != nil { t.Fatal(err) } defer rc.Close() if status >= 400 { t.Fatalf("upstream status %d: %s", status, respBody) } // 上游收到的 body:stream 强制 + 指纹已净化 var obj map[string]any if err := json.Unmarshal(gotBody, &obj); err != nil { t.Fatalf("wire body not json: %v", err) } if obj["stream"] != true { t.Error("wire body stream not forced") } sys, _ := obj["messages"].([]any)[0].(map[string]any)["content"].(string) for _, fp := range []string{"x-anthropic-billing-header", ccIdentity, ccBranch} { if strings.Contains(sys, fp) { t.Errorf("wire body contains fingerprint %q: %q", fp, sys) } } } // 出站边界(Codex 场景):CPA 把 instructions 折成 role=system 的首条消息, // 净化后 wire body 不得残留 Codex 逐字指纹;其余消息不受影响。 func TestChatStreamWireBodyCodexInstructionsSanitized(t *testing.T) { var gotBody []byte ts := newTestUpstream(t, func(w http.ResponseWriter, r *http.Request) { gotBody, _ = io.ReadAll(r.Body) w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}]}\n\ndata: [DONE]\n\n")) }) defer ts.Close() c := New() c.SanitizeFingerprints.Store(true) c.ChatBaseCN = ts.URL acct := &auth.Auth{AccessToken: "test-token", Domain: "copilot.tencent.com", UID: "u1"} body := []byte(`{"model":"kimi-k3","messages":[` + `{"role":"system","content":"` + codexInstructions + `"},` + `{"role":"user","content":"say ok"}]}`) rc, status, respBody, err := c.ChatStream(acct, body, "", ChatMeta{}) if err != nil { t.Fatal(err) } defer rc.Close() if status >= 400 { t.Fatalf("upstream status %d: %s", status, respBody) } var obj map[string]any if err := json.Unmarshal(gotBody, &obj); err != nil { t.Fatalf("wire body not json: %v", err) } sys, _ := obj["messages"].([]any)[0].(map[string]any)["content"].(string) if strings.Contains(sys, codexInstructions) || strings.Contains(sys, "in the Codex CLI, a terminal-based coding assistant.") { t.Errorf("wire body still contains codex fingerprint: %q", sys) } if !strings.Contains(sys, "running in the Codex CLI tool, a terminal-based") { t.Errorf("codex rewrite missing on wire: %q", sys) } user, _ := obj["messages"].([]any)[1].(map[string]any)["content"].(string) if user != "say ok" { t.Errorf("user message altered: %q", user) } } // 出站边界:关闭脱敏后 wire body 原样保留指纹(验证开关真实有效)。 func TestChatStreamWireBodySanitizeDisabled(t *testing.T) { var gotBody []byte ts := newTestUpstream(t, func(w http.ResponseWriter, r *http.Request) { gotBody, _ = io.ReadAll(r.Body) w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("data: [DONE]\n\n")) }) defer ts.Close() c := New() c.SanitizeFingerprints.Store(false) c.ChatBaseCN = ts.URL acct := &auth.Auth{AccessToken: "test-token", Domain: "copilot.tencent.com", UID: "u1"} body := []byte(`{"model":"glm-5.2","messages":[{"role":"system","content":"` + ccIdentity + `"}]}`) rc, status, _, err := c.ChatStream(acct, body, "", ChatMeta{}) if err != nil { t.Fatal(err) } defer rc.Close() if status >= 400 { t.Fatalf("upstream status %d", status) } if !strings.Contains(string(gotBody), ccIdentity) { t.Error("sanitize disabled should preserve fingerprint on wire") } } // TestSanitizeMessagesScrubsReasoningField thinking.go 的回填会把客户端送来的 // reasoning_content 镜像进 reasoning 字段;此前只洗 content / reasoning_content / // tool_calls → 镜像进 reasoning 的指纹(裸 "11128" 这类反探测串)原样出站, // 而请求体里出现裸 11128 本身就是上游整单拦截条件。 func TestSanitizeMessagesScrubsReasoningField(t *testing.T) { ms := []any{map[string]any{ "role": "assistant", "content": "hi", "reasoning": "upstream said 11128", }} if !sanitizeMessages(ms) { t.Fatal("reasoning 字段里的指纹未被净化") } got, _ := ms[0].(map[string]any)["reasoning"].(string) if strings.Contains(got, "11128") { t.Fatalf("reasoning 仍含裸指纹: %q", got) } } // newTestUpstream 起一个假上游并捕获请求。 func newTestUpstream(t *testing.T, h http.HandlerFunc) *httptest.Server { t.Helper() return httptest.NewServer(h) }