diff --git a/bundled_composition_contract_test.go b/bundled_composition_contract_test.go new file mode 100644 index 0000000..85227d9 --- /dev/null +++ b/bundled_composition_contract_test.go @@ -0,0 +1,115 @@ +package promptkit_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "go.maximumdirect.net/configkit" + "go.maximumdirect.net/promptkit" +) + +func TestBundledCompositionReplacementAndInheritance(t *testing.T) { + underlay := configkit.Input{Source: configkit.Mapping("caller underlay", map[string]any{ + "profiles": []any{map[string]any{"id": "gpt-sol-latest", "model": "underlay-model"}}, + })} + underlayEngine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{underlay}}}}}) + if err != nil { + t.Fatal(err) + } + underlayProfile, err := underlayEngine.InspectProfile(context.Background(), "gpt-sol-latest") + if err != nil || underlayProfile.EffectiveModelParams.Model != "underlay-model" || underlayProfile.EffectiveModelParams.BackendID != "" { + t.Fatalf("underlay replacement = %+v, %v", underlayProfile, err) + } + main := configkit.Input{Source: configkit.Mapping("caller main", map[string]any{ + "backends": []any{map[string]any{"id": "openrouter", "endpoint": "https://local.invalid/v1"}, map[string]any{"id": "other", "endpoint": "https://other.invalid/v1"}}, + "profiles": []any{ + map[string]any{"id": "gpt-sol-latest", "model": "main-model"}, + map[string]any{"id": "custom", "extends": map[string]any{"id": "gpt-luna-latest"}, "model": "custom-model"}, + map[string]any{"id": "auto", "backend": "other", "model": "other-model"}, + }, + "collections": map[string]any{"openrouter": map[string]any{ + "backends": []any{map[string]any{"id": "local", "endpoint": "https://collection.invalid/v1"}}, + "profiles": []any{map[string]any{"id": "profile", "backend": "openrouter/local", "model": "collection-model"}}, + }}, + })} + engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{underlay}}}, Main: &main}}) + if err != nil { + t.Fatal(err) + } + inspect := func(id string) promptkit.ExecutionTarget { + t.Helper() + profile, err := engine.InspectProfile(context.Background(), id) + if err != nil { + t.Fatal(err) + } + return profile.EffectiveModelParams + } + if target := inspect("gpt-sol-latest"); target.Model != "main-model" || target.BackendID != "" { + t.Fatalf("replacement = %+v", target) + } + if target := inspect("custom"); target.Model != "custom-model" || target.BackendID != "openrouter" || target.Endpoint != "https://local.invalid/v1" { + t.Fatalf("inherited = %+v", target) + } + if target := inspect("gpt-luna-latest"); target.Endpoint != "https://local.invalid/v1" || target.APIKeyEnv != "" { + t.Fatalf("backend replacement = %+v", target) + } + if target := inspect("openrouter/profile"); target.BackendID != "openrouter/local" || target.Model != "collection-model" { + t.Fatalf("collection = %+v", target) + } + for _, tier := range []string{"low", "medium", "high", "xhigh", "max"} { + target := inspect("auto-" + tier) + if target.BackendID != "other" || target.Model != "other-model" || !strings.Contains(fmt.Sprint(target.ExtraParams), "cost_tier:"+tier) || !strings.Contains(fmt.Sprint(target.ExtraParams), "id:auto-router") { + t.Errorf("tier %s = %+v", tier, target) + } + } +} + +func TestBundledCompositionSequenceModes(t *testing.T) { + for _, test := range []struct { + name, mode, id string + wantErr bool + count int + }{ + {"duplicate append", "append", "auto", true, 0}, + {"distinct append", "append", "custom", false, 18}, + {"replace", "replace", "custom", false, 1}, + } { + t.Run(test.name, func(t *testing.T) { + input := configkit.Input{Source: configkit.Mapping("sequence", map[string]any{ + "config": map[string]any{"sequences": []any{map[string]any{"destination": []any{"profiles"}, "mode": test.mode}}}, + "profiles": []any{map[string]any{"id": test.id, "model": "custom-model"}}, + })} + engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}}) + if test.wantErr { + if err == nil { + t.Fatal("expected duplicate rejection") + } + return + } + if err != nil { + t.Fatal(err) + } + profiles, err := engine.ListProfiles(context.Background()) + if err != nil || len(profiles) != test.count { + t.Fatalf("profiles = %v, %v", profiles, err) + } + }) + } +} + +func TestBundledCompositionRejectsStrictConflictsAndMissingBackends(t *testing.T) { + profile := configkit.Input{Source: configkit.Mapping("profile", map[string]any{"profiles": []any{map[string]any{"id": "auto", "model": "caller"}}})} + _, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{profile, profile}}}}}) + if err == nil { + t.Fatal("strict group accepted duplicate records") + } + input := configkit.Input{Source: configkit.Mapping("replace backends", map[string]any{ + "config": map[string]any{"sequences": []any{map[string]any{"destination": []any{"backends"}, "mode": "replace"}}}, + "backends": []any{map[string]any{"id": "other", "endpoint": "https://other.invalid/v1"}}, + })} + if _, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}}); err == nil { + t.Fatal("missing bundled backend was accepted") + } +} diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 00672e4..adfe228 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -183,6 +183,10 @@ Validation: targeted new composition tests plus existing source, inheritance, namespace, and profile-completion tests. Avoid reproducing ConfigKit's generic sequence implementation suite. +Completion: facade integration tests cover caller precedence, whole-record +replacement, inheritance, scoped resources, sequence modes, strict conflicts, +and missing backend rejection. Targeted tests pass on Go 1.27.0. + ## Stage 4: Verify Preparation And HTTP Request Construction Scope: bundled-profile execution integration with existing runner/client paths.