mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-20 00:18:55 +00:00
fix(claude): 让伪装的 Claude Code 版本真正生效,修复 Fable 5.1 仍然 400
claude-fable-5-1 已登记进 DefaultModels,但 OAuth 账号实际调用仍然全部失败:
400 claude_code_version_too_old
"Claude Code 2.1.220 does not support this model;
version 2.1.251 or newer is required."
两个独立的原因,缺一不可:
1. CLICurrentVersion 停留在 2.1.220,低于上游对该模型的客户端版本闸门。
升到 2.1.258(当前官方 CLI 最新版,npm @anthropic-ai/claude-code)。
2. 只升常量对所有已有账号无效。上游请求实际发送的 User-Agent 来自账号级
持久指纹(IdentityService,Redis fingerprint:<accountID>),不是
DefaultHeaders;defaultFingerprint 只在首次创建指纹时使用。
GetOrCreateFingerprint 的缓存命中路径只有两条改写分支:UA 畸形时自愈,
或客户端送来更新版本时升级。指纹 7 天 TTL 但活跃账号每 24h 续期,实际
近乎永不过期,系统内也没有重置入口——客户端版本比缓存旧时(例如缓存
2.1.220、客户端 2.1.75),存量账号会永远对上游声称 2.1.220。
生产实测:升级 CLICurrentVersion 并部署后仍报同样的 400,读 Redis 确认
出问题账号的缓存值为 "claude-cli/2.1.220 (external, cli)"。
修复:新增 floorClaudeCLIUserAgentVersion,把 CLICurrentVersion 作为缓存
指纹里 claude-cli 版本的下限,缓存命中与首次创建两条路径共用。
- 只对 claude-cli 产品生效,其它客户端一律不动
- 只升不降:版本等于或高于下限(含客户端上报的更新版本)不做任何改动
- 只替换版本号段,UA 其余部分原样保留,例如
"(external, claude-desktop-3p, agent-sdk/0.3.100)" 不受影响
- 与既有的"客户端更新版本"升级分支相互独立、二者取更新者
- X-Stainless-* 维持既有 merge 语义,不被下限逻辑覆盖
cc_version 无需单独处理:billing attribution block 的版本已由
syncBillingHeaderVersion 跟随实际发送的 User-Agent 同步。
测试:identity_service_version_floor_test.go 覆盖 helper 的 8 种 UA 形态与
5 个 GetOrCreateFingerprint 场景,其中一个直接复现线上故障(缓存 2.1.220 +
客户端 2.1.75 → 写回 2.1.258,Stainless 字段保持不变)。
constants_cli_version_test.go 锁定常量与 DefaultHeaders 的 UA 同源,并断言
不低于 Fable 5.1 的闸门下限。回退实现后对应用例确认转红。
identity_service_user_agent_validation_test.go 中 3 个把版本钉死在旧字面量
的既有用例改为钉 CLICurrentVersion 或更高版本——它们断言的正是本次有意改变
的行为,各自的原始意图(哨兵拒绝、合法升级生效、健康缓存不重写)保持不变。
This commit is contained in:
@@ -80,7 +80,7 @@ const DefaultCacheControlTTL = "5m"
|
||||
//
|
||||
// ⚠️ 读取实际生效的版本号请用 CLIVersion(),它会叠加 SUB2API_CLAUDE_CLI_VERSION 覆盖。
|
||||
// 直接引用本常量只在"表达内置基线"时才正确(例如覆盖值的下限校验)。
|
||||
const CLICurrentVersion = "2.1.220"
|
||||
const CLICurrentVersion = "2.1.258"
|
||||
|
||||
// FullClaudeCodeMimicryBetas 返回最"像"真实 Claude Code CLI 的完整 beta 列表,
|
||||
// 用于 OAuth 账号伪装成 Claude Code 时使用。
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package claude
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// fable51MinCLIVersion 是上游对 claude-fable-5-1 的客户端版本闸门下限。
|
||||
// 低于该版本时上游直接 400:
|
||||
//
|
||||
// claude_code_version_too_old
|
||||
// "Claude Code <ver> does not support this model; version 2.1.251 or newer is required."
|
||||
const fable51MinCLIVersion = "2.1.251"
|
||||
|
||||
// TestCLICurrentVersionMatchesDefaultUserAgent 锁定"常量与 UA 同源"这条约束。
|
||||
// 两者不一致会被 Anthropic 判为第三方客户端,且只改一处是历史上的常见疏漏,
|
||||
// 所以这里从 UA 里反解版本号来比对,而不是并列两个字面量。
|
||||
func TestCLICurrentVersionMatchesDefaultUserAgent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ua := DefaultHeaders["User-Agent"]
|
||||
const prefix = "claude-cli/"
|
||||
require.True(t, strings.HasPrefix(ua, prefix), "unexpected User-Agent: %q", ua)
|
||||
|
||||
rest := strings.TrimPrefix(ua, prefix)
|
||||
version := rest
|
||||
if idx := strings.IndexByte(rest, ' '); idx >= 0 {
|
||||
version = rest[:idx]
|
||||
}
|
||||
require.Equal(t, CLICurrentVersion, version,
|
||||
"DefaultHeaders User-Agent version must match CLICurrentVersion")
|
||||
}
|
||||
|
||||
// TestCLICurrentVersionSatisfiesFable51Gate 锁定伪装版本不低于 claude-fable-5-1
|
||||
// 的闸门下限。DefaultModels 里登记了该模型却发着更旧的版本号,等于模型清单可见
|
||||
// 但一调就 400。
|
||||
func TestCLICurrentVersionSatisfiesFable51Gate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.GreaterOrEqual(t, compareSemver(t, CLICurrentVersion, fable51MinCLIVersion), 0,
|
||||
"CLICurrentVersion %s is below the claude-fable-5-1 gate %s",
|
||||
CLICurrentVersion, fable51MinCLIVersion)
|
||||
}
|
||||
|
||||
// compareSemver 比较两个三段 semver,返回 -1 / 0 / 1。
|
||||
func compareSemver(t *testing.T, a, b string) int {
|
||||
t.Helper()
|
||||
|
||||
as, bs := strings.Split(a, "."), strings.Split(b, ".")
|
||||
require.Len(t, as, 3, "not a three-part semver: %q", a)
|
||||
require.Len(t, bs, 3, "not a three-part semver: %q", b)
|
||||
|
||||
for i := range as {
|
||||
ai, err := strconv.Atoi(as[i])
|
||||
require.NoError(t, err, "non-numeric segment in %q", a)
|
||||
bi, err := strconv.Atoi(bs[i])
|
||||
require.NoError(t, err, "non-numeric segment in %q", b)
|
||||
|
||||
switch {
|
||||
case ai > bi:
|
||||
return 1
|
||||
case ai < bi:
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -28,6 +28,10 @@ var (
|
||||
// <product>/<major>.<minor>.<patch> 之后必须紧跟空白或字符串结束。
|
||||
// 版本号带 -local / -dev / +build 等后缀的本地构建一律不接受。
|
||||
fingerprintUserAgentPattern = regexp.MustCompile(`^[A-Za-z0-9._-]+/\d+\.\d+\.\d+(\s|$)`)
|
||||
|
||||
// claudeCLIUAVersionPrefixRegex 匹配 claude-cli UA 开头的 "claude-cli/x.y.z" 版本号段,
|
||||
// 供版本下限抬升时就地替换版本号使用;UA 其余部分(括号内的真实客户端形态等)原样保留。
|
||||
claudeCLIUAVersionPrefixRegex = regexp.MustCompile(`(?i)^(claude-cli)/\d+\.\d+\.\d+`)
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -75,6 +79,38 @@ func isAcceptableFingerprintUserAgent(ua string) bool {
|
||||
return major <= currentMajor+maxClaudeCLIMajorVersionSkew
|
||||
}
|
||||
|
||||
// floorClaudeCLIUserAgentVersion 把 claude-cli UA 中的版本号抬升到 claude.CLICurrentVersion
|
||||
// 下限:低于下限时就地升到下限并返回 changed=true,否则原样返回。
|
||||
//
|
||||
// 为什么需要这条:账号级指纹"只升不降"、活跃账号懒续期后近乎永不过期,且系统内没有重置
|
||||
// 入口,defaultFingerprint 只在首次创建指纹时使用。存量账号缓存里的 claude-cli 版本停留在
|
||||
// 历史值(如 claude-cli/2.1.220),客户端送来更旧的版本时 isNewerVersion 不会触发升级——
|
||||
// 仅升 CLICurrentVersion 常量对所有已有账号完全无效,上游按指纹 UA 做客户端版本闸门
|
||||
// (如 Fable 5.1 要求 >= 2.1.251)时旧指纹永远过不去。
|
||||
//
|
||||
// 约束:
|
||||
// - 只对 claude-cli 产品生效,其它产品一律不动,避免误伤别的合法客户端;
|
||||
// - 只升不降:版本等于或高于 CLICurrentVersion(含客户端上报的更新版本)时不做任何改动;
|
||||
// - 只替换 claude-cli/ 后的版本号段,UA 其余部分(如 "(external, claude-desktop-3p,
|
||||
// agent-sdk/0.3.100)")原样保留,不重建整个字符串、不退化为 defaultFingerprint.UserAgent;
|
||||
// - X-Stainless-* 字段不在此处理,维持调用方的既有 merge 语义。
|
||||
func floorClaudeCLIUserAgentVersion(ua string) (string, bool) {
|
||||
if extractProduct(ua) != claudeCLIUserAgentProduct {
|
||||
return ua, false
|
||||
}
|
||||
floorUA := claudeCLIUserAgentProduct + "/" + claude.CLICurrentVersion
|
||||
// isNewerVersion(floor, ua) 为 true 当且仅当下限版本严格高于 ua:
|
||||
// ua 等于或高于下限、产品名不一致、或版本无法解析时都不做改动。
|
||||
if !isNewerVersion(floorUA, ua) {
|
||||
return ua, false
|
||||
}
|
||||
floored := claudeCLIUAVersionPrefixRegex.ReplaceAllString(ua, "${1}/"+claude.CLICurrentVersion)
|
||||
if floored == ua {
|
||||
return ua, false
|
||||
}
|
||||
return floored, true
|
||||
}
|
||||
|
||||
// 默认指纹值(当客户端未提供时使用)
|
||||
var defaultFingerprint = Fingerprint{
|
||||
UserAgent: "claude-cli/" + claude.CLIVersion() + " (external, cli)",
|
||||
@@ -157,12 +193,25 @@ func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID
|
||||
logger.LegacyPrintf("service.identity",
|
||||
"Replaced malformed cached fingerprint for account %d: %q -> %q",
|
||||
accountID, poisoned, cached.UserAgent)
|
||||
} else if uaAcceptable && isNewerVersion(clientUA, cached.UserAgent) {
|
||||
// 版本升级:merge 语义 — 仅更新请求中实际携带的字段,保留缓存值
|
||||
// 避免缺失的头被硬编码默认值覆盖(如新 CLI 版本 + 旧 SDK 默认值的不一致)
|
||||
mergeHeadersIntoFingerprint(cached, headers)
|
||||
needWrite = true
|
||||
logger.LegacyPrintf("service.identity", "Updated fingerprint for account %d: %s (merge update)", accountID, clientUA)
|
||||
} else {
|
||||
// 客户端送来更新版本时的常规升级:merge 语义 — 仅更新请求中实际携带的字段,
|
||||
// 保留缓存值,避免缺失的头被硬编码默认值覆盖(如新 CLI 版本 + 旧 SDK 默认值的不一致)
|
||||
if uaAcceptable && isNewerVersion(clientUA, cached.UserAgent) {
|
||||
mergeHeadersIntoFingerprint(cached, headers)
|
||||
needWrite = true
|
||||
logger.LegacyPrintf("service.identity", "Updated fingerprint for account %d: %s (merge update)", accountID, clientUA)
|
||||
}
|
||||
|
||||
// 版本下限抬升(floor):与上面的客户端升级相互独立、二者取更新者。客户端送来
|
||||
// 更旧版本时 isNewerVersion 不触发,此处仍能把低于 CLICurrentVersion 的存量指纹
|
||||
// 就地抬到下限并持久化,否则升 CLICurrentVersion 对所有已有账号无效,新模型的
|
||||
// 客户端版本闸门(如 Fable 5.1 要求 >= 2.1.251)永远过不去。
|
||||
if flooredUA, changed := floorClaudeCLIUserAgentVersion(cached.UserAgent); changed {
|
||||
cached.UserAgent = flooredUA
|
||||
needWrite = true
|
||||
logger.LegacyPrintf("service.identity",
|
||||
"Floored cached fingerprint claude-cli version for account %d: %s", accountID, flooredUA)
|
||||
}
|
||||
}
|
||||
|
||||
if !needWrite && time.Since(time.Unix(cached.UpdatedAt, 0)) > 24*time.Hour {
|
||||
@@ -208,7 +257,9 @@ func (s *IdentityService) createFingerprintFromHeaders(headers http.Header) *Fin
|
||||
// 获取User-Agent:只接受形态合法且版本合理的值,否则回退默认指纹。
|
||||
// 首次创建同样是持久化写入,必须与升级路径共用同一套校验。
|
||||
if ua := strings.TrimSpace(headers.Get("User-Agent")); isAcceptableFingerprintUserAgent(ua) {
|
||||
fp.UserAgent = ua
|
||||
// 首次创建与缓存命中路径共用同一个版本下限:合法但过旧的 claude-cli UA
|
||||
// 落库时同样不能低于 CLICurrentVersion,否则新账号一开始就带着过旧的持久身份。
|
||||
fp.UserAgent, _ = floorClaudeCLIUserAgentVersion(ua)
|
||||
} else {
|
||||
fp.UserAgent = defaultFingerprint.UserAgent
|
||||
}
|
||||
|
||||
@@ -105,8 +105,9 @@ func TestGetOrCreateFingerprintRejectsMalformedUserAgentOnCreate(t *testing.T) {
|
||||
// 升级路径:哨兵版本号不得覆盖已缓存的真实指纹。
|
||||
// isNewerVersion 是纯数值比较,999.0.0 恒大于任何真实版本,一旦写入永远无法夺回。
|
||||
func TestGetOrCreateFingerprintRejectsSentinelVersionOnUpgrade(t *testing.T) {
|
||||
// 缓存版本取 CLICurrentVersion(>= 下限),使本用例只验证哨兵拒绝,不与版本下限抬升耦合。
|
||||
cached := &Fingerprint{
|
||||
UserAgent: "claude-cli/2.1.22 (external, cli)",
|
||||
UserAgent: "claude-cli/" + claude.CLICurrentVersion + " (external, cli)",
|
||||
ClientID: "cid-1",
|
||||
UpdatedAt: time.Now().Unix(),
|
||||
}
|
||||
@@ -119,7 +120,7 @@ func TestGetOrCreateFingerprintRejectsSentinelVersionOnUpgrade(t *testing.T) {
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "claude-cli/2.1.22 (external, cli)", fp.UserAgent,
|
||||
require.Equal(t, "claude-cli/"+claude.CLICurrentVersion+" (external, cli)", fp.UserAgent,
|
||||
"真实指纹不得被哨兵版本覆盖")
|
||||
require.Zero(t, cache.setCalls, "被拒的 UA 不应触发任何写入")
|
||||
}
|
||||
@@ -133,7 +134,9 @@ func TestGetOrCreateFingerprintStillUpgradesOnValidNewerVersion(t *testing.T) {
|
||||
}}
|
||||
svc := NewIdentityService(cache)
|
||||
|
||||
newUA := "claude-cli/2.1.223 (external, cli)"
|
||||
// 客户端版本取高于 CLICurrentVersion 的合法值,使本用例只验证常规升级,
|
||||
// 不与版本下限抬升耦合(低于下限的客户端升级由 version_floor 测试覆盖)。
|
||||
newUA := "claude-cli/2.9.0 (external, cli)"
|
||||
fp, err := svc.GetOrCreateFingerprint(context.Background(), 1, headersWithUA(newUA))
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -203,9 +206,10 @@ func TestGetOrCreateFingerprintHealsPoisonedCacheWithoutValidClientUA(t *testing
|
||||
}
|
||||
|
||||
// 自愈只针对畸形缓存:合法缓存 + 非更新版本的合法 UA 不得触发额外写入。
|
||||
// 缓存版本取 CLICurrentVersion(>= 下限),低于下限的缓存抬升由 version_floor 测试覆盖。
|
||||
func TestGetOrCreateFingerprintDoesNotRewriteHealthyCache(t *testing.T) {
|
||||
cache := &stubIdentityCache{fingerprint: &Fingerprint{
|
||||
UserAgent: "claude-cli/2.1.220 (external, cli)",
|
||||
UserAgent: "claude-cli/" + claude.CLICurrentVersion + " (external, cli)",
|
||||
ClientID: "cid-1",
|
||||
UpdatedAt: time.Now().Unix(),
|
||||
}}
|
||||
@@ -217,7 +221,7 @@ func TestGetOrCreateFingerprintDoesNotRewriteHealthyCache(t *testing.T) {
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "claude-cli/2.1.220 (external, cli)", fp.UserAgent)
|
||||
require.Equal(t, "claude-cli/"+claude.CLICurrentVersion+" (external, cli)", fp.UserAgent)
|
||||
require.Zero(t, cache.setCalls)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||
)
|
||||
|
||||
// floorClaudeCLIUserAgentVersion 单元测试:版本下限抬升的各种形态。
|
||||
func TestFloorClaudeCLIUserAgentVersion(t *testing.T) {
|
||||
floorUA := "claude-cli/" + claude.CLICurrentVersion
|
||||
cases := []struct {
|
||||
name string
|
||||
ua string
|
||||
want string
|
||||
wantChanged bool
|
||||
}{
|
||||
// 线上故障形态:历史版本低于 CLICurrentVersion,就地抬到下限。
|
||||
{"old_version_upgraded", "claude-cli/2.1.220 (external, cli)", floorUA + " (external, cli)", true},
|
||||
// 只替换版本号段,括号内的真实客户端形态原样保留。
|
||||
{"old_version_with_desktop_3p_suffix",
|
||||
"claude-cli/2.1.100 (external, claude-desktop-3p, agent-sdk/0.3.100)",
|
||||
floorUA + " (external, claude-desktop-3p, agent-sdk/0.3.100)", true},
|
||||
// 等于下限:不得改动。
|
||||
{"equal_to_floor", floorUA + " (external, cli)", floorUA + " (external, cli)", false},
|
||||
// 高于下限:只升不降,不得把客户端上报的更新版本压回去。
|
||||
{"newer_than_floor_not_downgraded", "claude-cli/2.9.0 (external, cli)", "claude-cli/2.9.0 (external, cli)", false},
|
||||
// 非 claude-cli 产品:一律不动。
|
||||
{"other_product_untouched", "opencode/1.2.3 (external, cli)", "opencode/1.2.3 (external, cli)", false},
|
||||
// 空串 / 畸形:一律不动。
|
||||
{"empty", "", "", false},
|
||||
{"no_version", "claude-cli (external, cli)", "claude-cli (external, cli)", false},
|
||||
{"unparseable_version", "claude-cli/abc (external, cli)", "claude-cli/abc (external, cli)", false},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, changed := floorClaudeCLIUserAgentVersion(tc.ua)
|
||||
require.Equal(t, tc.want, got)
|
||||
require.Equal(t, tc.wantChanged, changed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetOrCreateFingerprint 集成行为,直接对应线上故障:缓存指纹停留在历史版本
|
||||
// 2.1.220(生产 Redis 中账号 147 的实际值),客户端送来更旧的 2.1.75。
|
||||
// 修复前:isNewerVersion 不触发、UA 形态合法不触发自愈,旧指纹被原样返回并
|
||||
// 近乎永不过期——上游按指纹 UA 做客户端版本闸门(Fable 5.1 要求 >= 2.1.251),
|
||||
// 仅升 CLICurrentVersion 对存量账号完全无效。
|
||||
func TestGetOrCreateFingerprintFloorsStaleCachedUserAgent(t *testing.T) {
|
||||
cache := &stubIdentityCache{fingerprint: &Fingerprint{
|
||||
UserAgent: "claude-cli/2.1.220 (external, cli)",
|
||||
ClientID: "cid-1",
|
||||
StainlessPackageVersion: "0.91.1",
|
||||
UpdatedAt: time.Now().Unix(),
|
||||
}}
|
||||
svc := NewIdentityService(cache)
|
||||
|
||||
fp, err := svc.GetOrCreateFingerprint(
|
||||
context.Background(), 147,
|
||||
headersWithUA("claude-cli/2.1.75 (external, cli)"),
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "claude-cli/"+claude.CLICurrentVersion+" (external, cli)", fp.UserAgent)
|
||||
require.Equal(t, 1, cache.setCalls, "下限抬升必须持久化写回缓存")
|
||||
require.Equal(t, "claude-cli/"+claude.CLICurrentVersion+" (external, cli)", cache.lastSet.UserAgent)
|
||||
// X-Stainless-* 维持既有 merge 语义:客户端未携带时保留缓存中的真实值,不被下限逻辑覆盖。
|
||||
require.Equal(t, "0.91.1", fp.StainlessPackageVersion)
|
||||
}
|
||||
|
||||
// 缓存版本高于下限:不得被降级,也不得触发多余写入。
|
||||
func TestGetOrCreateFingerprintDoesNotTouchCacheAboveFloor(t *testing.T) {
|
||||
ua := "claude-cli/2.9.0 (external, cli)"
|
||||
cache := &stubIdentityCache{fingerprint: &Fingerprint{
|
||||
UserAgent: ua,
|
||||
ClientID: "cid-1",
|
||||
StainlessPackageVersion: "0.91.1",
|
||||
UpdatedAt: time.Now().Unix(),
|
||||
}}
|
||||
svc := NewIdentityService(cache)
|
||||
|
||||
fp, err := svc.GetOrCreateFingerprint(
|
||||
context.Background(), 1,
|
||||
headersWithUA("claude-cli/2.1.75 (external, cli)"),
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ua, fp.UserAgent, "高于下限的缓存版本不得被降级")
|
||||
require.Zero(t, cache.setCalls)
|
||||
}
|
||||
|
||||
// 客户端送来高于下限的更新版本:常规升级照常生效,且不被下限压回。
|
||||
func TestGetOrCreateFingerprintClientNewerThanFloorStillWins(t *testing.T) {
|
||||
newUA := "claude-cli/2.9.0 (external, cli)"
|
||||
cache := &stubIdentityCache{fingerprint: &Fingerprint{
|
||||
UserAgent: "claude-cli/2.1.220 (external, cli)",
|
||||
ClientID: "cid-1",
|
||||
UpdatedAt: time.Now().Unix(),
|
||||
}}
|
||||
svc := NewIdentityService(cache)
|
||||
|
||||
fp, err := svc.GetOrCreateFingerprint(context.Background(), 1, headersWithUA(newUA))
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newUA, fp.UserAgent, "客户端更新版本优先于下限,不得被压回")
|
||||
require.Equal(t, 1, cache.setCalls)
|
||||
}
|
||||
|
||||
// 客户端版本高于缓存但低于下限:merge 升级后仍被抬到下限(二者取更新者)。
|
||||
func TestGetOrCreateFingerprintFloorWinsOverStaleClientUpgrade(t *testing.T) {
|
||||
cache := &stubIdentityCache{fingerprint: &Fingerprint{
|
||||
UserAgent: "claude-cli/2.1.22 (external, cli)",
|
||||
ClientID: "cid-1",
|
||||
UpdatedAt: time.Now().Unix(),
|
||||
}}
|
||||
svc := NewIdentityService(cache)
|
||||
|
||||
fp, err := svc.GetOrCreateFingerprint(
|
||||
context.Background(), 1,
|
||||
headersWithUA("claude-cli/2.1.223 (external, cli)"),
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "claude-cli/"+claude.CLICurrentVersion+" (external, cli)", fp.UserAgent)
|
||||
require.Equal(t, 1, cache.setCalls)
|
||||
}
|
||||
|
||||
// 首次创建路径同样受下限约束:合法但过旧的 claude-cli UA 落库时必须抬到 CLICurrentVersion。
|
||||
func TestCreateFingerprintFromHeadersFloorsOldClientUserAgent(t *testing.T) {
|
||||
cache := &stubIdentityCache{}
|
||||
svc := NewIdentityService(cache)
|
||||
|
||||
fp, err := svc.GetOrCreateFingerprint(
|
||||
context.Background(), 1,
|
||||
headersWithUA("claude-cli/2.1.75 (external, claude-desktop-3p)"),
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "claude-cli/"+claude.CLICurrentVersion+" (external, claude-desktop-3p)", fp.UserAgent)
|
||||
require.Equal(t, 1, cache.setCalls)
|
||||
}
|
||||
Reference in New Issue
Block a user