sub2api/backend/internal/pkg/claude/constants_test.go
win 0fefedf9cd feat(claude-mimic): upgrade Claude Code mimicry to 2.1.145 via bundle abstraction
反编译本地 Claude Code 2.1.145 二进制 (Bun 1.3.14 打包,
@anthropic-ai/sdk@0.94.0 嵌入) 提取真实指纹,系统性升级 mimicry。

核心改动:
- 新增 ClaudeCodeBundle struct 作为单一事实源,DefaultBundle 描述当前
  伪装目标的完整快照 (CLIVersion/SDKVersion/RuntimeVersion/OS/Arch)
- DefaultCLIVersion/DefaultStainlessPackageVersion/CLICurrentVersion/
  DefaultHeaders 全部派生自 DefaultBundle,消除三处 (2.1.92, 2.1.104,
  0.70.0, 0.81.0) 版本分裂
- CLI 版本 2.1.92/2.1.104 -> 2.1.145
- SDK 版本 0.70.0/0.81.0 -> 0.94.0
- 新增 12 个 2.1.145 反编译确认的 anthropic-beta token:
  advanced-tool-use, tool-search-tool, mcp-servers, mcp-client,
  mid-conversation-system, afk-mode, cache-diagnosis, context-hint,
  environments, managed-agents, skills, compact
- FullClaudeCodeMimicryBetas() 从 7 个 token 升级到 21 个 ordered list
- 修正 BetaTokenEfficientTools 错日期 (2026-03-28 -> 2025-02-19)
- 从默认 beta header 移除已 GA 的 BetaFineGrainedToolStreaming /
  BetaTokenEfficientTools (常量保留供客户端显式 merge)
- claudemask.RequiredNodeHeaders 加 X-Claude-Code-Session-Id 强制

新增 ensureClaudeCodeSessionID helper (claude_code_session_id.go):
- 真实 CLI 在 SDK 内强制 X-Claude-Code-Session-Id:y_(),缺失被判第三方
- OAuth mimic 路径: metadata.user_id 派生 -> canonical UUID 写入
  -> 兜底 uuid.NewString()
- API key passthrough 路径: 不从 body 派生,保护客户端原始语义
- 所有路径均对客户端传入的非法 UUID 执行删除 (避免恶意值上游透传)
- 所有写入 header 的 session-id 都通过 uuid.Parse 校验

测试:
- 新增 14 个 ensureClaudeCodeSessionID 单元测试,含恶意 UUID 注入拒绝
  + API key 路径隔离 + canonical 形式校验
- 新增 3 个 bundle 派生一致性测试
- mask_test 加 session-id 缺失校验 case
- 老 UA 断言 2.1.104 -> 2.1.145

不在范围:
- TLS 指纹 (utls 已处理)
- Bun.hash vs xxHash64 算法验证 (需 golden vectors,独立项目)

References:
- VERSION:2.1.145 BUILD_TIME:2026-05-19T01:36:35Z
  GIT_SHA:daa4c3755d45ab0cf97bb41db8c03bd2dfd2ff5f
2026-05-20 17:18:47 +08:00

181 lines
7.3 KiB
Go

package claude
import (
"strings"
"testing"
)
// TestDefaultBundleVersions verifies the active Claude Code bundle snapshot
// matches the reverse-engineered 2.1.145 binary. Bump this when DefaultBundle
// is upgraded to a newer CLI release.
func TestDefaultBundleVersions(t *testing.T) {
if got := DefaultBundle.CLIVersion; got != "2.1.145" {
t.Fatalf("DefaultBundle.CLIVersion = %q, want %q", got, "2.1.145")
}
if got := DefaultBundle.SDKVersion; got != "0.94.0" {
t.Fatalf("DefaultBundle.SDKVersion = %q, want %q", got, "0.94.0")
}
}
// TestDefaultBundleDrivesLegacyExports ensures all legacy exported vars stay
// in sync with DefaultBundle (single source of truth).
func TestDefaultBundleDrivesLegacyExports(t *testing.T) {
if DefaultCLIVersion != DefaultBundle.CLIVersion {
t.Fatalf("DefaultCLIVersion = %q, want %q (from DefaultBundle)", DefaultCLIVersion, DefaultBundle.CLIVersion)
}
if DefaultStainlessPackageVersion != DefaultBundle.SDKVersion {
t.Fatalf("DefaultStainlessPackageVersion = %q, want %q (from DefaultBundle)", DefaultStainlessPackageVersion, DefaultBundle.SDKVersion)
}
if DefaultStainlessRuntimeVersion != DefaultBundle.RuntimeVersion {
t.Fatalf("DefaultStainlessRuntimeVersion = %q, want %q (from DefaultBundle)", DefaultStainlessRuntimeVersion, DefaultBundle.RuntimeVersion)
}
if DefaultStainlessOS != DefaultBundle.DefaultOS {
t.Fatalf("DefaultStainlessOS = %q, want %q (from DefaultBundle)", DefaultStainlessOS, DefaultBundle.DefaultOS)
}
if DefaultStainlessArch != DefaultBundle.DefaultArch {
t.Fatalf("DefaultStainlessArch = %q, want %q (from DefaultBundle)", DefaultStainlessArch, DefaultBundle.DefaultArch)
}
if CLICurrentVersion != DefaultBundle.CLIVersion {
t.Fatalf("CLICurrentVersion = %q, want %q (from DefaultBundle)", CLICurrentVersion, DefaultBundle.CLIVersion)
}
}
// TestDefaultHeadersDrivenByBundle ensures DefaultHeaders map carries the
// values from DefaultBundle, not stale hard-coded literals.
func TestDefaultHeadersDrivenByBundle(t *testing.T) {
if got := DefaultHeaders["X-Stainless-Package-Version"]; got != DefaultBundle.SDKVersion {
t.Fatalf("DefaultHeaders[X-Stainless-Package-Version] = %q, want %q", got, DefaultBundle.SDKVersion)
}
if got := DefaultHeaders["X-Stainless-Runtime-Version"]; got != DefaultBundle.RuntimeVersion {
t.Fatalf("DefaultHeaders[X-Stainless-Runtime-Version] = %q, want %q", got, DefaultBundle.RuntimeVersion)
}
if got := DefaultHeaders["X-Stainless-OS"]; got != DefaultBundle.DefaultOS {
t.Fatalf("DefaultHeaders[X-Stainless-OS] = %q, want %q", got, DefaultBundle.DefaultOS)
}
if got := DefaultHeaders["X-Stainless-Arch"]; got != DefaultBundle.DefaultArch {
t.Fatalf("DefaultHeaders[X-Stainless-Arch] = %q, want %q", got, DefaultBundle.DefaultArch)
}
ua := DefaultHeaders["User-Agent"]
if !strings.Contains(ua, "claude-cli/"+DefaultBundle.CLIVersion) {
t.Fatalf("DefaultHeaders[User-Agent] = %q, want substring %q", ua, "claude-cli/"+DefaultBundle.CLIVersion)
}
}
func TestApplyFingerprintOverridesUpdatesSharedDefaults(t *testing.T) {
t.Setenv("CLAUDE_CODE_ENTRYPOINT", "")
t.Setenv("CLAUDE_AGENT_SDK_VERSION", "")
t.Setenv("CLAUDE_AGENT_SDK_CLIENT_APP", "")
t.Setenv("CLAUDE_CODE_WORKLOAD", "")
originalVersion := DefaultCLIVersion
originalPackageVersion := DefaultStainlessPackageVersion
originalRuntimeVersion := DefaultStainlessRuntimeVersion
originalOS := DefaultStainlessOS
originalArch := DefaultStainlessArch
t.Cleanup(func() {
ApplyFingerprintOverrides(
originalVersion,
originalPackageVersion,
originalRuntimeVersion,
originalOS,
originalArch,
)
})
ApplyFingerprintOverrides("2.1.999", "0.99.0", "v99.0.0", "Linux", "x64")
if got := DefaultCLIVersion; got != "2.1.999" {
t.Fatalf("DefaultCLIVersion = %q, want %q", got, "2.1.999")
}
if got := DefaultUserAgent(); got != "claude-cli/2.1.999 (external, cli)" {
t.Fatalf("DefaultUserAgent() = %q", got)
}
profile := DefaultDeviceProfile()
if profile.StainlessPackageVersion != "0.99.0" {
t.Fatalf("DefaultDeviceProfile().StainlessPackageVersion = %q", profile.StainlessPackageVersion)
}
if profile.StainlessRuntimeVersion != "v99.0.0" {
t.Fatalf("DefaultDeviceProfile().StainlessRuntimeVersion = %q", profile.StainlessRuntimeVersion)
}
if profile.StainlessOS != "Linux" {
t.Fatalf("DefaultDeviceProfile().StainlessOS = %q", profile.StainlessOS)
}
if profile.StainlessArch != "x64" {
t.Fatalf("DefaultDeviceProfile().StainlessArch = %q", profile.StainlessArch)
}
if got := DefaultHeaders["User-Agent"]; got != profile.UserAgent {
t.Fatalf("DefaultHeaders[User-Agent] = %q, want %q", got, profile.UserAgent)
}
if got := DefaultHeaders["X-Stainless-Package-Version"]; got != profile.StainlessPackageVersion {
t.Fatalf("DefaultHeaders[X-Stainless-Package-Version] = %q, want %q", got, profile.StainlessPackageVersion)
}
if got := DefaultHeaders["X-Stainless-Runtime-Version"]; got != profile.StainlessRuntimeVersion {
t.Fatalf("DefaultHeaders[X-Stainless-Runtime-Version] = %q, want %q", got, profile.StainlessRuntimeVersion)
}
if got := DefaultHeaders["X-Stainless-OS"]; got != profile.StainlessOS {
t.Fatalf("DefaultHeaders[X-Stainless-OS] = %q, want %q", got, profile.StainlessOS)
}
if got := DefaultHeaders["X-Stainless-Arch"]; got != profile.StainlessArch {
t.Fatalf("DefaultHeaders[X-Stainless-Arch] = %q, want %q", got, profile.StainlessArch)
}
if got := DefaultHeaders["anthropic-dangerous-direct-browser-access"]; got != "true" {
t.Fatalf("DefaultHeaders[anthropic-dangerous-direct-browser-access] = %q, want %q", got, "true")
}
}
func TestDefaultUserAgentReflectsLocalRuntimeDescriptors(t *testing.T) {
t.Setenv("CLAUDE_CODE_ENTRYPOINT", "sdk-cli")
t.Setenv("CLAUDE_AGENT_SDK_VERSION", "0.0.77")
t.Setenv("CLAUDE_AGENT_SDK_CLIENT_APP", "cron-daemon")
t.Setenv("CLAUDE_CODE_WORKLOAD", "cron")
got := DefaultUserAgent()
want := "claude-cli/2.1.145 (external, sdk-cli, agent-sdk/0.0.77, client-app/cron-daemon, workload/cron)"
if got != want {
t.Fatalf("DefaultUserAgent() = %q, want %q", got, want)
}
}
func TestDetailedCodeUserAgentReflectsEntrypointAndSDK(t *testing.T) {
t.Setenv("CLAUDE_CODE_ENTRYPOINT", "remote")
t.Setenv("CLAUDE_AGENT_SDK_VERSION", "0.0.77")
t.Setenv("CLAUDE_AGENT_SDK_CLIENT_APP", "desktop")
got := DetailedCodeUserAgent()
want := "claude-code/2.1.145 (remote, agent-sdk/0.0.77, client-app/desktop)"
if got != want {
t.Fatalf("DetailedCodeUserAgent() = %q, want %q", got, want)
}
}
func TestOptionalAPIHeaders(t *testing.T) {
t.Setenv("CLAUDE_CODE_CONTAINER_ID", "ctr-123")
t.Setenv("CLAUDE_CODE_REMOTE_SESSION_ID", "remote-456")
t.Setenv("CLAUDE_AGENT_SDK_CLIENT_APP", "desktop")
t.Setenv("CLAUDE_CODE_ADDITIONAL_PROTECTION", "true")
headers := OptionalAPIHeaders()
if got := headers["x-claude-remote-container-id"]; got != "ctr-123" {
t.Fatalf("x-claude-remote-container-id = %q", got)
}
if got := headers["x-claude-remote-session-id"]; got != "remote-456" {
t.Fatalf("x-claude-remote-session-id = %q", got)
}
if got := headers["x-client-app"]; got != "desktop" {
t.Fatalf("x-client-app = %q", got)
}
if got := headers["x-anthropic-additional-protection"]; got != "true" {
t.Fatalf("x-anthropic-additional-protection = %q", got)
}
}
func TestAttributionHeaderDisabled(t *testing.T) {
t.Setenv("CLAUDE_CODE_ATTRIBUTION_HEADER", "false")
if !AttributionHeaderDisabled() {
t.Fatal("expected AttributionHeaderDisabled() to be true when env=false")
}
}