account_mode1_v3_test.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. package service
  2. import (
  3. "bytes"
  4. "context"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "net/http/httptest"
  9. "strings"
  10. "testing"
  11. "github.com/Wei-Shaw/sub2api/internal/config"
  12. "github.com/gin-gonic/gin"
  13. "github.com/stretchr/testify/require"
  14. )
  15. func TestMode1V3MigrationPreservesOriginalSnapshotAndIdentity(t *testing.T) {
  16. a := mode1TestAccount(1)
  17. prev := map[string]any{"concurrency": 32, "codex_fingerprint_mode": "session", "enable_tls_fingerprint": true, "tls_fingerprint_builtin": "nodejs22", "tls_fingerprint_profile_id": 9}
  18. mode1Marker(a)["prev"] = prev
  19. a.Concurrency = 99 // identity migration preserves the editable account ceiling
  20. seed, _ := codexFingerprintSeed(a.Extra)
  21. repo := &upstreamBillingProbeAccountRepo{accounts: map[int64]*Account{1: a}}
  22. svc := NewAntiDegradeService(&adminServiceImpl{accountRepo: repo})
  23. p, err := svc.PreviewMode(context.Background(), 1, AntiDegradeMode1)
  24. require.NoError(t, err)
  25. require.True(t, p.Eligible)
  26. require.Equal(t, 2, p.PolicyVersion)
  27. require.Contains(t, p.Reason, "升级")
  28. updated, err := svc.ApplyAntiDegradeMode(context.Background(), 1, AntiDegradeMode1)
  29. require.NoError(t, err)
  30. require.Equal(t, 3, mode1Int(mode1Marker(updated)["policy_version"]))
  31. require.Equal(t, prev, antiDegradePrev(updated))
  32. require.Equal(t, seed, requireValidCodexFingerprintSeed(t, updated.Extra))
  33. require.False(t, updated.IsTLSFingerprintEnabled())
  34. require.Equal(t, 99, updated.Mode1EffectiveConcurrency())
  35. profile, err := resolveMode1TLSProfile(updated)
  36. require.NoError(t, err)
  37. require.Nil(t, profile)
  38. again, err := svc.ApplyAntiDegradeMode(context.Background(), 1, AntiDegradeMode1)
  39. require.NoError(t, err)
  40. require.Equal(t, prev, antiDegradePrev(again))
  41. restored, err := svc.Revert(context.Background(), 1)
  42. require.NoError(t, err)
  43. require.Equal(t, 99, restored.Concurrency)
  44. require.Equal(t, "session", restored.Extra["codex_fingerprint_mode"])
  45. require.Equal(t, "nodejs22", restored.Extra["tls_fingerprint_builtin"])
  46. require.Equal(t, 9, restored.Extra["tls_fingerprint_profile_id"])
  47. require.Equal(t, seed, requireValidCodexFingerprintSeed(t, restored.Extra))
  48. require.False(t, antiDegradeEnabled(restored))
  49. }
  50. func TestMode1InvalidVersionCanBeExplicitlyRestored(t *testing.T) {
  51. a := mode1TestAccount(1)
  52. mode1Marker(a)["policy_version"] = 999
  53. mode1Marker(a)["prev"] = map[string]any{"concurrency": 7, "codex_fingerprint_mode": "off"}
  54. repo := &upstreamBillingProbeAccountRepo{accounts: map[int64]*Account{1: a}}
  55. svc := NewAntiDegradeService(&adminServiceImpl{accountRepo: repo})
  56. _, err := resolveMode1TLSProfile(a)
  57. require.ErrorContains(t, err, "版本")
  58. p, err := svc.PreviewMode(context.Background(), 1, AntiDegradeMode1)
  59. require.NoError(t, err)
  60. require.False(t, p.Eligible)
  61. restored, err := svc.Revert(context.Background(), 1)
  62. require.NoError(t, err)
  63. require.False(t, antiDegradeEnabled(restored))
  64. require.Equal(t, 4, restored.Concurrency)
  65. }
  66. func TestMode1DamagedModeDoesNotSilentlyDisableProtection(t *testing.T) {
  67. for _, mode := range []any{nil, "unknown"} {
  68. a := mode1TestAccount(1)
  69. mode1Marker(a)["mode"] = mode
  70. _, err := resolveMode1TLSProfile(a)
  71. require.ErrorContains(t, err, "标记")
  72. }
  73. }
  74. func TestMode1V3StandardTransportWithTLSGloballyDisabled(t *testing.T) {
  75. a := mode1TestAccount(1)
  76. mode1Marker(a)["policy_version"] = 3
  77. a.Concurrency = 99
  78. u := &mode1HTTPRecorder{}
  79. // Installing/configuring a manager is not an unsupported-transport error.
  80. manager := &PluginManager{}
  81. gw := &OpenAIGatewayService{cfg: &config.Config{}, httpUpstream: u, pluginManager: manager}
  82. req := httptest.NewRequest(http.MethodPost, "https://localhost/responses", nil)
  83. r, err := gw.doOpenAIUpstream(req, "", a)
  84. require.NoError(t, err)
  85. r.Body.Close()
  86. tester := &AccountTestService{cfg: &config.Config{}, httpUpstream: u, pluginManager: manager}
  87. r, err = tester.doOpenAIAccountTestUpstream(req, "", a, true)
  88. require.NoError(t, err)
  89. r.Body.Close()
  90. require.Equal(t, 2, u.normal)
  91. require.Zero(t, u.tls)
  92. require.Equal(t, 99, u.cap)
  93. }
  94. // These are the actual request shapes that failed before any upstream dial in
  95. // v2, plus multi-turn/tool/reasoning variants. Exercise the real Forward path.
  96. func TestMode1ForwardRegressionV2AndV3(t *testing.T) {
  97. bodies := []string{
  98. `{"model":"gpt-5.2","input":"Return OK","stream":true}`,
  99. `{"model":"gpt-5.2","input":[{"role":"user","content":[{"type":"input_text","text":"Return OK"}]}],"max_output_tokens":100,"stream":true}`,
  100. `{"model":"gpt-5.4-high","input":"Return OK","reasoning":{"effort":"minimal"},"stream":true}`,
  101. `{"model":"gpt-5.2","input":[{"role":"system","content":"Keep context"},{"role":"user","content":"one"},{"role":"assistant","content":"two"},{"type":"reasoning","id":"rs_old","encrypted_content":"opaque"},{"role":"user","content":"continue"}],"reasoning":{"effort":"high"},"stream":true}`,
  102. `{"model":"gpt-5.2","input":[{"type":"function_call","call_id":"call_123","name":"lookup","arguments":"{}"},{"type":"function_call_output","call_id":"call_123","output":"answer"}],"tools":[{"type":"function","function":{"name":"lookup","parameters":{"type":"object"}}}],"stream":true}`,
  103. }
  104. for _, version := range []int{2, 3} {
  105. for _, passthrough := range []bool{false, true} {
  106. for index, raw := range bodies {
  107. t.Run(fmt.Sprintf("v%d/passthrough=%t/body=%d", version, passthrough, index), func(t *testing.T) {
  108. gin.SetMode(gin.TestMode)
  109. c, _ := gin.CreateTestContext(httptest.NewRecorder())
  110. body := []byte(raw)
  111. c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(body))
  112. c.Request.Header.Set("User-Agent", "local-test/1.0")
  113. c.Request.Header.Set("session-id", "regression-session")
  114. u := &mode1ForwardRecorder{httpUpstreamRecorder: httpUpstreamRecorder{resp: &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": {"text/event-stream"}}, Body: io.NopCloser(strings.NewReader("data: {\"type\":\"response.completed\",\"response\":{\"id\":\"test\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"OK\"}]}]}}\n\ndata: [DONE]\n\n"))}}}
  115. a := mode1TestAccount(1)
  116. mode1Marker(a)["policy_version"] = version
  117. a.Schedulable = true
  118. a.Credentials = map[string]any{"access_token": "local-dummy", "chatgpt_account_id": "dummy"}
  119. a.Extra["openai_passthrough"] = passthrough
  120. gw := &OpenAIGatewayService{cfg: &config.Config{}, httpUpstream: u, toolCorrector: NewCodexToolCorrector()}
  121. _, err := gw.Forward(context.Background(), c, a, body)
  122. require.NoError(t, err)
  123. require.Equal(t, 1, u.ordinary, "must reach the proven standard upstream transport")
  124. require.NoError(t, validateMode1RequestIntegrityForAccount(a, body, u.lastBody))
  125. })
  126. }
  127. }
  128. }
  129. }