openai_compact_probe_test.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. package service
  2. import (
  3. "errors"
  4. "net/http"
  5. "testing"
  6. "time"
  7. )
  8. func TestNormalizeAccountTestMode(t *testing.T) {
  9. tests := []struct {
  10. input string
  11. want string
  12. }{
  13. {input: "", want: AccountTestModeDefault},
  14. {input: "default", want: AccountTestModeDefault},
  15. {input: " compact ", want: AccountTestModeCompact},
  16. {input: "COMPACT", want: AccountTestModeCompact},
  17. {input: "unknown", want: AccountTestModeDefault},
  18. }
  19. for _, tt := range tests {
  20. if got := normalizeAccountTestMode(tt.input); got != tt.want {
  21. t.Fatalf("normalizeAccountTestMode(%q) = %q, want %q", tt.input, got, tt.want)
  22. }
  23. }
  24. }
  25. func TestBuildOpenAICompactProbeExtraUpdates_SuccessMarksSupported(t *testing.T) {
  26. now := time.Date(2026, 4, 10, 10, 0, 0, 0, time.UTC)
  27. updates := buildOpenAICompactProbeExtraUpdates(&http.Response{StatusCode: http.StatusOK}, []byte(`{"id":"cmp_1"}`), nil, true, now)
  28. if got := updates["openai_compact_supported"]; got != true {
  29. t.Fatalf("openai_compact_supported = %v, want true", got)
  30. }
  31. if got := updates["openai_compact_last_status"]; got != http.StatusOK {
  32. t.Fatalf("openai_compact_last_status = %v, want %d", got, http.StatusOK)
  33. }
  34. if got := updates["openai_compact_last_error"]; got != "" {
  35. t.Fatalf("openai_compact_last_error = %v, want empty string", got)
  36. }
  37. if got := updates["openai_compact_checked_at"]; got != now.Format(time.RFC3339) {
  38. t.Fatalf("openai_compact_checked_at = %v, want %s", got, now.Format(time.RFC3339))
  39. }
  40. }
  41. func TestBuildOpenAICompactProbeExtraUpdates_404MarksUnsupported(t *testing.T) {
  42. now := time.Date(2026, 4, 10, 10, 0, 0, 0, time.UTC)
  43. body := []byte(`404 page not found`)
  44. updates := buildOpenAICompactProbeExtraUpdates(&http.Response{StatusCode: http.StatusNotFound}, body, nil, false, now)
  45. if got := updates["openai_compact_supported"]; got != false {
  46. t.Fatalf("openai_compact_supported = %v, want false", got)
  47. }
  48. if got := updates["openai_compact_last_status"]; got != http.StatusNotFound {
  49. t.Fatalf("openai_compact_last_status = %v, want %d", got, http.StatusNotFound)
  50. }
  51. }
  52. func TestBuildOpenAICompactProbeExtraUpdates_502DoesNotMarkUnsupported(t *testing.T) {
  53. now := time.Date(2026, 4, 10, 10, 0, 0, 0, time.UTC)
  54. updates := buildOpenAICompactProbeExtraUpdates(&http.Response{StatusCode: http.StatusBadGateway}, []byte(`Upstream request failed`), nil, false, now)
  55. if _, exists := updates["openai_compact_supported"]; exists {
  56. t.Fatalf("did not expect openai_compact_supported for 502 response")
  57. }
  58. if got := updates["openai_compact_last_status"]; got != http.StatusBadGateway {
  59. t.Fatalf("openai_compact_last_status = %v, want %d", got, http.StatusBadGateway)
  60. }
  61. }
  62. func TestBuildOpenAICompactProbeExtraUpdates_RequestErrorDoesNotMarkUnsupported(t *testing.T) {
  63. now := time.Date(2026, 4, 10, 10, 0, 0, 0, time.UTC)
  64. updates := buildOpenAICompactProbeExtraUpdates(nil, nil, errors.New("dial tcp timeout"), false, now)
  65. if _, exists := updates["openai_compact_supported"]; exists {
  66. t.Fatalf("did not expect openai_compact_supported for request error")
  67. }
  68. if got, exists := updates["openai_compact_last_status"]; !exists || got != nil {
  69. t.Fatalf("openai_compact_last_status = %v, want nil key", got)
  70. }
  71. if got := updates["openai_compact_last_error"]; got == "" {
  72. t.Fatalf("expected openai_compact_last_error to be populated")
  73. }
  74. }
  75. func TestBuildOpenAICompactProbeExtraUpdates_NoResponseClearsLastStatus(t *testing.T) {
  76. now := time.Date(2026, 4, 10, 10, 0, 0, 0, time.UTC)
  77. updates := buildOpenAICompactProbeExtraUpdates(nil, nil, nil, false, now)
  78. if got, exists := updates["openai_compact_last_status"]; !exists || got != nil {
  79. t.Fatalf("openai_compact_last_status = %v, want nil key", got)
  80. }
  81. if got := updates["openai_compact_last_error"]; got != "compact probe failed" {
  82. t.Fatalf("openai_compact_last_error = %v, want compact probe failed", got)
  83. }
  84. }
  85. func TestBuildOpenAICompactProbeExtraUpdates_UnknownModelDoesNotMarkUnsupported(t *testing.T) {
  86. now := time.Date(2026, 4, 10, 10, 0, 0, 0, time.UTC)
  87. body := []byte(`{"error":{"message":"unknown model gpt-5.4-openai-compact"}}`)
  88. updates := buildOpenAICompactProbeExtraUpdates(&http.Response{StatusCode: http.StatusBadRequest}, body, nil, false, now)
  89. if _, exists := updates["openai_compact_supported"]; exists {
  90. t.Fatalf("did not expect openai_compact_supported for unknown-model diagnostics")
  91. }
  92. if got := updates["openai_compact_last_status"]; got != http.StatusBadRequest {
  93. t.Fatalf("openai_compact_last_status = %v, want %d", got, http.StatusBadRequest)
  94. }
  95. }
  96. func TestBuildOpenAICompactProbeExtraUpdates_EmptyFailureBodyFallsBackToHTTPStatus(t *testing.T) {
  97. now := time.Date(2026, 4, 10, 10, 0, 0, 0, time.UTC)
  98. updates := buildOpenAICompactProbeExtraUpdates(&http.Response{StatusCode: http.StatusServiceUnavailable}, nil, nil, false, now)
  99. if got := updates["openai_compact_last_status"]; got != http.StatusServiceUnavailable {
  100. t.Fatalf("openai_compact_last_status = %v, want %d", got, http.StatusServiceUnavailable)
  101. }
  102. if got := updates["openai_compact_last_error"]; got != "HTTP 503" {
  103. t.Fatalf("openai_compact_last_error = %v, want HTTP 503", got)
  104. }
  105. }
  106. func TestBuildOpenAICompactProbeExtraUpdates_2xxWithoutCompactionItemMarksUnsupported(t *testing.T) {
  107. now := time.Date(2026, 4, 10, 10, 0, 0, 0, time.UTC)
  108. updates := buildOpenAICompactProbeExtraUpdates(&http.Response{StatusCode: http.StatusOK}, []byte(`{"id":"resp_1","output":[]}`), nil, false, now)
  109. if got := updates["openai_compact_supported"]; got != false {
  110. t.Fatalf("openai_compact_supported = %v, want false(2xx 无 compaction item = v2 不可用)", got)
  111. }
  112. if got := updates["openai_compact_last_error"]; got == "" {
  113. t.Fatalf("expected openai_compact_last_error to explain the missing compaction item")
  114. }
  115. }
  116. func TestCreateOpenAICompactProbePayload_NativeV2Shape(t *testing.T) {
  117. payload := createOpenAICompactProbePayload("gpt-5.6-sol", true)
  118. if payload["stream"] != true {
  119. t.Fatalf("v2 probe payload must be streaming")
  120. }
  121. if payload["store"] != false {
  122. t.Fatalf("OAuth probe payload must carry store:false")
  123. }
  124. input, ok := payload["input"].([]any)
  125. if !ok || len(input) != 2 {
  126. t.Fatalf("expected 2 input items, got %v", payload["input"])
  127. }
  128. last, ok := input[len(input)-1].(map[string]any)
  129. if !ok || last["type"] != "compaction_trigger" {
  130. t.Fatalf("last input item must be compaction_trigger, got %v", input[len(input)-1])
  131. }
  132. apiKeyPayload := createOpenAICompactProbePayload("gpt-5.6-sol", false)
  133. if _, has := apiKeyPayload["store"]; has {
  134. t.Fatalf("API-key probe payload must not force store")
  135. }
  136. }
  137. func TestOpenAICompactProbeFoundCompactionItem(t *testing.T) {
  138. sseWithItem := []byte("event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"compaction\",\"id\":\"cmp_1\",\"encrypted_content\":\"blob\"}}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"output\":[]}}\n\n")
  139. if !openAICompactProbeFoundCompactionItem(sseWithItem) {
  140. t.Fatalf("SSE output_item.done 携带 compaction item 应判定为支持")
  141. }
  142. sseAlias := []byte("data: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"compaction_summary\",\"id\":\"cmp_2\"}}\n\n")
  143. if !openAICompactProbeFoundCompactionItem(sseAlias) {
  144. t.Fatalf("compaction_summary 别名应判定为支持")
  145. }
  146. sseNoItem := []byte("data: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"message\",\"id\":\"msg_1\"}}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_2\",\"output\":[]}}\n\n")
  147. if openAICompactProbeFoundCompactionItem(sseNoItem) {
  148. t.Fatalf("无 compaction item 的流不应判定为支持")
  149. }
  150. jsonWithItem := []byte(`{"id":"resp_3","output":[{"type":"compaction","id":"cmp_3"}]}`)
  151. if !openAICompactProbeFoundCompactionItem(jsonWithItem) {
  152. t.Fatalf("JSON output[] 携带 compaction item 应判定为支持(降级链兜底)")
  153. }
  154. if openAICompactProbeFoundCompactionItem(nil) {
  155. t.Fatalf("空响应不应判定为支持")
  156. }
  157. }
  158. func TestOpenAICompactProbeFoundCompactionItem_TerminalResponseOutput(t *testing.T) {
  159. // 部分上游只在终态 response.completed 的 output[] 给出 compaction item,
  160. // 事件流里没有 output_item.done——探针同样必须判定为支持。
  161. sseTerminalOnly := []byte("data: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"message\",\"id\":\"msg_1\"}}\n\n" +
  162. "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_t\",\"output\":[{\"type\":\"compaction\",\"id\":\"cmp_t\"}]}}\n\n")
  163. if !openAICompactProbeFoundCompactionItem(sseTerminalOnly) {
  164. t.Fatalf("终态 response.output 中的 compaction item 应判定为支持")
  165. }
  166. sseTerminalEmpty := []byte("data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_e\",\"output\":[]}}\n\n")
  167. if openAICompactProbeFoundCompactionItem(sseTerminalEmpty) {
  168. t.Fatalf("终态 output 为空且无 item 事件时不应判定为支持")
  169. }
  170. }