Bound authentication response recognition

This commit is contained in:
2026-11-05 13:49:51 +00:00
parent 1c075bd510
commit 7be677c80b
6 changed files with 93 additions and 7 deletions

View File

@@ -125,3 +125,17 @@ func TestBundledSmokeSelectionUsesEffectiveConfiguration(t *testing.T) {
}
}
}
func TestBundledSmokeAuthenticationResponseOverflow(t *testing.T) {
transport := roundTripFunc(func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(strings.Repeat("x", (1<<20)+1)))}, nil
})
engine, err := promptkit.NewEngine(promptkit.Config{HTTPClient: &http.Client{Transport: transport}})
if err != nil {
t.Fatal(err)
}
report, err := engine.SmokeTest(context.Background(), promptkit.SmokeTestRequest{Kind: promptkit.SmokeTestAuthentication, BackendID: "openrouter", APIKey: "private-key-marker"})
if err != nil || report == nil || !report.Completed || report.Reason != promptkit.SmokeTestReasonAuthenticationResponseLimitExceeded || report.Failure != promptkit.ExecutionFailureOther || report.Facts.CredentialsAccepted != nil {
t.Fatalf("overflow = %+v, %v", report, err)
}
}

View File

@@ -224,6 +224,10 @@ limit plus one, missing/inaccurate lengths, malformed bounded bodies, closure,
401 without reading, cancellation, and redacted explicit overflow. Keep this
fix independent of the later protocol relocation.
Completion: authentication recognition reads at most 1 MiB plus one byte and
reports a distinct overflow reason without asserting credential rejection.
Focused authentication and public smoke tests pass on Go 1.27.3.
## Stage 8: Correct Artifact Result Ownership
**Catalog:** 4. **Files:** public ArtifactReader GoDoc, adapter and ownership tests.

View File

@@ -112,12 +112,13 @@ type SmokeTestAttempt struct {
type SmokeTestReason string
const (
SmokeTestReasonNone SmokeTestReason = ""
SmokeTestReasonAuthenticationNotApplicable SmokeTestReason = "authentication_not_applicable"
SmokeTestReasonProbeNotConfigured SmokeTestReason = "probe_not_configured"
SmokeTestReasonHTTPDiagnosticsUnavailable SmokeTestReason = "http_diagnostics_unavailable"
SmokeTestReasonRequiredCredentialMissing SmokeTestReason = "required_credential_missing"
SmokeTestReasonAuthenticationResponseUnrecognized SmokeTestReason = "authentication_response_unrecognized"
SmokeTestReasonNone SmokeTestReason = ""
SmokeTestReasonAuthenticationNotApplicable SmokeTestReason = "authentication_not_applicable"
SmokeTestReasonProbeNotConfigured SmokeTestReason = "probe_not_configured"
SmokeTestReasonHTTPDiagnosticsUnavailable SmokeTestReason = "http_diagnostics_unavailable"
SmokeTestReasonRequiredCredentialMissing SmokeTestReason = "required_credential_missing"
SmokeTestReasonAuthenticationResponseUnrecognized SmokeTestReason = "authentication_response_unrecognized"
SmokeTestReasonAuthenticationResponseLimitExceeded SmokeTestReason = "authentication_response_limit_exceeded"
)
// CloneBackend returns a copy of a backend, including its mutable settings.

View File

@@ -17,6 +17,8 @@ type authenticationProbe struct {
client smokeHTTPGetter
}
const maxAuthenticationResponseBytes = 1 << 20
type authenticationProbeOutcome struct {
supported bool
applicable *bool
@@ -104,7 +106,17 @@ func (p authenticationProbe) runOpenRouterKey(
outcome.failure = domain.ExecutionFailureMalformedResponse
return outcome, nil
}
recognized, bodyErr := recognizedOpenRouterKeyResponse(response.Body)
contents, bodyErr := io.ReadAll(io.LimitReader(response.Body, maxAuthenticationResponseBytes+1))
if len(contents) > maxAuthenticationResponseBytes {
outcome.reason = domain.SmokeTestReasonAuthenticationResponseLimitExceeded
outcome.failure = domain.ExecutionFailureOther
return outcome, nil
}
if bodyErr != nil {
outcome.failure = httpProbeFailure(ctx, bodyErr)
return outcome, nil
}
recognized, bodyErr := recognizedOpenRouterKeyResponse(bytes.NewReader(contents))
if bodyErr != nil {
outcome.failure = httpProbeFailure(ctx, bodyErr)
return outcome, nil

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
"testing"
@@ -11,6 +12,57 @@ import (
"go.maximumdirect.net/promptkit/internal/domain"
)
type measuredAuthenticationBody struct {
reader io.Reader
read int
closed bool
}
func (b *measuredAuthenticationBody) Read(p []byte) (int, error) {
n, err := b.reader.Read(p)
b.read += n
return n, err
}
func (b *measuredAuthenticationBody) Close() error { b.closed = true; return nil }
func TestAuthenticationResponseRecognitionBound(t *testing.T) {
valid := `{"data":{"label":"member","is_free_tier":false,"usage":0}}`
for _, test := range []struct {
name string
size int
declared int64
status int
overflow bool
}{
{"exact limit", maxAuthenticationResponseBytes, -1, 200, false},
{"overflow unknown length", maxAuthenticationResponseBytes + 10, -1, 200, true},
{"overflow understated length", maxAuthenticationResponseBytes + 10, 1, 200, true},
{"unauthorized does not read", maxAuthenticationResponseBytes + 10, -1, 401, false},
} {
t.Run(test.name, func(t *testing.T) {
body := &measuredAuthenticationBody{reader: strings.NewReader(valid + strings.Repeat(" ", test.size-len(valid)))}
client := reachabilityClient(t, func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: test.status, Header: make(http.Header), Body: body, ContentLength: test.declared}, nil
})
outcome, err := (authenticationProbe{client: client}).run(context.Background(), backendWithAuthenticationKind("openrouter_key"), domain.SmokeTestTarget{Endpoint: "https://example.invalid/key"})
if err != nil || !body.closed {
t.Fatalf("outcome = %+v, err = %v, closed = %v", outcome, err, body.closed)
}
if test.overflow {
if outcome.reason != domain.SmokeTestReasonAuthenticationResponseLimitExceeded || outcome.failure != domain.ExecutionFailureOther || outcome.credentialsAccepted != nil || body.read != maxAuthenticationResponseBytes+1 {
t.Fatalf("overflow = %+v, read = %d", outcome, body.read)
}
} else if test.status == 401 {
if body.read != 0 || outcome.credentialsAccepted == nil || *outcome.credentialsAccepted {
t.Fatalf("unauthorized = %+v, read = %d", outcome, body.read)
}
} else if outcome.credentialsAccepted == nil || !*outcome.credentialsAccepted {
t.Fatalf("exact limit = %+v", outcome)
}
})
}
}
func TestAuthenticationProbeApplicabilityAndHTTPAvailability(t *testing.T) {
client := reachabilityClient(t, func(*http.Request) (*http.Response, error) {
t.Fatal("authentication applicability unexpectedly dispatched")

View File

@@ -219,6 +219,9 @@ const (
// SmokeTestReasonAuthenticationResponseUnrecognized marks an HTTP response
// whose authentication meaning could not be established.
SmokeTestReasonAuthenticationResponseUnrecognized SmokeTestReason = "authentication_response_unrecognized"
// SmokeTestReasonAuthenticationResponseLimitExceeded indicates that the
// authentication response exceeded the recognition allowance.
SmokeTestReasonAuthenticationResponseLimitExceeded SmokeTestReason = "authentication_response_limit_exceeded"
)
// SmokeTest runs one built-in diagnostic against the engine's captured