diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 8b37580..cf6e49c 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -577,6 +577,12 @@ measurements are observations, not performance guarantees. override endpoints, and privacy. Stage 7's bound remains in the actual path; Stage 22 finishes use-case cleanup and report projection. +Completion: the outbound adapter owns OpenRouter endpoint derivation, bounded +authentication requests, and account recognition. With maintainer approval, +orchestration was routed through it here to avoid duplicate implementations; +the report-copy cleanup remains in the next unit. Focused llm, use-case, engine, +and public smoke tests pass on Go 1.27.3. + ## Stage 22: Narrow Diagnostic Orchestration And Copies **Catalog:** 26, 27, completion. diff --git a/internal/llm/openrouter_authentication.go b/internal/llm/openrouter_authentication.go new file mode 100644 index 0000000..a3afd5b --- /dev/null +++ b/internal/llm/openrouter_authentication.go @@ -0,0 +1,105 @@ +package llm + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/url" + + "go.maximumdirect.net/promptkit/internal/domain" +) + +const maxAuthenticationResponseBytes = 1 << 20 + +// AuthenticationEvidence describes the observable result of a key probe. +type AuthenticationEvidence struct { + HTTP HTTPExecutionEvidence + Accepted *bool + Overflow bool + Unrecognized bool +} + +// OpenRouterKeyEndpoint derives the default key endpoint from a base endpoint. +func OpenRouterKeyEndpoint(base string) (string, error) { + return url.JoinPath(base, "key") +} + +// ProbeOpenRouterKey executes and interprets an OpenRouter key request. +func (c *OpenAICompatibleClient) ProbeOpenRouterKey(ctx context.Context, endpoint string, timeoutSeconds int, target domain.ExecutionTarget) (AuthenticationEvidence, error) { + parsed, err := url.Parse(endpoint) + if err != nil { + return AuthenticationEvidence{}, err + } + parsed.User = nil + response, evidence, requestErr := c.GetWithAuthenticationEvidence(ctx, parsed.String(), timeoutSeconds, target) + result := AuthenticationEvidence{HTTP: evidence} + if response != nil && response.Body != nil { + defer response.Body.Close() + } + if !evidence.ResponseReceived && response == nil { + return result, requestErr + } + status := evidence.HTTPStatus + if status == nil && response != nil { + status = &response.StatusCode + } + if status == nil { + result.Unrecognized = true + return result, nil + } + switch *status { + case http.StatusUnauthorized: + accepted := false + result.Accepted = &accepted + case http.StatusOK: + if response == nil || response.Body == nil { + result.Unrecognized = true + return result, nil + } + contents, err := io.ReadAll(io.LimitReader(response.Body, maxAuthenticationResponseBytes+1)) + if len(contents) > maxAuthenticationResponseBytes { + result.Overflow = true + return result, nil + } + if err != nil { + return result, err + } + if recognizedOpenRouterKeyResponse(contents) { + accepted := true + result.Accepted = &accepted + } else { + result.Unrecognized = true + } + } + return result, nil +} + +func recognizedOpenRouterKeyResponse(contents []byte) bool { + var envelope map[string]json.RawMessage + if err := json.Unmarshal(contents, &envelope); err != nil || envelope == nil { + return false + } + var data map[string]json.RawMessage + if err := json.Unmarshal(envelope["data"], &data); err != nil || data == nil { + return false + } + label := bytes.TrimSpace(data["label"]) + var labelValue string + if len(label) == 0 || label[0] != '"' || json.Unmarshal(label, &labelValue) != nil { + return false + } + freeTier := bytes.TrimSpace(data["is_free_tier"]) + if !bytes.Equal(freeTier, []byte("true")) && !bytes.Equal(freeTier, []byte("false")) { + return false + } + decoder := json.NewDecoder(bytes.NewReader(data["usage"])) + decoder.UseNumber() + var number any + if decoder.Decode(&number) != nil { + return false + } + _, ok := number.(json.Number) + return ok +} diff --git a/internal/llm/openrouter_authentication_test.go b/internal/llm/openrouter_authentication_test.go new file mode 100644 index 0000000..f071dc3 --- /dev/null +++ b/internal/llm/openrouter_authentication_test.go @@ -0,0 +1,146 @@ +package llm + +import ( + "context" + "errors" + "io" + "net/http" + "strings" + "testing" + + "go.maximumdirect.net/promptkit/internal/domain" +) + +func TestProbeOpenRouterKey(t *testing.T) { + valid := `{"data":{"label":"x","is_free_tier":false,"usage":0}}` + for _, tc := range []struct { + name string + status int + body string + key string + accepted *bool + unrecognized, overflow bool + }{ + {name: "recognized", status: 200, body: valid, key: "secret", accepted: boolValue(true)}, + {name: "exact limit", status: 200, body: valid + strings.Repeat(" ", maxAuthenticationResponseBytes-len(valid)), accepted: boolValue(true)}, + {name: "unknown", status: 200, body: `{"data":{"label":"x","is_free_tier":false,"usage":"0"}}`, unrecognized: true}, + {name: "trailing value", status: 200, body: valid + ` {}`, unrecognized: true}, + {name: "overflow", status: 200, body: strings.Repeat("x", maxAuthenticationResponseBytes+1), overflow: true}, + {name: "unauthorized anonymous", status: 401, accepted: boolValue(false)}, + {name: "other status", status: 503}, + } { + t.Run(tc.name, func(t *testing.T) { + var authorization string + client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{HTTPClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + if req.URL.User != nil { + t.Fatal("request retained URL credentials") + } + authorization = req.Header.Get("Authorization") + return &http.Response{StatusCode: tc.status, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(tc.body))}, nil + })}}) + if err != nil { + t.Fatal(err) + } + got, err := client.ProbeOpenRouterKey(context.Background(), "https://user:password@example.invalid/key", 0, domain.ExecutionTarget{APIKey: tc.key}) + if err != nil { + t.Fatal(err) + } + if got.HTTP.Endpoint != "https://example.invalid/key" || got.HTTP.HTTPStatus == nil || *got.HTTP.HTTPStatus != tc.status || got.Unrecognized != tc.unrecognized || got.Overflow != tc.overflow { + t.Fatalf("evidence = %+v", got) + } + if (got.Accepted == nil) != (tc.accepted == nil) || got.Accepted != nil && *got.Accepted != *tc.accepted { + t.Fatalf("acceptance = %v", got.Accepted) + } + if (authorization != "") != (tc.key != "") || got.HTTP.CredentialsSupplied != (tc.key != "") { + t.Fatalf("credential evidence = %+v", got.HTTP) + } + }) + } +} + +func boolValue(value bool) *bool { return &value } + +type authenticationFailingBody struct{ err error } + +func (b authenticationFailingBody) Read([]byte) (int, error) { return 0, b.err } +func (b authenticationFailingBody) Close() error { return nil } + +func TestProbeOpenRouterKeyRetainsBodyFailure(t *testing.T) { + want := errors.New("interrupted") + client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{HTTPClient: &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { + return &http.Response{StatusCode: 200, Header: make(http.Header), Body: authenticationFailingBody{err: want}}, nil + })}}) + if err != nil { + t.Fatal(err) + } + got, err := client.ProbeOpenRouterKey(context.Background(), "https://example.invalid/key", 0, domain.ExecutionTarget{}) + if !errors.Is(err, want) || !got.HTTP.ResponseReceived { + t.Fatalf("evidence = %+v, error = %v", got, err) + } +} + +type observedAuthenticationBody struct { + reader io.Reader + read int + closed bool +} + +func (b *observedAuthenticationBody) Read(p []byte) (int, error) { + n, err := b.reader.Read(p) + b.read += n + return n, err +} + +func (b *observedAuthenticationBody) Close() error { + b.closed = true + return nil +} + +func TestProbeOpenRouterKeyBoundsAndClosesBodies(t *testing.T) { + for _, tc := range []struct { + name string + status int + wantRead int + }{ + {"overflow", http.StatusOK, maxAuthenticationResponseBytes + 1}, + {"unauthorized", http.StatusUnauthorized, 0}, + {"other status", http.StatusServiceUnavailable, 0}, + } { + t.Run(tc.name, func(t *testing.T) { + body := &observedAuthenticationBody{reader: strings.NewReader(strings.Repeat("x", 2*maxAuthenticationResponseBytes))} + client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{HTTPClient: &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { + return &http.Response{StatusCode: tc.status, ContentLength: 1, Header: make(http.Header), Body: body}, nil + })}}) + if err != nil { + t.Fatal(err) + } + got, err := client.ProbeOpenRouterKey(context.Background(), "https://example.invalid/key", 0, domain.ExecutionTarget{}) + if err != nil || body.read != tc.wantRead || !body.closed || got.Overflow != (tc.status == http.StatusOK) { + t.Fatalf("evidence = %+v, read = %d, closed = %v, error = %v", got, body.read, body.closed, err) + } + }) + } +} + +func TestOpenRouterKeyEndpoint(t *testing.T) { + got, err := OpenRouterKeyEndpoint("https://example.invalid/api/v1/") + if err != nil || got != "https://example.invalid/api/v1/key" { + t.Fatalf("endpoint = %q, error = %v", got, err) + } +} + +func TestProbeOpenRouterKeyPreservesCancellation(t *testing.T) { + client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{HTTPClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + <-req.Context().Done() + return nil, req.Context().Err() + })}}) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() + got, err := client.ProbeOpenRouterKey(ctx, "https://example.invalid/key", 0, domain.ExecutionTarget{}) + if !errors.Is(err, context.Canceled) || got.Accepted != nil || got.HTTP.ResponseReceived { + t.Fatalf("evidence = %+v, error = %v", got, err) + } +} diff --git a/internal/usecase/smoke_authentication.go b/internal/usecase/smoke_authentication.go index bee43c5..cb81507 100644 --- a/internal/usecase/smoke_authentication.go +++ b/internal/usecase/smoke_authentication.go @@ -1,11 +1,8 @@ package usecase import ( - "bytes" "context" - "encoding/json" "fmt" - "io" "net/http" "go.maximumdirect.net/promptkit/internal/domain" @@ -13,11 +10,11 @@ import ( "go.maximumdirect.net/promptkit/internal/llm" ) -type authenticationProbe struct { - client smokeHTTPGetter +type smokeAuthenticationClient interface { + ProbeOpenRouterKey(context.Context, string, int, domain.ExecutionTarget) (llm.AuthenticationEvidence, error) } -const maxAuthenticationResponseBytes = 1 << 20 +type authenticationProbe struct{ client smokeAuthenticationClient } type authenticationProbeOutcome struct { supported bool @@ -28,16 +25,33 @@ type authenticationProbeOutcome struct { failure domain.ExecutionFailureKind } -func (p authenticationProbe) run( - ctx context.Context, - backend domain.Backend, - target domain.SmokeTestTarget, -) (authenticationProbeOutcome, error) { +func (p authenticationProbe) run(ctx context.Context, backend domain.Backend, target domain.SmokeTestTarget) (authenticationProbeOutcome, error) { outcome, err := p.support(backend) if err != nil || !outcome.supported || (outcome.applicable != nil && !*outcome.applicable) { return outcome, err } - return p.runOpenRouterKey(ctx, target) + result, requestErr := p.client.ProbeOpenRouterKey(ctx, target.Endpoint, target.EffectiveSettings.TimeoutSeconds, target.EffectiveSettings) + outcome.evidence = result.HTTP + outcome.credentialsAccepted = result.Accepted + if result.Overflow { + outcome.reason = domain.SmokeTestReasonAuthenticationResponseLimitExceeded + outcome.failure = domain.ExecutionFailureOther + } else if result.Unrecognized { + outcome.reason = domain.SmokeTestReasonAuthenticationResponseUnrecognized + outcome.failure = domain.ExecutionFailureMalformedResponse + } else if requestErr != nil { + if !result.HTTP.DispatchInvoked { + return outcome, fmt.Errorf("execute authentication request: %w", requestErr) + } + outcome.failure = httpProbeFailure(ctx, requestErr) + } else if result.HTTP.ResponseReceived { + if result.HTTP.HTTPStatus != nil && *result.HTTP.HTTPStatus != http.StatusOK && *result.HTTP.HTTPStatus != http.StatusUnauthorized { + outcome.failure = generation.ClassifyFailure(llm.ErrUnexpectedStatus) + } + } else { + outcome.failure = httpProbeFailure(ctx, nil) + } + return outcome, nil } func (p authenticationProbe) support(backend domain.Backend) (authenticationProbeOutcome, error) { @@ -50,11 +64,7 @@ func (p authenticationProbe) support(backend domain.Backend) (authenticationProb } switch *authentication.Kind { case "none": - return authenticationProbeOutcome{ - supported: true, - applicable: boolPointer(false), - reason: domain.SmokeTestReasonAuthenticationNotApplicable, - }, nil + return authenticationProbeOutcome{supported: true, applicable: boolPointer(false), reason: domain.SmokeTestReasonAuthenticationNotApplicable}, nil case "openrouter_key": if p.client == nil { return authenticationProbeOutcome{reason: domain.SmokeTestReasonHTTPDiagnosticsUnavailable}, nil @@ -64,139 +74,3 @@ func (p authenticationProbe) support(backend domain.Backend) (authenticationProb return authenticationProbeOutcome{}, fmt.Errorf("%w: unsupported authentication probe kind %q", ErrInvalidRequest, *authentication.Kind) } } - -func (p authenticationProbe) runOpenRouterKey( - ctx context.Context, - target domain.SmokeTestTarget, -) (authenticationProbeOutcome, error) { - endpoint, err := endpointWithoutUserInfo(target.Endpoint) - if err != nil { - return authenticationProbeOutcome{}, fmt.Errorf("prepare authentication endpoint: %w", err) - } - response, evidence, requestErr := p.client.GetWithAuthenticationEvidence( - ctx, - endpoint, - target.EffectiveSettings.TimeoutSeconds, - target.EffectiveSettings, - ) - if response != nil && response.Body != nil { - defer response.Body.Close() - } - outcome := authenticationProbeOutcome{ - supported: true, - applicable: boolPointer(true), - evidence: evidence, - } - if evidence.ResponseReceived || response != nil { - status := evidence.HTTPStatus - if status == nil && response != nil { - status = &response.StatusCode - } - if status == nil { - outcome.failure = domain.ExecutionFailureMalformedResponse - outcome.reason = domain.SmokeTestReasonAuthenticationResponseUnrecognized - return outcome, nil - } - switch *status { - case http.StatusUnauthorized: - outcome.credentialsAccepted = boolPointer(false) - case http.StatusOK: - if response == nil || response.Body == nil { - outcome.reason = domain.SmokeTestReasonAuthenticationResponseUnrecognized - outcome.failure = domain.ExecutionFailureMalformedResponse - return outcome, nil - } - 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 - } - if recognized { - outcome.credentialsAccepted = boolPointer(true) - } else { - outcome.reason = domain.SmokeTestReasonAuthenticationResponseUnrecognized - outcome.failure = domain.ExecutionFailureMalformedResponse - } - default: - outcome.failure = generation.ClassifyFailure(llm.ErrUnexpectedStatus) - } - return outcome, nil - } - - if evidence.DispatchInvoked { - if requestErr != nil { - outcome.failure = httpProbeFailure(ctx, requestErr) - } else { - outcome.failure = httpProbeFailure(ctx, nil) - } - return outcome, nil - } - if requestErr != nil { - return outcome, fmt.Errorf("execute authentication request: %w", requestErr) - } - outcome.failure = generation.ClassifyFailure(llm.ErrTransport) - return outcome, nil -} - -func recognizedOpenRouterKeyResponse(body io.Reader) (bool, error) { - contents, err := io.ReadAll(body) - if err != nil { - return false, err - } - var envelope map[string]json.RawMessage - if err := json.Unmarshal(contents, &envelope); err != nil || envelope == nil { - return false, nil - } - - dataJSON, ok := envelope["data"] - if !ok || !isJSONObject(dataJSON) { - return false, nil - } - var data map[string]json.RawMessage - if err := json.Unmarshal(dataJSON, &data); err != nil || data == nil { - return false, nil - } - label, labelOK := data["label"] - freeTier, freeTierOK := data["is_free_tier"] - usage, usageOK := data["usage"] - if !labelOK || !isJSONString(label) || !freeTierOK || !isJSONBoolean(freeTier) || !usageOK { - return false, nil - } - var number any - usageDecoder := json.NewDecoder(bytes.NewReader(usage)) - usageDecoder.UseNumber() - if usageDecoder.Decode(&number) != nil { - return false, nil - } - _, ok = number.(json.Number) - return ok, nil -} - -func isJSONObject(value json.RawMessage) bool { - value = bytes.TrimSpace(value) - return len(value) > 0 && value[0] == '{' -} - -func isJSONString(value json.RawMessage) bool { - value = bytes.TrimSpace(value) - if len(value) == 0 || value[0] != '"' { - return false - } - var parsed string - return json.Unmarshal(value, &parsed) == nil -} - -func isJSONBoolean(value json.RawMessage) bool { - value = bytes.TrimSpace(value) - return bytes.Equal(value, []byte("true")) || bytes.Equal(value, []byte("false")) -} diff --git a/internal/usecase/smoke_authentication_test.go b/internal/usecase/smoke_authentication_test.go index 8508fb0..545d059 100644 --- a/internal/usecase/smoke_authentication_test.go +++ b/internal/usecase/smoke_authentication_test.go @@ -26,6 +26,7 @@ func (b *measuredAuthenticationBody) Read(p []byte) (int, error) { func (b *measuredAuthenticationBody) Close() error { b.closed = true; return nil } func TestAuthenticationResponseRecognitionBound(t *testing.T) { + const maxAuthenticationResponseBytes = 1 << 20 valid := `{"data":{"label":"member","is_free_tier":false,"usage":0}}` for _, test := range []struct { name string @@ -211,21 +212,26 @@ func TestAuthenticationProbeInterpretsOpenRouterResponses(t *testing.T) { } func TestAuthenticationProbeRecognizesOnlyOneCompleteJSONObject(t *testing.T) { - for _, body := range []string{ - `{"data":{"label":"member","is_free_tier":false,"usage":1}} {}`, - `{"data":[]}`, - `{"data":{"label":"member","is_free_tier":false,"usage":null}}`, - `{"data":{"label":"member","is_free_tier":0,"usage":1}}`, - `{"data":{"is_free_tier":false,"usage":1}}`, - `{"data":{"label":null,"is_free_tier":false,"usage":1}}`, + for _, tc := range []struct { + body string + accepted bool + }{ + {`{"data":{"label":"member","is_free_tier":false,"usage":1}} {}`, false}, + {`{"data":[]}`, false}, + {`{"data":{"label":"member","is_free_tier":false,"usage":null}}`, false}, + {`{"data":{"label":"member","is_free_tier":0,"usage":1}}`, false}, + {`{"data":{"is_free_tier":false,"usage":1}}`, false}, + {`{"data":{"label":null,"is_free_tier":false,"usage":1}}`, false}, + {`{"data":{"label":"","is_free_tier":false,"usage":1e2,"ignored":true},"ignored":true}`, true}, } { - if recognized, err := recognizedOpenRouterKeyResponse(strings.NewReader(body)); err != nil || recognized { - t.Errorf("recognized invalid account response %s", body) + client := reachabilityClient(t, func(*http.Request) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusOK, Header: make(http.Header), Body: ioNopCloser(tc.body)}, nil + }) + outcome, err := (authenticationProbe{client: client}).run(context.Background(), backendWithAuthenticationKind("openrouter_key"), domain.SmokeTestTarget{Endpoint: "https://example.invalid/key"}) + if err != nil || (outcome.credentialsAccepted != nil && *outcome.credentialsAccepted) != tc.accepted { + t.Errorf("account response %s: outcome = %+v, error = %v", tc.body, outcome, err) } } - if recognized, err := recognizedOpenRouterKeyResponse(strings.NewReader(`{"data":{"label":"","is_free_tier":false,"usage":1e2,"ignored":true},"ignored":true}`)); err != nil || !recognized { - t.Fatal("valid minimal identifying response was rejected") - } } func TestAuthenticationProbeTreatsInterruptedResponseBodyAsTransportFailure(t *testing.T) { diff --git a/internal/usecase/smoke_reachability.go b/internal/usecase/smoke_reachability.go index 683b15d..503dbaf 100644 --- a/internal/usecase/smoke_reachability.go +++ b/internal/usecase/smoke_reachability.go @@ -14,7 +14,6 @@ import ( type smokeHTTPGetter interface { GetWithEvidence(context.Context, string, int) (*http.Response, llm.HTTPExecutionEvidence, error) - GetWithAuthenticationEvidence(context.Context, string, int, domain.ExecutionTarget) (*http.Response, llm.HTTPExecutionEvidence, error) } type reachabilityProbe struct { diff --git a/internal/usecase/smoke_selection.go b/internal/usecase/smoke_selection.go index 4a84de0..fa88f7b 100644 --- a/internal/usecase/smoke_selection.go +++ b/internal/usecase/smoke_selection.go @@ -3,10 +3,10 @@ package usecase import ( "context" "fmt" - "net/url" "go.maximumdirect.net/promptkit/internal/defaults" "go.maximumdirect.net/promptkit/internal/domain" + "go.maximumdirect.net/promptkit/internal/llm" ) const ( @@ -176,7 +176,7 @@ func resolveSmokeTestEndpoint(kind domain.SmokeTestKind, baseEndpoint string, sm return *authentication.Endpoint, nil } if authentication.Kind != nil && *authentication.Kind == "openrouter_key" { - return url.JoinPath(baseEndpoint, "key") + return llm.OpenRouterKeyEndpoint(baseEndpoint) } } } diff --git a/internal/usecase/smoke_tester.go b/internal/usecase/smoke_tester.go index eb1bd3d..afdd501 100644 --- a/internal/usecase/smoke_tester.go +++ b/internal/usecase/smoke_tester.go @@ -17,6 +17,7 @@ import ( type SmokeTester struct { runner *Runner httpClient smokeHTTPGetter + authClient smokeAuthenticationClient now func() time.Time } @@ -24,18 +25,22 @@ type SmokeTester struct { // leaves HTTP operations unsupported rather than falling back to another client. func NewSmokeTester(runner *Runner, httpClient *llm.OpenAICompatibleClient) *SmokeTester { var getter smokeHTTPGetter + var authentication smokeAuthenticationClient if httpClient != nil { getter = httpClient + authentication = httpClient } - return &SmokeTester{runner: runner, httpClient: getter, now: time.Now} + return &SmokeTester{runner: runner, httpClient: getter, authClient: authentication, now: time.Now} } func newSmokeTesterWithClock(runner *Runner, httpClient *llm.OpenAICompatibleClient, now func() time.Time) *SmokeTester { var getter smokeHTTPGetter + var authentication smokeAuthenticationClient if httpClient != nil { getter = httpClient + authentication = httpClient } - return &SmokeTester{runner: runner, httpClient: getter, now: now} + return &SmokeTester{runner: runner, httpClient: getter, authClient: authentication, now: now} } // Run selects and executes one diagnostic, retaining only factual results. @@ -126,7 +131,7 @@ func (t *SmokeTester) Run(ctx context.Context, request domain.SmokeTestRequest) return t.finishHTTP(ctx, report, finish) case domain.SmokeTestAuthentication: requestStartedAt := now().UTC() - outcome, err := (authenticationProbe{client: t.httpClient}).run(ctx, selection.backend, selection.target) + outcome, err := (authenticationProbe{client: t.authClient}).run(ctx, selection.backend, selection.target) requestDuration := nonnegativeDuration(now().UTC().Sub(requestStartedAt)) if err != nil { if contextErr := ctx.Err(); contextErr != nil { @@ -349,7 +354,7 @@ func (t *SmokeTester) setSupport(report *domain.SmokeTestReport, selection *smok report.Applicable = boolPointer(true) return nil case domain.SmokeTestAuthentication: - outcome, err := (authenticationProbe{client: t.httpClient}).support(selection.backend) + outcome, err := (authenticationProbe{client: t.authClient}).support(selection.backend) if err != nil { return err }