| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- package handler
- import (
- "fmt"
- "net/http"
- "net/http/httptest"
- "strings"
- "sync"
- "testing"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- )
- var handlerStructuredLogCaptureMu sync.Mutex
- type handlerInMemoryLogSink struct {
- mu sync.Mutex
- events []*logger.LogEvent
- }
- func (s *handlerInMemoryLogSink) WriteLogEvent(event *logger.LogEvent) {
- if event == nil {
- return
- }
- cloned := *event
- if event.Fields != nil {
- cloned.Fields = make(map[string]any, len(event.Fields))
- for k, v := range event.Fields {
- cloned.Fields[k] = v
- }
- }
- s.mu.Lock()
- s.events = append(s.events, &cloned)
- s.mu.Unlock()
- }
- func (s *handlerInMemoryLogSink) ContainsMessageAtLevel(substr, level string) bool {
- s.mu.Lock()
- defer s.mu.Unlock()
- wantLevel := strings.ToLower(strings.TrimSpace(level))
- for _, ev := range s.events {
- if ev == nil {
- continue
- }
- if strings.Contains(ev.Message, substr) && strings.ToLower(strings.TrimSpace(ev.Level)) == wantLevel {
- return true
- }
- }
- return false
- }
- func (s *handlerInMemoryLogSink) ContainsFieldValue(field, substr string) bool {
- s.mu.Lock()
- defer s.mu.Unlock()
- for _, ev := range s.events {
- if ev == nil || ev.Fields == nil {
- continue
- }
- if v, ok := ev.Fields[field]; ok && strings.Contains(fmt.Sprint(v), substr) {
- return true
- }
- }
- return false
- }
- func (s *handlerInMemoryLogSink) FieldValueForMessage(message, field string) (any, bool) {
- s.mu.Lock()
- defer s.mu.Unlock()
- for _, event := range s.events {
- if event == nil || event.Message != message || event.Fields == nil {
- continue
- }
- if value, ok := event.Fields[field]; ok {
- return value, true
- }
- }
- return nil, false
- }
- func captureHandlerStructuredLog(t *testing.T) (*handlerInMemoryLogSink, func()) {
- t.Helper()
- handlerStructuredLogCaptureMu.Lock()
- err := logger.Init(logger.InitOptions{
- Level: "debug",
- Format: "json",
- ServiceName: "sub2api",
- Environment: "test",
- Output: logger.OutputOptions{
- ToStdout: true,
- ToFile: false,
- },
- Sampling: logger.SamplingOptions{Enabled: false},
- })
- require.NoError(t, err)
- sink := &handlerInMemoryLogSink{}
- logger.SetSink(sink)
- return sink, func() {
- logger.SetSink(nil)
- handlerStructuredLogCaptureMu.Unlock()
- }
- }
- func TestIsOpenAILegacyCompactPath(t *testing.T) {
- require.False(t, isOpenAILegacyCompactPath(nil))
- gin.SetMode(gin.TestMode)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- for _, test := range []struct {
- path string
- want bool
- }{
- {path: "/v1/responses/compact", want: true},
- {path: "/v1/responses/compact/detail", want: true},
- {path: "/responses/compact/", want: true},
- {path: "/v1/responses", want: false},
- {path: "/openai/v1/responses", want: false},
- {path: "/responses", want: false},
- {path: "/backend-api/codex/responses", want: false},
- {path: "/v1/responses/resp_123/cancel", want: false},
- } {
- c.Request = httptest.NewRequest(http.MethodPost, test.path, nil)
- require.Equal(t, test.want, isOpenAILegacyCompactPath(c), test.path)
- }
- }
- func TestLogOpenAIRemoteCompactOutcome_Succeeded(t *testing.T) {
- gin.SetMode(gin.TestMode)
- logSink, restore := captureHandlerStructuredLog(t)
- defer restore()
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses/compact", nil)
- c.Request.Header.Set("User-Agent", "codex_cli_rs/0.125.0")
- c.Set(opsModelKey, "gpt-5.3-codex")
- c.Set(opsAccountIDKey, int64(123))
- c.Header("x-request-id", "rid-compact-ok")
- c.Status(http.StatusOK)
- h := &OpenAIGatewayHandler{}
- h.logOpenAIRemoteCompactOutcome(c, time.Now().Add(-8*time.Millisecond))
- require.True(t, logSink.ContainsMessageAtLevel("codex.remote_compact.succeeded", "info"))
- require.True(t, logSink.ContainsFieldValue("compact_outcome", "succeeded"))
- require.True(t, logSink.ContainsFieldValue("status_code", "200"))
- require.True(t, logSink.ContainsFieldValue("path", "/v1/responses/compact"))
- require.True(t, logSink.ContainsFieldValue("request_model", "gpt-5.3-codex"))
- require.True(t, logSink.ContainsFieldValue("account_id", "123"))
- require.True(t, logSink.ContainsFieldValue("upstream_request_id", "rid-compact-ok"))
- }
- func TestLogOpenAIRemoteCompactOutcome_Failed(t *testing.T) {
- gin.SetMode(gin.TestMode)
- logSink, restore := captureHandlerStructuredLog(t)
- defer restore()
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/responses/compact", nil)
- c.Request.Header.Set("User-Agent", "codex_cli_rs/0.125.0")
- c.Status(http.StatusBadGateway)
- h := &OpenAIGatewayHandler{}
- h.logOpenAIRemoteCompactOutcome(c, time.Now())
- require.True(t, logSink.ContainsMessageAtLevel("codex.remote_compact.failed", "warn"))
- require.True(t, logSink.ContainsFieldValue("compact_outcome", "failed"))
- require.True(t, logSink.ContainsFieldValue("status_code", "502"))
- require.True(t, logSink.ContainsFieldValue("path", "/responses/compact"))
- }
- func TestLogOpenAIRemoteCompactOutcome_NonCompactSkips(t *testing.T) {
- gin.SetMode(gin.TestMode)
- logSink, restore := captureHandlerStructuredLog(t)
- defer restore()
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
- c.Status(http.StatusOK)
- h := &OpenAIGatewayHandler{}
- h.logOpenAIRemoteCompactOutcome(c, time.Now())
- require.False(t, logSink.ContainsMessageAtLevel("codex.remote_compact.succeeded", "info"))
- require.False(t, logSink.ContainsMessageAtLevel("codex.remote_compact.failed", "warn"))
- }
- func TestOpenAIResponses_CompactUnauthorizedLogsFailed(t *testing.T) {
- gin.SetMode(gin.TestMode)
- logSink, restore := captureHandlerStructuredLog(t)
- defer restore()
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses/compact", strings.NewReader(`{"model":"gpt-5.3-codex"}`))
- c.Request.Header.Set("Content-Type", "application/json")
- c.Request.Header.Set("User-Agent", "codex_cli_rs/0.125.0")
- h := &OpenAIGatewayHandler{}
- h.Responses(c)
- require.Equal(t, http.StatusUnauthorized, rec.Code)
- require.True(t, logSink.ContainsMessageAtLevel("codex.remote_compact.failed", "warn"))
- require.True(t, logSink.ContainsFieldValue("status_code", "401"))
- require.True(t, logSink.ContainsFieldValue("path", "/v1/responses/compact"))
- }
|