Test typed sources through configuration composition

This commit is contained in:
2026-11-06 15:28:45 +00:00
parent 27966c1686
commit ea31e71280
2 changed files with 98 additions and 0 deletions

View File

@@ -494,6 +494,10 @@ the helper itself neither accesses files nor resolves inheritance.
## Stage 10: Verify Typed Sources Through Real Composition
**Completed:** Real engine composition tests cover inherited backend and prompt
records, layered completion, snapshots, explicit values, relative inherited file
content using ConfigKit's filesystem context, and deferred final validation.
**Scope:** Focused source-helper contract tests, not a second ConfigKit suite.
1. Extend existing backend-source tests with a real inherited child, preserving

View File

@@ -0,0 +1,94 @@
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)
}
}