Ctrl K
ComunidadGuías…

aibridge/bridge_internal_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package aibridge

import (
	"context"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"

	"cdr.dev/slog/v3/sloggers/slogtest"
	"github.com/coder/coder/v2/aibridge/mcpmock"
	"github.com/coder/coder/v2/testutil"
)

// InflightRequests returns the number of admitted requests still running.
func (b *RequestBridge) InflightRequests() int {
	b.inflight.mu.Lock()
	defer b.inflight.mu.Unlock()
	return b.inflight.active
}

// TestRequestBridgeShutdownDoesNotWaitForCanceledHandler verifies shutdown
// returns even when a canceled handler remains blocked.
func TestRequestBridgeShutdownDoesNotWaitForCanceledHandler(t *testing.T) {
	t.Parallel()
	for _, withMCP := range []bool{false, true} {
		name := "WithoutMCP"
		if withMCP {
			name = "WithMCP"
		}
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			ctx := testutil.Context(t, testutil.WaitShort)
			// Set up a handler that only test cleanup can release.
			started := make(chan context.Context, 1)
			release := make(chan struct{})
			finished := make(chan struct{})
			mux := http.NewServeMux()
			mux.HandleFunc("/", func(_ http.ResponseWriter, r *http.Request) {
				started <- r.Context()
				<-release // Deliberately ignore request cancellation.
			})
			bridge := &RequestBridge{
				inflight: NewInflightGate(slogtest.Make(t, nil)),
				logger:   slogtest.Make(t, nil),
			}
			bridge.handler = bridge.inflight.Middleware(mux)
			// MCP cleanup must be attempted while the handler is still blocked.
			if withMCP {
				proxy := mcpmock.NewMockServerProxier(gomock.NewController(t))
				proxy.EXPECT().Shutdown(gomock.Any()).DoAndReturn(func(shutdownCtx context.Context) error {
					assert.ErrorIs(t, shutdownCtx.Err(), context.DeadlineExceeded)
					assert.EqualValues(t, 1, bridge.InflightRequests())
					return nil
				})
				bridge.mcpProxy = proxy
			}
			t.Cleanup(func() {
				close(release)
				testutil.TryReceive(testutil.Context(t, testutil.WaitShort), t, finished)
			})
			go func() {
				defer close(finished)
				bridge.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
			}()
			requestCtx := testutil.TryReceive(ctx, t, started)
			// Expire shutdown while the request is in flight.
			deadlineCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
			defer cancel()
			shutdownDone := make(chan error, 1)
			go func() { shutdownDone <- bridge.Shutdown(deadlineCtx) }()
			// Shutdown returns and cancels the context without waiting for release.
			require.ErrorIs(t, testutil.TryReceive(ctx, t, shutdownDone), context.DeadlineExceeded)
			testutil.TryReceive(ctx, t, requestCtx.Done())
			require.EqualValues(t, 1, bridge.InflightRequests(), "handler is still blocked")
			// Admission stays closed even though the old handler is still running.
			rejected := httptest.NewRecorder()
			bridge.ServeHTTP(rejected, httptest.NewRequest(http.MethodGet, "/", nil))
			require.Equal(t, http.StatusServiceUnavailable, rejected.Code)
			require.Equal(t, "AI Gateway is shutting down\n", rejected.Body.String())
		})
	}
}