package promptkit_test import ( "context" "errors" "testing" "testing/fstest" "go.maximumdirect.net/configkit" "go.maximumdirect.net/promptkit" ) func TestTypedSourcesComposeAndCaptureValues(t *testing.T) { endpoint, childEndpoint, base := "https://base.test/v1", "https://child.test/v1", "base" limit, queue := 2, 0 backendSource := promptkit.BackendSource("backends", []promptkit.BackendConfig{{ID: "base", Endpoint: &endpoint, ConcurrencyLimit: &limit}, {ID: "child", Extends: &base, Endpoint: &childEndpoint, QueueCapacity: &queue}}) role, content, secondContent, empty, cacheType := "user", "hello {{.name}}", "second", "", "ephemeral" format, validation, description, profileID := "text", "none", "description", "local" required, repairs := false, 0 prompts := []promptkit.PromptConfig{ {ID: "base", DefaultProfile: &profileID, Description: &description, Inputs: []promptkit.PromptInputConfig{{Name: "input", Required: &required, Description: &empty}}, Messages: []promptkit.PromptMessageConfig{{Role: &role, Content: &content, CacheControl: &promptkit.PromptCacheControlConfig{Type: &cacheType, TTL: &empty}}, {Role: &role, Content: &secondContent}}, Output: &promptkit.PromptOutputConfig{Format: &format, ValidationMode: &validation, RepairAttempts: &repairs}}, {ID: "child", Extends: &base}, {ID: "layered"}, {ID: "empty-inputs", Inputs: []promptkit.PromptInputConfig{}, Messages: []promptkit.PromptMessageConfig{{Role: &role, Content: &content}}, Output: &promptkit.PromptOutputConfig{Format: &format, ValidationMode: &validation}}, {ID: "omitted-inputs", Messages: []promptkit.PromptMessageConfig{{Role: &role, Content: &content}}, Output: &promptkit.PromptOutputConfig{Format: &format, ValidationMode: &validation}}, } promptSource := promptkit.PromptSource("typed prompts", prompts) endpoint, childEndpoint, role, content, description = "changed", "changed", "changed", "changed", "changed" prompts[0].Inputs[0].Name = "changed" prompts[0].Messages[1].Content = &empty prompts[1].ID = "changed" profileEndpoint, model := "https://profile.test/v1", "model" main := configkit.Input{Source: configkit.Mapping("completion", map[string]any{"prompts": []any{map[string]any{"id": "layered", "messages": []any{map[string]any{"role": "user", "content": "layer"}}, "output": map[string]any{"format": "text", "validation_mode": "none"}}}})} profileSource := promptkit.ProfileSource("profiles", []promptkit.ProfileConfig{{ID: "local", Endpoint: &profileEndpoint, Model: &model}}) underlays := []configkit.Underlay{{Inputs: []configkit.Input{{Source: backendSource}, {Source: promptSource}, {Source: profileSource}}}} engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: underlays, Main: &main}}, promptkit.WithBundledOpenRouter(false)) if err != nil { t.Fatal(err) } ctx := context.Background() backend, err := engine.InspectBackend(ctx, "child") if err != nil || backend.BackendID != "child" || backend.Endpoint != "https://child.test/v1" || backend.ConcurrencyLimit == nil || *backend.ConcurrencyLimit != 2 || backend.QueueCapacity == nil || *backend.QueueCapacity != 0 { t.Fatalf("backend = %+v, %v", backend, err) } prompt, err := engine.InspectPrompt(ctx, "child") if err != nil { t.Fatal(err) } if prompt.DefaultProfileID == nil || *prompt.DefaultProfileID != "local" || prompt.Description == nil || *prompt.Description != "description" || prompt.Inputs == nil || len(*prompt.Inputs) != 1 || (*prompt.Inputs)[0].Name != "input" || (*prompt.Inputs)[0].Required == nil || *(*prompt.Inputs)[0].Required || (*prompt.Inputs)[0].Description == nil || *(*prompt.Inputs)[0].Description != "" || len(prompt.Messages) != 2 || prompt.Messages[0].Content != "hello {{.name}}" || prompt.Messages[1].Content != "second" || prompt.Messages[0].CacheControl.TTL == nil || *prompt.Messages[0].CacheControl.TTL != "" || prompt.OutputContract.RepairAttempts == nil || *prompt.OutputContract.RepairAttempts != 0 { t.Fatalf("prompt = %+v", prompt) } prepared, err := engine.Prepare(ctx, promptkit.RunRequest{PromptID: "child", Vars: map[string]string{"name": "reader"}}) if err != nil || prepared.Messages[0].Content != "hello reader" { t.Fatalf("preparation = %+v, %v", prepared, err) } for _, id := range []string{"layered", "empty-inputs", "omitted-inputs"} { result, err := engine.InspectPrompt(ctx, id) if err != nil { t.Fatal(err) } if id == "empty-inputs" && (result.Inputs == nil || len(*result.Inputs) != 0) { t.Fatalf("empty inputs = %+v", result.Inputs) } if id == "omitted-inputs" && result.Inputs != nil { t.Fatalf("omitted inputs = %+v", result.Inputs) } } } func TestTypedPromptSourceUsesExplicitFilesystemContext(t *testing.T) { role, path, format, validation, parent := "user", "body.md", "text", "none", "base" source := promptkit.PromptSource("file prompts", []promptkit.PromptConfig{{ID: "base", Messages: []promptkit.PromptMessageConfig{{Role: &role, ContentFile: &path}}, Output: &promptkit.PromptOutputConfig{Format: &format, ValidationMode: &validation}}, {ID: "child", Extends: &parent}}) files := fstest.MapFS{"templates/body.md": {Data: []byte("captured {{.value}}")}} filesystem := configkit.NewFilesystem("fixture", files) input := configkit.Input{Source: source, Filesystem: &configkit.FilesystemContext{FS: &filesystem, Root: ".", BaseDir: "templates"}} engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}}) if err != nil { t.Fatal(err) } files["templates/body.md"].Data = []byte("changed") prompt, err := engine.InspectPrompt(context.Background(), "child") if err != nil || prompt.Messages[0].Content != "captured {{.value}}" { t.Fatalf("file prompt = %+v, %v", prompt, err) } } func TestTypedPromptSourceDefersValidation(t *testing.T) { role, body, path, format, validation := "user", "body", "also-file", "text", "none" source := promptkit.PromptSource("invalid", []promptkit.PromptConfig{{ID: "bad", Messages: []promptkit.PromptMessageConfig{{Role: &role, Content: &body, ContentFile: &path}}, Output: &promptkit.PromptOutputConfig{Format: &format, ValidationMode: &validation}}}) _, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{Source: source}}}) if !errors.Is(err, promptkit.ErrInvalidConfig) { t.Fatalf("invalid final source: %v", err) } }