Files
sub2api/backend/internal/pkg/claude/cli_version_test.go
T
LuckyandClaude Opus 5 e9bad40b10 feat(claude): allow operators to override the advertised CLI version
Anthropic gates newer models on a minimum Claude Code client version. With
CLICurrentVersion pinned at 2.1.220, claude-fable-5-1 fails every request with
"Claude Code 2.1.220 does not support this model; version 2.1.251 or newer is
required", and the only fix is a release that bumps one constant.

Add SUB2API_CLAUDE_CLI_VERSION, mirroring what internal/pkg/xai already does
with XAI_GROK_CLI_VERSION. CLICurrentVersion remains the built-in pin; the new
CLIVersion() is what every consumer reads. With no env var set the resolved
value equals the pin, so existing deployments are unaffected.

The value is resolved once at package init rather than per call: the User-Agent
header and the request body's cc_version are written by different code paths for
the same request, and reading different values would make that request
contradict itself.

Validation is strict (three numeric segments, not below the pin) because
identity_service.go documents what a malformed version does once it reaches an
account's persistent fingerprint: sustained 429s without reset headers, 5s
fallback cooldown, pool collapse into a 503 storm, and no reset entry point.
Invalid values warn and fall back rather than being silently dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168uDMAj9anaJrnH8CeFze6
2026-09-04 18:01:34 +00:00

74 lines
2.5 KiB
Go

package claude
import "testing"
func TestIsSupportedCLIVersion(t *testing.T) {
cases := []struct {
name string
version string
want bool
}{
{"内置基线本身", CLICurrentVersion, true},
{"高于基线的补丁位", "2.1.251", true},
{"高于基线的次版本", "2.2.0", true},
{"高于基线的主版本", "3.0.0", true},
{"低于基线", "2.1.219", false},
{"远低于基线", "1.0.0", false},
{"空值", "", false},
{"只有两段", "2.2", false},
{"带 v 前缀", "v2.2.0", false},
{"预发布后缀", "2.2.0-local", false},
{"构建元数据", "2.2.0+build1", false},
{"哨兵版本带后缀", "999.0.0-local", false},
{"纯数字哨兵仍然合法(形态没问题,运维自负)", "999.0.0", true},
{"非数字", "abc", false},
{"多余的段", "2.2.0.1", false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := IsSupportedCLIVersion(tc.version); got != tc.want {
t.Fatalf("IsSupportedCLIVersion(%q) = %v, want %v", tc.version, got, tc.want)
}
})
}
}
func TestResolveCLIVersion(t *testing.T) {
cases := []struct {
name string
raw string
want string
}{
{"未配置时用内置基线", "", CLICurrentVersion},
{"只有空白等同未配置", " ", CLICurrentVersion},
{"合法覆盖生效", "2.1.251", "2.1.251"},
{"两侧空白被裁掉", " 2.1.251 ", "2.1.251"},
{"非法值回落基线", "not-a-version", CLICurrentVersion},
{"向下覆盖被拒", "2.0.0", CLICurrentVersion},
{"预发布后缀被拒(会毒化账号指纹)", "2.2.0-local", CLICurrentVersion},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := resolveCLIVersion(tc.raw); got != tc.want {
t.Fatalf("resolveCLIVersion(%q) = %q, want %q", tc.raw, got, tc.want)
}
})
}
}
// 伪装身份的自洽性:User-Agent 头里的版本号必须与 CLIVersion() 一致。
// 两者由不同代码路径写入同一个请求,不一致会被上游判为非正版客户端。
func TestDefaultHeadersUserAgentMatchesCLIVersion(t *testing.T) {
want := "claude-cli/" + CLIVersion() + " (external, cli)"
if got := DefaultHeaders["User-Agent"]; got != want {
t.Fatalf("DefaultHeaders[User-Agent] = %q, want %q", got, want)
}
}
// 没有配置覆盖时,CLIVersion() 必须等于内置基线——保证本 PR 对既有部署零行为变化。
func TestCLIVersionDefaultsToBuiltinPin(t *testing.T) {
if got := CLIVersion(); got != CLICurrentVersion {
t.Fatalf("CLIVersion() = %q, want built-in pin %q (测试进程未设置 %s)", got, CLICurrentVersion, CLIVersionEnv)
}
}