From 099e9c4a3e8541c3e92c7fd2a69894f606497ba5 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 26 May 2026 13:14:57 +0000 Subject: [PATCH] Use stable usecase sentinels for HTTP invalid-request mapping --- internal/adapter/http/handler.go | 4 ++-- internal/adapter/http/handler_test.go | 5 ++--- internal/usecase/runner.go | 18 ++++++++++-------- internal/usecase/runner_test.go | 6 ++++++ 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/internal/adapter/http/handler.go b/internal/adapter/http/handler.go index 5ce9d8c..27ee53a 100644 --- a/internal/adapter/http/handler.go +++ b/internal/adapter/http/handler.go @@ -175,9 +175,9 @@ func mapRunError(err error) (int, string, string) { return http.StatusBadRequest, "prompt_load_failed", "failed to load prompt definition" case errors.Is(err, profile.ErrInvalidYAML), errors.Is(err, profile.ErrInvalidProfile): return http.StatusBadRequest, "profile_load_failed", "failed to load execution profile" - case errors.Is(err, usecase.ErrInvalidRequest) && strings.Contains(err.Error(), "profile id is required either in request or prompt default_profile"): + case errors.Is(err, usecase.ErrProfileRequired): return http.StatusBadRequest, "profile_required", "profile_id is required when prompt default_profile is not set" - case errors.Is(err, usecase.ErrInvalidRequest) && strings.Contains(err.Error(), "api key environment variable"): + case errors.Is(err, usecase.ErrAPIKeyEnvMissing): return http.StatusBadRequest, "api_key_env_missing", "api_key_env is set but the environment variable is missing" case errors.Is(err, usecase.ErrInvalidRequest): return http.StatusBadRequest, "invalid_request", "invalid run request" diff --git a/internal/adapter/http/handler_test.go b/internal/adapter/http/handler_test.go index 06ae4c5..ec1b130 100644 --- a/internal/adapter/http/handler_test.go +++ b/internal/adapter/http/handler_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "errors" "fmt" "net/http" "net/http/httptest" @@ -347,10 +346,10 @@ func TestHandlerUsecaseErrorMapping(t *testing.T) { }{ {name: "prompt not found", err: wrap(usecase.ErrProfileLoad, promptdef.ErrPromptDefinitionNotFound), status: http.StatusNotFound, code: "prompt_not_found", message: "prompt definition not found"}, {name: "prompt load invalid", err: wrap(usecase.ErrProfileLoad, promptdef.ErrInvalidPromptDefinition), status: http.StatusBadRequest, code: "prompt_load_failed", message: "failed to load prompt definition"}, - {name: "missing profile/default", err: wrap(usecase.ErrInvalidRequest, errors.New("profile id is required either in request or prompt default_profile")), status: http.StatusBadRequest, code: "profile_required", message: "profile_id is required when prompt default_profile is not set"}, + {name: "missing profile/default", err: wrap(usecase.ErrInvalidRequest, usecase.ErrProfileRequired), status: http.StatusBadRequest, code: "profile_required", message: "profile_id is required when prompt default_profile is not set"}, {name: "profile not found", err: wrap(usecase.ErrProfileLoad, profile.ErrProfileNotFound), status: http.StatusNotFound, code: "profile_not_found", message: "execution profile not found"}, {name: "profile invalid", err: wrap(usecase.ErrProfileLoad, profile.ErrInvalidProfile), status: http.StatusBadRequest, code: "profile_load_failed", message: "failed to load execution profile"}, - {name: "api key env missing", err: wrap(usecase.ErrInvalidRequest, errors.New(`api key environment variable "SCRIPTORIUM_API_KEY" is not set`)), status: http.StatusBadRequest, code: "api_key_env_missing", message: "api_key_env is set but the environment variable is missing"}, + {name: "api key env missing", err: wrap(usecase.ErrInvalidRequest, usecase.ErrAPIKeyEnvMissing), status: http.StatusBadRequest, code: "api_key_env_missing", message: "api_key_env is set but the environment variable is missing"}, {name: "artifact", err: wrap(usecase.ErrArtifactLoad, fmt.Errorf("read failed")), status: http.StatusBadRequest, code: "artifact_read_failed", message: "failed to read input artifact", avoidCause: "read failed"}, {name: "prompt render", err: wrap(usecase.ErrPromptRender, fmt.Errorf("render failed")), status: http.StatusBadRequest, code: "prompt_render_failed", message: "failed to render prompt", avoidCause: "render failed"}, {name: "llm", err: wrap(usecase.ErrLLMGenerate, fmt.Errorf("llm failed")), status: http.StatusBadGateway, code: "llm_failed", message: "model generation request failed", avoidCause: "llm failed"}, diff --git a/internal/usecase/runner.go b/internal/usecase/runner.go index 6807132..4091403 100644 --- a/internal/usecase/runner.go +++ b/internal/usecase/runner.go @@ -24,12 +24,14 @@ import ( ) var ( - ErrInvalidRequest = errors.New("invalid run request") - ErrProfileLoad = errors.New("failed to load prompt definition") - ErrArtifactLoad = errors.New("failed to load artifact") - ErrPromptRender = errors.New("failed to render prompt") - ErrLLMGenerate = errors.New("failed to generate output") - ErrValidation = errors.New("failed to validate output") + ErrInvalidRequest = errors.New("invalid run request") + ErrProfileRequired = errors.New("profile selection is required") + ErrAPIKeyEnvMissing = errors.New("api_key_env points to an unset environment variable") + ErrProfileLoad = errors.New("failed to load prompt definition") + ErrArtifactLoad = errors.New("failed to load artifact") + ErrPromptRender = errors.New("failed to render prompt") + ErrLLMGenerate = errors.New("failed to generate output") + ErrValidation = errors.New("failed to validate output") ) // Runner executes the Scriptorium core use case. @@ -177,7 +179,7 @@ func (r *Runner) Prepare(ctx context.Context, req domain.RunRequest) (*domain.Pr selectedProfileID = strings.TrimSpace(def.DefaultProfile) } if selectedProfileID == "" { - return nil, fmt.Errorf("%w: profile id is required either in request or prompt default_profile", ErrInvalidRequest) + return nil, fmt.Errorf("%w: %w: profile id is required either in request or prompt default_profile", ErrInvalidRequest, ErrProfileRequired) } execProfile, err := r.profiles.GetProfile(ctx, selectedProfileID) @@ -376,7 +378,7 @@ func validateAPIKeyEnv(apiKeyEnv string) error { return nil } if strings.TrimSpace(os.Getenv(envName)) == "" { - return fmt.Errorf("api key environment variable %q is not set", envName) + return fmt.Errorf("%w: api key environment variable %q is not set", ErrAPIKeyEnvMissing, envName) } return nil } diff --git a/internal/usecase/runner_test.go b/internal/usecase/runner_test.go index 06df4ed..41a3711 100644 --- a/internal/usecase/runner_test.go +++ b/internal/usecase/runner_test.go @@ -234,6 +234,9 @@ func TestRunnerPrepareMissingExplicitProfileAndMissingDefaultProfileFails(t *tes if !errors.Is(err, ErrInvalidRequest) { t.Fatalf("expected ErrInvalidRequest, got %v", err) } + if !errors.Is(err, ErrProfileRequired) { + t.Fatalf("expected ErrProfileRequired, got %v", err) + } } func TestRunnerPrepareSelectedProfileDoesNotExistFails(t *testing.T) { @@ -862,6 +865,9 @@ func TestRunnerRunAPIKeyEnvMissingEnvironmentValueFailsClearly(t *testing.T) { if !errors.Is(err, ErrInvalidRequest) { t.Fatalf("expected ErrInvalidRequest, got %v", err) } + if !errors.Is(err, ErrAPIKeyEnvMissing) { + t.Fatalf("expected ErrAPIKeyEnvMissing, got %v", err) + } if !strings.Contains(err.Error(), "SCRIPTORIUM_MISSING_KEY") { t.Fatalf("expected missing env name in error, got %v", err) }