Add the unauthenticated reachability probe
This commit is contained in:
@@ -512,6 +512,13 @@ Validate: internal probe tests for 2xx/401/404/405, transport failure, timeout,
|
||||
and unsupported injected transport. Derive evidence from Stage 7 rather than
|
||||
repeat redirect matrices here. Close response bodies; do not retain them.
|
||||
|
||||
**Completed: 2026-10-13.** Added the unauthenticated, single-GET reachability
|
||||
probe over the built-in evidence-aware HTTP client. It records any response as
|
||||
reachable regardless of status, closes and discards bodies, and reports missing
|
||||
HTTP support without fallback. Validation passed: `go test -race
|
||||
./internal/usecase ./internal/llm`, `go vet ./...`, `go test ./...`, and
|
||||
`git diff --check`.
|
||||
|
||||
## Stage 12: Implement Authentication Applicability And OpenRouter Probe
|
||||
|
||||
Scope: named HTTP authentication adapter; no inference orchestration.
|
||||
|
||||
77
internal/usecase/smoke_reachability.go
Normal file
77
internal/usecase/smoke_reachability.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/generation"
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/llm"
|
||||
)
|
||||
|
||||
type smokeHTTPGetter interface {
|
||||
GetWithEvidence(context.Context, string, int) (*http.Response, llm.HTTPExecutionEvidence, error)
|
||||
}
|
||||
|
||||
type reachabilityProbe struct {
|
||||
client smokeHTTPGetter
|
||||
}
|
||||
|
||||
type reachabilityProbeOutcome struct {
|
||||
supported bool
|
||||
reason domain.SmokeTestReason
|
||||
initialEndpoint string
|
||||
reachable *bool
|
||||
evidence llm.HTTPExecutionEvidence
|
||||
failure domain.ExecutionFailureKind
|
||||
}
|
||||
|
||||
func (p reachabilityProbe) run(
|
||||
ctx context.Context,
|
||||
endpoint string,
|
||||
timeoutSeconds int,
|
||||
) (reachabilityProbeOutcome, error) {
|
||||
if p.client == nil {
|
||||
return reachabilityProbeOutcome{
|
||||
reason: domain.SmokeTestReasonHTTPDiagnosticsUnavailable,
|
||||
}, nil
|
||||
}
|
||||
|
||||
endpoint, err := endpointWithoutUserInfo(endpoint)
|
||||
if err != nil {
|
||||
return reachabilityProbeOutcome{}, fmt.Errorf("prepare reachability endpoint: %w", err)
|
||||
}
|
||||
result := reachabilityProbeOutcome{supported: true, initialEndpoint: endpoint}
|
||||
response, evidence, requestErr := p.client.GetWithEvidence(ctx, endpoint, timeoutSeconds)
|
||||
if response != nil && response.Body != nil {
|
||||
_ = response.Body.Close()
|
||||
}
|
||||
result.evidence = evidence
|
||||
if evidence.ResponseReceived || response != nil {
|
||||
result.reachable = boolPointer(true)
|
||||
return result, nil
|
||||
}
|
||||
if evidence.DispatchInvoked {
|
||||
result.reachable = boolPointer(false)
|
||||
}
|
||||
if requestErr != nil {
|
||||
result.failure = generation.ClassifyFailure(fmt.Errorf("%w: %w", llm.ErrTransport, requestErr))
|
||||
if !evidence.DispatchInvoked {
|
||||
return result, fmt.Errorf("execute reachability request: %w", requestErr)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func endpointWithoutUserInfo(endpoint string) (string, error) {
|
||||
parsed, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parsed.User = nil
|
||||
return parsed.String(), nil
|
||||
}
|
||||
|
||||
func boolPointer(value bool) *bool { return &value }
|
||||
136
internal/usecase/smoke_reachability_test.go
Normal file
136
internal/usecase/smoke_reachability_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/llm"
|
||||
)
|
||||
|
||||
func TestReachabilityProbeTreatsEveryHTTPResponseAsReachable(t *testing.T) {
|
||||
for _, status := range []int{
|
||||
http.StatusNoContent,
|
||||
http.StatusUnauthorized,
|
||||
http.StatusNotFound,
|
||||
http.StatusMethodNotAllowed,
|
||||
} {
|
||||
t.Run(http.StatusText(status), func(t *testing.T) {
|
||||
calls := 0
|
||||
body := &reachabilityProbeBody{}
|
||||
client := reachabilityClient(t, func(request *http.Request) (*http.Response, error) {
|
||||
calls++
|
||||
if request.Method != http.MethodGet {
|
||||
t.Errorf("method = %q, want GET", request.Method)
|
||||
}
|
||||
return &http.Response{StatusCode: status, Header: make(http.Header), Body: body}, nil
|
||||
})
|
||||
|
||||
result, err := (reachabilityProbe{client: client}).run(context.Background(), "https://example.invalid/health", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if calls != 1 || !body.closed {
|
||||
t.Fatalf("calls=%d body closed=%v", calls, body.closed)
|
||||
}
|
||||
if !result.supported || result.reachable == nil || !*result.reachable || !result.evidence.ResponseReceived {
|
||||
t.Fatalf("response evidence = %+v", result)
|
||||
}
|
||||
if result.evidence.HTTPStatus == nil || *result.evidence.HTTPStatus != status || result.failure != domain.ExecutionFailureNone {
|
||||
t.Fatalf("status/failure = %v / %v", result.evidence.HTTPStatus, result.failure)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReachabilityProbeRemovesURLCredentials(t *testing.T) {
|
||||
var observed *http.Request
|
||||
client := reachabilityClient(t, func(request *http.Request) (*http.Response, error) {
|
||||
observed = request
|
||||
return &http.Response{StatusCode: http.StatusNoContent, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(""))}, nil
|
||||
})
|
||||
endpoint := "https://operator:secret@example.invalid/health?check=1"
|
||||
|
||||
result, err := (reachabilityProbe{client: client}).run(context.Background(), endpoint, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if observed == nil || observed.URL.User != nil || observed.Header.Get("Authorization") != "" {
|
||||
t.Fatalf("probe request retained URL credentials: %+v", observed)
|
||||
}
|
||||
if result.initialEndpoint != "https://example.invalid/health?check=1" || result.evidence.CredentialsSupplied {
|
||||
t.Fatalf("credential-free evidence = %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReachabilityProbeReportsTransportFailureWithoutResponse(t *testing.T) {
|
||||
client := reachabilityClient(t, func(*http.Request) (*http.Response, error) {
|
||||
return nil, errors.New("opaque network failure")
|
||||
})
|
||||
|
||||
result, err := (reachabilityProbe{client: client}).run(context.Background(), "https://example.invalid/health", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.reachable == nil || *result.reachable || result.evidence.ResponseReceived || result.evidence.HTTPStatus != nil {
|
||||
t.Fatalf("no-response evidence = %+v", result)
|
||||
}
|
||||
if !result.evidence.DispatchInvoked || !result.evidence.RequestObserved || result.failure != domain.ExecutionFailureTransport {
|
||||
t.Fatalf("transport failure evidence = %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReachabilityProbeUsesTargetTimeout(t *testing.T) {
|
||||
client := reachabilityClient(t, func(request *http.Request) (*http.Response, error) {
|
||||
<-request.Context().Done()
|
||||
return nil, request.Context().Err()
|
||||
})
|
||||
|
||||
result, err := (reachabilityProbe{client: client}).run(context.Background(), "https://example.invalid/health", 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.reachable == nil || *result.reachable || result.failure != domain.ExecutionFailureDeadline {
|
||||
t.Fatalf("timeout evidence = %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReachabilityProbeIsUnsupportedWithoutHTTPClient(t *testing.T) {
|
||||
result, err := (reachabilityProbe{}).run(context.Background(), "https://example.invalid/health", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.supported || result.reason != domain.SmokeTestReasonHTTPDiagnosticsUnavailable || result.reachable != nil || result.evidence.DispatchInvoked {
|
||||
t.Fatalf("unsupported outcome = %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func reachabilityClient(t *testing.T, roundTrip func(*http.Request) (*http.Response, error)) *llm.OpenAICompatibleClient {
|
||||
t.Helper()
|
||||
client, err := llm.NewOpenAICompatibleClient(llm.OpenAICompatibleConfig{
|
||||
HTTPClient: &http.Client{Transport: reachabilityProbeTransport(roundTrip)},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
type reachabilityProbeTransport func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (t reachabilityProbeTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return t(request)
|
||||
}
|
||||
|
||||
type reachabilityProbeBody struct{ closed bool }
|
||||
|
||||
func (*reachabilityProbeBody) Read([]byte) (int, error) { return 0, io.EOF }
|
||||
|
||||
func (b *reachabilityProbeBody) Close() error {
|
||||
b.closed = true
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user