2026-05-04 21:08:40 -05:00
package cli
import (
2026-05-04 22:38:20 -05:00
"bytes"
2026-09-26 15:47:26 +00:00
"context"
2026-05-06 15:20:22 +00:00
"encoding/json"
2026-05-04 21:08:40 -05:00
"errors"
2026-05-05 10:49:33 -05:00
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
2026-05-26 13:12:34 +00:00
"reflect"
2026-05-04 22:38:20 -05:00
"strings"
2026-05-05 10:49:33 -05:00
"sync/atomic"
2026-05-04 21:08:40 -05:00
"testing"
2026-05-04 22:38:20 -05:00
"time"
2026-05-04 21:08:40 -05:00
2026-09-26 18:08:36 +00:00
configkit "gitea.maximumdirect.net/eric/ConfigKit"
2026-07-28 14:04:29 +00:00
"gitea.maximumdirect.net/eric/promptkit"
2026-05-05 10:49:33 -05:00
"gitea.maximumdirect.net/eric/scriptorium/internal/defaults"
2026-05-06 15:20:22 +00:00
renderformat "gitea.maximumdirect.net/eric/scriptorium/internal/format"
2026-05-04 21:08:40 -05:00
)
func TestParseMappingsSingleAndRepeated ( t * testing . T ) {
got , err := parseMappings ( [ ] string { "transcript=./t.md" , "glossary=./g.yml" } , false )
if err != nil {
t . Fatalf ( "unexpected error: %v" , err )
}
if got [ "transcript" ] != "./t.md" || got [ "glossary" ] != "./g.yml" {
t . Fatalf ( "unexpected mappings: %#v" , got )
}
}
func TestParseMappingsCommaSeparated ( t * testing . T ) {
got , err := parseMappings ( [ ] string { "transcript=./t.md,glossary=./g.yml" } , false )
if err != nil {
t . Fatalf ( "unexpected error: %v" , err )
}
if got [ "transcript" ] != "./t.md" || got [ "glossary" ] != "./g.yml" {
t . Fatalf ( "unexpected mappings: %#v" , got )
}
}
func TestParseMappingsVarWithEqualsInValue ( t * testing . T ) {
got , err := parseMappings ( [ ] string { "session_note=a=b=c" } , false )
if err != nil {
t . Fatalf ( "unexpected error: %v" , err )
}
if got [ "session_note" ] != "a=b=c" {
t . Fatalf ( "unexpected variable value: %#v" , got )
}
}
func TestParseMappingsMalformed ( t * testing . T ) {
tests := [ ] string { "" , "novalue" , "=emptyname" , "name=" }
for _ , tc := range tests {
_ , err := parseMappings ( [ ] string { tc } , false )
if err == nil {
t . Fatalf ( "expected error for %q" , tc )
}
}
}
func TestParseRunArgsRequiredFlags ( t * testing . T ) {
2026-09-26 18:08:36 +00:00
configPath := writeAppConfigFile ( t , "{}\n" )
2026-05-06 16:46:33 +00:00
_ , err := parseRunArgs ( [ ] string { "--config" , configPath , "--profile-dir" , "./profiles" , "--prompt" , "p" , "--input" , "a=b" } )
2026-05-05 10:49:33 -05:00
if err == nil {
t . Fatal ( "expected missing --prompt-dir error" )
}
2026-05-06 16:48:35 +00:00
if ! strings . Contains ( err . Error ( ) , "prompt directory is required" ) {
t . Fatalf ( "expected clear prompt-dir guidance, got %v" , err )
}
2026-05-05 10:49:33 -05:00
2026-07-04 16:48:18 +00:00
cfg , err := parseRunArgs ( [ ] string { "--config" , configPath , "--prompt-dir" , "./prompts" , "--prompt" , "p" , "--input" , "a=b" } )
if err != nil {
t . Fatalf ( "expected missing --profile-dir to be accepted, got %v" , err )
2026-05-04 21:08:40 -05:00
}
2026-07-04 16:48:18 +00:00
if cfg . profileDir != "" {
t . Fatalf ( "expected empty profile dir for built-ins, got %q" , cfg . profileDir )
2026-05-06 16:48:35 +00:00
}
2026-05-04 21:08:40 -05:00
2026-05-06 16:46:33 +00:00
_ , err = parseRunArgs ( [ ] string { "--config" , configPath , "--prompt-dir" , "./prompts" , "--profile-dir" , "./profiles" , "--input" , "a=b" } )
2026-05-04 21:08:40 -05:00
if err == nil {
2026-05-05 10:49:33 -05:00
t . Fatal ( "expected missing --prompt error" )
2026-05-04 21:08:40 -05:00
}
2026-08-29 14:12:51 +00:00
cfg , err = parseRunArgs ( [ ] string { "--config" , configPath , "--prompt-dir" , "./prompts" , "--profile-dir" , "./profiles" , "--prompt" , "p" } )
if err != nil {
t . Fatalf ( "expected omitted --input to be accepted, got %v" , err )
}
if len ( cfg . inputRaw ) != 0 {
t . Fatalf ( "expected no input mappings, got %#v" , cfg . inputRaw )
2026-05-04 21:08:40 -05:00
}
2026-05-05 08:41:07 -05:00
}
2026-05-04 21:08:40 -05:00
2026-05-05 10:49:33 -05:00
func TestParseRunArgsFlagMapping ( t * testing . T ) {
cfg , err := parseRunArgs ( [ ] string {
"--prompt-dir" , "./prompts" ,
"--profile-dir" , "./profiles" ,
"--prompt" , "prompt.a" ,
2026-08-29 14:12:51 +00:00
"--prompt-version" , "2" ,
2026-05-05 10:49:33 -05:00
"--profile" , "profile.a" ,
2026-08-29 14:22:45 +00:00
"--session-id" , "session-1" ,
2026-05-05 10:49:33 -05:00
"--input" , "a=b" ,
"--llm-base-url" , "http://x/v1" ,
"--model" , "m" ,
"--temperature" , "0.7" ,
"--max-tokens" , "111" ,
"--top-p" , "0.8" ,
2026-08-29 14:22:45 +00:00
"--reasoning-effort" , "medium" ,
2026-05-05 10:49:33 -05:00
"--timeout" , "30s" ,
"--api-key-env" , "SCRIPTORIUM_API_KEY" ,
} )
if err != nil {
t . Fatalf ( "expected valid args, got %v" , err )
}
if cfg . promptDir != filepath . Clean ( "./prompts" ) || cfg . profileDir != filepath . Clean ( "./profiles" ) {
t . Fatalf ( "unexpected dirs: prompt=%q profile=%q" , cfg . promptDir , cfg . profileDir )
}
2026-08-29 14:12:51 +00:00
if cfg . promptID != "prompt.a" || cfg . promptVersion != "2" || cfg . profileID != "profile.a" {
t . Fatalf ( "unexpected prompt/version/profile ids: %q %q %q" , cfg . promptID , cfg . promptVersion , cfg . profileID )
2026-05-05 10:49:33 -05:00
}
2026-08-29 14:22:45 +00:00
if cfg . sessionID != "session-1" || cfg . reasoningEffort != "medium" || ! cfg . reasoningEffortSet {
t . Fatalf ( "unexpected session or reasoning configuration: %+v" , cfg )
}
2026-05-05 10:49:33 -05:00
if ! cfg . llmBaseURLSet || ! cfg . modelSet || ! cfg . temperatureSet || ! cfg . maxTokensSet || ! cfg . topPSet || ! cfg . timeoutSet || ! cfg . apiKeyEnvSet {
t . Fatalf ( "expected override flags set, got %+v" , cfg )
}
}
2026-05-05 08:41:07 -05:00
func TestParseRunArgsAllowsOmittedModelAndBaseURL ( t * testing . T ) {
cfg , err := parseRunArgs ( [ ] string {
2026-05-05 10:49:33 -05:00
"--prompt-dir" , "./prompts" ,
2026-05-05 08:41:07 -05:00
"--profile-dir" , "./profiles" ,
2026-05-05 10:49:33 -05:00
"--prompt" , "p" ,
2026-05-05 08:41:07 -05:00
"--input" , "a=b" ,
} )
if err != nil {
t . Fatalf ( "expected valid args without model/base url, got %v" , err )
2026-05-04 21:08:40 -05:00
}
2026-05-05 08:41:07 -05:00
if cfg . llmBaseURL != "" || cfg . model != "" {
t . Fatalf ( "expected empty model/baseurl, got model=%q base=%q" , cfg . model , cfg . llmBaseURL )
2026-05-04 21:08:40 -05:00
}
}
2026-05-05 10:49:33 -05:00
func TestParseRunArgsRejectsRawLLMAPIKeyFlag ( t * testing . T ) {
_ , err := parseRunArgs ( [ ] string {
"--prompt-dir" , "./prompts" ,
"--profile-dir" , "./profiles" ,
"--prompt" , "p" ,
"--input" , "a=b" ,
"--llm-api-key" , "secret" ,
} )
if err == nil {
t . Fatal ( "expected unknown flag error for --llm-api-key" )
}
}
2026-05-04 21:19:29 -05:00
func TestParseServeArgsRequiredFlags ( t * testing . T ) {
2026-09-26 18:08:36 +00:00
configPath := writeAppConfigFile ( t , "{}\n" )
2026-05-06 16:46:33 +00:00
_ , err := parseServeArgs ( [ ] string { "--config" , configPath , "--profile-dir" , "./profiles" } )
2026-05-04 21:19:29 -05:00
if err == nil {
2026-05-05 10:49:33 -05:00
t . Fatal ( "expected missing --prompt-dir error" )
2026-05-04 21:19:29 -05:00
}
2026-05-06 16:48:35 +00:00
if ! strings . Contains ( err . Error ( ) , "prompt directory is required" ) {
t . Fatalf ( "expected clear prompt-dir guidance, got %v" , err )
}
2026-05-04 21:19:29 -05:00
2026-07-04 16:48:18 +00:00
cfg , err := parseServeArgs ( [ ] string { "--config" , configPath , "--prompt-dir" , "./prompts" } )
2026-05-04 21:19:29 -05:00
if err != nil {
2026-07-04 16:48:18 +00:00
t . Fatalf ( "expected missing --profile-dir to be accepted, got %v" , err )
}
if cfg . profileDir != "" {
t . Fatalf ( "expected empty profile dir for built-ins, got %q" , cfg . profileDir )
2026-05-04 21:19:29 -05:00
}
2026-05-05 10:49:33 -05:00
if cfg . addr != defaults . HTTPAddrDefault {
t . Fatalf ( "expected default addr %s, got %q" , defaults . HTTPAddrDefault , cfg . addr )
2026-05-04 21:19:29 -05:00
}
2026-05-05 20:01:45 -05:00
if cfg . schemaDir != defaults . SchemaDirDefault {
t . Fatalf ( "expected default schema dir %q, got %q" , defaults . SchemaDirDefault , cfg . schemaDir )
}
}
func TestParseServeArgsRejectsRuntimeOverrideFlags ( t * testing . T ) {
base := [ ] string { "--prompt-dir" , "./prompts" , "--profile-dir" , "./profiles" }
tests := [ ] [ ] string {
{ "--llm-base-url" , "http://localhost:8000/v1" } ,
{ "--model" , "gpt-4o-mini" } ,
{ "--timeout" , "30s" } ,
}
for _ , tc := range tests {
_ , err := parseServeArgs ( append ( base , tc ... ) )
if err == nil {
t . Fatalf ( "expected unknown flag error for %q" , tc [ 0 ] )
}
2026-05-04 22:38:20 -05:00
}
}
2026-08-29 14:12:51 +00:00
func TestUsageIncludesExecutionAndServeFlags ( t * testing . T ) {
2026-07-05 00:24:17 +00:00
var stderr bytes . Buffer
code := Run ( nil , io . Discard , & stderr )
if code != ExitRuntimeError {
t . Fatalf ( "expected usage path to return runtime error, got %d" , code )
}
usage := stderr . String ( )
for _ , want := range [ ] string {
2026-08-29 14:12:51 +00:00
"--prompt-version VERSION" ,
2026-08-29 14:22:45 +00:00
"--session-id ID" ,
"--reasoning-effort VALUE" ,
2026-07-05 00:24:17 +00:00
"--artifact-root" ,
"--max-request-bytes" ,
"--max-artifact-bytes" ,
"--max-response-bytes" ,
2026-08-29 15:12:45 +00:00
"inspect prompt" ,
"inspect profile" ,
2026-07-05 00:24:17 +00:00
} {
if ! strings . Contains ( usage , want ) {
t . Fatalf ( "expected usage to include %q, got %q" , want , usage )
}
}
}
2026-05-04 22:38:20 -05:00
func TestParseRunArgsTimeout ( t * testing . T ) {
cfg , err := parseRunArgs ( [ ] string {
2026-05-05 10:49:33 -05:00
"--prompt-dir" , "./prompts" ,
2026-05-04 22:38:20 -05:00
"--profile-dir" , "./profiles" ,
2026-05-05 10:49:33 -05:00
"--prompt" , "p" ,
2026-05-04 22:38:20 -05:00
"--input" , "a=b" ,
} )
if err != nil {
t . Fatalf ( "expected valid run args, got %v" , err )
}
2026-07-28 14:07:59 +00:00
if cfg . timeout != 0 {
t . Fatalf ( "expected omitted timeout to remain unset, got %s" , cfg . timeout )
2026-05-04 22:38:20 -05:00
}
cfg , err = parseRunArgs ( [ ] string {
2026-05-05 10:49:33 -05:00
"--prompt-dir" , "./prompts" ,
2026-05-04 22:38:20 -05:00
"--profile-dir" , "./profiles" ,
2026-05-05 10:49:33 -05:00
"--prompt" , "p" ,
2026-05-04 22:38:20 -05:00
"--input" , "a=b" ,
"--timeout" , "2m30s" ,
} )
if err != nil {
t . Fatalf ( "expected valid run args with timeout override, got %v" , err )
}
if cfg . timeout != 2 * time . Minute + 30 * time . Second {
t . Fatalf ( "expected timeout override 2m30s, got %s" , cfg . timeout )
}
2026-05-04 21:19:29 -05:00
}
2026-05-06 15:20:22 +00:00
func TestParseRenderArgsDefaultsAndFormat ( t * testing . T ) {
cfg , err := parseRenderArgs ( [ ] string {
"--prompt-dir" , "./prompts" ,
"--profile-dir" , "./profiles" ,
"--prompt" , "p" ,
"--input" , "a=b" ,
} )
if err != nil {
t . Fatalf ( "expected valid render args, got %v" , err )
}
if cfg . outputFormat != renderformat . DefaultPreparedRunOutputFormat {
t . Fatalf ( "expected default render format %q, got %q" , renderformat . DefaultPreparedRunOutputFormat , cfg . outputFormat )
}
}
func TestParseRenderArgsExplicitFormatsAndUnknown ( t * testing . T ) {
cfg , err := parseRenderArgs ( [ ] string {
"--prompt-dir" , "./prompts" ,
"--profile-dir" , "./profiles" ,
"--prompt" , "p" ,
"--input" , "a=b" ,
"--format" , "text" ,
} )
if err != nil {
t . Fatalf ( "expected valid text format, got %v" , err )
}
if cfg . outputFormat != renderformat . PreparedRunFormatText {
t . Fatalf ( "expected text format, got %q" , cfg . outputFormat )
}
cfg , err = parseRenderArgs ( [ ] string {
"--prompt-dir" , "./prompts" ,
"--profile-dir" , "./profiles" ,
"--prompt" , "p" ,
"--input" , "a=b" ,
"--format" , "json" ,
} )
if err != nil {
t . Fatalf ( "expected valid json format, got %v" , err )
}
if cfg . outputFormat != renderformat . PreparedRunFormatJSON {
t . Fatalf ( "expected json format, got %q" , cfg . outputFormat )
}
_ , err = parseRenderArgs ( [ ] string {
"--prompt-dir" , "./prompts" ,
"--profile-dir" , "./profiles" ,
"--prompt" , "p" ,
"--input" , "a=b" ,
"--format" , "yaml" ,
} )
if err == nil {
t . Fatal ( "expected unknown format error" )
}
if ! strings . Contains ( err . Error ( ) , "unknown prepared run format" ) {
t . Fatalf ( "expected clear unknown format error, got %v" , err )
}
}
2026-05-06 16:46:33 +00:00
func TestParseRunArgsWithExplicitConfigLoadsDirectories ( t * testing . T ) {
configPath := writeAppConfigFile ( t , `
prompt_dir : . / from - config / prompts
profile_dir : . / from - config / profiles
schema_dir : . / from - config / schemas
` )
cfg , err := parseRunArgs ( [ ] string {
"--config" , configPath ,
"--prompt" , "p" ,
"--input" , "a=b" ,
} )
if err != nil {
t . Fatalf ( "expected valid args, got %v" , err )
}
if cfg . promptDir != filepath . Clean ( "./from-config/prompts" ) {
t . Fatalf ( "expected prompt dir from config, got %q" , cfg . promptDir )
}
if cfg . profileDir != filepath . Clean ( "./from-config/profiles" ) {
t . Fatalf ( "expected profile dir from config, got %q" , cfg . profileDir )
}
if cfg . schemaDir != filepath . Clean ( "./from-config/schemas" ) {
t . Fatalf ( "expected schema dir from config, got %q" , cfg . schemaDir )
}
}
func TestParseRunArgsMissingExplicitConfigReturnsError ( t * testing . T ) {
_ , err := parseRunArgs ( [ ] string {
"--config" , filepath . Join ( t . TempDir ( ) , "missing.yml" ) ,
"--prompt" , "p" ,
"--input" , "a=b" ,
} )
if err == nil {
t . Fatal ( "expected explicit config missing error" )
}
}
2026-05-06 16:51:36 +00:00
func TestParseRunArgsInvalidExplicitConfigReturnsError ( t * testing . T ) {
configPath := writeAppConfigFile ( t , "api_key: secret\n" )
_ , err := parseRunArgs ( [ ] string {
"--config" , configPath ,
"--prompt" , "p" ,
"--input" , "a=b" ,
} )
if err == nil {
t . Fatal ( "expected invalid explicit config error" )
}
2026-09-26 18:08:36 +00:00
var loadError * configkit . LoadError
if ! errors . As ( err , & loadError ) {
t . Fatalf ( "expected structured application config error, got %v" , err )
2026-05-06 16:46:33 +00:00
}
}
func TestParseRunArgsCLIOverridesConfigDirectories ( t * testing . T ) {
configPath := writeAppConfigFile ( t , `
prompt_dir : . / from - config / prompts
profile_dir : . / from - config / profiles
schema_dir : . / from - config / schemas
` )
cfg , err := parseRunArgs ( [ ] string {
"--config" , configPath ,
"--prompt-dir" , "./from-cli/prompts" ,
"--profile-dir" , "./from-cli/profiles" ,
"--schema-dir" , "./from-cli/schemas" ,
"--prompt" , "p" ,
"--input" , "a=b" ,
} )
if err != nil {
t . Fatalf ( "expected valid args, got %v" , err )
}
if cfg . promptDir != filepath . Clean ( "./from-cli/prompts" ) {
t . Fatalf ( "expected CLI prompt dir override, got %q" , cfg . promptDir )
}
if cfg . profileDir != filepath . Clean ( "./from-cli/profiles" ) {
t . Fatalf ( "expected CLI profile dir override, got %q" , cfg . profileDir )
}
if cfg . schemaDir != filepath . Clean ( "./from-cli/schemas" ) {
t . Fatalf ( "expected CLI schema dir override, got %q" , cfg . schemaDir )
}
}
2026-09-26 18:08:36 +00:00
func TestApplicationFlagsPreserveExplicitEmptyValues ( t * testing . T ) {
configPath := writeAppConfigFile ( t , ` prompt_dir : . / configured / prompts
profile_dir : . / configured / profiles
schema_dir : . / configured / schemas
server :
addr : ":9090"
artifact_root : . / configured / artifacts
` )
if _ , err := parseRunArgs ( [ ] string { "--config" , configPath , "--prompt" , "p" , "--prompt-dir=" } ) ; err == nil || ! strings . Contains ( err . Error ( ) , "prompt directory is required" ) {
t . Fatalf ( "explicit empty prompt directory restored lower value: %v" , err )
}
render , err := parseRenderArgs ( [ ] string { "--config" , configPath , "--prompt" , "p" , "--profile-dir=" , "--schema-dir=" } )
if err != nil || render . profileDir != "" || render . schemaDir != "" {
t . Fatalf ( "render did not preserve explicit empty sources: cfg=%+v err=%v" , render , err )
}
serve , err := parseServeArgs ( [ ] string { "--config" , configPath , "--addr=" , "--artifact-root=" } )
if err != nil || serve . addr != "" || serve . artifactRoot != "" {
t . Fatalf ( "serve did not preserve explicit empty settings: cfg=%+v err=%v" , serve , err )
}
prompt , err := parsePromptInspectionArgs ( [ ] string { "--config" , configPath , "--prompt" , "p" , "--prompt-dir=" } )
if err != nil {
t . Fatal ( err )
}
if _ , err := resolveAppSettingsForPromptInspection ( prompt ) ; err == nil || ! strings . Contains ( err . Error ( ) , "prompt directory is required" ) {
t . Fatalf ( "prompt inspection restored lower directory: %v" , err )
}
profile , err := parseProfileInspectionArgs ( [ ] string { "--config" , configPath , "--profile" , "p" , "--profile-dir=" } )
if err != nil {
t . Fatal ( err )
}
settings , err := resolveAppSettingsForProfileInspection ( profile )
if err != nil || settings . profileDir != "" {
t . Fatalf ( "profile inspection required a source or restored lower value: settings=%+v err=%v" , settings , err )
}
}
func TestApplicationLoadUsesNativeYAMLAndSafeDiagnostics ( t * testing . T ) {
for _ , body := range [ ] string {
"" ,
"server: null\n" ,
"server:\n addr: null\n" ,
"server:\n unknown: 1\n" ,
"prompt_dir: &alias ./prompts\n" ,
"prompt_dir: ./prompts\n---\nprofile_dir: ./profiles\n" ,
} {
path := writeAppConfigFile ( t , body )
_ , err := parseRunArgs ( [ ] string { "--config" , path , "--prompt-dir" , "./prompts" , "--prompt" , "p" } )
var loadError * configkit . LoadError
if ! errors . As ( err , & loadError ) {
t . Fatalf ( "native YAML failure %q lost structured diagnostics: %v" , body , err )
}
}
const rejected = "sensitive-synthetic-marker"
path := writeAppConfigFile ( t , "defaults:\n render_format: " + rejected + "\n" )
code , stdout , stderr := runCLICommand ( t , renderCommand , [ ] string { "--config" , path , "--prompt-dir" , "./prompts" , "--prompt" , "p" } )
if code != ExitRuntimeError || stdout != "" || ! strings . Contains ( stderr , "validate.error" ) || strings . Contains ( stderr , rejected ) {
t . Fatalf ( "unsafe or misclassified load diagnostic: code=%d stdout=%q stderr=%q" , code , stdout , stderr )
}
}
type failingDiagnosticWriter struct { }
func ( failingDiagnosticWriter ) Write ( [ ] byte ) ( int , error ) {
return 0 , errors . New ( "diagnostic sink unavailable" )
}
func TestApplicationDiagnosticWriterFailure ( t * testing . T ) {
diagnostics := [ ] configkit . Diagnostic { { Code : "test.error" , Message : "safe diagnostic" } }
if err := writeApplicationWarnings ( failingDiagnosticWriter { } , diagnostics ) ; err == nil {
t . Fatal ( "expected diagnostic writer failure" )
}
path := writeAppConfigFile ( t , "unknown_field: secret-value\n" )
if code := runCommand ( [ ] string { "--config" , path , "--prompt" , "sample" } , io . Discard , failingDiagnosticWriter { } ) ; code != ExitRuntimeError {
t . Fatalf ( "diagnostic writer failure must retain runtime error exit status, got %d" , code )
}
}
2026-05-06 16:46:33 +00:00
func TestParseRenderArgsWithExplicitConfigLoadsDirectoriesAndFormat ( t * testing . T ) {
configPath := writeAppConfigFile ( t , `
prompt_dir : . / from - config / prompts
profile_dir : . / from - config / profiles
defaults :
render_format : json
` )
cfg , err := parseRenderArgs ( [ ] string {
"--config" , configPath ,
"--prompt" , "p" ,
"--input" , "a=b" ,
} )
if err != nil {
t . Fatalf ( "expected valid args, got %v" , err )
}
if cfg . promptDir != filepath . Clean ( "./from-config/prompts" ) {
t . Fatalf ( "expected prompt dir from config, got %q" , cfg . promptDir )
}
if cfg . profileDir != filepath . Clean ( "./from-config/profiles" ) {
t . Fatalf ( "expected profile dir from config, got %q" , cfg . profileDir )
}
if cfg . outputFormat != renderformat . PreparedRunFormatJSON {
t . Fatalf ( "expected render format from config, got %q" , cfg . outputFormat )
}
}
2026-05-26 13:12:34 +00:00
func TestParseRenderArgsExplicitFormatOverridesConfigDefaultFormat ( t * testing . T ) {
configPath := writeAppConfigFile ( t , `
prompt_dir : . / from - config / prompts
profile_dir : . / from - config / profiles
defaults :
render_format : json
` )
cfg , err := parseRenderArgs ( [ ] string {
"--config" , configPath ,
"--prompt" , "p" ,
"--input" , "a=b" ,
"--format" , "text" ,
} )
if err != nil {
t . Fatalf ( "expected valid args, got %v" , err )
}
if cfg . outputFormat != renderformat . PreparedRunFormatText {
t . Fatalf ( "expected explicit --format text to override config default, got %q" , cfg . outputFormat )
}
}
2026-05-06 16:46:33 +00:00
func TestParseServeArgsWithExplicitConfigLoadsSettingsAndCLIAddrOverrides ( t * testing . T ) {
configPath := writeAppConfigFile ( t , `
prompt_dir : . / from - config / prompts
profile_dir : . / from - config / profiles
schema_dir : . / from - config / schemas
server :
addr : 127.0 .0 .1 : 9000
2026-07-04 23:34:44 +00:00
artifact_root : . / from - config / artifacts
2026-07-05 00:17:07 +00:00
max_request_bytes : 1024
max_artifact_bytes : 2048
max_response_bytes : 4096
2026-05-06 16:46:33 +00:00
` )
cfg , err := parseServeArgs ( [ ] string {
"--config" , configPath ,
"--addr" , ":7777" ,
2026-07-04 23:34:44 +00:00
"--artifact-root" , "./from-cli/artifacts" ,
2026-07-05 00:17:07 +00:00
"--max-request-bytes" , "0" ,
"--max-artifact-bytes" , "8192" ,
"--max-response-bytes" , "16384" ,
2026-05-06 16:46:33 +00:00
} )
if err != nil {
t . Fatalf ( "expected valid args, got %v" , err )
}
if cfg . promptDir != filepath . Clean ( "./from-config/prompts" ) {
t . Fatalf ( "expected prompt dir from config, got %q" , cfg . promptDir )
}
if cfg . profileDir != filepath . Clean ( "./from-config/profiles" ) {
t . Fatalf ( "expected profile dir from config, got %q" , cfg . profileDir )
}
if cfg . schemaDir != filepath . Clean ( "./from-config/schemas" ) {
t . Fatalf ( "expected schema dir from config, got %q" , cfg . schemaDir )
}
if cfg . addr != ":7777" {
t . Fatalf ( "expected CLI addr override, got %q" , cfg . addr )
}
2026-07-04 23:34:44 +00:00
if cfg . artifactRoot != filepath . Clean ( "./from-cli/artifacts" ) {
t . Fatalf ( "expected CLI artifact root override, got %q" , cfg . artifactRoot )
}
2026-07-05 00:17:07 +00:00
if cfg . maxRequestBytes != 0 {
t . Fatalf ( "expected CLI max request bytes override, got %d" , cfg . maxRequestBytes )
}
if cfg . maxArtifactBytes != 8192 {
t . Fatalf ( "expected CLI max artifact bytes override, got %d" , cfg . maxArtifactBytes )
}
if cfg . maxResponseBytes != 16384 {
t . Fatalf ( "expected CLI max response bytes override, got %d" , cfg . maxResponseBytes )
}
2026-05-06 16:46:33 +00:00
}
2026-05-06 16:48:35 +00:00
func TestParseServeArgsWithConfigProvidesRequiredDirectoriesAndAddr ( t * testing . T ) {
configPath := writeAppConfigFile ( t , `
prompt_dir : . / from - config / prompts
profile_dir : . / from - config / profiles
schema_dir : . / from - config / schemas
server :
addr : 127.0 .0 .1 : 9000
2026-07-04 23:34:44 +00:00
artifact_root : . / from - config / artifacts
2026-07-05 00:17:07 +00:00
max_request_bytes : 1024
max_artifact_bytes : 2048
max_response_bytes : 4096
2026-05-06 16:48:35 +00:00
` )
cfg , err := parseServeArgs ( [ ] string {
"--config" , configPath ,
} )
if err != nil {
t . Fatalf ( "expected valid args, got %v" , err )
}
if cfg . promptDir != filepath . Clean ( "./from-config/prompts" ) {
t . Fatalf ( "expected prompt dir from config, got %q" , cfg . promptDir )
}
if cfg . profileDir != filepath . Clean ( "./from-config/profiles" ) {
t . Fatalf ( "expected profile dir from config, got %q" , cfg . profileDir )
}
if cfg . schemaDir != filepath . Clean ( "./from-config/schemas" ) {
t . Fatalf ( "expected schema dir from config, got %q" , cfg . schemaDir )
}
if cfg . addr != "127.0.0.1:9000" {
t . Fatalf ( "expected addr from config, got %q" , cfg . addr )
}
2026-07-04 23:34:44 +00:00
if cfg . artifactRoot != filepath . Clean ( "./from-config/artifacts" ) {
t . Fatalf ( "expected artifact root from config, got %q" , cfg . artifactRoot )
}
2026-07-05 00:17:07 +00:00
if cfg . maxRequestBytes != 1024 {
t . Fatalf ( "expected max request bytes from config, got %d" , cfg . maxRequestBytes )
}
if cfg . maxArtifactBytes != 2048 {
t . Fatalf ( "expected max artifact bytes from config, got %d" , cfg . maxArtifactBytes )
}
if cfg . maxResponseBytes != 4096 {
t . Fatalf ( "expected max response bytes from config, got %d" , cfg . maxResponseBytes )
}
}
func TestParseServeArgsRejectsNegativeSizeLimits ( t * testing . T ) {
tests := [ ] struct {
name string
flag string
} {
{ name : "request" , flag : "--max-request-bytes" } ,
{ name : "artifact" , flag : "--max-artifact-bytes" } ,
{ name : "response" , flag : "--max-response-bytes" } ,
}
for _ , tc := range tests {
t . Run ( tc . name , func ( t * testing . T ) {
_ , err := parseServeArgs ( [ ] string {
"--prompt-dir" , "./prompts" ,
tc . flag , "-1" ,
} )
if err == nil {
t . Fatal ( "expected negative size limit error" )
}
} )
}
}
func TestRunAndRenderRejectServeSizeLimitFlags ( t * testing . T ) {
for _ , tc := range [ ] struct {
name string
parse func ( [ ] string ) error
} {
{
name : "run" ,
parse : func ( args [ ] string ) error {
_ , err := parseRunArgs ( args )
return err
} ,
} ,
{
name : "render" ,
parse : func ( args [ ] string ) error {
_ , err := parseRenderArgs ( args )
return err
} ,
} ,
} {
t . Run ( tc . name , func ( t * testing . T ) {
err := tc . parse ( [ ] string {
"--prompt-dir" , "./prompts" ,
"--prompt" , "p" ,
"--input" , "a=b" ,
"--max-request-bytes" , "1024" ,
} )
if err == nil {
t . Fatal ( "expected unsupported flag error" )
}
} )
}
2026-05-06 16:48:35 +00:00
}
2026-05-26 13:12:34 +00:00
func TestRunAndRenderBuildEquivalentRuntimeOverrideRequestsForSharedFlags ( t * testing . T ) {
runCfg , err := parseRunArgs ( [ ] string {
"--prompt-dir" , "./prompts" ,
"--profile-dir" , "./profiles" ,
"--prompt" , "prompt-1" ,
"--profile" , "profile-1" ,
"--input" , "transcript=./transcript.md" ,
"--var" , "session_date=2026-05-01" ,
"--llm-base-url" , "http://localhost:8000/v1" ,
"--model" , "model-x" ,
"--temperature" , "0.8" ,
"--max-tokens" , "123" ,
"--top-p" , "0.6" ,
"--timeout" , "90s" ,
"--api-key-env" , "SCRIPTORIUM_API_KEY" ,
} )
if err != nil {
t . Fatalf ( "expected valid run args, got %v" , err )
}
renderCfg , err := parseRenderArgs ( [ ] string {
"--prompt-dir" , "./prompts" ,
"--profile-dir" , "./profiles" ,
"--prompt" , "prompt-1" ,
"--profile" , "profile-1" ,
"--input" , "transcript=./transcript.md" ,
"--var" , "session_date=2026-05-01" ,
"--llm-base-url" , "http://localhost:8000/v1" ,
"--model" , "model-x" ,
"--temperature" , "0.8" ,
"--max-tokens" , "123" ,
"--top-p" , "0.6" ,
"--timeout" , "90s" ,
"--api-key-env" , "SCRIPTORIUM_API_KEY" ,
} )
if err != nil {
t . Fatalf ( "expected valid render args, got %v" , err )
}
runReq , err := buildRunRequestFromConfig ( runCfg )
if err != nil {
t . Fatalf ( "expected run request build success, got %v" , err )
}
renderReq , err := buildRunRequestFromConfig ( & renderCfg . runConfig )
if err != nil {
t . Fatalf ( "expected render request build success, got %v" , err )
}
if ! reflect . DeepEqual ( runReq , renderReq ) {
t . Fatalf ( "expected run/render shared flag requests to match.\nrun=%#v\nrender=%#v" , runReq , renderReq )
}
}
2026-07-28 00:45:59 +00:00
func TestBuildRunRequestPreservesNumericOverridePresence ( t * testing . T ) {
omitted , err := buildRunRequestFromConfig ( & runConfig {
promptID : "prompt-1" ,
inputRaw : [ ] string { "transcript=./transcript.md" } ,
} )
if err != nil {
t . Fatalf ( "expected omitted override request to build, got %v" , err )
}
if omitted . Execution != nil {
t . Fatalf ( "expected omitted numeric flags to leave execution override nil, got %#v" , omitted . Execution )
}
explicitZeros , err := buildRunRequestFromConfig ( & runConfig {
promptID : "prompt-1" ,
inputRaw : [ ] string { "transcript=./transcript.md" } ,
temperatureSet : true ,
maxTokensSet : true ,
topPSet : true ,
timeoutSet : true ,
} )
if err != nil {
t . Fatalf ( "expected explicit zero override request to build, got %v" , err )
}
if explicitZeros . Execution == nil {
t . Fatal ( "expected explicit numeric flags to create execution override" )
}
if explicitZeros . Execution . Temperature == nil || explicitZeros . Execution . MaxTokens == nil || explicitZeros . Execution . TopP == nil || explicitZeros . Execution . TimeoutSeconds == nil {
t . Fatalf ( "expected explicit zero numeric overrides to remain non-nil, got %#v" , explicitZeros . Execution )
}
if * explicitZeros . Execution . Temperature != 0 || * explicitZeros . Execution . MaxTokens != 0 || * explicitZeros . Execution . TopP != 0 || * explicitZeros . Execution . TimeoutSeconds != 0 {
t . Fatalf ( "expected explicit numeric overrides to retain zero values, got %#v" , explicitZeros . Execution )
}
}
2026-08-29 14:22:45 +00:00
func TestBuildRunRequestPreservesReasoningEffortPresenceAndSessionID ( t * testing . T ) {
omitted , err := buildRunRequestFromConfig ( & runConfig { promptID : "prompt-1" } )
if err != nil {
t . Fatalf ( "expected omitted request to build, got %v" , err )
}
if omitted . Execution != nil {
t . Fatalf ( "expected omitted reasoning flag to leave execution nil, got %#v" , omitted . Execution )
}
for _ , tc := range [ ] struct {
name string
value string
} {
{ name : "replacement" , value : "high" } ,
{ name : "clear" , value : "" } ,
} {
t . Run ( tc . name , func ( t * testing . T ) {
req , err := buildRunRequestFromConfig ( & runConfig {
promptID : "prompt-1" ,
sessionID : "session-1" ,
reasoningEffort : tc . value ,
reasoningEffortSet : true ,
} )
if err != nil {
t . Fatalf ( "expected request to build, got %v" , err )
}
if req . SessionID != "session-1" || req . Execution == nil || req . Execution . ReasoningEffort == nil || * req . Execution . ReasoningEffort != tc . value {
t . Fatalf ( "unexpected mapped request: %#v" , req )
}
} )
}
}
2026-08-29 14:12:51 +00:00
func TestBuildRunRequestAllowsOmittedInputsAndMapsPromptVersion ( t * testing . T ) {
req , err := buildRunRequestFromConfig ( & runConfig {
promptID : "prompt-1" ,
promptVersion : "2" ,
} )
if err != nil {
t . Fatalf ( "expected request without inputs to build, got %v" , err )
}
if req . PromptVersion != "2" {
t . Fatalf ( "expected prompt version to be mapped, got %q" , req . PromptVersion )
}
if req . Inputs != nil {
t . Fatalf ( "expected omitted inputs to remain nil, got %#v" , req . Inputs )
}
}
2026-05-06 16:48:35 +00:00
func TestParseRunArgsFailsClearlyWhenNoEffectivePromptDir ( t * testing . T ) {
configPath := writeAppConfigFile ( t , `
profile_dir : . / profiles
` )
_ , err := parseRunArgs ( [ ] string {
"--config" , configPath ,
"--prompt" , "p" ,
"--input" , "a=b" ,
} )
if err == nil {
t . Fatal ( "expected missing prompt_dir error" )
}
if ! strings . Contains ( err . Error ( ) , "prompt directory is required" ) || ! strings . Contains ( err . Error ( ) , "config.yml prompt_dir" ) {
t . Fatalf ( "expected clear prompt_dir guidance, got %v" , err )
}
}
2026-07-04 16:48:18 +00:00
func TestParseRunArgsAcceptsMissingEffectiveProfileDir ( t * testing . T ) {
2026-05-06 16:48:35 +00:00
configPath := writeAppConfigFile ( t , `
prompt_dir : . / prompts
` )
2026-07-04 16:48:18 +00:00
cfg , err := parseRunArgs ( [ ] string {
2026-05-06 16:48:35 +00:00
"--config" , configPath ,
"--prompt" , "p" ,
"--input" , "a=b" ,
} )
2026-07-04 16:48:18 +00:00
if err != nil {
t . Fatalf ( "expected missing profile_dir to be accepted, got %v" , err )
2026-05-06 16:48:35 +00:00
}
2026-07-04 16:48:18 +00:00
if cfg . profileDir != "" {
t . Fatalf ( "expected empty profile dir for built-ins, got %q" , cfg . profileDir )
2026-05-06 16:48:35 +00:00
}
}
func TestParseRenderArgsFailsClearlyWhenNoEffectivePromptDir ( t * testing . T ) {
configPath := writeAppConfigFile ( t , `
profile_dir : . / profiles
` )
_ , err := parseRenderArgs ( [ ] string {
"--config" , configPath ,
"--prompt" , "p" ,
"--input" , "a=b" ,
} )
if err == nil {
t . Fatal ( "expected missing prompt_dir error" )
}
if ! strings . Contains ( err . Error ( ) , "prompt directory is required" ) || ! strings . Contains ( err . Error ( ) , "config.yml prompt_dir" ) {
t . Fatalf ( "expected clear prompt_dir guidance, got %v" , err )
}
}
2026-07-04 16:48:18 +00:00
func TestParseRenderArgsAcceptsMissingEffectiveProfileDir ( t * testing . T ) {
2026-05-06 16:48:35 +00:00
configPath := writeAppConfigFile ( t , `
prompt_dir : . / prompts
` )
2026-07-04 16:48:18 +00:00
cfg , err := parseRenderArgs ( [ ] string {
2026-05-06 16:48:35 +00:00
"--config" , configPath ,
"--prompt" , "p" ,
"--input" , "a=b" ,
} )
2026-07-04 16:48:18 +00:00
if err != nil {
t . Fatalf ( "expected missing profile_dir to be accepted, got %v" , err )
2026-05-06 16:48:35 +00:00
}
2026-07-04 16:48:18 +00:00
if cfg . profileDir != "" {
t . Fatalf ( "expected empty profile dir for built-ins, got %q" , cfg . profileDir )
2026-05-06 16:48:35 +00:00
}
}
2026-05-04 21:08:40 -05:00
func TestDetermineExitCode ( t * testing . T ) {
if got := determineExitCode ( errors . New ( "boom" ) , nil ) ; got != ExitRuntimeError {
t . Fatalf ( "expected runtime exit code, got %d" , got )
}
2026-07-28 14:04:29 +00:00
if got := determineExitCode ( nil , & promptkit . RunResult { Validation : promptkit . ValidationResult { Status : promptkit . ValidationFailed } } ) ; got != ExitValidationFailed {
2026-05-04 21:08:40 -05:00
t . Fatalf ( "expected validation exit code, got %d" , got )
}
2026-07-28 14:04:29 +00:00
if got := determineExitCode ( nil , & promptkit . RunResult { Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed } } ) ; got != ExitOK {
2026-05-04 21:08:40 -05:00
t . Fatalf ( "expected success exit code for passed validation, got %d" , got )
}
2026-07-28 14:04:29 +00:00
if got := determineExitCode ( nil , & promptkit . RunResult { Validation : promptkit . ValidationResult { Status : promptkit . ValidationSkipped } } ) ; got != ExitOK {
2026-05-04 21:08:40 -05:00
t . Fatalf ( "expected success exit code for skipped validation, got %d" , got )
}
}
2026-05-04 22:38:20 -05:00
func TestRunCommandVarsOptional ( t * testing . T ) {
var stdout bytes . Buffer
var stderr bytes . Buffer
code := runCommand ( [ ] string {
2026-05-05 10:58:22 -05:00
"--prompt-dir" , "./prompts" ,
2026-05-04 22:38:20 -05:00
"--profile-dir" , "./profiles" ,
2026-05-05 10:49:33 -05:00
"--prompt" , "p" ,
2026-05-04 22:38:20 -05:00
"--input" , "transcript=./t.md" ,
2026-05-05 20:01:45 -05:00
"--llm-base-url" , "http://[::1" ,
2026-05-04 22:38:20 -05:00
"--model" , "m" ,
} , & stdout , & stderr )
if code != ExitRuntimeError {
t . Fatalf ( "expected runtime error exit code, got %d" , code )
}
if strings . Contains ( stderr . String ( ) , "var parse error" ) {
t . Fatalf ( "expected --var to be optional, got stderr=%q" , stderr . String ( ) )
}
2026-05-05 20:01:45 -05:00
if ! strings . Contains ( stderr . String ( ) , "llm client error" ) && ! strings . Contains ( stderr . String ( ) , "run error" ) {
t . Fatalf ( "expected post-parse execution error, got stderr=%q" , stderr . String ( ) )
2026-05-04 22:38:20 -05:00
}
2026-05-05 08:41:07 -05:00
if stdout . Len ( ) != 0 {
t . Fatalf ( "expected no stdout output on error, got %q" , stdout . String ( ) )
}
}
2026-05-06 16:48:35 +00:00
func TestRunCommandSucceedsWithPromptAndProfileDirsFromConfig ( t * testing . T ) {
2026-05-26 13:20:07 +00:00
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello" )
2026-05-06 16:48:35 +00:00
ts := newTestLLMServer ( "from-config-dirs" , nil )
defer ts . Close ( )
2026-05-26 13:20:07 +00:00
writePromptFile ( t , lib . promptDir , "prompt.default" , "local-default" )
writeProfileFile ( t , lib . profileDir , "local-default" , ts . URL + "/v1" , "profile-model" )
2026-05-06 16:48:35 +00:00
configPath := writeAppConfigFile ( t , fmt . Sprintf ( `
prompt_dir : % s
profile_dir : % s
2026-05-26 13:20:07 +00:00
` , lib . promptDir , lib . profileDir ) )
2026-05-06 16:48:35 +00:00
2026-05-26 13:20:07 +00:00
code , stdout , stderr := runCLICommand ( t , runCommand , [ ] string {
2026-05-06 16:48:35 +00:00
"--config" , configPath ,
"--prompt" , "prompt.default" ,
"--input" , "transcript=" + inputPath ,
2026-05-26 13:20:07 +00:00
} )
2026-05-06 16:48:35 +00:00
if code != ExitOK {
2026-05-26 13:20:07 +00:00
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
2026-05-06 16:48:35 +00:00
}
2026-05-26 13:20:07 +00:00
if stdout != "from-config-dirs" {
t . Fatalf ( "unexpected stdout output: %q" , stdout )
2026-05-06 16:48:35 +00:00
}
}
2026-05-06 15:20:22 +00:00
func TestRenderCommandDefaultFormatTextIncludesPreparedDetailsAndNoSecrets ( t * testing . T ) {
const envName = "SCRIPTORIUM_RENDER_TEST_API_KEY"
const secret = "super-secret-render-key"
t . Setenv ( envName , secret )
2026-05-26 13:20:07 +00:00
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello transcript" )
2026-05-06 15:20:22 +00:00
2026-05-26 13:20:07 +00:00
writePromptFileWithTemplate ( t , lib . promptDir , "prompt.render" , "local-default" , "Date {{.session_date}} - Summarize: {{input \"transcript\"}}" )
writeProfileFile ( t , lib . profileDir , "local-default" , "http://127.0.0.1:1/v1" , "profile-model" )
2026-05-06 15:20:22 +00:00
2026-05-26 13:20:07 +00:00
code , stdout , stderr := runCLICommand ( t , renderCommand , [ ] string {
"--prompt-dir" , lib . promptDir ,
"--profile-dir" , lib . profileDir ,
2026-05-06 15:20:22 +00:00
"--prompt" , "prompt.render" ,
"--profile" , "local-default" ,
"--input" , "transcript=" + inputPath ,
"--var" , "session_date=2026-05-04" ,
"--llm-base-url" , "http://override.local/v1" ,
"--model" , "override-model" ,
"--temperature" , "0.7" ,
"--max-tokens" , "55" ,
"--top-p" , "0.2" ,
"--timeout" , "20s" ,
"--api-key-env" , envName ,
2026-05-26 13:20:07 +00:00
} )
2026-05-06 15:20:22 +00:00
if code != ExitOK {
2026-05-26 13:20:07 +00:00
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
2026-05-06 15:20:22 +00:00
}
2026-05-26 13:20:07 +00:00
if stderr != "" {
t . Fatalf ( "expected empty stderr on success, got %q" , stderr )
2026-05-06 15:20:22 +00:00
}
2026-05-26 13:20:07 +00:00
out := stdout
2026-05-06 15:20:22 +00:00
for _ , want := range [ ] string {
"prompt: prompt.render" ,
"selected_profile_id: local-default" ,
"endpoint: http://override.local/v1" ,
"model: override-model" ,
"temperature: 0.7" ,
"max_tokens: 55" ,
"top_p: 0.2" ,
"timeout_seconds: 20" ,
"api_key_env: " + envName ,
"rendered_prompt_hash:" ,
"messages:" ,
"Date 2026-05-04" ,
"Summarize:" ,
"hello transcript" ,
} {
if ! strings . Contains ( out , want ) {
t . Fatalf ( "expected render text output to include %q, got:\n%s" , want , out )
}
}
if strings . Contains ( out , secret ) {
t . Fatalf ( "render output unexpectedly contained secret api key value: %s" , out )
}
}
2026-07-04 13:20:39 +00:00
func TestRenderCommandExplicitZeroTemperatureReachesEffectiveSettings ( t * testing . T ) {
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello transcript" )
writePromptFile ( t , lib . promptDir , "prompt.render" , "local-default" )
profile := ` id : local - default
endpoint : http : //127.0.0.1:1/v1
model : profile - model
temperature : 0.7
`
if err := os . WriteFile ( filepath . Join ( lib . profileDir , "local-default.yaml" ) , [ ] byte ( profile ) , 0 o644 ) ; err != nil {
t . Fatalf ( "failed to write profile fixture: %v" , err )
}
code , stdout , stderr := runCLICommand ( t , renderCommand , [ ] string {
"--prompt-dir" , lib . promptDir ,
"--profile-dir" , lib . profileDir ,
"--prompt" , "prompt.render" ,
"--input" , "transcript=" + inputPath ,
"--temperature" , "0" ,
} )
if code != ExitOK {
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
}
if ! strings . Contains ( stdout , "\n temperature: 0\n" ) {
t . Fatalf ( "expected explicit zero temperature in effective settings, got:\n%s" , stdout )
}
}
2026-05-06 16:48:35 +00:00
func TestRenderCommandSucceedsWithPromptAndProfileDirsFromConfig ( t * testing . T ) {
2026-05-26 13:20:07 +00:00
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello transcript" )
2026-05-06 16:48:35 +00:00
2026-05-26 13:20:07 +00:00
writePromptFile ( t , lib . promptDir , "prompt.render" , "local-default" )
writeProfileFile ( t , lib . profileDir , "local-default" , "http://127.0.0.1:1/v1" , "profile-model" )
2026-05-06 16:48:35 +00:00
configPath := writeAppConfigFile ( t , fmt . Sprintf ( `
prompt_dir : % s
profile_dir : % s
2026-05-26 13:20:07 +00:00
` , lib . promptDir , lib . profileDir ) )
2026-05-06 16:48:35 +00:00
2026-05-26 13:20:07 +00:00
code , stdout , stderr := runCLICommand ( t , renderCommand , [ ] string {
2026-05-06 16:48:35 +00:00
"--config" , configPath ,
"--prompt" , "prompt.render" ,
"--input" , "transcript=" + inputPath ,
2026-05-26 13:20:07 +00:00
} )
2026-05-06 16:48:35 +00:00
if code != ExitOK {
2026-05-26 13:20:07 +00:00
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
2026-05-06 16:48:35 +00:00
}
2026-05-26 13:20:07 +00:00
if ! strings . Contains ( stdout , "prompt: prompt.render" ) {
t . Fatalf ( "expected rendered output, got %q" , stdout )
2026-05-06 16:48:35 +00:00
}
}
2026-09-26 18:12:27 +00:00
func TestRenderCommandLoadsImportsAndOverlayWithoutChangingExecutionProfile ( t * testing . T ) {
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello transcript" )
writePromptFile ( t , lib . promptDir , "prompt.render" , "local-default" )
writeProfileFile ( t , lib . profileDir , "local-default" , "http://127.0.0.1:1/v1" , "profile-model" )
if err := os . MkdirAll ( filepath . Join ( lib . rootDir , "fragments" ) , 0 o755 ) ; err != nil {
t . Fatal ( err )
}
if err := os . MkdirAll ( filepath . Join ( lib . rootDir , "overlays" ) , 0 o755 ) ; err != nil {
t . Fatal ( err )
}
base := filepath . Join ( lib . rootDir , "config.yml" )
for path , body := range map [ string ] string {
base : "configkit:\n imports:\n - path: fragments/sources.yml\n" ,
filepath . Join ( lib . rootDir , "fragments" , "sources.yml" ) : fmt . Sprintf ( "prompt_dir: %q\nprofile_dir: %q\n" , lib . promptDir , lib . profileDir ) ,
filepath . Join ( lib . rootDir , "overlays" , "local.yml" ) : "defaults:\n render_format: json\n" ,
} {
if err := os . WriteFile ( path , [ ] byte ( body ) , 0 o644 ) ; err != nil {
t . Fatal ( err )
}
}
args := [ ] string { "--config" , base , "--config-overlay" , "local" , "--profile" , "local-default" , "--prompt" , "prompt.render" , "--input" , "transcript=" + inputPath }
cfg , err := parseRenderArgs ( args )
if err != nil {
t . Fatal ( err )
}
if cfg . profileID != "local-default" || cfg . outputFormat != renderformat . PreparedRunFormatJSON {
t . Fatalf ( "overlay changed execution profile or output selection: %+v" , cfg )
}
code , stdout , stderr := runCLICommand ( t , renderCommand , args )
if code != ExitOK || ! strings . Contains ( stdout , ` "selected_profile_id": "local-default" ` ) {
t . Fatalf ( "render did not use composed config and independent execution profile: code=%d out=%q err=%q" , code , stdout , stderr )
}
}
2026-08-29 14:18:29 +00:00
func TestRenderCommandUsesConfiguredCustomBackend ( t * testing . T ) {
lib := newCLITestLibrary ( t )
writePromptDefinition ( t , lib . promptDir , "custom.yaml" , ` id : custom
version : "1"
default_profile : local - gpu
messages :
- role : user
content : "hello"
output :
format : text
validation_mode : none
` )
if err := os . WriteFile ( filepath . Join ( lib . profileDir , "local-gpu.yaml" ) , [ ] byte ( ` id : local - gpu
backend : local - gpu
model : local - model
` ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write profile fixture: %v" , err )
}
configPath := writeAppConfigFile ( t , fmt . Sprintf ( `
prompt_dir : % s
profile_dir : % s
backends :
local - gpu :
endpoint : http : //localhost:11434/v1
extra_params :
provider_option : enabled
concurrency_limit : 2
queue_capacity : 0
` , lib . promptDir , lib . profileDir ) )
2026-08-29 14:26:04 +00:00
code , stdout , stderr := runCLICommand ( t , renderCommand , [ ] string {
2026-08-29 14:18:29 +00:00
"--config" , configPath ,
"--prompt" , "custom" ,
} )
if code != ExitOK {
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
}
2026-08-29 14:26:04 +00:00
if ! strings . Contains ( stdout , "selected_backend_id: local-gpu" ) {
t . Fatalf ( "expected configured backend in prepared output, got:\n%s" , stdout )
}
2026-08-29 14:18:29 +00:00
}
2026-09-26 18:21:38 +00:00
func TestConfiguredBackendPreservesIntegerParameterAndOptionalCredential ( t * testing . T ) {
lib := newCLITestLibrary ( t )
writePromptDefinition ( t , lib . promptDir , "custom.yaml" , ` id : custom
version : "1"
default_profile : local - gpu
messages :
- role : user
content : "hello"
output :
format : text
validation_mode : none
` )
if err := os . WriteFile ( filepath . Join ( lib . profileDir , "local-gpu.yaml" ) , [ ] byte ( "id: local-gpu\nbackend: local-gpu\nmodel: local-model\n" ) , 0 o644 ) ; err != nil {
t . Fatal ( err )
}
const credentialName = "SCRIPTORIUM_OPTIONAL_BACKEND_KEY"
t . Setenv ( credentialName , "" )
type observedRequest struct {
number json . Number
authorization string
}
observed := make ( chan observedRequest , 1 )
provider := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
request := observedRequest { authorization : r . Header . Get ( "Authorization" ) }
var body map [ string ] any
decoder := json . NewDecoder ( r . Body )
decoder . UseNumber ( )
if err := decoder . Decode ( & body ) ; err == nil {
request . number , _ = body [ "synthetic_count" ] . ( json . Number )
}
observed <- request
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ , _ = w . Write ( [ ] byte ( ` { "choices":[ { "message": { "role":"assistant","content":"generated"}}]} ` ) )
} ) )
defer provider . Close ( )
configPath := writeAppConfigFile ( t , fmt . Sprintf ( ` prompt_dir : % q
profile_dir : % q
backends :
local - gpu :
endpoint : % q
api_key_env : % s
extra_params :
synthetic_count : 7
` , lib . promptDir , lib . profileDir , provider . URL + "/v1" , credentialName ) )
cfg , err := parseRunArgs ( [ ] string { "--config" , configPath , "--prompt" , "custom" } )
if err != nil || len ( cfg . backends ) != 1 || cfg . backends [ 0 ] . ExtraParams [ "synthetic_count" ] != int64 ( 7 ) {
t . Fatalf ( "ConfigKit integer did not reach runtime settings: cfg=%+v err=%v" , cfg , err )
}
code , stdout , stderr := runCLICommand ( t , runCommand , [ ] string { "--config" , configPath , "--prompt" , "custom" } )
if code != ExitOK {
t . Fatalf ( "backend run failed: code=%d out=%q err=%q" , code , stdout , stderr )
}
request := <- observed
if ! strings . Contains ( stdout , "generated" ) || request . number != "7" || request . authorization != "" {
t . Fatalf ( "backend request changed: code=%d out=%q err=%q count=%q auth=%q" , code , stdout , stderr , request . number , request . authorization )
}
}
2026-08-29 14:22:45 +00:00
func TestRenderCommandMapsReasoningEffortAndSessionID ( t * testing . T ) {
lib := newCLITestLibrary ( t )
writePromptDefinition ( t , lib . promptDir , "session.yaml" , ` id : session
version : "1"
default_profile : local
session_id : definition - session
messages :
- role : user
content : "hello"
output :
format : text
validation_mode : none
` )
if err := os . WriteFile ( filepath . Join ( lib . profileDir , "local.yaml" ) , [ ] byte ( ` id : local
endpoint : http : //127.0.0.1:1/v1
model : local - model
reasoning_effort : low
` ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write profile fixture: %v" , err )
}
for _ , tc := range [ ] struct {
name string
args [ ] string
wantReasoning string
wantSessionID string
absentReasoning bool
} {
{ name : "omitted reasoning inherits profile" , wantReasoning : "low" , wantSessionID : "definition-session" } ,
{ name : "nonblank reasoning replaces profile" , args : [ ] string { "--reasoning-effort" , "high" } , wantReasoning : "high" , wantSessionID : "definition-session" } ,
{ name : "empty reasoning clears profile" , args : [ ] string { "--reasoning-effort=" } , wantSessionID : "definition-session" , absentReasoning : true } ,
{ name : "direct session replaces definition" , args : [ ] string { "--session-id" , "direct-session" } , wantReasoning : "low" , wantSessionID : "direct-session" } ,
} {
t . Run ( tc . name , func ( t * testing . T ) {
args := [ ] string { "--prompt-dir" , lib . promptDir , "--profile-dir" , lib . profileDir , "--prompt" , "session" }
args = append ( args , tc . args ... )
code , stdout , stderr := runCLICommand ( t , renderCommand , args )
if code != ExitOK {
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
}
if ! strings . Contains ( stdout , "session_id: " + tc . wantSessionID ) {
t . Fatalf ( "expected session ID %q, got:\n%s" , tc . wantSessionID , stdout )
}
hasReasoning := strings . Contains ( stdout , "reasoning_effort:" )
if tc . absentReasoning {
if hasReasoning {
t . Fatalf ( "expected cleared reasoning to be omitted, got:\n%s" , stdout )
}
return
}
if ! strings . Contains ( stdout , "reasoning_effort: " + tc . wantReasoning ) {
t . Fatalf ( "expected reasoning effort %q, got:\n%s" , tc . wantReasoning , stdout )
}
} )
}
}
func TestRenderCommandOmitsEmptyEffectiveSessionID ( t * testing . T ) {
lib := newCLITestLibrary ( t )
writePromptDefinition ( t , lib . promptDir , "plain.yaml" , ` id : plain
version : "1"
default_profile : local
messages :
- role : user
content : "hello"
output :
format : text
validation_mode : none
` )
writeProfileFile ( t , lib . profileDir , "local" , "http://127.0.0.1:1/v1" , "local-model" )
code , stdout , stderr := runCLICommand ( t , renderCommand , [ ] string {
"--prompt-dir" , lib . promptDir ,
"--profile-dir" , lib . profileDir ,
"--prompt" , "plain" ,
} )
if code != ExitOK {
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
}
if strings . Contains ( stdout , "session_id:" ) {
t . Fatalf ( "expected no effective session ID, got:\n%s" , stdout )
}
}
func TestRenderCommandRejectsOverlongSessionID ( t * testing . T ) {
lib := newCLITestLibrary ( t )
writePromptDefinition ( t , lib . promptDir , "session.yaml" , ` id : session
version : "1"
default_profile : local
messages :
- role : user
content : "hello"
output :
format : text
validation_mode : none
` )
writeProfileFile ( t , lib . profileDir , "local" , "http://127.0.0.1:1/v1" , "local-model" )
code , _ , stderr := runCLICommand ( t , renderCommand , [ ] string {
"--prompt-dir" , lib . promptDir ,
"--profile-dir" , lib . profileDir ,
"--prompt" , "session" ,
"--session-id" , strings . Repeat ( "x" , 257 ) ,
} )
if code != ExitRuntimeError {
t . Fatalf ( "expected runtime error, got %d stderr=%q" , code , stderr )
}
if ! strings . Contains ( stderr , "invalid run request" ) {
t . Fatalf ( "expected invalid-request context, got %q" , stderr )
}
}
2026-08-29 14:29:14 +00:00
func TestInspectPromptCommandFormatsDefinitionWithoutProfileOrGeneration ( t * testing . T ) {
lib := newCLITestLibrary ( t )
writePromptDefinition ( t , lib . promptDir , "inspect.yaml" , ` id : inspect
version : "1"
messages :
- role : user
content : "hello"
output :
format : text
validation_mode : none
` )
code , stdout , stderr := runCLICommand ( t , inspectCommand , [ ] string { "prompt" , "--prompt-dir" , lib . promptDir , "--prompt" , "inspect" , "--format" , "json" } )
if code != ExitOK {
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
}
if ! strings . Contains ( stdout , ` "prompt_id": "inspect" ` ) || ! strings . Contains ( stdout , ` "inputs": [] ` ) {
t . Fatalf ( "unexpected inspection output: %s" , stdout )
}
code , stdout , stderr = runCLICommand ( t , inspectCommand , [ ] string { "prompt" , "--prompt-dir" , lib . promptDir , "--prompt" , "missing" } )
if code != ExitRuntimeError || stdout != "" || ! strings . Contains ( stderr , "inspect error" ) {
t . Fatalf ( "expected failed inspection without output, got code=%d stdout=%q stderr=%q" , code , stdout , stderr )
}
}
2026-08-29 15:16:04 +00:00
func TestInspectionParsersPreserveExplicitEmptyConfigPath ( t * testing . T ) {
promptConfig , err := parsePromptInspectionArgs ( [ ] string { "--config=" , "--prompt" , "fixture" } )
if err != nil {
t . Fatalf ( "parse prompt inspection: %v" , err )
}
if promptConfig . configPath != "" || ! promptConfig . configExplicit {
t . Fatalf ( "expected explicit empty prompt config path, got %+v" , promptConfig )
}
profileConfig , err := parseProfileInspectionArgs ( [ ] string { "--config=" , "--profile" , "fixture" } )
if err != nil {
t . Fatalf ( "parse profile inspection: %v" , err )
}
if profileConfig . configPath != "" || ! profileConfig . configExplicit {
t . Fatalf ( "expected explicit empty profile config path, got %+v" , profileConfig )
}
}
2026-09-26 15:24:55 +00:00
func TestPromptkitDefinitionsRenderThroughCLI ( t * testing . T ) {
fixtureRoot := definitionFixtureRoot ( t )
configPath := writeDefinitionFixtureConfig ( t , fixtureRoot , true )
2026-08-29 15:12:27 +00:00
inputPath := filepath . Join ( fixtureRoot , "inputs" , "source.md" )
code , stdout , stderr := runCLICommand ( t , renderCommand , [ ] string {
"--config" , configPath ,
"--prompt" , "compat.complete" ,
"--prompt-version" , "1.0.0" ,
"--input" , "source=" + inputPath ,
"--var" , "topic=testing" ,
"--format" , "json" ,
} )
if code != ExitOK {
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
}
var payload map [ string ] any
if err := json . Unmarshal ( [ ] byte ( stdout ) , & payload ) ; err != nil {
t . Fatalf ( "decode rendered fixture: %v\nbody=%s" , err , stdout )
}
if payload [ "prompt_version" ] != "1.0.0" || payload [ "selected_profile_id" ] != "custom-derived" || payload [ "selected_backend_id" ] != "fixture-custom" {
t . Fatalf ( "unexpected selected definition and target: %#v" , payload )
}
if payload [ "session_id" ] != "fixture-testing" {
t . Fatalf ( "expected rendered session template, got %#v" , payload [ "session_id" ] )
}
outputContract := payload [ "output_contract" ] . ( map [ string ] any )
if outputContract [ "format" ] != "json" || outputContract [ "validation_mode" ] != "json_schema" || outputContract [ "repair_attempts" ] != float64 ( 2 ) {
t . Fatalf ( "unexpected output contract: %#v" , outputContract )
}
if _ , ok := payload [ "structured_output" ] . ( map [ string ] any ) [ "json_schema" ] ; ! ok {
t . Fatalf ( "expected loaded JSON schema metadata, got %#v" , payload [ "structured_output" ] )
}
messages := payload [ "messages" ] . ( [ ] any )
wantRoles := [ ] string { "developer" , "system" , "user" , "assistant" }
for i , wantRole := range wantRoles {
message := messages [ i ] . ( map [ string ] any )
if message [ "role" ] != wantRole {
t . Fatalf ( "message %d: expected role %q, got %#v" , i , wantRole , message [ "role" ] )
}
}
cacheControl := messages [ 0 ] . ( map [ string ] any ) [ "cache_control" ] . ( map [ string ] any )
if cacheControl [ "type" ] != "ephemeral" || cacheControl [ "ttl" ] != "1h" {
t . Fatalf ( "unexpected cache control: %#v" , cacheControl )
}
if ! strings . Contains ( messages [ 2 ] . ( map [ string ] any ) [ "content" ] . ( string ) , "stable, synthetic material" ) {
t . Fatalf ( "file-backed input template was not rendered: %#v" , messages [ 2 ] )
}
inputHashes := payload [ "input_hashes" ] . ( map [ string ] any )
if len ( inputHashes ) != 1 || inputHashes [ "source" ] == "" {
t . Fatalf ( "required and omitted optional inputs were not preserved: %#v" , inputHashes )
}
}
2026-09-26 15:28:30 +00:00
func TestTransientRetrySettingsAndCLIOverride ( t * testing . T ) {
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello" )
writePromptFile ( t , lib . promptDir , "retry.prompt" , "local-default" )
var hits int32
provider := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
if atomic . AddInt32 ( & hits , 1 ) == 1 {
http . Error ( w , "temporary" , http . StatusServiceUnavailable )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ , _ = w . Write ( [ ] byte ( ` { "choices":[ { "message": { "role":"assistant","content":"recovered"}}]} ` ) )
} ) )
defer provider . Close ( )
writeProfileFile ( t , lib . profileDir , "local-default" , provider . URL + "/v1" , "local-model" )
configPath := writeAppConfigFile ( t , fmt . Sprintf ( "prompt_dir: %q\nprofile_dir: %q\ntransient_retries: 1\n" , lib . promptDir , lib . profileDir ) )
args := [ ] string { "--config" , configPath , "--prompt" , "retry.prompt" , "--input" , "transcript=" + inputPath }
serve , err := parseServeArgs ( [ ] string { "--config" , configPath } )
if err != nil || serve . transientRetries == nil || * serve . transientRetries != 1 {
t . Fatalf ( "server did not inherit retry setting: cfg=%+v err=%v" , serve , err )
}
if _ , err := newEngine ( serve . engineSettings ( ) ) ; err != nil {
t . Fatalf ( "server engine configuration: %v" , err )
}
code , stdout , stderr := runCLICommand ( t , runCommand , args )
if code != ExitOK || stdout != "recovered" || atomic . LoadInt32 ( & hits ) != 2 {
t . Fatalf ( "configured retry was not applied: code=%d stdout=%q stderr=%q hits=%d" , code , stdout , stderr , atomic . LoadInt32 ( & hits ) )
}
atomic . StoreInt32 ( & hits , 0 )
code , stdout , stderr = runCLICommand ( t , runCommand , append ( append ( [ ] string { } , args ... ) , "--transient-retries" , "0" ) )
if code != ExitRuntimeError || stdout != "" || atomic . LoadInt32 ( & hits ) != 1 {
t . Fatalf ( "explicit zero did not override configuration: code=%d stdout=%q stderr=%q hits=%d" , code , stdout , stderr , atomic . LoadInt32 ( & hits ) )
}
for _ , tc := range [ ] struct {
name string
args [ ] string
want * int
} {
{ name : "omitted" , args : args } ,
{ name : "zero" , args : append ( append ( [ ] string { } , args ... ) , "--transient-retries" , "0" ) , want : intPointer ( 0 ) } ,
{ name : "positive" , args : append ( append ( [ ] string { } , args ... ) , "--transient-retries" , "3" ) , want : intPointer ( 3 ) } ,
} {
t . Run ( tc . name , func ( t * testing . T ) {
cfg , err := parseRunArgs ( tc . args )
if err != nil {
t . Fatal ( err )
}
req , err := buildRunRequestFromConfig ( cfg )
if err != nil {
t . Fatal ( err )
}
if ! reflect . DeepEqual ( req . TransientRetries , tc . want ) {
t . Fatalf ( "request retry override: got %v, want %v" , req . TransientRetries , tc . want )
}
} )
}
for _ , invalid := range [ ] string { "-1" , "wrong" } {
if _ , err := parseRunArgs ( append ( append ( [ ] string { } , args ... ) , "--transient-retries" , invalid ) ) ; err == nil {
t . Fatalf ( "run accepted invalid retry count %q" , invalid )
}
if _ , err := parseRenderArgs ( append ( append ( [ ] string { } , args ... ) , "--transient-retries" , invalid ) ) ; err == nil {
t . Fatalf ( "render accepted invalid retry count %q" , invalid )
}
}
code , stdout , stderr = runCLICommand ( t , renderCommand , append ( append ( [ ] string { } , args ... ) , "--transient-retries" , "0" , "--format" , "json" ) )
if code != ExitOK || strings . Contains ( stdout , "transient_retries" ) {
t . Fatalf ( "render retry override changed prepared output: code=%d stdout=%q stderr=%q" , code , stdout , stderr )
}
}
2026-09-26 15:34:55 +00:00
func TestRunTimeoutParsingAndCancellation ( t * testing . T ) {
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello" )
args := [ ] string { "--prompt-dir" , lib . promptDir , "--profile-dir" , lib . profileDir , "--prompt" , "timed" , "--input" , "transcript=" + inputPath }
for _ , tc := range [ ] struct {
name string
flag [ ] string
want time . Duration
} {
{ name : "omitted" } ,
{ name : "zero" , flag : [ ] string { "--run-timeout" , "0s" } } ,
{ name : "subsecond" , flag : [ ] string { "--run-timeout" , "125ms" } , want : 125 * time . Millisecond } ,
} {
t . Run ( tc . name , func ( t * testing . T ) {
cfg , err := parseRunArgs ( append ( append ( [ ] string { } , args ... ) , tc . flag ... ) )
if err != nil || cfg . runTimeout != tc . want {
t . Fatalf ( "run timeout: cfg=%+v err=%v" , cfg , err )
}
} )
}
if _ , err := parseRunArgs ( append ( append ( [ ] string { } , args ... ) , "--run-timeout" , "-1ms" ) ) ; err == nil {
t . Fatal ( "negative run timeout accepted" )
}
if _ , err := parseRenderArgs ( append ( append ( [ ] string { } , args ... ) , "--run-timeout" , "1s" ) ) ; err == nil {
t . Fatal ( "render accepted run-only timeout" )
}
started := make ( chan struct { } , 1 )
release := make ( chan struct { } )
provider := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
started <- struct { } { }
<- release
} ) )
defer provider . Close ( )
defer close ( release )
writePromptFile ( t , lib . promptDir , "timed" , "local-default" )
writeProfileFile ( t , lib . profileDir , "local-default" , provider . URL + "/v1" , "local-model" )
code , stdout , stderr := runCLICommand ( t , runCommand , append ( append ( [ ] string { } , args ... ) , "--run-timeout" , "500ms" , "--transient-retries" , "0" ) )
if code != ExitRuntimeError || stdout != "" || ! strings . Contains ( stderr , "execution deadline exceeded" ) {
t . Fatalf ( "deadline outcome: code=%d stdout=%q stderr=%q" , code , stdout , stderr )
}
select {
case <- started :
default :
t . Fatal ( "provider call did not start before deadline" )
}
}
2026-09-26 15:47:26 +00:00
func TestRunCommandWritesSeparateExecutionReport ( t * testing . T ) {
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello" )
var hits int32
provider := newTestLLMServer ( "generated" , & hits )
defer provider . Close ( )
writePromptFile ( t , lib . promptDir , "reported" , "local-default" )
writeProfileFile ( t , lib . profileDir , "local-default" , provider . URL + "/v1" , "local-model" )
path := filepath . Join ( t . TempDir ( ) , "execution.json" )
code , stdout , stderr := runCLICommand ( t , runCommand , [ ] string {
"--prompt-dir" , lib . promptDir , "--profile-dir" , lib . profileDir ,
"--prompt" , "reported" , "--input" , "transcript=" + inputPath ,
"--execution-report" , path ,
} )
if code != ExitOK || stdout != "generated" || atomic . LoadInt32 ( & hits ) != 1 {
t . Fatalf ( "run with report: code=%d stdout=%q stderr=%q hits=%d" , code , stdout , stderr , atomic . LoadInt32 ( & hits ) )
}
data , err := os . ReadFile ( path )
if err != nil {
t . Fatal ( err )
}
if ! bytes . HasSuffix ( data , [ ] byte ( "\n" ) ) {
t . Fatalf ( "report is not newline-terminated: %q" , data )
}
var report map [ string ] any
if err := json . Unmarshal ( data , & report ) ; err != nil {
t . Fatal ( err )
}
if report [ "prompt_id" ] != "reported" || report [ "outcome" ] != "success" || report [ "attempts" ] != float64 ( 1 ) {
t . Fatalf ( "unexpected execution report: %#v" , report )
}
}
func TestExecutionReportFailureAndOutputHandling ( t * testing . T ) {
terminal := func ( ctx context . Context , req promptkit . RunRequest ) ( * promptkit . RunResult , error ) {
if req . Observer != nil {
req . Observer ( promptkit . ExecutionEvent { Kind : promptkit . ExecutionFinished , PromptID : "p" , Outcome : promptkit . ExecutionOutcomeSuccess , Attempts : 1 } )
}
return & promptkit . RunResult { Artifact : promptkit . Artifact { Body : [ ] byte ( "artifact" ) } , Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed } } , nil
}
for _ , tc := range [ ] struct {
name string
modify func ( * runConfig , string )
run func ( context . Context , promptkit . RunRequest ) ( * promptkit . RunResult , error )
wantCode int
wantOutput string
wantDiagnostic string
wantReport bool
} {
{ name : "default streams" , run : terminal , wantCode : ExitOK , wantOutput : "artifact" } ,
{ name : "validation failure" , run : func ( ctx context . Context , req promptkit . RunRequest ) ( * promptkit . RunResult , error ) {
req . Observer ( promptkit . ExecutionEvent { Kind : promptkit . ExecutionFinished , Outcome : promptkit . ExecutionOutcomeInvalidOutput } )
return & promptkit . RunResult { Artifact : promptkit . Artifact { Body : [ ] byte ( "invalid" ) } , Validation : promptkit . ValidationResult { Status : promptkit . ValidationFailed } } , nil
} , wantCode : ExitValidationFailed , wantOutput : "invalid" , wantReport : true } ,
{ name : "execution failure" , run : func ( ctx context . Context , req promptkit . RunRequest ) ( * promptkit . RunResult , error ) {
req . Observer ( promptkit . ExecutionEvent { Kind : promptkit . ExecutionFinished , Outcome : promptkit . ExecutionOutcomeError , Failure : promptkit . ExecutionFailureProviderStatus } )
return nil , promptkit . ErrLLMGenerate
} , wantCode : ExitRuntimeError , wantDiagnostic : "run error" , wantReport : true } ,
{ name : "artifact write failure still writes report" , run : terminal , modify : func ( cfg * runConfig , root string ) {
cfg . outputPath = filepath . Join ( root , "missing" , "artifact.txt" )
} , wantCode : ExitRuntimeError , wantDiagnostic : "output write error" , wantReport : true } ,
{ name : "report write failure preserves artifact" , run : terminal , modify : func ( cfg * runConfig , root string ) {
cfg . executionReportPath = filepath . Join ( root , "missing" , "execution.json" )
} , wantCode : ExitRuntimeError , wantOutput : "artifact" , wantDiagnostic : "execution report write error" } ,
{ name : "execution and report failures are both visible" , run : func ( ctx context . Context , req promptkit . RunRequest ) ( * promptkit . RunResult , error ) {
req . Observer ( promptkit . ExecutionEvent { Kind : promptkit . ExecutionFinished , Outcome : promptkit . ExecutionOutcomeError } )
return nil , promptkit . ErrLLMGenerate
} , modify : func ( cfg * runConfig , root string ) {
cfg . executionReportPath = filepath . Join ( root , "missing" , "execution.json" )
} , wantCode : ExitRuntimeError , wantDiagnostic : "execution report write error" } ,
} {
t . Run ( tc . name , func ( t * testing . T ) {
root := t . TempDir ( )
cfg := & runConfig { executionReportPath : filepath . Join ( root , "execution.json" ) }
if tc . name == "default streams" {
cfg . executionReportPath = ""
}
if tc . modify != nil {
tc . modify ( cfg , root )
}
var stdout , stderr bytes . Buffer
code := executeRun ( cfg , promptkit . RunRequest { PromptID : "p" } , tc . run , & stdout , & stderr )
if code != tc . wantCode || stdout . String ( ) != tc . wantOutput || ( tc . wantDiagnostic != "" && ! strings . Contains ( stderr . String ( ) , tc . wantDiagnostic ) ) {
t . Fatalf ( "unexpected output: code=%d stdout=%q stderr=%q" , code , stdout . String ( ) , stderr . String ( ) )
}
if tc . wantReport {
if _ , err := os . Stat ( cfg . executionReportPath ) ; err != nil {
t . Fatalf ( "terminal report missing: %v" , err )
}
}
if tc . name == "execution and report failures are both visible" && ! strings . Contains ( stderr . String ( ) , "run error" ) {
t . Fatalf ( "execution failure diagnostic was lost: %s" , stderr . String ( ) )
}
} )
}
}
func TestExecutionReportLeavesExistingFileWhenNoTerminalEvent ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "execution.json" )
if err := os . WriteFile ( path , [ ] byte ( "old report" ) , 0 o644 ) ; err != nil {
t . Fatal ( err )
}
cfg := & runConfig { executionReportPath : path }
var stdout , stderr bytes . Buffer
code := executeRun ( cfg , promptkit . RunRequest { PromptID : "p" } , func ( ctx context . Context , req promptkit . RunRequest ) ( * promptkit . RunResult , error ) {
if req . Observer == nil {
t . Fatal ( "requested report did not attach observer" )
}
return & promptkit . RunResult { Artifact : promptkit . Artifact { Body : [ ] byte ( "artifact" ) } , Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed } } , nil
} , & stdout , & stderr )
data , err := os . ReadFile ( path )
if err != nil {
t . Fatal ( err )
}
if code != ExitOK || stdout . String ( ) != "artifact" || string ( data ) != "old report" || ! strings . Contains ( stderr . String ( ) , "no terminal observation" ) {
t . Fatalf ( "no-terminal handling: code=%d stdout=%q stderr=%q report=%q" , code , stdout . String ( ) , stderr . String ( ) , data )
}
}
func TestExecutionReportPathValidationAndPreInvocationFailure ( t * testing . T ) {
root := t . TempDir ( )
args := [ ] string { "--prompt-dir" , root , "--prompt" , "p" }
path := filepath . Join ( root , "execution.json" )
for _ , tail := range [ ] [ ] string {
{ "--execution-report" , "" } ,
{ "--out" , path , "--execution-report" , path } ,
{ "--out" , path , "--execution-report" , filepath . Join ( root , "subdir" , ".." , "execution.json" ) } ,
} {
if _ , err := parseRunArgs ( append ( append ( [ ] string { } , args ... ) , tail ... ) ) ; err == nil {
t . Fatalf ( "accepted invalid report destination: %q" , tail )
}
}
if err := os . WriteFile ( path , [ ] byte ( "old report" ) , 0 o644 ) ; err != nil {
t . Fatal ( err )
}
alias := filepath . Join ( root , "alias.json" )
if err := os . Link ( path , alias ) ; err != nil {
t . Fatal ( err )
}
if _ , err := parseRunArgs ( append ( append ( [ ] string { } , args ... ) , "--out" , path , "--execution-report" , alias ) ) ; err == nil {
t . Fatal ( "accepted two names for the same output file" )
}
if cfg , err := parseRunArgs ( append ( append ( [ ] string { } , args ... ) , "--execution-report" , "-" ) ) ; err != nil || cfg . executionReportPath != "-" {
t . Fatalf ( "ordinary dash path was rejected: cfg=%+v err=%v" , cfg , err )
}
if _ , err := parseRenderArgs ( append ( append ( [ ] string { } , args ... ) , "--execution-report" , path ) ) ; err == nil {
t . Fatal ( "render accepted run-only report flag" )
}
code , stdout , stderr := runCLICommand ( t , runCommand , [ ] string { "--config" , filepath . Join ( root , "missing.yml" ) , "--prompt" , "p" , "--execution-report" , path } )
data , err := os . ReadFile ( path )
if err != nil {
t . Fatal ( err )
}
2026-09-26 18:08:36 +00:00
if code != ExitRuntimeError || stdout != "" || ! strings . Contains ( stderr , "source.missing" ) || string ( data ) != "old report" {
2026-09-26 15:47:26 +00:00
t . Fatalf ( "pre-invocation failure changed report: code=%d stdout=%q stderr=%q report=%q" , code , stdout , stderr , data )
}
}
2026-09-26 15:28:30 +00:00
func intPointer ( value int ) * int { return & value }
2026-09-26 15:24:55 +00:00
func TestPromptkitPromptInspectionSelectsVersionsAndContracts ( t * testing . T ) {
fixtureRoot := definitionFixtureRoot ( t )
configPath := writeDefinitionFixtureConfig ( t , fixtureRoot , true )
2026-08-29 15:12:27 +00:00
tests := [ ] struct {
promptID string
version string
format string
mode string
} {
{ promptID : "compat.complete" , version : "1.0.0" , format : "json" , mode : "json_schema" } ,
{ promptID : "compat.complete" , version : "2.0.0" , format : "text" , mode : "basic" } ,
{ promptID : "compat.json" , version : "1.0.0" , format : "json" , mode : "json" } ,
{ promptID : "compat.none" , version : "1.0.0" , format : "text" , mode : "none" } ,
}
for _ , tc := range tests {
t . Run ( tc . promptID + "@" + tc . version , func ( t * testing . T ) {
code , stdout , stderr := runCLICommand ( t , inspectCommand , [ ] string {
"prompt" ,
"--config" , configPath ,
"--prompt" , tc . promptID ,
"--prompt-version" , tc . version ,
"--format" , "json" ,
} )
if code != ExitOK {
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
}
var inspection renderformat . PromptInspection
if err := json . Unmarshal ( [ ] byte ( stdout ) , & inspection ) ; err != nil {
t . Fatalf ( "decode prompt inspection: %v\nbody=%s" , err , stdout )
}
if inspection . PromptVersion != tc . version || inspection . OutputContract . Format != tc . format || inspection . OutputContract . ValidationMode != tc . mode {
t . Fatalf ( "unexpected prompt inspection: %+v" , inspection )
}
} )
}
}
2026-09-26 15:24:55 +00:00
func TestPromptkitProfileInspectionResolvesSupportedTargets ( t * testing . T ) {
2026-08-29 15:12:27 +00:00
const secret = "sentinel-profile-secret"
t . Setenv ( "FIXTURE_PROFILE_API_KEY" , secret )
2026-09-26 15:24:55 +00:00
fixtureRoot := definitionFixtureRoot ( t )
configPath := writeDefinitionFixtureConfig ( t , fixtureRoot , true )
2026-08-29 15:12:27 +00:00
tests := [ ] struct {
name string
profileID string
wantBackend string
wantModel string
wantAPIKeyEnv string
} {
2026-08-29 15:16:04 +00:00
{ name : "inherited custom backend" , profileID : "custom-derived" , wantBackend : "fixture-custom" , wantModel : "fixture-derived-model" , wantAPIKeyEnv : "FIXTURE_PROFILE_API_KEY" } ,
{ name : "endpoint only" , profileID : "endpoint-only" , wantModel : "fixture-endpoint-model" } ,
2026-09-26 15:24:55 +00:00
{ name : "built in" , profileID : "promptkit-openrouter-auto" , wantBackend : "openrouter" , wantModel : "openrouter/auto" , wantAPIKeyEnv : "OPENROUTER_API_KEY" } ,
2026-08-29 15:12:27 +00:00
}
for _ , tc := range tests {
t . Run ( tc . name , func ( t * testing . T ) {
args := [ ] string { "profile" , "--config" , configPath , "--profile" , tc . profileID , "--format" , "json" }
code , stdout , stderr := runCLICommand ( t , inspectCommand , args )
if code != ExitOK {
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
}
if strings . Contains ( stdout , secret ) || strings . Contains ( stderr , secret ) {
t . Fatalf ( "inspection exposed environment secret: stdout=%q stderr=%q" , stdout , stderr )
}
var inspection renderformat . ProfileInspection
if err := json . Unmarshal ( [ ] byte ( stdout ) , & inspection ) ; err != nil {
t . Fatalf ( "decode profile inspection: %v\nbody=%s" , err , stdout )
}
params := inspection . EffectiveModelParams
if inspection . ProfileID != tc . profileID || params . BackendID != tc . wantBackend || params . Model != tc . wantModel || params . APIKeyEnv != tc . wantAPIKeyEnv {
t . Fatalf ( "unexpected effective profile: %+v" , inspection )
}
if tc . profileID == "custom-derived" {
if params . ServiceTier != "flex" || params . ReasoningEffort != "high" || params . TimeoutSeconds != 45 || len ( params . ExtraParams ) == 0 {
t . Fatalf ( "inherited profile controls were not resolved: %+v" , params )
}
}
} )
}
2026-08-29 15:16:04 +00:00
2026-09-26 15:24:55 +00:00
configWithoutPromptDir := writeDefinitionFixtureConfig ( t , fixtureRoot , false )
2026-08-29 15:16:04 +00:00
code , _ , stderr := runCLICommand ( t , inspectCommand , [ ] string {
"profile" ,
"--config" , configWithoutPromptDir ,
"--profile-dir" , filepath . Join ( fixtureRoot , "profiles" ) ,
"--profile" , "custom-derived" ,
} )
if code != ExitOK {
t . Fatalf ( "profile inspection unexpectedly required a prompt directory: %q" , stderr )
}
2026-08-29 15:12:27 +00:00
}
2026-09-26 15:24:55 +00:00
func TestMaintainedProfilesResolveAcrossSelectionAndSourcePrecedence ( t * testing . T ) {
promptDir := t . TempDir ( )
profileDir := t . TempDir ( )
writePromptDefinition ( t , promptDir , "maintained.yaml" , ` id : maintained
version : "1.0.0"
default_profile : promptkit - openrouter - standard
messages :
- role : user
content : Hello
output :
format : text
validation_mode : none
` )
configPath := writeAppConfigFile ( t , fmt . Sprintf ( "prompt_dir: %q\nprofile_dir: %q\n" , promptDir , profileDir ) )
for _ , profileID := range [ ] string { "promptkit-openrouter-standard" , "promptkit-openrouter-auto" } {
t . Run ( profileID + " inspection" , func ( t * testing . T ) {
code , stdout , stderr := runCLICommand ( t , inspectCommand , [ ] string { "profile" , "--config" , configPath , "--profile" , profileID , "--format" , "json" } )
if code != ExitOK {
t . Fatalf ( "inspect %s: code=%d stderr=%q" , profileID , code , stderr )
}
var inspection renderformat . ProfileInspection
if err := json . Unmarshal ( [ ] byte ( stdout ) , & inspection ) ; err != nil {
t . Fatal ( err )
}
if inspection . ProfileID != profileID || inspection . EffectiveModelParams . BackendID != "openrouter" || inspection . EffectiveModelParams . Model == "" {
t . Fatalf ( "maintained profile did not resolve: %+v" , inspection )
}
if profileID == "promptkit-openrouter-auto" && inspection . EffectiveModelParams . Model != "openrouter/auto" {
t . Fatalf ( "unexpected auto target: %+v" , inspection . EffectiveModelParams )
}
} )
}
for _ , tc := range [ ] struct {
name string
selection [ ] string
wantID string
} {
{ name : "prompt default" , wantID : "promptkit-openrouter-standard" } ,
{ name : "explicit selection" , selection : [ ] string { "--profile" , "promptkit-openrouter-auto" } , wantID : "promptkit-openrouter-auto" } ,
} {
t . Run ( tc . name , func ( t * testing . T ) {
args := append ( [ ] string { "--config" , configPath , "--prompt" , "maintained" , "--format" , "json" } , tc . selection ... )
code , stdout , stderr := runCLICommand ( t , renderCommand , args )
if code != ExitOK {
t . Fatalf ( "render: code=%d stderr=%q" , code , stderr )
}
var rendered struct {
SelectedProfileID string ` json:"selected_profile_id" `
SelectedBackendID string ` json:"selected_backend_id" `
}
if err := json . Unmarshal ( [ ] byte ( stdout ) , & rendered ) ; err != nil {
t . Fatal ( err )
}
if rendered . SelectedProfileID != tc . wantID || rendered . SelectedBackendID != "openrouter" {
t . Fatalf ( "unexpected selection: %+v" , rendered )
}
} )
}
writePromptDefinition ( t , profileDir , "derived.yaml" , "id: derived-maintained\nbase_profile: promptkit-openrouter-standard\nmodel: fixture-inherited-model\n" )
writePromptDefinition ( t , profileDir , "replacement.yaml" , "id: promptkit-openrouter-auto\nbackend: openrouter\nmodel: fixture-override-model\n" )
for _ , tc := range [ ] struct { id , model string } {
{ id : "derived-maintained" , model : "fixture-inherited-model" } ,
{ id : "promptkit-openrouter-auto" , model : "fixture-override-model" } ,
} {
t . Run ( tc . id + " source" , func ( t * testing . T ) {
code , stdout , stderr := runCLICommand ( t , inspectCommand , [ ] string { "profile" , "--config" , configPath , "--profile" , tc . id , "--format" , "json" } )
if code != ExitOK {
t . Fatalf ( "inspect: code=%d stderr=%q" , code , stderr )
}
var inspection renderformat . ProfileInspection
if err := json . Unmarshal ( [ ] byte ( stdout ) , & inspection ) ; err != nil {
t . Fatal ( err )
}
if inspection . EffectiveModelParams . BackendID != "openrouter" || inspection . EffectiveModelParams . Model != tc . model {
t . Fatalf ( "unexpected effective target: %+v" , inspection )
}
} )
}
}
2026-08-29 15:12:27 +00:00
func TestProfileInspectionHonorsDirectoryPrecedenceOutputAndFailures ( t * testing . T ) {
2026-09-26 15:24:55 +00:00
fixtureRoot := definitionFixtureRoot ( t )
configPath := writeDefinitionFixtureConfig ( t , fixtureRoot , true )
2026-08-29 15:12:27 +00:00
overrideDir := t . TempDir ( )
writeProfileFile ( t , overrideDir , "custom-derived" , "http://127.0.0.1:9000/v1" , "override-model" )
outPath := filepath . Join ( t . TempDir ( ) , "inspection.json" )
code , stdout , stderr := runCLICommand ( t , inspectCommand , [ ] string {
"profile" ,
"--config" , configPath ,
"--profile-dir" , overrideDir ,
"--profile" , "custom-derived" ,
"--format" , "json" ,
"--out" , outPath ,
} )
if code != ExitOK || stdout != "" {
t . Fatalf ( "expected file output, got code=%d stdout=%q stderr=%q" , code , stdout , stderr )
}
output , err := os . ReadFile ( outPath )
if err != nil {
t . Fatalf ( "read profile inspection output: %v" , err )
}
if ! strings . Contains ( string ( output ) , ` "model": "override-model" ` ) || ! strings . Contains ( string ( output ) , ` "backend_id": "" ` ) {
t . Fatalf ( "profile directory override was not used: %s" , output )
}
for _ , profileID := range [ ] string { "missing" , "invalid" } {
t . Run ( profileID , func ( t * testing . T ) {
profileDir := filepath . Join ( fixtureRoot , "profiles" )
if profileID == "invalid" {
profileDir = t . TempDir ( )
writePromptDefinition ( t , profileDir , "invalid.yaml" , "id: invalid\nendpoint: not-a-url\nmodel: fixture\n" )
}
code , stdout , stderr := runCLICommand ( t , inspectCommand , [ ] string {
"profile" ,
"--config" , configPath ,
"--profile-dir" , profileDir ,
"--profile" , profileID ,
} )
if code != ExitRuntimeError || stdout != "" || ! strings . Contains ( stderr , "inspect error" ) {
t . Fatalf ( "expected inspection failure without partial output, got code=%d stdout=%q stderr=%q" , code , stdout , stderr )
}
} )
}
}
2026-08-29 14:18:29 +00:00
func TestConfiguredBackendValidationComesFromPromptkit ( t * testing . T ) {
lib := newCLITestLibrary ( t )
for _ , tc := range [ ] struct {
name string
backend string
} {
{
name : "reserved ID" ,
backend : ` openrouter :
endpoint : http : //localhost:11434/v1`,
} ,
{
name : "invalid endpoint" ,
backend : ` local - gpu :
endpoint : not - a - url ` ,
} ,
{
name : "invalid capacity relationship" ,
backend : ` local - gpu :
endpoint : http : //localhost:11434/v1
queue_capacity : 0 ` ,
} ,
{
name : "reserved extra parameter" ,
backend : ` local - gpu :
endpoint : http : //localhost:11434/v1
extra_params :
model : forbidden ` ,
} ,
} {
t . Run ( tc . name , func ( t * testing . T ) {
configPath := writeAppConfigFile ( t , fmt . Sprintf ( `
prompt_dir : % s
profile_dir : % s
backends :
% s
` , lib . promptDir , lib . profileDir , tc . backend ) )
cfg , err := parseRenderArgs ( [ ] string { "--config" , configPath , "--prompt" , "custom" } )
if err != nil {
t . Fatalf ( "expected config decoding to succeed, got %v" , err )
}
_ , err = newEngine ( cfg . runConfig . engineSettings ( ) )
if ! errors . Is ( err , promptkit . ErrInvalidConfig ) {
t . Fatalf ( "expected Promptkit ErrInvalidConfig, got %v" , err )
}
if ! strings . Contains ( err . Error ( ) , "engine initialization from application configuration" ) {
t . Fatalf ( "expected application context, got %v" , err )
}
} )
}
}
2026-05-06 15:20:22 +00:00
func TestRenderCommandExplicitTextFormatWorks ( t * testing . T ) {
2026-05-26 13:20:07 +00:00
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello transcript" )
2026-05-06 15:20:22 +00:00
2026-05-26 13:20:07 +00:00
writePromptFile ( t , lib . promptDir , "prompt.render" , "local-default" )
writeProfileFile ( t , lib . profileDir , "local-default" , "http://127.0.0.1:1/v1" , "profile-model" )
2026-05-06 15:20:22 +00:00
2026-05-26 13:20:07 +00:00
code , stdout , stderr := runCLICommand ( t , renderCommand , [ ] string {
"--prompt-dir" , lib . promptDir ,
"--profile-dir" , lib . profileDir ,
2026-05-06 15:20:22 +00:00
"--prompt" , "prompt.render" ,
"--input" , "transcript=" + inputPath ,
"--format" , "text" ,
2026-05-26 13:20:07 +00:00
} )
2026-05-06 15:20:22 +00:00
if code != ExitOK {
2026-05-26 13:20:07 +00:00
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
2026-05-06 15:20:22 +00:00
}
2026-05-26 13:20:07 +00:00
if ! strings . Contains ( stdout , "prompt: prompt.render" ) {
t . Fatalf ( "expected text output for explicit --format text, got %q" , stdout )
2026-05-06 15:20:22 +00:00
}
}
func TestRenderCommandExplicitJSONFormatOutputsValidJSON ( t * testing . T ) {
2026-05-26 13:20:07 +00:00
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello transcript" )
2026-05-06 15:20:22 +00:00
2026-05-26 13:20:07 +00:00
writePromptFile ( t , lib . promptDir , "prompt.render" , "local-default" )
writeProfileFile ( t , lib . profileDir , "local-default" , "http://127.0.0.1:1/v1" , "profile-model" )
2026-05-06 15:20:22 +00:00
2026-05-26 13:20:07 +00:00
code , stdout , stderr := runCLICommand ( t , renderCommand , [ ] string {
"--prompt-dir" , lib . promptDir ,
"--profile-dir" , lib . profileDir ,
2026-05-06 15:20:22 +00:00
"--prompt" , "prompt.render" ,
"--input" , "transcript=" + inputPath ,
"--format" , "json" ,
2026-05-26 13:20:07 +00:00
} )
2026-05-06 15:20:22 +00:00
if code != ExitOK {
2026-05-26 13:20:07 +00:00
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
2026-05-06 15:20:22 +00:00
}
var payload map [ string ] any
2026-05-26 13:20:07 +00:00
if err := json . Unmarshal ( [ ] byte ( stdout ) , & payload ) ; err != nil {
t . Fatalf ( "expected valid json output, got %v\nbody=%s" , err , stdout )
2026-05-06 15:20:22 +00:00
}
if payload [ "prompt_id" ] != "prompt.render" {
t . Fatalf ( "expected prompt_id, got %#v" , payload [ "prompt_id" ] )
}
if payload [ "selected_profile_id" ] != "local-default" {
t . Fatalf ( "expected selected_profile_id, got %#v" , payload [ "selected_profile_id" ] )
}
if _ , ok := payload [ "messages" ] ; ! ok {
t . Fatalf ( "expected messages in render json output, got %#v" , payload )
}
}
func TestRenderCommandUnknownFormatFailsClearly ( t * testing . T ) {
var stdout bytes . Buffer
var stderr bytes . Buffer
code := renderCommand ( [ ] string {
"--prompt-dir" , "./prompts" ,
"--profile-dir" , "./profiles" ,
"--prompt" , "p" ,
"--input" , "transcript=./x.md" ,
"--format" , "yaml" ,
} , & stdout , & stderr )
if code != ExitRuntimeError {
t . Fatalf ( "expected ExitRuntimeError, got %d" , code )
}
if ! strings . Contains ( stderr . String ( ) , "render parse error" ) || ! strings . Contains ( stderr . String ( ) , "unknown prepared run format" ) {
t . Fatalf ( "expected clear unknown-format parse error, got %q" , stderr . String ( ) )
}
}
func TestRenderCommandOutWritesToFile ( t * testing . T ) {
2026-05-26 13:20:07 +00:00
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello transcript" )
outPath := filepath . Join ( lib . rootDir , "render.txt" )
2026-05-06 15:20:22 +00:00
2026-05-26 13:20:07 +00:00
writePromptFile ( t , lib . promptDir , "prompt.render" , "local-default" )
writeProfileFile ( t , lib . profileDir , "local-default" , "http://127.0.0.1:1/v1" , "profile-model" )
2026-05-06 15:20:22 +00:00
2026-05-26 13:20:07 +00:00
code , stdout , stderr := runCLICommand ( t , renderCommand , [ ] string {
"--prompt-dir" , lib . promptDir ,
"--profile-dir" , lib . profileDir ,
2026-05-06 15:20:22 +00:00
"--prompt" , "prompt.render" ,
"--input" , "transcript=" + inputPath ,
"--out" , outPath ,
2026-05-26 13:20:07 +00:00
} )
2026-05-06 15:20:22 +00:00
if code != ExitOK {
2026-05-26 13:20:07 +00:00
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
2026-05-06 15:20:22 +00:00
}
2026-05-26 13:20:07 +00:00
if stdout != "" {
t . Fatalf ( "expected empty stdout when --out is set, got %q" , stdout )
2026-05-06 15:20:22 +00:00
}
out , err := os . ReadFile ( outPath )
if err != nil {
t . Fatalf ( "failed reading render output file: %v" , err )
}
if ! strings . Contains ( string ( out ) , "prompt: prompt.render" ) {
t . Fatalf ( "expected render output in file, got %q" , string ( out ) )
}
}
2026-05-06 15:59:22 +00:00
func TestRenderCommandPromptDefaultProfileWorksThroughCLIPath ( t * testing . T ) {
2026-05-26 13:20:07 +00:00
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello" )
2026-05-06 15:59:22 +00:00
2026-05-26 13:20:07 +00:00
writePromptFile ( t , lib . promptDir , "prompt.default" , "local-default" )
writeProfileFile ( t , lib . profileDir , "local-default" , "http://127.0.0.1:1/v1" , "default-model" )
2026-05-06 15:59:22 +00:00
2026-05-26 13:20:07 +00:00
code , stdout , stderr := runCLICommand ( t , renderCommand , [ ] string {
"--prompt-dir" , lib . promptDir ,
"--profile-dir" , lib . profileDir ,
2026-05-06 15:59:22 +00:00
"--prompt" , "prompt.default" ,
"--input" , "transcript=" + inputPath ,
2026-05-26 13:20:07 +00:00
} )
2026-05-06 15:59:22 +00:00
if code != ExitOK {
2026-05-26 13:20:07 +00:00
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
2026-05-06 15:59:22 +00:00
}
2026-05-26 13:20:07 +00:00
out := stdout
2026-05-06 15:59:22 +00:00
if ! strings . Contains ( out , "selected_profile_id: local-default" ) {
t . Fatalf ( "expected prompt default profile in output, got %q" , out )
}
if ! strings . Contains ( out , "model: default-model" ) {
t . Fatalf ( "expected model from default profile in output, got %q" , out )
}
}
2026-07-04 16:48:18 +00:00
func TestRenderCommandUsesBuiltInProfileWithoutProfileDir ( t * testing . T ) {
t . Setenv ( "OPENROUTER_API_KEY" , "test-key" )
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello" )
writePromptFile ( t , lib . promptDir , "prompt.builtin" , "mistral-small-3" )
code , stdout , stderr := runCLICommand ( t , renderCommand , [ ] string {
"--prompt-dir" , lib . promptDir ,
"--prompt" , "prompt.builtin" ,
"--input" , "transcript=" + inputPath ,
} )
if code != ExitOK {
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
}
if ! strings . Contains ( stdout , "selected_profile_id: mistral-small-3" ) {
t . Fatalf ( "expected built-in selected profile, got %q" , stdout )
}
if ! strings . Contains ( stdout , "model: mistralai/mistral-small-3.2-24b-instruct" ) {
t . Fatalf ( "expected built-in model, got %q" , stdout )
}
}
2026-05-06 15:59:22 +00:00
func TestRenderCommandExplicitProfileOverridesPromptDefault ( t * testing . T ) {
2026-05-26 13:20:07 +00:00
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello" )
2026-05-06 15:59:22 +00:00
2026-05-26 13:20:07 +00:00
writePromptFile ( t , lib . promptDir , "prompt.default" , "local-default" )
writeProfileFile ( t , lib . profileDir , "local-default" , "http://127.0.0.1:1/v1" , "default-model" )
writeProfileFile ( t , lib . profileDir , "quality" , "http://127.0.0.1:1/v1" , "quality-model" )
2026-05-06 15:59:22 +00:00
2026-05-26 13:20:07 +00:00
code , stdout , stderr := runCLICommand ( t , renderCommand , [ ] string {
"--prompt-dir" , lib . promptDir ,
"--profile-dir" , lib . profileDir ,
2026-05-06 15:59:22 +00:00
"--prompt" , "prompt.default" ,
"--profile" , "quality" ,
"--input" , "transcript=" + inputPath ,
2026-05-26 13:20:07 +00:00
} )
2026-05-06 15:59:22 +00:00
if code != ExitOK {
2026-05-26 13:20:07 +00:00
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
2026-05-06 15:59:22 +00:00
}
2026-05-26 13:20:07 +00:00
out := stdout
2026-05-06 15:59:22 +00:00
if ! strings . Contains ( out , "selected_profile_id: quality" ) {
t . Fatalf ( "expected explicit profile in output, got %q" , out )
}
if ! strings . Contains ( out , "model: quality-model" ) {
t . Fatalf ( "expected model from explicit profile in output, got %q" , out )
}
}
2026-08-29 14:12:51 +00:00
func TestRenderCommandUsesDefinitionInputRulesAndPromptVersions ( t * testing . T ) {
lib := newCLITestLibrary ( t )
writeProfileFile ( t , lib . profileDir , "local" , "http://127.0.0.1:1/v1" , "model" )
writePromptDefinition ( t , lib . promptDir , "sole.yaml" , ` id : sole
version : "1"
default_profile : local
messages :
- role : user
content : "hello"
output :
format : text
validation_mode : none
` )
writePromptDefinition ( t , lib . promptDir , "versioned-one.yaml" , ` id : versioned
version : "1"
default_profile : local
messages :
- role : user
content : "one"
output :
format : text
validation_mode : none
` )
writePromptDefinition ( t , lib . promptDir , "versioned-two.yaml" , ` id : versioned
version : "2"
default_profile : local
messages :
- role : user
content : "two"
output :
format : text
validation_mode : none
` )
writePromptDefinition ( t , lib . promptDir , "optional.yaml" , ` id : optional
version : "1"
default_profile : local
inputs :
- name : note
required : false
messages :
- role : user
content : "hello"
output :
format : text
validation_mode : none
` )
writePromptDefinition ( t , lib . promptDir , "required.yaml" , ` id : required
version : "1"
default_profile : local
inputs :
- name : note
required : true
messages :
- role : user
content : "hello"
output :
format : text
validation_mode : none
` )
writePromptDefinition ( t , lib . promptDir , "template.yaml" , ` id : template
version : "1"
default_profile : local
messages :
- role : user
content : ' { { input "note" } } '
output :
format : text
validation_mode : none
` )
baseArgs := [ ] string { "--prompt-dir" , lib . promptDir , "--profile-dir" , lib . profileDir }
for _ , tc := range [ ] struct {
name string
args [ ] string
wantCode int
wantText string
} {
{ name : "sole version selected when omitted" , args : [ ] string { "--prompt" , "sole" } , wantCode : ExitOK , wantText : "prompt_version: 1" } ,
{ name : "explicit version selected" , args : [ ] string { "--prompt" , "versioned" , "--prompt-version" , "2" } , wantCode : ExitOK , wantText : "prompt_version: 2" } ,
{ name : "multiple versions require selection" , args : [ ] string { "--prompt" , "versioned" } , wantCode : ExitRuntimeError , wantText : "duplicate prompt definition id" } ,
{ name : "no declared inputs" , args : [ ] string { "--prompt" , "sole" } , wantCode : ExitOK , wantText : "prompt: sole" } ,
{ name : "optional input omitted" , args : [ ] string { "--prompt" , "optional" } , wantCode : ExitOK , wantText : "prompt: optional" } ,
{ name : "required input omitted" , args : [ ] string { "--prompt" , "required" } , wantCode : ExitRuntimeError , wantText : "required" } ,
{ name : "template input omitted" , args : [ ] string { "--prompt" , "template" } , wantCode : ExitRuntimeError , wantText : "note" } ,
} {
t . Run ( tc . name , func ( t * testing . T ) {
code , stdout , stderr := runCLICommand ( t , renderCommand , append ( append ( [ ] string { } , baseArgs ... ) , tc . args ... ) )
if code != tc . wantCode {
t . Fatalf ( "expected exit %d, got %d stderr=%q" , tc . wantCode , code , stderr )
}
if ! strings . Contains ( stdout + stderr , tc . wantText ) {
t . Fatalf ( "expected output to contain %q, stdout=%q stderr=%q" , tc . wantText , stdout , stderr )
}
} )
}
}
2026-05-05 10:49:33 -05:00
func TestRunCommandPromptDefaultProfileWorksThroughCLIPath ( t * testing . T ) {
2026-05-26 13:20:07 +00:00
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello" )
2026-05-05 10:49:33 -05:00
ts := newTestLLMServer ( "default-output" , nil )
defer ts . Close ( )
2026-05-26 13:20:07 +00:00
writePromptFile ( t , lib . promptDir , "prompt.default" , "local-default" )
writeProfileFile ( t , lib . profileDir , "local-default" , ts . URL + "/v1" , "profile-model" )
2026-05-05 10:49:33 -05:00
2026-05-26 13:20:07 +00:00
code , stdout , stderr := runCLICommand ( t , runCommand , [ ] string {
"--prompt-dir" , lib . promptDir ,
"--profile-dir" , lib . profileDir ,
2026-05-05 10:49:33 -05:00
"--prompt" , "prompt.default" ,
"--input" , "transcript=" + inputPath ,
2026-05-26 13:20:07 +00:00
} )
2026-05-05 10:49:33 -05:00
if code != ExitOK {
2026-05-26 13:20:07 +00:00
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
2026-05-05 10:49:33 -05:00
}
2026-05-26 13:20:07 +00:00
if stdout != "default-output" {
t . Fatalf ( "unexpected stdout output: %q" , stdout )
2026-05-05 10:49:33 -05:00
}
2026-05-26 13:20:07 +00:00
if ! strings . Contains ( stderr , "selected_profile=local-default" ) {
t . Fatalf ( "expected selected profile in summary, got %q" , stderr )
2026-05-05 10:49:33 -05:00
}
}
func TestRunCommandExplicitProfileOverridesPromptDefault ( t * testing . T ) {
2026-05-26 13:20:07 +00:00
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello" )
2026-05-05 10:49:33 -05:00
defaultServer := newTestLLMServer ( "from-default" , nil )
defer defaultServer . Close ( )
overrideServer := newTestLLMServer ( "from-override" , nil )
defer overrideServer . Close ( )
2026-05-26 13:20:07 +00:00
writePromptFile ( t , lib . promptDir , "prompt.default" , "local-default" )
writeProfileFile ( t , lib . profileDir , "local-default" , defaultServer . URL + "/v1" , "default-model" )
writeProfileFile ( t , lib . profileDir , "quality" , overrideServer . URL + "/v1" , "quality-model" )
2026-05-05 10:49:33 -05:00
2026-05-26 13:20:07 +00:00
code , stdout , stderr := runCLICommand ( t , runCommand , [ ] string {
"--prompt-dir" , lib . promptDir ,
"--profile-dir" , lib . profileDir ,
2026-05-05 10:49:33 -05:00
"--prompt" , "prompt.default" ,
"--profile" , "quality" ,
"--input" , "transcript=" + inputPath ,
2026-05-26 13:20:07 +00:00
} )
2026-05-05 10:49:33 -05:00
if code != ExitOK {
2026-05-26 13:20:07 +00:00
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
2026-05-05 10:49:33 -05:00
}
2026-05-26 13:20:07 +00:00
if stdout != "from-override" {
t . Fatalf ( "expected explicit profile output, got %q" , stdout )
2026-05-05 10:49:33 -05:00
}
2026-05-26 13:20:07 +00:00
if ! strings . Contains ( stderr , "selected_profile=quality" ) {
t . Fatalf ( "expected selected profile quality, got %q" , stderr )
2026-05-05 10:49:33 -05:00
}
}
func TestRunCommandRuntimeFlagsOverrideSelectedProfileValues ( t * testing . T ) {
2026-05-26 13:20:07 +00:00
lib := newCLITestLibrary ( t )
inputPath := lib . writeInputFile ( t , "transcript.md" , "hello" )
2026-05-05 10:49:33 -05:00
var baseHits int32
baseServer := newTestLLMServer ( "base" , & baseHits )
defer baseServer . Close ( )
var overrideHits int32
var observedBody string
overrideServer := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
atomic . AddInt32 ( & overrideHits , 1 )
body , _ := io . ReadAll ( r . Body )
observedBody = string ( body )
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ , _ = w . Write ( [ ] byte ( ` { "choices":[ { "message": { "role":"assistant","content":"override"}}],"usage": { "prompt_tokens":1,"completion_tokens":1,"total_tokens":2}} ` ) )
} ) )
defer overrideServer . Close ( )
2026-05-26 13:20:07 +00:00
writePromptFile ( t , lib . promptDir , "prompt.default" , "local-default" )
writeProfileFile ( t , lib . profileDir , "local-default" , baseServer . URL + "/v1" , "profile-model" )
2026-05-05 10:49:33 -05:00
2026-05-26 13:20:07 +00:00
code , stdout , stderr := runCLICommand ( t , runCommand , [ ] string {
"--prompt-dir" , lib . promptDir ,
"--profile-dir" , lib . profileDir ,
2026-05-05 10:49:33 -05:00
"--prompt" , "prompt.default" ,
"--input" , "transcript=" + inputPath ,
"--llm-base-url" , overrideServer . URL + "/v1" ,
"--model" , "override-model" ,
"--temperature" , "0.7" ,
"--max-tokens" , "55" ,
"--top-p" , "0.2" ,
"--timeout" , "20s" ,
2026-05-26 13:20:07 +00:00
} )
2026-05-05 10:49:33 -05:00
if code != ExitOK {
2026-05-26 13:20:07 +00:00
t . Fatalf ( "expected ExitOK, got %d stderr=%q" , code , stderr )
2026-05-05 10:49:33 -05:00
}
if atomic . LoadInt32 ( & baseHits ) != 0 {
t . Fatalf ( "expected base profile endpoint not to be hit, got %d" , baseHits )
}
if atomic . LoadInt32 ( & overrideHits ) != 1 {
t . Fatalf ( "expected override endpoint to be hit once, got %d" , overrideHits )
}
2026-05-26 13:20:07 +00:00
if stdout != "override" {
t . Fatalf ( "unexpected stdout output: %q" , stdout )
2026-05-05 10:49:33 -05:00
}
if ! strings . Contains ( observedBody , ` "model":"override-model" ` ) {
t . Fatalf ( "expected override model in request body, got %s" , observedBody )
}
if ! strings . Contains ( observedBody , ` "temperature":0.7 ` ) || ! strings . Contains ( observedBody , ` "max_tokens":55 ` ) || ! strings . Contains ( observedBody , ` "top_p":0.2 ` ) {
t . Fatalf ( "expected override generation params in request body, got %s" , observedBody )
}
}
2026-05-05 08:41:07 -05:00
func TestWriteOutputAndSummaryUseSeparateWriters ( t * testing . T ) {
var stdout bytes . Buffer
var stderr bytes . Buffer
if err := writeOutput ( & stdout , "" , [ ] byte ( "artifact-body" ) ) ; err != nil {
t . Fatalf ( "unexpected writeOutput error: %v" , err )
}
2026-07-28 14:04:29 +00:00
printSummary ( & stderr , & promptkit . RunResult {
2026-05-05 10:09:31 -05:00
PromptID : "p" ,
PromptVersion : "1" ,
SelectedProfileID : "exec" ,
ModelName : "m" ,
2026-07-28 14:04:29 +00:00
Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed , Mode : promptkit . ValidationBasic } ,
2026-05-05 10:09:31 -05:00
RenderedPromptHash : "h" ,
InputHashes : map [ string ] string { "in" : "x" } ,
2026-05-05 08:41:07 -05:00
} )
if stdout . String ( ) != "artifact-body" {
t . Fatalf ( "expected artifact output on stdout, got %q" , stdout . String ( ) )
}
2026-05-05 10:09:31 -05:00
if ! strings . Contains ( stderr . String ( ) , "prompt=p@1" ) {
2026-05-05 08:41:07 -05:00
t . Fatalf ( "expected summary on stderr, got %q" , stderr . String ( ) )
}
2026-07-02 23:07:57 +00:00
if strings . Contains ( stderr . String ( ) , "cached_tokens=" ) || strings . Contains ( stderr . String ( ) , "cache_write_tokens=" ) {
t . Fatalf ( "expected zero cache usage to be omitted from summary, got %q" , stderr . String ( ) )
}
2026-08-29 14:26:04 +00:00
if strings . Contains ( stderr . String ( ) , "backend=" ) {
t . Fatalf ( "expected endpoint-only backend to be omitted from summary, got %q" , stderr . String ( ) )
}
2026-07-02 23:07:57 +00:00
}
func TestPrintSummaryIncludesCacheUsageWhenPresent ( t * testing . T ) {
var stderr bytes . Buffer
2026-07-28 14:04:29 +00:00
printSummary ( & stderr , & promptkit . RunResult {
2026-07-02 23:07:57 +00:00
PromptID : "p" ,
PromptVersion : "1" ,
SelectedProfileID : "exec" ,
ModelName : "m" ,
2026-07-28 14:04:29 +00:00
Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed , Mode : promptkit . ValidationBasic } ,
2026-07-02 23:07:57 +00:00
RenderedPromptHash : "h" ,
InputHashes : map [ string ] string { "in" : "x" } ,
2026-07-28 14:04:29 +00:00
Usage : promptkit . TokenUsage {
2026-07-02 23:07:57 +00:00
PromptTokens : 10 ,
CompletionTokens : 5 ,
TotalTokens : 15 ,
CachedTokens : 0 ,
CacheWriteTokens : 3 ,
} ,
} )
summary := stderr . String ( )
if ! strings . Contains ( summary , "usage=10/5/15" ) {
t . Fatalf ( "expected base usage summary, got %q" , summary )
}
if ! strings . Contains ( summary , "cached_tokens=0 cache_write_tokens=3" ) {
t . Fatalf ( "expected cache usage in summary, got %q" , summary )
}
2026-08-29 14:26:04 +00:00
if strings . Contains ( summary , "backend=" ) {
t . Fatalf ( "expected endpoint-only backend to be omitted from summary, got %q" , summary )
}
}
func TestPrintSummaryIncludesBackendWhenPresent ( t * testing . T ) {
var stderr bytes . Buffer
printSummary ( & stderr , & promptkit . RunResult {
PromptID : "p" ,
PromptVersion : "1" ,
SelectedProfileID : "exec" ,
SelectedBackendID : "local" ,
ModelName : "m" ,
Validation : promptkit . ValidationResult { Status : promptkit . ValidationPassed , Mode : promptkit . ValidationBasic } ,
RenderedPromptHash : "h" ,
} )
if ! strings . Contains ( stderr . String ( ) , "backend=local" ) {
t . Fatalf ( "expected backend in summary, got %q" , stderr . String ( ) )
}
}
func TestRunErrorMessageDoesNotExposeCapacityDetails ( t * testing . T ) {
got := runErrorMessage ( & promptkit . CapacityError { BackendID : "private-backend" } )
if got != "run error: model backend capacity is exhausted" {
t . Fatalf ( "unexpected capacity diagnostic: %q" , got )
}
2026-05-04 22:38:20 -05:00
}
2026-05-05 10:49:33 -05:00
2026-05-26 13:20:07 +00:00
type cliTestLibrary struct {
rootDir string
promptDir string
profileDir string
}
func newCLITestLibrary ( t * testing . T ) * cliTestLibrary {
t . Helper ( )
root := t . TempDir ( )
lib := & cliTestLibrary {
rootDir : root ,
promptDir : filepath . Join ( root , "prompts" ) ,
profileDir : filepath . Join ( root , "profiles" ) ,
}
if err := os . MkdirAll ( lib . promptDir , 0 o755 ) ; err != nil {
t . Fatalf ( "failed to create prompt fixture directory: %v" , err )
}
if err := os . MkdirAll ( lib . profileDir , 0 o755 ) ; err != nil {
t . Fatalf ( "failed to create profile fixture directory: %v" , err )
}
return lib
}
func ( l * cliTestLibrary ) writeInputFile ( t * testing . T , name , body string ) string {
t . Helper ( )
path := filepath . Join ( l . rootDir , name )
if err := os . MkdirAll ( filepath . Dir ( path ) , 0 o755 ) ; err != nil {
t . Fatalf ( "failed to create input fixture directory: %v" , err )
}
if err := os . WriteFile ( path , [ ] byte ( body ) , 0 o644 ) ; err != nil {
t . Fatalf ( "failed to write input fixture: %v" , err )
}
return path
}
func runCLICommand ( t * testing . T , command func ( [ ] string , io . Writer , io . Writer ) int , args [ ] string ) ( int , string , string ) {
t . Helper ( )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := command ( args , & stdout , & stderr )
return code , stdout . String ( ) , stderr . String ( )
}
2026-09-26 15:24:55 +00:00
func definitionFixtureRoot ( t * testing . T ) string {
2026-08-29 15:12:27 +00:00
t . Helper ( )
root , err := filepath . Abs ( filepath . Join ( ".." , ".." , ".." , "testdata" , "promptkit-v0.9" ) )
if err != nil {
t . Fatalf ( "resolve Promptkit v0.9 fixture root: %v" , err )
}
if _ , err := os . Stat ( root ) ; err != nil {
t . Fatalf ( "stat Promptkit v0.9 fixture root: %v" , err )
}
return root
}
2026-09-26 15:24:55 +00:00
func writeDefinitionFixtureConfig ( t * testing . T , fixtureRoot string , includeSources bool ) string {
2026-08-29 15:12:27 +00:00
t . Helper ( )
sources := ""
if includeSources {
sources = fmt . Sprintf ( "prompt_dir: %q\nprofile_dir: %q\nschema_dir: %q\n" , filepath . Join ( fixtureRoot , "prompts" ) , filepath . Join ( fixtureRoot , "profiles" ) , filepath . Join ( fixtureRoot , "schemas" ) )
}
return writeAppConfigFile ( t , sources + ` backends :
fixture - custom :
endpoint : http : //127.0.0.1:11434/v1
api_key_env : FIXTURE_BACKEND_API_KEY
extra_params :
backend_option :
enabled : true
concurrency_limit : 2
queue_capacity : 0
` )
}
2026-05-05 10:49:33 -05:00
func writePromptFile ( t * testing . T , dir , id , defaultProfile string ) {
2026-05-06 15:20:22 +00:00
t . Helper ( )
writePromptFileWithTemplate ( t , dir , id , defaultProfile , "Summarize: {{input \"transcript\"}}" )
}
2026-08-29 14:12:51 +00:00
func writePromptDefinition ( t * testing . T , dir , name , definition string ) {
t . Helper ( )
if err := os . WriteFile ( filepath . Join ( dir , name ) , [ ] byte ( definition ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write prompt definition: %v" , err )
}
}
2026-05-06 15:20:22 +00:00
func writePromptFileWithTemplate ( t * testing . T , dir , id , defaultProfile , templateContent string ) {
2026-05-05 10:49:33 -05:00
t . Helper ( )
data := fmt . Sprintf ( ` id : % s
version : "1.0.0"
default_profile : % s
inputs :
- name : transcript
required : true
messages :
- role : user
2026-05-06 15:20:22 +00:00
content : % q
2026-05-05 10:49:33 -05:00
output :
format : text
validation_mode : none
repair_attempts : 0
2026-05-06 15:20:22 +00:00
` , id , defaultProfile , templateContent )
2026-05-05 10:49:33 -05:00
if err := os . WriteFile ( filepath . Join ( dir , id + ".yaml" ) , [ ] byte ( data ) , 0 o644 ) ; err != nil {
t . Fatalf ( "failed to write prompt fixture: %v" , err )
}
}
func writeProfileFile ( t * testing . T , dir , id , endpoint , model string ) {
t . Helper ( )
data := fmt . Sprintf ( "id: %s\nendpoint: %s\nmodel: %s\n" , id , endpoint , model )
if err := os . WriteFile ( filepath . Join ( dir , id + ".yaml" ) , [ ] byte ( data ) , 0 o644 ) ; err != nil {
t . Fatalf ( "failed to write profile fixture: %v" , err )
}
}
func newTestLLMServer ( content string , hitCounter * int32 ) * httptest . Server {
return httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
if hitCounter != nil {
atomic . AddInt32 ( hitCounter , 1 )
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ , _ = w . Write ( [ ] byte ( fmt . Sprintf ( ` { "choices":[ { "message": { "role":"assistant","content":%q}}],"usage": { "prompt_tokens":1,"completion_tokens":1,"total_tokens":2}} ` , content ) ) )
} ) )
}
2026-05-06 16:46:33 +00:00
func writeAppConfigFile ( t * testing . T , content string ) string {
t . Helper ( )
path := filepath . Join ( t . TempDir ( ) , "config.yml" )
if err := os . WriteFile ( path , [ ] byte ( content ) , 0 o644 ) ; err != nil {
t . Fatalf ( "failed to write app config fixture: %v" , err )
}
return path
}