package promptkit_test import ( "context" "encoding/json" "errors" "fmt" "io" "math" "net/http" "net/http/httptest" "os" "path/filepath" "reflect" "strconv" "strings" "testing" "testing/fstest" "time" "gitea.maximumdirect.net/eric/promptkit" ) const ( frameworkContractRoot = "./testdata/framework" frameworkPromptDir = frameworkContractRoot + "/prompts" frameworkProfileDir = frameworkContractRoot + "/profiles" frameworkSchemaDir = frameworkContractRoot + "/schemas" frameworkMarkdownSummaryPromptID = "contract.markdown_summary" frameworkStructuredEventsPromptID = "contract.structured_events" frameworkFastProfileID = "contract-fast" frameworkQualityProfileID = "contract-quality" frameworkTranscriptPath = frameworkContractRoot + "/fixtures/transcript.md" frameworkGlossaryPath = frameworkContractRoot + "/fixtures/glossary.yml" ) func TestNewEngineRejectsMissingPromptDir(t *testing.T) { _, err := promptkit.NewEngine(promptkit.Config{ProfileDir: frameworkProfileDir}) if !errors.Is(err, promptkit.ErrInvalidConfig) { t.Fatalf("expected ErrInvalidConfig, got %v", err) } } func TestNewEngineAcceptsMissingProfileDir(t *testing.T) { _, err := promptkit.NewEngine(promptkit.Config{PromptDir: frameworkPromptDir}) if err != nil { t.Fatalf("expected missing profile dir to use built-ins, got %v", err) } } func TestPrepareWorksWithFrameworkContractCorpus(t *testing.T) { engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, ProfileDir: frameworkProfileDir, SchemaDir: frameworkSchemaDir, }) if err != nil { t.Fatalf("construct engine from framework contract corpus: %v", err) } tests := []struct { name string promptID string profileID string model string structured bool }{ { name: "markdown summary", promptID: frameworkMarkdownSummaryPromptID, profileID: frameworkFastProfileID, model: "contract-fast-model", }, { name: "structured events", promptID: frameworkStructuredEventsPromptID, profileID: frameworkQualityProfileID, model: "contract-quality-model", structured: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: tt.promptID, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.File(frameworkTranscriptPath), "glossary": promptkit.File(frameworkGlossaryPath), }, }) if err != nil { t.Fatalf("prepare framework contract prompt: %v", err) } if prepared.PromptID != tt.promptID { t.Fatalf("unexpected prompt id: got %q, want %q", prepared.PromptID, tt.promptID) } if prepared.SelectedProfileID != tt.profileID { t.Fatalf("unexpected selected profile: got %q, want %q", prepared.SelectedProfileID, tt.profileID) } if prepared.EffectiveModelParams.Model != tt.model { t.Fatalf("unexpected effective model: got %q, want %q", prepared.EffectiveModelParams.Model, tt.model) } if len(prepared.Messages) != 2 { t.Fatalf("expected rendered messages, got %d", len(prepared.Messages)) } if !strings.Contains(prepared.Messages[1].Content, "Nia labels the archive.") { t.Fatalf("expected relative prompt content to render the transcript, got %q", prepared.Messages[1].Content) } if prepared.InputHashes["transcript"] == "" || prepared.InputHashes["glossary"] == "" { t.Fatalf("expected input hashes, got %#v", prepared.InputHashes) } if !tt.structured { if prepared.StructuredOutput != nil { t.Fatalf("expected no structured output specification, got %#v", prepared.StructuredOutput) } return } if prepared.StructuredOutput == nil || prepared.StructuredOutput.JSONSchema == nil { t.Fatalf("expected loaded JSON Schema structured output, got %#v", prepared.StructuredOutput) } schema, ok := prepared.StructuredOutput.JSONSchema.Schema.(map[string]any) if !ok || schema["type"] != "object" { t.Fatalf("expected loaded object JSON Schema, got %#v", prepared.StructuredOutput.JSONSchema.Schema) } properties, ok := schema["properties"].(map[string]any) if !ok || properties["events"] == nil { t.Fatalf("expected loaded events schema property, got %#v", schema) } }) } } func TestPrepareWorksWithInlineInputs(t *testing.T) { engine := newContractEngine(t) prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin scouts the tower.\nKara lights a lantern."), "glossary": promptkit.InlineWithURI("memory://glossary.yml", "party:\n - Rin\n - Kara\n"), }, }) if err != nil { t.Fatalf("expected prepare to succeed, got %v", err) } if len(prepared.Messages) != 2 { t.Fatalf("expected rendered messages, got %d", len(prepared.Messages)) } rendered := prepared.Messages[1].Content if !strings.Contains(rendered, "Rin scouts the tower.") || !strings.Contains(rendered, "party:") { t.Fatalf("expected inline inputs in rendered prompt, got %q", rendered) } } func TestPreparedRunJSONDoesNotExposeSecretOrTargetPresence(t *testing.T) { const envName = "PROMPTKIT_API_KEY" const secret = "public-api-test-secret" t.Setenv(envName, secret) profileDir := t.TempDir() writePublicProfileFileWithAPIKeyEnv(t, profileDir, "prepared-secret", "http://localhost:8000/v1", "prepared-secret-model", envName) engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, ProfileDir: profileDir, SchemaDir: frameworkSchemaDir, }) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkStructuredEventsPromptID, ProfileID: "prepared-secret", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.File(frameworkTranscriptPath), "glossary": promptkit.File(frameworkGlossaryPath), }, }) if err != nil { t.Fatalf("expected prepare to succeed, got %v", err) } payload, err := json.Marshal(prepared) if err != nil { t.Fatalf("expected prepared run to marshal, got %v", err) } out := string(payload) if strings.Contains(out, secret) { t.Fatalf("prepared run JSON leaked raw API key value: %s", out) } if !strings.Contains(out, envName) { t.Fatalf("prepared run JSON should retain api_key_env name, got %s", out) } for _, forbidden := range []string{"TargetPresence", "target_presence"} { if strings.Contains(out, forbidden) { t.Fatalf("prepared run JSON exposed internal target presence metadata %q: %s", forbidden, out) } } } func TestRunRequestFormattingRedactsDirectAPIKey(t *testing.T) { const ( secret = "run-request-api-key-sentinel" inputURI = "memory://run-request-uri-sentinel" inputBody = "run-request-body-sentinel" variableValue = "run-request-variable-sentinel" ) req := promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: frameworkFastProfileID, APIKey: secret, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.InlineWithURI(inputURI, inputBody), }, Vars: map[string]string{"audience": variableValue}, } for _, formatted := range []string{ req.String(), req.GoString(), fmt.Sprintf("%v", req), fmt.Sprintf("%+v", req), fmt.Sprintf("%#v", req), } { for _, privateValue := range []string{secret, inputURI, inputBody, variableValue} { if strings.Contains(formatted, privateValue) { t.Fatalf("formatted RunRequest leaked private value %q: %s", privateValue, formatted) } } for _, summary := range []string{ `PromptID:"` + frameworkMarkdownSummaryPromptID + `"`, `ProfileID:"` + frameworkFastProfileID + `"`, "APIKeySet:true", "Inputs:1", "Vars:1", } { if !strings.Contains(formatted, summary) { t.Fatalf("formatted RunRequest omitted structural summary %q: %s", summary, formatted) } } } payload, err := json.Marshal(req) if err != nil { t.Fatalf("expected RunRequest to marshal, got %v", err) } if strings.Contains(string(payload), secret) { t.Fatalf("RunRequest JSON leaked API key: %s", payload) } } func TestGenerateRequestFormattingRedactsDirectAPIKey(t *testing.T) { const secret = "generate-request-secret" req := promptkit.GenerateRequest{ Prompt: promptkit.RenderedPrompt{Messages: []promptkit.RenderedMessage{ {Role: "user", Content: "secret prompt content"}, }}, Target: promptkit.ExecutionTarget{ Model: "test-model", ExtraParams: map[string]any{ "provider_option": "on", }, }, APIKey: secret, } for _, formatted := range []string{ fmt.Sprint(req), fmt.Sprintf("%+v", req), fmt.Sprintf("%#v", req), } { if strings.Contains(formatted, secret) { t.Fatalf("formatted GenerateRequest leaked API key: %s", formatted) } if strings.Contains(formatted, "secret prompt content") { t.Fatalf("formatted GenerateRequest leaked prompt content: %s", formatted) } if !strings.Contains(formatted, "APIKeySet:true") { t.Fatalf("formatted GenerateRequest should indicate an API key is set, got %s", formatted) } } payload, err := json.Marshal(req) if err != nil { t.Fatalf("expected GenerateRequest to marshal, got %v", err) } if strings.Contains(string(payload), secret) { t.Fatalf("GenerateRequest JSON leaked API key: %s", payload) } } func TestEngineExecutionSettingPrecedence(t *testing.T) { floatPointer := func(value float64) *float64 { return &value } intPointer := func(value int) *int { return &value } stringPointer := func(value string) *string { return &value } defaultsProfile := executionProfileFixture{ id: "settings-defaults", endpoint: "http://profile-defaults.test/v1", model: "profile-defaults-model", serviceTier: "profile-defaults-tier", reasoningEffort: "profile-defaults-reasoning", apiKeyEnv: "PROMPTKIT_PRECEDENCE_DEFAULTS", extraParamSource: "profile-defaults", } profileSettings := executionProfileFixture{ id: "settings-profile", endpoint: "http://profile-settings.test/v1", model: "profile-settings-model", temperature: 0.31, maxTokens: 311, topP: 0.61, timeoutSeconds: 71, serviceTier: "profile-settings-tier", reasoningEffort: "profile-settings-reasoning", apiKeyEnv: "PROMPTKIT_PRECEDENCE_PROFILE", extraParamSource: "profile-settings", } requestProfile := executionProfileFixture{ id: "settings-request", endpoint: "http://profile-request.test/v1", model: "profile-request-model", temperature: 0.29, maxTokens: 299, topP: 0.59, timeoutSeconds: 79, serviceTier: "profile-request-tier", reasoningEffort: "profile-request-reasoning", apiKeyEnv: "PROMPTKIT_PRECEDENCE_REQUEST_PROFILE", extraParamSource: "profile-request", } zeroOverrideProfile := executionProfileFixture{ id: "settings-zero", endpoint: "http://profile-zero.test/v1", model: "profile-zero-model", temperature: 0.43, maxTokens: 433, topP: 0.73, timeoutSeconds: 83, serviceTier: "profile-zero-tier", reasoningEffort: "profile-zero-reasoning", apiKeyEnv: "PROMPTKIT_PRECEDENCE_ZERO", extraParamSource: "profile-zero", } requestTarget := promptkit.ExecutionTarget{ Endpoint: "http://request-settings.test/v1", Model: "request-settings-model", Temperature: 0.87, MaxTokens: 877, TopP: 0.97, TimeoutSeconds: 177, ServiceTier: "request-settings-tier", ReasoningEffort: "request-settings-reasoning", APIKeyEnv: "PROMPTKIT_PRECEDENCE_REQUEST", ExtraParams: map[string]any{"source": "request-settings"}, } zeroOverrideTarget := executionTargetFromProfileFixture(zeroOverrideProfile) zeroOverrideTarget.Temperature = 0 zeroOverrideTarget.MaxTokens = 0 zeroOverrideTarget.TopP = 0 zeroOverrideTarget.TimeoutSeconds = 0 tests := []struct { name string profile executionProfileFixture override *promptkit.ExecutionTargetOverride want promptkit.ExecutionTarget wantPresence promptkit.ExecutionTargetPresence }{ { name: "unspecified provider controls retain framework timeout", profile: defaultsProfile, want: promptkit.ExecutionTarget{ Endpoint: defaultsProfile.endpoint, Model: defaultsProfile.model, Temperature: 0, MaxTokens: 0, TopP: 0, TimeoutSeconds: 600, ServiceTier: defaultsProfile.serviceTier, ReasoningEffort: defaultsProfile.reasoningEffort, APIKeyEnv: defaultsProfile.apiKeyEnv, ExtraParams: map[string]any{"source": defaultsProfile.extraParamSource}, }, }, { name: "profile settings replace framework defaults", profile: profileSettings, want: executionTargetFromProfileFixture(profileSettings), }, { name: "request settings replace profile settings", profile: requestProfile, override: &promptkit.ExecutionTargetOverride{ Endpoint: requestTarget.Endpoint, Model: requestTarget.Model, Temperature: floatPointer(requestTarget.Temperature), MaxTokens: intPointer(requestTarget.MaxTokens), TopP: floatPointer(requestTarget.TopP), TimeoutSeconds: intPointer(requestTarget.TimeoutSeconds), ServiceTier: requestTarget.ServiceTier, ReasoningEffort: stringPointer(requestTarget.ReasoningEffort), APIKeyEnv: requestTarget.APIKeyEnv, ExtraParams: requestTarget.ExtraParams, }, want: requestTarget, wantPresence: promptkit.ExecutionTargetPresence{Temperature: true, MaxTokens: true, TopP: true, TimeoutSeconds: true}, }, { name: "explicit request zero replaces profile settings", profile: zeroOverrideProfile, override: &promptkit.ExecutionTargetOverride{ Temperature: floatPointer(0), MaxTokens: intPointer(0), TopP: floatPointer(0), TimeoutSeconds: intPointer(0), }, want: zeroOverrideTarget, wantPresence: promptkit.ExecutionTargetPresence{Temperature: true, MaxTokens: true, TopP: true, TimeoutSeconds: true}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Setenv(tt.profile.apiKeyEnv, "set") if tt.override != nil && tt.override.APIKeyEnv != "" { t.Setenv(tt.override.APIKeyEnv, "set") } profileDir := t.TempDir() writeExecutionProfileFixture(t, profileDir, tt.profile) fake := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}} engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, ProfileDir: profileDir, SchemaDir: frameworkSchemaDir, }, promptkit.WithLLMClient(fake)) if err != nil { t.Fatalf("construct engine: %v", err) } _, err = engine.Run(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: tt.profile.id, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Nia labels the archive."), "glossary": promptkit.Inline("archive: A catalogued collection."), }, Execution: tt.override, }) if err != nil { t.Fatalf("run engine: %v", err) } if len(fake.requests) != 1 { t.Fatalf("expected one generation request, got %d", len(fake.requests)) } got := fake.requests[0] if !reflect.DeepEqual(got.Target, tt.want) { t.Fatalf("unexpected effective target:\ngot=%#v\nwant=%#v", got.Target, tt.want) } if got.TargetPresence != tt.wantPresence { t.Fatalf("unexpected target presence: got=%+v want=%+v", got.TargetPresence, tt.wantPresence) } }) } t.Run("blank request reasoning clears profile setting", func(t *testing.T) { profile := executionProfileFixture{ id: "settings-reasoning-clear", endpoint: "http://profile-reasoning.test/v1", model: "profile-reasoning-model", reasoningEffort: "medium", } profileDir := t.TempDir() writeExecutionProfileFixture(t, profileDir, profile) engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, ProfileDir: profileDir, SchemaDir: frameworkSchemaDir, }) if err != nil { t.Fatalf("construct engine: %v", err) } prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: profile.id, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Nia labels the archive."), "glossary": promptkit.Inline("archive: A catalogued collection."), }, Execution: &promptkit.ExecutionTargetOverride{ ReasoningEffort: stringPointer(" \t "), }, }) if err != nil { t.Fatalf("prepare engine: %v", err) } if prepared.EffectiveModelParams.ReasoningEffort != "" { t.Fatalf("expected blank request reasoning to clear profile value, got %q", prepared.EffectiveModelParams.ReasoningEffort) } }) } func TestRunSucceedsWithInjectedLLMClient(t *testing.T) { const envName = "PROMPTKIT_API_KEY" const secret = "run-secret-value" t.Setenv(envName, secret) fake := &fakeLLMClient{ response: &promptkit.GenerateResponse{ Content: "# Summary\n\nDone.", Usage: promptkit.TokenUsage{ PromptTokens: 10, CompletionTokens: 5, TotalTokens: 15, CachedTokens: 3, CacheWriteTokens: 2, }, }, } engine := newContractEngineWithOptions(t, frameworkSchemaDir, promptkit.WithLLMClient(fake)) result, err := engine.Run(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, Execution: &promptkit.ExecutionTargetOverride{ APIKeyEnv: envName, }, }) if err != nil { t.Fatalf("expected run to succeed, got %v", err) } if result.RunID == "" { t.Fatalf("expected run id") } if result.RawOutput != fake.response.Content { t.Fatalf("unexpected raw output: %q", result.RawOutput) } if string(result.Artifact.Body) != fake.response.Content { t.Fatalf("unexpected artifact body: %q", string(result.Artifact.Body)) } if result.Artifact.ContentType != "text/markdown" { t.Fatalf("unexpected artifact content type: %q", result.Artifact.ContentType) } if result.Validation.Status != promptkit.ValidationPassed || !result.Validation.IsValid { t.Fatalf("expected passed validation, got %+v", result.Validation) } if result.PromptID != frameworkMarkdownSummaryPromptID || result.SelectedProfileID != frameworkFastProfileID || result.ModelName != "contract-fast-model" { t.Fatalf("unexpected run metadata: %+v", result) } if result.Usage.TotalTokens != 15 || result.Usage.CachedTokens != 3 || result.Usage.CacheWriteTokens != 2 { t.Fatalf("unexpected usage: %+v", result.Usage) } payload, err := json.Marshal(result) if err != nil { t.Fatalf("expected run result to marshal, got %v", err) } if strings.Contains(string(payload), secret) { t.Fatalf("run result JSON leaked raw API key value: %s", payload) } } func TestEngineRunWithDirectorySourcesAndFileInputs(t *testing.T) { fake := &fakeLLMClient{ response: &promptkit.GenerateResponse{ Content: `{"events":[{"title":"Archive labelled"}]}`, Usage: promptkit.TokenUsage{ PromptTokens: 42, CompletionTokens: 36, TotalTokens: 78, }, }, } engine := newContractEngineWithOptions(t, frameworkSchemaDir, promptkit.WithLLMClient(fake)) result, err := engine.Run(context.Background(), promptkit.RunRequest{ PromptID: frameworkStructuredEventsPromptID, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.File(frameworkTranscriptPath), "glossary": promptkit.File(frameworkGlossaryPath), }, }) if err != nil { t.Fatalf("expected run to succeed, got %v", err) } if result.PromptID != frameworkStructuredEventsPromptID || result.SelectedProfileID != frameworkQualityProfileID { t.Fatalf("unexpected run metadata: %+v", result) } if result.RunID == "" || result.PromptHash == "" || result.RenderedPromptHash == "" { t.Fatalf("expected run and prompt hashes, got %+v", result) } if result.InputHashes["transcript"] == "" || result.InputHashes["glossary"] == "" { t.Fatalf("expected both input hashes, got %#v", result.InputHashes) } if len(fake.requests) != 1 || fake.requests[0].StructuredOutput == nil || fake.requests[0].StructuredOutput.Type != promptkit.StructuredOutputJSONSchema || fake.requests[0].StructuredOutput.JSONSchema == nil || fake.requests[0].StructuredOutput.JSONSchema.Schema == nil { t.Fatalf("expected provider JSON Schema structured output, got %+v", fake.requests) } if result.Validation.Status != promptkit.ValidationPassed || !result.Validation.IsValid || result.Validation.Mode != promptkit.ValidationJSONSchema { t.Fatalf("expected passed JSON Schema validation, got %+v", result.Validation) } if result.Artifact.ContentType != "application/json" { t.Fatalf("expected JSON artifact, got %q", result.Artifact.ContentType) } if result.RawOutput != fake.response.Content || result.Usage != fake.response.Usage { t.Fatalf("expected preserved output and usage, got output=%q usage=%+v", result.RawOutput, result.Usage) } if result.StartTime.IsZero() || result.EndTime.IsZero() || result.EndTime.Before(result.StartTime) || result.Duration < 0 { t.Fatalf("expected ordered non-zero timestamps and non-negative duration, got start=%v end=%v duration=%v", result.StartTime, result.EndTime, result.Duration) } } func TestRunPassesPreparedRequestToInjectedLLMClient(t *testing.T) { const directKey = "direct-injected-key" const directSession = "assembled-session" fake := &fakeLLMClient{ response: &promptkit.GenerateResponse{Content: `{"events":[{"title":"Archive labelled"}]}`}, } engine := newContractEngineWithOptions(t, frameworkSchemaDir, promptkit.WithLLMClient(fake)) runRequest := promptkit.RunRequest{ PromptID: frameworkStructuredEventsPromptID, SessionID: " " + directSession + " ", APIKey: directKey, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, } prepared, err := engine.Prepare(context.Background(), runRequest) if err != nil { t.Fatalf("expected prepare to succeed, got %v", err) } result, err := engine.Run(context.Background(), runRequest) if err != nil { t.Fatalf("expected run to succeed, got %v", err) } if len(fake.requests) != 1 { t.Fatalf("expected one generate request, got %d", len(fake.requests)) } req := fake.requests[0] if len(req.Prompt.Messages) != 2 || !strings.Contains(req.Prompt.Messages[1].Content, "Rin opens the gate.") { t.Fatalf("expected rendered prompt in generate request, got %+v", req.Prompt) } if prepared.SessionID != directSession || req.Prompt.SessionID != directSession || result.SessionID != directSession { t.Fatalf( "direct session did not propagate consistently: prepared=%q generated=%q result=%q", prepared.SessionID, req.Prompt.SessionID, result.SessionID, ) } if req.StructuredOutput == nil || req.StructuredOutput.Type != promptkit.StructuredOutputJSONSchema || req.StructuredOutput.JSONSchema == nil { t.Fatalf("expected structured output handoff, got %+v", req.StructuredOutput) } if req.APIKey != directKey { t.Fatalf("expected direct key on injected generate request") } payload, err := json.Marshal(req) if err != nil { t.Fatalf("expected generate request to marshal, got %v", err) } if strings.Contains(string(payload), directKey) { t.Fatalf("generate request JSON leaked direct API key: %s", payload) } } func TestEngineRunPropagatesCallerCancellation(t *testing.T) { const synchronizationTimeout = 5 * time.Second started := make(chan struct{}) transport := roundTripFunc(func(req *http.Request) (*http.Response, error) { close(started) <-req.Context().Done() return nil, req.Context().Err() }) engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, ProfileDir: frameworkProfileDir, SchemaDir: frameworkSchemaDir, HTTPClient: &http.Client{Transport: transport}, }) if err != nil { t.Fatalf("construct engine: %v", err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() result := make(chan error, 1) go func() { _, err := engine.Run(ctx, promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Nia labels the archive."), "glossary": promptkit.Inline("archive: A catalogued collection."), }, }) result <- err }() watchdog := time.NewTimer(synchronizationTimeout) defer watchdog.Stop() select { case <-started: case err := <-result: t.Fatalf("Engine.Run returned before the transport started: %v", err) case <-watchdog.C: t.Fatal("timed out waiting for the transport to start") } cancel() select { case err := <-result: if !errors.Is(err, promptkit.ErrLLMGenerate) { t.Fatalf("expected ErrLLMGenerate after caller cancellation, got %v", err) } case <-watchdog.C: t.Fatal("timed out waiting for Engine.Run to return after cancellation") } } func TestRunRejectsReservedExtraParamsBeforeProviderCall(t *testing.T) { called := false transport := roundTripFunc(func(*http.Request) (*http.Response, error) { called = true return nil, errors.New("provider should not be called") }) engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, ProfileDir: frameworkProfileDir, SchemaDir: frameworkSchemaDir, HTTPClient: &http.Client{Transport: transport}, }) if err != nil { t.Fatalf("construct engine: %v", err) } _, err = engine.Run(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Nia labels the archive."), "glossary": promptkit.Inline("archive: A catalogued collection."), }, Execution: &promptkit.ExecutionTargetOverride{ ExtraParams: map[string]any{"model": "collision"}, }, }) if !errors.Is(err, promptkit.ErrInvalidRequest) { t.Fatalf("expected ErrInvalidRequest, got %v", err) } if called { t.Fatal("expected reserved provider parameter to fail before the provider call") } } func TestRunUsesDirectAPIKeyWithDefaultLLMClient(t *testing.T) { const directKey = "direct-public-key" const missingEnv = "PROMPTKIT_PUBLIC_DIRECT_MISSING" t.Setenv(missingEnv, "") var gotAuth string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotAuth = r.Header.Get("Authorization") if r.URL.Path != "/v1/chat/completions" { t.Errorf("unexpected path: %s", r.URL.Path) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{ "choices": [{"message": {"role": "assistant", "content": "# Summary\n\nDone."}}], "usage": {"prompt_tokens": 3, "completion_tokens": 4, "total_tokens": 7} }`)) })) defer server.Close() profileDir := t.TempDir() writePublicProfileFileWithAPIKeyEnv(t, profileDir, "direct-auth", server.URL+"/v1", "test-model", missingEnv) engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, ProfileDir: profileDir, SchemaDir: frameworkSchemaDir, }) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } result, err := engine.Run(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "direct-auth", APIKey: directKey, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if err != nil { t.Fatalf("expected run with direct API key to succeed, got %v", err) } if gotAuth != "Bearer "+directKey { t.Fatalf("unexpected Authorization header: %q", gotAuth) } if result.Usage.TotalTokens != 7 { t.Fatalf("unexpected usage: %+v", result.Usage) } payload, err := json.Marshal(result) if err != nil { t.Fatalf("expected run result to marshal, got %v", err) } if strings.Contains(string(payload), directKey) { t.Fatalf("run result JSON leaked direct API key: %s", payload) } } func TestRunUsesResolvedBackendWithBuiltInLLMClient(t *testing.T) { const ( backendID = "local-test" envName = "PROMPTKIT_BACKEND_TRANSPORT_KEY" apiKey = "synthetic-backend-key" ) t.Setenv(envName, apiKey) var ( gotAuth string gotBody map[string]any ) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotAuth = r.Header.Get("Authorization") if r.URL.Path != "/v1/chat/completions" { t.Errorf("unexpected path: %s", r.URL.Path) } if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil { t.Errorf("decode request body: %v", err) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{ "choices": [{"message": {"role": "assistant", "content": "# Summary\n\nDone."}}], "usage": {"prompt_tokens": 3, "completion_tokens": 4, "total_tokens": 7} }`)) })) defer server.Close() engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, SchemaDir: frameworkSchemaDir, }, promptkit.WithBackend(promptkit.Backend{ ID: backendID, Endpoint: server.URL + "/v1", APIKeyEnv: envName, ExtraParams: map[string]any{ "provider": "synthetic", }, }), promptkit.WithProfiles(promptkit.Profile{ ID: "backend-transport", BackendID: backendID, Model: "test-model", }), ) if err != nil { t.Fatalf("construct engine: %v", err) } result, err := engine.Run(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "backend-transport", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if err != nil { t.Fatalf("run with resolved backend: %v", err) } if gotAuth != "Bearer "+apiKey { t.Fatalf("unexpected Authorization header: %q", gotAuth) } if gotBody["model"] != "test-model" || gotBody["provider"] != "synthetic" { t.Fatalf("backend defaults did not reach provider payload: %#v", gotBody) } for _, field := range []string{"backend_id", "api_key_env"} { if _, ok := gotBody[field]; ok { t.Fatalf("internal metadata field %q was serialized to provider payload: %#v", field, gotBody) } } bodyJSON, err := json.Marshal(gotBody) if err != nil { t.Fatalf("marshal captured provider payload: %v", err) } if strings.Contains(string(bodyJSON), apiKey) { t.Fatalf("credential value was serialized to provider payload: %s", bodyJSON) } if result.SelectedBackendID != backendID || result.EffectiveModelParams.Endpoint != server.URL+"/v1" || result.EffectiveModelParams.APIKeyEnv != envName { t.Fatalf("unexpected resolved backend metadata: %+v", result) } } func TestPrepareDirectAPIKeyBypassesMissingEnvWithoutLeakingOrHashing(t *testing.T) { const missingEnv = "PROMPTKIT_PUBLIC_PREPARE_MISSING" const firstKey = "first-direct-key" const secondKey = "second-direct-key" t.Setenv(missingEnv, "") profileDir := t.TempDir() writePublicProfileFileWithAPIKeyEnv(t, profileDir, "direct-prepare", "http://localhost:8000/v1", "test-model", missingEnv) engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, ProfileDir: profileDir, SchemaDir: frameworkSchemaDir, }) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } baseReq := promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "direct-prepare", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, } firstReq := baseReq firstReq.APIKey = firstKey firstPrepared, err := engine.Prepare(context.Background(), firstReq) if err != nil { t.Fatalf("expected prepare with direct API key to succeed, got %v", err) } secondReq := baseReq secondReq.APIKey = secondKey secondPrepared, err := engine.Prepare(context.Background(), secondReq) if err != nil { t.Fatalf("expected prepare with alternate direct API key to succeed, got %v", err) } if firstPrepared.PromptHash != secondPrepared.PromptHash { t.Fatalf("direct API keys changed prompt hash: %q vs %q", firstPrepared.PromptHash, secondPrepared.PromptHash) } if firstPrepared.RenderedPromptHash != secondPrepared.RenderedPromptHash { t.Fatalf("direct API keys changed rendered prompt hash: %q vs %q", firstPrepared.RenderedPromptHash, secondPrepared.RenderedPromptHash) } payload, err := json.Marshal(firstPrepared) if err != nil { t.Fatalf("expected prepared run to marshal, got %v", err) } if strings.Contains(string(payload), firstKey) { t.Fatalf("prepared run JSON leaked direct API key: %s", payload) } } func TestMissingCredentialsFailClearlyWhenProfileRequiresAuth(t *testing.T) { const missingEnv = "PROMPTKIT_PUBLIC_AUTH_MISSING" t.Setenv(missingEnv, "") profileDir := t.TempDir() writePublicProfileFileWithAPIKeyEnv(t, profileDir, "requires-auth", "http://localhost:8000/v1", "test-model", missingEnv) engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, ProfileDir: profileDir, SchemaDir: frameworkSchemaDir, }) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } _, err = engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "requires-auth", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if !errors.Is(err, promptkit.ErrInvalidRequest) { t.Fatalf("expected invalid request for missing credentials, got %v", err) } if !errors.Is(err, promptkit.ErrAPIKeyEnvMissing) { t.Fatalf("expected missing credential environment error, got %v", err) } if err == nil || !strings.Contains(err.Error(), missingEnv) { t.Fatalf("expected missing env name in error, got %v", err) } } func TestWithArtifactReaderRejectsNilReader(t *testing.T) { _, err := promptkit.NewEngine(contractConfig(frameworkSchemaDir), promptkit.WithArtifactReader(nil)) if !errors.Is(err, promptkit.ErrInvalidConfig) { t.Fatalf("expected ErrInvalidConfig, got %v", err) } } func TestArtifactReaderReceivesPublicReferenceAndPreparesArtifact(t *testing.T) { reader := &recordingArtifactReader{ artifact: &promptkit.Artifact{ ContentType: "text/plain", Body: []byte("Reader-supplied transcript."), URI: "reader://transcript", Size: int64(len("Reader-supplied transcript.")), Hash: "reader-transcript-hash", }, } engine := newArtifactReaderEngine(t, reader) ref := promptkit.ArtifactRef{ Type: promptkit.ArtifactRefInline, URI: "reader://transcript", Body: "request body", } prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: "artifact-reader", Inputs: map[string]promptkit.ArtifactRef{ "transcript": ref, }, }) if err != nil { t.Fatalf("prepare with artifact reader: %v", err) } if len(reader.refs) != 1 || !reflect.DeepEqual(reader.refs[0], ref) { t.Fatalf("reader received %#v, want %#v", reader.refs, ref) } if prepared.InputHashes["transcript"] != "reader-transcript-hash" { t.Fatalf("unexpected input hash: %#v", prepared.InputHashes) } if len(prepared.Messages) != 1 || !strings.Contains(prepared.Messages[0].Content, "Reader-supplied transcript.") { t.Fatalf("prepared prompt omitted reader artifact: %#v", prepared.Messages) } } func TestArtifactReaderFailuresPreserveArtifactLoadErrors(t *testing.T) { readerErr := errors.New("artifact reader failed") tests := []struct { name string ctx context.Context reader *recordingArtifactReader wantNested error }{ { name: "reader error", ctx: context.Background(), reader: &recordingArtifactReader{err: readerErr}, wantNested: readerErr, }, { name: "nil artifact", ctx: context.Background(), reader: &recordingArtifactReader{}, }, { name: "reader cancellation", ctx: context.Background(), reader: &recordingArtifactReader{read: func(context.Context, promptkit.ArtifactRef) (*promptkit.Artifact, error) { return nil, context.Canceled }}, wantNested: context.Canceled, }, { name: "public reader error", ctx: context.Background(), reader: &recordingArtifactReader{err: promptkit.ErrInvalidRequest}, wantNested: promptkit.ErrInvalidRequest, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { engine := newArtifactReaderEngine(t, tc.reader) _, err := engine.Prepare(tc.ctx, promptkit.RunRequest{ PromptID: "artifact-reader", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("input"), }, }) if !errors.Is(err, promptkit.ErrArtifactLoad) { t.Fatalf("expected ErrArtifactLoad, got %v", err) } if tc.wantNested != nil && !errors.Is(err, tc.wantNested) { t.Fatalf("expected nested %v, got %v", tc.wantNested, err) } }) } } func TestRunAddsLLMGenerateToCollaboratorPublicError(t *testing.T) { engine := newContractEngineWithOptions(t, frameworkSchemaDir, promptkit.WithLLMClient(&fakeLLMClient{err: promptkit.ErrArtifactLoad}), ) _, err := engine.Run(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if !errors.Is(err, promptkit.ErrLLMGenerate) { t.Fatalf("expected ErrLLMGenerate, got %v", err) } if !errors.Is(err, promptkit.ErrArtifactLoad) { t.Fatalf("expected preserved ErrArtifactLoad, got %v", err) } } func TestPrepareWithoutProfileMatchesSpecificPublicError(t *testing.T) { promptDir := t.TempDir() writePublicPromptFile(t, promptDir, "profile-required", "") engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: promptDir, SchemaDir: frameworkSchemaDir, }) if err != nil { t.Fatalf("construct engine: %v", err) } _, err = engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: "profile-required", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("input"), }, }) if !errors.Is(err, promptkit.ErrInvalidRequest) { t.Fatalf("expected ErrInvalidRequest, got %v", err) } if !errors.Is(err, promptkit.ErrProfileRequired) { t.Fatalf("expected ErrProfileRequired, got %v", err) } } func TestRunValidationFailureReturnsResult(t *testing.T) { fake := &fakeLLMClient{ response: &promptkit.GenerateResponse{Content: ""}, } engine := newContractEngineWithOptions(t, frameworkSchemaDir, promptkit.WithLLMClient(fake)) result, err := engine.Run(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if err != nil { t.Fatalf("expected validation failure as successful result, got %v", err) } if result.Validation.Status != promptkit.ValidationFailed || result.Validation.IsValid { t.Fatalf("expected failed validation result, got %+v", result.Validation) } if len(result.Validation.Errors) == 0 { t.Fatalf("expected validation errors") } } func TestPublicErrorsSupportErrorsIs(t *testing.T) { llmErr := errors.New("llm failed") tests := []struct { name string req promptkit.RunRequest client promptkit.LLMClient schemaDir string want error notWant error }{ { name: "invalid request", req: promptkit.RunRequest{}, client: &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}}, want: promptkit.ErrInvalidRequest, }, { name: "prompt not found", req: promptkit.RunRequest{PromptID: "missing.prompt"}, client: &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}}, want: promptkit.ErrPromptNotFound, notWant: promptkit.ErrPromptLoad, }, { name: "profile not found", req: promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "missing-profile", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), }, }, client: &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}}, want: promptkit.ErrProfileNotFound, notWant: promptkit.ErrProfileLoad, }, { name: "artifact load", req: promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.File(filepath.Join(t.TempDir(), "does-not-exist.md")), }, }, client: &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}}, want: promptkit.ErrArtifactLoad, }, { name: "prompt render", req: promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, }, client: &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}}, want: promptkit.ErrPromptRender, }, { name: "llm failure", req: promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }, client: &fakeLLMClient{err: llmErr}, want: promptkit.ErrLLMGenerate, }, { name: "nil llm response", req: promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }, client: &fakeLLMClient{}, want: promptkit.ErrLLMGenerate, }, { name: "validation runtime failure", req: promptkit.RunRequest{ PromptID: frameworkStructuredEventsPromptID, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), }, }, client: &fakeLLMClient{response: &promptkit.GenerateResponse{Content: `{"events":[]}`}}, schemaDir: t.TempDir(), want: promptkit.ErrValidation, }, } t.Setenv("PROMPTKIT_API_KEY", "test-secret") for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { schemaDir := tc.schemaDir if schemaDir == "" { schemaDir = frameworkSchemaDir } engine := newContractEngineWithOptions(t, schemaDir, promptkit.WithLLMClient(tc.client)) _, err := engine.Run(context.Background(), tc.req) if !errors.Is(err, tc.want) { t.Fatalf("expected errors.Is(%v), got %v", tc.want, err) } if tc.notWant != nil && errors.Is(err, tc.notWant) { t.Fatalf("did not expect errors.Is(%v), got %v", tc.notWant, err) } }) } } func TestSelectedProfileRawAPIKeyMapsToProfileLoad(t *testing.T) { profileDir := t.TempDir() if err := os.WriteFile(filepath.Join(profileDir, "raw.yaml"), []byte(` id: raw-profile endpoint: http://localhost:8000/v1 model: model api_key: secret `), 0644); err != nil { t.Fatal(err) } engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, ProfileDir: profileDir, SchemaDir: frameworkSchemaDir, }) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } _, err = engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "raw-profile", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if !errors.Is(err, promptkit.ErrProfileLoad) { t.Fatalf("expected ErrProfileLoad, got %v", err) } if errors.Is(err, promptkit.ErrPromptLoad) { t.Fatalf("did not expect ErrPromptLoad, got %v", err) } } func TestSelectedProfileInvalidYAMLMapsToProfileLoad(t *testing.T) { profileDir := t.TempDir() if err := os.WriteFile(filepath.Join(profileDir, "broken.yaml"), []byte(` id: broken-profile unknown_field: true `), 0644); err != nil { t.Fatal(err) } engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, ProfileDir: profileDir, SchemaDir: frameworkSchemaDir, }) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } _, err = engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "broken-profile", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if !errors.Is(err, promptkit.ErrProfileLoad) { t.Fatalf("expected ErrProfileLoad, got %v", err) } if errors.Is(err, promptkit.ErrPromptLoad) { t.Fatalf("did not expect ErrPromptLoad, got %v", err) } } func TestPromptRepositoryReadFailureMapsToPromptLoad(t *testing.T) { missingPromptDir := filepath.Join(t.TempDir(), "missing-prompts") engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: missingPromptDir, ProfileDir: frameworkProfileDir, SchemaDir: frameworkSchemaDir, }) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } _, err = engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if !errors.Is(err, promptkit.ErrPromptLoad) { t.Fatalf("expected ErrPromptLoad, got %v", err) } if errors.Is(err, promptkit.ErrProfileLoad) { t.Fatalf("did not expect ErrProfileLoad, got %v", err) } } func TestSelectedProfileRepositoryReadFailureMapsToProfileLoad(t *testing.T) { missingProfileDir := filepath.Join(t.TempDir(), "missing-profiles") engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, ProfileDir: missingProfileDir, SchemaDir: frameworkSchemaDir, }) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } _, err = engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: frameworkFastProfileID, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if !errors.Is(err, promptkit.ErrProfileLoad) { t.Fatalf("expected ErrProfileLoad, got %v", err) } if errors.Is(err, promptkit.ErrPromptLoad) { t.Fatalf("did not expect ErrPromptLoad, got %v", err) } } func TestPrepareUsesBuiltInProfileWithoutProfileDir(t *testing.T) { t.Setenv("OPENROUTER_API_KEY", "test-key") engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, SchemaDir: frameworkSchemaDir, }) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "mistral-small-3", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if err != nil { t.Fatalf("expected built-in profile prepare to succeed, got %v", err) } if prepared.SelectedProfileID != "mistral-small-3" { t.Fatalf("unexpected selected profile: %q", prepared.SelectedProfileID) } if prepared.SelectedBackendID != promptkit.BackendOpenRouter { t.Fatalf("unexpected selected backend: %q", prepared.SelectedBackendID) } if prepared.EffectiveModelParams.BackendID != promptkit.BackendOpenRouter { t.Fatalf("unexpected effective backend: %q", prepared.EffectiveModelParams.BackendID) } if prepared.EffectiveModelParams.Endpoint != "https://openrouter.ai/api/v1" { t.Fatalf("unexpected built-in endpoint: %q", prepared.EffectiveModelParams.Endpoint) } if prepared.EffectiveModelParams.APIKeyEnv != "OPENROUTER_API_KEY" { t.Fatalf("unexpected built-in api key environment name: %q", prepared.EffectiveModelParams.APIKeyEnv) } if prepared.EffectiveModelParams.Model != "mistralai/mistral-small-3.2-24b-instruct" { t.Fatalf("unexpected built-in model: %q", prepared.EffectiveModelParams.Model) } } func TestPromptDefaultProfileCanUseBuiltInProfile(t *testing.T) { t.Setenv("OPENROUTER_API_KEY", "test-key") promptDir := t.TempDir() writePublicPromptFile(t, promptDir, "prompt.builtin.default", "mistral-small-3") engine, err := promptkit.NewEngine(promptkit.Config{PromptDir: promptDir}) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: "prompt.builtin.default", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), }, }) if err != nil { t.Fatalf("expected built-in default profile prepare to succeed, got %v", err) } if prepared.SelectedProfileID != "mistral-small-3" { t.Fatalf("unexpected selected profile: %q", prepared.SelectedProfileID) } } func TestCustomProfileOverridesBuiltInProfile(t *testing.T) { t.Setenv("OPENROUTER_API_KEY", "test-key") profileDir := t.TempDir() writePublicProfileFile(t, profileDir, "mistral-small-3", "http://localhost:8000/v1", "custom-model") engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, ProfileDir: profileDir, SchemaDir: frameworkSchemaDir, }) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "mistral-small-3", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if err != nil { t.Fatalf("expected custom profile prepare to succeed, got %v", err) } if prepared.EffectiveModelParams.Model != "custom-model" { t.Fatalf("expected custom profile to override built-in, got %q", prepared.EffectiveModelParams.Model) } } func TestMalformedCustomProfileDoesNotFallbackToBuiltIn(t *testing.T) { t.Setenv("OPENROUTER_API_KEY", "test-key") profileDir := t.TempDir() if err := os.WriteFile(filepath.Join(profileDir, "mistral-small-3.yml"), []byte(` id: mistral-small-3 endpoint: http://localhost:8000/v1 model: custom-model unexpected: true `), 0o644); err != nil { t.Fatal(err) } engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, ProfileDir: profileDir, SchemaDir: frameworkSchemaDir, }) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } _, err = engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "mistral-small-3", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if !errors.Is(err, promptkit.ErrProfileLoad) { t.Fatalf("expected custom profile load error, got %v", err) } } func TestPrepareWorksWithPromptFSAndRelativeContentFile(t *testing.T) { promptFS := fstest.MapFS{ "assets/prompts/fs-summary.yaml": &fstest.MapFile{Data: []byte(` id: fs.summary version: "1.0.0" default_profile: contract-fast inputs: - name: transcript required: true messages: - role: user content_file: ./messages/summary.tmpl output: format: text validation_mode: none repair_attempts: 0 `)}, "assets/prompts/messages/summary.tmpl": &fstest.MapFile{Data: []byte(`Summarize {{input "transcript"}} from prompt fs.`)}, } engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: t.TempDir(), ProfileDir: frameworkProfileDir, SchemaDir: frameworkSchemaDir, }, promptkit.WithPromptFS(promptFS, "assets/prompts")) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: "fs.summary", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), }, }) if err != nil { t.Fatalf("expected prepare to succeed, got %v", err) } if len(prepared.Messages) != 1 || !strings.Contains(prepared.Messages[0].Content, "prompt fs") { t.Fatalf("expected content_file body from prompt fs, got %+v", prepared.Messages) } } func TestPrepareWithPromptFSRejectsEscapedContentFile(t *testing.T) { promptFS := fstest.MapFS{ "assets/prompts/fs-escape.yaml": &fstest.MapFile{Data: []byte(` id: fs.escape version: "1.0.0" default_profile: contract-fast messages: - role: user content_file: ../outside.tmpl output: format: text validation_mode: none repair_attempts: 0 `)}, "assets/outside.tmpl": &fstest.MapFile{Data: []byte(`Outside root.`)}, } engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: t.TempDir(), ProfileDir: frameworkProfileDir, SchemaDir: frameworkSchemaDir, }, promptkit.WithPromptFS(promptFS, "assets/prompts")) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } _, err = engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "fs.escape"}) if !errors.Is(err, promptkit.ErrPromptLoad) { t.Fatalf("expected ErrPromptLoad, got %v", err) } } func TestPrepareWorksWithPromptFile(t *testing.T) { promptDir := t.TempDir() promptPath := filepath.Join(promptDir, "single.yaml") if err := os.WriteFile(promptPath, []byte(` id: single.file.prompt version: "1.0.0" default_profile: contract-fast inputs: - name: transcript required: true messages: - role: user content_file: ./single.tmpl output: format: text validation_mode: none repair_attempts: 0 `), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(promptDir, "single.tmpl"), []byte(`Summarize {{input "transcript"}} from file.`), 0o644); err != nil { t.Fatal(err) } engine, err := promptkit.NewEngine(promptkit.Config{ ProfileDir: frameworkProfileDir, SchemaDir: frameworkSchemaDir, }, promptkit.WithPromptFile(promptPath)) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: "single.file.prompt", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), }, }) if err != nil { t.Fatalf("expected prepare to succeed, got %v", err) } if prepared.PromptID != "single.file.prompt" { t.Fatalf("unexpected prompt id: %q", prepared.PromptID) } if len(prepared.Messages) != 1 || !strings.Contains(prepared.Messages[0].Content, "from file") { t.Fatalf("expected content_file body from prompt file, got %+v", prepared.Messages) } } func TestPrepareWorksWithProfileFSOverBuiltIns(t *testing.T) { profileFS := fstest.MapFS{ "profiles/mistral-small-3.yaml": &fstest.MapFile{Data: []byte(` id: mistral-small-3 endpoint: http://profile-fs/v1 model: profile-fs-model `)}, } engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, SchemaDir: frameworkSchemaDir, }, promptkit.WithProfileFS(profileFS, "profiles")) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "mistral-small-3", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if err != nil { t.Fatalf("expected prepare to succeed, got %v", err) } if prepared.EffectiveModelParams.Model != "profile-fs-model" { t.Fatalf("expected profile fs to override built-in, got %q", prepared.EffectiveModelParams.Model) } } func TestPrepareWorksWithProfileFileOverBuiltIns(t *testing.T) { profileDir := t.TempDir() profilePath := filepath.Join(profileDir, "mistral-small-3.yaml") if err := os.WriteFile(profilePath, []byte(` id: mistral-small-3 endpoint: http://profile-file/v1 model: profile-file-model `), 0o644); err != nil { t.Fatal(err) } engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, SchemaDir: frameworkSchemaDir, }, promptkit.WithProfileFile(profilePath)) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "mistral-small-3", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if err != nil { t.Fatalf("expected prepare to succeed, got %v", err) } if prepared.EffectiveModelParams.Model != "profile-file-model" { t.Fatalf("expected profile file to override built-in, got %q", prepared.EffectiveModelParams.Model) } } func TestPrepareWorksWithInMemoryProfilesWithoutProfileFiles(t *testing.T) { engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, SchemaDir: frameworkSchemaDir, }, promptkit.WithProfiles(promptkit.Profile{ ID: "memory-profile", Endpoint: "http://memory-profile/v1", Model: "memory-model", })) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "memory-profile", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if err != nil { t.Fatalf("expected prepare to succeed, got %v", err) } if prepared.EffectiveModelParams.Model != "memory-model" { t.Fatalf("expected in-memory profile model, got %q", prepared.EffectiveModelParams.Model) } } func TestInMemoryProfilesOverrideBuiltInsAndProfileSources(t *testing.T) { profileFS := fstest.MapFS{ "profiles/mistral-small-3.yaml": &fstest.MapFile{Data: []byte(` id: mistral-small-3 endpoint: http://profile-fs/v1 model: profile-fs-model `)}, } engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, SchemaDir: frameworkSchemaDir, }, promptkit.WithProfileFS(profileFS, "profiles"), promptkit.WithProfiles(promptkit.Profile{ ID: "mistral-small-3", Endpoint: "http://memory-profile/v1", Model: "memory-profile-model", }), ) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "mistral-small-3", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if err != nil { t.Fatalf("expected prepare to succeed, got %v", err) } if prepared.EffectiveModelParams.Model != "memory-profile-model" { t.Fatalf("expected in-memory profile to have highest precedence, got %q", prepared.EffectiveModelParams.Model) } } func TestWithProfilesRejectsDuplicateIDs(t *testing.T) { _, err := promptkit.NewEngine(promptkit.Config{PromptDir: frameworkPromptDir}, promptkit.WithProfiles( promptkit.Profile{ID: "duplicate", Endpoint: "http://one/v1", Model: "one"}, promptkit.Profile{ID: "duplicate", Endpoint: "http://two/v1", Model: "two"}, ), ) if !errors.Is(err, promptkit.ErrInvalidConfig) { t.Fatalf("expected ErrInvalidConfig, got %v", err) } } func TestWithProfilesRejectsInvalidExecutionSettings(t *testing.T) { type testCase struct { name string profile promptkit.Profile } tests := []testCase{ {name: "non-finite temperature", profile: promptkit.Profile{Temperature: math.NaN()}}, {name: "non-finite top p", profile: promptkit.Profile{TopP: math.Inf(-1)}}, } if strconv.IntSize == 64 { durationLimit := int64(math.MaxInt64 / int64(time.Second)) tests = append(tests, testCase{name: "unrepresentable timeout", profile: promptkit.Profile{TimeoutSeconds: int(durationLimit) + 1}}) } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.profile.ID = "invalid-settings" tt.profile.Endpoint = "http://example.test/v1" tt.profile.Model = "model" _, err := promptkit.NewEngine(promptkit.Config{PromptDir: frameworkPromptDir}, promptkit.WithProfiles(tt.profile), ) if !errors.Is(err, promptkit.ErrInvalidConfig) { t.Fatalf("expected ErrInvalidConfig, got %v", err) } }) } } func TestOpenAICompatibleProfileRunsThroughNormalProfilePath(t *testing.T) { fake := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}} prof := promptkit.OpenAICompatibleProfile(promptkit.OpenAICompatibleProfileConfig{ ID: "template-profile", BackendID: " openrouter ", Endpoint: "http://template/v1", Model: "template-model", APIKeyRequired: true, ExtraParams: map[string]any{ "provider": "template", }, }) engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, SchemaDir: frameworkSchemaDir, }, promptkit.WithProfiles(prof), promptkit.WithLLMClient(fake)) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } _, err = engine.Run(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "template-profile", APIKey: "template-key", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if err != nil { t.Fatalf("expected run to succeed, got %v", err) } if len(fake.requests) != 1 { t.Fatalf("expected one request, got %d", len(fake.requests)) } if fake.requests[0].Target.BackendID != promptkit.BackendOpenRouter || fake.requests[0].Target.Model != "template-model" || fake.requests[0].APIKey != "template-key" { t.Fatalf("unexpected generated request: %+v", fake.requests[0]) } if !reflect.DeepEqual(fake.requests[0].Target.ExtraParams, map[string]any{"provider": "template"}) { t.Fatalf("unexpected extra params: %#v", fake.requests[0].Target.ExtraParams) } } func TestOpenAICompatibleProfileMapsEveryField(t *testing.T) { extraParams := map[string]any{"provider_option": "distinct-extra-params"} got := promptkit.OpenAICompatibleProfile(promptkit.OpenAICompatibleProfileConfig{ ID: "distinct-id", BackendID: "distinct-backend", Endpoint: "https://distinct.example/v1", Model: "distinct-model", APIKeyRequired: true, Temperature: 0.25, MaxTokens: 321, TopP: 0.75, TimeoutSeconds: 43, ServiceTier: "distinct-service-tier", ReasoningEffort: "distinct-reasoning-effort", ExtraParams: extraParams, }) want := promptkit.Profile{ ID: "distinct-id", BackendID: "distinct-backend", Endpoint: "https://distinct.example/v1", Model: "distinct-model", Temperature: 0.25, MaxTokens: 321, TopP: 0.75, TimeoutSeconds: 43, ServiceTier: "distinct-service-tier", ReasoningEffort: "distinct-reasoning-effort", APIKeyRequired: true, ExtraParams: map[string]any{"provider_option": "distinct-extra-params"}, } if !reflect.DeepEqual(got, want) { t.Fatalf("profile mapping:\ngot %#v\nwant %#v", got, want) } extraParams["added_after_construction"] = true if _, ok := got.ExtraParams["added_after_construction"]; ok { t.Fatal("profile retained the configuration map") } } func TestEngineRunLayersTransportAndGenerationTimeouts(t *testing.T) { intPointer := func(value int) *int { return &value } tests := []struct { name string configTimeout time.Duration suppliedClientTimeout time.Duration profileTimeoutSeconds int requestTimeoutSeconds *int callerTimeout time.Duration wantRemainingAtRequest time.Duration }{ { name: "positive supplied client cap takes precedence over config", configTimeout: 2 * time.Second, suppliedClientTimeout: 6 * time.Second, wantRemainingAtRequest: 6 * time.Second, }, { name: "zero supplied client timeout inherits config cap", configTimeout: 5 * time.Second, wantRemainingAtRequest: 5 * time.Second, }, { name: "zero configuration uses ten minute transport default", wantRemainingAtRequest: 10 * time.Minute, }, { name: "negative configuration uses ten minute transport default", configTimeout: -2 * time.Second, suppliedClientTimeout: -3 * time.Second, wantRemainingAtRequest: 10 * time.Minute, }, { name: "profile deadline is shorter than transport cap", suppliedClientTimeout: 6 * time.Second, profileTimeoutSeconds: 4, wantRemainingAtRequest: 4 * time.Second, }, { name: "request deadline is shorter than profile and transport limits", suppliedClientTimeout: 6 * time.Second, profileTimeoutSeconds: 4, requestTimeoutSeconds: intPointer(2), wantRemainingAtRequest: 2 * time.Second, }, { name: "explicit zero removes generation deadline but retains transport cap", suppliedClientTimeout: 5 * time.Second, profileTimeoutSeconds: 2, requestTimeoutSeconds: intPointer(0), wantRemainingAtRequest: 5 * time.Second, }, { name: "framework default remains layered with shorter transport cap", configTimeout: 7 * time.Second, suppliedClientTimeout: 3 * time.Second, wantRemainingAtRequest: 3 * time.Second, }, { name: "caller deadline remains layered with other limits", suppliedClientTimeout: 6 * time.Second, profileTimeoutSeconds: 4, callerTimeout: 2 * time.Second, wantRemainingAtRequest: 2 * time.Second, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var ( sawDeadline bool remaining time.Duration ) transport := roundTripFunc(func(req *http.Request) (*http.Response, error) { deadline, ok := req.Context().Deadline() sawDeadline = ok if ok { remaining = time.Until(deadline) } return &http.Response{ StatusCode: http.StatusOK, Status: "200 OK", Header: make(http.Header), Body: io.NopCloser(strings.NewReader( `{"choices":[{"message":{"content":"ok"}}]}`, )), Request: req, }, nil }) httpClient := &http.Client{ Timeout: tc.suppliedClientTimeout, Transport: transport, } engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, SchemaDir: frameworkSchemaDir, Timeout: tc.configTimeout, HTTPClient: httpClient, }, promptkit.WithProfiles(promptkit.Profile{ ID: "layered-timeout", Endpoint: "http://timeout.test/v1", Model: "timeout-model", TimeoutSeconds: tc.profileTimeoutSeconds, })) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } ctx := context.Background() cancel := func() {} if tc.callerTimeout > 0 { ctx, cancel = context.WithTimeout(ctx, tc.callerTimeout) } defer cancel() _, err = engine.Run(ctx, promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "layered-timeout", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, Execution: &promptkit.ExecutionTargetOverride{ TimeoutSeconds: tc.requestTimeoutSeconds, }, }) if err != nil { t.Fatalf("expected run to succeed, got %v", err) } if !sawDeadline { t.Fatal("expected outbound request context to have a deadline") } const deadlineTolerance = 750 * time.Millisecond if remaining < tc.wantRemainingAtRequest-deadlineTolerance || remaining > tc.wantRemainingAtRequest+50*time.Millisecond { t.Fatalf( "unexpected request deadline: remaining=%v want approximately %v", remaining, tc.wantRemainingAtRequest, ) } }) } } func TestOpenAICompatibleProfileDefersExtraParamsValidation(t *testing.T) { cyclic := map[string]any{} cyclic["self"] = cyclic prof := promptkit.OpenAICompatibleProfile(promptkit.OpenAICompatibleProfileConfig{ ID: "cyclic-template-profile", Endpoint: "http://cyclic-template/v1", Model: "cyclic-template-model", ExtraParams: cyclic, }) _, err := promptkit.NewEngine(promptkit.Config{PromptDir: frameworkPromptDir}, promptkit.WithProfiles(prof), ) if !errors.Is(err, promptkit.ErrInvalidConfig) { t.Fatalf("expected ErrInvalidConfig, got %v", err) } } func TestOpenAICompatibleProfileNestedExtraParamsRunThroughWithProfiles(t *testing.T) { fake := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}} nested := map[string]any{ "labels": map[string]string{"route": "primary"}, "ids": []int{1, 2, 3}, } extraParams := map[string]any{ "nested": nested, } prof := promptkit.OpenAICompatibleProfile(promptkit.OpenAICompatibleProfileConfig{ ID: "nested-template-profile", Endpoint: "http://nested-template/v1", Model: "nested-template-model", ExtraParams: extraParams, }) extraParams["added"] = "mutated-after-construction" engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, SchemaDir: frameworkSchemaDir, }, promptkit.WithProfiles(prof), promptkit.WithLLMClient(fake)) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } nested["added"] = "mutated-after-construction" _, err = engine.Run(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "nested-template-profile", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if err != nil { t.Fatalf("expected run to succeed, got %v", err) } want := map[string]any{ "nested": map[string]any{ "labels": map[string]string{"route": "primary"}, "ids": []int{1, 2, 3}, }, } if !reflect.DeepEqual(fake.requests[0].Target.ExtraParams, want) { t.Fatalf("unexpected extra params:\ngot=%#v\nwant=%#v", fake.requests[0].Target.ExtraParams, want) } } func TestInMemoryProfileAPIKeyRequiredBehavior(t *testing.T) { engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, SchemaDir: frameworkSchemaDir, }, promptkit.WithProfiles(promptkit.Profile{ ID: "requires-key", Endpoint: "http://requires-key/v1", Model: "requires-key-model", APIKeyRequired: true, })) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } req := promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "requires-key", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, } _, err = engine.Prepare(context.Background(), req) if !errors.Is(err, promptkit.ErrInvalidRequest) { t.Fatalf("expected ErrInvalidRequest without API key, got %v", err) } req.APIKey = "direct-required-key" if _, err := engine.Prepare(context.Background(), req); err != nil { t.Fatalf("expected direct API key to satisfy APIKeyRequired, got %v", err) } } func TestInMemoryProfileWithoutAPIKeyRequiredWorksWithoutKey(t *testing.T) { engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, SchemaDir: frameworkSchemaDir, }, promptkit.WithProfiles(promptkit.Profile{ ID: "no-key-required", Endpoint: "http://no-key/v1", Model: "no-key-model", })) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } _, err = engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "no-key-required", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if err != nil { t.Fatalf("expected prepare without API key to succeed, got %v", err) } } func TestInMemoryProfileExtraParamsAreCopiedAcrossPublicBoundary(t *testing.T) { fake := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}} labels := map[string]string{"route": "primary"} ids := []int{1, 2, 3} extraParams := map[string]any{ "labels": labels, "ids": ids, } engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, SchemaDir: frameworkSchemaDir, }, promptkit.WithProfiles(promptkit.Profile{ ID: "copy-profile", Endpoint: "http://copy/v1", Model: "copy-model", ExtraParams: extraParams, }), promptkit.WithLLMClient(fake), ) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } labels["route"] = "mutated-before-run" ids[0] = 99 extraParams["added"] = "mutated" _, err = engine.Run(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "copy-profile", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if err != nil { t.Fatalf("expected run to succeed, got %v", err) } want := map[string]any{ "labels": map[string]string{"route": "primary"}, "ids": []int{1, 2, 3}, } if !reflect.DeepEqual(fake.requests[0].Target.ExtraParams, want) { t.Fatalf("captured extra params changed after mutation:\ngot=%#v\nwant=%#v", fake.requests[0].Target.ExtraParams, want) } } func TestWithProfilesRejectsInvalidExtraParams(t *testing.T) { tests := []struct { name string extraParams map[string]any }{ {name: "function", extraParams: map[string]any{"bad": func() {}}}, {name: "channel", extraParams: map[string]any{"bad": make(chan struct{})}}, {name: "struct", extraParams: map[string]any{"bad": struct{ Name string }{Name: "bad"}}}, {name: "non string map key", extraParams: map[string]any{"bad": map[int]string{1: "one"}}}, {name: "nan", extraParams: map[string]any{"bad": math.NaN()}}, {name: "positive infinity", extraParams: map[string]any{"bad": math.Inf(1)}}, {name: "negative infinity", extraParams: map[string]any{"bad": math.Inf(-1)}}, {name: "excessively large value", extraParams: map[string]any{"bad": excessivelyLargeJSONValue()}}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { _, err := promptkit.NewEngine(promptkit.Config{PromptDir: frameworkPromptDir}, promptkit.WithProfiles(promptkit.Profile{ ID: "invalid-extra-params", Endpoint: "http://invalid/v1", Model: "invalid-model", ExtraParams: tc.extraParams, }), ) if !errors.Is(err, promptkit.ErrInvalidConfig) { t.Fatalf("expected ErrInvalidConfig, got %v", err) } }) } } func TestWithProfilesRejectsCyclicExtraParams(t *testing.T) { cyclicMap := map[string]any{} cyclicMap["self"] = cyclicMap cyclicSlice := []any{nil} cyclicSlice[0] = cyclicSlice tests := []struct { name string extraParams map[string]any }{ {name: "map", extraParams: cyclicMap}, {name: "slice", extraParams: map[string]any{"cycle": cyclicSlice}}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { _, err := promptkit.NewEngine(promptkit.Config{PromptDir: frameworkPromptDir}, promptkit.WithProfiles(promptkit.Profile{ ID: "cyclic-extra-params", Endpoint: "http://cyclic/v1", Model: "cyclic-model", ExtraParams: tc.extraParams, }), ) if !errors.Is(err, promptkit.ErrInvalidConfig) { t.Fatalf("expected ErrInvalidConfig, got %v", err) } }) } } func TestRunStructuredOutputWorksWithSchemaFS(t *testing.T) { fake := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: `{"events":[]}`}} engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: t.TempDir(), ProfileDir: frameworkProfileDir, SchemaDir: t.TempDir(), }, promptkit.WithPromptFS(publicStructuredPromptFS("schema.fs.prompt", "events.schema.json"), "prompts"), promptkit.WithSchemaFS(publicSchemaFS(), "schemas"), promptkit.WithLLMClient(fake), ) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } result, err := engine.Run(context.Background(), promptkit.RunRequest{ PromptID: "schema.fs.prompt", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), }, }) if err != nil { t.Fatalf("expected run to succeed, got %v", err) } if result.Validation.Status != promptkit.ValidationPassed || !result.Validation.IsValid { t.Fatalf("expected schema validation to pass, got %+v", result.Validation) } if len(fake.requests) != 1 || fake.requests[0].StructuredOutput == nil { t.Fatalf("expected structured output request, got %+v", fake.requests) } } func TestRunStructuredOutputWorksWithSchemaFile(t *testing.T) { schemaDir := t.TempDir() schemaPath := filepath.Join(schemaDir, "events.schema.json") if err := os.WriteFile(schemaPath, []byte(publicSchemaJSON()), 0o644); err != nil { t.Fatal(err) } fake := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: `{"events":[]}`}} engine, err := promptkit.NewEngine(promptkit.Config{ ProfileDir: frameworkProfileDir, }, promptkit.WithPromptFS(publicStructuredPromptFS("schema.file.prompt", "events.schema.json"), "prompts"), promptkit.WithSchemaFile(schemaPath), promptkit.WithLLMClient(fake), ) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } result, err := engine.Run(context.Background(), promptkit.RunRequest{ PromptID: "schema.file.prompt", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), }, }) if err != nil { t.Fatalf("expected run to succeed, got %v", err) } if result.Validation.Status != promptkit.ValidationPassed || !result.Validation.IsValid { t.Fatalf("expected schema validation to pass, got %+v", result.Validation) } } func TestSourceOptionsRejectInvalidInputs(t *testing.T) { missingFile := filepath.Join(t.TempDir(), "missing.yaml") directoryPath := t.TempDir() tests := []struct { name string opt promptkit.Option }{ {name: "prompt fs nil", opt: promptkit.WithPromptFS(nil, "prompts")}, {name: "prompt fs empty root", opt: promptkit.WithPromptFS(fstest.MapFS{}, "")}, {name: "prompt file empty", opt: promptkit.WithPromptFile("")}, {name: "prompt file missing", opt: promptkit.WithPromptFile(missingFile)}, {name: "prompt file directory", opt: promptkit.WithPromptFile(directoryPath)}, {name: "profile fs nil", opt: promptkit.WithProfileFS(nil, "profiles")}, {name: "profile fs empty root", opt: promptkit.WithProfileFS(fstest.MapFS{}, "")}, {name: "profile file empty", opt: promptkit.WithProfileFile("")}, {name: "fallback profile fs nil", opt: promptkit.WithFallbackProfileFS(nil, "profiles")}, {name: "fallback profile fs empty root", opt: promptkit.WithFallbackProfileFS(fstest.MapFS{}, "")}, {name: "schema fs nil", opt: promptkit.WithSchemaFS(nil, "schemas")}, {name: "schema fs empty root", opt: promptkit.WithSchemaFS(fstest.MapFS{}, "")}, {name: "schema file empty", opt: promptkit.WithSchemaFile("")}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { _, err := promptkit.NewEngine(promptkit.Config{PromptDir: frameworkPromptDir}, tc.opt) if !errors.Is(err, promptkit.ErrInvalidConfig) { t.Fatalf("expected ErrInvalidConfig, got %v", err) } }) } } func TestPackageOptionsComposeFromSlice(t *testing.T) { fake := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}} options := []promptkit.Option{ nil, promptkit.WithProfiles(promptkit.Profile{ ID: "slice-profile", Endpoint: "http://slice/v1", Model: "slice-model", }), promptkit.WithLLMClient(fake), } engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, SchemaDir: frameworkSchemaDir, }, options...) if err != nil { t.Fatalf("expected package-provided options to compose, got %v", err) } _, err = engine.Run(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: "slice-profile", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, }) if err != nil { t.Fatalf("expected run with composed options to succeed, got %v", err) } if len(fake.requests) != 1 { t.Fatalf("expected one generate request, got %d", len(fake.requests)) } if fake.requests[0].Target.Model != "slice-model" { t.Fatalf("expected profile from composed options, got %q", fake.requests[0].Target.Model) } } func TestExtraParamsTypedNestedValuesAreCopiedAcrossPublicBoundary(t *testing.T) { fake := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}} engine := newContractEngineWithOptions(t, frameworkSchemaDir, promptkit.WithLLMClient(fake)) labels := map[string]string{"route": "primary"} counts := map[string]int{"retry_budget": 2} weights := []float64{0.25, 0.75} ids := []int{1, 2, 3} nested := map[string]any{ "labels": labels, "counts": counts, "weights": weights, "ids": ids, } extraParams := map[string]any{ "labels": labels, "counts": counts, "nested": nested, } _, err := engine.Run(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, Execution: &promptkit.ExecutionTargetOverride{ExtraParams: extraParams}, }) if err != nil { t.Fatalf("expected run to succeed, got %v", err) } if len(fake.requests) != 1 { t.Fatalf("expected one generate request, got %d", len(fake.requests)) } captured := fake.requests[0].Target.ExtraParams labels["route"] = "mutated" counts["retry_budget"] = 99 weights[0] = 9.9 ids[0] = 99 nested["added"] = "mutated" extraParams["new_top_level"] = "mutated" want := map[string]any{ "labels": map[string]string{"route": "primary"}, "counts": map[string]int{"retry_budget": 2}, "nested": map[string]any{ "labels": map[string]string{"route": "primary"}, "counts": map[string]int{"retry_budget": 2}, "weights": []float64{0.25, 0.75}, "ids": []int{1, 2, 3}, }, } if !reflect.DeepEqual(captured, want) { t.Fatalf("captured extra_params changed after mutating source:\ngot=%#v\nwant=%#v", captured, want) } } func TestRunRejectsInvalidExtraParams(t *testing.T) { cyclicMap := map[string]any{} cyclicMap["self"] = cyclicMap cyclicSlice := []any{nil} cyclicSlice[0] = cyclicSlice tests := []struct { name string extraParams map[string]any }{ {name: "function", extraParams: map[string]any{"bad": func() {}}}, {name: "channel", extraParams: map[string]any{"bad": make(chan struct{})}}, {name: "struct", extraParams: map[string]any{"bad": struct{ Name string }{Name: "bad"}}}, {name: "non string map key", extraParams: map[string]any{"bad": map[int]string{1: "one"}}}, {name: "nan", extraParams: map[string]any{"bad": math.NaN()}}, {name: "positive infinity", extraParams: map[string]any{"bad": math.Inf(1)}}, {name: "negative infinity", extraParams: map[string]any{"bad": math.Inf(-1)}}, {name: "cyclic map", extraParams: cyclicMap}, {name: "cyclic slice", extraParams: map[string]any{"cycle": cyclicSlice}}, {name: "malformed JSON number", extraParams: map[string]any{"value": json.Number("+1")}}, {name: "empty nested key", extraParams: map[string]any{"nested": map[string]any{"": true}}}, {name: "excessively deep value", extraParams: map[string]any{"nested": excessivelyDeepJSONValue()}}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { fake := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}} engine := newContractEngineWithOptions(t, frameworkSchemaDir, promptkit.WithLLMClient(fake)) _, err := engine.Run(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), }, Execution: &promptkit.ExecutionTargetOverride{ExtraParams: tc.extraParams}, }) if !errors.Is(err, promptkit.ErrInvalidRequest) { t.Fatalf("expected ErrInvalidRequest, got %v", err) } if len(fake.requests) != 0 { t.Fatalf("expected invalid request to fail before LLM call, got %d requests", len(fake.requests)) } }) } } func excessivelyDeepJSONValue() any { const clearlyUnsafeContainerDepth = 1_000 var value any = true for level := 0; level < clearlyUnsafeContainerDepth; level++ { if level%2 == 0 { value = map[string]any{"child": value} } else { value = []any{value} } } return value } func excessivelyLargeJSONValue() any { const clearlyUnsafeSharedOccurrences = 60_000 shared := []any{true} values := make([]any, clearlyUnsafeSharedOccurrences) for i := range values { values[i] = shared } return values } func TestWithLLMClientRejectsNilClient(t *testing.T) { _, err := promptkit.NewEngine(contractConfig(frameworkSchemaDir), promptkit.WithLLMClient(nil)) if !errors.Is(err, promptkit.ErrInvalidConfig) { t.Fatalf("expected ErrInvalidConfig, got %v", err) } } func TestNewEngineConstructsDefaultLLMClientWithoutCredentials(t *testing.T) { if _, err := promptkit.NewEngine(contractConfig(frameworkSchemaDir)); err != nil { t.Fatalf("expected default engine construction without credentials to succeed, got %v", err) } } func newContractEngine(t *testing.T) *promptkit.Engine { t.Helper() for _, path := range []string{ frameworkPromptDir, frameworkProfileDir, frameworkSchemaDir, } { if _, err := os.Stat(path); err != nil { t.Fatalf("expected framework contract path %s to exist: %v", path, err) } } engine, err := promptkit.NewEngine(contractConfig(frameworkSchemaDir)) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } return engine } func newContractEngineWithOptions(t *testing.T, schemaDir string, opts ...promptkit.Option) *promptkit.Engine { t.Helper() engine, err := promptkit.NewEngine(contractConfig(schemaDir), opts...) if err != nil { t.Fatalf("expected engine construction to succeed, got %v", err) } return engine } func newArtifactReaderEngine(t *testing.T, reader promptkit.ArtifactReader) *promptkit.Engine { t.Helper() promptDir := t.TempDir() writePublicPromptFile(t, promptDir, "artifact-reader", frameworkFastProfileID) engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: promptDir, ProfileDir: frameworkProfileDir, SchemaDir: frameworkSchemaDir, }, promptkit.WithArtifactReader(reader)) if err != nil { t.Fatalf("construct engine with artifact reader: %v", err) } return engine } func contractConfig(schemaDir string) promptkit.Config { return promptkit.Config{ PromptDir: frameworkPromptDir, ProfileDir: frameworkProfileDir, SchemaDir: schemaDir, } } func writePublicPromptFile(t *testing.T, dir, id, defaultProfile string) { t.Helper() data := `id: ` + id + ` version: "1.0.0" default_profile: ` + defaultProfile + ` inputs: - name: transcript required: true messages: - role: user content: "Summarize: {{input \"transcript\"}}" output: format: text validation_mode: none repair_attempts: 0 ` if err := os.WriteFile(filepath.Join(dir, id+".yaml"), []byte(data), 0o644); err != nil { t.Fatalf("failed to write prompt fixture: %v", err) } } func writePublicProfileFile(t *testing.T, dir, id, endpoint, model string) { t.Helper() data := `id: ` + id + ` endpoint: ` + endpoint + ` model: ` + model + ` ` if err := os.WriteFile(filepath.Join(dir, id+".yaml"), []byte(data), 0o644); err != nil { t.Fatalf("failed to write profile fixture: %v", err) } } func writePublicProfileFileWithAPIKeyEnv(t *testing.T, dir, id, endpoint, model, apiKeyEnv string) { t.Helper() data := `id: ` + id + ` endpoint: ` + endpoint + ` model: ` + model + ` api_key_env: ` + apiKeyEnv + ` ` if err := os.WriteFile(filepath.Join(dir, id+".yaml"), []byte(data), 0o644); err != nil { t.Fatalf("failed to write profile fixture: %v", err) } } func publicStructuredPromptFS(id string, schemaPath string) fstest.MapFS { return fstest.MapFS{ "prompts/prompt.yaml": &fstest.MapFile{Data: []byte(`id: ` + id + ` version: "1.0.0" default_profile: contract-fast inputs: - name: transcript required: true messages: - role: user content: "Extract events from {{input \"transcript\"}}." output: format: json validation_mode: json_schema schema_path: ` + schemaPath + ` repair_attempts: 0 `)}, } } func publicSchemaFS() fstest.MapFS { return fstest.MapFS{ "schemas/events.schema.json": &fstest.MapFile{Data: []byte(publicSchemaJSON())}, } } func publicSchemaJSON() string { return `{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["events"], "properties": { "events": {"type": "array"} } }` } type executionProfileFixture struct { id string endpoint string model string temperature float64 maxTokens int topP float64 timeoutSeconds int serviceTier string reasoningEffort string apiKeyEnv string extraParamSource string } func executionTargetFromProfileFixture(profile executionProfileFixture) promptkit.ExecutionTarget { return promptkit.ExecutionTarget{ Endpoint: profile.endpoint, Model: profile.model, Temperature: profile.temperature, MaxTokens: profile.maxTokens, TopP: profile.topP, TimeoutSeconds: profile.timeoutSeconds, ServiceTier: profile.serviceTier, ReasoningEffort: profile.reasoningEffort, APIKeyEnv: profile.apiKeyEnv, ExtraParams: map[string]any{"source": profile.extraParamSource}, } } func writeExecutionProfileFixture(t *testing.T, dir string, profile executionProfileFixture) { t.Helper() data := fmt.Sprintf(`id: %s endpoint: %s model: %s temperature: %g max_tokens: %d top_p: %g timeout_seconds: %d service_tier: %s reasoning_effort: %s api_key_env: %s extra_params: source: %q `, profile.id, profile.endpoint, profile.model, profile.temperature, profile.maxTokens, profile.topP, profile.timeoutSeconds, profile.serviceTier, profile.reasoningEffort, profile.apiKeyEnv, profile.extraParamSource, ) if err := os.WriteFile(filepath.Join(dir, profile.id+".yaml"), []byte(data), 0o644); err != nil { t.Fatalf("write execution profile fixture: %v", err) } } type fakeLLMClient struct { response *promptkit.GenerateResponse err error requests []promptkit.GenerateRequest } type recordingArtifactReader struct { artifact *promptkit.Artifact err error refs []promptkit.ArtifactRef read func(context.Context, promptkit.ArtifactRef) (*promptkit.Artifact, error) } func (r *recordingArtifactReader) Read(ctx context.Context, ref promptkit.ArtifactRef) (*promptkit.Artifact, error) { r.refs = append(r.refs, ref) if r.read != nil { return r.read(ctx, ref) } if r.err != nil { return nil, r.err } return r.artifact, nil } type roundTripFunc func(*http.Request) (*http.Response, error) func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } func (f *fakeLLMClient) Generate(_ context.Context, req promptkit.GenerateRequest) (*promptkit.GenerateResponse, error) { f.requests = append(f.requests, req) if f.err != nil { return nil, f.err } return f.response, nil }