add the greeting msg
This commit is contained in:
170
go-api/internal/httpserver/gatewaymessage_test.go
Normal file
170
go-api/internal/httpserver/gatewaymessage_test.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package httpserver
|
||||
|
||||
// Unit tests for the wording a GatewayFailure produces.
|
||||
//
|
||||
// Internal rather than httpserver_test because the function under test is the
|
||||
// mapping itself, and the mapping is unexported. Pure: no server, no database,
|
||||
// no fixture — a cause goes in and a sentence comes out.
|
||||
//
|
||||
// What these assert is one property, and it is the one the old wording broke:
|
||||
// a reader is told to retry EXACTLY when retrying can work. A rate limit clears
|
||||
// on its own; a rejected credential, a model id the endpoint does not have, and
|
||||
// an unconfigured deployment do not, and telling somebody to wait a minute for
|
||||
// any of those is a loop with no exit.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/krow/krow-backend/go-api/internal/gateway"
|
||||
"github.com/krow/krow-backend/go-api/internal/runtime"
|
||||
)
|
||||
|
||||
// invitesRetry reports whether a sentence tells the reader to try again.
|
||||
//
|
||||
// Deliberately looser than an equality check on the whole string: what must
|
||||
// hold is the ADVICE, not the copy, so rewording a sentence does not fail a
|
||||
// test that was never about the words.
|
||||
func invitesRetry(message string) bool {
|
||||
m := strings.ToLower(message)
|
||||
return strings.Contains(m, "ask again") || strings.Contains(m, "try again")
|
||||
}
|
||||
|
||||
func TestGatewayFailureMessageInvitesRetryOnlyWhenRetryingCanWork(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
cause error
|
||||
retry bool
|
||||
}{
|
||||
{
|
||||
name: "a rate limit clears on its own",
|
||||
cause: &gateway.Error{Code: gateway.CodeRateLimited, Status: 429},
|
||||
retry: true,
|
||||
},
|
||||
{
|
||||
name: "a provider 5xx is worth another attempt",
|
||||
cause: &gateway.Error{Code: gateway.CodeUpstream, Status: 503},
|
||||
retry: true,
|
||||
},
|
||||
{
|
||||
name: "a rejected credential will be rejected again",
|
||||
cause: &gateway.Error{Code: gateway.CodeUnauthorized, Status: 401},
|
||||
retry: false,
|
||||
},
|
||||
{
|
||||
name: "a model the endpoint does not have stays absent",
|
||||
cause: &gateway.Error{Code: gateway.CodeInvalidRequest, Status: 404},
|
||||
retry: false,
|
||||
},
|
||||
{
|
||||
name: "an unconfigured deployment cannot answer at all",
|
||||
cause: &gateway.Error{Code: gateway.CodeNotConfigured},
|
||||
retry: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := gatewayFailureMessage(tc.cause)
|
||||
if got == "" {
|
||||
t.Fatal("a failed run must say something")
|
||||
}
|
||||
if invitesRetry(got) != tc.retry {
|
||||
t.Errorf("retry advice = %v, want %v\n message: %q",
|
||||
invitesRetry(got), tc.retry, got)
|
||||
}
|
||||
// Nothing ran, so nothing can have been written. The reassurance is
|
||||
// the whole reason this termination is not frightening.
|
||||
if !strings.Contains(got, "Nothing was changed") {
|
||||
t.Errorf("message must say nothing was changed: %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The three that need a person are the three that used to be indistinguishable
|
||||
// from load. Each must point at one, or the reader has no idea what to do next.
|
||||
func TestGatewayFailureMessageNamesAnAdministratorWhenOneIsNeeded(t *testing.T) {
|
||||
for _, code := range []string{
|
||||
gateway.CodeUnauthorized,
|
||||
gateway.CodeInvalidRequest,
|
||||
gateway.CodeNotConfigured,
|
||||
} {
|
||||
got := gatewayFailureMessage(&gateway.Error{Code: code})
|
||||
if !strings.Contains(strings.ToLower(got), "administrator") {
|
||||
t.Errorf("%s: must send the reader to an administrator: %q", code, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vendor names, model ids and HTTP statuses are for the trajectory, not for a
|
||||
// venue manager — they cannot act on any of them.
|
||||
func TestGatewayFailureMessageLeaksNoOperatorDetail(t *testing.T) {
|
||||
for _, code := range []string{
|
||||
gateway.CodeRateLimited,
|
||||
gateway.CodeUpstream,
|
||||
gateway.CodeUnauthorized,
|
||||
gateway.CodeInvalidRequest,
|
||||
gateway.CodeNotConfigured,
|
||||
} {
|
||||
got := gatewayFailureMessage(&gateway.Error{
|
||||
Code: code,
|
||||
Status: 429,
|
||||
Message: "gemini-3.5-flash-lite quota exceeded for project 12345",
|
||||
})
|
||||
for _, leak := range []string{"gemini", "429", "quota", "http", "12345"} {
|
||||
if strings.Contains(strings.ToLower(got), leak) {
|
||||
t.Errorf("%s: message carries operator detail %q: %q", code, leak, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A cause that is not a gateway error — lost, wrapped away, or a non-gateway
|
||||
// failure that reached this termination — still has to produce a sentence.
|
||||
func TestGatewayFailureMessageFallsBackWithoutAGatewayError(t *testing.T) {
|
||||
for _, cause := range []error{nil, errors.New("something else entirely")} {
|
||||
if got := gatewayFailureMessage(cause); got == "" {
|
||||
t.Errorf("cause %v produced no message", cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The cause arrives wrapped in a RuntimeError, which is how the surface
|
||||
// actually receives it. If Unwrap ever stopped reaching the gateway error,
|
||||
// every failure would silently fall back to "usually it is busy" — the exact
|
||||
// bug this change exists to fix, reintroduced without a compile error.
|
||||
func TestGatewayFailureMessageReadsThroughARuntimeError(t *testing.T) {
|
||||
wrapped := &runtime.RuntimeError{
|
||||
Code: "runtime.gatewayfailure",
|
||||
Cause: &gateway.Error{Code: gateway.CodeUnauthorized, Status: 401},
|
||||
}
|
||||
|
||||
got := gatewayFailureMessage(wrapped)
|
||||
if invitesRetry(got) {
|
||||
t.Errorf("a wrapped credential failure must not invite a retry: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The other terminations are unchanged by the new parameter: they ignore the
|
||||
// cause, so passing one must not alter a single word.
|
||||
func TestTerminationMessageIgnoresTheCauseElsewhere(t *testing.T) {
|
||||
cause := &gateway.Error{Code: gateway.CodeUnauthorized}
|
||||
|
||||
for _, term := range []runtime.Termination{
|
||||
runtime.TerminationBudgetExceeded,
|
||||
runtime.TerminationDeadline,
|
||||
runtime.TerminationConfirmationPending,
|
||||
runtime.TerminationToolFailure,
|
||||
runtime.TerminationRefused,
|
||||
} {
|
||||
if terminationMessage(term, nil) != terminationMessage(term, cause) {
|
||||
t.Errorf("%s: wording changed with the cause", term)
|
||||
}
|
||||
}
|
||||
|
||||
if terminationMessage(runtime.TerminationCompleted, nil) != "" {
|
||||
t.Error("a completed run has nothing to say")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user