package service import ( "strings" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) func TestUpstreamResponseModelObserverTerminalWinsAndRecordsConflict(t *testing.T) { observer := &upstreamResponseModelObserver{} observer.ObserveOpenAI([]byte(`{"type":"response.created","response":{"model":"gpt-5.5"}}`), "response.created") observer.ObserveOpenAI([]byte(`{"type":"response.completed","response":{"model":"gpt-5.4"}}`), "response.completed") require.Equal(t, "gpt-5.4", observer.Model()) require.True(t, observer.Conflict()) } func TestUpstreamResponseModelObserverSupportsAnthropicAndGeminiShapes(t *testing.T) { t.Run("anthropic", func(t *testing.T) { observer := &upstreamResponseModelObserver{} observer.ObserveAnthropic([]byte(`{"type":"message_start","message":{"model":"claude-sonnet-4-20250514"}}`)) require.Equal(t, "claude-sonnet-4-20250514", observer.Model()) }) t.Run("gemini outer and nested", func(t *testing.T) { observer := &upstreamResponseModelObserver{} observer.ObserveGemini([]byte(`{"response":{"modelVersion":"gemini-2.5-pro"}}`)) observer.ObserveGemini([]byte(`{"modelVersion":"gemini-2.5-pro-latest"}`)) require.Equal(t, "gemini-2.5-pro-latest", observer.Model()) require.True(t, observer.Conflict()) }) } func TestUpstreamResponseModelObservationAttemptReset(t *testing.T) { gin.SetMode(gin.TestMode) c, _ := gin.CreateTestContext(nil) first := beginUpstreamResponseModelObservation(c) first.Observe("failed-attempt-model", false) second := beginUpstreamResponseModelObservation(c) second.Observe("successful-attempt-model", false) require.Equal(t, "successful-attempt-model", observedUpstreamResponseModel(c)) require.False(t, observedUpstreamResponseModelConflict(c)) } func TestUpstreamModelMismatchThreeStateAndCaseInsensitiveComparison(t *testing.T) { require.Nil(t, upstreamModelMismatch("gpt-5.5", "")) matched := upstreamModelMismatch("gpt-5.5", "GPT-5.5") require.NotNil(t, matched) require.False(t, *matched) mismatched := upstreamModelMismatch("gpt-5.5", "gpt-5.4") require.NotNil(t, mismatched) require.True(t, *mismatched) } func TestUpstreamModelMismatchTreatsGrokBuildRuntimeIDsAsAliases(t *testing.T) { tests := []struct { name string sentModel string responseModel string }{ { name: "issue 5634 grok 4.6", sentModel: "grok-4.6", responseModel: "grok-4.6-build", }, { name: "grok 4.6 latest", sentModel: "grok-4.6-latest", responseModel: "grok-4.6-build", }, { name: "issue 5647 grok 4.5 latest", sentModel: "grok-4.5-latest", responseModel: "grok-4.5-build", }, { name: "grok 4.5 canonical", sentModel: "grok-4.5", responseModel: "GROK-4.5-BUILD", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mismatch := upstreamModelMismatch(tt.sentModel, tt.responseModel) require.NotNil(t, mismatch) require.False(t, *mismatch) }) } } func TestUpstreamModelMismatchDoesNotCollapseDifferentModels(t *testing.T) { tests := []struct { name string sentModel string responseModel string }{ { name: "different grok versions", sentModel: "grok-4.5", responseModel: "grok-4.6-build", }, { name: "unrelated build suffix", sentModel: "gpt-5.5", responseModel: "gpt-5.5-build", }, { name: "different grok runtime", sentModel: "grok-build-0.1", responseModel: "grok-4.5-build", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mismatch := upstreamModelMismatch(tt.sentModel, tt.responseModel) require.NotNil(t, mismatch) require.True(t, *mismatch) }) } } func TestObserveOpenAISSEBodyIgnoresMalformedPayload(t *testing.T) { observer := &upstreamResponseModelObserver{} observeOpenAISSEBody(observer, "data: not-json\n\ndata: {\"type\":\"response.completed\",\"response\":{\"model\":\"gpt-5.4\"}}\n\n") require.Equal(t, "gpt-5.4", observer.Model()) require.False(t, observer.Conflict()) } func TestObserveAntigravityGeminiSSELineReadsWrapperModelWithoutUnwrap(t *testing.T) { tests := []struct { name string payload string want string }{ { name: "top-level sibling", payload: `{"modelVersion":"gemini-3-pro","response":{"candidates":[]}}`, want: "gemini-3-pro", }, { name: "single wrapper", payload: `{"response":{"modelVersion":"gemini-3-pro","candidates":[]}}`, want: "gemini-3-pro", }, { name: "nested response after one wrapper", payload: `{"response":{"response":{"modelVersion":"gemini-3-pro","candidates":[]}}}`, want: "gemini-3-pro", }, { name: "outer declaration takes precedence", payload: `{"modelVersion":"gemini-outer","response":{"modelVersion":"gemini-inner","candidates":[]}}`, want: "gemini-outer", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gin.SetMode(gin.TestMode) c, _ := gin.CreateTestContext(nil) beginUpstreamResponseModelObservation(c) svc := &AntigravityGatewayService{} svc.observeAntigravityGeminiSSELine(c, "data: "+tt.payload) require.Equal(t, tt.want, observedUpstreamResponseModel(c)) require.False(t, observedUpstreamResponseModelConflict(c)) }) } } func TestUpstreamResponseModelObserverRejectsMalformedJSONWithModelField(t *testing.T) { observer := &upstreamResponseModelObserver{} observer.ObserveOpenAI([]byte(`{"response":{"model":"gpt-5.4"}`), "response.completed") require.Empty(t, observer.Model()) } func TestUpstreamResponseModelObserverBoundsUntrustedModelName(t *testing.T) { observer := &upstreamResponseModelObserver{} observer.Observe(" "+strings.Repeat("циб", upstreamResponseModelMaxLength+1)+" ", false) require.Len(t, []rune(observer.Model()), upstreamResponseModelMaxLength) }