Add the unauthenticated reachability probe

This commit is contained in:
2026-10-13 17:12:31 +00:00
parent 2b723760d1
commit 2e81c9e61b
3 changed files with 220 additions and 0 deletions

View File

@@ -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.

View 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 }

View 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
}