identity_service_version_floor_test.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. package service
  2. import (
  3. "context"
  4. "testing"
  5. "time"
  6. "github.com/stretchr/testify/require"
  7. "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
  8. )
  9. // floorClaudeCLIUserAgentVersion 单元测试:版本下限抬升的各种形态。
  10. func TestFloorClaudeCLIUserAgentVersion(t *testing.T) {
  11. floorUA := "claude-cli/" + claude.CLICurrentVersion
  12. cases := []struct {
  13. name string
  14. ua string
  15. want string
  16. wantChanged bool
  17. }{
  18. // 线上故障形态:历史版本低于 CLICurrentVersion,就地抬到下限。
  19. {"old_version_upgraded", "claude-cli/2.1.220 (external, cli)", floorUA + " (external, cli)", true},
  20. // 只替换版本号段,括号内的真实客户端形态原样保留。
  21. {"old_version_with_desktop_3p_suffix",
  22. "claude-cli/2.1.100 (external, claude-desktop-3p, agent-sdk/0.3.100)",
  23. floorUA + " (external, claude-desktop-3p, agent-sdk/0.3.100)", true},
  24. // 等于下限:不得改动。
  25. {"equal_to_floor", floorUA + " (external, cli)", floorUA + " (external, cli)", false},
  26. // 高于下限:只升不降,不得把客户端上报的更新版本压回去。
  27. {"newer_than_floor_not_downgraded", "claude-cli/2.9.0 (external, cli)", "claude-cli/2.9.0 (external, cli)", false},
  28. // 非 claude-cli 产品:一律不动。
  29. {"other_product_untouched", "opencode/1.2.3 (external, cli)", "opencode/1.2.3 (external, cli)", false},
  30. // 空串 / 畸形:一律不动。
  31. {"empty", "", "", false},
  32. {"no_version", "claude-cli (external, cli)", "claude-cli (external, cli)", false},
  33. {"unparseable_version", "claude-cli/abc (external, cli)", "claude-cli/abc (external, cli)", false},
  34. }
  35. for _, tc := range cases {
  36. t.Run(tc.name, func(t *testing.T) {
  37. got, changed := floorClaudeCLIUserAgentVersion(tc.ua)
  38. require.Equal(t, tc.want, got)
  39. require.Equal(t, tc.wantChanged, changed)
  40. })
  41. }
  42. }
  43. // GetOrCreateFingerprint 集成行为,直接对应线上故障:缓存指纹停留在历史版本
  44. // 2.1.220(生产 Redis 中账号 147 的实际值),客户端送来更旧的 2.1.75。
  45. // 修复前:isNewerVersion 不触发、UA 形态合法不触发自愈,旧指纹被原样返回并
  46. // 近乎永不过期——上游按指纹 UA 做客户端版本闸门(Fable 5.1 要求 >= 2.1.251),
  47. // 仅升 CLICurrentVersion 对存量账号完全无效。
  48. func TestGetOrCreateFingerprintFloorsStaleCachedUserAgent(t *testing.T) {
  49. cache := &stubIdentityCache{fingerprint: &Fingerprint{
  50. UserAgent: "claude-cli/2.1.220 (external, cli)",
  51. ClientID: "cid-1",
  52. StainlessPackageVersion: "0.91.1",
  53. UpdatedAt: time.Now().Unix(),
  54. }}
  55. svc := NewIdentityService(cache)
  56. fp, err := svc.GetOrCreateFingerprint(
  57. context.Background(), 147,
  58. headersWithUA("claude-cli/2.1.75 (external, cli)"),
  59. )
  60. require.NoError(t, err)
  61. require.Equal(t, "claude-cli/"+claude.CLICurrentVersion+" (external, cli)", fp.UserAgent)
  62. require.Equal(t, 1, cache.setCalls, "下限抬升必须持久化写回缓存")
  63. require.Equal(t, "claude-cli/"+claude.CLICurrentVersion+" (external, cli)", cache.lastSet.UserAgent)
  64. // X-Stainless-* 维持既有 merge 语义:客户端未携带时保留缓存中的真实值,不被下限逻辑覆盖。
  65. require.Equal(t, "0.91.1", fp.StainlessPackageVersion)
  66. }
  67. // 缓存版本高于下限:不得被降级,也不得触发多余写入。
  68. func TestGetOrCreateFingerprintDoesNotTouchCacheAboveFloor(t *testing.T) {
  69. ua := "claude-cli/2.9.0 (external, cli)"
  70. cache := &stubIdentityCache{fingerprint: &Fingerprint{
  71. UserAgent: ua,
  72. ClientID: "cid-1",
  73. StainlessPackageVersion: "0.91.1",
  74. UpdatedAt: time.Now().Unix(),
  75. }}
  76. svc := NewIdentityService(cache)
  77. fp, err := svc.GetOrCreateFingerprint(
  78. context.Background(), 1,
  79. headersWithUA("claude-cli/2.1.75 (external, cli)"),
  80. )
  81. require.NoError(t, err)
  82. require.Equal(t, ua, fp.UserAgent, "高于下限的缓存版本不得被降级")
  83. require.Zero(t, cache.setCalls)
  84. }
  85. // 客户端送来高于下限的更新版本:常规升级照常生效,且不被下限压回。
  86. func TestGetOrCreateFingerprintClientNewerThanFloorStillWins(t *testing.T) {
  87. newUA := "claude-cli/2.9.0 (external, cli)"
  88. cache := &stubIdentityCache{fingerprint: &Fingerprint{
  89. UserAgent: "claude-cli/2.1.220 (external, cli)",
  90. ClientID: "cid-1",
  91. UpdatedAt: time.Now().Unix(),
  92. }}
  93. svc := NewIdentityService(cache)
  94. fp, err := svc.GetOrCreateFingerprint(context.Background(), 1, headersWithUA(newUA))
  95. require.NoError(t, err)
  96. require.Equal(t, newUA, fp.UserAgent, "客户端更新版本优先于下限,不得被压回")
  97. require.Equal(t, 1, cache.setCalls)
  98. }
  99. // 客户端版本高于缓存但低于下限:merge 升级后仍被抬到下限(二者取更新者)。
  100. func TestGetOrCreateFingerprintFloorWinsOverStaleClientUpgrade(t *testing.T) {
  101. cache := &stubIdentityCache{fingerprint: &Fingerprint{
  102. UserAgent: "claude-cli/2.1.22 (external, cli)",
  103. ClientID: "cid-1",
  104. UpdatedAt: time.Now().Unix(),
  105. }}
  106. svc := NewIdentityService(cache)
  107. fp, err := svc.GetOrCreateFingerprint(
  108. context.Background(), 1,
  109. headersWithUA("claude-cli/2.1.223 (external, cli)"),
  110. )
  111. require.NoError(t, err)
  112. require.Equal(t, "claude-cli/"+claude.CLICurrentVersion+" (external, cli)", fp.UserAgent)
  113. require.Equal(t, 1, cache.setCalls)
  114. }
  115. // 首次创建路径同样受下限约束:合法但过旧的 claude-cli UA 落库时必须抬到 CLICurrentVersion。
  116. func TestCreateFingerprintFromHeadersFloorsOldClientUserAgent(t *testing.T) {
  117. cache := &stubIdentityCache{}
  118. svc := NewIdentityService(cache)
  119. fp, err := svc.GetOrCreateFingerprint(
  120. context.Background(), 1,
  121. headersWithUA("claude-cli/2.1.75 (external, claude-desktop-3p)"),
  122. )
  123. require.NoError(t, err)
  124. require.Equal(t, "claude-cli/"+claude.CLICurrentVersion+" (external, claude-desktop-3p)", fp.UserAgent)
  125. require.Equal(t, 1, cache.setCalls)
  126. }