package httpadapter import ( "bytes" "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "time" "gitea.maximumdirect.net/eric/scriptorium/internal/domain" "gitea.maximumdirect.net/eric/scriptorium/internal/profile" "gitea.maximumdirect.net/eric/scriptorium/internal/usecase" ) type fakeRunner struct { result *domain.RunResult err error last domain.RunRequest } func (f *fakeRunner) Run(ctx context.Context, req domain.RunRequest) (*domain.RunResult, error) { f.last = req if f.err != nil { return nil, f.err } return f.result, nil } func TestHandlerPostRunsSuccess(t *testing.T) { start := time.Now().UTC() end := start.Add(2 * time.Second) r := &fakeRunner{result: &domain.RunResult{ Artifact: domain.Artifact{ Name: "output", ContentType: "text/plain", Body: []byte("hello"), Size: 5, Hash: "abc", }, Validation: domain.ValidationResult{Status: domain.ValidationPassed, Mode: domain.ValidationBasic, IsValid: true}, ProfileID: "p1", ProfileVersion: "1.0.0", ModelName: "m1", Endpoint: "http://llm/v1", InputHashes: map[string]string{"transcript": "h1"}, PromptHash: "ph", Usage: domain.TokenUsage{PromptTokens: 1, CompletionTokens: 2, TotalTokens: 3}, StartTime: start, EndTime: end, RawOutput: "hello", }} h := NewHandler(r) body := []byte(`{ "profile_id": "p1", "inputs": { "transcript": {"type": "file", "uri": "./t.md"} }, "vars": {"k": "v"}, "model": {"model": "gpt-x"} }`) req := httptest.NewRequest(http.MethodPost, "/v1/runs", bytes.NewReader(body)) w := httptest.NewRecorder() h.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var resp map[string]any if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("invalid JSON response: %v", err) } artifact := resp["artifact"].(map[string]any) if artifact["body"] != "hello" { t.Fatalf("expected artifact body hello, got %#v", artifact["body"]) } if resp["raw_model_output"] != "hello" { t.Fatalf("expected raw model output hello, got %#v", resp["raw_model_output"]) } if r.last.ProfileID != "p1" { t.Fatalf("expected request profile_id p1, got %q", r.last.ProfileID) } if r.last.Model == nil || r.last.Model.Model != "gpt-x" { t.Fatalf("expected model override, got %#v", r.last.Model) } } func TestHandlerInvalidJSON(t *testing.T) { h := NewHandler(&fakeRunner{}) req := httptest.NewRequest(http.MethodPost, "/v1/runs", bytes.NewBufferString("{")) w := httptest.NewRecorder() h.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", w.Code) } } func TestHandlerMissingProfileID(t *testing.T) { h := NewHandler(&fakeRunner{}) req := httptest.NewRequest(http.MethodPost, "/v1/runs", bytes.NewBufferString(`{"inputs":{"x":{"type":"file","uri":"a"}}}`)) w := httptest.NewRecorder() h.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", w.Code) } } func TestHandlerUsecaseErrorMapping(t *testing.T) { tests := []struct { name string err error status int }{ {name: "profile not found", err: wrap(usecase.ErrProfileLoad, profile.ErrProfileNotFound), status: http.StatusNotFound}, {name: "artifact", err: wrap(usecase.ErrArtifactLoad, fmt.Errorf("read failed")), status: http.StatusBadRequest}, {name: "prompt", err: wrap(usecase.ErrPromptRender, fmt.Errorf("render failed")), status: http.StatusBadRequest}, {name: "llm", err: wrap(usecase.ErrLLMGenerate, fmt.Errorf("llm failed")), status: http.StatusBadGateway}, {name: "validation runtime", err: wrap(usecase.ErrValidation, fmt.Errorf("validator broke")), status: http.StatusInternalServerError}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { h := NewHandler(&fakeRunner{err: tc.err}) req := httptest.NewRequest(http.MethodPost, "/v1/runs", bytes.NewBufferString(`{"profile_id":"p","inputs":{"x":{"type":"file","uri":"a"}}}`)) w := httptest.NewRecorder() h.ServeHTTP(w, req) if w.Code != tc.status { t.Fatalf("expected %d, got %d body=%s", tc.status, w.Code, w.Body.String()) } }) } } func TestHandlerValidationFailureStillSuccess(t *testing.T) { h := NewHandler(&fakeRunner{result: &domain.RunResult{ Artifact: domain.Artifact{Body: []byte("bad json")}, RawOutput: "bad json", Validation: domain.ValidationResult{ Status: domain.ValidationFailed, Mode: domain.ValidationJSON, Errors: []string{"invalid JSON"}, }, }}) req := httptest.NewRequest(http.MethodPost, "/v1/runs", bytes.NewBufferString(`{"profile_id":"p","inputs":{"x":{"type":"file","uri":"a"}}}`)) w := httptest.NewRecorder() h.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var resp map[string]any if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("invalid JSON response: %v", err) } validation := resp["validation"].(map[string]any) if status, ok := validation["Status"].(string); !ok || status != "failed" { t.Fatalf("expected validation Status=failed, got %#v", validation["Status"]) } } func wrap(stage error, cause error) error { return fmt.Errorf("%w: %w", stage, cause) }