mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Merge pull request #6481 from alfadb/fix/claude-cli-version-floor
fix(claude): 让伪装的 Claude Code 版本真正生效,修复 Fable 5.1 仍然 400
This commit is contained in:
@@ -9,7 +9,7 @@ func TestIsSupportedCLIVersion(t *testing.T) {
|
||||
want bool
|
||||
}{
|
||||
{"内置基线本身", CLICurrentVersion, true},
|
||||
{"高于基线的补丁位", "2.1.251", true},
|
||||
{"高于基线的补丁位", "2.1.259", true},
|
||||
{"高于基线的次版本", "2.2.0", true},
|
||||
{"高于基线的主版本", "3.0.0", true},
|
||||
{"低于基线", "2.1.219", false},
|
||||
@@ -41,8 +41,8 @@ func TestResolveCLIVersion(t *testing.T) {
|
||||
}{
|
||||
{"未配置时用内置基线", "", CLICurrentVersion},
|
||||
{"只有空白等同未配置", " ", CLICurrentVersion},
|
||||
{"合法覆盖生效", "2.1.251", "2.1.251"},
|
||||
{"两侧空白被裁掉", " 2.1.251 ", "2.1.251"},
|
||||
{"合法覆盖生效", "2.1.259", "2.1.259"},
|
||||
{"两侧空白被裁掉", " 2.1.259 ", "2.1.259"},
|
||||
{"非法值回落基线", "not-a-version", CLICurrentVersion},
|
||||
{"向下覆盖被拒", "2.0.0", CLICurrentVersion},
|
||||
{"预发布后缀被拒(会毒化账号指纹)", "2.2.0-local", CLICurrentVersion},
|
||||
|
||||
@@ -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,29 @@ 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)
|
||||
} 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):heal 与 healthy 两条缓存命中路径共同的后置条件,与
|
||||
// 上面的客户端常规升级相互独立、二者取更新者。客户端送来更旧版本时
|
||||
// isNewerVersion 不触发,此处仍能把低于 CLICurrentVersion 的存量指纹就地抬到
|
||||
// 下限并持久化,否则升 CLICurrentVersion 对所有已有账号无效,新模型的客户端
|
||||
// 版本闸门(如 Fable 5.1 要求 >= 2.1.251)永远过不去。自愈路径同样必须经过:
|
||||
// 畸形缓存 + 合法但过旧的客户端 UA 首次读取就要落到下限,不能先落库一个
|
||||
// 过旧版本、等下一次读取才纠正。
|
||||
if flooredUA, changed := floorClaudeCLIUserAgentVersion(cached.UserAgent); changed {
|
||||
cached.UserAgent = flooredUA
|
||||
needWrite = true
|
||||
logger.LegacyPrintf("service.identity", "Updated fingerprint for account %d: %s (merge update)", accountID, clientUA)
|
||||
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 +261,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)
|
||||
@@ -176,8 +179,10 @@ func TestGetOrCreateFingerprintHealsPoisonedCacheUsingValidClientUA(t *testing.T
|
||||
fp, err := svc.GetOrCreateFingerprint(context.Background(), 1, headersWithUA(realUA))
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, realUA, fp.UserAgent,
|
||||
"真实客户端必须能从被毒化的指纹手中夺回账号身份")
|
||||
// 真实客户端从被毒化的指纹手中夺回账号身份,但版本下限在自愈的同一读取内生效:
|
||||
// 合法而过旧的 UA 不能先以过旧版本落库、等下次读取才被抬升。
|
||||
require.Equal(t, "claude-cli/"+claude.CLICurrentVersion+" (external, cli)", fp.UserAgent)
|
||||
require.Equal(t, fp.UserAgent, cache.lastSet.UserAgent, "自愈与下限抬升应合并为同一次落库")
|
||||
require.Equal(t, 1, cache.setCalls)
|
||||
require.NotContains(t, cache.lastSet.UserAgent, "999.0.0")
|
||||
require.Equal(t, "cid-1", fp.ClientID, "自愈不应重置 ClientID")
|
||||
@@ -203,9 +208,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 +223,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