package usecase import ( "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "math" "path/filepath" "reflect" "regexp" "strconv" "strings" "sync" "testing" "time" "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 } type staticPromptRepo struct { def *domain.PromptDefinition } func (r staticPromptRepo) GetPromptDefinition( context.Context, string, string, ) (*domain.PromptDefinition, error) { return r.def, nil } type staticExecutionProfileRepo struct { profile *domain.ExecutionProfile } func (r staticExecutionProfileRepo) GetProfile( context.Context, string, ) (*domain.ExecutionProfile, error) { value := *r.profile return &value, nil } type staticArtifactReader struct{} func (staticArtifactReader) Read( context.Context, domain.ArtifactRef, ) (*domain.Artifact, error) { return &domain.Artifact{Body: []byte("artifact"), Hash: hashString("artifact")}, nil } type staticRenderer struct{} func (staticRenderer) Render( context.Context, *domain.PromptDefinition, map[string]*domain.Artifact, map[string]string, ) (*domain.RenderedPrompt, error) { return &domain.RenderedPrompt{ Messages: []domain.RenderedMessage{{Role: "user", Content: "hello"}}, }, nil } 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 } type recordingRunAdmitter struct { mu sync.Mutex next RunAdmitter backendIDs []string } func (a *recordingRunAdmitter) Admit(ctx context.Context, backendID string) (func(), error) { a.mu.Lock() a.backendIDs = append(a.backendIDs, backendID) a.mu.Unlock() return a.next.Admit(ctx, backendID) } func (a *recordingRunAdmitter) admittedBackendIDs() []string { a.mu.Lock() defer a.mu.Unlock() return append([]string(nil), a.backendIDs...) } type controlledRepairLLM struct { mu sync.Mutex active int peak int calls int repairCalls int events chan controlledGeneration backendIDs []string } type controlledGeneration struct { release chan struct{} } func (c *controlledRepairLLM) Generate( ctx context.Context, req domain.GenerateRequest, ) (*domain.GenerateResponse, error) { isRepair := len(req.Prompt.Messages) > 0 && strings.HasPrefix(req.Prompt.Messages[0].Content, "You repair invalid JSON") c.mu.Lock() c.calls++ c.active++ if c.active > c.peak { c.peak = c.active } if isRepair { c.repairCalls++ } c.backendIDs = append(c.backendIDs, req.Target.BackendID) c.mu.Unlock() defer func() { c.mu.Lock() c.active-- c.mu.Unlock() }() event := controlledGeneration{release: make(chan struct{})} c.events <- event select { case <-event.release: case <-ctx.Done(): return nil, ctx.Err() } if isRepair { return &domain.GenerateResponse{Content: `{}`}, nil } return &domain.GenerateResponse{Content: `{"broken":`}, nil } func (c *controlledRepairLLM) snapshot() (peak, calls, repairCalls int, backendIDs []string) { c.mu.Lock() defer c.mu.Unlock() return c.peak, c.calls, c.repairCalls, append([]string(nil), c.backendIDs...) } 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) } }) t.Run("malformed 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: "session" + string([]byte{0xff}), 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) { type testCase struct { name string override *domain.ExecutionTargetOverride } tests := []testCase{ {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)}}, {name: "temperature is not finite", override: &domain.ExecutionTargetOverride{Temperature: float64Ptr(math.NaN())}}, {name: "top p is not finite", override: &domain.ExecutionTargetOverride{TopP: float64Ptr(math.Inf(1))}}, } if strconv.IntSize == 64 { durationLimit := int64(math.MaxInt64 / int64(time.Second)) tests = append(tests, testCase{name: "timeout cannot be represented as a duration", override: &domain.ExecutionTargetOverride{TimeoutSeconds: intPtr(int(durationLimit) + 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 wantCapacityType bool }{ { name: "capacity exhausted", admissionError: capacity.ErrCapacityExceeded, wantCapacityType: 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) } var capacityErr *CapacityError if tc.wantCapacityType { if !errors.As(err, &capacityErr) { t.Fatalf("capacity error=%v, want internal typed identity", err) } if capacityErr.BackendID != "custom" { t.Fatalf("capacity backend ID=%q, want custom", capacityErr.BackendID) } } else if errors.As(err, &capacityErr) { t.Fatalf("non-capacity admission error exposed typed capacity identity: %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 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) result, 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 result.EffectiveModelParams.APIKey != "" { t.Fatal("run result retained direct API key") } 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 TestRunnerSchedulesInitialAndRepairGenerationThroughOneBackendPool(t *testing.T) { manager, err := capacity.NewManager(map[string]domain.BackendCapacityPolicy{ "custom": { ConcurrencyLimit: 1, QueueCapacity: 1, }, }) if err != nil { t.Fatalf("construct capacity manager: %v", err) } baseClient := &controlledRepairLLM{ events: make(chan controlledGeneration, 4), } scheduledClient := capacity.NewClient(manager, baseClient) admitter := &recordingRunAdmitter{next: manager} runner := NewRunnerWithRepairer( staticPromptRepo{def: promptDef(domain.FormatJSON, domain.ValidationJSON, 1)}, staticExecutionProfileRepo{profile: &domain.ExecutionProfile{ ID: "exec", BackendID: "custom", Model: "model", }}, fakeBackendResolver{backends: map[string]domain.Backend{ "custom": {ID: "custom", Endpoint: "http://backend.example/v1"}, }}, staticArtifactReader{}, staticRenderer{}, scheduledClient, validate.NewStandardValidator("."), NewDefaultOutputRepairer(scheduledClient), admitter, ) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() type runOutcome struct { result *domain.RunResult err error } outcomes := make(chan runOutcome, 2) request := domain.RunRequest{ PromptID: "p", ProfileID: "exec", Inputs: singleInputRef(), } for i := 0; i < 2; i++ { go func() { result, runErr := runner.Run(ctx, request) outcomes <- runOutcome{result: result, err: runErr} }() } for i := 0; i < 4; i++ { select { case event := <-baseClient.events: close(event.release) case <-ctx.Done(): t.Fatalf("timed out waiting for generation %d: %v", i+1, ctx.Err()) } } for i := 0; i < 2; i++ { select { case outcome := <-outcomes: if outcome.err != nil || outcome.result == nil { t.Fatalf("run outcome=(%+v, %v), want success", outcome.result, outcome.err) } if outcome.result.Validation.Status != domain.ValidationPassed || outcome.result.Validation.RepairAttempts != 1 { t.Fatalf("unexpected repaired result: %+v", outcome.result.Validation) } case <-ctx.Done(): t.Fatalf("timed out waiting for repaired run: %v", ctx.Err()) } } admitted := admitter.admittedBackendIDs() if !reflect.DeepEqual(admitted, []string{"custom", "custom"}) { t.Fatalf("admission backend IDs=%#v, want one admission per run", admitted) } peak, calls, repairCalls, backendIDs := baseClient.snapshot() if peak != 1 || calls != 4 || repairCalls != 2 { t.Fatalf( "generation observations=(peak=%d calls=%d repairs=%d), want (1, 4, 2)", peak, calls, repairCalls, ) } if !reflect.DeepEqual(backendIDs, []string{"custom", "custom", "custom", "custom"}) { t.Fatalf("generation backend IDs=%#v, want custom for initial and repair calls", backendIDs) } } 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 := resolveExecutionTarget(nil, profileValue, nil) 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 := resolveExecutionTarget(nil, profileValue, override) 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, _ := resolveExecutionTarget(nil, profileValue, &domain.ExecutionTargetOverride{ ReasoningEffort: tt.override, }) 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, _ := resolveExecutionTarget(backendValue, profileValue, override) 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, _ = resolveExecutionTarget(backendValue, &domain.ExecutionProfile{ ID: "exec", BackendID: "custom", Model: "profile-model", }, nil) 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) }