2026-07-05 18:21:37 +00:00
package llm
import (
2026-08-26 23:50:24 +00:00
"bytes"
2026-07-05 18:21:37 +00:00
"context"
"encoding/json"
"errors"
2026-08-25 20:04:56 +00:00
"fmt"
2026-07-28 16:31:36 +00:00
"io"
2026-08-03 16:17:40 +00:00
"io/fs"
2026-07-28 16:31:36 +00:00
"net/http"
2026-07-30 05:14:22 +00:00
"net/http/httptest"
2026-07-28 13:36:01 -05:00
"os"
"path/filepath"
2026-07-30 02:18:14 +00:00
"reflect"
2026-07-05 18:21:37 +00:00
"strings"
"sync"
"sync/atomic"
"testing"
"testing/fstest"
"time"
2026-07-30 02:18:14 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
2026-07-05 18:21:37 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
2026-07-28 16:31:36 +00:00
"gitea.maximumdirect.net/eric/promptkit"
2026-07-05 18:21:37 +00:00
)
2026-07-28 16:31:36 +00:00
func TestPromptKitClientMapsPromptRequestAndUnmarshalsOutput ( t * testing . T ) {
fake := & fakePromptKitLLM { content : ` { "ok":true} ` }
client := newTestPromptKitClient ( t , fake )
2026-07-05 18:21:37 +00:00
var out struct {
OK bool ` json:"ok" `
}
resp , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
StageName : "test-stage" ,
2026-07-30 01:58:40 +00:00
PromptID : "adapter.direct-session" ,
2026-07-05 18:21:37 +00:00
PromptVersion : "v1" ,
ProfileID : "explicit-profile" ,
2026-07-30 01:58:40 +00:00
SessionID : " session-123 " ,
2026-07-05 18:21:37 +00:00
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "sha256:source" , "file:///source.json" ) ,
} ,
Vars : map [ string ] any { "custom" : "value" } ,
} , & out )
if err != nil {
t . Fatalf ( "CompleteStructured() error = %v, want nil" , err )
}
if ! out . OK {
t . Fatalf ( "decoded output OK = false, want true" )
}
2026-07-28 16:47:07 +00:00
if resp . Provider != "promptkit" || resp . Model != "explicit-model" || resp . ProfileID != "explicit-profile" {
2026-07-05 18:21:37 +00:00
t . Fatalf ( "response metadata = %#v" , resp )
}
if resp . PromptTokens != 11 || resp . CompletionTokens != 7 || resp . TotalTokens != 18 {
t . Fatalf ( "usage = %#v, want mapped token counts" , resp )
}
2026-09-25 22:01:16 +00:00
if resp . Execution == nil || resp . Execution . Outcome != "success" || resp . Execution . Attempts != 1 ||
! resp . Execution . Usage . PromptTokens . Known || resp . Execution . Usage . PromptTokens . Value != 11 {
t . Fatalf ( "execution summary = %#v" , resp . Execution )
}
2026-07-08 08:37:26 -05:00
if resp . Debug == nil || resp . Debug . Prompt == nil {
t . Fatalf ( "debug prompt = nil, want prepared prompt material" )
}
2026-07-30 01:58:40 +00:00
if resp . Debug . Prompt . PromptID != "adapter.direct-session" ||
resp . Debug . Prompt . SelectedProfileID != "explicit-profile" ||
2026-07-30 02:18:14 +00:00
resp . Debug . Prompt . SelectedBackendID != "test-backend" ||
2026-07-30 01:58:40 +00:00
resp . Debug . Prompt . SessionID != "session-123" {
2026-07-30 02:18:14 +00:00
t . Fatalf ( "debug prompt metadata = %#v, want prompt/profile/backend/session" , resp . Debug . Prompt )
}
if resp . Debug . Prompt . EffectiveModelParams [ "backend_id" ] != "test-backend" ||
resp . Debug . Prompt . EffectiveModelParams [ "reasoning_effort" ] != "profile-reasoning" {
t . Fatalf ( "debug effective model params = %#v, want backend and reasoning" , resp . Debug . Prompt . EffectiveModelParams )
2026-07-08 08:37:26 -05:00
}
if len ( resp . Debug . Prompt . Messages ) != 1 || ! strings . Contains ( resp . Debug . Prompt . Messages [ 0 ] . Content , ` { "source":true} ` ) {
t . Fatalf ( "debug prompt messages = %#v, want rendered input content" , resp . Debug . Prompt . Messages )
}
if resp . Debug . Response == nil || resp . Debug . Response . Content != ` { "ok":true} ` {
t . Fatalf ( "debug response = %#v, want raw response content" , resp . Debug . Response )
}
2026-07-28 16:31:36 +00:00
if resp . Debug . Response . Usage . CachedTokens != 5 || resp . Debug . Response . Usage . CacheWriteTokens != 3 {
t . Fatalf ( "debug usage = %#v, want cached token counts" , resp . Debug . Response . Usage )
}
2026-07-30 02:18:14 +00:00
if resp . Debug . Response . EffectiveModelParams [ "backend_id" ] != "test-backend" ||
resp . Debug . Response . EffectiveModelParams [ "reasoning_effort" ] != "profile-reasoning" {
t . Fatalf ( "debug response effective model params = %#v, want backend and reasoning" , resp . Debug . Response . EffectiveModelParams )
}
2026-07-08 08:37:26 -05:00
debugJSON , err := json . Marshal ( resp . Debug )
if err != nil {
t . Fatalf ( "marshal debug material: %v" , err )
}
if strings . Contains ( string ( debugJSON ) , "secret-token" ) || strings . Contains ( string ( debugJSON ) , "sk-" ) {
t . Fatalf ( "debug material contains secret material: %s" , debugJSON )
}
2026-07-05 18:21:37 +00:00
gotReq := fake . lastRequest ( )
if gotReq . Prompt . SessionID != "session-123" {
t . Fatalf ( "session id = %q, want session-123" , gotReq . Prompt . SessionID )
}
if gotReq . Target . Model != "explicit-model" {
t . Fatalf ( "model = %q, want explicit-model" , gotReq . Target . Model )
}
2026-07-30 02:18:14 +00:00
if gotReq . Target . BackendID != "test-backend" {
t . Fatalf ( "backend id = %q, want test-backend" , gotReq . Target . BackendID )
}
2026-07-28 16:31:36 +00:00
if len ( gotReq . Prompt . Messages ) != 1 ||
! strings . Contains ( gotReq . Prompt . Messages [ 0 ] . Content , ` { "source":true} ` ) ||
! strings . Contains ( gotReq . Prompt . Messages [ 0 ] . Content , "value" ) {
t . Fatalf ( "rendered messages = %#v, want transcript input and variable content" , gotReq . Prompt . Messages )
2026-07-05 18:21:37 +00:00
}
if gotReq . StructuredOutput == nil {
t . Fatalf ( "structured output = nil, want JSON schema" )
}
manifests := client . LLMProfileManifests ( )
2026-07-28 16:31:36 +00:00
if len ( manifests ) != 1 ||
manifests [ 0 ] . ID != "explicit-profile" ||
2026-07-28 16:47:07 +00:00
manifests [ 0 ] . Provider != "promptkit" ||
2026-07-30 02:18:14 +00:00
manifests [ 0 ] . Model != "explicit-model" ||
manifests [ 0 ] . BackendID != "test-backend" ||
manifests [ 0 ] . ReasoningEffort != "profile-reasoning" {
2026-07-05 18:21:37 +00:00
t . Fatalf ( "profile manifests = %#v" , manifests )
}
}
2026-08-26 23:50:24 +00:00
func TestPromptKitClientAppendsSemanticCorrectionAfterRenderedPrompt ( t * testing . T ) {
fake := & fakePromptKitLLM { content : ` { "ok":true} ` }
client := newTestPromptKitClient ( t , fake )
request := contracts . StructuredCompletionRequest {
PromptID : "adapter.direct-session" ,
ProfileID : "explicit-profile" ,
SessionID : "correction-session" ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
Vars : map [ string ] any { "custom" : "value" } ,
}
var ordinary map [ string ] any
if _ , err := client . CompleteStructured ( context . Background ( ) , request , & ordinary ) ; err != nil {
t . Fatalf ( "ordinary CompleteStructured() error = %v" , err )
}
ordinaryMessages := append ( [ ] promptkit . RenderedMessage ( nil ) , fake . lastRequest ( ) . Prompt . Messages ... )
if len ( ordinaryMessages ) != 1 {
t . Fatalf ( "ordinary rendered messages = %#v, want only the declared prompt message" , ordinaryMessages )
}
correction , err := contracts . NewSemanticCorrection ( [ ] byte ( ` { "previous":"response"} ` ) , "Return the corrected JSON object." )
if err != nil {
t . Fatalf ( "NewSemanticCorrection() error = %v" , err )
}
request . Correction = correction
var corrected map [ string ] any
if _ , err := client . CompleteStructured ( context . Background ( ) , request , & corrected ) ; err != nil {
t . Fatalf ( "corrected CompleteStructured() error = %v" , err )
}
correctedMessages := fake . lastRequest ( ) . Prompt . Messages
if len ( correctedMessages ) != len ( ordinaryMessages ) + 2 {
t . Fatalf ( "corrected message count = %d, want %d" , len ( correctedMessages ) , len ( ordinaryMessages ) + 2 )
}
if ! reflect . DeepEqual ( correctedMessages [ : len ( ordinaryMessages ) ] , ordinaryMessages ) {
t . Fatalf ( "ordinary rendered prefix changed: got %#v, want %#v" , correctedMessages [ : len ( ordinaryMessages ) ] , ordinaryMessages )
}
if got , want := correctedMessages [ len ( ordinaryMessages ) ] , ( promptkit . RenderedMessage { Role : promptkit . RoleAssistant , Content : ` { "previous":"response"} ` } ) ; got != want {
t . Fatalf ( "assistant correction message = %#v, want %#v" , got , want )
}
if got , want := correctedMessages [ len ( ordinaryMessages ) + 1 ] , ( promptkit . RenderedMessage { Role : promptkit . RoleUser , Content : "Return the corrected JSON object." } ) ; got != want {
t . Fatalf ( "user correction message = %#v, want %#v" , got , want )
}
}
func TestPromptKitClientRejectsInvalidCorrectionsBeforePromptPreparation ( t * testing . T ) {
const sensitiveResponse = "assistant-response-must-not-appear-in-errors"
for _ , correction := range [ ] * contracts . SemanticCorrection {
{ AssistantResponse : [ ] byte ( sensitiveResponse ) , UserGuidance : " \t" } ,
{ AssistantResponse : bytes . Repeat ( [ ] byte ( sensitiveResponse ) , contracts . MaxAssistantResponseBytes / len ( sensitiveResponse ) + 1 ) , UserGuidance : "Use a smaller response." } ,
} {
fake := & fakePromptKitLLM { content : ` { "ok":true} ` }
client := newTestPromptKitClient ( t , fake )
var out map [ string ] any
_ , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest { Correction : correction } , & out )
if err == nil || ! strings . Contains ( err . Error ( ) , "correction" ) {
t . Fatalf ( "CompleteStructured() error = %v, want correction validation failure" , err )
}
if strings . Contains ( err . Error ( ) , sensitiveResponse ) {
t . Fatalf ( "correction validation error leaked response content: %v" , err )
}
if got := atomic . LoadInt32 ( & fake . calls ) ; got != 0 {
t . Fatalf ( "provider calls = %d, want no provider call after invalid correction" , got )
}
}
}
2026-08-03 16:17:40 +00:00
func TestPromptKitClientUsesOnePreparedSnapshotForDebugAndGeneration ( t * testing . T ) {
const initialPrompt = ` id : snapshot . test
version : "v1"
default_profile : snapshot - profile
inputs :
- name : transcript
required : true
content_type : application / json
messages :
- role : user
content : "Snapshot A: {{ input \"transcript\" }}"
output :
format : json
validation_mode : json_schema
schema_path : adapter . schema . json
repair_attempts : 0
`
const updatedPrompt = ` id : snapshot . test
version : "v1"
default_profile : snapshot - profile
inputs :
- name : transcript
required : true
content_type : application / json
messages :
- role : user
content : "Snapshot B: {{ input \"transcript\" }}"
output :
format : json
validation_mode : json_schema
schema_path : adapter . schema . json
repair_attempts : 0
`
source := & switchingPromptFS { files : fstest . MapFS {
"snapshot.test.yaml" : { Data : [ ] byte ( initialPrompt ) } ,
} }
fake := & fakePromptKitLLM { content : ` { "ok":true} ` }
client , err := NewPromptKitClient ( PromptKitClientConfig {
Assets : newTestPromptKitAssets ( t ) ,
EngineOptions : [ ] promptkit . Option {
promptkit . WithPromptFS ( source , "." ) ,
promptkit . WithBackend ( promptkit . Backend { ID : "snapshot-backend" , Endpoint : "http://promptkit.test/v1" } ) ,
promptkit . WithProfiles ( promptkit . OpenAICompatibleProfile ( promptkit . OpenAICompatibleProfileConfig {
ID : "snapshot-profile" ,
BackendID : "snapshot-backend" ,
Model : "snapshot-model" ,
} ) ) ,
promptkit . WithLLMClient ( fake ) ,
} ,
} )
if err != nil {
t . Fatalf ( "NewPromptKitClient() error = %v, want nil" , err )
}
opened := source . holdNextPromptRead ( )
defer source . resumePromptRead ( )
type completion struct {
response contracts . StructuredCompletionResponse
err error
}
completed := make ( chan completion , 1 )
go func ( ) {
var out struct {
OK bool ` json:"ok" `
}
response , callErr := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
PromptID : "snapshot.test" ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
} , & out )
completed <- completion { response : response , err : callErr }
} ( )
select {
case <- opened :
case <- time . After ( time . Second ) :
t . Fatal ( "PromptKit did not read the prompt source" )
}
source . replacePrompt ( [ ] byte ( updatedPrompt ) )
source . resumePromptRead ( )
result := <- completed
if result . err != nil {
t . Fatalf ( "CompleteStructured() error = %v, want nil" , result . err )
}
if result . response . Debug == nil || result . response . Debug . Prompt == nil || len ( result . response . Debug . Prompt . Messages ) != 1 {
t . Fatalf ( "debug prompt = %#v, want one prepared message" , result . response . Debug )
}
debugContent := result . response . Debug . Prompt . Messages [ 0 ] . Content
generatedContent := fake . lastRequest ( ) . Prompt . Messages [ 0 ] . Content
if debugContent != generatedContent {
t . Fatalf ( "debug content = %q, generation content = %q, want one snapshot" , debugContent , generatedContent )
}
if ! strings . Contains ( debugContent , "Snapshot A" ) || strings . Contains ( debugContent , "Snapshot B" ) {
t . Fatalf ( "snapshot content = %q, want the source read before it changed" , debugContent )
}
}
2026-07-30 01:58:40 +00:00
func TestPromptKitClientRetainsSessionPromptVariable ( t * testing . T ) {
fake := & fakePromptKitLLM { content : ` { "ok":true} ` }
client := newTestPromptKitClient ( t , fake )
var out map [ string ] any
_ , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
PromptID : "adapter.test" ,
SessionID : " canonical-session " ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
Vars : map [ string ] any {
"custom" : "value" ,
"session_id" : "caller-session" ,
} ,
} , & out )
if err != nil {
t . Fatalf ( "CompleteStructured() error = %v, want nil" , err )
}
gotReq := fake . lastRequest ( )
if gotReq . Prompt . SessionID != "canonical-session" {
t . Fatalf ( "session id = %q, want canonical-session" , gotReq . Prompt . SessionID )
}
if len ( gotReq . Prompt . Messages ) != 1 ||
! strings . Contains ( gotReq . Prompt . Messages [ 0 ] . Content , "Session: canonical-session" ) ||
strings . Contains ( gotReq . Prompt . Messages [ 0 ] . Content , "caller-session" ) {
t . Fatalf ( "rendered messages = %#v, want canonical session compatibility variable" , gotReq . Prompt . Messages )
}
}
func TestPromptKitClientDoesNotInventDirectSession ( t * testing . T ) {
fake := & fakePromptKitLLM { content : ` { "ok":true} ` }
client := newTestPromptKitClient ( t , fake )
var out map [ string ] any
resp , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
PromptID : "adapter.direct-session" ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
Vars : map [ string ] any { "custom" : "value" } ,
} , & out )
if err != nil {
t . Fatalf ( "CompleteStructured() error = %v, want nil" , err )
}
if got := fake . lastRequest ( ) . Prompt . SessionID ; got != "" {
t . Fatalf ( "session id = %q, want empty" , got )
}
if resp . Debug == nil || resp . Debug . Prompt == nil || resp . Debug . Prompt . SessionID != "" {
t . Fatalf ( "debug prompt = %#v, want no effective session" , resp . Debug )
}
}
2026-07-30 02:03:04 +00:00
func TestPromptKitClientAppliesReasoningEffortOverride ( t * testing . T ) {
tests := [ ] struct {
name string
override func ( ) * string
mutateAfterCreate bool
want string
} {
{
name : "inherit" ,
want : "profile-reasoning" ,
} ,
{
name : "replace" ,
override : func ( ) * string { value := "focused" ; return & value } ,
want : "focused" ,
} ,
{
name : "clear" ,
override : func ( ) * string { value := "" ; return & value } ,
want : "" ,
} ,
{
name : "defensive copy" ,
override : func ( ) * string { value := "original" ; return & value } ,
mutateAfterCreate : true ,
want : "original" ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
fake := & fakePromptKitLLM { content : ` { "ok":true} ` }
var override * string
if tt . override != nil {
override = tt . override ( )
}
client := newTestPromptKitClientWithReasoning ( t , fake , override )
if tt . mutateAfterCreate {
* override = "mutated"
}
var out map [ string ] any
_ , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
PromptID : "adapter.direct-session" ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
Vars : map [ string ] any { "custom" : "value" } ,
} , & out )
if err != nil {
t . Fatalf ( "CompleteStructured() error = %v, want nil" , err )
}
if got := fake . lastRequest ( ) . Target . ReasoningEffort ; got != tt . want {
t . Fatalf ( "reasoning effort = %q, want %q" , got , tt . want )
}
} )
}
}
2026-07-28 16:31:36 +00:00
func TestNewPromptKitClientReportsAssetAndEngineConstructionFailures ( t * testing . T ) {
t . Run ( "assets" , func ( t * testing . T ) {
registry := NewAssetRegistry ( )
for _ , root := range [ ] string { "one" , "two" } {
if err := registry . RegisterPromptFS ( fstest . MapFS {
root + "/prompt.yaml" : { Data : [ ] byte ( "id: duplicate" ) } ,
} , root ) ; err != nil {
t . Fatalf ( "RegisterPromptFS() error = %v" , err )
}
}
if _ , err := NewPromptKitClient ( PromptKitClientConfig { Assets : registry } ) ; err == nil ||
! strings . Contains ( err . Error ( ) , "duplicate asset path" ) {
t . Fatalf ( "NewPromptKitClient() error = %v, want asset construction failure" , err )
}
} )
t . Run ( "engine" , func ( t * testing . T ) {
if _ , err := NewPromptKitClient ( PromptKitClientConfig {
Assets : newTestPromptKitAssets ( t ) ,
EngineOptions : [ ] promptkit . Option { promptkit . WithProfileFile ( "" ) } ,
} ) ; err == nil || ! strings . Contains ( err . Error ( ) , "create PromptKit engine" ) {
t . Fatalf ( "NewPromptKitClient() error = %v, want engine construction failure" , err )
}
} )
}
2026-07-28 13:36:01 -05:00
func TestPromptKitClientCheckpointFingerprintTracksProfileSource ( t * testing . T ) {
profilePath := filepath . Join ( t . TempDir ( ) , "profiles.yml" )
2026-08-03 16:12:29 +00:00
const credentialEnvironment = "PROMPTKIT_TEST_API_KEY"
2026-07-28 13:36:01 -05:00
writeProfile := func ( model string ) {
t . Helper ( )
2026-08-03 16:12:29 +00:00
content := "id: checkpoint-profile\nendpoint: http://promptkit.test/v1\nmodel: " + model + "\napi_key_env: " + credentialEnvironment + "\n"
2026-07-28 13:36:01 -05:00
if err := os . WriteFile ( profilePath , [ ] byte ( content ) , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
}
fingerprintFor := func ( ) CheckpointFingerprint {
t . Helper ( )
client , err := NewPromptKitClient ( PromptKitClientConfig {
Assets : newTestPromptKitAssets ( t ) ,
ProfileFile : profilePath ,
} )
if err != nil {
t . Fatal ( err )
}
values , err := client . LLMCheckpointFingerprints ( )
if err != nil {
t . Fatal ( err )
}
if len ( values ) != 1 || values [ 0 ] . Name != promptKitProfileFingerprintName {
t . Fatalf ( "checkpoint fingerprints = %#v, want one profile-source identity" , values )
}
return values [ 0 ]
}
writeProfile ( "model-one" )
first := fingerprintFor ( )
2026-08-03 16:12:29 +00:00
repeated := fingerprintFor ( )
if first != repeated {
t . Fatalf ( "profile-source fingerprint = %#v then %#v for unchanged source" , first , repeated )
}
2026-07-28 13:36:01 -05:00
writeProfile ( "model-two" )
second := fingerprintFor ( )
if first == second {
t . Fatalf ( "profile-source fingerprint = %#v for both profile models" , first )
}
2026-08-03 16:12:29 +00:00
if strings . TrimSpace ( first . Value ) == "" {
t . Fatal ( "profile-source fingerprint is empty" )
}
if strings . Contains ( first . Value , profilePath ) ||
strings . Contains ( first . Value , "model-one" ) ||
strings . Contains ( first . Value , credentialEnvironment ) {
2026-07-28 13:36:01 -05:00
t . Fatalf ( "profile-source fingerprint exposes source details: %#v" , first )
}
client , err := NewPromptKitClient ( PromptKitClientConfig { Assets : newTestPromptKitAssets ( t ) } )
if err != nil {
t . Fatal ( err )
}
copy , err := client . LLMCheckpointFingerprints ( )
if err != nil {
t . Fatal ( err )
}
copy [ 0 ] . Value = "mutated"
fresh , err := client . LLMCheckpointFingerprints ( )
if err != nil {
t . Fatal ( err )
}
if fresh [ 0 ] . Value == "mutated" {
t . Fatal ( "LLMCheckpointFingerprints exposed mutable backing storage" )
}
2026-08-03 16:12:29 +00:00
secondClient , err := NewPromptKitClient ( PromptKitClientConfig { Assets : newTestPromptKitAssets ( t ) } )
if err != nil {
t . Fatal ( err )
}
secondBuiltin , err := secondClient . LLMCheckpointFingerprints ( )
if err != nil {
t . Fatal ( err )
}
if len ( secondBuiltin ) != 1 || fresh [ 0 ] != secondBuiltin [ 0 ] {
t . Fatalf ( "built-in profile fingerprints = %#v and %#v, want deterministic identity" , fresh , secondBuiltin )
}
if strings . TrimSpace ( fresh [ 0 ] . Value ) == "" {
t . Fatal ( "built-in profile fingerprint is empty" )
2026-07-30 01:54:47 +00:00
}
2026-08-03 18:35:40 +00:00
t . Run ( "directory layout" , func ( t * testing . T ) {
profileDir := t . TempDir ( )
firstPath := filepath . Join ( profileDir , "first-profile.yaml" )
secondPath := filepath . Join ( profileDir , "second-profile.yaml" )
content := [ ] byte ( "id: directory-profile\nendpoint: http://promptkit.test/v1\nmodel: directory-model\n" )
if err := os . WriteFile ( firstPath , content , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
first , err := promptKitProfileFingerprint ( profileDir , "" , "" )
if err != nil {
t . Fatal ( err )
}
if err := os . Rename ( firstPath , secondPath ) ; err != nil {
t . Fatal ( err )
}
second , err := promptKitProfileFingerprint ( profileDir , "" , "" )
if err != nil {
t . Fatal ( err )
}
if first == second {
t . Fatalf ( "profile-source fingerprint = %#v after source filename changed" , first )
}
if strings . Contains ( first . Value , firstPath ) || strings . Contains ( second . Value , secondPath ) {
t . Fatalf ( "profile-source fingerprint exposes source path: %#v, %#v" , first , second )
}
} )
2026-08-25 19:31:00 +00:00
t . Run ( "inherited parent" , func ( t * testing . T ) {
profileDir := t . TempDir ( )
parentPath := filepath . Join ( profileDir , "parent.yaml" )
leafPath := filepath . Join ( profileDir , "leaf.yaml" )
if err := os . WriteFile ( parentPath , [ ] byte ( "id: parent\nendpoint: http://promptkit.test/v1\nmodel: parent-one\n" ) , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
if err := os . WriteFile ( leafPath , [ ] byte ( "id: leaf\nbase_profile: parent\nmodel: leaf-model\n" ) , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
first , err := promptKitProfileFingerprint ( profileDir , "" , "" )
if err != nil {
t . Fatal ( err )
}
if err := os . WriteFile ( parentPath , [ ] byte ( "id: parent\nendpoint: http://promptkit.test/v1\nmodel: parent-two\n" ) , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
second , err := promptKitProfileFingerprint ( profileDir , "" , "" )
if err != nil {
t . Fatal ( err )
}
if first == second || strings . Contains ( first . Value , "parent-one" ) || strings . Contains ( first . Value , parentPath ) || strings . Contains ( first . Value , leafPath ) {
t . Fatalf ( "inherited profile fingerprint = %#v then %#v" , first , second )
}
} )
2026-07-28 13:36:01 -05:00
}
2026-08-03 17:25:44 +00:00
func TestPromptKitProfileFingerprintReadErrorsDoNotExposeSourcePaths ( t * testing . T ) {
for _ , source := range [ ] struct {
name string
profileDir string
profileFile string
} {
{
name : "file" ,
profileFile : filepath . Join ( t . TempDir ( ) , "missing-profile.yaml" ) ,
} ,
{
name : "directory" ,
profileDir : filepath . Join ( t . TempDir ( ) , "missing-profiles" ) ,
} ,
} {
t . Run ( source . name , func ( t * testing . T ) {
_ , err := promptKitProfileFingerprint ( source . profileDir , source . profileFile , "" )
if err == nil {
t . Fatal ( "promptKitProfileFingerprint() error = nil, want source read failure" )
}
if ( source . profileDir != "" && strings . Contains ( err . Error ( ) , source . profileDir ) ) ||
( source . profileFile != "" && strings . Contains ( err . Error ( ) , source . profileFile ) ) {
t . Fatalf ( "fingerprint error exposes profile source path: %q" , err )
}
} )
}
}
2026-08-03 16:34:48 +00:00
func TestPromptKitClientUsesFallbackProfilesForExecutionAndInspection ( t * testing . T ) {
assets := newTestPromptKitAssets ( t )
const profileID = "fallback-profile"
if err := assets . RegisterFallbackProfileFS ( fstest . MapFS {
"profiles/fallback.yaml" : { Data : [ ] byte ( "id: " + profileID + "\nendpoint: http://promptkit.test/v1\nmodel: fallback-model\n" ) } ,
} , "profiles" ) ; err != nil {
t . Fatalf ( "RegisterFallbackProfileFS() error = %v, want nil" , err )
}
fake := & fakePromptKitLLM { content : ` { "ok":true} ` }
client , err := NewPromptKitClient ( PromptKitClientConfig {
Assets : assets ,
EngineOptions : [ ] promptkit . Option { promptkit . WithLLMClient ( fake ) } ,
} )
if err != nil {
t . Fatalf ( "NewPromptKitClient() error = %v, want nil" , err )
}
var out map [ string ] any
response , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
PromptID : "adapter.test" ,
ProfileID : profileID ,
SessionID : "fallback-profile-test" ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
} , & out )
if err != nil {
t . Fatalf ( "CompleteStructured() error = %v, want nil" , err )
}
if response . ProfileID != profileID || response . Model != "fallback-model" {
t . Fatalf ( "completion response = %#v, want fallback profile" , response )
}
inspector , err := NewPromptKitProfileInspector ( PromptKitProfileInspectorConfig { Assets : assets } )
if err != nil {
t . Fatalf ( "NewPromptKitProfileInspector() error = %v, want nil" , err )
}
inspection , err := inspector . InspectProfile ( context . Background ( ) , profileID )
if err != nil {
t . Fatalf ( "InspectProfile() error = %v, want nil" , err )
}
if inspection . ProfileID != profileID || inspection . Model != "fallback-model" {
t . Fatalf ( "profile inspection = %#v, want fallback profile" , inspection )
}
}
2026-08-25 19:31:00 +00:00
func TestPromptKitClientUsesInheritedFilesystemProfileForInspectionAndExecution ( t * testing . T ) {
profileDir := t . TempDir ( )
if err := os . WriteFile ( filepath . Join ( profileDir , "base.yaml" ) , [ ] byte ( "id: base\nendpoint: http://promptkit.test/v1\nmodel: base-model\nreasoning_effort: medium\n" ) , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
if err := os . WriteFile ( filepath . Join ( profileDir , "leaf.yaml" ) , [ ] byte ( "id: inherited-profile\nbase_profile: base\nmodel: leaf-model\n" ) , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
fake := & fakePromptKitLLM { content : ` { "ok":true} ` }
client , err := NewPromptKitClient ( PromptKitClientConfig { Assets : newTestPromptKitAssets ( t ) , ProfileDir : profileDir , EngineOptions : [ ] promptkit . Option { promptkit . WithLLMClient ( fake ) } } )
if err != nil {
t . Fatal ( err )
}
inspector , err := NewPromptKitProfileInspector ( PromptKitProfileInspectorConfig { Source : PromptKitProfileSourceConfig { ProfileDir : profileDir } } )
if err != nil {
t . Fatal ( err )
}
inspection , err := inspector . InspectProfile ( context . Background ( ) , "inherited-profile" )
if err != nil {
t . Fatal ( err )
}
if inspection . ProfileID != "inherited-profile" || inspection . Model != "leaf-model" {
t . Fatalf ( "inspection = %#v, want resolved leaf target" , inspection )
}
var out map [ string ] any
response , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest { PromptID : "adapter.test" , ProfileID : "inherited-profile" , SessionID : "inheritance-test" , Inputs : contracts . LLMInputSet { "transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) } } , & out )
if err != nil {
t . Fatal ( err )
}
if response . ProfileID != inspection . ProfileID || response . Model != inspection . Model || fake . lastRequest ( ) . Target . ReasoningEffort != "medium" {
t . Fatalf ( "response=%#v target=%#v inspection=%#v" , response , fake . lastRequest ( ) . Target , inspection )
}
}
func TestPromptKitProfileInspectorExposesRakestrawhomeBuiltIn ( t * testing . T ) {
inspector , err := NewPromptKitProfileInspector ( PromptKitProfileInspectorConfig { } )
if err != nil {
t . Fatal ( err )
}
inspection , err := inspector . InspectProfile ( context . Background ( ) , "rakestrawhome-gemma-4-31b" )
if err != nil {
t . Fatal ( err )
}
if inspection . BackendID != promptkit . BackendRakestrawHome || inspection . Model != "google/gemma-4-31b-it" {
t . Fatalf ( "inspection = %#v" , inspection )
}
}
func TestPromptKitClientAllowsMissingOptionalFilesystemCredential ( t * testing . T ) {
server := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
if got := r . Header . Get ( "Authorization" ) ; got != "" {
t . Fatalf ( "Authorization = %q, want omitted" , got )
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ , _ = w . Write ( [ ] byte ( ` { "choices":[ { "message": { "content":" { \"ok\":true}"}}]} ` ) )
} ) )
defer server . Close ( )
t . Setenv ( "NOTARIUS_OPTIONAL_PROFILE_KEY" , "" )
profilePath := filepath . Join ( t . TempDir ( ) , "optional.yaml" )
if err := os . WriteFile ( profilePath , [ ] byte ( "id: optional-profile\nendpoint: " + server . URL + "/v1\nmodel: optional-model\napi_key_env: NOTARIUS_OPTIONAL_PROFILE_KEY\n" ) , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
client , err := NewPromptKitClient ( PromptKitClientConfig { Assets : newTestPromptKitAssets ( t ) , ProfileFile : profilePath } )
if err != nil {
t . Fatal ( err )
}
var out map [ string ] any
if _ , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest { PromptID : "adapter.test" , ProfileID : "optional-profile" , SessionID : "optional-credential-test" , Inputs : contracts . LLMInputSet { "transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) } } , & out ) ; err != nil {
t . Fatal ( err )
}
}
2026-08-25 19:34:27 +00:00
func TestPromptKitClientMapsHTTPGenerationErrorToApplicationBoundary ( t * testing . T ) {
const credential = "selected-test-credential"
server := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
w . WriteHeader ( http . StatusServiceUnavailable )
_ , _ = w . Write ( [ ] byte ( ` { "error": { "code":"temporary","type":"provider_error","message":"marker ` + credential + ` Bearer bearer-secret"}} ` ) )
} ) )
defer server . Close ( )
t . Setenv ( "NOTARIUS_GENERATION_TEST_KEY" , credential )
profilePath := filepath . Join ( t . TempDir ( ) , "profile.yaml" )
if err := os . WriteFile ( profilePath , [ ] byte ( "id: generation-profile\nendpoint: " + server . URL + "/v1\nmodel: test\napi_key_env: NOTARIUS_GENERATION_TEST_KEY\n" ) , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
client , err := NewPromptKitClient ( PromptKitClientConfig { Assets : newTestPromptKitAssets ( t ) , ProfileFile : profilePath } )
if err != nil {
t . Fatal ( err )
}
var out map [ string ] any
response , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest { PromptID : "adapter.test" , ProfileID : "generation-profile" , SessionID : "generation-error-test" , Inputs : contracts . LLMInputSet { "transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) } } , & out )
if ! errors . Is ( err , contracts . ErrLLMGeneration ) {
t . Fatalf ( "error = %v" , err )
}
var generation * contracts . LLMGenerationError
if ! errors . As ( err , & generation ) || generation . StatusCode ( ) != http . StatusServiceUnavailable {
t . Fatalf ( "generation error = %#v" , generation )
}
if strings . Contains ( err . Error ( ) , credential ) || strings . Contains ( err . Error ( ) , "marker" ) {
t . Fatalf ( "ordinary error leaked provider detail: %v" , err )
}
if response . Debug == nil || response . Debug . Response == nil || response . Debug . Response . ProviderError == nil {
t . Fatalf ( "debug = %#v" , response . Debug )
}
debug := response . Debug . Response . ProviderError
if debug . StatusCode != http . StatusServiceUnavailable || debug . Code != "temporary" || strings . Contains ( debug . Message , credential ) || strings . Contains ( debug . Message , "bearer-secret" ) || ! strings . Contains ( debug . Message , "marker" ) {
t . Fatalf ( "debug provider error = %#v" , debug )
}
}
2026-08-25 19:35:27 +00:00
func TestPromptKitClientRejectsInvalidRepairAttemptOverride ( t * testing . T ) {
for _ , attempts := range [ ] int { - 1 , 4 } {
t . Run ( "invalid" , func ( t * testing . T ) {
fake := & fakePromptKitLLM { content : ` { "ok":true} ` }
client := newTestPromptKitClient ( t , fake )
var out map [ string ] any
_ , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest { PromptID : "adapter.test" , StructuredOutputRepairAttempts : & attempts } , & out )
if err == nil || fake . calls != 0 {
t . Fatalf ( "error=%v calls=%d" , err , fake . calls )
}
} )
}
}
2026-08-03 16:34:48 +00:00
func TestPromptKitClientCheckpointFingerprintTracksFallbackProfileAssets ( t * testing . T ) {
fingerprintFor := func ( content string ) CheckpointFingerprint {
t . Helper ( )
assets := newTestPromptKitAssets ( t )
if err := assets . RegisterFallbackProfileFS ( fstest . MapFS {
"profiles/fallback.yaml" : { Data : [ ] byte ( content ) } ,
} , "profiles" ) ; err != nil {
t . Fatal ( err )
}
client , err := NewPromptKitClient ( PromptKitClientConfig { Assets : assets } )
if err != nil {
t . Fatal ( err )
}
fingerprints , err := client . LLMCheckpointFingerprints ( )
if err != nil {
t . Fatal ( err )
}
if len ( fingerprints ) != 1 || fingerprints [ 0 ] . Name != promptKitProfileFingerprintName {
t . Fatalf ( "checkpoint fingerprints = %#v, want profile source identity" , fingerprints )
}
return fingerprints [ 0 ]
}
first := fingerprintFor ( "id: fallback\nendpoint: http://promptkit.test/v1\nmodel: model-one\n" )
second := fingerprintFor ( "id: fallback\nendpoint: http://promptkit.test/v1\nmodel: model-two\n" )
if first == second {
t . Fatalf ( "checkpoint fingerprints = %#v and %#v, want fallback asset change" , first , second )
}
if strings . Contains ( first . Value , "model-one" ) || strings . Contains ( first . Value , "fallback.yaml" ) {
t . Fatalf ( "checkpoint fingerprint leaked fallback source details: %#v" , first )
}
}
2026-07-30 05:17:39 +00:00
func TestPromptKitClientCheckpointFingerprintTracksLocalBackendTarget ( t * testing . T ) {
const (
firstEndpoint = "http://localhost:8000/v1"
secondEndpoint = "https://inference.example.test/v1"
)
fingerprintsFor := func ( endpoint string , concurrencyLimit int ) [ ] CheckpointFingerprint {
t . Helper ( )
client , err := NewPromptKitClient ( PromptKitClientConfig {
Assets : newTestPromptKitAssets ( t ) ,
LocalBackend : & PromptKitLocalBackendConfig {
Endpoint : endpoint ,
ConcurrencyLimit : concurrencyLimit ,
} ,
} )
if err != nil {
t . Fatal ( err )
}
values , err := client . LLMCheckpointFingerprints ( )
if err != nil {
t . Fatal ( err )
}
return values
}
baseline := fingerprintsFor ( firstEndpoint , 0 )
if len ( baseline ) != 2 ||
baseline [ 0 ] . Name != promptKitProfileFingerprintName ||
baseline [ 1 ] . Name != promptKitLocalBackendFingerprintName {
t . Fatalf ( "checkpoint fingerprints = %#v, want profile source then local backend target" , baseline )
}
endpointChanged := fingerprintsFor ( secondEndpoint , 0 )
if baseline [ 0 ] != endpointChanged [ 0 ] || baseline [ 1 ] == endpointChanged [ 1 ] {
t . Fatalf ( "endpoint fingerprints = %#v and %#v, want only local target to change" , baseline , endpointChanged )
}
concurrencyChanged := fingerprintsFor ( firstEndpoint , 4 )
if ! reflect . DeepEqual ( baseline , concurrencyChanged ) {
t . Fatalf ( "concurrency fingerprints = %#v, want %#v" , concurrencyChanged , baseline )
}
for _ , values := range [ ] [ ] CheckpointFingerprint { baseline , endpointChanged } {
for _ , fingerprint := range values {
if strings . Contains ( fingerprint . Value , firstEndpoint ) ||
strings . Contains ( fingerprint . Value , secondEndpoint ) {
t . Fatalf ( "checkpoint fingerprint exposes endpoint: %#v" , fingerprint )
}
}
}
localBackend := & PromptKitLocalBackendConfig {
Endpoint : " " + firstEndpoint + " " ,
ConcurrencyLimit : 0 ,
}
client , err := NewPromptKitClient ( PromptKitClientConfig {
Assets : newTestPromptKitAssets ( t ) ,
LocalBackend : localBackend ,
} )
if err != nil {
t . Fatal ( err )
}
localBackend . Endpoint = secondEndpoint
copy , err := client . LLMCheckpointFingerprints ( )
if err != nil {
t . Fatal ( err )
}
if ! reflect . DeepEqual ( copy , baseline ) {
t . Fatalf ( "fingerprints after input mutation = %#v, want retained target %#v" , copy , baseline )
}
copy [ 0 ] . Value = "mutated-profile"
copy [ 1 ] . Value = "mutated-target"
fresh , err := client . LLMCheckpointFingerprints ( )
if err != nil {
t . Fatal ( err )
}
if ! reflect . DeepEqual ( fresh , baseline ) {
t . Fatalf ( "fingerprints after returned-slice mutation = %#v, want %#v" , fresh , baseline )
}
}
2026-07-28 16:31:36 +00:00
func TestPromptKitClientUsesPromptDefaultProfileWhenRequestProfileEmpty ( t * testing . T ) {
fake := & fakePromptKitLLM { content : ` { "ok":true} ` }
client := newTestPromptKitClient ( t , fake )
2026-07-05 18:21:37 +00:00
var out map [ string ] any
if _ , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
PromptID : "adapter.test" ,
SessionID : "session-123" ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
} , & out ) ; err != nil {
t . Fatalf ( "CompleteStructured() error = %v, want nil" , err )
}
if got := fake . lastRequest ( ) . Target . Model ; got != "default-model" {
t . Fatalf ( "model = %q, want prompt default profile model" , got )
}
2026-07-30 02:18:14 +00:00
manifests := client . LLMProfileManifests ( )
if len ( manifests ) != 1 || manifests [ 0 ] . BackendID != "" || manifests [ 0 ] . ReasoningEffort != "profile-reasoning" {
t . Fatalf ( "endpoint-only profile manifests = %#v, want omitted backend and effective reasoning" , manifests )
}
}
2026-07-30 05:14:22 +00:00
func TestPromptKitClientUsesConfiguredLocalBackend ( t * testing . T ) {
var providerCalls atomic . Int32
server := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
providerCalls . Add ( 1 )
if r . URL . Path != "/v1/chat/completions" {
t . Errorf ( "provider path = %q, want /v1/chat/completions" , r . URL . Path )
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ , _ = w . Write ( [ ] byte ( ` {
"choices" : [ { "message" : { "role" : "assistant" , "content" : "{\"ok\":true}" } } ] ,
"usage" : { "prompt_tokens" : 3 , "completion_tokens" : 4 , "total_tokens" : 7 }
} ` ) )
} ) )
defer server . Close ( )
profilePath := filepath . Join ( t . TempDir ( ) , "profiles.yml" )
if err := os . WriteFile ( profilePath , [ ] byte ( ` id : local - profile
backend : local
model : local - model
` ) , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
localBackend := & PromptKitLocalBackendConfig {
Endpoint : " " + server . URL + "/v1 " ,
ConcurrencyLimit : 2 ,
}
client , err := NewPromptKitClient ( PromptKitClientConfig {
Assets : newTestPromptKitAssets ( t ) ,
ProfileFile : profilePath ,
LocalBackend : localBackend ,
} )
if err != nil {
t . Fatalf ( "NewPromptKitClient() error = %v, want nil" , err )
}
localBackend . Endpoint = "http://127.0.0.1:1/v1"
request := contracts . StructuredCompletionRequest {
PromptID : "adapter.direct-session" ,
ProfileID : "local-profile" ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
Vars : map [ string ] any { "custom" : "value" } ,
}
var out map [ string ] any
response , err := client . CompleteStructured ( context . Background ( ) , request , & out )
if err != nil {
t . Fatalf ( "CompleteStructured() error = %v, want nil" , err )
}
if providerCalls . Load ( ) != 1 {
t . Fatalf ( "provider calls = %d, want 1" , providerCalls . Load ( ) )
}
if response . ProfileID != "local-profile" || response . Model != "local-model" {
t . Fatalf ( "response metadata = %#v" , response )
}
if response . Debug == nil || response . Debug . Prompt == nil ||
response . Debug . Prompt . SelectedBackendID != promptkit . BackendLocal {
t . Fatalf ( "response debug prompt = %#v, want local backend" , response . Debug )
}
manifests := client . LLMProfileManifests ( )
if len ( manifests ) != 1 || manifests [ 0 ] . BackendID != promptkit . BackendLocal {
t . Fatalf ( "profile manifests = %#v, want local backend" , manifests )
}
clientWithoutRegistration , err := NewPromptKitClient ( PromptKitClientConfig {
Assets : newTestPromptKitAssets ( t ) ,
ProfileFile : profilePath ,
} )
if err != nil {
t . Fatalf ( "NewPromptKitClient() without local registration error = %v, want nil" , err )
}
_ , err = clientWithoutRegistration . CompleteStructured ( context . Background ( ) , request , & out )
if err == nil ||
! strings . Contains ( err . Error ( ) , "prepare PromptKit prompt" ) ||
! strings . Contains ( err . Error ( ) , promptkit . BackendLocal ) {
t . Fatalf ( "CompleteStructured() without registration error = %v, want preparation failure with local backend context" , err )
}
2026-08-09 00:44:38 +00:00
for _ , sentinel := range [ ] error { promptkit . ErrProfileLoad , promptkit . ErrInvalidRequest , promptkit . ErrPromptNotFound } {
if errors . Is ( err , sentinel ) {
t . Fatalf ( "preparation failure exposes PromptKit sentinel %v: %v" , sentinel , err )
}
}
2026-07-30 05:14:22 +00:00
if providerCalls . Load ( ) != 1 {
t . Fatalf ( "provider calls after missing-registration failure = %d, want 1" , providerCalls . Load ( ) )
}
}
2026-09-25 21:56:29 +00:00
func TestPromptKitClientAppliesConfiguredBuiltInTransientRetries ( t * testing . T ) {
for _ , test := range [ ] struct {
name string
retries int
statuses [ ] int
wantCalls int32
wantErr bool
} {
{ name : "eligible failure then success" , retries : 1 , statuses : [ ] int { http . StatusServiceUnavailable , http . StatusOK } , wantCalls : 2 } ,
{ name : "disabled" , retries : 0 , statuses : [ ] int { http . StatusServiceUnavailable } , wantCalls : 1 , wantErr : true } ,
{ name : "exhausted" , retries : 1 , statuses : [ ] int { http . StatusServiceUnavailable , http . StatusServiceUnavailable } , wantCalls : 2 , wantErr : true } ,
{ name : "noneligible client error" , retries : 2 , statuses : [ ] int { http . StatusBadRequest } , wantCalls : 1 , wantErr : true } ,
} {
t . Run ( test . name , func ( t * testing . T ) {
var calls atomic . Int32
server := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , _ * http . Request ) {
index := calls . Add ( 1 ) - 1
if int ( index ) >= len ( test . statuses ) {
t . Errorf ( "unexpected HTTP attempt %d" , index + 1 )
w . WriteHeader ( http . StatusInternalServerError )
return
}
status := test . statuses [ index ]
w . Header ( ) . Set ( "Content-Type" , "application/json" )
if status != http . StatusOK {
w . WriteHeader ( status )
_ , _ = w . Write ( [ ] byte ( ` { "error": { "message":"provider failure"}} ` ) )
return
}
_ , _ = w . Write ( [ ] byte ( ` { "choices":[ { "message": { "content":" { \"ok\":true}"}}]} ` ) )
} ) )
defer server . Close ( )
profilePath := filepath . Join ( t . TempDir ( ) , "profile.yml" )
if err := os . WriteFile ( profilePath , [ ] byte ( "id: retry-profile\nendpoint: " + server . URL + "/v1\nmodel: retry-model\n" ) , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
client , err := NewPromptKitClient ( PromptKitClientConfig {
Assets : newTestPromptKitAssets ( t ) ,
ProfileFile : profilePath ,
TransientRetries : & test . retries ,
} )
if err != nil {
t . Fatal ( err )
}
var out map [ string ] any
_ , err = client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
PromptID : "adapter.test" ,
ProfileID : "retry-profile" ,
SessionID : "retry-test" ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
} , & out )
if ( err != nil ) != test . wantErr {
t . Fatalf ( "CompleteStructured() error = %v, want error=%t" , err , test . wantErr )
}
2026-09-25 21:59:15 +00:00
if test . wantErr {
var generation * contracts . LLMGenerationError
if ! errors . As ( err , & generation ) || generation . FailureKind ( ) != contracts . LLMGenerationFailureProviderStatus {
t . Fatalf ( "generation error = %#v, want provider-status classification" , generation )
}
wantExhausted := test . name == "exhausted"
if generation . RetriesExhausted ( ) != wantExhausted {
t . Fatalf ( "retries exhausted = %t, want %t" , generation . RetriesExhausted ( ) , wantExhausted )
}
if wantExhausted && ( generation . Attempts ( ) != 2 || generation . StatusCode ( ) != http . StatusServiceUnavailable ) {
t . Fatalf ( "exhausted generation = %#v" , generation )
}
}
2026-09-25 21:56:29 +00:00
if got := calls . Load ( ) ; got != test . wantCalls {
t . Fatalf ( "HTTP calls = %d, want %d" , got , test . wantCalls )
}
} )
}
}
2026-07-30 02:18:14 +00:00
func TestLLMProfileRecorderDistinguishesEffectiveTargets ( t * testing . T ) {
recorder := NewLLMProfileRecorder ( )
for _ , profile := range [ ] artifacts . LLMProfileManifest {
{ ID : "profile" , Provider : "promptkit" , Model : "model" , BackendID : "backend-b" , ReasoningEffort : "low" } ,
{ ID : " profile " , Provider : " promptkit " , Model : " model " , BackendID : " backend-a " , ReasoningEffort : " low " } ,
{ ID : "profile" , Provider : "promptkit" , Model : "model" , BackendID : "backend-a" , ReasoningEffort : "high" } ,
{ ID : "profile" , Provider : "promptkit" , Model : "model" , BackendID : "backend-a" , ReasoningEffort : "low" } ,
} {
recorder . Record ( profile )
}
want := [ ] artifacts . LLMProfileManifest {
{ ID : "profile" , Provider : "promptkit" , Model : "model" , BackendID : "backend-a" , ReasoningEffort : "high" } ,
{ ID : "profile" , Provider : "promptkit" , Model : "model" , BackendID : "backend-a" , ReasoningEffort : "low" } ,
{ ID : "profile" , Provider : "promptkit" , Model : "model" , BackendID : "backend-b" , ReasoningEffort : "low" } ,
}
if got := recorder . Manifests ( ) ; ! reflect . DeepEqual ( got , want ) {
t . Fatalf ( "profile manifests = %#v, want %#v" , got , want )
}
2026-07-05 18:21:37 +00:00
}
2026-07-28 16:31:36 +00:00
func TestPromptKitClientValidationFailureReturnsError ( t * testing . T ) {
2026-08-25 20:04:56 +00:00
attempts := 1
fake := & fakePromptKitLLM { responses : [ ] promptkit . GenerateResponse { { Content : ` { "bad":true} ` } , { Content : ` { "bad":true} ` } } }
client := newTestPromptKitClient ( t , fake )
2026-07-05 18:21:37 +00:00
var out map [ string ] any
2026-07-07 23:08:14 -05:00
resp , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
2026-08-25 20:04:56 +00:00
PromptID : "adapter.test" ,
SessionID : "session-123" ,
StructuredOutputRepairAttempts : & attempts ,
2026-07-05 18:21:37 +00:00
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
} , & out )
2026-07-26 01:20:28 +00:00
if err == nil || ! errors . Is ( err , contracts . ErrInvalidStructuredOutput ) || ! strings . Contains ( err . Error ( ) , "validation failed" ) {
2026-07-05 18:21:37 +00:00
t . Fatalf ( "CompleteStructured() error = %v, want validation failure" , err )
}
2026-08-25 20:04:56 +00:00
if got := atomic . LoadInt32 ( & fake . calls ) ; got != 2 {
t . Fatalf ( "provider calls = %d, want exhausted repair budget" , got )
}
2026-07-07 23:08:14 -05:00
if got := string ( resp . Content ) ; got != ` { "bad":true} ` {
t . Fatalf ( "response content = %q, want raw failed output" , got )
}
2026-07-08 08:37:26 -05:00
if resp . Debug == nil || resp . Debug . Response == nil || resp . Debug . Response . Content != ` { "bad":true} ` {
t . Fatalf ( "debug response = %#v, want raw failed output" , resp . Debug )
}
if resp . Debug . Prompt == nil || len ( resp . Debug . Prompt . Messages ) == 0 {
t . Fatalf ( "debug prompt = %#v, want prepared prompt material" , resp . Debug . Prompt )
}
2026-07-07 23:08:14 -05:00
}
2026-08-25 20:04:56 +00:00
func TestPromptKitClientRepairsStructuredOutputAndReportsCumulativeUsage ( t * testing . T ) {
attempts := 1
fake := & fakePromptKitLLM { responses : [ ] promptkit . GenerateResponse {
{ Content : ` { "bad":true} ` , Usage : promptkit . TokenUsage { PromptTokens : 3 , CompletionTokens : 5 , TotalTokens : 8 } } ,
{ Content : ` { "ok":true} ` , Usage : promptkit . TokenUsage { PromptTokens : 7 , CompletionTokens : 11 , TotalTokens : 18 } } ,
} }
client := newTestPromptKitClient ( t , fake )
2026-08-26 23:50:24 +00:00
correction , err := contracts . NewSemanticCorrection ( [ ] byte ( ` { "bad":true} ` ) , "Return the required ok field." )
if err != nil {
t . Fatalf ( "NewSemanticCorrection() error = %v" , err )
}
2026-08-25 20:04:56 +00:00
var out struct {
OK bool ` json:"ok" `
}
response , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
PromptID : "adapter.test" ,
StructuredOutputRepairAttempts : & attempts ,
SessionID : "repair-test" ,
2026-08-26 23:50:24 +00:00
Correction : correction ,
2026-08-25 20:04:56 +00:00
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
} , & out )
if err != nil || ! out . OK {
t . Fatalf ( "CompleteStructured() = (%#v, %v), want repaired success" , response , err )
}
if got := atomic . LoadInt32 ( & fake . calls ) ; got != 2 {
t . Fatalf ( "provider calls = %d, want initial generation and one repair" , got )
}
2026-08-26 23:50:24 +00:00
requests := fake . requestsSnapshot ( )
if len ( requests ) != 2 || len ( requests [ 0 ] . Prompt . Messages ) < 3 {
t . Fatalf ( "repair requests = %#v, want correction messages on the initial prepared request" , requests )
}
messages := requests [ 0 ] . Prompt . Messages
if got , want := messages [ len ( messages ) - 2 ] , ( promptkit . RenderedMessage { Role : promptkit . RoleAssistant , Content : ` { "bad":true} ` } ) ; got != want {
t . Fatalf ( "repair assistant correction = %#v, want %#v" , got , want )
}
if got , want := messages [ len ( messages ) - 1 ] , ( promptkit . RenderedMessage { Role : promptkit . RoleUser , Content : "Return the required ok field." } ) ; got != want {
t . Fatalf ( "repair user correction = %#v, want %#v" , got , want )
}
2026-08-25 20:04:56 +00:00
if response . RepairAttempts != 1 || response . PromptTokens != 10 || response . CompletionTokens != 16 || response . TotalTokens != 26 {
t . Fatalf ( "response repair and usage = %#v, want one repair and PromptKit cumulative usage" , response )
}
if response . Debug == nil || response . Debug . Prompt == nil || response . Debug . Response == nil {
t . Fatalf ( "debug = %#v, want prompt and response details" , response . Debug )
}
if response . Debug . Prompt . OutputContract [ "repair_attempts" ] != float64 ( 1 ) ||
response . Debug . Response . Validation [ "repair_attempts" ] != float64 ( 1 ) ||
response . Debug . Response . Content != ` { "ok":true} ` ||
response . Debug . Response . Usage . TotalTokens != 26 {
t . Fatalf ( "debug repair result = %#v, want configured contract and repaired response" , response . Debug )
}
}
2026-07-28 16:31:36 +00:00
func TestPromptKitClientDecodeFailureReturnsRawResponse ( t * testing . T ) {
client := newTestPromptKitClient ( t , & fakePromptKitLLM { content : ` { "ok":true} ` } )
2026-07-07 23:08:14 -05:00
var out [ ] any
resp , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
PromptID : "adapter.test" ,
SessionID : "session-123" ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
} , & out )
2026-07-28 16:31:36 +00:00
if err == nil || ! errors . Is ( err , contracts . ErrInvalidStructuredOutput ) || ! strings . Contains ( err . Error ( ) , "decode PromptKit structured output" ) {
2026-07-07 23:08:14 -05:00
t . Fatalf ( "CompleteStructured() error = %v, want decode failure" , err )
}
if got := string ( resp . Content ) ; got != ` { "ok":true} ` {
t . Fatalf ( "response content = %q, want raw decode-failed output" , got )
}
2026-07-08 08:37:26 -05:00
if resp . Debug == nil || resp . Debug . Response == nil || resp . Debug . Response . Content != ` { "ok":true} ` {
t . Fatalf ( "debug response = %#v, want raw decode-failed output" , resp . Debug )
}
2026-07-05 18:21:37 +00:00
}
2026-07-28 16:31:36 +00:00
func TestPromptKitClientProviderFailureIncludesContextAndRedactsBearerToken ( t * testing . T ) {
2026-08-09 00:44:38 +00:00
providerErr := & credentialBearingProviderError { }
client := newTestPromptKitClient ( t , & fakePromptKitLLM { err : providerErr } )
2026-07-05 18:21:37 +00:00
var out map [ string ] any
2026-07-08 08:37:26 -05:00
resp , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
2026-07-05 18:21:37 +00:00
PromptID : "adapter.test" ,
SessionID : "session-123" ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
} , & out )
if err == nil {
t . Fatalf ( "CompleteStructured() error = nil, want provider error" )
}
2026-07-26 01:20:28 +00:00
if errors . Is ( err , contracts . ErrInvalidStructuredOutput ) {
t . Fatalf ( "provider error = %v, must not be classified as invalid structured output" , err )
}
2026-07-28 16:31:36 +00:00
if ! strings . Contains ( err . Error ( ) , ` run PromptKit prompt "adapter.test" ` ) {
2026-07-05 18:21:37 +00:00
t . Fatalf ( "error = %q, want operation context" , err . Error ( ) )
}
if strings . Contains ( err . Error ( ) , "secret-token" ) || ! strings . Contains ( err . Error ( ) , "Bearer [REDACTED]" ) {
t . Fatalf ( "error = %q, want redacted bearer token" , err . Error ( ) )
}
2026-08-09 00:44:38 +00:00
for current := err ; current != nil ; current = errors . Unwrap ( current ) {
if strings . Contains ( current . Error ( ) , "secret-token" ) {
t . Fatalf ( "error chain exposes credential: %v" , err )
}
}
2026-08-25 19:34:27 +00:00
if ! errors . Is ( err , contracts . ErrLLMGeneration ) {
t . Fatalf ( "provider failure = %v, want application generation classification" , err )
2026-08-09 00:44:38 +00:00
}
var recoveredProviderErr * credentialBearingProviderError
if errors . As ( err , & recoveredProviderErr ) {
t . Fatalf ( "provider error escaped the adapter: %v" , err )
}
if errors . Is ( err , promptkit . ErrLLMGenerate ) {
t . Fatalf ( "provider failure exposes PromptKit generation sentinel: %v" , err )
}
2026-08-25 19:34:27 +00:00
if resp . Debug == nil || resp . Debug . Prompt == nil || resp . Debug . Response != nil {
t . Fatalf ( "debug material = %#v, want prepared prompt without provider details" , resp . Debug )
2026-07-08 08:37:26 -05:00
}
2026-07-05 18:21:37 +00:00
}
2026-07-30 02:23:51 +00:00
func TestPromptKitClientTranslatesBackendCapacityExhaustion ( t * testing . T ) {
queueCapacity := 0
fake := & fakePromptKitLLM {
err : errors . New ( "provider failed with Bearer secret-token" ) ,
block : make ( chan struct { } ) ,
}
client , err := NewPromptKitClient ( PromptKitClientConfig {
Assets : newTestPromptKitAssets ( t ) ,
EngineOptions : [ ] promptkit . Option {
promptkit . WithBackend ( promptkit . Backend {
ID : "limited-backend" ,
Endpoint : "http://127.0.0.1:1/v1" ,
ConcurrencyLimit : 1 ,
QueueCapacity : & queueCapacity ,
} ) ,
promptkit . WithProfiles ( promptkit . OpenAICompatibleProfile ( promptkit . OpenAICompatibleProfileConfig {
ID : "limited-profile" ,
BackendID : "limited-backend" ,
Model : "limited-model" ,
} ) ) ,
promptkit . WithLLMClient ( fake ) ,
} ,
} )
if err != nil {
t . Fatalf ( "NewPromptKitClient() error = %v, want nil" , err )
}
defer func ( ) {
select {
case <- fake . block :
default :
close ( fake . block )
}
} ( )
request := contracts . StructuredCompletionRequest {
PromptID : "adapter.direct-session" ,
ProfileID : "limited-profile" ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
Vars : map [ string ] any { "custom" : "value" } ,
}
firstResult := make ( chan error , 1 )
go func ( ) {
var out map [ string ] any
_ , callErr := client . CompleteStructured ( context . Background ( ) , request , & out )
firstResult <- callErr
} ( )
waitForAtomicAtLeast ( t , & fake . calls , 1 )
var out map [ string ] any
response , capacityErr := client . CompleteStructured ( context . Background ( ) , request , & out )
if len ( response . Content ) != 0 {
t . Fatalf ( "capacity response = %#v, want empty" , response )
}
if ! errors . Is ( capacityErr , contracts . ErrLLMCapacityExceeded ) {
t . Fatalf ( "capacity error = %v, want ErrLLMCapacityExceeded" , capacityErr )
}
if errors . Is ( capacityErr , contracts . ErrInvalidStructuredOutput ) {
t . Fatalf ( "capacity error = %v, must not be invalid structured output" , capacityErr )
}
if errors . Is ( capacityErr , promptkit . ErrCapacityExceeded ) {
t . Fatalf ( "capacity error exposes PromptKit sentinel: %v" , capacityErr )
}
if ! strings . Contains ( capacityErr . Error ( ) , ` run PromptKit prompt "adapter.direct-session" ` ) ||
2026-08-03 16:17:40 +00:00
! strings . Contains ( capacityErr . Error ( ) , ` backend "limited-backend" ` ) ||
2026-07-30 02:23:51 +00:00
! strings . Contains ( capacityErr . Error ( ) , "backend capacity exceeded" ) {
2026-08-03 16:17:40 +00:00
t . Fatalf ( "capacity error = %q, want prompt, backend, and upstream diagnostic context" , capacityErr )
}
var upstreamCapacityErr * promptkit . CapacityError
if errors . As ( capacityErr , & upstreamCapacityErr ) {
t . Fatalf ( "capacity error exposes PromptKit capacity type: %v" , capacityErr )
2026-07-30 02:23:51 +00:00
}
if calls := atomic . LoadInt32 ( & fake . calls ) ; calls != 1 {
t . Fatalf ( "provider calls after capacity rejection = %d, want 1" , calls )
}
close ( fake . block )
firstErr := <- firstResult
if firstErr == nil || strings . Contains ( firstErr . Error ( ) , "secret-token" ) ||
! strings . Contains ( firstErr . Error ( ) , "Bearer [REDACTED]" ) {
t . Fatalf ( "admitted provider error = %v, want redacted diagnostic" , firstErr )
}
if calls := atomic . LoadInt32 ( & fake . calls ) ; calls != 1 {
t . Fatalf ( "provider calls after release = %d, want no adapter retry" , calls )
}
}
2026-07-28 16:31:36 +00:00
func TestPromptKitClientContextCancellationIsRespected ( t * testing . T ) {
2026-08-25 23:46:52 +00:00
attempts := 1
for _ , test := range [ ] struct {
name string
attempts * int
} {
{ name : "during preparation" } ,
{ name : "during override inspection" , attempts : & attempts } ,
} {
t . Run ( test . name , func ( t * testing . T ) {
ctx , cancel := context . WithCancel ( context . Background ( ) )
cancel ( )
client := newTestPromptKitClient ( t , & fakePromptKitLLM { content : ` { "ok":true} ` } )
2026-07-05 18:21:37 +00:00
2026-08-25 23:46:52 +00:00
var out map [ string ] any
_ , err := client . CompleteStructured ( ctx , contracts . StructuredCompletionRequest {
PromptID : "adapter.test" ,
StructuredOutputRepairAttempts : test . attempts ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
} , & out )
if ! errors . Is ( err , context . Canceled ) || errors . Is ( err , contracts . ErrInvalidStructuredOutput ) {
t . Fatalf ( "CompleteStructured() error = %v, want context canceled" , err )
}
} )
2026-07-05 18:21:37 +00:00
}
}
2026-07-28 16:31:36 +00:00
func TestPromptKitClientForwardsConfiguredTransportTimeout ( t * testing . T ) {
var remaining time . Duration
transport := roundTripFunc ( func ( req * http . Request ) ( * http . Response , error ) {
deadline , ok := req . Context ( ) . Deadline ( )
if ! ok {
t . Fatal ( "outbound request context has no deadline" )
}
remaining = time . Until ( deadline )
return & http . Response {
StatusCode : http . StatusOK ,
Status : "200 OK" ,
Header : make ( http . Header ) ,
Body : io . NopCloser ( strings . NewReader (
` { "choices":[ { "message": { "content":" { \"ok\":true}"}}]} ` ,
) ) ,
Request : req ,
} , nil
} )
const configuredTimeout = 2 * time . Second
client , err := NewPromptKitClient ( PromptKitClientConfig {
Assets : newTestPromptKitAssets ( t ) ,
Timeout : configuredTimeout ,
HTTPClient : & http . Client { Transport : transport } ,
EngineOptions : [ ] promptkit . Option {
promptkit . WithProfiles ( promptkit . OpenAICompatibleProfile ( promptkit . OpenAICompatibleProfileConfig {
ID : "default-profile" ,
Endpoint : "http://promptkit.test/v1" ,
Model : "default-model" ,
} ) ) ,
} ,
} )
if err != nil {
t . Fatalf ( "NewPromptKitClient() error = %v" , err )
}
var out map [ string ] any
if _ , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
PromptID : "adapter.test" ,
SessionID : "session-123" ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
} , & out ) ; err != nil {
t . Fatalf ( "CompleteStructured() error = %v" , err )
}
if remaining < configuredTimeout - time . Second || remaining > configuredTimeout {
t . Fatalf ( "transport deadline remaining = %v, want near %v" , remaining , configuredTimeout )
}
}
2026-09-25 21:59:15 +00:00
func TestPromptKitClientClassifiesBuiltInTransportAndMalformedResponses ( t * testing . T ) {
for _ , test := range [ ] struct {
name string
kind contracts . LLMGenerationFailureKind
roundTrip func ( * http . Request ) ( * http . Response , error )
} {
{
name : "transport" ,
kind : contracts . LLMGenerationFailureTransport ,
roundTrip : func ( * http . Request ) ( * http . Response , error ) {
return nil , errors . New ( "transport unavailable" )
} ,
} ,
{
name : "malformed response" ,
kind : contracts . LLMGenerationFailureMalformedResponse ,
roundTrip : func ( request * http . Request ) ( * http . Response , error ) {
return & http . Response { StatusCode : http . StatusOK , Header : make ( http . Header ) , Body : io . NopCloser ( strings . NewReader ( "not-json" ) ) , Request : request } , nil
} ,
} ,
} {
t . Run ( test . name , func ( t * testing . T ) {
client , err := NewPromptKitClient ( PromptKitClientConfig {
Assets : newTestPromptKitAssets ( t ) ,
HTTPClient : & http . Client { Transport : roundTripFunc ( test . roundTrip ) } ,
EngineOptions : [ ] promptkit . Option { promptkit . WithProfiles ( promptkit . OpenAICompatibleProfile ( promptkit . OpenAICompatibleProfileConfig {
ID : "default-profile" , Endpoint : "http://promptkit.test/v1" , Model : "default-model" ,
} ) ) } ,
} )
if err != nil {
t . Fatal ( err )
}
var out map [ string ] any
_ , err = client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
PromptID : "adapter.test" , SessionID : "failure-kind-test" ,
Inputs : contracts . LLMInputSet { "transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) } ,
} , & out )
var generation * contracts . LLMGenerationError
if ! errors . As ( err , & generation ) || generation . FailureKind ( ) != test . kind || generation . RetriesExhausted ( ) || generation . Attempts ( ) != 0 {
t . Fatalf ( "generation error = %#v, want kind %q" , generation , test . kind )
}
} )
}
}
2026-07-28 16:31:36 +00:00
func TestPromptKitClientClassifiesEmptyStructuredCompletion ( t * testing . T ) {
client := newTestPromptKitClient ( t , & fakePromptKitLLM { allowEmpty : true } )
2026-07-26 01:20:28 +00:00
var out map [ string ] any
_ , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
PromptID : "adapter.test" ,
SessionID : "session-123" ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
} , & out )
if ! errors . Is ( err , contracts . ErrInvalidStructuredOutput ) {
t . Fatalf ( "CompleteStructured() error = %v, want invalid structured output" , err )
}
}
2026-07-28 16:31:36 +00:00
func TestScheduledPromptKitClientBoundsConcurrentCalls ( t * testing . T ) {
fake := & fakePromptKitLLM {
2026-07-05 18:21:37 +00:00
content : ` { "ok":true} ` ,
block : make ( chan struct { } ) ,
}
2026-07-28 16:31:36 +00:00
client := newTestPromptKitClient ( t , fake )
2026-07-05 18:21:37 +00:00
scheduler , err := NewScheduler ( 1 )
if err != nil {
t . Fatalf ( "NewScheduler() error = %v, want nil" , err )
}
scheduled := NewScheduledClient ( client , scheduler )
var wg sync . WaitGroup
for i := 0 ; i < 3 ; i ++ {
wg . Add ( 1 )
go func ( ) {
defer wg . Done ( )
var out map [ string ] any
_ , callErr := scheduled . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
PromptID : "adapter.test" ,
SessionID : "session-123" ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
} , & out )
if callErr != nil {
t . Errorf ( "CompleteStructured() error = %v, want nil" , callErr )
}
} ( )
}
waitForAtomicAtLeast ( t , & fake . calls , 1 )
time . Sleep ( 20 * time . Millisecond )
if got := atomic . LoadInt32 ( & fake . maxInFlight ) ; got > 1 {
t . Fatalf ( "max in-flight calls = %d, want <= 1" , got )
}
close ( fake . block )
wg . Wait ( )
}
2026-08-25 20:04:56 +00:00
func TestScheduledPromptKitClientHoldsPermitAcrossStructuredOutputRepair ( t * testing . T ) {
attempts := 1
fake := & fakePromptKitLLM {
responses : [ ] promptkit . GenerateResponse {
{ Content : ` { "bad":true} ` } ,
{ Content : ` { "ok":true} ` } ,
{ Content : ` { "ok":true} ` } ,
} ,
block : make ( chan struct { } ) ,
}
scheduler , err := NewScheduler ( 1 )
if err != nil {
t . Fatalf ( "NewScheduler() error = %v" , err )
}
scheduled := NewScheduledClient ( newTestPromptKitClient ( t , fake ) , scheduler )
firstDone := make ( chan error , 1 )
go func ( ) {
var out map [ string ] any
_ , callErr := scheduled . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
PromptID : "adapter.test" ,
SessionID : "repair-session" ,
StructuredOutputRepairAttempts : & attempts ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
} , & out )
firstDone <- callErr
} ( )
waitForAtomicAtLeast ( t , & fake . calls , 1 )
secondDone := make ( chan error , 1 )
go func ( ) {
var out map [ string ] any
_ , callErr := scheduled . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest {
PromptID : "adapter.test" ,
SessionID : "queued-session" ,
Inputs : contracts . LLMInputSet {
"transcript" : contracts . NewLLMInputMaterial ( "transcript" , "application/json" , [ ] byte ( ` { "source":true} ` ) , "" , "" ) ,
} ,
} , & out )
secondDone <- callErr
} ( )
close ( fake . block )
if err := <- firstDone ; err != nil {
t . Fatalf ( "repaired completion error = %v" , err )
}
if err := <- secondDone ; err != nil {
t . Fatalf ( "queued completion error = %v" , err )
}
requests := fake . requestsSnapshot ( )
if len ( requests ) != 3 || requests [ 1 ] . Prompt . SessionID != "repair-session" || requests [ 2 ] . Prompt . SessionID != "queued-session" {
t . Fatalf ( "generation order = %#v, want repair before queued completion" , requests )
}
if got := atomic . LoadInt32 ( & fake . maxInFlight ) ; got > 1 {
t . Fatalf ( "max in-flight calls = %d, want one scheduled logical completion" , got )
}
}
2026-07-28 16:31:36 +00:00
func TestPromptKitClientValidatesRequest ( t * testing . T ) {
client := newTestPromptKitClient ( t , & fakePromptKitLLM { content : ` { "ok":true} ` } )
2026-07-05 18:21:37 +00:00
var out map [ string ] any
if _ , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest { } , & out ) ; err == nil || ! strings . Contains ( err . Error ( ) , "prompt_id" ) {
t . Fatalf ( "missing prompt id error = %v, want prompt_id validation" , err )
}
if _ , err := client . CompleteStructured ( context . Background ( ) , contracts . StructuredCompletionRequest { PromptID : "adapter.test" } , nil ) ; err == nil || ! strings . Contains ( err . Error ( ) , "non-nil pointer" ) {
t . Fatalf ( "nil output error = %v, want output validation" , err )
}
}
2026-07-28 16:31:36 +00:00
func newTestPromptKitClient ( t * testing . T , fake * fakePromptKitLLM ) * PromptKitClient {
2026-07-30 02:03:04 +00:00
return newTestPromptKitClientWithReasoning ( t , fake , nil )
}
func newTestPromptKitClientWithReasoning ( t * testing . T , fake * fakePromptKitLLM , reasoningEffort * string ) * PromptKitClient {
2026-07-28 16:31:36 +00:00
t . Helper ( )
registry := newTestPromptKitAssets ( t )
client , err := NewPromptKitClient ( PromptKitClientConfig {
2026-07-30 02:03:04 +00:00
Assets : registry ,
ReasoningEffort : reasoningEffort ,
2026-07-28 16:31:36 +00:00
EngineOptions : [ ] promptkit . Option {
2026-07-30 02:18:14 +00:00
promptkit . WithBackend ( promptkit . Backend {
ID : "test-backend" ,
Endpoint : "http://127.0.0.1:1/v1" ,
} ) ,
2026-07-28 16:31:36 +00:00
promptkit . WithProfiles (
promptkit . OpenAICompatibleProfile ( promptkit . OpenAICompatibleProfileConfig {
2026-07-30 02:03:04 +00:00
ID : "default-profile" ,
Endpoint : "http://127.0.0.1:1/v1" ,
Model : "default-model" ,
ReasoningEffort : "profile-reasoning" ,
2026-07-28 16:31:36 +00:00
} ) ,
promptkit . OpenAICompatibleProfile ( promptkit . OpenAICompatibleProfileConfig {
2026-07-30 02:03:04 +00:00
ID : "explicit-profile" ,
2026-07-30 02:18:14 +00:00
BackendID : "test-backend" ,
2026-07-30 02:03:04 +00:00
Model : "explicit-model" ,
ReasoningEffort : "profile-reasoning" ,
2026-07-28 16:31:36 +00:00
} ) ,
) ,
promptkit . WithLLMClient ( fake ) ,
} ,
} )
if err != nil {
t . Fatalf ( "NewPromptKitClient() error = %v, want nil" , err )
}
return client
}
func newTestPromptKitAssets ( t * testing . T ) * AssetRegistry {
2026-07-05 18:21:37 +00:00
t . Helper ( )
registry := NewAssetRegistry ( )
if err := registry . RegisterPromptFS ( fstest . MapFS {
"adapter.test.yaml" : { Data : [ ] byte ( ` id : adapter . test
version : "v1"
default_profile : default - profile
session_id : "{{ .session_id }}"
2026-07-30 01:58:40 +00:00
inputs :
- name : transcript
required : true
content_type : application / json
messages :
- role : user
content : "Transcript: {{ input \"transcript\" }} Custom: {{ index . \"custom\" }} Session: {{ .session_id }}"
output :
format : json
validation_mode : json_schema
schema_path : adapter . schema . json
repair_attempts : 0
` ) } ,
"adapter.direct-session.yaml" : { Data : [ ] byte ( ` id : adapter . direct - session
version : "v1"
default_profile : default - profile
2026-07-05 18:21:37 +00:00
inputs :
- name : transcript
required : true
content_type : application / json
messages :
- role : user
2026-07-28 16:31:36 +00:00
content : "Transcript: {{ input \"transcript\" }} Custom: {{ index . \"custom\" }}"
2026-07-05 18:21:37 +00:00
output :
format : json
validation_mode : json_schema
schema_path : adapter . schema . json
repair_attempts : 0
` ) } ,
} , "." ) ; err != nil {
t . Fatalf ( "RegisterPromptFS() error = %v" , err )
}
if err := registry . RegisterSchemaFS ( fstest . MapFS {
"adapter.schema.json" : { Data : [ ] byte ( ` { "type":"object","required":["ok"],"properties": { "ok": { "type":"boolean"}}} ` ) } ,
} , "." ) ; err != nil {
t . Fatalf ( "RegisterSchemaFS() error = %v" , err )
}
2026-07-28 16:31:36 +00:00
return registry
2026-07-05 18:21:37 +00:00
}
2026-08-03 16:17:40 +00:00
type switchingPromptFS struct {
mu sync . Mutex
files fstest . MapFS
pauseNextOpen bool
opened chan struct { }
resume chan struct { }
}
func ( f * switchingPromptFS ) Open ( name string ) ( fs . File , error ) {
f . mu . Lock ( )
file , err := f . files . Open ( name )
pause := f . pauseNextOpen && name == "snapshot.test.yaml"
resume := f . resume
if pause {
f . pauseNextOpen = false
close ( f . opened )
}
f . mu . Unlock ( )
if pause {
<- resume
}
return file , err
}
func ( f * switchingPromptFS ) ReadDir ( name string ) ( [ ] fs . DirEntry , error ) {
f . mu . Lock ( )
defer f . mu . Unlock ( )
return f . files . ReadDir ( name )
}
func ( f * switchingPromptFS ) holdNextPromptRead ( ) <- chan struct { } {
f . mu . Lock ( )
defer f . mu . Unlock ( )
f . pauseNextOpen = true
f . opened = make ( chan struct { } )
f . resume = make ( chan struct { } )
return f . opened
}
func ( f * switchingPromptFS ) replacePrompt ( content [ ] byte ) {
f . mu . Lock ( )
defer f . mu . Unlock ( )
f . files [ "snapshot.test.yaml" ] = & fstest . MapFile { Data : append ( [ ] byte ( nil ) , content ... ) }
}
func ( f * switchingPromptFS ) resumePromptRead ( ) {
f . mu . Lock ( )
resume := f . resume
f . resume = nil
f . mu . Unlock ( )
if resume != nil {
close ( resume )
}
}
2026-07-28 16:31:36 +00:00
type fakePromptKitLLM struct {
2026-07-05 18:21:37 +00:00
content string
2026-08-25 20:04:56 +00:00
responses [ ] promptkit . GenerateResponse
2026-07-26 01:20:28 +00:00
allowEmpty bool
2026-07-05 18:21:37 +00:00
err error
block chan struct { }
mu sync . Mutex
2026-07-28 16:31:36 +00:00
last promptkit . GenerateRequest
2026-08-25 20:04:56 +00:00
requests [ ] promptkit . GenerateRequest
2026-07-05 18:21:37 +00:00
calls int32
inFlight int32
maxInFlight int32
}
2026-08-09 00:44:38 +00:00
type credentialBearingProviderError struct { }
func ( * credentialBearingProviderError ) Error ( ) string {
return "provider failed with Bearer secret-token"
}
2026-07-28 16:31:36 +00:00
func ( f * fakePromptKitLLM ) Generate ( ctx context . Context , req promptkit . GenerateRequest ) ( * promptkit . GenerateResponse , error ) {
2026-07-05 18:21:37 +00:00
f . mu . Lock ( )
f . last = req
2026-08-25 20:04:56 +00:00
f . requests = append ( f . requests , req )
2026-07-05 18:21:37 +00:00
f . mu . Unlock ( )
2026-08-25 20:04:56 +00:00
call := atomic . AddInt32 ( & f . calls , 1 )
2026-07-05 18:21:37 +00:00
current := atomic . AddInt32 ( & f . inFlight , 1 )
for {
seen := atomic . LoadInt32 ( & f . maxInFlight )
if current <= seen || atomic . CompareAndSwapInt32 ( & f . maxInFlight , seen , current ) {
break
}
}
defer atomic . AddInt32 ( & f . inFlight , - 1 )
if f . block != nil {
select {
case <- f . block :
case <- ctx . Done ( ) :
return nil , ctx . Err ( )
}
}
if f . err != nil {
return nil , f . err
}
2026-08-25 20:04:56 +00:00
if len ( f . responses ) > 0 {
index := int ( call - 1 )
if index >= len ( f . responses ) {
return nil , fmt . Errorf ( "unexpected provider call %d" , call )
}
response := f . responses [ index ]
return & response , nil
}
2026-07-05 18:21:37 +00:00
content := f . content
2026-07-26 01:20:28 +00:00
if content == "" && ! f . allowEmpty {
2026-07-05 18:21:37 +00:00
content = ` { "ok":true} `
}
2026-07-26 01:20:28 +00:00
if ! f . allowEmpty && ! json . Valid ( [ ] byte ( content ) ) {
2026-07-05 18:21:37 +00:00
return nil , errors . New ( "test fake must return JSON content" )
}
2026-07-28 16:31:36 +00:00
return & promptkit . GenerateResponse {
2026-07-05 18:21:37 +00:00
Content : content ,
2026-07-28 16:31:36 +00:00
Usage : promptkit . TokenUsage {
2026-07-05 18:21:37 +00:00
PromptTokens : 11 ,
CompletionTokens : 7 ,
TotalTokens : 18 ,
2026-07-28 16:31:36 +00:00
CachedTokens : 5 ,
CacheWriteTokens : 3 ,
2026-07-05 18:21:37 +00:00
} ,
} , nil
}
2026-07-28 16:31:36 +00:00
type roundTripFunc func ( * http . Request ) ( * http . Response , error )
func ( f roundTripFunc ) RoundTrip ( req * http . Request ) ( * http . Response , error ) {
return f ( req )
}
func ( f * fakePromptKitLLM ) lastRequest ( ) promptkit . GenerateRequest {
2026-07-05 18:21:37 +00:00
f . mu . Lock ( )
defer f . mu . Unlock ( )
return f . last
}
2026-08-25 20:04:56 +00:00
func ( f * fakePromptKitLLM ) requestsSnapshot ( ) [ ] promptkit . GenerateRequest {
f . mu . Lock ( )
defer f . mu . Unlock ( )
return append ( [ ] promptkit . GenerateRequest ( nil ) , f . requests ... )
}