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-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-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-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-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" } ,
{ 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" ] )
}
}