2479 lines
88 KiB
Go
2479 lines
88 KiB
Go
package usecase
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/capacity"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/defaults"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/llm"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/profile"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/prompt"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/promptdef"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/validate"
|
|
)
|
|
|
|
type fakePromptRepo struct {
|
|
def *domain.PromptDefinition
|
|
err error
|
|
lastID string
|
|
lastVersion string
|
|
}
|
|
|
|
type fakeExecutionProfileRepo struct {
|
|
profiles map[string]*domain.ExecutionProfile
|
|
err error
|
|
lastID string
|
|
}
|
|
|
|
type fakeBackendResolver struct {
|
|
backends map[string]domain.Backend
|
|
}
|
|
|
|
func (f fakeBackendResolver) GetBackend(id string) (domain.Backend, error) {
|
|
value, ok := f.backends[id]
|
|
if !ok {
|
|
return domain.Backend{}, errors.New("backend not found")
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (f *fakeExecutionProfileRepo) GetProfile(ctx context.Context, id string) (*domain.ExecutionProfile, error) {
|
|
f.lastID = id
|
|
if f.err != nil {
|
|
return nil, f.err
|
|
}
|
|
if p, ok := f.profiles[id]; ok {
|
|
cp := *p
|
|
return &cp, nil
|
|
}
|
|
return nil, errors.New("profile not found")
|
|
}
|
|
|
|
func (f *fakePromptRepo) GetPromptDefinition(ctx context.Context, id string, version string) (*domain.PromptDefinition, error) {
|
|
f.lastID = id
|
|
f.lastVersion = version
|
|
if f.err != nil {
|
|
return nil, f.err
|
|
}
|
|
return f.def, nil
|
|
}
|
|
|
|
type fakeArtifactReader struct {
|
|
artifactsByURI map[string]*domain.Artifact
|
|
errByURI map[string]error
|
|
calls int
|
|
}
|
|
|
|
func (f *fakeArtifactReader) Read(ctx context.Context, ref domain.ArtifactRef) (*domain.Artifact, error) {
|
|
f.calls++
|
|
if err, ok := f.errByURI[ref.URI]; ok {
|
|
return nil, err
|
|
}
|
|
if art, ok := f.artifactsByURI[ref.URI]; ok {
|
|
cp := *art
|
|
return &cp, nil
|
|
}
|
|
return nil, errors.New("artifact not found")
|
|
}
|
|
|
|
type fakeRenderer struct {
|
|
rendered *domain.RenderedPrompt
|
|
err error
|
|
calls int
|
|
}
|
|
|
|
func (f *fakeRenderer) Render(ctx context.Context, def *domain.PromptDefinition, inputs map[string]*domain.Artifact, vars map[string]string) (*domain.RenderedPrompt, error) {
|
|
f.calls++
|
|
if f.err != nil {
|
|
return nil, f.err
|
|
}
|
|
return f.rendered, nil
|
|
}
|
|
|
|
type fakeLLM struct {
|
|
resp *domain.GenerateResponse
|
|
err error
|
|
lastReq domain.GenerateRequest
|
|
calls int
|
|
forbid bool
|
|
}
|
|
|
|
func (f *fakeLLM) Generate(ctx context.Context, req domain.GenerateRequest) (*domain.GenerateResponse, error) {
|
|
f.calls++
|
|
f.lastReq = req
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if f.forbid {
|
|
return nil, errors.New("llm should not be called")
|
|
}
|
|
if f.err != nil {
|
|
return nil, f.err
|
|
}
|
|
return f.resp, nil
|
|
}
|
|
|
|
type fakeValidator struct {
|
|
result domain.ValidationResult
|
|
err error
|
|
schemaDoc any
|
|
schemaErr error
|
|
schemaLoadPath string
|
|
schemaLoads int
|
|
validateCalls int
|
|
}
|
|
|
|
func (f *fakeValidator) Validate(ctx context.Context, artifact *domain.Artifact, contract domain.OutputContract) (domain.ValidationResult, error) {
|
|
f.validateCalls++
|
|
if f.err != nil {
|
|
return domain.ValidationResult{}, f.err
|
|
}
|
|
return f.result, nil
|
|
}
|
|
|
|
func (f *fakeValidator) LoadSchemaDocument(ctx context.Context, schemaPath string) (any, error) {
|
|
f.schemaLoads++
|
|
f.schemaLoadPath = schemaPath
|
|
if f.schemaErr != nil {
|
|
return nil, f.schemaErr
|
|
}
|
|
if f.schemaDoc != nil {
|
|
return f.schemaDoc, nil
|
|
}
|
|
return map[string]any{"type": "object"}, nil
|
|
}
|
|
|
|
type fakeRepairer struct {
|
|
responses []*domain.GenerateResponse
|
|
err error
|
|
calls int
|
|
reqs []RepairRequest
|
|
}
|
|
|
|
type fakeRunAdmitter struct {
|
|
backendIDs []string
|
|
err error
|
|
releaseCalls int
|
|
}
|
|
|
|
func (f *fakeRunAdmitter) Admit(_ context.Context, backendID string) (func(), error) {
|
|
f.backendIDs = append(f.backendIDs, backendID)
|
|
if f.err != nil {
|
|
return nil, f.err
|
|
}
|
|
return func() {
|
|
f.releaseCalls++
|
|
}, nil
|
|
}
|
|
|
|
func (f *fakeRepairer) Repair(ctx context.Context, req RepairRequest) (*domain.GenerateResponse, error) {
|
|
f.calls++
|
|
f.reqs = append(f.reqs, req)
|
|
if f.err != nil {
|
|
return nil, f.err
|
|
}
|
|
if len(f.responses) == 0 {
|
|
return nil, errors.New("no repair response configured")
|
|
}
|
|
idx := f.calls - 1
|
|
if idx >= len(f.responses) {
|
|
idx = len(f.responses) - 1
|
|
}
|
|
return f.responses[idx], nil
|
|
}
|
|
|
|
func TestRunnerPrepareWithExplicitProfileSelection(t *testing.T) {
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatMarkdown, domain.ValidationBasic, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}
|
|
reader := &fakeArtifactReader{artifactsByURI: map[string]*domain.Artifact{
|
|
"a://t": {Body: []byte("transcript"), Hash: hashString("transcript")},
|
|
"a://g": {Body: []byte("glossary"), Hash: hashString("glossary")},
|
|
}}
|
|
renderer := &fakeRenderer{rendered: &domain.RenderedPrompt{SessionID: "session-123", Messages: []domain.RenderedMessage{{Role: "system", Content: "sys"}, {Role: "user", Content: "usr"}}}}
|
|
llmClient := &fakeLLM{forbid: true}
|
|
|
|
runner := NewRunner(promptRepo, execRepo, nil, reader, renderer, llmClient, nil, nil)
|
|
prepared, err := runner.Prepare(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
PromptVersion: "1",
|
|
ProfileID: "exec",
|
|
Inputs: map[string]domain.ArtifactRef{
|
|
"transcript": {Type: domain.ArtifactRefFile, URI: "a://t"},
|
|
"glossary": {Type: domain.ArtifactRefFile, URI: "a://g"},
|
|
},
|
|
Execution: &domain.ExecutionTargetOverride{Endpoint: "http://override/v1", Model: "m", Temperature: float64Ptr(0.3), TimeoutSeconds: intPtr(90)},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if prepared.PromptID != "p" || prepared.PromptVersion != "1" {
|
|
t.Fatalf("unexpected prepared prompt metadata: %+v", prepared)
|
|
}
|
|
if prepared.SelectedProfileID != "exec" {
|
|
t.Fatalf("expected selected profile exec, got %q", prepared.SelectedProfileID)
|
|
}
|
|
if prepared.PromptHash == "" || prepared.RenderedPromptHash == "" {
|
|
t.Fatal("expected prompt hashes")
|
|
}
|
|
if prepared.EffectiveModelParams.Model != "m" || prepared.EffectiveModelParams.Endpoint != "http://override/v1" {
|
|
t.Fatalf("unexpected model params: %+v", prepared.EffectiveModelParams)
|
|
}
|
|
if prepared.OutputContract.Format != domain.FormatMarkdown {
|
|
t.Fatalf("expected output format markdown, got %q", prepared.OutputContract.Format)
|
|
}
|
|
if len(prepared.InputHashes) != 2 || prepared.InputHashes["transcript"] == "" || prepared.InputHashes["glossary"] == "" {
|
|
t.Fatalf("expected input hashes, got %#v", prepared.InputHashes)
|
|
}
|
|
if len(prepared.Messages) != 2 {
|
|
t.Fatalf("expected two messages, got %d", len(prepared.Messages))
|
|
}
|
|
if prepared.SessionID != "session-123" {
|
|
t.Fatalf("expected prepared session id, got %q", prepared.SessionID)
|
|
}
|
|
if llmClient.calls != 0 {
|
|
t.Fatalf("prepare should not call llm, calls=%d", llmClient.calls)
|
|
}
|
|
}
|
|
|
|
func TestRunnerDirectSessionResolution(t *testing.T) {
|
|
t.Run("direct value wins and changes only the rendered prompt hash", func(t *testing.T) {
|
|
def := promptDef(domain.FormatText, domain.ValidationNone, 0)
|
|
def.SessionID = "template-{{.template_session}}"
|
|
promptRepo := &fakePromptRepo{def: def}
|
|
runner := NewRunner(
|
|
promptRepo,
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}},
|
|
nil,
|
|
defaultArtifactReader(),
|
|
prompt.NewGoRenderer(),
|
|
&fakeLLM{forbid: true},
|
|
nil, nil)
|
|
|
|
req := domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
Vars: map[string]string{"template_session": "from-template"},
|
|
}
|
|
|
|
req.SessionID = " direct-one "
|
|
first, err := runner.Prepare(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("prepare first direct session: %v", err)
|
|
}
|
|
req.SessionID = "direct-two"
|
|
second, err := runner.Prepare(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("prepare second direct session: %v", err)
|
|
}
|
|
|
|
if first.SessionID != "direct-one" || second.SessionID != "direct-two" {
|
|
t.Fatalf("direct sessions were not normalized: first=%q second=%q", first.SessionID, second.SessionID)
|
|
}
|
|
if first.PromptHash != second.PromptHash {
|
|
t.Fatalf("direct session changed prompt-definition hash: first=%q second=%q", first.PromptHash, second.PromptHash)
|
|
}
|
|
if first.RenderedPromptHash == second.RenderedPromptHash {
|
|
t.Fatal("changing direct session did not change rendered-prompt hash")
|
|
}
|
|
if def.SessionID != "template-{{.template_session}}" {
|
|
t.Fatalf("repository-owned prompt definition was mutated: %q", def.SessionID)
|
|
}
|
|
})
|
|
|
|
t.Run("direct value bypasses failing session template without changing messages", func(t *testing.T) {
|
|
def := promptDef(domain.FormatText, domain.ValidationNone, 0)
|
|
def.SessionID = "{{.missing_session}}"
|
|
def.Templates = []domain.PromptMessageTemplate{
|
|
{Role: "user", Content: "Hello {{.name}}"},
|
|
}
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: def},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}},
|
|
nil,
|
|
defaultArtifactReader(),
|
|
prompt.NewGoRenderer(),
|
|
&fakeLLM{forbid: true},
|
|
nil, nil)
|
|
|
|
prepared, err := runner.Prepare(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
SessionID: "direct-session",
|
|
Inputs: singleInputRef(),
|
|
Vars: map[string]string{"name": "Rin"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("prepare with direct session: %v", err)
|
|
}
|
|
if prepared.SessionID != "direct-session" {
|
|
t.Fatalf("prepared session id = %q, want direct-session", prepared.SessionID)
|
|
}
|
|
if len(prepared.Messages) != 1 || prepared.Messages[0].Content != "Hello Rin" {
|
|
t.Fatalf("message templates did not render normally: %+v", prepared.Messages)
|
|
}
|
|
})
|
|
|
|
t.Run("blank direct value retains prompt template behavior", func(t *testing.T) {
|
|
def := promptDef(domain.FormatText, domain.ValidationNone, 0)
|
|
def.SessionID = " template-{{.template_session}} "
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: def},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}},
|
|
nil,
|
|
defaultArtifactReader(),
|
|
prompt.NewGoRenderer(),
|
|
&fakeLLM{forbid: true},
|
|
nil, nil)
|
|
|
|
prepared, err := runner.Prepare(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
SessionID: " \t ",
|
|
Inputs: singleInputRef(),
|
|
Vars: map[string]string{"template_session": "rendered"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("prepare with prompt session template: %v", err)
|
|
}
|
|
if prepared.SessionID != "template-rendered" {
|
|
t.Fatalf("prepared session id = %q, want template-rendered", prepared.SessionID)
|
|
}
|
|
})
|
|
|
|
t.Run("overlong direct value fails before loading or generation", func(t *testing.T) {
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: "unexpected"}}
|
|
runner := NewRunner(
|
|
promptRepo,
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}},
|
|
nil,
|
|
defaultArtifactReader(),
|
|
defaultRenderer(),
|
|
llmClient,
|
|
nil, nil)
|
|
|
|
_, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
SessionID: strings.Repeat("界", domain.SessionIDMaxLength+1),
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if !errors.Is(err, ErrInvalidRequest) {
|
|
t.Fatalf("expected ErrInvalidRequest, got %v", err)
|
|
}
|
|
if promptRepo.lastID != "" {
|
|
t.Fatalf("invalid direct session loaded prompt %q", promptRepo.lastID)
|
|
}
|
|
if llmClient.calls != 0 {
|
|
t.Fatalf("invalid direct session invoked generation %d times", llmClient.calls)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRunnerPrepareUsesPromptDefaultProfileWhenNoExplicitProfileID(t *testing.T) {
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
promptRepo.def.DefaultProfile = "from-prompt"
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"from-prompt": {ID: "from-prompt", Endpoint: "http://llm/v1", Model: "m"},
|
|
}}
|
|
|
|
runner := newMinimalRunner(promptRepo, execRepo)
|
|
prepared, err := runner.Prepare(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if execRepo.lastID != "from-prompt" {
|
|
t.Fatalf("expected prompt default profile lookup, got %q", execRepo.lastID)
|
|
}
|
|
if prepared.SelectedProfileID != "from-prompt" {
|
|
t.Fatalf("expected selected profile from-prompt, got %q", prepared.SelectedProfileID)
|
|
}
|
|
}
|
|
|
|
func TestRunnerPrepareMissingExplicitProfileAndMissingDefaultProfileFails(t *testing.T) {
|
|
repo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
repo.def.DefaultProfile = ""
|
|
runner := newMinimalRunner(repo, &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}})
|
|
_, err := runner.Prepare(context.Background(), domain.RunRequest{PromptID: "p", Inputs: singleInputRef()})
|
|
if !errors.Is(err, ErrInvalidRequest) {
|
|
t.Fatalf("expected ErrInvalidRequest, got %v", err)
|
|
}
|
|
if !errors.Is(err, ErrProfileRequired) {
|
|
t.Fatalf("expected ErrProfileRequired, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerPrepareSelectedProfileDoesNotExistFails(t *testing.T) {
|
|
repo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
repo.def.DefaultProfile = "does-not-exist"
|
|
runner := newMinimalRunner(repo, &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{}})
|
|
_, err := runner.Prepare(context.Background(), domain.RunRequest{PromptID: "p", Inputs: singleInputRef()})
|
|
if !errors.Is(err, ErrProfileLoad) {
|
|
t.Fatalf("expected ErrProfileLoad, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerPreparePromptLoadFailure(t *testing.T) {
|
|
runner := NewRunner(&fakePromptRepo{err: errors.New("boom")}, &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}, nil, defaultArtifactReader(), defaultRenderer(), &fakeLLM{}, nil, nil)
|
|
_, err := runner.Prepare(context.Background(), domain.RunRequest{PromptID: "p"})
|
|
if !errors.Is(err, ErrPromptLoad) {
|
|
t.Fatalf("expected ErrPromptLoad, got %v", err)
|
|
}
|
|
if errors.Is(err, ErrProfileLoad) {
|
|
t.Fatalf("did not expect ErrProfileLoad, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerPrepareRuntimeOverrideBeatsSelectedProfileValue(t *testing.T) {
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {
|
|
ID: "exec",
|
|
Endpoint: "http://profile/v1",
|
|
Model: "profile-model",
|
|
Temperature: 0.2,
|
|
MaxTokens: 500,
|
|
TopP: 0.9,
|
|
TimeoutSeconds: 120,
|
|
ServiceTier: "priority",
|
|
},
|
|
}}
|
|
runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), &fakeLLM{forbid: true}, nil, nil)
|
|
|
|
prepared, err := runner.Prepare(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
Execution: &domain.ExecutionTargetOverride{
|
|
Endpoint: "http://override/v1",
|
|
Model: "override-model",
|
|
Temperature: float64Ptr(0.7),
|
|
TimeoutSeconds: intPtr(30),
|
|
ServiceTier: "flex",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if prepared.EffectiveModelParams.Endpoint != "http://override/v1" || prepared.EffectiveModelParams.Model != "override-model" {
|
|
t.Fatalf("expected endpoint/model override to win, got %+v", prepared.EffectiveModelParams)
|
|
}
|
|
if prepared.EffectiveModelParams.TopP != 0.9 {
|
|
t.Fatalf("expected profile top_p to remain, got %v", prepared.EffectiveModelParams.TopP)
|
|
}
|
|
if prepared.EffectiveModelParams.ServiceTier != "flex" {
|
|
t.Fatalf("expected service_tier override to win, got %q", prepared.EffectiveModelParams.ServiceTier)
|
|
}
|
|
}
|
|
|
|
func TestRunnerPrepareRequestNumericOverridePresence(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
override *domain.ExecutionTargetOverride
|
|
wantTemperature float64
|
|
wantMaxTokens int
|
|
wantTopP float64
|
|
wantTimeoutSecs int
|
|
wantPresence domain.ExecutionTargetPresence
|
|
}{
|
|
{
|
|
name: "omitted preserves profile values",
|
|
override: &domain.ExecutionTargetOverride{},
|
|
wantTemperature: 0.7,
|
|
wantMaxTokens: 321,
|
|
wantTopP: 0.8,
|
|
wantTimeoutSecs: 45,
|
|
},
|
|
{
|
|
name: "explicit zero temperature",
|
|
override: &domain.ExecutionTargetOverride{Temperature: float64Ptr(0)},
|
|
wantTemperature: 0,
|
|
wantMaxTokens: 321,
|
|
wantTopP: 0.8,
|
|
wantTimeoutSecs: 45,
|
|
wantPresence: domain.ExecutionTargetPresence{Temperature: true},
|
|
},
|
|
{
|
|
name: "explicit zero max tokens",
|
|
override: &domain.ExecutionTargetOverride{MaxTokens: intPtr(0)},
|
|
wantTemperature: 0.7,
|
|
wantMaxTokens: 0,
|
|
wantTopP: 0.8,
|
|
wantTimeoutSecs: 45,
|
|
wantPresence: domain.ExecutionTargetPresence{MaxTokens: true},
|
|
},
|
|
{
|
|
name: "explicit zero top p",
|
|
override: &domain.ExecutionTargetOverride{TopP: float64Ptr(0)},
|
|
wantTemperature: 0.7,
|
|
wantMaxTokens: 321,
|
|
wantTopP: 0,
|
|
wantTimeoutSecs: 45,
|
|
wantPresence: domain.ExecutionTargetPresence{TopP: true},
|
|
},
|
|
{
|
|
name: "explicit zero timeout",
|
|
override: &domain.ExecutionTargetOverride{TimeoutSeconds: intPtr(0)},
|
|
wantTemperature: 0.7,
|
|
wantMaxTokens: 321,
|
|
wantTopP: 0.8,
|
|
wantTimeoutSecs: 0,
|
|
wantPresence: domain.ExecutionTargetPresence{TimeoutSeconds: true},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {
|
|
ID: "exec",
|
|
Endpoint: "http://profile/v1",
|
|
Model: "profile-model",
|
|
Temperature: 0.7,
|
|
MaxTokens: 321,
|
|
TopP: 0.8,
|
|
TimeoutSeconds: 45,
|
|
},
|
|
}}, nil,
|
|
|
|
defaultArtifactReader(),
|
|
defaultRenderer(),
|
|
&fakeLLM{forbid: true},
|
|
nil, nil)
|
|
|
|
prepared, err := runner.Prepare(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
Execution: tc.override,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
got := prepared.EffectiveModelParams
|
|
if got.Temperature != tc.wantTemperature ||
|
|
got.MaxTokens != tc.wantMaxTokens ||
|
|
got.TopP != tc.wantTopP ||
|
|
got.TimeoutSeconds != tc.wantTimeoutSecs {
|
|
t.Fatalf("unexpected effective numeric settings: %+v", got)
|
|
}
|
|
if prepared.TargetPresence != tc.wantPresence {
|
|
t.Fatalf("unexpected target presence: got %+v want %+v", prepared.TargetPresence, tc.wantPresence)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunnerPrepareInvalidRequestNumericOverridesFail(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
override *domain.ExecutionTargetOverride
|
|
}{
|
|
{name: "temperature below range", override: &domain.ExecutionTargetOverride{Temperature: float64Ptr(-0.1)}},
|
|
{name: "temperature above range", override: &domain.ExecutionTargetOverride{Temperature: float64Ptr(2.1)}},
|
|
{name: "max tokens below range", override: &domain.ExecutionTargetOverride{MaxTokens: intPtr(-1)}},
|
|
{name: "top p below range", override: &domain.ExecutionTargetOverride{TopP: float64Ptr(-0.1)}},
|
|
{name: "top p above range", override: &domain.ExecutionTargetOverride{TopP: float64Ptr(1.1)}},
|
|
{name: "timeout below range", override: &domain.ExecutionTargetOverride{TimeoutSeconds: intPtr(-1)}},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}, nil,
|
|
|
|
defaultArtifactReader(),
|
|
defaultRenderer(),
|
|
&fakeLLM{forbid: true},
|
|
nil, nil)
|
|
|
|
_, err := runner.Prepare(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
Execution: tc.override,
|
|
})
|
|
if !errors.Is(err, ErrInvalidRequest) {
|
|
t.Fatalf("expected ErrInvalidRequest, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunnerPrepareSelectedProfileBeatsBuiltInDefault(t *testing.T) {
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {
|
|
ID: "exec",
|
|
Endpoint: "http://profile/v1",
|
|
Model: "profile-model",
|
|
TopP: 0.8,
|
|
TimeoutSeconds: 90,
|
|
ServiceTier: "priority",
|
|
},
|
|
}}
|
|
runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), &fakeLLM{forbid: true}, nil, nil)
|
|
|
|
prepared, err := runner.Prepare(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if prepared.EffectiveModelParams.TopP != 0.8 {
|
|
t.Fatalf("expected profile top_p to beat default, got %v", prepared.EffectiveModelParams.TopP)
|
|
}
|
|
if prepared.EffectiveModelParams.TimeoutSeconds != 90 {
|
|
t.Fatalf("expected profile timeout to beat default, got %d", prepared.EffectiveModelParams.TimeoutSeconds)
|
|
}
|
|
if prepared.EffectiveModelParams.ServiceTier != "priority" {
|
|
t.Fatalf("expected profile service_tier to beat default, got %q", prepared.EffectiveModelParams.ServiceTier)
|
|
}
|
|
}
|
|
|
|
func TestRunnerPrepareFileBackedPromptBodiesRenderCorrectly(t *testing.T) {
|
|
promptDir := filepath.Join("..", "promptdef", "testdata")
|
|
profileDir := filepath.Join("..", "profile", "testdata")
|
|
|
|
reader := &fakeArtifactReader{
|
|
artifactsByURI: map[string]*domain.Artifact{
|
|
"a://transcript": {
|
|
Name: "transcript",
|
|
Body: []byte("Session transcript body."),
|
|
Hash: hashString("Session transcript body."),
|
|
},
|
|
},
|
|
}
|
|
llmClient := &fakeLLM{forbid: true}
|
|
runner := NewRunner(
|
|
promptdef.NewFilesystemRepository(promptDir),
|
|
profile.NewFilesystemRepository(profileDir), nil,
|
|
|
|
reader,
|
|
prompt.NewGoRenderer(),
|
|
llmClient,
|
|
nil, nil)
|
|
|
|
prepared, err := runner.Prepare(context.Background(), domain.RunRequest{
|
|
PromptID: "valid-file-backed",
|
|
ProfileID: "local-default",
|
|
Inputs: map[string]domain.ArtifactRef{
|
|
"transcript": {Type: domain.ArtifactRefFile, URI: "a://transcript"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if len(prepared.Messages) != 2 {
|
|
t.Fatalf("expected two rendered messages, got %d", len(prepared.Messages))
|
|
}
|
|
if !strings.Contains(prepared.Messages[1].Content, "Session transcript body.") {
|
|
t.Fatalf("expected file-backed template content to render input, got %q", prepared.Messages[1].Content)
|
|
}
|
|
}
|
|
|
|
func TestRunnerPrepareRequiredInputMissingFails(t *testing.T) {
|
|
def := promptDef(domain.FormatText, domain.ValidationNone, 0)
|
|
def.Templates = []domain.PromptMessageTemplate{{Role: "user", Content: `{{input "transcript"}}`}}
|
|
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: def},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}, nil,
|
|
|
|
defaultArtifactReader(),
|
|
prompt.NewGoRenderer(),
|
|
&fakeLLM{forbid: true},
|
|
nil, nil)
|
|
|
|
_, err := runner.Prepare(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: map[string]domain.ArtifactRef{},
|
|
})
|
|
if !errors.Is(err, ErrPromptRender) {
|
|
t.Fatalf("expected ErrPromptRender, got %v", err)
|
|
}
|
|
if !errors.Is(err, prompt.ErrMissingRequiredInput) {
|
|
t.Fatalf("expected ErrMissingRequiredInput, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerPrepareUnknownTemplateInputReferenceFails(t *testing.T) {
|
|
def := promptDef(domain.FormatText, domain.ValidationNone, 0)
|
|
def.Inputs = []domain.PromptInput{{Name: "transcript", Required: false}}
|
|
def.Templates = []domain.PromptMessageTemplate{{Role: "user", Content: `{{input "ghost"}}`}}
|
|
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: def},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}, nil,
|
|
|
|
defaultArtifactReader(),
|
|
prompt.NewGoRenderer(),
|
|
&fakeLLM{forbid: true},
|
|
nil, nil)
|
|
|
|
_, err := runner.Prepare(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: map[string]domain.ArtifactRef{},
|
|
})
|
|
if !errors.Is(err, ErrPromptRender) {
|
|
t.Fatalf("expected ErrPromptRender, got %v", err)
|
|
}
|
|
if !errors.Is(err, prompt.ErrUnknownInput) {
|
|
t.Fatalf("expected ErrUnknownInput, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerPrepareAPIKeyEnvNameIncludedButNotResolvedValue(t *testing.T) {
|
|
const envName = "PROMPTKIT_TEST_API_KEY"
|
|
const secret = "top-secret-value"
|
|
t.Setenv(envName, secret)
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", Endpoint: "http://profile/v1", Model: "profile-model", APIKeyEnv: envName},
|
|
}}
|
|
runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), &fakeLLM{forbid: true}, nil, nil)
|
|
|
|
prepared, err := runner.Prepare(context.Background(), domain.RunRequest{PromptID: "p", ProfileID: "exec", Inputs: singleInputRef()})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if prepared.EffectiveModelParams.APIKeyEnv != envName {
|
|
t.Fatalf("expected api key env name, got %q", prepared.EffectiveModelParams.APIKeyEnv)
|
|
}
|
|
metadataDump := fmt.Sprintf("%+v|%s|%s", prepared.EffectiveModelParams, prepared.PromptHash, prepared.RenderedPromptHash)
|
|
if strings.Contains(metadataDump, secret) {
|
|
t.Fatalf("unexpected api key value in prepared metadata dump: %s", metadataDump)
|
|
}
|
|
}
|
|
|
|
func TestRunnerPrepareJSONSchemaBuildsStructuredOutputSpec(t *testing.T) {
|
|
def := promptDef(domain.FormatJSON, domain.ValidationJSONSchema, 0)
|
|
def.Validation.SchemaPath = "events.schema.json"
|
|
validator := &fakeValidator{
|
|
schemaDoc: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"events": map[string]any{"type": "array"},
|
|
},
|
|
},
|
|
}
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: def},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}, nil,
|
|
|
|
defaultArtifactReader(),
|
|
defaultRenderer(),
|
|
&fakeLLM{forbid: true},
|
|
validator, nil)
|
|
|
|
prepared, err := runner.Prepare(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if validator.schemaLoads != 1 {
|
|
t.Fatalf("expected one schema load, got %d", validator.schemaLoads)
|
|
}
|
|
if validator.schemaLoadPath != "events.schema.json" {
|
|
t.Fatalf("expected schema path events.schema.json, got %q", validator.schemaLoadPath)
|
|
}
|
|
if prepared.StructuredOutput == nil {
|
|
t.Fatal("expected structured output spec")
|
|
}
|
|
if prepared.StructuredOutput.Type != domain.StructuredOutputJSONSchema {
|
|
t.Fatalf("expected structured output type json_schema, got %q", prepared.StructuredOutput.Type)
|
|
}
|
|
if prepared.StructuredOutput.JSONSchema == nil {
|
|
t.Fatal("expected structured output json_schema payload")
|
|
}
|
|
if prepared.StructuredOutput.JSONSchema.Name != "p_1" {
|
|
t.Fatalf("expected derived schema name p_1, got %q", prepared.StructuredOutput.JSONSchema.Name)
|
|
}
|
|
if prepared.StructuredOutput.JSONSchema.Strict != true {
|
|
t.Fatalf("expected strict=true, got %v", prepared.StructuredOutput.JSONSchema.Strict)
|
|
}
|
|
}
|
|
|
|
func TestRunnerPrepareJSONSchemaSchemaLoadFailureReturnsValidationError(t *testing.T) {
|
|
def := promptDef(domain.FormatJSON, domain.ValidationJSONSchema, 0)
|
|
def.Validation.SchemaPath = "missing.schema.json"
|
|
validator := &fakeValidator{schemaErr: errors.New("schema unavailable")}
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: def},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}, nil,
|
|
|
|
defaultArtifactReader(),
|
|
defaultRenderer(),
|
|
&fakeLLM{forbid: true},
|
|
validator, nil)
|
|
|
|
_, err := runner.Prepare(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if !errors.Is(err, ErrValidation) {
|
|
t.Fatalf("expected ErrValidation, got %v", err)
|
|
}
|
|
if validator.schemaLoads != 1 {
|
|
t.Fatalf("expected one schema load attempt, got %d", validator.schemaLoads)
|
|
}
|
|
if validator.schemaLoadPath != "missing.schema.json" {
|
|
t.Fatalf("expected schema path missing.schema.json, got %q", validator.schemaLoadPath)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunJSONSchemaSchemaLoadFailureFailsBeforeLLM(t *testing.T) {
|
|
def := promptDef(domain.FormatJSON, domain.ValidationJSONSchema, 0)
|
|
def.Validation.SchemaPath = "missing.schema.json"
|
|
llmClient := &fakeLLM{forbid: true}
|
|
validator := &fakeValidator{schemaErr: errors.New("schema unavailable")}
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: def},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}, nil,
|
|
|
|
defaultArtifactReader(),
|
|
defaultRenderer(),
|
|
llmClient,
|
|
validator, nil)
|
|
|
|
_, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if !errors.Is(err, ErrValidation) {
|
|
t.Fatalf("expected ErrValidation, got %v", err)
|
|
}
|
|
if llmClient.calls != 0 {
|
|
t.Fatalf("expected llm not called when schema loading fails, calls=%d", llmClient.calls)
|
|
}
|
|
}
|
|
|
|
func TestDeriveStructuredSchemaName(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
id string
|
|
version string
|
|
want string
|
|
}{
|
|
{
|
|
name: "sanitizes punctuation and keeps dashes",
|
|
id: "prompt.id/alpha",
|
|
version: "1.0.0-beta",
|
|
want: "prompt_id_alpha_1_0_0-beta",
|
|
},
|
|
{
|
|
name: "fallback when empty",
|
|
id: "",
|
|
version: "",
|
|
want: "promptkit_schema",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := deriveStructuredSchemaName(tc.id, tc.version)
|
|
if got != tc.want {
|
|
t.Fatalf("expected %q, got %q", tc.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHashRenderedPromptIncludesCacheControlWhenPresent(t *testing.T) {
|
|
uncached := domain.RenderedPrompt{Messages: []domain.RenderedMessage{
|
|
{Role: "system", Content: "sys"},
|
|
{Role: "user", Content: "usr"},
|
|
}}
|
|
wantLegacyHash := hashString("system\nsys\n---\nuser\nusr\n---\n")
|
|
if got := hashRenderedPrompt(uncached); got != wantLegacyHash {
|
|
t.Fatalf("expected no-cache hash to preserve legacy input, got %q want %q", got, wantLegacyHash)
|
|
}
|
|
|
|
withCache := domain.RenderedPrompt{Messages: []domain.RenderedMessage{
|
|
{
|
|
Role: "system",
|
|
Content: "sys",
|
|
CacheControl: &domain.CacheControl{
|
|
Type: domain.CacheControlEphemeral,
|
|
TTL: "1h",
|
|
},
|
|
},
|
|
{Role: "user", Content: "usr"},
|
|
}}
|
|
alsoWithCache := domain.RenderedPrompt{Messages: []domain.RenderedMessage{
|
|
{
|
|
Role: "system",
|
|
Content: "sys",
|
|
CacheControl: &domain.CacheControl{
|
|
Type: domain.CacheControlEphemeral,
|
|
TTL: "1h",
|
|
},
|
|
},
|
|
{Role: "user", Content: "usr"},
|
|
}}
|
|
withoutTTL := domain.RenderedPrompt{Messages: []domain.RenderedMessage{
|
|
{
|
|
Role: "system",
|
|
Content: "sys",
|
|
CacheControl: &domain.CacheControl{
|
|
Type: domain.CacheControlEphemeral,
|
|
},
|
|
},
|
|
{Role: "user", Content: "usr"},
|
|
}}
|
|
|
|
cachedHash := hashRenderedPrompt(withCache)
|
|
if cachedHash == hashRenderedPrompt(uncached) {
|
|
t.Fatal("expected cache control to change rendered prompt hash")
|
|
}
|
|
if cachedHash != hashRenderedPrompt(alsoWithCache) {
|
|
t.Fatal("expected identical cache control metadata to produce stable hash")
|
|
}
|
|
if cachedHash == hashRenderedPrompt(withoutTTL) {
|
|
t.Fatal("expected ttl changes to affect rendered prompt hash")
|
|
}
|
|
}
|
|
|
|
func TestHashRenderedPromptIncludesSessionIDWhenPresent(t *testing.T) {
|
|
withoutSession := domain.RenderedPrompt{Messages: []domain.RenderedMessage{
|
|
{Role: "system", Content: "sys"},
|
|
{Role: "user", Content: "usr"},
|
|
}}
|
|
withSession := domain.RenderedPrompt{
|
|
SessionID: "session-123",
|
|
Messages: []domain.RenderedMessage{
|
|
{Role: "system", Content: "sys"},
|
|
{Role: "user", Content: "usr"},
|
|
},
|
|
}
|
|
alsoWithSession := domain.RenderedPrompt{
|
|
SessionID: "session-123",
|
|
Messages: []domain.RenderedMessage{
|
|
{Role: "system", Content: "sys"},
|
|
{Role: "user", Content: "usr"},
|
|
},
|
|
}
|
|
otherSession := domain.RenderedPrompt{
|
|
SessionID: "session-456",
|
|
Messages: []domain.RenderedMessage{
|
|
{Role: "system", Content: "sys"},
|
|
{Role: "user", Content: "usr"},
|
|
},
|
|
}
|
|
|
|
sessionHash := hashRenderedPrompt(withSession)
|
|
if sessionHash == hashRenderedPrompt(withoutSession) {
|
|
t.Fatal("expected session_id to change rendered prompt hash")
|
|
}
|
|
if sessionHash != hashRenderedPrompt(alsoWithSession) {
|
|
t.Fatal("expected identical session_id to produce stable hash")
|
|
}
|
|
if sessionHash == hashRenderedPrompt(otherSession) {
|
|
t.Fatal("expected session_id value changes to affect rendered prompt hash")
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunSuccessful(t *testing.T) {
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatMarkdown, domain.ValidationBasic, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}
|
|
reader := &fakeArtifactReader{artifactsByURI: map[string]*domain.Artifact{
|
|
"a://t": {Body: []byte("transcript"), Hash: hashString("transcript")},
|
|
"a://g": {Body: []byte("glossary"), Hash: hashString("glossary")},
|
|
}}
|
|
renderer := &fakeRenderer{rendered: &domain.RenderedPrompt{SessionID: "session-123", Messages: []domain.RenderedMessage{{Role: "system", Content: "sys"}, {Role: "user", Content: "usr"}}}}
|
|
llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: "# recap", Usage: domain.TokenUsage{TotalTokens: 7}}}
|
|
|
|
runner := NewRunner(promptRepo, execRepo, nil, reader, renderer, llmClient, nil, nil)
|
|
res, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
PromptVersion: "1",
|
|
ProfileID: "exec",
|
|
Inputs: map[string]domain.ArtifactRef{
|
|
"transcript": {Type: domain.ArtifactRefFile, URI: "a://t"},
|
|
"glossary": {Type: domain.ArtifactRefFile, URI: "a://g"},
|
|
},
|
|
Execution: &domain.ExecutionTargetOverride{Endpoint: "http://override/v1", Model: "m", Temperature: float64Ptr(0.3), TimeoutSeconds: intPtr(90)},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.PromptID != "p" || res.PromptVersion != "1" {
|
|
t.Fatalf("unexpected prompt metadata: %+v", res)
|
|
}
|
|
if res.SelectedProfileID != "exec" {
|
|
t.Fatalf("expected selected profile exec, got %q", res.SelectedProfileID)
|
|
}
|
|
if ok, _ := regexp.MatchString(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`, res.RunID); !ok {
|
|
t.Fatalf("invalid run id: %q", res.RunID)
|
|
}
|
|
if res.PromptHash == "" || res.RenderedPromptHash == "" {
|
|
t.Fatal("expected prompt hashes")
|
|
}
|
|
if res.EffectiveModelParams.Model != "m" || res.Endpoint != "http://override/v1" {
|
|
t.Fatalf("unexpected model params: %+v", res.EffectiveModelParams)
|
|
}
|
|
if res.Artifact.Name != defaults.OutputArtifactName {
|
|
t.Fatalf("expected default output artifact name %q, got %q", defaults.OutputArtifactName, res.Artifact.Name)
|
|
}
|
|
if res.Artifact.ContentType != defaults.ContentTypeTextMarkdown {
|
|
t.Fatalf("expected markdown content type %q, got %q", defaults.ContentTypeTextMarkdown, res.Artifact.ContentType)
|
|
}
|
|
if res.RawOutput != "# recap" {
|
|
t.Fatalf("expected raw output, got %q", res.RawOutput)
|
|
}
|
|
if res.Validation.Status != domain.ValidationSkipped {
|
|
t.Fatalf("expected skipped validation, got %q", res.Validation.Status)
|
|
}
|
|
if llmClient.lastReq.Target.TimeoutSeconds != 90 {
|
|
t.Fatalf("expected timeout propagation, got %d", llmClient.lastReq.Target.TimeoutSeconds)
|
|
}
|
|
if !llmClient.lastReq.TargetPresence.Temperature || !llmClient.lastReq.TargetPresence.TimeoutSeconds {
|
|
t.Fatalf("expected numeric override presence to be sent to llm, got %+v", llmClient.lastReq.TargetPresence)
|
|
}
|
|
if llmClient.lastReq.Prompt.SessionID != "session-123" {
|
|
t.Fatalf("expected session id to be sent to llm, got %q", llmClient.lastReq.Prompt.SessionID)
|
|
}
|
|
if res.SessionID != "session-123" {
|
|
t.Fatalf("expected session id in run result, got %q", res.SessionID)
|
|
}
|
|
if res.Usage.TotalTokens != 7 {
|
|
t.Fatalf("expected token usage to be retained, got %+v", res.Usage)
|
|
}
|
|
if res.StartTime.IsZero() || res.EndTime.IsZero() || res.EndTime.Before(res.StartTime) {
|
|
t.Fatalf("unexpected run timing: start=%v end=%v", res.StartTime, res.EndTime)
|
|
}
|
|
if res.Duration != res.EndTime.Sub(res.StartTime) {
|
|
t.Fatalf("expected duration %v, got %v", res.EndTime.Sub(res.StartTime), res.Duration)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunPassesExtraParamsToGenerateRequestTarget(t *testing.T) {
|
|
extraParams := map[string]any{
|
|
"string_value": "enabled",
|
|
"number_value": 42,
|
|
"boolean_value": true,
|
|
"object_value": map[string]any{"nested": "value"},
|
|
"array_value": []any{"first", 3, false},
|
|
}
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {
|
|
ID: "exec",
|
|
Endpoint: "http://profile/v1",
|
|
Model: "profile-model",
|
|
ExtraParams: extraParams,
|
|
},
|
|
}}
|
|
llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}}
|
|
runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), llmClient, nil, nil)
|
|
|
|
res, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if !reflect.DeepEqual(res.EffectiveModelParams.ExtraParams, extraParams) {
|
|
t.Fatalf("expected run result extra_params to match profile values, got %#v", res.EffectiveModelParams.ExtraParams)
|
|
}
|
|
if !reflect.DeepEqual(llmClient.lastReq.Target.ExtraParams, extraParams) {
|
|
t.Fatalf("expected generate request extra_params to match profile values, got %#v", llmClient.lastReq.Target.ExtraParams)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunAndPrepareResolveSameProfileAndEffectiveSettings(t *testing.T) {
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatMarkdown, domain.ValidationBasic, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}
|
|
reader := &fakeArtifactReader{artifactsByURI: map[string]*domain.Artifact{
|
|
"a://t": {Body: []byte("transcript"), Hash: hashString("transcript")},
|
|
}}
|
|
renderer := &fakeRenderer{rendered: &domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "system", Content: "sys"}, {Role: "user", Content: "usr"}}}}
|
|
llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: "# recap"}}
|
|
runner := NewRunner(promptRepo, execRepo, nil, reader, renderer, llmClient, nil, nil)
|
|
|
|
req := domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: map[string]domain.ArtifactRef{
|
|
"transcript": {Type: domain.ArtifactRefFile, URI: "a://t"},
|
|
},
|
|
Execution: &domain.ExecutionTargetOverride{Endpoint: "http://override/v1", Model: "m", Temperature: float64Ptr(0.3), TimeoutSeconds: intPtr(90)},
|
|
}
|
|
|
|
prepared, err := runner.Prepare(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("prepare should succeed, got %v", err)
|
|
}
|
|
|
|
res, err := runner.Run(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("run should succeed, got %v", err)
|
|
}
|
|
|
|
if res.SelectedProfileID != prepared.SelectedProfileID {
|
|
t.Fatalf("expected selected profile to match prepare, run=%q prepare=%q", res.SelectedProfileID, prepared.SelectedProfileID)
|
|
}
|
|
if !reflect.DeepEqual(res.EffectiveModelParams, prepared.EffectiveModelParams) {
|
|
t.Fatalf("effective model params mismatch:\nrun=%+v\nprepare=%+v", res.EffectiveModelParams, prepared.EffectiveModelParams)
|
|
}
|
|
if !reflect.DeepEqual(res.InputHashes, prepared.InputHashes) {
|
|
t.Fatalf("input hashes mismatch:\nrun=%#v\nprepare=%#v", res.InputHashes, prepared.InputHashes)
|
|
}
|
|
if res.RenderedPromptHash != prepared.RenderedPromptHash {
|
|
t.Fatalf("expected rendered prompt hash to match prepare, run=%q prepare=%q", res.RenderedPromptHash, prepared.RenderedPromptHash)
|
|
}
|
|
if !reflect.DeepEqual(llmClient.lastReq.Prompt.Messages, prepared.Messages) {
|
|
t.Fatalf("expected run to send prepare-rendered messages to llm")
|
|
}
|
|
}
|
|
|
|
func TestRunnerAdmissionUsesResolvedBackendIdentity(t *testing.T) {
|
|
t.Run("selected backend survives endpoint override", func(t *testing.T) {
|
|
admitter := &fakeRunAdmitter{}
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {
|
|
ID: "exec",
|
|
BackendID: "custom",
|
|
Model: "model",
|
|
},
|
|
}},
|
|
fakeBackendResolver{backends: map[string]domain.Backend{
|
|
"custom": {ID: "custom", Endpoint: "http://backend.example/v1"},
|
|
}},
|
|
defaultArtifactReader(),
|
|
defaultRenderer(),
|
|
&fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}},
|
|
nil,
|
|
admitter,
|
|
)
|
|
|
|
result, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
Execution: &domain.ExecutionTargetOverride{
|
|
Endpoint: "http://override.example/v1",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("run: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(admitter.backendIDs, []string{"custom"}) {
|
|
t.Fatalf("admitted backend IDs=%#v, want custom", admitter.backendIDs)
|
|
}
|
|
if result.SelectedBackendID != "custom" ||
|
|
result.Endpoint != "http://override.example/v1" {
|
|
t.Fatalf("unexpected routed result: %+v", result)
|
|
}
|
|
})
|
|
|
|
t.Run("endpoint-only preparation remains unrestricted", func(t *testing.T) {
|
|
admitter := &fakeRunAdmitter{}
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": defaultExecutionProfile(),
|
|
}},
|
|
nil,
|
|
defaultArtifactReader(),
|
|
defaultRenderer(),
|
|
&fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}},
|
|
nil,
|
|
admitter,
|
|
)
|
|
request := domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
}
|
|
|
|
if _, err := runner.Prepare(context.Background(), request); err != nil {
|
|
t.Fatalf("prepare: %v", err)
|
|
}
|
|
if len(admitter.backendIDs) != 0 {
|
|
t.Fatalf("prepare called admission with %#v", admitter.backendIDs)
|
|
}
|
|
if _, err := runner.Run(context.Background(), request); err != nil {
|
|
t.Fatalf("run: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(admitter.backendIDs, []string{""}) {
|
|
t.Fatalf("admitted backend IDs=%#v, want blank ID", admitter.backendIDs)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRunnerAdmissionFailureSkipsCompletionCollaborators(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
admissionError error
|
|
wantBackendContext bool
|
|
}{
|
|
{
|
|
name: "capacity exhausted",
|
|
admissionError: capacity.ErrCapacityExceeded,
|
|
wantBackendContext: true,
|
|
},
|
|
{
|
|
name: "context canceled",
|
|
admissionError: context.Canceled,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
def := promptDef(domain.FormatJSON, domain.ValidationJSONSchema, 1)
|
|
def.Validation.SchemaPath = "schema.json"
|
|
reader := defaultArtifactReader()
|
|
renderer := defaultRenderer()
|
|
llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: `{}`}}
|
|
validator := &fakeValidator{}
|
|
repairer := &fakeRepairer{
|
|
responses: []*domain.GenerateResponse{{Content: `{}`}},
|
|
}
|
|
admitter := &fakeRunAdmitter{err: tc.admissionError}
|
|
runner := NewRunnerWithRepairer(
|
|
&fakePromptRepo{def: def},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", BackendID: "custom", Model: "model"},
|
|
}},
|
|
fakeBackendResolver{backends: map[string]domain.Backend{
|
|
"custom": {ID: "custom", Endpoint: "http://backend.example/v1"},
|
|
}},
|
|
reader,
|
|
renderer,
|
|
llmClient,
|
|
validator,
|
|
repairer,
|
|
admitter,
|
|
)
|
|
|
|
result, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if result != nil {
|
|
t.Fatalf("admission failure returned partial result: %+v", result)
|
|
}
|
|
if !errors.Is(err, tc.admissionError) {
|
|
t.Fatalf("admission error=%v, want identity %v", err, tc.admissionError)
|
|
}
|
|
if errors.Is(err, ErrInvalidRequest) || errors.Is(err, ErrLLMGenerate) {
|
|
t.Fatalf("admission error was recategorized: %v", err)
|
|
}
|
|
if tc.wantBackendContext && !strings.Contains(err.Error(), "custom") {
|
|
t.Fatalf("capacity error lacks backend context: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(admitter.backendIDs, []string{"custom"}) {
|
|
t.Fatalf("admitted backend IDs=%#v, want custom", admitter.backendIDs)
|
|
}
|
|
if admitter.releaseCalls != 0 ||
|
|
validator.schemaLoads != 0 ||
|
|
validator.validateCalls != 0 ||
|
|
reader.calls != 0 ||
|
|
renderer.calls != 0 ||
|
|
llmClient.calls != 0 ||
|
|
repairer.calls != 0 {
|
|
t.Fatalf(
|
|
"later collaborators invoked: releases=%d schema=%d validate=%d artifacts=%d render=%d llm=%d repair=%d",
|
|
admitter.releaseCalls,
|
|
validator.schemaLoads,
|
|
validator.validateCalls,
|
|
reader.calls,
|
|
renderer.calls,
|
|
llmClient.calls,
|
|
repairer.calls,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunnerReleasesAdmissionAcrossRunOutcomes(t *testing.T) {
|
|
artifactFailure := errors.New("artifact failed")
|
|
generationFailure := errors.New("generation failed")
|
|
validationFailure := errors.New("validation failed")
|
|
tests := []struct {
|
|
name string
|
|
artifactError error
|
|
generationErr error
|
|
validationErr error
|
|
wantError error
|
|
}{
|
|
{name: "success"},
|
|
{
|
|
name: "completion failure",
|
|
artifactError: artifactFailure,
|
|
wantError: ErrArtifactLoad,
|
|
},
|
|
{
|
|
name: "generation failure",
|
|
generationErr: generationFailure,
|
|
wantError: ErrLLMGenerate,
|
|
},
|
|
{
|
|
name: "validation failure",
|
|
validationErr: validationFailure,
|
|
wantError: ErrValidation,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
reader := defaultArtifactReader()
|
|
if tc.artifactError != nil {
|
|
reader.errByURI = map[string]error{"a://ok": tc.artifactError}
|
|
}
|
|
llmClient := &fakeLLM{
|
|
resp: &domain.GenerateResponse{Content: "ok"},
|
|
err: tc.generationErr,
|
|
}
|
|
validator := &fakeValidator{
|
|
result: domain.ValidationResult{
|
|
Status: domain.ValidationPassed,
|
|
Mode: domain.ValidationBasic,
|
|
IsValid: true,
|
|
},
|
|
err: tc.validationErr,
|
|
}
|
|
admitter := &fakeRunAdmitter{}
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationBasic, 0)},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": defaultExecutionProfile(),
|
|
}},
|
|
nil,
|
|
reader,
|
|
defaultRenderer(),
|
|
llmClient,
|
|
validator,
|
|
admitter,
|
|
)
|
|
|
|
result, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if tc.wantError == nil {
|
|
if err != nil || result == nil {
|
|
t.Fatalf("successful run=(%+v, %v)", result, err)
|
|
}
|
|
} else {
|
|
if result != nil || !errors.Is(err, tc.wantError) {
|
|
t.Fatalf("failed run=(%+v, %v), want %v", result, err, tc.wantError)
|
|
}
|
|
}
|
|
if len(admitter.backendIDs) != 1 || admitter.releaseCalls != 1 {
|
|
t.Fatalf("admission calls=%#v releases=%d, want one each",
|
|
admitter.backendIDs, admitter.releaseCalls)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunExplicitProfileIDIsUsed(t *testing.T) {
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
promptRepo.def.DefaultProfile = "default-prof"
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"explicit-prof": {ID: "explicit-prof", Endpoint: "http://explicit/v1", Model: "explicit"},
|
|
"default-prof": {ID: "default-prof", Endpoint: "http://default/v1", Model: "default"},
|
|
}}
|
|
|
|
runner := newMinimalRunner(promptRepo, execRepo)
|
|
res, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "explicit-prof",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if execRepo.lastID != "explicit-prof" {
|
|
t.Fatalf("expected explicit profile lookup, got %q", execRepo.lastID)
|
|
}
|
|
if res.SelectedProfileID != "explicit-prof" {
|
|
t.Fatalf("expected selected profile explicit-prof, got %q", res.SelectedProfileID)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunPromptDefaultProfileIsUsedWhenNoExplicitProfileID(t *testing.T) {
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
promptRepo.def.DefaultProfile = "from-prompt"
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"from-prompt": {ID: "from-prompt", Endpoint: "http://llm/v1", Model: "m"},
|
|
}}
|
|
|
|
runner := newMinimalRunner(promptRepo, execRepo)
|
|
res, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if execRepo.lastID != "from-prompt" {
|
|
t.Fatalf("expected prompt default profile lookup, got %q", execRepo.lastID)
|
|
}
|
|
if res.SelectedProfileID != "from-prompt" {
|
|
t.Fatalf("expected selected profile from-prompt, got %q", res.SelectedProfileID)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunMissingExplicitProfileAndMissingDefaultProfileFails(t *testing.T) {
|
|
repo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
repo.def.DefaultProfile = ""
|
|
runner := newMinimalRunner(repo, &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}})
|
|
_, err := runner.Run(context.Background(), domain.RunRequest{PromptID: "p", Inputs: singleInputRef()})
|
|
if !errors.Is(err, ErrInvalidRequest) {
|
|
t.Fatalf("expected ErrInvalidRequest, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunInvalidDefaultProfileFails(t *testing.T) {
|
|
repo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
repo.def.DefaultProfile = "does-not-exist"
|
|
runner := newMinimalRunner(repo, &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{}})
|
|
_, err := runner.Run(context.Background(), domain.RunRequest{PromptID: "p", Inputs: singleInputRef()})
|
|
if !errors.Is(err, ErrProfileLoad) {
|
|
t.Fatalf("expected ErrProfileLoad, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunExecutionProfileLoadFailure(t *testing.T) {
|
|
repo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
runner := newMinimalRunner(repo, &fakeExecutionProfileRepo{err: errors.New("load failed")})
|
|
_, err := runner.Run(context.Background(), domain.RunRequest{PromptID: "p", ProfileID: "exec", Inputs: singleInputRef()})
|
|
if !errors.Is(err, ErrProfileLoad) {
|
|
t.Fatalf("expected profile load failure, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunExplicitRuntimeOverrideBeatsSelectedProfileValue(t *testing.T) {
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {
|
|
ID: "exec",
|
|
Endpoint: "http://profile/v1",
|
|
Model: "profile-model",
|
|
Temperature: 0.2,
|
|
MaxTokens: 500,
|
|
TopP: 0.9,
|
|
TimeoutSeconds: 120,
|
|
ServiceTier: "priority",
|
|
},
|
|
}}
|
|
llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}}
|
|
runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), llmClient, nil, nil)
|
|
|
|
res, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
Execution: &domain.ExecutionTargetOverride{
|
|
Endpoint: "http://override/v1",
|
|
Model: "override-model",
|
|
Temperature: float64Ptr(0.7),
|
|
TimeoutSeconds: intPtr(30),
|
|
ServiceTier: "flex",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.Endpoint != "http://override/v1" || res.ModelName != "override-model" {
|
|
t.Fatalf("expected endpoint/model override to win, got endpoint=%q model=%q", res.Endpoint, res.ModelName)
|
|
}
|
|
if res.EffectiveModelParams.Temperature != 0.7 || res.EffectiveModelParams.TimeoutSeconds != 30 {
|
|
t.Fatalf("expected numeric override to win, got %+v", res.EffectiveModelParams)
|
|
}
|
|
if res.EffectiveModelParams.TopP != 0.9 {
|
|
t.Fatalf("expected non-overridden profile top_p to remain, got %v", res.EffectiveModelParams.TopP)
|
|
}
|
|
if res.EffectiveModelParams.ServiceTier != "flex" {
|
|
t.Fatalf("expected service_tier override to win, got %q", res.EffectiveModelParams.ServiceTier)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunSelectedProfileBeatsBuiltInDefault(t *testing.T) {
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {
|
|
ID: "exec",
|
|
Endpoint: "http://profile/v1",
|
|
Model: "profile-model",
|
|
TopP: 0.8,
|
|
TimeoutSeconds: 90,
|
|
ServiceTier: "priority",
|
|
},
|
|
}}
|
|
llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}}
|
|
runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), llmClient, nil, nil)
|
|
|
|
res, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.EffectiveModelParams.TopP != 0.8 {
|
|
t.Fatalf("expected profile top_p to beat default, got %v", res.EffectiveModelParams.TopP)
|
|
}
|
|
if res.EffectiveModelParams.TimeoutSeconds != 90 {
|
|
t.Fatalf("expected profile timeout to beat default, got %d", res.EffectiveModelParams.TimeoutSeconds)
|
|
}
|
|
if res.EffectiveModelParams.ServiceTier != "priority" {
|
|
t.Fatalf("expected profile service_tier to beat default, got %q", res.EffectiveModelParams.ServiceTier)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunBuiltInDefaultsUsedWhenProfileOmitsOptionalFields(t *testing.T) {
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", Endpoint: "http://profile/v1", Model: "profile-model"},
|
|
}}
|
|
llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}}
|
|
runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), llmClient, nil, nil)
|
|
|
|
res, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.EffectiveModelParams.Temperature != defaults.ExecutionDefaultTemperature {
|
|
t.Fatalf("expected default temperature %v, got %v", defaults.ExecutionDefaultTemperature, res.EffectiveModelParams.Temperature)
|
|
}
|
|
if res.EffectiveModelParams.TopP != defaults.ExecutionDefaultTopP {
|
|
t.Fatalf("expected default top_p %v, got %v", defaults.ExecutionDefaultTopP, res.EffectiveModelParams.TopP)
|
|
}
|
|
if res.EffectiveModelParams.MaxTokens != defaults.ExecutionDefaultMaxTokens {
|
|
t.Fatalf("expected default max_tokens %d, got %d", defaults.ExecutionDefaultMaxTokens, res.EffectiveModelParams.MaxTokens)
|
|
}
|
|
if res.EffectiveModelParams.TimeoutSeconds != defaults.ExecutionDefaultTimeoutSeconds {
|
|
t.Fatalf("expected default timeout_seconds %d, got %d", defaults.ExecutionDefaultTimeoutSeconds, res.EffectiveModelParams.TimeoutSeconds)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunAPIKeyEnvResolvesFromEnvironment(t *testing.T) {
|
|
t.Setenv("PROMPTKIT_TEST_API_KEY", "secret")
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", Endpoint: "http://profile/v1", Model: "profile-model", APIKeyEnv: "PROMPTKIT_TEST_API_KEY"},
|
|
}}
|
|
runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), &fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}}, nil, nil)
|
|
|
|
res, err := runner.Run(context.Background(), domain.RunRequest{PromptID: "p", ProfileID: "exec", Inputs: singleInputRef()})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.EffectiveModelParams.APIKeyEnv != "PROMPTKIT_TEST_API_KEY" {
|
|
t.Fatalf("expected api_key_env name in effective params, got %q", res.EffectiveModelParams.APIKeyEnv)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunAPIKeyEnvMissingEnvironmentValueFailsClearly(t *testing.T) {
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", Endpoint: "http://profile/v1", Model: "profile-model", APIKeyEnv: "PROMPTKIT_MISSING_KEY"},
|
|
}}
|
|
runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), &fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}}, nil, nil)
|
|
|
|
_, err := runner.Run(context.Background(), domain.RunRequest{PromptID: "p", ProfileID: "exec", Inputs: singleInputRef()})
|
|
if !errors.Is(err, ErrInvalidRequest) {
|
|
t.Fatalf("expected ErrInvalidRequest, got %v", err)
|
|
}
|
|
if !errors.Is(err, ErrAPIKeyEnvMissing) {
|
|
t.Fatalf("expected ErrAPIKeyEnvMissing, got %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "PROMPTKIT_MISSING_KEY") {
|
|
t.Fatalf("expected missing env name in error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunDirectAPIKeyBypassesMissingEnvAndReachesLLM(t *testing.T) {
|
|
const directKey = "direct-runner-key"
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", Endpoint: "http://profile/v1", Model: "profile-model", APIKeyEnv: "PROMPTKIT_MISSING_KEY"},
|
|
}}
|
|
llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}}
|
|
runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), llmClient, nil, nil)
|
|
|
|
_, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
APIKey: directKey,
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if llmClient.lastReq.Target.APIKey != directKey {
|
|
t.Fatalf("expected direct API key to reach LLM request")
|
|
}
|
|
if llmClient.lastReq.Target.APIKeyEnv != "PROMPTKIT_MISSING_KEY" {
|
|
t.Fatalf("expected api_key_env name to remain on target, got %q", llmClient.lastReq.Target.APIKeyEnv)
|
|
}
|
|
}
|
|
|
|
func TestRunnerPrepareAPIKeyRequiredFailsWithoutDirectKey(t *testing.T) {
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", Endpoint: "http://profile/v1", Model: "profile-model", APIKeyRequired: true},
|
|
}}
|
|
runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), &fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}}, nil, nil)
|
|
|
|
_, err := runner.Prepare(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if !errors.Is(err, ErrAPIKeyRequired) {
|
|
t.Fatalf("expected ErrAPIKeyRequired, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunAPIKeyRequiredSucceedsWithDirectKey(t *testing.T) {
|
|
const directKey = "direct-required-key"
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", Endpoint: "http://profile/v1", Model: "profile-model", APIKeyRequired: true},
|
|
}}
|
|
llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}}
|
|
runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), llmClient, nil, nil)
|
|
|
|
_, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
APIKey: directKey,
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if llmClient.lastReq.Target.APIKey != directKey {
|
|
t.Fatalf("expected direct API key to reach LLM request")
|
|
}
|
|
if !llmClient.lastReq.Target.APIKeyRequired {
|
|
t.Fatalf("expected APIKeyRequired to be carried to target")
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunRuntimeAPIKeyEnvOverrideWorks(t *testing.T) {
|
|
const envName = "PROMPTKIT_RUNTIME_API_KEY"
|
|
t.Setenv(envName, "runtime-secret")
|
|
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", Endpoint: "http://profile/v1", Model: "profile-model"},
|
|
}}
|
|
runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), &fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}}, nil, nil)
|
|
|
|
res, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
Execution: &domain.ExecutionTargetOverride{APIKeyEnv: envName},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.EffectiveModelParams.APIKeyEnv != envName {
|
|
t.Fatalf("expected runtime api_key_env override in effective params, got %q", res.EffectiveModelParams.APIKeyEnv)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunRuntimeAPIKeyEnvOverrideBeatsProfile(t *testing.T) {
|
|
const profileEnv = "PROMPTKIT_PROFILE_API_KEY"
|
|
const runtimeEnv = "PROMPTKIT_RUNTIME_API_KEY"
|
|
t.Setenv(runtimeEnv, "runtime-secret")
|
|
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", Endpoint: "http://profile/v1", Model: "profile-model", APIKeyEnv: profileEnv},
|
|
}}
|
|
runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), &fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}}, nil, nil)
|
|
|
|
res, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
Execution: &domain.ExecutionTargetOverride{APIKeyEnv: runtimeEnv},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.EffectiveModelParams.APIKeyEnv != runtimeEnv {
|
|
t.Fatalf("expected runtime override to beat profile api_key_env, got %q", res.EffectiveModelParams.APIKeyEnv)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunAPIKeyValueNotPresentInMetadata(t *testing.T) {
|
|
const envName = "PROMPTKIT_TEST_API_KEY"
|
|
const secret = "top-secret-value"
|
|
t.Setenv(envName, secret)
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", Endpoint: "http://profile/v1", Model: "profile-model", APIKeyEnv: envName},
|
|
}}
|
|
runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), &fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}}, nil, nil)
|
|
|
|
res, err := runner.Run(context.Background(), domain.RunRequest{PromptID: "p", ProfileID: "exec", Inputs: singleInputRef()})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.EffectiveModelParams.APIKeyEnv != envName {
|
|
t.Fatalf("expected api key env name, got %q", res.EffectiveModelParams.APIKeyEnv)
|
|
}
|
|
metadataDump := fmt.Sprintf("%+v|%s|%s|%s|%s", res.EffectiveModelParams, res.Endpoint, res.ModelName, res.PromptHash, res.RenderedPromptHash)
|
|
if strings.Contains(metadataDump, secret) {
|
|
t.Fatalf("unexpected api key value in metadata dump: %s", metadataDump)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunPromptLoadFailure(t *testing.T) {
|
|
runner := NewRunner(&fakePromptRepo{err: errors.New("boom")}, &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}, nil, defaultArtifactReader(), defaultRenderer(), &fakeLLM{}, nil, nil)
|
|
_, err := runner.Run(context.Background(), domain.RunRequest{PromptID: "p"})
|
|
if !errors.Is(err, ErrPromptLoad) {
|
|
t.Fatalf("expected ErrPromptLoad, got %v", err)
|
|
}
|
|
if errors.Is(err, ErrProfileLoad) {
|
|
t.Fatalf("did not expect ErrProfileLoad, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunArtifactLoadFailure(t *testing.T) {
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}, nil,
|
|
|
|
&fakeArtifactReader{errByURI: map[string]error{"a://bad": errors.New("read failed")}},
|
|
&fakeRenderer{rendered: &domain.RenderedPrompt{}},
|
|
&fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}},
|
|
nil, nil)
|
|
|
|
_, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: map[string]domain.ArtifactRef{"transcript": {Type: domain.ArtifactRefFile, URI: "a://bad"}},
|
|
})
|
|
if !errors.Is(err, ErrArtifactLoad) {
|
|
t.Fatalf("expected ErrArtifactLoad, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunPromptRenderFailure(t *testing.T) {
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}, nil,
|
|
|
|
defaultArtifactReader(),
|
|
&fakeRenderer{err: errors.New("render failed")},
|
|
&fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}},
|
|
nil, nil)
|
|
|
|
_, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if !errors.Is(err, ErrPromptRender) {
|
|
t.Fatalf("expected ErrPromptRender, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunLLMFailure(t *testing.T) {
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}, nil,
|
|
|
|
defaultArtifactReader(),
|
|
defaultRenderer(),
|
|
&fakeLLM{err: errors.New("llm failed")},
|
|
nil, nil)
|
|
|
|
_, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if !errors.Is(err, ErrLLMGenerate) {
|
|
t.Fatalf("expected ErrLLMGenerate, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunCancellationPreservesGenerationCategory(t *testing.T) {
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}, nil,
|
|
|
|
defaultArtifactReader(),
|
|
defaultRenderer(),
|
|
&fakeLLM{resp: &domain.GenerateResponse{Content: "ignored"}},
|
|
nil, nil)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
_, err := runner.Run(ctx, domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if !errors.Is(err, ErrLLMGenerate) {
|
|
t.Fatalf("expected ErrLLMGenerate, got %v", err)
|
|
}
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("expected context cancellation identity, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunLLMInvalidRequestMapsToUsecaseInvalidRequest(t *testing.T) {
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}, nil,
|
|
|
|
defaultArtifactReader(),
|
|
defaultRenderer(),
|
|
&fakeLLM{err: llm.ErrInvalidRequest},
|
|
nil, nil)
|
|
|
|
_, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if !errors.Is(err, ErrInvalidRequest) {
|
|
t.Fatalf("expected ErrInvalidRequest, got %v", err)
|
|
}
|
|
if errors.Is(err, ErrLLMGenerate) {
|
|
t.Fatalf("did not expect ErrLLMGenerate, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunValidationStillWorks(t *testing.T) {
|
|
validator := &fakeValidator{result: domain.ValidationResult{Status: domain.ValidationFailed, Mode: domain.ValidationBasic, Errors: []string{"bad"}, IsValid: false}}
|
|
runner := NewRunner(
|
|
&fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationBasic, 0)},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}, nil,
|
|
|
|
defaultArtifactReader(),
|
|
defaultRenderer(),
|
|
&fakeLLM{resp: &domain.GenerateResponse{Content: "raw output"}},
|
|
validator, nil)
|
|
|
|
res, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.Validation.Status != domain.ValidationFailed || res.RawOutput != "raw output" {
|
|
t.Fatalf("unexpected validation/raw output: %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunStructuredRepairRemainsBoundedAndUsesEffectiveModelSettings(t *testing.T) {
|
|
repairer := &fakeRepairer{responses: []*domain.GenerateResponse{{Content: `{"broken":`}, {Content: `{"still":`}}}
|
|
llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: `{"initial":`}}
|
|
|
|
runner := NewRunnerWithRepairer(
|
|
&fakePromptRepo{def: promptDef(domain.FormatJSON, domain.ValidationJSON, 1)},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", BackendID: "custom", Model: "profile-model", TimeoutSeconds: 55},
|
|
}}, fakeBackendResolver{backends: map[string]domain.Backend{
|
|
"custom": {ID: "custom", Endpoint: "http://backend/v1"},
|
|
}},
|
|
|
|
defaultArtifactReader(),
|
|
defaultRenderer(),
|
|
llmClient,
|
|
validate.NewStandardValidator("."),
|
|
repairer, nil)
|
|
|
|
res, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
Execution: &domain.ExecutionTargetOverride{Endpoint: "http://override/v1", Model: "override-model", TimeoutSeconds: intPtr(22)},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if repairer.calls != 1 || res.Validation.RepairAttempts != 1 {
|
|
t.Fatalf("expected one bounded repair, calls=%d attempts=%d", repairer.calls, res.Validation.RepairAttempts)
|
|
}
|
|
if len(repairer.reqs) != 1 {
|
|
t.Fatalf("expected one repair request, got %d", len(repairer.reqs))
|
|
}
|
|
if repairer.reqs[0].Target.Endpoint != "http://override/v1" || repairer.reqs[0].Target.Model != "override-model" {
|
|
t.Fatalf("expected repair to use effective target, got %+v", repairer.reqs[0].Target)
|
|
}
|
|
if repairer.reqs[0].Target.TimeoutSeconds != 22 {
|
|
t.Fatalf("expected repair to use effective timeout, got %d", repairer.reqs[0].Target.TimeoutSeconds)
|
|
}
|
|
if llmClient.lastReq.Target.BackendID != "custom" ||
|
|
repairer.reqs[0].Target.BackendID != "custom" ||
|
|
res.SelectedBackendID != "custom" {
|
|
t.Fatalf("expected backend identity in generation, repair, and result: generate=%q repair=%q result=%q",
|
|
llmClient.lastReq.Target.BackendID, repairer.reqs[0].Target.BackendID, res.SelectedBackendID)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunRepairCarriesEffectiveSessionID(t *testing.T) {
|
|
llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: `{"broken":`}}
|
|
runner := NewRunnerWithRepairer(
|
|
&fakePromptRepo{def: promptDef(domain.FormatJSON, domain.ValidationJSON, 1)},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", Endpoint: "http://example.test/v1", Model: "model"},
|
|
}},
|
|
nil,
|
|
defaultArtifactReader(),
|
|
defaultRenderer(),
|
|
llmClient,
|
|
validate.NewStandardValidator("."),
|
|
NewDefaultOutputRepairer(llmClient), nil)
|
|
|
|
result, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
SessionID: " repair-session ",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if llmClient.calls != 2 {
|
|
t.Fatalf("expected initial generation and one repair, got %d calls", llmClient.calls)
|
|
}
|
|
if llmClient.lastReq.Prompt.SessionID != "repair-session" {
|
|
t.Fatalf("expected repair generation to retain effective session, got %q", llmClient.lastReq.Prompt.SessionID)
|
|
}
|
|
if result.SessionID != "repair-session" {
|
|
t.Fatalf("expected result to retain effective session, got %q", result.SessionID)
|
|
}
|
|
}
|
|
|
|
func TestRunnerRunJSONSchemaRepairCarriesStructuredOutputSpec(t *testing.T) {
|
|
def := promptDef(domain.FormatJSON, domain.ValidationJSONSchema, 1)
|
|
def.Validation.SchemaPath = "events.schema.json"
|
|
|
|
validator := &fakeValidator{
|
|
result: domain.ValidationResult{
|
|
Status: domain.ValidationFailed,
|
|
Mode: domain.ValidationJSONSchema,
|
|
Errors: []string{"schema mismatch"},
|
|
IsValid: false,
|
|
},
|
|
schemaDoc: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"events": map[string]any{"type": "array"},
|
|
},
|
|
},
|
|
}
|
|
repairer := &fakeRepairer{
|
|
responses: []*domain.GenerateResponse{
|
|
{Content: `{"events":[]}`},
|
|
},
|
|
}
|
|
llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: `{"events":[1]}`}}
|
|
runner := NewRunnerWithRepairer(
|
|
&fakePromptRepo{def: def},
|
|
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}, nil,
|
|
|
|
defaultArtifactReader(),
|
|
defaultRenderer(),
|
|
llmClient,
|
|
validator,
|
|
repairer, nil)
|
|
|
|
_, err := runner.Run(context.Background(), domain.RunRequest{
|
|
PromptID: "p",
|
|
ProfileID: "exec",
|
|
Inputs: singleInputRef(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if llmClient.lastReq.StructuredOutput == nil || llmClient.lastReq.StructuredOutput.JSONSchema == nil {
|
|
t.Fatalf("expected initial llm request to include structured output, got %+v", llmClient.lastReq.StructuredOutput)
|
|
}
|
|
if len(repairer.reqs) != 1 {
|
|
t.Fatalf("expected one repair request, got %d", len(repairer.reqs))
|
|
}
|
|
if repairer.reqs[0].StructuredOutput == nil || repairer.reqs[0].StructuredOutput.JSONSchema == nil {
|
|
t.Fatalf("expected repair request structured output, got %+v", repairer.reqs[0].StructuredOutput)
|
|
}
|
|
if repairer.reqs[0].StructuredOutput.JSONSchema.Name != "p_1" {
|
|
t.Fatalf("expected derived schema name p_1, got %q", repairer.reqs[0].StructuredOutput.JSONSchema.Name)
|
|
}
|
|
}
|
|
|
|
func TestExecutionProfileToTargetPopulatesAllFieldsAndCopiesExtraParams(t *testing.T) {
|
|
src := &domain.ExecutionProfile{
|
|
ID: "exec",
|
|
Endpoint: "http://profile/v1",
|
|
Model: "profile-model",
|
|
Temperature: 0.2,
|
|
MaxTokens: 123,
|
|
TopP: 0.75,
|
|
TimeoutSeconds: 90,
|
|
ServiceTier: "priority",
|
|
ReasoningEffort: "medium",
|
|
APIKeyEnv: "PROMPTKIT_API_KEY",
|
|
APIKeyRequired: true,
|
|
ExtraParams: map[string]any{
|
|
"provider_option": "on",
|
|
},
|
|
}
|
|
|
|
target := executionProfileToTarget(src)
|
|
if target.Endpoint != src.Endpoint ||
|
|
target.Model != src.Model ||
|
|
target.Temperature != src.Temperature ||
|
|
target.MaxTokens != src.MaxTokens ||
|
|
target.TopP != src.TopP ||
|
|
target.TimeoutSeconds != src.TimeoutSeconds ||
|
|
target.ServiceTier != src.ServiceTier ||
|
|
target.ReasoningEffort != src.ReasoningEffort ||
|
|
target.APIKeyEnv != src.APIKeyEnv ||
|
|
target.APIKeyRequired != src.APIKeyRequired {
|
|
t.Fatalf("expected all profile fields to populate target, got %+v", target)
|
|
}
|
|
if !reflect.DeepEqual(target.ExtraParams, src.ExtraParams) {
|
|
t.Fatalf("expected extra_params to match, got %#v", target.ExtraParams)
|
|
}
|
|
|
|
src.ExtraParams["provider_option"] = "changed"
|
|
if target.ExtraParams["provider_option"] != "on" {
|
|
t.Fatalf("expected extra_params copy to be independent, got %#v", target.ExtraParams)
|
|
}
|
|
}
|
|
|
|
func TestResolveExecutionTargetProfileValuesPopulateAllSupportedFields(t *testing.T) {
|
|
profileValue := &domain.ExecutionProfile{
|
|
ID: "exec",
|
|
Endpoint: "http://profile/v1",
|
|
Model: "profile-model",
|
|
Temperature: 0.3,
|
|
MaxTokens: 222,
|
|
TopP: 0.6,
|
|
TimeoutSeconds: 77,
|
|
ServiceTier: "priority",
|
|
ReasoningEffort: "low",
|
|
APIKeyEnv: "PROFILE_KEY",
|
|
ExtraParams: map[string]any{
|
|
"profile_option": "enabled",
|
|
},
|
|
}
|
|
|
|
target, presence, err := resolveExecutionTarget(nil, profileValue, nil)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if presence != (domain.ExecutionTargetPresence{}) {
|
|
t.Fatalf("expected no request override presence, got %+v", presence)
|
|
}
|
|
if target.Endpoint != profileValue.Endpoint ||
|
|
target.Model != profileValue.Model ||
|
|
target.Temperature != profileValue.Temperature ||
|
|
target.MaxTokens != profileValue.MaxTokens ||
|
|
target.TopP != profileValue.TopP ||
|
|
target.TimeoutSeconds != profileValue.TimeoutSeconds ||
|
|
target.ServiceTier != profileValue.ServiceTier ||
|
|
target.ReasoningEffort != profileValue.ReasoningEffort ||
|
|
target.APIKeyEnv != profileValue.APIKeyEnv ||
|
|
target.APIKeyRequired != profileValue.APIKeyRequired {
|
|
t.Fatalf("expected profile values to populate target, got %+v", target)
|
|
}
|
|
if !reflect.DeepEqual(target.ExtraParams, profileValue.ExtraParams) {
|
|
t.Fatalf("expected profile extra_params in target, got %#v", target.ExtraParams)
|
|
}
|
|
}
|
|
|
|
func TestResolveExecutionTargetRuntimeOverridesBeatProfileForAllOverrideableFields(t *testing.T) {
|
|
profileValue := &domain.ExecutionProfile{
|
|
ID: "exec",
|
|
Endpoint: "http://profile/v1",
|
|
Model: "profile-model",
|
|
Temperature: 0.2,
|
|
MaxTokens: 200,
|
|
TopP: 0.8,
|
|
TimeoutSeconds: 90,
|
|
ServiceTier: "priority",
|
|
ReasoningEffort: "medium",
|
|
APIKeyEnv: "PROFILE_KEY",
|
|
ExtraParams: map[string]any{
|
|
"profile_only": "yes",
|
|
},
|
|
}
|
|
override := &domain.ExecutionTargetOverride{
|
|
Endpoint: "http://override/v1",
|
|
Model: "override-model",
|
|
Temperature: float64Ptr(0.9),
|
|
MaxTokens: intPtr(111),
|
|
TopP: float64Ptr(0.5),
|
|
TimeoutSeconds: intPtr(30),
|
|
ServiceTier: "flex",
|
|
ReasoningEffort: stringPtr("high"),
|
|
APIKeyEnv: "RUNTIME_KEY",
|
|
ExtraParams: map[string]any{
|
|
"runtime_only": "yes",
|
|
},
|
|
}
|
|
|
|
target, presence, err := resolveExecutionTarget(nil, profileValue, override)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if presence != (domain.ExecutionTargetPresence{Temperature: true, MaxTokens: true, TopP: true, TimeoutSeconds: true}) {
|
|
t.Fatalf("unexpected override presence: %+v", presence)
|
|
}
|
|
if target.Endpoint != override.Endpoint ||
|
|
target.Model != override.Model ||
|
|
target.Temperature != *override.Temperature ||
|
|
target.MaxTokens != *override.MaxTokens ||
|
|
target.TopP != *override.TopP ||
|
|
target.TimeoutSeconds != *override.TimeoutSeconds ||
|
|
target.ServiceTier != override.ServiceTier ||
|
|
target.ReasoningEffort != *override.ReasoningEffort ||
|
|
target.APIKeyEnv != override.APIKeyEnv {
|
|
t.Fatalf("expected runtime overrides to win for all fields, got %+v", target)
|
|
}
|
|
if !reflect.DeepEqual(target.ExtraParams, override.ExtraParams) {
|
|
t.Fatalf("expected runtime extra_params to replace profile extra_params, got %#v", target.ExtraParams)
|
|
}
|
|
}
|
|
|
|
func TestResolveExecutionTargetReasoningOverrideStates(t *testing.T) {
|
|
profileValue := &domain.ExecutionProfile{
|
|
ReasoningEffort: "medium",
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
override *string
|
|
want string
|
|
}{
|
|
{
|
|
name: "nil inherits profile value",
|
|
want: "medium",
|
|
},
|
|
{
|
|
name: "nonblank replaces and trims profile value",
|
|
override: stringPtr(" high "),
|
|
want: "high",
|
|
},
|
|
{
|
|
name: "blank clears profile value",
|
|
override: stringPtr(" \t "),
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
target, _, err := resolveExecutionTarget(nil, profileValue, &domain.ExecutionTargetOverride{
|
|
ReasoningEffort: tt.override,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("resolve execution target: %v", err)
|
|
}
|
|
if target.ReasoningEffort != tt.want {
|
|
t.Fatalf("reasoning effort = %q, want %q", target.ReasoningEffort, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMergeExecutionTargetEmptyStringOverridesDoNotErase(t *testing.T) {
|
|
base := domain.ExecutionTarget{
|
|
Endpoint: "http://base/v1",
|
|
Model: "base-model",
|
|
ServiceTier: "priority",
|
|
ReasoningEffort: "medium",
|
|
APIKeyEnv: "BASE_KEY",
|
|
}
|
|
override := domain.ExecutionTarget{
|
|
Endpoint: "http://override/v1",
|
|
Model: "override-model",
|
|
ServiceTier: " ",
|
|
ReasoningEffort: " ",
|
|
APIKeyEnv: "",
|
|
}
|
|
|
|
merged := mergeExecutionTarget(base, override)
|
|
if merged.Endpoint != "http://override/v1" || merged.Model != "override-model" {
|
|
t.Fatalf("expected endpoint/model to override, got %+v", merged)
|
|
}
|
|
if merged.ServiceTier != "priority" {
|
|
t.Fatalf("expected empty service_tier override to be ignored, got %q", merged.ServiceTier)
|
|
}
|
|
if merged.ReasoningEffort != "medium" {
|
|
t.Fatalf("expected empty reasoning_effort override to be ignored, got %q", merged.ReasoningEffort)
|
|
}
|
|
if merged.APIKeyEnv != "BASE_KEY" {
|
|
t.Fatalf("expected empty api_key_env override to be ignored, got %q", merged.APIKeyEnv)
|
|
}
|
|
}
|
|
|
|
func TestMergeExecutionTargetEmptyExtraParamsDoesNotErase(t *testing.T) {
|
|
base := domain.ExecutionTarget{
|
|
ExtraParams: map[string]any{
|
|
"keep": "value",
|
|
},
|
|
}
|
|
override := domain.ExecutionTarget{
|
|
ExtraParams: map[string]any{},
|
|
}
|
|
|
|
merged := mergeExecutionTarget(base, override)
|
|
if !reflect.DeepEqual(merged.ExtraParams, base.ExtraParams) {
|
|
t.Fatalf("expected empty extra_params override not to erase base values, got %#v", merged.ExtraParams)
|
|
}
|
|
}
|
|
|
|
func TestBuildOutputArtifactDefaults(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
format domain.OutputFormat
|
|
contentType string
|
|
}{
|
|
{name: "text", format: domain.FormatText, contentType: defaults.ContentTypeTextPlain},
|
|
{name: "markdown", format: domain.FormatMarkdown, contentType: defaults.ContentTypeTextMarkdown},
|
|
{name: "json", format: domain.FormatJSON, contentType: defaults.ContentTypeApplicationJSON},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
art := buildOutputArtifact("body", tc.format)
|
|
if art.Name != defaults.OutputArtifactName {
|
|
t.Fatalf("expected artifact name %q, got %q", defaults.OutputArtifactName, art.Name)
|
|
}
|
|
if art.ContentType != tc.contentType {
|
|
t.Fatalf("expected content type %q, got %q", tc.contentType, art.ContentType)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func promptDef(format domain.OutputFormat, mode domain.ValidationMode, attempts int) *domain.PromptDefinition {
|
|
return &domain.PromptDefinition{
|
|
ID: "p",
|
|
Version: "1",
|
|
DefaultProfile: "exec",
|
|
Inputs: []domain.PromptInput{{Name: "transcript", Required: true}},
|
|
Templates: []domain.PromptMessageTemplate{{Role: "user", Content: "x"}},
|
|
OutputFormat: format,
|
|
Validation: domain.OutputContract{
|
|
ValidationMode: mode,
|
|
RepairAttempts: attempts,
|
|
Format: format,
|
|
},
|
|
}
|
|
}
|
|
|
|
func hashString(s string) string {
|
|
sum := sha256.Sum256([]byte(s))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func defaultExecutionProfile() *domain.ExecutionProfile {
|
|
return &domain.ExecutionProfile{
|
|
ID: "exec",
|
|
Endpoint: "http://llm/v1",
|
|
Model: "model-from-profile",
|
|
}
|
|
}
|
|
|
|
func TestResolveExecutionTargetUsesBackendProfileAndRequestPrecedence(t *testing.T) {
|
|
backendValue := &domain.Backend{
|
|
ID: "custom",
|
|
Endpoint: "http://backend/v1",
|
|
APIKeyEnv: "BACKEND_KEY",
|
|
ExtraParams: map[string]any{"backend": true},
|
|
}
|
|
profileValue := &domain.ExecutionProfile{
|
|
ID: "exec",
|
|
BackendID: "custom",
|
|
Endpoint: "http://profile/v1",
|
|
Model: "profile-model",
|
|
APIKeyEnv: "PROFILE_KEY",
|
|
ExtraParams: map[string]any{"profile": true},
|
|
}
|
|
override := &domain.ExecutionTargetOverride{
|
|
Endpoint: "http://request/v1",
|
|
APIKeyEnv: "REQUEST_KEY",
|
|
ExtraParams: map[string]any{"request": true},
|
|
}
|
|
|
|
target, _, err := resolveExecutionTarget(backendValue, profileValue, override)
|
|
if err != nil {
|
|
t.Fatalf("resolve target: %v", err)
|
|
}
|
|
if target.BackendID != "custom" {
|
|
t.Fatalf("endpoint override changed backend identity: %+v", target)
|
|
}
|
|
if target.Endpoint != "http://request/v1" || target.APIKeyEnv != "REQUEST_KEY" {
|
|
t.Fatalf("request values did not win: %+v", target)
|
|
}
|
|
if !reflect.DeepEqual(target.ExtraParams, map[string]any{"request": true}) {
|
|
t.Fatalf("expected whole-map request replacement, got %#v", target.ExtraParams)
|
|
}
|
|
|
|
target, _, err = resolveExecutionTarget(backendValue, &domain.ExecutionProfile{
|
|
ID: "exec", BackendID: "custom", Model: "profile-model",
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatalf("resolve backend defaults: %v", err)
|
|
}
|
|
if target.Endpoint != backendValue.Endpoint ||
|
|
target.APIKeyEnv != backendValue.APIKeyEnv ||
|
|
!reflect.DeepEqual(target.ExtraParams, backendValue.ExtraParams) {
|
|
t.Fatalf("backend defaults were not inherited: %+v", target)
|
|
}
|
|
}
|
|
|
|
func TestRunnerPrepareBackendResolutionAndCredentialPrecedence(t *testing.T) {
|
|
resolver := fakeBackendResolver{backends: map[string]domain.Backend{
|
|
"custom": {ID: "custom", Endpoint: "http://backend/v1", APIKeyEnv: "BACKEND_KEY"},
|
|
}}
|
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
|
|
t.Run("unknown backend is a profile load failure", func(t *testing.T) {
|
|
runner := NewRunner(promptRepo, &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", BackendID: "unknown", Model: "model"},
|
|
}}, resolver, defaultArtifactReader(), defaultRenderer(), &fakeLLM{forbid: true}, nil, nil)
|
|
|
|
_, err := runner.Prepare(context.Background(), domain.RunRequest{PromptID: "p", ProfileID: "exec", Inputs: singleInputRef()})
|
|
if !errors.Is(err, ErrProfileLoad) {
|
|
t.Fatalf("expected ErrProfileLoad, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("nil resolver is a profile load failure", func(t *testing.T) {
|
|
runner := NewRunner(promptRepo, &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", BackendID: "custom", Model: "model"},
|
|
}}, nil, defaultArtifactReader(), defaultRenderer(), &fakeLLM{forbid: true}, nil, nil)
|
|
|
|
_, err := runner.Prepare(context.Background(), domain.RunRequest{PromptID: "p", ProfileID: "exec", Inputs: singleInputRef()})
|
|
if !errors.Is(err, ErrProfileLoad) {
|
|
t.Fatalf("expected ErrProfileLoad, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("request environment wins", func(t *testing.T) {
|
|
t.Setenv("REQUEST_KEY", "request-secret")
|
|
runner := NewRunner(promptRepo, &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", BackendID: "custom", Model: "model", APIKeyEnv: "PROFILE_KEY"},
|
|
}}, resolver, defaultArtifactReader(), defaultRenderer(), &fakeLLM{forbid: true}, nil, nil)
|
|
|
|
prepared, err := runner.Prepare(context.Background(), domain.RunRequest{
|
|
PromptID: "p", ProfileID: "exec", Inputs: singleInputRef(),
|
|
Execution: &domain.ExecutionTargetOverride{APIKeyEnv: "REQUEST_KEY"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("prepare: %v", err)
|
|
}
|
|
if prepared.EffectiveModelParams.APIKeyEnv != "REQUEST_KEY" {
|
|
t.Fatalf("unexpected credential source: %+v", prepared.EffectiveModelParams)
|
|
}
|
|
})
|
|
|
|
t.Run("required direct key clears inherited environment", func(t *testing.T) {
|
|
t.Setenv("BACKEND_KEY", "backend-secret")
|
|
runner := NewRunner(promptRepo, &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
"exec": {ID: "exec", BackendID: "custom", Model: "model", APIKeyRequired: true},
|
|
}}, resolver, defaultArtifactReader(), defaultRenderer(), &fakeLLM{forbid: true}, nil, nil)
|
|
|
|
_, err := runner.Prepare(context.Background(), domain.RunRequest{PromptID: "p", ProfileID: "exec", Inputs: singleInputRef()})
|
|
if !errors.Is(err, ErrAPIKeyRequired) {
|
|
t.Fatalf("expected ErrAPIKeyRequired, got %v", err)
|
|
}
|
|
prepared, err := runner.Prepare(context.Background(), domain.RunRequest{
|
|
PromptID: "p", ProfileID: "exec", Inputs: singleInputRef(), APIKey: "direct-secret",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("prepare with direct key: %v", err)
|
|
}
|
|
if prepared.EffectiveModelParams.APIKeyEnv != "" {
|
|
t.Fatalf("expected inherited environment name to be cleared, got %q", prepared.EffectiveModelParams.APIKeyEnv)
|
|
}
|
|
})
|
|
}
|
|
|
|
func defaultArtifactReader() *fakeArtifactReader {
|
|
return &fakeArtifactReader{artifactsByURI: map[string]*domain.Artifact{
|
|
"a://ok": {Body: []byte("x"), Hash: hashString("x")},
|
|
}}
|
|
}
|
|
|
|
func defaultRenderer() *fakeRenderer {
|
|
return &fakeRenderer{rendered: &domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hello"}}}}
|
|
}
|
|
|
|
func singleInputRef() map[string]domain.ArtifactRef {
|
|
return map[string]domain.ArtifactRef{"transcript": {Type: domain.ArtifactRefFile, URI: "a://ok"}}
|
|
}
|
|
|
|
func float64Ptr(v float64) *float64 {
|
|
return &v
|
|
}
|
|
|
|
func stringPtr(v string) *string {
|
|
return &v
|
|
}
|
|
|
|
func intPtr(v int) *int {
|
|
return &v
|
|
}
|
|
|
|
func newMinimalRunner(promptRepo *fakePromptRepo, execRepo *fakeExecutionProfileRepo) *Runner {
|
|
return NewRunner(
|
|
promptRepo,
|
|
execRepo, nil,
|
|
|
|
defaultArtifactReader(),
|
|
defaultRenderer(),
|
|
&fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}},
|
|
nil, nil)
|
|
|
|
}
|