102 lines
5.2 KiB
Go
102 lines
5.2 KiB
Go
|
|
package promptkit_test
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"strings"
|
||
|
|
"sync"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"go.maximumdirect.net/configkit"
|
||
|
|
"go.maximumdirect.net/promptkit"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestResourceInspectionIsolationAndNullInheritance(t *testing.T) {
|
||
|
|
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "unexpected"}}
|
||
|
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{Source: configkit.Mapping("resources", map[string]any{
|
||
|
|
"backends": []any{map[string]any{"id": "local", "endpoint": "https://backend.test/v1", "api_key_env": "UNSET_INSPECTION_KEY", "extra_params": map[string]any{"nested": map[string]any{"array": []any{"captured"}}}, "smoke_tests": map[string]any{"default_profile": "missing", "reachability": map[string]any{}}}},
|
||
|
|
"profiles": []any{map[string]any{"id": "parent", "backend": "local", "endpoint": "https://parent.test/v1", "extra_params": map[string]any{"parent": true}, "model": "model"}, map[string]any{"id": "child", "extends": map[string]any{"id": "parent"}, "endpoint": nil, "extra_params": nil, "api_key_env": nil}, map[string]any{"id": "partial"}},
|
||
|
|
"prompts": []any{map[string]any{"id": "reference", "default_profile": "missing", "messages": []any{map[string]any{"role": "user", "content": "{{.missing}}"}}, "output": map[string]any{"format": "json", "validation_mode": "json_schema", "schema_path": "missing.json"}}, map[string]any{"id": "ready", "inputs": []any{map[string]any{"name": "x", "required": false}}, "messages": []any{map[string]any{"role": "user", "content": "hello", "cache_control": map[string]any{"type": "ephemeral", "ttl": ""}}}, "output": map[string]any{"format": "text", "validation_mode": "none"}}},
|
||
|
|
})}}}, promptkit.WithLLMClient(client), promptkit.WithBundledOpenRouter(false))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
ctx := context.Background()
|
||
|
|
if got, err := engine.InspectPrompt(ctx, "reference"); err != nil || got.DefaultProfileID == nil || *got.DefaultProfileID != "missing" || got.OutputContract.SchemaPath == nil || *got.OutputContract.SchemaPath != "missing.json" || got.Messages[0].Content != "{{.missing}}" {
|
||
|
|
t.Fatalf("reference = %+v, %v", got, err)
|
||
|
|
}
|
||
|
|
if got, err := engine.InspectProfile(ctx, "partial"); err != nil || got.Endpoint != nil || got.Model != nil {
|
||
|
|
t.Fatalf("partial = %+v, %v", got, err)
|
||
|
|
}
|
||
|
|
profile, err := engine.InspectProfile(ctx, "child")
|
||
|
|
if err != nil || profile.Endpoint != nil || profile.ExtraParams != nil || profile.APIKeyEnv != nil {
|
||
|
|
t.Fatalf("child = %+v, %v", profile, err)
|
||
|
|
}
|
||
|
|
view, err := engine.InspectConfiguration()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
for _, field := range []string{"endpoint", "extra_params", "api_key_env"} {
|
||
|
|
path := configkit.Keys("profiles").Index(1).Key(field)
|
||
|
|
explanation, ok := view.Explain(path)
|
||
|
|
if !ok || !strings.Contains(explanation.Effective.YAML, "null") || len(explanation.History) == 0 {
|
||
|
|
t.Errorf("%s provenance = %+v, %t", field, explanation, ok)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
backend, err := engine.InspectBackend(ctx, "local")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if backend.SmokeTests == nil || backend.SmokeTests.DefaultProfile == nil || *backend.SmokeTests.DefaultProfile != "missing" {
|
||
|
|
t.Fatalf("diagnostics = %+v", backend)
|
||
|
|
}
|
||
|
|
*backend.APIKeyEnv = "MUTATED"
|
||
|
|
*backend.SmokeTests.DefaultProfile = "mutated"
|
||
|
|
(*backend.ExtraParams)["nested"].(map[string]any)["array"].([]any)[0] = "mutated"
|
||
|
|
*profile.Model = "mutated"
|
||
|
|
prompt, err := engine.InspectPrompt(ctx, "ready")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
(*prompt.Inputs)[0].Name = "changed"
|
||
|
|
*(*prompt.Inputs)[0].Required = true
|
||
|
|
prompt.Messages[0].Content = "changed"
|
||
|
|
*prompt.Messages[0].CacheControl.TTL = "changed"
|
||
|
|
again, err := engine.InspectBackend(ctx, "local")
|
||
|
|
if err != nil || *again.APIKeyEnv != "UNSET_INSPECTION_KEY" || *again.SmokeTests.DefaultProfile != "missing" || (*again.ExtraParams)["nested"].(map[string]any)["array"].([]any)[0] != "captured" {
|
||
|
|
t.Fatalf("backend mutation leaked: %+v, %v", again, err)
|
||
|
|
}
|
||
|
|
againPrompt, err := engine.InspectPrompt(ctx, "ready")
|
||
|
|
if err != nil || (*againPrompt.Inputs)[0].Name != "x" || *(*againPrompt.Inputs)[0].Required || againPrompt.Messages[0].Content != "hello" || *againPrompt.Messages[0].CacheControl.TTL != "" {
|
||
|
|
t.Fatalf("prompt mutation leaked: %+v, %v", againPrompt, err)
|
||
|
|
}
|
||
|
|
prepared, err := engine.Prepare(ctx, promptkit.RunRequest{PromptID: "ready", ProfileID: "child"})
|
||
|
|
if err != nil || prepared.EffectiveModelParams.Endpoint != "https://backend.test/v1" || prepared.EffectiveModelParams.APIKeyEnv != "UNSET_INSPECTION_KEY" || prepared.EffectiveModelParams.Model != "model" || prepared.EffectiveModelParams.ExtraParams["nested"].(map[string]any)["array"].([]any)[0] != "captured" {
|
||
|
|
t.Fatalf("prepared = %+v, %v", prepared, err)
|
||
|
|
}
|
||
|
|
if len(client.requests) != 0 {
|
||
|
|
t.Fatal("inspection invoked provider")
|
||
|
|
}
|
||
|
|
var wg sync.WaitGroup
|
||
|
|
for i := 0; i < 12; i++ {
|
||
|
|
wg.Add(1)
|
||
|
|
go func() {
|
||
|
|
defer wg.Done()
|
||
|
|
for j := 0; j < 20; j++ {
|
||
|
|
b, err := engine.InspectBackend(ctx, "local")
|
||
|
|
if err != nil {
|
||
|
|
t.Error(err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
(*b.ExtraParams)["nested"].(map[string]any)["array"].([]any)[0] = "local"
|
||
|
|
p, err := engine.InspectPrompt(ctx, "ready")
|
||
|
|
if err != nil {
|
||
|
|
t.Error(err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
p.Messages[0].Content = "local"
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
wg.Wait()
|
||
|
|
}
|