diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index d3bfd7a..35366f6 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -591,6 +591,13 @@ selection before dispatch, canceled-before-dispatch, timeout with live caller, and available evidence after cancellation. Use real internal components with controlled HTTP effects, not a new mock framework. +**Completed: 2026-10-13.** Added the internal synchronous HTTP report +coordinator with captured selection, metadata applicability, timing, sanitized +report targets, per-attempt transport facts, and caller-cancellation handling. +HTTP probes bypass generation admission and inherited execution observation. +Validation passed: `go test -race ./internal/usecase ./internal/llm`, +`go vet ./...`, `go test ./...`, and `git diff --check`. + ## Stage 15: Integrate Inference Diagnostics And Engine Assembly Scope: inference orchestration in `internal/usecase` and `internal/engine` wiring. diff --git a/internal/usecase/smoke_authentication.go b/internal/usecase/smoke_authentication.go index 6920923..dceb8fc 100644 --- a/internal/usecase/smoke_authentication.go +++ b/internal/usecase/smoke_authentication.go @@ -31,6 +31,14 @@ func (p authenticationProbe) run( 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) +} + +func (p authenticationProbe) support(backend domain.Backend) (authenticationProbeOutcome, error) { if backend.SmokeTests == nil || backend.SmokeTests.Authentication == nil { return authenticationProbeOutcome{reason: domain.SmokeTestReasonProbeNotConfigured}, nil } @@ -46,7 +54,10 @@ func (p authenticationProbe) run( reason: domain.SmokeTestReasonAuthenticationNotApplicable, }, nil case "openrouter_key": - return p.runOpenRouterKey(ctx, target) + if p.client == nil { + return authenticationProbeOutcome{reason: domain.SmokeTestReasonHTTPDiagnosticsUnavailable}, nil + } + return authenticationProbeOutcome{supported: true, applicable: boolPointer(true)}, nil default: return authenticationProbeOutcome{}, fmt.Errorf("%w: unsupported authentication probe kind %q", ErrInvalidRequest, *authentication.Kind) } @@ -56,14 +67,6 @@ func (p authenticationProbe) runOpenRouterKey( ctx context.Context, target domain.SmokeTestTarget, ) (authenticationProbeOutcome, error) { - applicable := boolPointer(true) - if p.client == nil { - return authenticationProbeOutcome{ - applicable: applicable, - reason: domain.SmokeTestReasonHTTPDiagnosticsUnavailable, - }, nil - } - endpoint, err := endpointWithoutUserInfo(target.Endpoint) if err != nil { return authenticationProbeOutcome{}, fmt.Errorf("prepare authentication endpoint: %w", err) @@ -79,7 +82,7 @@ func (p authenticationProbe) runOpenRouterKey( } outcome := authenticationProbeOutcome{ supported: true, - applicable: applicable, + applicable: boolPointer(true), evidence: evidence, } if evidence.ResponseReceived || response != nil { diff --git a/internal/usecase/smoke_authentication_test.go b/internal/usecase/smoke_authentication_test.go index fed0a8b..4bcdb6b 100644 --- a/internal/usecase/smoke_authentication_test.go +++ b/internal/usecase/smoke_authentication_test.go @@ -38,10 +38,9 @@ func TestAuthenticationProbeApplicabilityAndHTTPAvailability(t *testing.T) { reason: domain.SmokeTestReasonAuthenticationNotApplicable, }, { - name: "HTTP adapter unavailable", - backend: backendWithAuthenticationKind("openrouter_key"), - applicable: boolPointer(true), - reason: domain.SmokeTestReasonHTTPDiagnosticsUnavailable, + name: "HTTP adapter unavailable", + backend: backendWithAuthenticationKind("openrouter_key"), + reason: domain.SmokeTestReasonHTTPDiagnosticsUnavailable, }, } { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/usecase/smoke_tester.go b/internal/usecase/smoke_tester.go new file mode 100644 index 0000000..f7462cb --- /dev/null +++ b/internal/usecase/smoke_tester.go @@ -0,0 +1,264 @@ +package usecase + +import ( + "context" + "fmt" + "time" + + "gitea.maximumdirect.net/eric/promptkit/internal/domain" + "gitea.maximumdirect.net/eric/promptkit/internal/generation" + "gitea.maximumdirect.net/eric/promptkit/internal/llm" + "gitea.maximumdirect.net/eric/promptkit/internal/observation" +) + +// SmokeTester coordinates internal HTTP smoke-test operations over captured +// selection collaborators. Inference operations are added separately. +type SmokeTester struct { + runner *Runner + httpClient smokeHTTPGetter + now func() time.Time +} + +// NewSmokeTester creates an internal diagnostic coordinator. A nil HTTP client +// leaves HTTP operations unsupported rather than falling back to another client. +func NewSmokeTester(runner *Runner, httpClient *llm.OpenAICompatibleClient) *SmokeTester { + var getter smokeHTTPGetter + if httpClient != nil { + getter = httpClient + } + return &SmokeTester{runner: runner, httpClient: getter, now: time.Now} +} + +func newSmokeTesterWithClock(runner *Runner, httpClient *llm.OpenAICompatibleClient, now func() time.Time) *SmokeTester { + var getter smokeHTTPGetter + if httpClient != nil { + getter = httpClient + } + return &SmokeTester{runner: runner, httpClient: getter, now: now} +} + +// Run selects and executes one HTTP diagnostic, retaining only factual results. +func (t *SmokeTester) Run(ctx context.Context, request domain.SmokeTestRequest) (*domain.SmokeTestReport, error) { + if t == nil || t.runner == nil { + return nil, fmt.Errorf("%w: smoke tester is not configured", ErrInvalidRequest) + } + ctx = observation.WithoutExecutionState(ctx) + now := t.now + if now == nil { + now = time.Now + } + startedAt := now().UTC() + report := &domain.SmokeTestReport{ + Kind: request.Kind, + Timing: domain.SmokeTestTiming{ + StartedAt: startedAt, + }, + } + finish := func(err error) (*domain.SmokeTestReport, error) { + report.Timing.Duration = nonnegativeDuration(now().UTC().Sub(startedAt)) + return report, err + } + + selection, err := t.runner.resolveSmokeTestTarget(ctx, request) + if err != nil { + if contextErr := ctx.Err(); contextErr != nil { + report.Failure = generation.ClassifyFailure(contextErr) + return finish(contextErr) + } + return nil, err + } + report.Target, err = smokeReportTarget(selection.target) + if err != nil { + return nil, err + } + if err := t.setSupport(report, selection); err != nil { + return nil, err + } + if contextErr := ctx.Err(); contextErr != nil { + report.Failure = generation.ClassifyFailure(contextErr) + return finish(contextErr) + } + if !report.Supported || (report.Applicable != nil && !*report.Applicable) { + report.Completed = true + return finish(nil) + } + + requestStartedAt := now().UTC() + switch request.Kind { + case domain.SmokeTestReachability: + outcome, err := (reachabilityProbe{client: t.httpClient}).run( + ctx, + selection.target.Endpoint, + selection.target.EffectiveSettings.TimeoutSeconds, + ) + requestDuration := nonnegativeDuration(now().UTC().Sub(requestStartedAt)) + if err != nil { + if contextErr := ctx.Err(); contextErr != nil { + report.Failure = generation.ClassifyFailure(contextErr) + return finish(contextErr) + } + return nil, err + } + report.Supported = outcome.supported + report.Reason = outcome.reason + report.Failure = outcome.failure + report.Facts = httpExecutionFacts(outcome.evidence) + if outcome.reachable != nil { + reachable := *outcome.reachable + report.Facts.EndpointReachable = &reachable + } + t.recordHTTPAttempt(report, report.Target.Endpoint, outcome.evidence, requestStartedAt, requestDuration, outcome.failure, nil) + return t.finishHTTP(ctx, report, finish) + case domain.SmokeTestAuthentication: + outcome, err := (authenticationProbe{client: t.httpClient}).run(ctx, selection.backend, selection.target) + requestDuration := nonnegativeDuration(now().UTC().Sub(requestStartedAt)) + if err != nil { + if contextErr := ctx.Err(); contextErr != nil { + report.Failure = generation.ClassifyFailure(contextErr) + return finish(contextErr) + } + return nil, err + } + report.Supported = outcome.supported + report.Applicable = cloneOptionalBool(outcome.applicable) + report.Reason = outcome.reason + report.Failure = outcome.failure + report.Facts = httpExecutionFacts(outcome.evidence) + report.Facts.CredentialsAccepted = cloneOptionalBool(outcome.credentialsAccepted) + t.recordHTTPAttempt(report, report.Target.Endpoint, outcome.evidence, requestStartedAt, requestDuration, outcome.failure, outcome.credentialsAccepted) + return t.finishHTTP(ctx, report, finish) + default: + return nil, fmt.Errorf("%w: HTTP smoke tester cannot run kind %q", ErrInvalidRequest, request.Kind) + } +} + +func (t *SmokeTester) setSupport(report *domain.SmokeTestReport, selection *smokeTestSelection) error { + switch report.Kind { + case domain.SmokeTestReachability: + if t.httpClient == nil { + report.Reason = domain.SmokeTestReasonHTTPDiagnosticsUnavailable + return nil + } + report.Supported = true + report.Applicable = boolPointer(true) + return nil + case domain.SmokeTestAuthentication: + outcome, err := (authenticationProbe{client: t.httpClient}).support(selection.backend) + if err != nil { + return err + } + report.Supported = outcome.supported + report.Applicable = cloneOptionalBool(outcome.applicable) + report.Reason = outcome.reason + return nil + default: + return fmt.Errorf("%w: HTTP smoke tester cannot run kind %q", ErrInvalidRequest, report.Kind) + } +} + +func (t *SmokeTester) finishHTTP( + ctx context.Context, + report *domain.SmokeTestReport, + finish func(error) (*domain.SmokeTestReport, error), +) (*domain.SmokeTestReport, error) { + if contextErr := ctx.Err(); contextErr != nil && isContextFailure(report.Failure) { + report.Completed = false + report.Failure = generation.ClassifyFailure(contextErr) + return finish(contextErr) + } + report.Completed = true + return finish(nil) +} + +func (t *SmokeTester) recordHTTPAttempt( + report *domain.SmokeTestReport, + initialEndpoint string, + evidence llm.HTTPExecutionEvidence, + startedAt time.Time, + duration time.Duration, + failure domain.ExecutionFailureKind, + credentialsAccepted *bool, +) { + if !evidence.DispatchInvoked { + return + } + reportDuration := duration + if report.Timing.RequestDuration == nil { + report.Timing.RequestDuration = &reportDuration + } else { + *report.Timing.RequestDuration += reportDuration + } + attempt := domain.SmokeTestAttempt{ + Dispatched: true, + InitialEndpoint: initialEndpoint, + Endpoint: evidence.Endpoint, + Failure: failure, + Usage: domain.ObservedTokenUsage{}, + Facts: httpExecutionFacts(evidence), + Timing: domain.SmokeTestTiming{ + StartedAt: startedAt, + Duration: duration, + RequestDuration: &duration, + }, + } + if evidence.HTTPStatus != nil { + status := *evidence.HTTPStatus + attempt.HTTPStatus = &status + } + attempt.Facts.CredentialsAccepted = cloneOptionalBool(credentialsAccepted) + report.Attempts = append(report.Attempts, attempt) +} + +func httpExecutionFacts(evidence llm.HTTPExecutionEvidence) domain.SmokeTestFacts { + var facts domain.SmokeTestFacts + if evidence.ResponseReceived || evidence.DispatchInvoked { + responseReceived := evidence.ResponseReceived + facts.ResponseReceived = &responseReceived + } + if evidence.DispatchInvoked { + reachable := evidence.ResponseReceived + facts.EndpointReachable = &reachable + } + if evidence.RequestObserved { + credentialsSupplied := evidence.CredentialsSupplied + facts.CredentialsSupplied = &credentialsSupplied + } + return facts +} + +func smokeReportTarget(target domain.SmokeTestTarget) (domain.SmokeTestTarget, error) { + cloned, err := domain.CloneSmokeTestReport(domain.SmokeTestReport{Target: target}) + if err != nil { + return domain.SmokeTestTarget{}, fmt.Errorf("clone smoke-test target settings: %w", err) + } + target = cloned.Target + target.EffectiveSettings.APIKey = "" + target.Endpoint, err = endpointWithoutUserInfo(target.Endpoint) + if err != nil { + return domain.SmokeTestTarget{}, fmt.Errorf("%w: invalid resolved diagnostic endpoint", ErrInvalidRequest) + } + target.EffectiveSettings.Endpoint, err = endpointWithoutUserInfo(target.EffectiveSettings.Endpoint) + if err != nil { + return domain.SmokeTestTarget{}, fmt.Errorf("%w: invalid resolved execution endpoint", ErrInvalidRequest) + } + return target, nil +} + +func cloneOptionalBool(value *bool) *bool { + if value == nil { + return nil + } + cloned := *value + return &cloned +} + +func isContextFailure(failure domain.ExecutionFailureKind) bool { + return failure == domain.ExecutionFailureCanceled || failure == domain.ExecutionFailureDeadline +} + +func nonnegativeDuration(duration time.Duration) time.Duration { + if duration < 0 { + return 0 + } + return duration +} diff --git a/internal/usecase/smoke_tester_test.go b/internal/usecase/smoke_tester_test.go new file mode 100644 index 0000000..f7e93fb --- /dev/null +++ b/internal/usecase/smoke_tester_test.go @@ -0,0 +1,332 @@ +package usecase + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + "testing" + "time" + + configkit "gitea.maximumdirect.net/eric/ConfigKit" + "gitea.maximumdirect.net/eric/promptkit/internal/configuration" + "gitea.maximumdirect.net/eric/promptkit/internal/domain" + "gitea.maximumdirect.net/eric/promptkit/internal/llm" + "gitea.maximumdirect.net/eric/promptkit/internal/observation" +) + +func TestSmokeTesterAssemblesReachabilityReportWithoutGenerationAdmission(t *testing.T) { + values := smokeSelectionConfig() + alpha := values["backends"].(map[string]any)["alpha"].(map[string]any) + alphaSmoke := alpha["smoke_tests"].(map[string]any) + alphaReachability := alphaSmoke["reachability"].(map[string]any) + alphaReachability["endpoint"] = "https://operator:private@example.invalid/health?check=1" + runner := smokeTesterRunner(t, values) + admitter := &smokeAdmissionSpy{} + runner.admitter = admitter + + var observedURL string + var authorization string + body := &reachabilityProbeBody{} + client := reachabilityClient(t, func(request *http.Request) (*http.Response, error) { + observedURL = request.URL.String() + authorization = request.Header.Get("Authorization") + return &http.Response{StatusCode: http.StatusUnauthorized, Header: make(http.Header), Body: body}, nil + }) + var observedEvents []domain.ExecutionEvent + recorder, err := observation.New(func(event domain.ExecutionEvent) { observedEvents = append(observedEvents, event) }) + if err != nil { + t.Fatal(err) + } + recorder.Start() + ctx := observation.WithRecorder(context.Background(), recorder) + + clock := smokeClock() + report, err := newSmokeTesterWithClock(runner, client, clock).Run(ctx, domain.SmokeTestRequest{ + Kind: domain.SmokeTestReachability, + BackendID: "alpha", + APIKey: "synthetic-secret", + }) + if err != nil { + t.Fatal(err) + } + if !body.closed || observedURL != "https://example.invalid/health?check=1" || authorization != "" { + t.Fatalf("request URL=%q authorization=%q body closed=%v", observedURL, authorization, body.closed) + } + if report.Kind != domain.SmokeTestReachability || !report.Supported || report.Applicable == nil || !*report.Applicable || !report.Completed || report.Failure != domain.ExecutionFailureNone { + t.Fatalf("report outcome = %+v", report) + } + if report.Target.Endpoint != "https://example.invalid/health?check=1" || + report.Target.EffectiveSettings.Endpoint != "https://alpha.example/v1" || + report.Target.EffectiveSettings.APIKey != "" { + t.Fatalf("report target = %+v", report.Target) + } + if report.Facts.EndpointReachable == nil || !*report.Facts.EndpointReachable || + report.Facts.ResponseReceived == nil || !*report.Facts.ResponseReceived || + report.Facts.CredentialsSupplied == nil || *report.Facts.CredentialsSupplied { + t.Fatalf("report facts = %+v", report.Facts) + } + if len(report.Attempts) != 1 || !report.Attempts[0].Dispatched || + report.Attempts[0].InitialEndpoint != report.Target.Endpoint || + report.Attempts[0].Endpoint != report.Target.Endpoint || + report.Attempts[0].HTTPStatus == nil || *report.Attempts[0].HTTPStatus != http.StatusUnauthorized { + t.Fatalf("attempts = %+v", report.Attempts) + } + if report.Timing.StartedAt.IsZero() || report.Timing.Duration <= 0 || report.Timing.RequestDuration == nil || + report.Attempts[0].Timing.RequestDuration == nil || report.Usage.PromptTokens.Known || report.Timing.CapacityWait != nil { + t.Fatalf("report timing/usage = %+v", report) + } + if strings.Contains(fmt.Sprintf("%+v", report), "synthetic-secret") || strings.Contains(fmt.Sprintf("%+v", report), "private") { + t.Fatalf("report retained endpoint credentials: %+v", report) + } + if len(observedEvents) != 1 || observedEvents[0].Kind != domain.ExecutionStarted { + t.Fatalf("diagnostic traffic entered inherited observer: %+v", observedEvents) + } + if admitter.calls != 0 { + t.Fatalf("HTTP diagnostic acquired generation capacity %d times", admitter.calls) + } +} + +func TestSmokeTesterAssemblesAuthenticationRejectionReport(t *testing.T) { + runner, _ := newSmokeSelectionRunner(t) + var authorization string + client := reachabilityClient(t, func(request *http.Request) (*http.Response, error) { + authorization = request.Header.Get("Authorization") + return &http.Response{ + StatusCode: http.StatusUnauthorized, + Header: make(http.Header), + Body: ioNopCloser("provider body is not retained"), + }, nil + }) + + report, err := NewSmokeTester(runner, client).Run(context.Background(), domain.SmokeTestRequest{ + Kind: domain.SmokeTestAuthentication, + BackendID: "beta", + APIKey: "synthetic-secret", + }) + if err != nil { + t.Fatal(err) + } + if authorization != "Bearer synthetic-secret" { + t.Fatalf("Authorization = %q", authorization) + } + if !report.Supported || report.Applicable == nil || !*report.Applicable || !report.Completed || report.Failure != domain.ExecutionFailureNone { + t.Fatalf("report outcome = %+v", report) + } + if report.Facts.CredentialsSupplied == nil || !*report.Facts.CredentialsSupplied || + report.Facts.EndpointReachable == nil || !*report.Facts.EndpointReachable || + report.Facts.ResponseReceived == nil || !*report.Facts.ResponseReceived || + report.Facts.CredentialsAccepted == nil || *report.Facts.CredentialsAccepted { + t.Fatalf("report facts = %+v", report.Facts) + } + if len(report.Attempts) != 1 || report.Attempts[0].HTTPStatus == nil || *report.Attempts[0].HTTPStatus != http.StatusUnauthorized || + report.Attempts[0].Endpoint != "https://beta.example/v1/key" { + t.Fatalf("attempts = %+v", report.Attempts) + } + if report.Target.Endpoint != "https://beta.example/v1/key" || report.Target.EffectiveSettings.APIKey != "" || + strings.Contains(fmt.Sprintf("%+v", report), "synthetic-secret") || + strings.Contains(fmt.Sprintf("%+v", report), "provider body") { + t.Fatalf("report retained sensitive values: %+v", report) + } +} + +func TestSmokeTesterReportsUnsupportedAndInapplicableAuthentication(t *testing.T) { + for _, tc := range []struct { + name string + backend string + client bool + configureNone bool + supported bool + applicable *bool + reason domain.SmokeTestReason + }{ + { + name: "unconfigured", + backend: "epsilon", + reason: domain.SmokeTestReasonProbeNotConfigured, + applicable: nil, + }, + { + name: "HTTP support absent", + backend: "beta", + reason: domain.SmokeTestReasonHTTPDiagnosticsUnavailable, + applicable: nil, + }, + { + name: "known inapplicable without HTTP support", + backend: "epsilon", + configureNone: true, + supported: true, + applicable: boolPointer(false), + reason: domain.SmokeTestReasonAuthenticationNotApplicable, + }, + } { + t.Run(tc.name, func(t *testing.T) { + values := smokeSelectionConfig() + if tc.configureNone { + backend := values["backends"].(map[string]any)[tc.backend].(map[string]any) + backend["smoke_tests"].(map[string]any)["authentication"] = map[string]any{"kind": "none"} + } + runner := smokeTesterRunner(t, values) + var client *llm.OpenAICompatibleClient + if tc.client { + client = reachabilityClient(t, func(*http.Request) (*http.Response, error) { + t.Fatal("unsupported or inapplicable authentication dispatched") + return nil, nil + }) + } + report, err := NewSmokeTester(runner, client).Run(context.Background(), domain.SmokeTestRequest{ + Kind: domain.SmokeTestAuthentication, BackendID: tc.backend, + }) + if err != nil { + t.Fatal(err) + } + if report.Supported != tc.supported || report.Reason != tc.reason || !report.Completed || + !sameBool(report.Applicable, tc.applicable) || len(report.Attempts) != 0 || report.Facts != (domain.SmokeTestFacts{}) { + t.Fatalf("report = %+v", report) + } + }) + } +} + +func TestSmokeTesterReportsReachabilityUnavailableWithoutHTTPClient(t *testing.T) { + runner, _ := newSmokeSelectionRunner(t) + report, err := NewSmokeTester(runner, nil).Run(context.Background(), domain.SmokeTestRequest{ + Kind: domain.SmokeTestReachability, BackendID: "alpha", + }) + if err != nil { + t.Fatal(err) + } + if report.Supported || report.Applicable != nil || !report.Completed || + report.Reason != domain.SmokeTestReasonHTTPDiagnosticsUnavailable || + len(report.Attempts) != 0 || report.Facts != (domain.SmokeTestFacts{}) { + t.Fatalf("unsupported reachability report = %+v", report) + } +} + +func TestSmokeTesterSelectionAndCancellationDoNotDispatch(t *testing.T) { + runner, _ := newSmokeSelectionRunner(t) + calls := 0 + client := reachabilityClient(t, func(*http.Request) (*http.Response, error) { + calls++ + return &http.Response{StatusCode: http.StatusNoContent, Header: make(http.Header), Body: &reachabilityProbeBody{}}, nil + }) + tester := NewSmokeTester(runner, client) + + if report, err := tester.Run(context.Background(), domain.SmokeTestRequest{ + Kind: domain.SmokeTestReachability, BackendID: "missing", + }); report != nil || !errors.Is(err, ErrProfileLoad) { + t.Fatalf("invalid selection report=%+v error=%v", report, err) + } + if calls != 0 { + t.Fatalf("invalid selection dispatched %d requests", calls) + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + report, err := tester.Run(ctx, domain.SmokeTestRequest{ + Kind: domain.SmokeTestReachability, BackendID: "alpha", + }) + if !errors.Is(err, context.Canceled) || report == nil || report.Completed || report.Failure != domain.ExecutionFailureCanceled { + t.Fatalf("canceled report=%+v error=%v", report, err) + } + if report.Facts != (domain.SmokeTestFacts{}) || len(report.Attempts) != 0 || calls != 0 { + t.Fatalf("cancellation dispatched or invented facts: %+v calls=%d", report, calls) + } +} + +func TestSmokeTesterConfiguredTimeoutConcludesWithReport(t *testing.T) { + runner, _ := newSmokeSelectionRunner(t) + timeout := 1 + client := reachabilityClient(t, func(request *http.Request) (*http.Response, error) { + <-request.Context().Done() + return nil, request.Context().Err() + }) + report, err := NewSmokeTester(runner, client).Run(context.Background(), domain.SmokeTestRequest{ + Kind: domain.SmokeTestReachability, BackendID: "alpha", + Execution: &domain.ExecutionTargetOverride{TimeoutSeconds: &timeout}, + }) + if err != nil { + t.Fatal(err) + } + if !report.Completed || report.Failure != domain.ExecutionFailureDeadline || + report.Facts.EndpointReachable == nil || *report.Facts.EndpointReachable || + report.Facts.ResponseReceived == nil || *report.Facts.ResponseReceived || + report.Facts.CredentialsSupplied == nil || *report.Facts.CredentialsSupplied { + t.Fatalf("configured timeout report = %+v", report) + } + if len(report.Attempts) != 1 || report.Attempts[0].Failure != domain.ExecutionFailureDeadline || + report.Attempts[0].HTTPStatus != nil || report.Timing.RequestDuration == nil { + t.Fatalf("configured timeout attempt = %+v", report.Attempts) + } +} + +func TestSmokeTesterRetainsHTTPResponseEvidenceWhenCallerCancelsDuringBodyRead(t *testing.T) { + runner, _ := newSmokeSelectionRunner(t) + ctx, cancel := context.WithCancel(context.Background()) + client := reachabilityClient(t, func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: cancelingSmokeBody{cancel: cancel}, + }, nil + }) + + report, err := NewSmokeTester(runner, client).Run(ctx, domain.SmokeTestRequest{ + Kind: domain.SmokeTestAuthentication, BackendID: "beta", APIKey: "synthetic-secret", + }) + if !errors.Is(err, context.Canceled) || report == nil || report.Completed || report.Failure != domain.ExecutionFailureCanceled { + t.Fatalf("partial report=%+v error=%v", report, err) + } + if report.Facts.ResponseReceived == nil || !*report.Facts.ResponseReceived || + report.Facts.EndpointReachable == nil || !*report.Facts.EndpointReachable || + report.Facts.CredentialsSupplied == nil || !*report.Facts.CredentialsSupplied || + report.Facts.CredentialsAccepted != nil || len(report.Attempts) != 1 || + report.Attempts[0].HTTPStatus == nil || *report.Attempts[0].HTTPStatus != http.StatusOK { + t.Fatalf("partial HTTP evidence = %+v", report) + } + if strings.Contains(fmt.Sprintf("%+v", report), "synthetic-secret") { + t.Fatalf("partial report retained credential: %+v", report) + } +} + +func smokeTesterRunner(t *testing.T, values map[string]any) *Runner { + t.Helper() + loaded, err := configuration.Load(configkit.Input{Source: configkit.Mapping("smoke-test", values)}, nil) + if err != nil { + t.Fatal(err) + } + return NewRunner(nil, loaded, nil, nil, nil, nil, nil) +} + +func smokeClock() func() time.Time { + current := time.Date(2026, time.October, 13, 12, 0, 0, 0, time.UTC) + return func() time.Time { + current = current.Add(time.Millisecond) + return current + } +} + +type smokeAdmissionSpy struct{ calls int } + +func (a *smokeAdmissionSpy) Admit(context.Context, string) (func(), error) { + a.calls++ + return func() {}, nil +} + +type cancelingSmokeBody struct { + cancel context.CancelFunc + read bool +} + +func (b cancelingSmokeBody) Read(buffer []byte) (int, error) { + if b.read { + return 0, context.Canceled + } + b.read = true + b.cancel() + return copy(buffer, []byte(`{"data":{"label":"member","is_free_tier":false,"usage":1}}`)), context.Canceled +} + +func (cancelingSmokeBody) Close() error { return nil }