2026-05-04 21:19:29 -05:00
package httpadapter
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
2026-07-04 23:34:44 +00:00
"os"
"path/filepath"
2026-05-26 13:07:35 +00:00
"reflect"
2026-05-05 08:41:07 -05:00
"strings"
2026-08-29 14:26:04 +00:00
"sync/atomic"
2026-05-04 21:19:29 -05:00
"testing"
"time"
2026-07-28 14:04:29 +00:00
"gitea.maximumdirect.net/eric/promptkit"
2026-05-04 21:19:29 -05:00
)
type fakeRunner struct {
2026-07-28 14:04:29 +00:00
result * promptkit . RunResult
2026-05-04 21:19:29 -05:00
err error
2026-07-28 14:04:29 +00:00
last promptkit . RunRequest
2026-05-04 21:19:29 -05:00
}
2026-09-26 15:39:17 +00:00
type runnerFunc func ( context . Context , promptkit . RunRequest ) ( * promptkit . RunResult , error )
func ( run runnerFunc ) Run ( ctx context . Context , req promptkit . RunRequest ) ( * promptkit . RunResult , error ) {
return run ( ctx , req )
}
2026-07-28 14:04:29 +00:00
func ( f * fakeRunner ) Run ( ctx context . Context , req promptkit . RunRequest ) ( * promptkit . RunResult , error ) {
2026-05-04 21:19:29 -05:00
f . last = req
if f . err != nil {
return nil , f . err
}
return f . result , nil
}
2026-09-26 15:51:47 +00:00
func TestHandlerExecutionReportOptInAndResultShapes ( t * testing . T ) {
for _ , tc := range [ ] struct {
name , body string
wantReport bool
invalid bool
} {
{ "omitted" , ` { "prompt_id":"p"} ` , false , false } ,
{ "null" , ` { "prompt_id":"p","include_execution_report":null} ` , false , false } ,
{ "false" , ` { "prompt_id":"p","include_execution_report":false} ` , false , false } ,
{ "success" , ` { "prompt_id":"p","include_execution_report":true} ` , true , false } ,
{ "invalid output" , ` { "prompt_id":"p","include_execution_report":true} ` , true , true } ,
} {
t . Run ( tc . name , func ( t * testing . T ) {
runner := runnerFunc ( func ( _ context . Context , req promptkit . RunRequest ) ( * promptkit . RunResult , error ) {
if ( req . Observer != nil ) != tc . wantReport {
t . Fatalf ( "unexpected observer presence: %v" , req . Observer != nil )
}
status := promptkit . ValidationPassed
outcome := promptkit . ExecutionOutcomeSuccess
if tc . invalid {
status = promptkit . ValidationFailed
outcome = promptkit . ExecutionOutcomeInvalidOutput
}
if req . Observer != nil {
req . Observer ( promptkit . ExecutionEvent { Kind : promptkit . ExecutionFinished , RunID : "observed" , Outcome : outcome , Failure : promptkit . ExecutionFailureNone , Attempts : 2 } )
}
return & promptkit . RunResult { Artifact : promptkit . Artifact { Body : [ ] byte ( "body" ) } , Validation : promptkit . ValidationResult { Status : status } , Usage : promptkit . TokenUsage { PromptTokens : 7 } } , nil
} )
w := httptest . NewRecorder ( )
NewHandler ( runner ) . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( tc . body ) ) )
if w . Code != http . StatusOK {
t . Fatalf ( "status=%d body=%s" , w . Code , w . Body . String ( ) )
}
var response map [ string ] any
if err := json . Unmarshal ( w . Body . Bytes ( ) , & response ) ; err != nil {
t . Fatal ( err )
}
if ( response [ "execution" ] != nil ) != tc . wantReport {
t . Fatalf ( "unexpected report: %#v" , response [ "execution" ] )
}
if tc . wantReport {
report := response [ "execution" ] . ( map [ string ] any )
wantOutcome := "success"
if tc . invalid {
wantOutcome = "invalid_output"
}
if report [ "outcome" ] != wantOutcome || report [ "attempts" ] != float64 ( 2 ) {
t . Fatalf ( "unexpected report: %#v" , report )
}
}
if response [ "metadata" ] . ( map [ string ] any ) [ "usage" ] . ( map [ string ] any ) [ "prompt_tokens" ] != float64 ( 7 ) {
t . Fatalf ( "metadata usage changed: %#v" , response [ "metadata" ] )
}
} )
}
w := httptest . NewRecorder ( )
NewHandler ( & fakeRunner { } ) . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( ` { "prompt_id":"p","include_execution_report":"true"} ` ) ) )
assertHTTPErrorCode ( t , w , http . StatusBadRequest , "invalid_json" )
}
func TestHandlerExecutionReportFailureAndAbsence ( t * testing . T ) {
secret := "private provider diagnostic"
runner := runnerFunc ( func ( _ context . Context , req promptkit . RunRequest ) ( * promptkit . RunResult , error ) {
req . Observer ( promptkit . ExecutionEvent { Kind : promptkit . ExecutionFinished , Outcome : promptkit . ExecutionOutcomeError , Failure : promptkit . ExecutionFailureProviderStatus , StatusCode : 503 , RetriesExhausted : true , Attempts : 3 } )
return nil , fmt . Errorf ( "%w: %s" , promptkit . ErrLLMGenerate , secret )
} )
w := httptest . NewRecorder ( )
NewHandler ( runner ) . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( ` { "prompt_id":"p","include_execution_report":true} ` ) ) )
assertHTTPErrorCode ( t , w , http . StatusBadGateway , "llm_failed" )
var response map [ string ] any
if err := json . Unmarshal ( w . Body . Bytes ( ) , & response ) ; err != nil {
t . Fatal ( err )
}
report := response [ "execution" ] . ( map [ string ] any )
if report [ "failure" ] != "provider_status" || report [ "retries_exhausted" ] != true || report [ "attempts" ] != float64 ( 3 ) {
t . Fatalf ( "unexpected failure report: %#v" , report )
}
if strings . Contains ( w . Body . String ( ) , secret ) {
t . Fatalf ( "response leaked provider diagnostic: %s" , w . Body . String ( ) )
}
noTerminal := runnerFunc ( func ( _ context . Context , req promptkit . RunRequest ) ( * promptkit . RunResult , error ) {
if req . Observer == nil {
t . Fatal ( "missing opted-in observer" )
}
return nil , promptkit . ErrPromptLoad
} )
w = httptest . NewRecorder ( )
NewHandler ( noTerminal ) . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( ` { "prompt_id":"p","include_execution_report":true} ` ) ) )
assertHTTPErrorCode ( t , w , http . StatusBadRequest , "prompt_load_failed" )
if strings . Contains ( w . Body . String ( ) , "execution" ) {
t . Fatalf ( "unexpected absent report: %s" , w . Body . String ( ) )
}
w = httptest . NewRecorder ( )
NewHandler ( noTerminal ) . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( ` { "include_execution_report":true} ` ) ) )
assertHTTPErrorCode ( t , w , http . StatusBadRequest , "invalid_request" )
if strings . Contains ( w . Body . String ( ) , "execution" ) {
t . Fatalf ( "pre-invocation failure has report: %s" , w . Body . String ( ) )
}
}
func TestHandlerExecutionReportResponseLimit ( t * testing . T ) {
for _ , failure := range [ ] bool { false , true } {
runner := runnerFunc ( func ( _ context . Context , req promptkit . RunRequest ) ( * promptkit . RunResult , error ) {
if req . Observer != nil {
req . Observer ( promptkit . ExecutionEvent { Kind : promptkit . ExecutionFinished , Outcome : promptkit . ExecutionOutcomeError , Failure : promptkit . ExecutionFailureGeneration } )
}
if failure {
return nil , promptkit . ErrLLMGenerate
}
return & promptkit . RunResult { } , nil
} )
body := ` { "prompt_id":"p"} `
base := httptest . NewRecorder ( )
NewHandler ( runner ) . ServeHTTP ( base , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( body ) ) )
body = ` { "prompt_id":"p","include_execution_report":true} `
limited := httptest . NewRecorder ( )
NewHandlerWithOptions ( runner , HandlerOptions { MaxResponseBytes : int64 ( base . Body . Len ( ) ) } ) . ServeHTTP ( limited , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( body ) ) )
assertHTTPErrorCode ( t , limited , http . StatusRequestEntityTooLarge , "response_too_large" )
if strings . Contains ( limited . Body . String ( ) , "execution" ) {
t . Fatalf ( "overflow fallback included report: %s" , limited . Body . String ( ) )
}
}
}
func TestHandlerRealEngineFailureIncludesTerminalReport ( t * testing . T ) {
provider := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
http . Error ( w , "private provider detail" , http . StatusServiceUnavailable )
} ) )
defer provider . Close ( )
h := newRetryHandler ( t , provider . URL + "/v1" , intPointer ( 0 ) , 0 , nil )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( ` { "prompt_id":"p","include_execution_report":true} ` ) ) )
assertHTTPErrorCode ( t , w , http . StatusBadGateway , "llm_failed" )
var response map [ string ] any
if err := json . Unmarshal ( w . Body . Bytes ( ) , & response ) ; err != nil {
t . Fatal ( err )
}
report := response [ "execution" ] . ( map [ string ] any )
if report [ "outcome" ] != "error" || report [ "failure" ] != "provider_status" || report [ "attempts" ] != float64 ( 1 ) {
t . Fatalf ( "unexpected terminal report: %#v" , report )
}
if strings . Contains ( w . Body . String ( ) , "private provider detail" ) {
t . Fatalf ( "provider diagnostic leaked: %s" , w . Body . String ( ) )
}
}
func TestHandlerConcurrentExecutionReportsAreIsolated ( t * testing . T ) {
started := make ( chan struct { } , 2 )
release := make ( chan struct { } )
defer func ( ) {
select {
case <- release :
default :
close ( release )
}
} ( )
runner := runnerFunc ( func ( _ context . Context , req promptkit . RunRequest ) ( * promptkit . RunResult , error ) {
started <- struct { } { }
<- release
req . Observer ( promptkit . ExecutionEvent { Kind : promptkit . ExecutionFinished , RunID : req . SessionID , Outcome : promptkit . ExecutionOutcomeSuccess , Attempts : uint64 ( len ( req . SessionID ) ) } )
return & promptkit . RunResult { } , nil
} )
h := NewHandler ( runner )
responses := make ( [ ] * httptest . ResponseRecorder , 2 )
done := make ( chan struct { } , 2 )
for i , id := range [ ] string { "one" , "second" } {
responses [ i ] = httptest . NewRecorder ( )
go func ( w * httptest . ResponseRecorder , session string ) {
h . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( fmt . Sprintf ( ` { "prompt_id":"p","session_id":%q,"include_execution_report":true} ` , session ) ) ) )
done <- struct { } { }
} ( responses [ i ] , id )
}
for range 2 {
select {
case <- started :
case <- time . After ( 3 * time . Second ) :
t . Fatal ( "concurrent runs did not enter runner" )
}
}
close ( release )
for range 2 {
select {
case <- done :
case <- time . After ( 3 * time . Second ) :
t . Fatal ( "concurrent run did not complete" )
}
}
for i , id := range [ ] string { "one" , "second" } {
var response map [ string ] any
if err := json . Unmarshal ( responses [ i ] . Body . Bytes ( ) , & response ) ; err != nil {
t . Fatal ( err )
}
report := response [ "execution" ] . ( map [ string ] any )
if report [ "run_id" ] != id || report [ "attempts" ] != float64 ( len ( id ) ) {
t . Fatalf ( "report crossed requests: %#v" , report )
}
}
}
2026-07-26 14:25:44 +00:00
func TestMaintainedHTTPRunExampleMatchesRequestContract ( t * testing . T ) {
body , err := os . ReadFile ( filepath . Join ( ".." , ".." , ".." , "examples" , "http-run.json" ) )
if err != nil {
t . Fatalf ( "read maintained HTTP request example: %v" , err )
}
2026-07-28 14:04:29 +00:00
runner := & fakeRunner { result : & promptkit . RunResult { } }
2026-07-26 14:25:44 +00:00
h := NewHandler ( runner )
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 maintained HTTP request example to be accepted, got %d: %s" , w . Code , w . Body . String ( ) )
}
2026-07-26 17:44:51 +00:00
var invalidExample map [ string ] json . RawMessage
if err := json . Unmarshal ( body , & invalidExample ) ; err != nil {
t . Fatalf ( "decode maintained HTTP request example: %v" , err )
2026-07-26 14:25:44 +00:00
}
2026-07-26 17:44:51 +00:00
invalidExample [ "unexpected" ] = json . RawMessage ( ` true ` )
invalidBody , err := json . Marshal ( invalidExample )
if err != nil {
t . Fatalf ( "encode structurally invalid request example: %v" , err )
2026-07-26 14:25:44 +00:00
}
2026-07-26 17:44:51 +00:00
invalidReq := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewReader ( invalidBody ) )
invalidW := httptest . NewRecorder ( )
h . ServeHTTP ( invalidW , invalidReq )
assertHTTPErrorCode ( t , invalidW , http . StatusBadRequest , "invalid_json" )
2026-07-26 14:25:44 +00:00
}
2026-09-26 15:31:31 +00:00
func TestHandlerTransientRetryOverridePresenceAndStrictness ( t * testing . T ) {
for _ , tc := range [ ] struct {
name string
body string
want * int
status int
code string
} {
{ name : "omitted" , body : ` { "prompt_id":"p"} ` } ,
{ name : "null" , body : ` { "prompt_id":"p","transient_retries":null} ` } ,
{ name : "zero" , body : ` { "prompt_id":"p","transient_retries":0} ` , want : intPointer ( 0 ) } ,
{ name : "positive" , body : ` { "prompt_id":"p","transient_retries":3} ` , want : intPointer ( 3 ) } ,
{ name : "negative" , body : ` { "prompt_id":"p","transient_retries":-1} ` , status : http . StatusBadRequest , code : "invalid_request" } ,
{ name : "string" , body : ` { "prompt_id":"p","transient_retries":"2"} ` , status : http . StatusBadRequest , code : "invalid_json" } ,
{ name : "fractional" , body : ` { "prompt_id":"p","transient_retries":1.5} ` , status : http . StatusBadRequest , code : "invalid_json" } ,
} {
t . Run ( tc . name , func ( t * testing . T ) {
runner := & fakeRunner { result : & promptkit . RunResult { } }
w := httptest . NewRecorder ( )
NewHandler ( runner ) . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( tc . body ) ) )
if tc . status != 0 {
assertHTTPErrorCode ( t , w , tc . status , tc . code )
if runner . last . PromptID != "" {
t . Fatalf ( "runner invoked for invalid request: %+v" , runner . last )
}
return
}
if w . Code != http . StatusOK || ! reflect . DeepEqual ( runner . last . TransientRetries , tc . want ) {
t . Fatalf ( "retry mapping: status=%d got=%v want=%v body=%s" , w . Code , runner . last . TransientRetries , tc . want , w . Body . String ( ) )
}
} )
}
}
2026-09-26 15:39:17 +00:00
func TestHandlerRunTimeoutValidationAndContextMapping ( t * testing . T ) {
for _ , tc := range [ ] struct {
name , body , code string
status int
} {
{ name : "negative" , body : ` { "prompt_id":"p","run_timeout_seconds":-1} ` , status : http . StatusBadRequest , code : "invalid_request" } ,
{ name : "duration overflow" , body : ` { "prompt_id":"p","run_timeout_seconds":9223372037} ` , status : http . StatusBadRequest , code : "invalid_request" } ,
{ name : "string" , body : ` { "prompt_id":"p","run_timeout_seconds":"1"} ` , status : http . StatusBadRequest , code : "invalid_json" } ,
{ name : "fractional" , body : ` { "prompt_id":"p","run_timeout_seconds":1.5} ` , status : http . StatusBadRequest , code : "invalid_json" } ,
{ name : "int64 overflow" , body : ` { "prompt_id":"p","run_timeout_seconds":9223372036854775808} ` , status : http . StatusBadRequest , code : "invalid_json" } ,
} {
t . Run ( tc . name , func ( t * testing . T ) {
runner := & fakeRunner { result : & promptkit . RunResult { } }
w := httptest . NewRecorder ( )
NewHandler ( runner ) . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( tc . body ) ) )
assertHTTPErrorCode ( t , w , tc . status , tc . code )
if runner . last . PromptID != "" {
t . Fatalf ( "runner invoked for invalid timeout: %+v" , runner . last )
}
} )
}
parent , cancel := context . WithDeadline ( context . Background ( ) , time . Now ( ) . Add ( 5 * time . Second ) )
defer cancel ( )
parentDeadline , _ := parent . Deadline ( )
for _ , tc := range [ ] struct {
2026-09-26 16:17:23 +00:00
name , body string
parent context . Context
wantDeadline time . Time
2026-09-26 15:39:17 +00:00
} {
2026-09-26 16:17:23 +00:00
{ name : "omitted inherits deadline" , body : ` { "prompt_id":"p"} ` , parent : parent , wantDeadline : parentDeadline } ,
{ name : "null inherits deadline" , body : ` { "prompt_id":"p","run_timeout_seconds":null} ` , parent : parent , wantDeadline : parentDeadline } ,
{ name : "zero inherits deadline" , body : ` { "prompt_id":"p","run_timeout_seconds":0} ` , parent : parent , wantDeadline : parentDeadline } ,
{ name : "positive preserves earlier deadline" , body : ` { "prompt_id":"p","run_timeout_seconds":10} ` , parent : parent , wantDeadline : parentDeadline } ,
{ name : "omitted adds no deadline" , body : ` { "prompt_id":"p"} ` , parent : context . Background ( ) } ,
{ name : "null adds no deadline" , body : ` { "prompt_id":"p","run_timeout_seconds":null} ` , parent : context . Background ( ) } ,
{ name : "zero adds no deadline" , body : ` { "prompt_id":"p","run_timeout_seconds":0} ` , parent : context . Background ( ) } ,
2026-09-26 15:39:17 +00:00
} {
t . Run ( tc . name , func ( t * testing . T ) {
called := false
h := NewHandler ( runnerFunc ( func ( ctx context . Context , req promptkit . RunRequest ) ( * promptkit . RunResult , error ) {
called = true
deadline , ok := ctx . Deadline ( )
2026-09-26 16:17:23 +00:00
if ok != ! tc . wantDeadline . IsZero ( ) || ( ok && ! deadline . Equal ( tc . wantDeadline ) ) {
t . Fatalf ( "execution deadline: got %v (present=%t), want %v" , deadline , ok , tc . wantDeadline )
2026-09-26 15:39:17 +00:00
}
return & promptkit . RunResult { } , nil
} ) )
w := httptest . NewRecorder ( )
2026-09-26 16:17:23 +00:00
h . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( tc . body ) ) . WithContext ( tc . parent ) )
2026-09-26 15:39:17 +00:00
if ! called || w . Code != http . StatusOK {
t . Fatalf ( "context mapping: called=%t status=%d body=%s" , called , w . Code , w . Body . String ( ) )
}
} )
}
canceled , cancelParent := context . WithCancel ( context . Background ( ) )
cancelParent ( )
h := NewHandler ( runnerFunc ( func ( ctx context . Context , req promptkit . RunRequest ) ( * promptkit . RunResult , error ) {
2026-09-26 16:17:23 +00:00
if ctx . Err ( ) != context . Canceled {
t . Fatalf ( "parent cancellation did not reach execution: %v" , ctx . Err ( ) )
}
2026-09-26 15:39:17 +00:00
return nil , ctx . Err ( )
} ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( ` { "prompt_id":"p","run_timeout_seconds":10} ` ) ) . WithContext ( canceled ) )
assertHTTPErrorCode ( t , w , http . StatusRequestTimeout , "request_canceled" )
h = NewHandler ( runnerFunc ( func ( ctx context . Context , req promptkit . RunRequest ) ( * promptkit . RunResult , error ) {
return nil , fmt . Errorf ( "attempt: %w: %w" , promptkit . ErrLLMGenerate , context . DeadlineExceeded )
} ) )
w = httptest . NewRecorder ( )
h . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( ` { "prompt_id":"p","run_timeout_seconds":10} ` ) ) )
assertHTTPErrorCode ( t , w , http . StatusBadGateway , "llm_failed" )
successContext , cancelSuccess := context . WithCancel ( context . Background ( ) )
h = NewHandler ( runnerFunc ( func ( ctx context . Context , req promptkit . RunRequest ) ( * promptkit . RunResult , error ) {
cancelSuccess ( )
return & promptkit . RunResult { } , nil
} ) )
w = httptest . NewRecorder ( )
h . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( ` { "prompt_id":"p","run_timeout_seconds":10} ` ) ) . WithContext ( successContext ) )
if w . Code != http . StatusOK {
t . Fatalf ( "successful result was reclassified after cancellation: %d %s" , w . Code , w . Body . String ( ) )
}
}
2026-09-26 15:31:31 +00:00
func intPointer ( value int ) * int { return & value }
2026-07-04 23:34:44 +00:00
type handlerLLMClient struct { }
2026-07-28 14:04:29 +00:00
func ( handlerLLMClient ) Generate ( ctx context . Context , req promptkit . GenerateRequest ) ( * promptkit . GenerateResponse , error ) {
return & promptkit . GenerateResponse { Content : "ok" } , nil
2026-07-04 23:34:44 +00:00
}
2026-08-29 14:26:04 +00:00
type blockingLLMClient struct {
started chan struct { }
release chan struct { }
current int32
peak int32
}
func ( c * blockingLLMClient ) Generate ( ctx context . Context , req promptkit . GenerateRequest ) ( * promptkit . GenerateResponse , error ) {
current := atomic . AddInt32 ( & c . current , 1 )
defer atomic . AddInt32 ( & c . current , - 1 )
for {
peak := atomic . LoadInt32 ( & c . peak )
if current <= peak || atomic . CompareAndSwapInt32 ( & c . peak , peak , current ) {
break
}
}
select {
case c . started <- struct { } { } :
default :
}
select {
case <- c . release :
return & promptkit . GenerateResponse { Content : "ok" } , nil
case <- ctx . Done ( ) :
return nil , ctx . Err ( )
}
}
2026-05-05 10:54:35 -05:00
func TestHandlerPostRunsSuccessWithExplicitProfileID ( t * testing . T ) {
2026-05-04 21:19:29 -05:00
start := time . Now ( ) . UTC ( )
end := start . Add ( 2 * time . Second )
2026-05-05 10:54:35 -05:00
const envName = "SCRIPTORIUM_API_KEY"
const secret = "never-include-me"
2026-07-28 14:04:29 +00:00
r := & fakeRunner { result : & promptkit . RunResult {
2026-05-05 08:41:07 -05:00
RunID : "11111111-1111-4111-8111-111111111111" ,
2026-07-28 14:04:29 +00:00
Artifact : promptkit . Artifact {
2026-05-04 21:19:29 -05:00
Name : "output" ,
ContentType : "text/plain" ,
Body : [ ] byte ( "hello" ) ,
Size : 5 ,
Hash : "abc" ,
} ,
2026-07-28 14:04:29 +00:00
Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed , Mode : promptkit . ValidationBasic , IsValid : true } ,
2026-05-05 10:09:31 -05:00
PromptID : "prompt-1" ,
PromptVersion : "1.0.0" ,
PromptHash : "phash" ,
RenderedPromptHash : "rhash" ,
SelectedProfileID : "exec-default" ,
2026-08-29 14:26:04 +00:00
SelectedBackendID : "local" ,
2026-05-05 10:09:31 -05:00
ModelName : "m1" ,
Endpoint : "http://llm/v1" ,
2026-07-28 14:04:29 +00:00
EffectiveModelParams : promptkit . ExecutionTarget {
2026-08-29 14:26:04 +00:00
BackendID : "local" ,
2026-05-05 08:41:07 -05:00
Endpoint : "http://llm/v1" ,
Model : "m1" ,
Temperature : 0.2 ,
MaxTokens : 42 ,
TopP : 0.9 ,
TimeoutSeconds : 120 ,
2026-05-26 07:44:49 -05:00
ServiceTier : "priority" ,
2026-05-05 10:54:35 -05:00
APIKeyEnv : envName ,
2026-05-05 08:41:07 -05:00
} ,
InputHashes : map [ string ] string { "transcript" : "h1" } ,
2026-07-28 14:04:29 +00:00
Usage : promptkit . TokenUsage {
2026-07-02 23:07:57 +00:00
PromptTokens : 1 ,
CompletionTokens : 2 ,
TotalTokens : 3 ,
CachedTokens : 4 ,
CacheWriteTokens : 5 ,
} ,
StartTime : start ,
EndTime : end ,
Duration : 2 * time . Second ,
RawOutput : "hello" ,
2026-05-04 21:19:29 -05:00
} }
h := NewHandler ( r )
body := [ ] byte ( ` {
2026-05-05 10:09:31 -05:00
"prompt_id" : "prompt-1" ,
"profile_id" : "exec-default" ,
2026-05-04 21:19:29 -05:00
"inputs" : {
"transcript" : { "type" : "file" , "uri" : "./t.md" }
} ,
"vars" : { "k" : "v" } ,
2026-05-26 07:44:49 -05:00
"model" : { "model" : "gpt-x" , "timeout_seconds" : 120 , "service_tier" : "flex" , "api_key_env" : "SCRIPTORIUM_API_KEY" }
2026-05-04 21:19:29 -05:00
} ` )
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 )
}
2026-05-05 08:25:32 -05:00
metadata := resp [ "metadata" ] . ( map [ string ] any )
2026-05-05 10:54:35 -05:00
if metadata [ "prompt_id" ] != "prompt-1" {
t . Fatalf ( "unexpected metadata.prompt_id: %#v" , metadata [ "prompt_id" ] )
2026-05-05 08:41:07 -05:00
}
2026-05-05 10:54:35 -05:00
if metadata [ "prompt_version" ] != "1.0.0" {
t . Fatalf ( "unexpected metadata.prompt_version: %#v" , metadata [ "prompt_version" ] )
2026-05-05 08:41:07 -05:00
}
2026-05-05 10:54:35 -05:00
if metadata [ "selected_profile_id" ] != "exec-default" {
t . Fatalf ( "unexpected metadata.selected_profile_id: %#v" , metadata [ "selected_profile_id" ] )
2026-05-05 08:25:32 -05:00
}
2026-08-29 14:26:04 +00:00
if metadata [ "selected_backend_id" ] != "local" {
t . Fatalf ( "unexpected metadata.selected_backend_id: %#v" , metadata [ "selected_backend_id" ] )
}
2026-05-05 10:54:35 -05:00
if metadata [ "model_name" ] != "m1" || metadata [ "endpoint" ] != "http://llm/v1" {
t . Fatalf ( "unexpected model metadata: name=%#v endpoint=%#v" , metadata [ "model_name" ] , metadata [ "endpoint" ] )
2026-05-05 08:41:07 -05:00
}
2026-07-02 23:07:57 +00:00
usage := metadata [ "usage" ] . ( map [ string ] any )
if usage [ "prompt_tokens" ] != float64 ( 1 ) || usage [ "completion_tokens" ] != float64 ( 2 ) || usage [ "total_tokens" ] != float64 ( 3 ) {
t . Fatalf ( "unexpected base usage metadata: %#v" , usage )
}
if usage [ "cached_tokens" ] != float64 ( 4 ) || usage [ "cache_write_tokens" ] != float64 ( 5 ) {
t . Fatalf ( "unexpected cache usage metadata: %#v" , usage )
}
2026-05-05 08:41:07 -05:00
modelParams := metadata [ "model_params" ] . ( map [ string ] any )
2026-08-29 14:26:04 +00:00
if modelParams [ "backend_id" ] != "local" {
t . Fatalf ( "expected model_params.backend_id=local, got %#v" , modelParams [ "backend_id" ] )
}
2026-05-05 10:54:35 -05:00
if modelParams [ "api_key_env" ] != envName {
t . Fatalf ( "expected model_params.api_key_env=%q, got %#v" , envName , modelParams [ "api_key_env" ] )
2026-05-05 08:41:07 -05:00
}
2026-05-26 07:44:49 -05:00
if modelParams [ "service_tier" ] != "priority" {
t . Fatalf ( "expected model_params.service_tier=priority, got %#v" , modelParams [ "service_tier" ] )
}
2026-05-05 10:54:35 -05:00
if strings . Contains ( w . Body . String ( ) , secret ) {
t . Fatalf ( "response leaked raw API key value: %s" , w . Body . String ( ) )
2026-05-04 21:19:29 -05:00
}
2026-05-05 20:01:45 -05:00
if _ , ok := resp [ "raw_model_output" ] ; ok {
t . Fatalf ( "expected raw_model_output to be omitted by default, got %#v" , resp [ "raw_model_output" ] )
}
2026-05-04 21:19:29 -05:00
2026-05-05 10:09:31 -05:00
if r . last . PromptID != "prompt-1" {
t . Fatalf ( "expected request prompt_id prompt-1, got %q" , r . last . PromptID )
2026-05-04 21:19:29 -05:00
}
2026-05-05 10:09:31 -05:00
if r . last . ProfileID != "exec-default" {
t . Fatalf ( "expected request profile_id exec-default, got %q" , r . last . ProfileID )
2026-05-04 21:19:29 -05:00
}
2026-05-05 10:09:31 -05:00
if r . last . Execution == nil || r . last . Execution . Model != "gpt-x" {
t . Fatalf ( "expected model override, got %#v" , r . last . Execution )
}
2026-07-04 13:20:39 +00:00
if r . last . Execution . TimeoutSeconds == nil || * r . last . Execution . TimeoutSeconds != 120 {
2026-05-05 10:09:31 -05:00
t . Fatalf ( "expected timeout_seconds override 120, got %#v" , r . last . Execution )
2026-05-04 22:38:20 -05:00
}
2026-05-26 07:44:49 -05:00
if r . last . Execution . ServiceTier != "flex" {
t . Fatalf ( "expected service_tier override flex, got %#v" , r . last . Execution )
}
2026-05-04 21:19:29 -05:00
}
2026-08-29 14:12:51 +00:00
func TestHandlerAllowsOmittedInputsAndMapsPromptVersion ( t * testing . T ) {
r := & fakeRunner { result : & promptkit . RunResult {
Artifact : promptkit . Artifact { Body : [ ] byte ( "ok" ) } ,
Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed , Mode : promptkit . ValidationNone , IsValid : true } ,
EffectiveModelParams : promptkit . ExecutionTarget { Endpoint : "http://llm/v1" , Model : "m1" } ,
} }
h := NewHandler ( r )
for _ , body := range [ ] string {
` { "prompt_id":"prompt-1","prompt_version":"2"} ` ,
` { "prompt_id":"prompt-1","prompt_version":"2","inputs": { }} ` ,
} {
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( body ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
if w . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d body=%s" , w . Code , w . Body . String ( ) )
}
if r . last . PromptVersion != "2" {
t . Fatalf ( "expected prompt version to be mapped, got %q" , r . last . PromptVersion )
}
if r . last . Inputs != nil {
t . Fatalf ( "expected omitted or empty inputs to remain nil, got %#v" , r . last . Inputs )
}
}
}
func TestHandlerDelegatesDefinitionInputRequirements ( t * testing . T ) {
tests := [ ] struct {
name string
definition string
wantStatus int
wantCode string
} {
{
name : "no declared inputs" ,
definition : ` id : p
version : "1"
default_profile : exec
messages :
- role : user
content : "hello"
output :
format : text
validation_mode : none
` ,
wantStatus : http . StatusOK ,
} ,
{
name : "optional input omitted" ,
definition : ` id : p
version : "1"
default_profile : exec
inputs :
- name : note
required : false
messages :
- role : user
content : "hello"
output :
format : text
validation_mode : none
` ,
wantStatus : http . StatusOK ,
} ,
{
name : "required input omitted" ,
definition : ` id : p
version : "1"
default_profile : exec
inputs :
- name : note
required : true
messages :
- role : user
content : "hello"
output :
format : text
validation_mode : none
` ,
wantStatus : http . StatusBadRequest ,
wantCode : "prompt_render_failed" ,
} ,
{
name : "template input omitted" ,
definition : ` id : p
version : "1"
default_profile : exec
messages :
- role : user
content : ' { { input "note" } } '
output :
format : text
validation_mode : none
` ,
wantStatus : http . StatusBadRequest ,
wantCode : "prompt_render_failed" ,
} ,
}
for _ , tc := range tests {
t . Run ( tc . name , func ( t * testing . T ) {
h := newDefinitionHandler ( t , tc . definition )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` { "prompt_id":"p"} ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
if w . Code != tc . wantStatus {
t . Fatalf ( "expected %d, got %d body=%s" , tc . wantStatus , w . Code , w . Body . String ( ) )
}
if tc . wantCode != "" {
assertHTTPErrorCode ( t , w , tc . wantStatus , tc . wantCode )
}
} )
}
}
2026-07-04 23:34:44 +00:00
func TestHandlerInlineRefsWorkWithoutArtifactRoot ( t * testing . T ) {
h := newArtifactRootHandler ( t , "" )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` {
"prompt_id" : "p" ,
"inputs" : { "x" : { "type" : "inline" , "body" : "inline body" } }
} ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
if w . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d body=%s" , w . Code , w . Body . String ( ) )
}
}
func TestHandlerFileRefsWithoutArtifactRootAreRejected ( t * testing . T ) {
h := newArtifactRootHandler ( t , "" )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` {
"prompt_id" : "p" ,
"inputs" : { "x" : { "type" : "file" , "uri" : "input.txt" } }
} ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
assertHTTPErrorCode ( t , w , http . StatusBadRequest , "artifact_not_allowed" )
}
func TestHandlerFileRefsUnderArtifactRootWork ( t * testing . T ) {
root := t . TempDir ( )
if err := os . WriteFile ( filepath . Join ( root , "input.txt" ) , [ ] byte ( "allowed" ) , 0 o644 ) ; err != nil {
t . Fatal ( err )
}
h := newArtifactRootHandler ( t , root )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` {
"prompt_id" : "p" ,
"inputs" : { "x" : { "type" : "file" , "uri" : "input.txt" } }
} ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
if w . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d body=%s" , w . Code , w . Body . String ( ) )
}
}
2026-07-05 00:17:07 +00:00
func TestHandlerFileRefsAboveArtifactLimitAreRejected ( t * testing . T ) {
root := t . TempDir ( )
if err := os . WriteFile ( filepath . Join ( root , "large.txt" ) , [ ] byte ( "123456" ) , 0 o644 ) ; err != nil {
t . Fatal ( err )
}
h := newArtifactRootHandlerWithLimit ( t , root , 5 )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` {
"prompt_id" : "p" ,
"inputs" : { "x" : { "type" : "file" , "uri" : "large.txt" } }
} ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
assertHTTPErrorCode ( t , w , http . StatusRequestEntityTooLarge , "artifact_too_large" )
}
2026-07-04 23:34:44 +00:00
func TestHandlerFileRefsOutsideArtifactRootAreRejected ( t * testing . T ) {
root := t . TempDir ( )
outside := t . TempDir ( )
if err := os . WriteFile ( filepath . Join ( outside , "secret.txt" ) , [ ] byte ( "denied" ) , 0 o644 ) ; err != nil {
t . Fatal ( err )
}
h := newArtifactRootHandler ( t , root )
tests := [ ] struct {
name string
uri string
} {
{ name : "relative traversal" , uri : filepath . Join ( ".." , filepath . Base ( outside ) , "secret.txt" ) } ,
{ name : "absolute outside root" , uri : filepath . Join ( outside , "secret.txt" ) } ,
}
for _ , tc := range tests {
t . Run ( tc . name , func ( t * testing . T ) {
body := fmt . Sprintf ( ` {
"prompt_id" : "p" ,
"inputs" : { "x" : { "type" : "file" , "uri" : % q } }
} ` , tc . uri )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( body ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
assertHTTPErrorCode ( t , w , http . StatusBadRequest , "artifact_not_allowed" )
} )
}
}
2026-05-05 10:54:35 -05:00
func TestHandlerPostRunsSuccessUsingPromptDefaultProfile ( t * testing . T ) {
2026-07-28 14:04:29 +00:00
r := & fakeRunner { result : & promptkit . RunResult {
Artifact : promptkit . Artifact { Body : [ ] byte ( "ok" ) } ,
2026-05-05 10:54:35 -05:00
PromptID : "prompt-1" ,
PromptVersion : "1.0.0" ,
SelectedProfileID : "prompt-default" ,
2026-07-28 14:04:29 +00:00
Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed , Mode : promptkit . ValidationBasic , IsValid : true } ,
EffectiveModelParams : promptkit . ExecutionTarget { Endpoint : "http://llm/v1" , Model : "m1" } ,
2026-05-05 10:54:35 -05:00
} }
h := NewHandler ( r )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` { "prompt_id":"prompt-1","inputs": { "x": { "type":"file","uri":"a"}}} ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
if w . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d body=%s" , w . Code , w . Body . String ( ) )
}
if r . last . ProfileID != "" {
t . Fatalf ( "expected empty request profile_id when omitted, got %q" , r . last . ProfileID )
}
var resp map [ string ] any
if err := json . Unmarshal ( w . Body . Bytes ( ) , & resp ) ; err != nil {
t . Fatalf ( "invalid JSON response: %v" , err )
}
metadata := resp [ "metadata" ] . ( map [ string ] any )
if metadata [ "selected_profile_id" ] != "prompt-default" {
t . Fatalf ( "expected selected_profile_id from result, got %#v" , metadata [ "selected_profile_id" ] )
}
2026-07-02 23:07:57 +00:00
usage := metadata [ "usage" ] . ( map [ string ] any )
if usage [ "cached_tokens" ] != float64 ( 0 ) || usage [ "cache_write_tokens" ] != float64 ( 0 ) {
t . Fatalf ( "expected zero cache usage fields to be included, got %#v" , usage )
}
2026-05-05 10:54:35 -05:00
}
2026-05-26 13:07:35 +00:00
func TestHandlerModelOverrideMapsAllSupportedExecutionFields ( t * testing . T ) {
2026-07-28 14:04:29 +00:00
r := & fakeRunner { result : & promptkit . RunResult {
Artifact : promptkit . Artifact { Body : [ ] byte ( "ok" ) } ,
Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed , Mode : promptkit . ValidationBasic , IsValid : true } ,
EffectiveModelParams : promptkit . ExecutionTarget { Endpoint : "http://llm/v1" , Model : "m1" } ,
2026-05-26 13:07:35 +00:00
} }
h := NewHandler ( r )
reqBody := ` {
"prompt_id" : "prompt-1" ,
"inputs" : { "transcript" : { "type" : "file" , "uri" : "./t.md" } } ,
"model" : {
"endpoint" : "http://override/v1" ,
"model" : "override-model" ,
"temperature" : 0.6 ,
"max_tokens" : 250 ,
"top_p" : 0.85 ,
"timeout_seconds" : 33 ,
"service_tier" : "flex" ,
"reasoning_effort" : "medium" ,
"api_key_env" : "SCRIPTORIUM_API_KEY" ,
"extra_params" : { "provider_option" : "on" }
}
} `
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( reqBody ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
if w . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d body=%s" , w . Code , w . Body . String ( ) )
}
if r . last . Execution == nil {
t . Fatalf ( "expected execution override in run request" )
}
got := r . last . Execution
if got . Endpoint != "http://override/v1" ||
got . Model != "override-model" ||
got . ServiceTier != "flex" ||
got . APIKeyEnv != "SCRIPTORIUM_API_KEY" {
t . Fatalf ( "unexpected mapped execution target: %+v" , got )
}
2026-08-29 14:08:35 +00:00
if got . ReasoningEffort == nil || * got . ReasoningEffort != "medium" {
t . Fatalf ( "unexpected mapped reasoning_effort: %#v" , got . ReasoningEffort )
}
2026-07-04 13:20:39 +00:00
if got . Temperature == nil || * got . Temperature != 0.6 {
t . Fatalf ( "unexpected mapped temperature: %#v" , got . Temperature )
}
if got . MaxTokens == nil || * got . MaxTokens != 250 {
t . Fatalf ( "unexpected mapped max_tokens: %#v" , got . MaxTokens )
}
if got . TopP == nil || * got . TopP != 0.85 {
t . Fatalf ( "unexpected mapped top_p: %#v" , got . TopP )
}
if got . TimeoutSeconds == nil || * got . TimeoutSeconds != 33 {
t . Fatalf ( "unexpected mapped timeout_seconds: %#v" , got . TimeoutSeconds )
}
if ! reflect . DeepEqual ( got . ExtraParams , map [ string ] any { "provider_option" : "on" } ) {
2026-05-26 13:07:35 +00:00
t . Fatalf ( "unexpected mapped extra_params: %#v" , got . ExtraParams )
}
}
2026-08-29 14:08:35 +00:00
func TestHandlerModelOverridePreservesReasoningEffortPresence ( t * testing . T ) {
tests := [ ] struct {
name string
model string
wantPresent bool
wantValue string
} {
{ name : "omitted" , model : ` { } ` } ,
2026-08-29 14:22:45 +00:00
{ name : "nonblank" , model : ` { "reasoning_effort":"high"} ` , wantPresent : true , wantValue : "high" } ,
2026-08-29 14:08:35 +00:00
{ name : "explicit empty" , model : ` { "reasoning_effort":""} ` , wantPresent : true } ,
2026-08-29 14:22:45 +00:00
{ name : "null" , model : ` { "reasoning_effort":null} ` } ,
2026-08-29 14:08:35 +00:00
}
for _ , tc := range tests {
t . Run ( tc . name , func ( t * testing . T ) {
r := & fakeRunner { result : & promptkit . RunResult {
Artifact : promptkit . Artifact { Body : [ ] byte ( "ok" ) } ,
Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed , Mode : promptkit . ValidationBasic , IsValid : true } ,
EffectiveModelParams : promptkit . ExecutionTarget { Endpoint : "http://llm/v1" , Model : "m1" } ,
} }
h := NewHandler ( r )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` { "prompt_id":"prompt-1","inputs": { "transcript": { "type":"file","uri":"./t.md"}},"model": ` + tc . model + ` } ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
if w . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d body=%s" , w . Code , w . Body . String ( ) )
}
if r . last . Execution == nil {
t . Fatal ( "expected execution override" )
}
if ( r . last . Execution . ReasoningEffort != nil ) != tc . wantPresent {
t . Fatalf ( "unexpected reasoning_effort presence: %#v" , r . last . Execution . ReasoningEffort )
}
if tc . wantPresent && * r . last . Execution . ReasoningEffort != tc . wantValue {
t . Fatalf ( "unexpected reasoning_effort: got %q want %q" , * r . last . Execution . ReasoningEffort , tc . wantValue )
}
} )
}
}
2026-08-29 14:22:45 +00:00
func TestHandlerMapsSessionIDAndReportsEffectiveResultSessionID ( t * testing . T ) {
r := & fakeRunner { result : & promptkit . RunResult {
Artifact : promptkit . Artifact { Body : [ ] byte ( "ok" ) } ,
Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed , Mode : promptkit . ValidationBasic , IsValid : true } ,
EffectiveModelParams : promptkit . ExecutionTarget { Endpoint : "http://llm/v1" , Model : "m1" } ,
SessionID : "effective-session" ,
} }
h := NewHandler ( r )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` { "prompt_id":"prompt-1","session_id":"request-session"} ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
if w . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d body=%s" , w . Code , w . Body . String ( ) )
}
if r . last . SessionID != "request-session" {
t . Fatalf ( "expected session ID in run request, got %q" , r . last . SessionID )
}
var resp map [ string ] any
if err := json . Unmarshal ( w . Body . Bytes ( ) , & resp ) ; err != nil {
t . Fatalf ( "invalid JSON response: %v" , err )
}
metadata := resp [ "metadata" ] . ( map [ string ] any )
if metadata [ "session_id" ] != "effective-session" {
t . Fatalf ( "expected effective session ID in response, got %#v" , metadata [ "session_id" ] )
}
}
func TestHandlerOmitsEmptyEffectiveSessionID ( t * testing . T ) {
r := & fakeRunner { result : & promptkit . RunResult {
Artifact : promptkit . Artifact { Body : [ ] byte ( "ok" ) } ,
Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed , Mode : promptkit . ValidationBasic , IsValid : true } ,
EffectiveModelParams : promptkit . ExecutionTarget { Endpoint : "http://llm/v1" , Model : "m1" } ,
} }
h := NewHandler ( r )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` { "prompt_id":"prompt-1"} ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
if w . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d body=%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 )
}
metadata := resp [ "metadata" ] . ( map [ string ] any )
if _ , ok := metadata [ "session_id" ] ; ok {
t . Fatalf ( "expected empty effective session ID to be omitted, got %#v" , metadata )
}
}
func TestHandlerSessionIDUsesPromptkitResolution ( t * testing . T ) {
for _ , tc := range [ ] struct {
name string
definitionSession string
requestSession string
wantSession string
} {
{ name : "definition session" , definitionSession : "definition-session" , wantSession : "definition-session" } ,
{ name : "direct session" , definitionSession : "definition-session" , requestSession : "direct-session" , wantSession : "direct-session" } ,
{ name : "no effective session" } ,
} {
t . Run ( tc . name , func ( t * testing . T ) {
promptDir := t . TempDir ( )
profileDir := t . TempDir ( )
sessionLine := ""
if tc . definitionSession != "" {
sessionLine = "session_id: " + tc . definitionSession + "\n"
}
if err := os . WriteFile ( filepath . Join ( promptDir , "prompt.yaml" ) , [ ] byte ( "id: p\nversion: \"1\"\ndefault_profile: exec\n" + sessionLine + ` messages :
- role : user
content : "hi"
output :
format : text
validation_mode : none
` ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write prompt fixture: %v" , err )
}
if err := os . WriteFile ( filepath . Join ( profileDir , "profile.yaml" ) , [ ] byte ( "id: exec\nendpoint: http://127.0.0.1:1/v1\nmodel: test\n" ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write profile fixture: %v" , err )
}
engine , err := promptkit . NewEngine ( promptkit . Config { PromptDir : promptDir , ProfileDir : profileDir } , promptkit . WithLLMClient ( handlerLLMClient { } ) )
if err != nil {
t . Fatalf ( "new engine: %v" , err )
}
body := ` { "prompt_id":"p"} `
if tc . requestSession != "" {
body = ` { "prompt_id":"p","session_id":" ` + tc . requestSession + ` "} `
}
w := httptest . NewRecorder ( )
NewHandler ( engine ) . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( body ) ) )
if w . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d body=%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 )
}
metadata := resp [ "metadata" ] . ( map [ string ] any )
if tc . wantSession == "" {
if _ , ok := metadata [ "session_id" ] ; ok {
t . Fatalf ( "expected session_id to be omitted, got %#v" , metadata )
}
return
}
if metadata [ "session_id" ] != tc . wantSession {
t . Fatalf ( "expected effective session ID %q, got %#v" , tc . wantSession , metadata [ "session_id" ] )
}
} )
}
}
2026-08-29 14:26:04 +00:00
func TestHandlerSharesBackendCapacityAcrossConcurrentRequests ( t * testing . T ) {
promptDir := t . TempDir ( )
profileDir := t . TempDir ( )
if err := os . WriteFile ( filepath . Join ( promptDir , "prompt.yaml" ) , [ ] byte ( ` id : p
version : "1"
default_profile : limited
messages :
- role : user
content : "hi"
output :
format : text
validation_mode : none
` ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write prompt fixture: %v" , err )
}
if err := os . WriteFile ( filepath . Join ( profileDir , "profile.yaml" ) , [ ] byte ( ` id : limited
backend : limited
model : test
` ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write profile fixture: %v" , err )
}
queueCapacity := 0
client := & blockingLLMClient { started : make ( chan struct { } , 1 ) , release : make ( chan struct { } ) }
engine , err := promptkit . NewEngine (
promptkit . Config { PromptDir : promptDir , ProfileDir : profileDir } ,
promptkit . WithBackend ( promptkit . Backend {
ID : "limited" ,
Endpoint : "http://127.0.0.1:1/v1" ,
ConcurrencyLimit : 1 ,
QueueCapacity : & queueCapacity ,
} ) ,
promptkit . WithLLMClient ( client ) ,
)
if err != nil {
t . Fatalf ( "new engine: %v" , err )
}
h := NewHandler ( engine )
first := httptest . NewRecorder ( )
firstDone := make ( chan struct { } )
go func ( ) {
defer close ( firstDone )
h . ServeHTTP ( first , httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` { "prompt_id":"p"} ` ) ) )
} ( )
select {
case <- client . started :
case <- time . After ( time . Second ) :
t . Fatal ( "first request did not reach generation" )
}
second := httptest . NewRecorder ( )
h . ServeHTTP ( second , httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` { "prompt_id":"p"} ` ) ) )
assertHTTPErrorCode ( t , second , http . StatusServiceUnavailable , "capacity_exceeded" )
if second . Header ( ) . Get ( "Retry-After" ) != "" {
t . Fatalf ( "expected no Retry-After header, got %q" , second . Header ( ) . Get ( "Retry-After" ) )
}
if strings . Contains ( second . Body . String ( ) , "limited" ) {
t . Fatalf ( "capacity response leaked backend details: %s" , second . Body . String ( ) )
}
close ( client . release )
select {
case <- firstDone :
case <- time . After ( time . Second ) :
t . Fatal ( "first request did not complete" )
}
if first . Code != http . StatusOK {
t . Fatalf ( "expected first request to succeed, got %d body=%s" , first . Code , first . Body . String ( ) )
}
if atomic . LoadInt32 ( & client . peak ) != 1 {
t . Fatalf ( "expected peak generation concurrency of one, got %d" , atomic . LoadInt32 ( & client . peak ) )
}
}
2026-09-26 15:31:31 +00:00
func TestHandlerBuiltInTransientRetriesInheritAndAllowExplicitZero ( t * testing . T ) {
var hits int32
provider := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
if atomic . AddInt32 ( & hits , 1 ) == 1 {
http . Error ( w , "private provider detail" , http . StatusServiceUnavailable )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ , _ = w . Write ( [ ] byte ( ` { "choices":[ { "message": { "role":"assistant","content":"recovered"}}]} ` ) )
} ) )
defer provider . Close ( )
h := newRetryHandler ( t , provider . URL + "/v1" , intPointer ( 1 ) , 0 , nil )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( ` { "prompt_id":"p"} ` ) ) )
if w . Code != http . StatusOK || atomic . LoadInt32 ( & hits ) != 2 {
t . Fatalf ( "inherited retry: status=%d hits=%d body=%s" , w . Code , atomic . LoadInt32 ( & hits ) , w . Body . String ( ) )
}
atomic . StoreInt32 ( & hits , 0 )
w = httptest . NewRecorder ( )
h . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( ` { "prompt_id":"p","transient_retries":0} ` ) ) )
assertHTTPErrorCode ( t , w , http . StatusBadGateway , "llm_failed" )
if atomic . LoadInt32 ( & hits ) != 1 || strings . Contains ( w . Body . String ( ) , "private provider detail" ) {
t . Fatalf ( "explicit zero or safe error failed: hits=%d body=%s" , atomic . LoadInt32 ( & hits ) , w . Body . String ( ) )
}
}
func TestHandlerRetainsSharedBackendLimitDuringRetry ( t * testing . T ) {
started := make ( chan struct { } , 1 )
release := make ( chan struct { } )
var hits , active , peak int32
provider := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
current := atomic . AddInt32 ( & active , 1 )
defer atomic . AddInt32 ( & active , - 1 )
for {
old := atomic . LoadInt32 ( & peak )
if current <= old || atomic . CompareAndSwapInt32 ( & peak , old , current ) {
break
}
}
if atomic . AddInt32 ( & hits , 1 ) == 1 {
started <- struct { } { }
<- release
http . Error ( w , "temporary" , http . StatusServiceUnavailable )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ , _ = w . Write ( [ ] byte ( ` { "choices":[ { "message": { "role":"assistant","content":"ok"}}]} ` ) )
} ) )
defer provider . Close ( )
2026-09-26 15:39:17 +00:00
defer func ( ) {
select {
case <- release :
default :
close ( release )
}
} ( )
2026-09-26 15:31:31 +00:00
queueCapacity := 0
h := newRetryHandler ( t , provider . URL + "/v1" , intPointer ( 1 ) , 1 , & queueCapacity )
first := httptest . NewRecorder ( )
done := make ( chan struct { } )
go func ( ) {
defer close ( done )
h . ServeHTTP ( first , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( ` { "prompt_id":"p"} ` ) ) )
} ( )
select {
case <- started :
case <- time . After ( 3 * time . Second ) :
t . Fatal ( "first provider call did not start" )
}
second := httptest . NewRecorder ( )
h . ServeHTTP ( second , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( ` { "prompt_id":"p","transient_retries":0} ` ) ) )
assertHTTPErrorCode ( t , second , http . StatusServiceUnavailable , "capacity_exceeded" )
close ( release )
select {
case <- done :
case <- time . After ( 5 * time . Second ) :
t . Fatal ( "retrying request did not complete" )
}
if first . Code != http . StatusOK || atomic . LoadInt32 ( & hits ) != 2 || atomic . LoadInt32 ( & peak ) > 1 {
t . Fatalf ( "shared backend limit failed: first=%d hits=%d peak=%d body=%s" , first . Code , atomic . LoadInt32 ( & hits ) , atomic . LoadInt32 ( & peak ) , first . Body . String ( ) )
}
}
2026-09-26 15:39:17 +00:00
func TestHandlerDeadlineCancelsBuiltInRetryWait ( t * testing . T ) {
var hits int32
provider := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
atomic . AddInt32 ( & hits , 1 )
w . Header ( ) . Set ( "Retry-After" , "3600" )
http . Error ( w , "temporary" , http . StatusServiceUnavailable )
} ) )
defer provider . Close ( )
h := newRetryHandler ( t , provider . URL + "/v1" , intPointer ( 1 ) , 0 , nil )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( ` { "prompt_id":"p","run_timeout_seconds":1} ` ) ) )
assertHTTPErrorCode ( t , w , http . StatusGatewayTimeout , "deadline_exceeded" )
if atomic . LoadInt32 ( & hits ) != 1 {
t . Fatalf ( "expected one provider call before canceled retry wait, got %d" , atomic . LoadInt32 ( & hits ) )
}
}
func TestHandlerDeadlineCancelsCapacityWaitAndReleasesAdmission ( t * testing . T ) {
started := make ( chan struct { } , 1 )
release := make ( chan struct { } )
var hits int32
provider := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
if atomic . AddInt32 ( & hits , 1 ) == 1 {
started <- struct { } { }
<- release
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ , _ = w . Write ( [ ] byte ( ` { "choices":[ { "message": { "role":"assistant","content":"ok"}}]} ` ) )
} ) )
defer provider . Close ( )
defer func ( ) {
select {
case <- release :
default :
close ( release )
}
} ( )
queue := 1
h := newRetryHandler ( t , provider . URL + "/v1" , intPointer ( 0 ) , 1 , & queue )
first := httptest . NewRecorder ( )
done := make ( chan struct { } )
go func ( ) {
defer close ( done )
h . ServeHTTP ( first , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( ` { "prompt_id":"p"} ` ) ) )
} ( )
select {
case <- started :
case <- time . After ( 3 * time . Second ) :
t . Fatal ( "first provider call did not start" )
}
waiting := httptest . NewRecorder ( )
h . ServeHTTP ( waiting , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( ` { "prompt_id":"p","run_timeout_seconds":1} ` ) ) )
assertHTTPErrorCode ( t , waiting , http . StatusGatewayTimeout , "deadline_exceeded" )
if atomic . LoadInt32 ( & hits ) != 1 {
t . Fatalf ( "capacity waiter reached provider: hits=%d" , atomic . LoadInt32 ( & hits ) )
}
close ( release )
select {
case <- done :
case <- time . After ( 3 * time . Second ) :
t . Fatal ( "first request did not finish" )
}
if first . Code != http . StatusOK {
t . Fatalf ( "first request: status=%d body=%s" , first . Code , first . Body . String ( ) )
}
third := httptest . NewRecorder ( )
h . ServeHTTP ( third , httptest . NewRequest ( http . MethodPost , "/v1/runs" , strings . NewReader ( ` { "prompt_id":"p"} ` ) ) )
if third . Code != http . StatusOK || atomic . LoadInt32 ( & hits ) != 2 {
t . Fatalf ( "admission was not released: status=%d hits=%d body=%s" , third . Code , atomic . LoadInt32 ( & hits ) , third . Body . String ( ) )
}
}
2026-09-26 15:31:31 +00:00
func newRetryHandler ( t * testing . T , endpoint string , retries * int , concurrency int , queue * int ) * Handler {
t . Helper ( )
promptDir := t . TempDir ( )
profileDir := t . TempDir ( )
if err := os . WriteFile ( filepath . Join ( promptDir , "prompt.yaml" ) , [ ] byte ( "id: p\nversion: \"1\"\ndefault_profile: limited\nmessages:\n - role: user\n content: hi\noutput:\n format: text\n validation_mode: none\n" ) , 0 o644 ) ; err != nil {
t . Fatal ( err )
}
if err := os . WriteFile ( filepath . Join ( profileDir , "profile.yaml" ) , [ ] byte ( "id: limited\nbackend: limited\nmodel: test\n" ) , 0 o644 ) ; err != nil {
t . Fatal ( err )
}
engine , err := promptkit . NewEngine ( promptkit . Config { PromptDir : promptDir , ProfileDir : profileDir , TransientRetries : retries } , promptkit . WithBackend ( promptkit . Backend { ID : "limited" , Endpoint : endpoint , ConcurrencyLimit : concurrency , QueueCapacity : queue } ) )
if err != nil {
t . Fatal ( err )
}
return NewHandler ( engine )
}
2026-08-29 14:22:45 +00:00
func TestHandlerRejectsOverlongSessionIDAndNonStringReasoningEffort ( t * testing . T ) {
engine := newHandlerEngine ( t )
for _ , body := range [ ] string {
` { "prompt_id":"p","session_id":" ` + strings . Repeat ( "x" , 257 ) + ` "} ` ,
` { "prompt_id":"p","model": { "reasoning_effort":1}} ` ,
} {
w := httptest . NewRecorder ( )
NewHandler ( engine ) . ServeHTTP ( w , httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( body ) ) )
if w . Code != http . StatusBadRequest {
t . Fatalf ( "expected 400, got %d body=%s" , w . Code , w . Body . String ( ) )
}
}
}
2026-07-04 13:24:27 +00:00
func TestHandlerModelOverrideAcceptsJSONCompatibleExtraParams ( t * testing . T ) {
2026-07-28 14:04:29 +00:00
r := & fakeRunner { result : & promptkit . RunResult {
Artifact : promptkit . Artifact { Body : [ ] byte ( "ok" ) } ,
Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed , Mode : promptkit . ValidationBasic , IsValid : true } ,
EffectiveModelParams : promptkit . ExecutionTarget { Endpoint : "http://llm/v1" , Model : "m1" } ,
2026-07-04 13:24:27 +00:00
} }
h := NewHandler ( r )
reqBody := ` {
"prompt_id" : "prompt-1" ,
"inputs" : { "transcript" : { "type" : "file" , "uri" : "./t.md" } } ,
"model" : {
"extra_params" : {
"string_value" : "enabled" ,
"number_value" : 42 ,
"boolean_value" : true ,
"object_value" : { "nested" : "value" , "count" : 2 } ,
"array_value" : [ "first" , 3 , false ]
}
}
} `
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( reqBody ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
if w . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d body=%s" , w . Code , w . Body . String ( ) )
}
if r . last . Execution == nil {
t . Fatal ( "expected execution override in run request" )
}
want := map [ string ] any {
"string_value" : "enabled" ,
"number_value" : float64 ( 42 ) ,
"boolean_value" : true ,
"object_value" : map [ string ] any { "nested" : "value" , "count" : float64 ( 2 ) } ,
"array_value" : [ ] any { "first" , float64 ( 3 ) , false } ,
}
if ! reflect . DeepEqual ( r . last . Execution . ExtraParams , want ) {
t . Fatalf ( "unexpected mapped extra_params:\ngot=%#v\nwant=%#v" , r . last . Execution . ExtraParams , want )
}
}
2026-07-04 13:20:39 +00:00
func TestHandlerModelOverrideExplicitZeroTemperatureMapsAsPresent ( t * testing . T ) {
2026-07-28 14:04:29 +00:00
r := & fakeRunner { result : & promptkit . RunResult {
Artifact : promptkit . Artifact { Body : [ ] byte ( "ok" ) } ,
Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed , Mode : promptkit . ValidationBasic , IsValid : true } ,
EffectiveModelParams : promptkit . ExecutionTarget { Endpoint : "http://llm/v1" , Model : "m1" , Temperature : 0 } ,
2026-07-04 13:20:39 +00:00
} }
h := NewHandler ( r )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` {
"prompt_id" : "prompt-1" ,
"inputs" : { "transcript" : { "type" : "file" , "uri" : "./t.md" } } ,
"model" : { "temperature" : 0 }
} ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
if w . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d body=%s" , w . Code , w . Body . String ( ) )
}
if r . last . Execution == nil || r . last . Execution . Temperature == nil {
t . Fatalf ( "expected temperature override to be present, got %#v" , r . last . Execution )
}
if * r . last . Execution . Temperature != 0 {
t . Fatalf ( "expected zero temperature override, got %v" , * r . last . Execution . Temperature )
}
}
func TestHandlerModelOverrideOmittedTemperatureMapsAsAbsent ( t * testing . T ) {
2026-07-28 14:04:29 +00:00
r := & fakeRunner { result : & promptkit . RunResult {
Artifact : promptkit . Artifact { Body : [ ] byte ( "ok" ) } ,
Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed , Mode : promptkit . ValidationBasic , IsValid : true } ,
EffectiveModelParams : promptkit . ExecutionTarget { Endpoint : "http://llm/v1" , Model : "m1" , Temperature : 0.7 } ,
2026-07-04 13:20:39 +00:00
} }
h := NewHandler ( r )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` {
"prompt_id" : "prompt-1" ,
"inputs" : { "transcript" : { "type" : "file" , "uri" : "./t.md" } } ,
"model" : { "model" : "override-model" }
} ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
if w . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d body=%s" , w . Code , w . Body . String ( ) )
}
if r . last . Execution == nil {
t . Fatal ( "expected model override" )
}
if r . last . Execution . Temperature != nil {
t . Fatalf ( "expected omitted temperature to remain absent, got %#v" , r . last . Execution . Temperature )
}
var resp map [ string ] any
if err := json . Unmarshal ( w . Body . Bytes ( ) , & resp ) ; err != nil {
t . Fatalf ( "invalid JSON response: %v" , err )
}
metadata := resp [ "metadata" ] . ( map [ string ] any )
params := metadata [ "model_params" ] . ( map [ string ] any )
if params [ "temperature" ] != 0.7 {
t . Fatalf ( "expected effective profile/default temperature in response, got %#v" , params [ "temperature" ] )
}
}
2026-05-26 13:07:35 +00:00
func TestHandlerResponseMetadataModelParamsIncludesAllSupportedFields ( t * testing . T ) {
2026-07-28 14:04:29 +00:00
r := & fakeRunner { result : & promptkit . RunResult {
Artifact : promptkit . Artifact {
2026-05-26 13:07:35 +00:00
Name : "output" ,
ContentType : "text/plain" ,
Body : [ ] byte ( "ok" ) ,
Size : 2 ,
Hash : "abc" ,
} ,
2026-07-28 14:04:29 +00:00
Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed , Mode : promptkit . ValidationBasic , IsValid : true } ,
EffectiveModelParams : promptkit . ExecutionTarget {
2026-05-26 13:07:35 +00:00
Endpoint : "http://llm/v1" ,
Model : "gpt-test" ,
Temperature : 0.4 ,
MaxTokens : 321 ,
TopP : 0.7 ,
TimeoutSeconds : 45 ,
ServiceTier : "priority" ,
ReasoningEffort : "high" ,
APIKeyEnv : "SCRIPTORIUM_API_KEY" ,
2026-07-04 13:20:39 +00:00
ExtraParams : map [ string ] any {
2026-05-26 13:07:35 +00:00
"provider_option" : "on" ,
2026-07-04 13:24:27 +00:00
"number_value" : 42 ,
"object_value" : map [ string ] any { "nested" : "value" } ,
2026-05-26 13:07:35 +00:00
} ,
} ,
} }
h := NewHandler ( r )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` { "prompt_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 body=%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 )
}
metadata := resp [ "metadata" ] . ( map [ string ] any )
params := metadata [ "model_params" ] . ( map [ string ] any )
if params [ "endpoint" ] != "http://llm/v1" {
t . Fatalf ( "unexpected endpoint: %#v" , params [ "endpoint" ] )
}
if params [ "model" ] != "gpt-test" {
t . Fatalf ( "unexpected model: %#v" , params [ "model" ] )
}
if params [ "temperature" ] != 0.4 {
t . Fatalf ( "unexpected temperature: %#v" , params [ "temperature" ] )
}
if params [ "max_tokens" ] != float64 ( 321 ) {
t . Fatalf ( "unexpected max_tokens: %#v" , params [ "max_tokens" ] )
}
if params [ "top_p" ] != 0.7 {
t . Fatalf ( "unexpected top_p: %#v" , params [ "top_p" ] )
}
if params [ "timeout_seconds" ] != float64 ( 45 ) {
t . Fatalf ( "unexpected timeout_seconds: %#v" , params [ "timeout_seconds" ] )
}
if params [ "service_tier" ] != "priority" {
t . Fatalf ( "unexpected service_tier: %#v" , params [ "service_tier" ] )
}
if params [ "reasoning_effort" ] != "high" {
t . Fatalf ( "unexpected reasoning_effort: %#v" , params [ "reasoning_effort" ] )
}
if params [ "api_key_env" ] != "SCRIPTORIUM_API_KEY" {
t . Fatalf ( "unexpected api_key_env: %#v" , params [ "api_key_env" ] )
}
extraParams , ok := params [ "extra_params" ] . ( map [ string ] any )
if ! ok {
t . Fatalf ( "expected extra_params object, got %#v" , params [ "extra_params" ] )
}
if extraParams [ "provider_option" ] != "on" {
t . Fatalf ( "unexpected extra_params.provider_option: %#v" , extraParams [ "provider_option" ] )
}
2026-07-04 13:24:27 +00:00
if extraParams [ "number_value" ] != float64 ( 42 ) {
t . Fatalf ( "unexpected extra_params.number_value: %#v" , extraParams [ "number_value" ] )
}
objectValue , ok := extraParams [ "object_value" ] . ( map [ string ] any )
if ! ok || objectValue [ "nested" ] != "value" {
t . Fatalf ( "unexpected extra_params.object_value: %#v" , extraParams [ "object_value" ] )
}
2026-05-26 13:07:35 +00:00
}
2026-05-04 21:19:29 -05:00
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 )
}
}
2026-07-05 00:17:07 +00:00
func TestHandlerRejectsTrailingJSON ( t * testing . T ) {
h := NewHandler ( & fakeRunner { } )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` { "prompt_id":"p","inputs": { "x": { "type":"file","uri":"a"}}} { } ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
assertHTTPErrorCode ( t , w , http . StatusBadRequest , "invalid_json" )
}
func TestHandlerRequestTooLarge ( t * testing . T ) {
h := NewHandlerWithOptions ( & fakeRunner { } , HandlerOptions { MaxRequestBytes : 12 } )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` { "prompt_id":"p","inputs": { "x": { "type":"file","uri":"a"}}} ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
assertHTTPErrorCode ( t , w , http . StatusRequestEntityTooLarge , "request_too_large" )
}
func TestHandlerMalformedJSONBelowLimitStillBadRequest ( t * testing . T ) {
h := NewHandlerWithOptions ( & fakeRunner { } , HandlerOptions { MaxRequestBytes : 1024 } )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( "{" ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
assertHTTPErrorCode ( t , w , http . StatusBadRequest , "invalid_json" )
}
func TestHandlerResponseTooLarge ( t * testing . T ) {
2026-07-28 14:04:29 +00:00
h := NewHandlerWithOptions ( & fakeRunner { result : & promptkit . RunResult {
Artifact : promptkit . Artifact { Body : [ ] byte ( strings . Repeat ( "x" , 128 ) ) } ,
Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed , Mode : promptkit . ValidationBasic , IsValid : true } ,
EffectiveModelParams : promptkit . ExecutionTarget { Endpoint : "http://llm/v1" , Model : "m1" } ,
2026-07-05 00:17:07 +00:00
} } , HandlerOptions { MaxRequestBytes : 1024 , MaxResponseBytes : 64 } )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` { "prompt_id":"p","inputs": { "x": { "type":"file","uri":"a"}}} ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
assertHTTPErrorCode ( t , w , http . StatusRequestEntityTooLarge , "response_too_large" )
}
func TestHandlerRawOutputDoesNotBypassResponseLimit ( t * testing . T ) {
2026-07-28 14:04:29 +00:00
h := NewHandlerWithOptions ( & fakeRunner { result : & promptkit . RunResult {
Artifact : promptkit . Artifact { Body : [ ] byte ( "ok" ) } ,
2026-07-05 00:17:07 +00:00
RawOutput : strings . Repeat ( "raw" , 80 ) ,
2026-07-28 14:04:29 +00:00
Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed , Mode : promptkit . ValidationBasic , IsValid : true } ,
EffectiveModelParams : promptkit . ExecutionTarget { Endpoint : "http://llm/v1" , Model : "m1" } ,
2026-07-05 00:17:07 +00:00
} } , HandlerOptions { MaxRequestBytes : 1024 , MaxResponseBytes : 128 } )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` {
"prompt_id" : "p" ,
"inputs" : { "x" : { "type" : "file" , "uri" : "a" } } ,
"include_raw_output" : true
} ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
assertHTTPErrorCode ( t , w , http . StatusRequestEntityTooLarge , "response_too_large" )
}
2026-05-05 10:09:31 -05:00
func TestHandlerMissingPromptID ( t * testing . T ) {
2026-05-04 21:19:29 -05:00
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 )
}
2026-05-05 10:54:35 -05:00
var resp map [ string ] any
if err := json . Unmarshal ( w . Body . Bytes ( ) , & resp ) ; err != nil {
t . Fatalf ( "invalid JSON response: %v" , err )
}
errBody := resp [ "error" ] . ( map [ string ] any )
if errBody [ "code" ] != "invalid_request" {
2026-07-04 09:00:19 -05:00
t . Fatalf ( "expected invalid_request code, got %#v" , errBody [ "code" ] )
}
}
2026-07-28 00:52:24 +00:00
func TestHandlerReservedExtraParamsThroughEngineMapsToInvalidRequest ( t * testing . T ) {
h := NewHandler ( newHandlerEngineWithDefaultClient ( t ) )
2026-07-04 09:00:19 -05:00
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` {
"prompt_id" : "p" ,
2026-07-28 00:52:24 +00:00
"inputs" : { "x" : { "type" : "inline" , "body" : "input" } } ,
2026-07-04 09:00:19 -05:00
"model" : { "extra_params" : { "model" : "collision" } }
} ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
if w . Code != http . StatusBadRequest {
t . Fatalf ( "expected 400, got %d body=%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 )
}
errBody := resp [ "error" ] . ( map [ string ] any )
if errBody [ "code" ] != "invalid_request" {
2026-05-05 10:54:35 -05:00
t . Fatalf ( "expected invalid_request code, got %#v" , errBody [ "code" ] )
}
2026-05-04 21:19:29 -05:00
}
2026-07-28 00:52:24 +00:00
func TestHandlerPublicErrorMapping ( t * testing . T ) {
2026-05-04 21:19:29 -05:00
tests := [ ] struct {
2026-05-05 10:54:35 -05:00
name string
err error
status int
code string
message string
avoidCause string
2026-05-04 21:19:29 -05:00
} {
2026-07-28 14:04:29 +00:00
{ name : "prompt not found" , err : promptkit . ErrPromptNotFound , status : http . StatusNotFound , code : "prompt_not_found" , message : "prompt definition not found" } ,
{ name : "prompt load" , err : wrap ( promptkit . ErrPromptLoad , fmt . Errorf ( "read failed" ) ) , status : http . StatusBadRequest , code : "prompt_load_failed" , message : "failed to load prompt definition" , avoidCause : "read failed" } ,
{ name : "missing profile/default" , err : wrap ( promptkit . ErrProfileRequired , promptkit . ErrInvalidRequest ) , status : http . StatusBadRequest , code : "profile_required" , message : "profile_id is required when prompt default_profile is not set" } ,
{ name : "profile not found" , err : promptkit . ErrProfileNotFound , status : http . StatusNotFound , code : "profile_not_found" , message : "execution profile not found" } ,
{ name : "profile load" , err : wrap ( promptkit . ErrProfileLoad , fmt . Errorf ( "read failed" ) ) , status : http . StatusBadRequest , code : "profile_load_failed" , message : "failed to load execution profile" , avoidCause : "read failed" } ,
{ name : "api key env missing" , err : wrap ( promptkit . ErrAPIKeyEnvMissing , promptkit . ErrInvalidRequest ) , status : http . StatusBadRequest , code : "api_key_env_missing" , message : "api_key_env is set but the environment variable is missing" } ,
{ name : "invalid request" , err : promptkit . ErrInvalidRequest , status : http . StatusBadRequest , code : "invalid_request" , message : "invalid run request" } ,
2026-07-28 00:52:24 +00:00
{ name : "file denied" , err : ErrFileNotAllowed , status : http . StatusBadRequest , code : "artifact_not_allowed" , message : "file input artifact is not allowed" } ,
{ name : "file outside root" , err : ErrFileOutsideRoot , status : http . StatusBadRequest , code : "artifact_not_allowed" , message : "file input artifact is not allowed" } ,
{ name : "file too large" , err : ErrFileTooLarge , status : http . StatusRequestEntityTooLarge , code : "artifact_too_large" , message : "file input artifact is too large" } ,
2026-07-28 14:04:29 +00:00
{ name : "artifact" , err : wrap ( promptkit . 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 ( promptkit . ErrPromptRender , fmt . Errorf ( "render failed" ) ) , status : http . StatusBadRequest , code : "prompt_render_failed" , message : "failed to render prompt" , avoidCause : "render failed" } ,
2026-08-29 14:26:04 +00:00
{ name : "capacity" , err : & promptkit . CapacityError { BackendID : "private-backend" } , status : http . StatusServiceUnavailable , code : "capacity_exceeded" , message : "model backend capacity is exhausted" , avoidCause : "private-backend" } ,
2026-07-28 14:04:29 +00:00
{ name : "llm" , err : wrap ( promptkit . ErrLLMGenerate , fmt . Errorf ( "llm failed" ) ) , status : http . StatusBadGateway , code : "llm_failed" , message : "model generation request failed" , avoidCause : "llm failed" } ,
2026-09-26 15:31:31 +00:00
{ name : "transport" , err : wrap ( promptkit . ErrTransport , fmt . Errorf ( "private transport detail" ) ) , status : http . StatusBadGateway , code : "llm_failed" , message : "model generation request failed" , avoidCause : "private transport detail" } ,
{ name : "malformed response" , err : wrap ( promptkit . ErrMalformedResponse , fmt . Errorf ( "private response detail" ) ) , status : http . StatusBadGateway , code : "llm_failed" , message : "model generation request failed" , avoidCause : "private response detail" } ,
{ name : "retry exhaustion" , err : wrap ( promptkit . ErrRetriesExhausted , fmt . Errorf ( "private provider detail" ) ) , status : http . StatusBadGateway , code : "llm_failed" , message : "model generation request failed" , avoidCause : "private provider detail" } ,
{ name : "retry exhaustion value" , err : fmt . Errorf ( "generation: %w" , & promptkit . RetryExhaustedError { } ) , status : http . StatusBadGateway , code : "llm_failed" , message : "model generation request failed" } ,
2026-07-28 14:04:29 +00:00
{ name : "validation runtime" , err : wrap ( promptkit . ErrValidation , fmt . Errorf ( "validator broke" ) ) , status : http . StatusInternalServerError , code : "validation_runtime_failed" , message : "validation runtime failed" , avoidCause : "validator broke" } ,
2026-05-04 21:19:29 -05:00
}
for _ , tc := range tests {
t . Run ( tc . name , func ( t * testing . T ) {
h := NewHandler ( & fakeRunner { err : tc . err } )
2026-05-05 10:09:31 -05:00
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` { "prompt_id":"p","inputs": { "x": { "type":"file","uri":"a"}}} ` ) )
2026-05-04 21:19:29 -05:00
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 ( ) )
}
2026-05-05 08:41:07 -05:00
var resp map [ string ] any
if err := json . Unmarshal ( w . Body . Bytes ( ) , & resp ) ; err != nil {
t . Fatalf ( "invalid JSON response: %v" , err )
}
errBody := resp [ "error" ] . ( map [ string ] any )
2026-05-05 10:54:35 -05:00
if errBody [ "code" ] != tc . code {
t . Fatalf ( "expected code %q, got %#v" , tc . code , errBody [ "code" ] )
2026-05-05 08:41:07 -05:00
}
2026-05-05 10:54:35 -05:00
if errBody [ "message" ] != tc . message {
t . Fatalf ( "expected message %q, got %#v" , tc . message , errBody [ "message" ] )
2026-05-05 08:41:07 -05:00
}
2026-05-05 10:54:35 -05:00
if tc . avoidCause != "" && strings . Contains ( w . Body . String ( ) , tc . avoidCause ) {
t . Fatalf ( "expected response to avoid leaking cause details, got %s" , w . Body . String ( ) )
2026-05-05 08:41:07 -05:00
}
2026-05-04 21:19:29 -05:00
} )
}
}
2026-05-05 10:54:35 -05:00
func TestHandlerRawAPIKeyRejectedByStrictJSON ( t * testing . T ) {
h := NewHandler ( & fakeRunner { } )
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` { "prompt_id":"p","inputs": { "x": { "type":"file","uri":"a"}},"model": { "model":"m","api_key":"secret"}} ` ) )
w := httptest . NewRecorder ( )
h . ServeHTTP ( w , req )
if w . Code != http . StatusBadRequest {
t . Fatalf ( "expected 400, got %d body=%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 )
}
errBody := resp [ "error" ] . ( map [ string ] any )
if errBody [ "code" ] != "invalid_json" {
t . Fatalf ( "expected invalid_json code, got %#v" , errBody [ "code" ] )
}
}
2026-05-05 20:01:45 -05:00
func TestHandlerValidationFailureStillSuccessAndRawOutputOptIn ( t * testing . T ) {
2026-07-28 14:04:29 +00:00
h := NewHandler ( & fakeRunner { result : & promptkit . RunResult {
Artifact : promptkit . Artifact { Body : [ ] byte ( "bad json" ) } ,
2026-05-04 21:19:29 -05:00
RawOutput : "bad json" ,
2026-07-28 14:04:29 +00:00
Validation : promptkit . ValidationResult {
Status : promptkit . ValidationFailed ,
Mode : promptkit . ValidationJSON ,
2026-05-04 21:19:29 -05:00
Errors : [ ] string { "invalid JSON" } ,
} ,
} } )
2026-05-05 10:09:31 -05:00
req := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` { "prompt_id":"p","inputs": { "x": { "type":"file","uri":"a"}}} ` ) )
2026-05-04 21:19:29 -05:00
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 )
2026-05-05 08:25:32 -05:00
if status , ok := validation [ "status" ] . ( string ) ; ! ok || status != "failed" {
t . Fatalf ( "expected validation status=failed, got %#v" , validation [ "status" ] )
2026-05-04 21:19:29 -05:00
}
2026-05-05 20:01:45 -05:00
if _ , ok := resp [ "raw_model_output" ] ; ok {
t . Fatalf ( "expected raw_model_output omitted by default, got %#v" , resp [ "raw_model_output" ] )
}
reqWithRaw := httptest . NewRequest ( http . MethodPost , "/v1/runs" , bytes . NewBufferString ( ` { "prompt_id":"p","inputs": { "x": { "type":"file","uri":"a"}},"include_raw_output":true} ` ) )
wWithRaw := httptest . NewRecorder ( )
h . ServeHTTP ( wWithRaw , reqWithRaw )
if wWithRaw . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d: %s" , wWithRaw . Code , wWithRaw . Body . String ( ) )
}
var respWithRaw map [ string ] any
if err := json . Unmarshal ( wWithRaw . Body . Bytes ( ) , & respWithRaw ) ; err != nil {
t . Fatalf ( "invalid JSON response: %v" , err )
}
if got , ok := respWithRaw [ "raw_model_output" ] . ( string ) ; ! ok || got != "bad json" {
t . Fatalf ( "expected raw_model_output to be included when requested, got %#v" , respWithRaw [ "raw_model_output" ] )
}
2026-05-04 21:19:29 -05:00
}
func wrap ( stage error , cause error ) error {
return fmt . Errorf ( "%w: %w" , stage , cause )
}
2026-07-04 23:34:44 +00:00
func newArtifactRootHandler ( t * testing . T , root string ) * Handler {
t . Helper ( )
2026-07-05 00:17:07 +00:00
return newArtifactRootHandlerWithLimit ( t , root , 0 )
}
func newArtifactRootHandlerWithLimit ( t * testing . T , root string , maxArtifactBytes int64 ) * Handler {
t . Helper ( )
2026-07-28 00:52:24 +00:00
reader , err := NewRestrictedArtifactReader ( root , maxArtifactBytes )
2026-07-04 23:34:44 +00:00
if err != nil {
t . Fatalf ( "expected restricted artifact reader: %v" , err )
}
2026-07-28 14:04:29 +00:00
return NewHandler ( newHandlerEngine ( t , promptkit . WithArtifactReader ( reader ) ) )
2026-07-28 00:52:24 +00:00
}
2026-07-28 14:04:29 +00:00
func newHandlerEngine ( t * testing . T , options ... promptkit . Option ) * promptkit . Engine {
2026-07-28 00:52:24 +00:00
t . Helper ( )
2026-07-28 14:04:29 +00:00
return newHandlerEngineWithOptions ( t , append ( options , promptkit . WithLLMClient ( handlerLLMClient { } ) ) ... )
2026-07-28 00:52:24 +00:00
}
2026-07-28 14:04:29 +00:00
func newHandlerEngineWithDefaultClient ( t * testing . T , options ... promptkit . Option ) * promptkit . Engine {
2026-07-28 00:52:24 +00:00
t . Helper ( )
return newHandlerEngineWithOptions ( t , options ... )
}
2026-07-28 14:04:29 +00:00
func newHandlerEngineWithOptions ( t * testing . T , options ... promptkit . Option ) * promptkit . Engine {
2026-07-28 00:52:24 +00:00
t . Helper ( )
promptDir := t . TempDir ( )
profileDir := t . TempDir ( )
if err := os . WriteFile ( filepath . Join ( promptDir , "prompt.yaml" ) , [ ] byte ( ` id : p
version : "1"
default_profile : exec
messages :
- role : user
content : "hi"
output :
format : text
validation_mode : none
repair_attempts : 0
` ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write prompt fixture: %v" , err )
}
if err := os . WriteFile ( filepath . Join ( profileDir , "profile.yaml" ) , [ ] byte ( ` id : exec
endpoint : http : //example.invalid/v1
model : model
` ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write profile fixture: %v" , err )
}
2026-07-28 14:04:29 +00:00
engine , err := promptkit . NewEngine ( promptkit . Config {
2026-07-28 00:52:24 +00:00
PromptDir : promptDir ,
ProfileDir : profileDir ,
} , options ... )
if err != nil {
t . Fatalf ( "construct public engine: %v" , err )
}
return engine
2026-07-04 23:34:44 +00:00
}
2026-08-29 14:12:51 +00:00
func newDefinitionHandler ( t * testing . T , definition string ) * Handler {
t . Helper ( )
promptDir := t . TempDir ( )
profileDir := t . TempDir ( )
if err := os . WriteFile ( filepath . Join ( promptDir , "prompt.yaml" ) , [ ] byte ( definition ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write prompt fixture: %v" , err )
}
if err := os . WriteFile ( filepath . Join ( profileDir , "profile.yaml" ) , [ ] byte ( ` id : exec
endpoint : http : //example.invalid/v1
model : model
` ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write profile fixture: %v" , err )
}
engine , err := promptkit . NewEngine ( promptkit . Config {
PromptDir : promptDir ,
ProfileDir : profileDir ,
} , promptkit . WithLLMClient ( handlerLLMClient { } ) )
if err != nil {
t . Fatalf ( "construct public engine: %v" , err )
}
return NewHandler ( engine )
}
2026-07-04 23:34:44 +00:00
func assertHTTPErrorCode ( t * testing . T , w * httptest . ResponseRecorder , status int , code string ) {
t . Helper ( )
if w . Code != status {
t . Fatalf ( "expected %d, got %d body=%s" , status , 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 )
}
errBody := resp [ "error" ] . ( map [ string ] any )
if errBody [ "code" ] != code {
t . Fatalf ( "expected code %q, got %#v" , code , errBody [ "code" ] )
}
}