openai_ws_client_read_test.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "net/http"
  6. "net/http/httptest"
  7. "strings"
  8. "testing"
  9. "time"
  10. coderws "github.com/coder/websocket"
  11. "github.com/stretchr/testify/require"
  12. )
  13. func TestReadOpenAIWSClientMessage_ControlCloseFrames(t *testing.T) {
  14. tests := []struct {
  15. name string
  16. timeout time.Duration
  17. timeoutStatus coderws.StatusCode
  18. timeoutReason string
  19. cancelCause error
  20. wantStatus coderws.StatusCode
  21. wantReason string
  22. }{
  23. {
  24. name: "inter-turn idle sends normal close",
  25. timeout: 25 * time.Millisecond,
  26. timeoutStatus: coderws.StatusNormalClosure,
  27. timeoutReason: "websocket idle timeout",
  28. wantStatus: coderws.StatusNormalClosure,
  29. wantReason: "websocket idle timeout",
  30. },
  31. {
  32. name: "first message timeout sends policy close",
  33. timeout: 25 * time.Millisecond,
  34. timeoutStatus: coderws.StatusPolicyViolation,
  35. timeoutReason: "missing first response.create message",
  36. wantStatus: coderws.StatusPolicyViolation,
  37. wantReason: "missing first response.create message",
  38. },
  39. {
  40. name: "lease loss sends retry close",
  41. cancelCause: ErrOpenAIWSIngressLeaseLost,
  42. wantStatus: coderws.StatusTryAgainLater,
  43. wantReason: "websocket ingress capacity lease lost; please reconnect",
  44. },
  45. }
  46. for _, tt := range tests {
  47. t.Run(tt.name, func(t *testing.T) {
  48. controlCtx, cancelControl := context.WithCancelCause(context.Background())
  49. defer cancelControl(context.Canceled)
  50. serverResult := make(chan error, 1)
  51. readStarted := make(chan struct{})
  52. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  53. conn, err := coderws.Accept(w, r, nil)
  54. if err != nil {
  55. serverResult <- err
  56. return
  57. }
  58. defer func() { _ = conn.CloseNow() }()
  59. close(readStarted)
  60. _, _, err = ReadOpenAIWSClientMessage(
  61. controlCtx,
  62. conn,
  63. tt.timeout,
  64. tt.timeoutStatus,
  65. tt.timeoutReason,
  66. )
  67. serverResult <- err
  68. }))
  69. defer server.Close()
  70. dialCtx, cancelDial := context.WithTimeout(context.Background(), time.Second)
  71. clientConn, _, err := coderws.Dial(dialCtx, "ws"+strings.TrimPrefix(server.URL, "http"), nil)
  72. cancelDial()
  73. require.NoError(t, err)
  74. defer func() { _ = clientConn.CloseNow() }()
  75. <-readStarted
  76. if tt.cancelCause != nil {
  77. cancelControl(tt.cancelCause)
  78. }
  79. readCtx, cancelRead := context.WithTimeout(context.Background(), time.Second)
  80. _, _, err = clientConn.Read(readCtx)
  81. cancelRead()
  82. var clientClose coderws.CloseError
  83. require.ErrorAs(t, err, &clientClose)
  84. require.Equal(t, tt.wantStatus, clientClose.Code)
  85. require.Equal(t, tt.wantReason, clientClose.Reason)
  86. select {
  87. case serverErr := <-serverResult:
  88. var closeErr *OpenAIWSClientCloseError
  89. require.ErrorAs(t, serverErr, &closeErr)
  90. require.Equal(t, tt.wantStatus, closeErr.StatusCode())
  91. require.Equal(t, tt.wantReason, closeErr.Reason())
  92. case <-time.After(time.Second):
  93. t.Fatal("server read goroutine did not exit after close handshake")
  94. }
  95. })
  96. }
  97. }
  98. func TestReadOpenAIWSClientMessage_ParentCancellationStillJoinsRead(t *testing.T) {
  99. controlCtx, cancelControl := context.WithCancelCause(context.Background())
  100. serverResult := make(chan error, 1)
  101. readStarted := make(chan struct{})
  102. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  103. conn, err := coderws.Accept(w, r, nil)
  104. if err != nil {
  105. serverResult <- err
  106. return
  107. }
  108. defer func() { _ = conn.CloseNow() }()
  109. close(readStarted)
  110. _, _, err = ReadOpenAIWSClientMessage(controlCtx, conn, 0, 0, "")
  111. serverResult <- err
  112. }))
  113. defer server.Close()
  114. dialCtx, cancelDial := context.WithTimeout(context.Background(), time.Second)
  115. clientConn, _, err := coderws.Dial(dialCtx, "ws"+strings.TrimPrefix(server.URL, "http"), nil)
  116. cancelDial()
  117. require.NoError(t, err)
  118. defer func() { _ = clientConn.CloseNow() }()
  119. <-readStarted
  120. cancelControl(errors.New("server shutting down"))
  121. readCtx, cancelRead := context.WithTimeout(context.Background(), time.Second)
  122. _, _, err = clientConn.Read(readCtx)
  123. cancelRead()
  124. var clientClose coderws.CloseError
  125. require.ErrorAs(t, err, &clientClose)
  126. require.Equal(t, coderws.StatusGoingAway, clientClose.Code)
  127. require.Equal(t, "websocket request canceled", clientClose.Reason)
  128. select {
  129. case <-serverResult:
  130. case <-time.After(time.Second):
  131. t.Fatal("server read goroutine leaked after parent cancellation")
  132. }
  133. }