intelligent_test_assessment_test.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. package service
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "strings"
  7. "testing"
  8. "github.com/stretchr/testify/require"
  9. )
  10. func TestIntelligentAssessmentSeparatesAnswerAndFormat(t *testing.T) {
  11. cfg := IntelligentTestConfig{ExpectedAnswer: "12", AnswerType: "number", AnswerUnit: "颗"}
  12. for _, output := range []string{"12", "最后盒子里有12颗糖。", "ANSWER: 12颗", "**ANSWER: 12**", "```text\nANSWER: 12\n```", "ANSWER: 12\n这是最终数量。", "ANSWER: 12\nANSWER: 12", "ANSWER: 12.0", "ANSWER: 12", `{"answer":12}`, "ANSWER: 24/2", "ANSWER: 1.2e1"} {
  13. t.Run(output, func(t *testing.T) {
  14. result := evaluateIntelligentAnswer(output, cfg)
  15. require.Equal(t, "completed", result.Status)
  16. require.Equal(t, "correct", result.Detail["answer_verdict"], result.Detail)
  17. require.Equal(t, 100.0, *result.Score)
  18. require.Equal(t, "insufficient_evidence", result.Detail["capability_verdict"])
  19. })
  20. }
  21. require.Equal(t, "non_compliant", evaluateIntelligentAnswer("12", cfg).Detail["format_verdict"])
  22. require.Equal(t, "compliant", evaluateIntelligentAnswer("ANSWER: 12颗", cfg).Detail["format_verdict"])
  23. cfg.AnswerFormat = "free_text"
  24. require.Equal(t, "not_required", evaluateIntelligentAnswer("12", cfg).Detail["format_verdict"])
  25. for _, output := range []string{"ANSWER: 12\nANSWER: 11", "ANSWER: 12\n11", `{"answer":12,"final_answer":11}`, "计算中出现12,但无法确定最终答案。", "ANSWER: 12米", "ANSWER: 1e999999", "ANSWER: 1,2", "ANSWER: 12或13", ""} {
  26. result := evaluateIntelligentAnswer(output, cfg)
  27. require.Equal(t, "undetermined", result.Detail["answer_verdict"], output)
  28. require.Nil(t, result.Score)
  29. }
  30. wrong := evaluateIntelligentAnswer("中间值是12\nANSWER: 11", cfg)
  31. require.Equal(t, "incorrect", wrong.Detail["answer_verdict"])
  32. require.Equal(t, 0.0, *wrong.Score)
  33. reasoning := evaluateIntelligentAnswer("错误推导: 2+2=12\nANSWER: 12", cfg)
  34. require.Equal(t, "correct", reasoning.Detail["answer_verdict"])
  35. require.Contains(t, reasoning.Detail["limitation"], "未验证完整推导")
  36. cfg.ExpectedAnswer = "1,200"
  37. require.Equal(t, "correct", evaluateIntelligentAnswer("1200", cfg).Detail["answer_verdict"])
  38. cfg.AnswerType, cfg.ExpectedAnswer = "text", "012"
  39. require.Equal(t, "incorrect", evaluateIntelligentAnswer("12", cfg).Detail["answer_verdict"])
  40. }
  41. func TestIntelligentAssessmentRejectsNegatedAndDuplicateConflicts(t *testing.T) {
  42. cfg := IntelligentTestConfig{ExpectedAnswer: "12", AnswerType: "number", AnswerUnit: "颗"}
  43. for _, output := range []string{"因此答案不是12", "最后盒子里没有12颗糖", "因此假设答案是12", "最终答案可能是12", "因此答案至少为12", "> ANSWER: 12", `{"answer":11,"answer":12}`, `{"answer":12,"answer":11}`} {
  44. result := evaluateIntelligentAnswer(output, cfg)
  45. require.Equal(t, "undetermined", result.Detail["answer_verdict"], output)
  46. require.Nil(t, result.Score, output)
  47. }
  48. require.Equal(t, "correct", evaluateIntelligentAnswer(`{"answer":12,"answer":12}`, cfg).Detail["answer_verdict"])
  49. require.Equal(t, "correct", evaluateIntelligentAnswer("最后盒子里有12颗糖。", cfg).Detail["answer_verdict"])
  50. }
  51. func TestIntelligentAssessmentUnitModesAreExplicit(t *testing.T) {
  52. cfg := IntelligentTestConfig{Prompt: "盒子里有 24 颗糖。小明取走总数的四分之一", ExpectedAnswer: "12", Evaluator: "exact_answer", TimeoutSeconds: 120}
  53. require.Equal(t, "correct", evaluateIntelligentAnswer("12颗", cfg).Detail["answer_verdict"], "old snapshots retain compatibility")
  54. cfg.AnswerUnitMode = "none"
  55. require.Equal(t, "undetermined", evaluateIntelligentAnswer("12颗", cfg).Detail["answer_verdict"])
  56. require.Equal(t, "correct", evaluateIntelligentAnswer("12", cfg).Detail["answer_verdict"])
  57. cfg.AnswerUnitMode = "configured"
  58. require.Error(t, validateIntelligentTestConfig(&cfg, DefaultIntelligentTestEvaluators()))
  59. cfg.AnswerUnit = "颗"
  60. require.NoError(t, validateIntelligentTestConfig(&cfg, DefaultIntelligentTestEvaluators()))
  61. require.Equal(t, "correct", evaluateIntelligentAnswer("12颗糖", cfg).Detail["answer_verdict"])
  62. require.Equal(t, 3, evaluateIntelligentAnswer("12", cfg).Detail["evaluator_version"])
  63. require.NotNil(t, PublicIntelligentAssessment(map[string]any{"evaluator_version": 2, "answer_verdict": "correct"}))
  64. require.False(t, IsCurrentIntelligentAssessment(map[string]any{"evaluator_version": 2}))
  65. }
  66. func TestIntelligentSVGPreviewPreservesStaticDrawingAndRejectsEmpty(t *testing.T) {
  67. raw := `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100"><style>.bird{fill:#fff;stroke:#123;stroke-width:2} #eye{fill:black} circle{fill:red}</style><defs><g id="wheel"><circle r="12" fill="none" stroke="black"/></g></defs><use href="#wheel" x="30" y="75"/><use href="#wheel" x="130" y="75"/><path class="bird" d="M50 40 Q70 5 100 40 L130 45 L100 50 Q60 80 50 40Z"/><circle id="eye" cx="91" cy="30" r="2"/><text x="10" y="95">A<tspan>B</tspan>C</text></svg>`
  68. safe, adjusted, err := PrepareIntelligentSVGPreview(raw)
  69. require.NoError(t, err)
  70. require.True(t, adjusted)
  71. require.Contains(t, safe, `translate(30 75)`)
  72. require.Contains(t, safe, `fill="#fff"`)
  73. require.Contains(t, safe, `id="eye" cx="91" cy="30" r="2" fill="black"`)
  74. require.Contains(t, safe, `>A<tspan>B</tspan>C</text>`)
  75. require.NotContains(t, safe, "<style")
  76. require.NotContains(t, safe, "<use")
  77. _, err = SanitizeIntelligentTestSVG(safe)
  78. require.NoError(t, err)
  79. result := evaluateIntelligentSVG(raw)
  80. require.Equal(t, "completed", result.Status)
  81. require.Nil(t, result.Score)
  82. require.NotEmpty(t, result.Image)
  83. for _, empty := range []string{`<svg><rect/></svg>`, `<svg><rect width="0" height="20"/></svg>`, `<svg><defs><circle r="5"/></defs></svg>`, `<svg><g opacity="0.0"><circle r="4"/></g></svg>`, `<svg><use href="#missing"/></svg>`, `<svg><g id="cycle"><use href="#cycle"/></g></svg>`, `<svg><circle></svg>`} {
  84. result := evaluateIntelligentSVG(empty)
  85. require.Empty(t, result.Image, empty)
  86. require.Nil(t, result.Score)
  87. require.Equal(t, "unavailable", result.Detail["image_state"])
  88. }
  89. }
  90. func TestIntelligentSVGPreviewStripsActiveAndExternalContent(t *testing.T) {
  91. for _, payload := range []string{
  92. `<script>alert(1)</script>`, `<foreignObject><style>circle{fill:red}</style><div>unsafe</div></foreignObject>`,
  93. `<image href="https://private.invalid/secret"/>`, `<use href="https://private.invalid/a.svg#x"/>`,
  94. `<animate attributeName="href" values="javascript:alert(1)"/>`,
  95. `<style>circle{fill:url(https://private.invalid/a)}</style>`, `<style>@import 'https://private.invalid/a';</style>`,
  96. } {
  97. safe, _, err := PrepareIntelligentSVGPreview(`<svg onload="alert(1)">` + payload + `<circle r="5" fill="blue" style="stroke:url(javascript:alert(1))"/></svg>`)
  98. require.NoError(t, err, payload)
  99. require.NotRegexp(t, `(?i)onload|script|foreignObject|<image|<use|<style|https:|<animate`, safe)
  100. require.Contains(t, safe, `fill="blue"`)
  101. }
  102. _, _, err := PrepareIntelligentSVGPreview(`<svg><!DOCTYPE x><circle r="2"/></svg>`)
  103. require.Error(t, err)
  104. }
  105. func TestIntelligentTerminalOnlyOutputAndErrorCategories(t *testing.T) {
  106. for _, delta := range []bool{false, true} {
  107. svc, _ := intelligentRunnerFixture(t, func(w http.ResponseWriter, r *http.Request) {
  108. w.Header().Set("Content-Type", "text/event-stream")
  109. if delta {
  110. fmt.Fprint(w, "data: {\"type\":\"response.output_text.delta\",\"delta\":\"ANSWER: 12\"}\n\n")
  111. }
  112. fmt.Fprint(w, "data: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"ANSWER: 12\"}]}]}}\n\n")
  113. })
  114. record := &IntelligentTestRecord{AccountID: 81, Input: "test", ConfigSnapshot: &IntelligentTestConfig{Model: "gpt-5.4"}}
  115. require.NoError(t, svc.RunIntelligentTest(context.Background(), record))
  116. require.Equal(t, "ANSWER: 12", strings.TrimSpace(record.Result))
  117. require.Equal(t, "gpt-5.4", record.ConfigSnapshot.Model)
  118. require.Equal(t, "gpt-5.4", record.ConfigSnapshot.Execution.RequestedModel)
  119. }
  120. for _, tt := range []struct {
  121. code int
  122. message, want string
  123. }{
  124. {400, "max_output_tokens must be positive", "request_error"}, {400, "max_tokens exceeds request limit", "request_error"},
  125. {401, "max_tokens invalid", "account_error"}, {429, "expired token", "rate_limited"}, {503, "unavailable", "network_error"},
  126. {404, "model missing", "model_error"}, {0, "expired token", "account_error"}, {0, "connection refused", "network_error"},
  127. } {
  128. require.Equal(t, tt.want, classifyIntelligentError(tt.code, tt.message))
  129. }
  130. }
  131. func TestIntelligentAssessmentPublicProjection(t *testing.T) {
  132. detail := evaluateIntelligentAnswer("ANSWER: 12", IntelligentTestConfig{ExpectedAnswer: "12"}).Detail
  133. detail["original_judgment"] = map[string]any{"config": "private"}
  134. public := PublicIntelligentAssessment(detail)
  135. require.Equal(t, "correct", public["answer_verdict"])
  136. for _, private := range []string{"expected_answer", "actual_answer", "original_judgment", "reason", "candidate_answers"} {
  137. require.NotContains(t, public, private)
  138. }
  139. require.Nil(t, PublicIntelligentAssessment(map[string]any{"reason": "legacy private"}))
  140. }
  141. func TestProtectedRandomProxyRequestPreservesFixedProxy(t *testing.T) {
  142. proxyID, clear := int64(9), int64(0)
  143. a := &Account{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Concurrency: 4, ProxyID: &proxyID}
  144. PrepareNewAccountProtection(a)
  145. repo := &upstreamBillingProbeAccountRepo{accounts: map[int64]*Account{1: a}}
  146. svc := &adminServiceImpl{accountRepo: repo}
  147. _, err := svc.UpdateAccount(context.Background(), 1, &UpdateAccountInput{ProxyID: &clear, Extra: map[string]any{"proxy_mode": "random"}})
  148. require.ErrorIs(t, err, ErrProtectedProxyModeChange)
  149. require.EqualValues(t, 9, *a.ProxyID)
  150. require.Equal(t, "legacy", a.ProtectionMode())
  151. require.False(t, a.IsRandomProxy())
  152. }