gateway_dateline_normalization_test.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. package service
  2. import (
  3. "context"
  4. "testing"
  5. "github.com/Wei-Shaw/sub2api/internal/config"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/anthropicfp"
  7. "github.com/stretchr/testify/require"
  8. )
  9. // TestGatewayClientDatelineNormalization_Scope covers the account/switch matrix
  10. // for the shouldNormalizeClientDateline gate: Anthropic OAuth/SetupToken pass
  11. // only when the switch is on; API-Key and non-Anthropic platforms are excluded
  12. // unconditionally.
  13. func TestGatewayClientDatelineNormalization_Scope(t *testing.T) {
  14. repo := &gatewayTTLSettingRepo{data: map[string]string{}}
  15. gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{})
  16. svc := &GatewayService{
  17. settingService: NewSettingService(repo, &config.Config{}),
  18. }
  19. ctx := context.Background()
  20. // Default (missing key): fallback in parseSettings/cache loader is true.
  21. require.True(t, svc.shouldNormalizeClientDateline(ctx, &Account{Platform: PlatformAnthropic, Type: AccountTypeOAuth}))
  22. require.True(t, svc.shouldNormalizeClientDateline(ctx, &Account{Platform: PlatformAnthropic, Type: AccountTypeSetupToken}))
  23. require.False(t, svc.shouldNormalizeClientDateline(ctx, &Account{Platform: PlatformAnthropic, Type: AccountTypeAPIKey}))
  24. require.False(t, svc.shouldNormalizeClientDateline(ctx, &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth}))
  25. // Switch off: no account qualifies.
  26. repo.data[SettingKeyEnableClientDatelineNormalization] = "false"
  27. gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{})
  28. require.False(t, svc.shouldNormalizeClientDateline(ctx, &Account{Platform: PlatformAnthropic, Type: AccountTypeOAuth}))
  29. require.False(t, svc.shouldNormalizeClientDateline(ctx, &Account{Platform: PlatformAnthropic, Type: AccountTypeSetupToken}))
  30. // Switch back on: OAuth qualifies again.
  31. repo.data[SettingKeyEnableClientDatelineNormalization] = "true"
  32. gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{})
  33. require.True(t, svc.shouldNormalizeClientDateline(ctx, &Account{Platform: PlatformAnthropic, Type: AccountTypeOAuth}))
  34. }
  35. // TestGatewayClientDatelineNormalization_HelperNoRewrite exercises the code
  36. // path used by Forward: the helper must return ok=false when the switch is
  37. // off, when the account is API-Key, when the account is nil, and when the
  38. // body carries no fingerprinted dateline. It must return ok=true and a
  39. // rewritten body when both the switch is on and the account is Anthropic
  40. // OAuth/SetupToken and a rewrite actually happened.
  41. func TestGatewayClientDatelineNormalization_HelperNoRewrite(t *testing.T) {
  42. repo := &gatewayTTLSettingRepo{data: map[string]string{
  43. SettingKeyEnableClientDatelineNormalization: "true",
  44. }}
  45. gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{})
  46. svc := &GatewayService{
  47. settingService: NewSettingService(repo, &config.Config{}),
  48. }
  49. ctx := context.Background()
  50. dirty := []byte(`{"messages":[{"role":"user","content":"<system-reminder>\nToday’s date is 2026/07/01.\n</system-reminder>"}]}`)
  51. clean := []byte(`{"messages":[{"role":"user","content":"just hello"}]}`)
  52. // API-Key account: never rewrites, even with dirty payload.
  53. next, ok := svc.normalizeClientDatelineIfEnabled(ctx, &Account{Platform: PlatformAnthropic, Type: AccountTypeAPIKey}, dirty)
  54. require.False(t, ok)
  55. require.Nil(t, next)
  56. // Nil account: safe no-op.
  57. next, ok = svc.normalizeClientDatelineIfEnabled(ctx, nil, dirty)
  58. require.False(t, ok)
  59. require.Nil(t, next)
  60. // OAuth account + clean body: no changes, ok=false.
  61. next, ok = svc.normalizeClientDatelineIfEnabled(ctx, &Account{Platform: PlatformAnthropic, Type: AccountTypeOAuth}, clean)
  62. require.False(t, ok)
  63. require.Nil(t, next)
  64. // OAuth account + dirty body: rewritten, ok=true.
  65. next, ok = svc.normalizeClientDatelineIfEnabled(ctx, &Account{Platform: PlatformAnthropic, Type: AccountTypeOAuth}, dirty)
  66. require.True(t, ok)
  67. require.NotNil(t, next)
  68. require.Contains(t, string(next), "Today's date is 2026-07-01.")
  69. require.NotContains(t, string(next), "2026/07/01")
  70. require.NotContains(t, string(next), "Today’s date is")
  71. // SetupToken account + dirty body: rewritten, ok=true.
  72. next, ok = svc.normalizeClientDatelineIfEnabled(ctx, &Account{Platform: PlatformAnthropic, Type: AccountTypeSetupToken}, dirty)
  73. require.True(t, ok)
  74. require.Contains(t, string(next), "Today's date is 2026-07-01.")
  75. // Switch off: even OAuth account is not rewritten.
  76. repo.data[SettingKeyEnableClientDatelineNormalization] = "false"
  77. gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{})
  78. next, ok = svc.normalizeClientDatelineIfEnabled(ctx, &Account{Platform: PlatformAnthropic, Type: AccountTypeOAuth}, dirty)
  79. require.False(t, ok)
  80. require.Nil(t, next)
  81. }
  82. // TestGatewayClientDatelineNormalization_LeavesUserProseUntouched double-checks
  83. // that the pure normalizer never touches content outside <system-reminder>
  84. // blocks. This is an integration guard between the switch-gated helper and
  85. // the pkg/anthropicfp scope contract, tripped by anyone who broadens scope.
  86. func TestGatewayClientDatelineNormalization_LeavesUserProseUntouched(t *testing.T) {
  87. repo := &gatewayTTLSettingRepo{data: map[string]string{
  88. SettingKeyEnableClientDatelineNormalization: "true",
  89. }}
  90. gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{})
  91. svc := &GatewayService{
  92. settingService: NewSettingService(repo, &config.Config{}),
  93. }
  94. ctx := context.Background()
  95. // User prose that happens to include a fingerprint-looking sentence
  96. // (outside <system-reminder>) must be preserved byte-for-byte.
  97. body := []byte(`{"messages":[{"role":"user","content":"I wrote: Today’s date is 2026/07/01. What do you think?"}]}`)
  98. next, ok := svc.normalizeClientDatelineIfEnabled(ctx, &Account{Platform: PlatformAnthropic, Type: AccountTypeOAuth}, body)
  99. require.False(t, ok, "must not rewrite user prose outside <system-reminder>")
  100. require.Nil(t, next)
  101. // Direct pure-fn check for redundancy.
  102. out, hits, changed := anthropicfp.NormalizeDateline(body)
  103. require.False(t, changed)
  104. require.Empty(t, hits)
  105. require.Equal(t, body, out)
  106. }