intelligent_test_runner_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "net/http/httptest"
  9. "net/url"
  10. "strings"
  11. "testing"
  12. "time"
  13. "github.com/Wei-Shaw/sub2api/internal/config"
  14. "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
  15. "github.com/gin-gonic/gin"
  16. "github.com/stretchr/testify/require"
  17. )
  18. type intelligentRunnerRepo struct {
  19. AccountRepository
  20. account *Account
  21. writes int
  22. }
  23. func (r *intelligentRunnerRepo) GetByID(context.Context, int64) (*Account, error) {
  24. return r.account, nil
  25. }
  26. func (r *intelligentRunnerRepo) SetError(context.Context, int64, string) error {
  27. r.writes++
  28. return nil
  29. }
  30. func (r *intelligentRunnerRepo) UpdateExtra(context.Context, int64, map[string]any) error {
  31. r.writes++
  32. return nil
  33. }
  34. func (r *intelligentRunnerRepo) SetRateLimited(context.Context, int64, time.Time) error {
  35. r.writes++
  36. return nil
  37. }
  38. type intelligentNetworkUpstream struct{ client *http.Client }
  39. func (h intelligentNetworkUpstream) Do(req *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
  40. return h.client.Do(req)
  41. }
  42. func (h intelligentNetworkUpstream) DoWithTLS(req *http.Request, p string, id int64, n int, _ *tlsfingerprint.Profile) (*http.Response, error) {
  43. return h.Do(req, p, id, n)
  44. }
  45. func intelligentRunnerFixture(t *testing.T, upstream http.HandlerFunc) (*AccountTestService, *intelligentRunnerRepo) {
  46. t.Helper()
  47. server := httptest.NewServer(upstream)
  48. t.Cleanup(server.Close)
  49. repo := &intelligentRunnerRepo{account: &Account{ID: 81, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Concurrency: 1, Credentials: map[string]any{"api_key": "sk-private-test-secret-abc123", "base_url": server.URL}, Extra: map[string]any{"anti_degradation": true}}}
  50. cfg := &config.Config{}
  51. cfg.Security.URLAllowlist.AllowInsecureHTTP = true
  52. svc := NewAccountTestService(repo, nil, nil, nil, nil, intelligentNetworkUpstream{client: server.Client()}, cfg, nil)
  53. return svc, repo
  54. }
  55. func TestResolveIntelligentTestModelCodexFallback(t *testing.T) {
  56. codex := &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth}
  57. require.Equal(t, "gpt-5.3-codex", resolveIntelligentTestModel(codex, ""))
  58. for _, configured := range []string{"gpt-5.4", "GPT-5.4", "gpt-5.4-mini"} {
  59. require.Equal(t, configured, resolveIntelligentTestModel(codex, configured))
  60. }
  61. require.Equal(t, "gpt-5.3-codex", resolveIntelligentTestModel(codex, "gpt-5.3-codex"))
  62. apiKey := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
  63. require.Equal(t, "gpt-5.4", resolveIntelligentTestModel(apiKey, "gpt-5.4"))
  64. }
  65. func TestIntelligentRunnerActualHTTPPromptOutputAndRaw(t *testing.T) {
  66. gin.SetMode(gin.TestMode)
  67. var received map[string]any
  68. svc, repo := intelligentRunnerFixture(t, func(w http.ResponseWriter, r *http.Request) {
  69. require.Equal(t, "/v1/responses", r.URL.Path)
  70. require.Equal(t, "Bearer sk-private-test-secret-abc123", r.Header.Get("Authorization"))
  71. require.NoError(t, json.NewDecoder(r.Body).Decode(&received))
  72. w.Header().Set("Content-Type", "text/event-stream")
  73. fmt.Fprint(w, "data: {\"type\":\"response.output_text.delta\",\"delta\":\"24 - 6 = 18; 18 / 2 + 3 = 12.\\nANSWER: 12\"}\n\ndata: {\"type\":\"response.completed\"}\n\n")
  74. })
  75. record := &IntelligentTestRecord{AccountID: 81, Input: "Please solve the configured candy question", Model: "test-model", ConfigSnapshot: &IntelligentTestConfig{Model: "test-model"}}
  76. require.NoError(t, svc.RunIntelligentTest(context.Background(), record))
  77. body, _ := json.Marshal(received)
  78. require.Contains(t, string(body), record.Input)
  79. require.NotContains(t, string(body), `"hi"`)
  80. require.Contains(t, record.Result, "ANSWER: 12")
  81. require.Contains(t, record.RawResponse, "response.completed")
  82. require.NotContains(t, record.RawResponse, "sk-private")
  83. require.Equal(t, 0, repo.writes)
  84. require.Equal(t, true, repo.account.Extra["anti_degradation"])
  85. }
  86. func TestIntelligentRunnerFailureDoesNotModifyAccountAndRedacts(t *testing.T) {
  87. for _, status := range []int{401, 429, 404} {
  88. t.Run(fmt.Sprint(status), func(t *testing.T) {
  89. svc, repo := intelligentRunnerFixture(t, func(w http.ResponseWriter, r *http.Request) {
  90. w.WriteHeader(status)
  91. fmt.Fprint(w, `{"error":{"message":"upstream error sk-private-test-secret-abc123"}}`)
  92. })
  93. record := &IntelligentTestRecord{AccountID: 81, Input: "test", Model: "model", ConfigSnapshot: &IntelligentTestConfig{}}
  94. require.Error(t, svc.RunIntelligentTest(context.Background(), record))
  95. require.Equal(t, map[int]string{401: "account_error", 429: "rate_limited", 404: "model_error"}[status], record.Status)
  96. require.NotContains(t, record.RawResponse, "sk-private-test-secret")
  97. require.NotContains(t, record.ErrorMessage, "sk-private-test-secret")
  98. require.Equal(t, 0, repo.writes)
  99. })
  100. }
  101. }
  102. func TestIntelligentRunnerIncompleteAndSyntheticNeverSuccess(t *testing.T) {
  103. svc, repo := intelligentRunnerFixture(t, func(w http.ResponseWriter, r *http.Request) {
  104. fmt.Fprint(w, "data: {\"type\":\"response.output_text.delta\",\"delta\":\"ANSWER: 12\"}\n\n")
  105. })
  106. record := &IntelligentTestRecord{AccountID: 81, Input: "test", Model: "model", ConfigSnapshot: &IntelligentTestConfig{}}
  107. require.Error(t, svc.RunIntelligentTest(context.Background(), record))
  108. require.Equal(t, "failed", record.Status)
  109. repo.account.Extra["synthetic_ui_test"] = true
  110. require.Error(t, svc.RunIntelligentTest(context.Background(), record))
  111. require.NotContains(t, record.Result, "healthy and interactive")
  112. }
  113. func TestIntelligentRunnerContextCancellation(t *testing.T) {
  114. svc, _ := intelligentRunnerFixture(t, func(w http.ResponseWriter, r *http.Request) {
  115. w.Header().Set("Content-Type", "text/event-stream")
  116. fmt.Fprint(w, "data: {\"type\":\"response.output_text.delta\",\"delta\":\"working\"}\n\n")
  117. w.(http.Flusher).Flush()
  118. <-r.Context().Done()
  119. })
  120. ctx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond)
  121. defer cancel()
  122. record := &IntelligentTestRecord{AccountID: 81, Input: "test", ConfigSnapshot: &IntelligentTestConfig{}}
  123. require.Error(t, svc.RunIntelligentTest(ctx, record))
  124. }
  125. func TestIntelligentRawCompletionRejectsTruncation(t *testing.T) {
  126. cases := []struct {
  127. raw string
  128. ok bool
  129. }{{`{"stop_reason":"end_turn"}`, true}, {`{"stop_reason":"max_tokens"}`, false}, {"data: {\"candidates\":[{\"finishReason\":\"STOP\"}]}\n", true}, {"data: {\"candidates\":[{\"finishReason\":\"MAX_TOKENS\"}]}\n", false}, {"data: {\"type\":\"response.completed\",\"response\":{\"status\":\"incomplete\"}}\n", false}, {"data: {\"type\":\"message_stop\"}\n", true}, {`{"choices":[{"finish_reason":null}]}`, false}}
  130. for _, c := range cases {
  131. require.Equal(t, c.ok, intelligentRawComplete(c.raw), c.raw)
  132. }
  133. }
  134. func TestIntelligentSVGAndCandyEvaluation(t *testing.T) {
  135. good := `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100"><title>Pelican</title><circle cx="20" cy="70" r="15"/><path d="M20 70 L60 10 L100 70 Z" fill="none"/></svg>`
  136. safe, err := SanitizeIntelligentTestSVG("```svg\n" + good + "\n```")
  137. require.NoError(t, err)
  138. require.Contains(t, safe, "<circle")
  139. for _, bad := range []string{`<svg><script>alert(1)</script><circle r="1"/></svg>`, `<svg><foreignObject/><circle r="1"/></svg>`, `<svg onload="alert(1)"><circle r="1"/></svg>`, `<svg><image href="https://secret"/><circle r="1"/></svg>`, `<svg><circle fill="url(https://secret)"/></svg>`, `<svg><text>only text</text></svg>`, `<svg><circle></svg>`} {
  140. _, err := SanitizeIntelligentTestSVG(bad)
  141. require.Error(t, err, bad)
  142. }
  143. evaluators := DefaultIntelligentTestEvaluators()
  144. result := evaluators["svg_structure"].Evaluate(good, IntelligentTestConfig{})
  145. require.Equal(t, "completed", result.Status)
  146. require.Nil(t, result.Score)
  147. require.Equal(t, "not_evaluated", result.Detail["answer_verdict"])
  148. cfg := IntelligentTestConfig{ExpectedAnswer: "12"}
  149. require.Equal(t, "incorrect", evaluators["exact_answer"].Evaluate("ANSWER: 11", cfg).Detail["answer_verdict"])
  150. require.Equal(t, "undetermined", evaluators["exact_answer"].Evaluate("ANSWER: 12\nANSWER: 11", cfg).Detail["answer_verdict"])
  151. require.Equal(t, "correct", evaluators["exact_answer"].Evaluate("12", cfg).Detail["answer_verdict"])
  152. require.Equal(t, "completed", evaluators["exact_answer"].Evaluate("reasoning\nANSWER: 12", cfg).Status)
  153. }
  154. func TestIntelligentCaptureBounded(t *testing.T) {
  155. capture := &intelligentCapture{}
  156. n, err := io.Copy(capture, strings.NewReader(strings.Repeat("x", 2<<20)))
  157. require.NoError(t, err)
  158. require.EqualValues(t, 2<<20, n)
  159. require.Equal(t, 1<<20, capture.body.Len())
  160. require.True(t, capture.truncated)
  161. }
  162. type intelligentRedirectUpstream struct {
  163. target *url.URL
  164. client *http.Client
  165. requests int
  166. }
  167. func (h *intelligentRedirectUpstream) Do(req *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
  168. h.requests++
  169. copy := req.Clone(req.Context())
  170. target := *req.URL
  171. target.Scheme = h.target.Scheme
  172. target.Host = h.target.Host
  173. copy.URL = &target
  174. copy.Host = ""
  175. return h.client.Do(copy)
  176. }
  177. func (h *intelligentRedirectUpstream) DoWithTLS(req *http.Request, p string, id int64, n int, _ *tlsfingerprint.Profile) (*http.Response, error) {
  178. return h.Do(req, p, id, n)
  179. }
  180. func TestIntelligentAntigravitySingleObservationPreservesPolicy(t *testing.T) {
  181. var sent string
  182. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  183. body, _ := io.ReadAll(r.Body)
  184. sent = string(body)
  185. w.WriteHeader(429)
  186. fmt.Fprint(w, `{"error":{"code":429,"status":"RESOURCE_EXHAUSTED"}}`)
  187. }))
  188. defer server.Close()
  189. target, _ := url.Parse(server.URL)
  190. transport := &intelligentRedirectUpstream{target: target, client: server.Client()}
  191. repo := &intelligentRunnerRepo{account: &Account{ID: 81, Platform: PlatformAntigravity, Type: AccountTypeOAuth, Concurrency: 1, Extra: map[string]any{"anti_degradation": true}}}
  192. gateway := &AntigravityGatewayService{httpUpstream: transport, accountRepo: repo}
  193. capture := &intelligentCapture{}
  194. ctx := context.WithValue(context.Background(), intelligentRunKey{}, &intelligentRunContext{prompt: "draw the configured pelican", capture: capture})
  195. body, err := intelligentAntigravityBody(ctx, []byte(`{"model":"test","request":{"contents":[{"parts":[{"text":"."}]}],"generationConfig":{"maxOutputTokens":1}}}`))
  196. require.NoError(t, err)
  197. resp, err := gateway.runIntelligentAntigravity(ctx, repo.account, "fake-access-token", body)
  198. require.NoError(t, err)
  199. _, err = io.ReadAll(resp.Body)
  200. require.NoError(t, err)
  201. require.NoError(t, resp.Body.Close())
  202. require.Equal(t, 429, resp.StatusCode)
  203. require.Equal(t, 1, transport.requests)
  204. require.Zero(t, repo.writes)
  205. require.Contains(t, sent, "draw the configured pelican")
  206. require.Contains(t, sent, `"maxOutputTokens":8192`)
  207. require.Equal(t, true, repo.account.Extra["anti_degradation"])
  208. }