diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 2375da9..3c22d6d 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -647,6 +647,13 @@ configuration fixtures not yet converted. Validation: real-loader tests assert effective behavior and structured diagnostic identity/paths as appropriate, not complete upstream error wording or ordering. +Completion: loader fixtures now exercise identified lists, inherited records, +strict duplicates, invalid identities and parent references, all six registered +destinations, final identity enforcement, layered upsert/append/replace, and +request completion of partial profiles. The focused real-loader composition, +target, validation, and syntax tests passed on Go 1.27.0. Header and remaining +adapter fixtures are handled in the next assignment. + ## Stage 14: Complete Header, Origin, And Adapter Fixture Coverage Scope: remaining `internal/configuration` prompt, inspection, listing, and diff --git a/internal/configuration/load_test.go b/internal/configuration/load_test.go index a546b17..fb64934 100644 --- a/internal/configuration/load_test.go +++ b/internal/configuration/load_test.go @@ -1,6 +1,7 @@ package configuration import ( + "context" "errors" "strings" "testing" @@ -33,6 +34,265 @@ func TestLoadIdentifiedResourceLists(t *testing.T) { } } +func TestIdentifiedResourcesComposeAcrossNamespacesAndKinds(t *testing.T) { + resources := func() map[string]any { + return map[string]any{ + "backends": []any{ + map[string]any{"id": "shared", "endpoint": "https://example.invalid/v1"}, + map[string]any{"id": "child", "extends": map[string]any{"id": "shared"}}, + }, + "profiles": []any{ + map[string]any{"id": "shared", "model": "model"}, + map[string]any{"id": "child", "extends": map[string]any{"id": "shared"}}, + }, + "prompts": []any{ + map[string]any{"id": "shared", "messages": []any{map[string]any{"role": "user", "content": "inherited"}}, + "output": map[string]any{"format": "text", "validation_mode": "basic"}}, + map[string]any{"id": "child", "extends": map[string]any{"id": "shared"}}, + }, + } + } + values := resources() + values["collections"] = map[string]any{"*": resources(), "123": resources()} + state, err := load(mapping("identified namespaces", values), nil) + if err != nil { + t.Fatal(err) + } + for _, prefix := range []string{"", "*/", "123/"} { + backend, err := state.registry.GetBackend(prefix + "child") + if err != nil || backend.ID != prefix+"child" || backend.Endpoint != "https://example.invalid/v1" { + t.Fatalf("inherited backend %q = %+v, %v", prefix, backend, err) + } + profile, err := state.GetProfileIdentity(context.Background(), prefix+"child") + if err != nil || profile != prefix+"child" { + t.Fatalf("inherited profile %q = %q, %v", prefix, profile, err) + } + definition, _, err := state.prompt(prefix + "child") + if err != nil || definition.ID != prefix+"child" || len(definition.Templates) != 1 || definition.Templates[0].Content != "inherited" { + t.Fatalf("inherited prompt %q = %+v, %v", prefix, definition, err) + } + } +} + +func TestIdentifiedResourcesRejectInvalidInheritance(t *testing.T) { + for _, kind := range []string{"backends", "profiles", "prompts"} { + for name, records := range map[string][]any{ + "missing": {map[string]any{"id": "child", "extends": map[string]any{"id": "missing"}}}, + "self": {map[string]any{"id": "child", "extends": map[string]any{"id": "child"}}}, + "cycle": { + map[string]any{"id": "first", "extends": map[string]any{"id": "second"}}, + map[string]any{"id": "second", "extends": map[string]any{"id": "first"}}, + }, + } { + t.Run(kind+"/"+name, func(t *testing.T) { + _, err := load(mapping("invalid inheritance", map[string]any{kind: records}), nil) + var loadErr *configkit.LoadError + if !errors.As(err, &loadErr) { + t.Fatalf("load error = %v, want an upstream inheritance diagnostic", err) + } + for _, diagnostic := range loadErr.Diagnostics() { + if diagnostic.Code == configkit.CodeInheritanceMissing || diagnostic.Code == configkit.CodeInheritanceCycle { + return + } + } + t.Fatalf("missing inheritance diagnostic: %+v", loadErr.Diagnostics()) + }) + } + } +} + +func TestIdentifiedResourcesRejectInvalidLocalIDs(t *testing.T) { + for _, kind := range []string{"backends", "profiles", "prompts"} { + for _, test := range []struct { + name string + record map[string]any + }{ + {name: "missing", record: map[string]any{}}, + {name: "null", record: map[string]any{"id": nil}}, + {name: "non-string", record: map[string]any{"id": 42}}, + {name: "blank", record: map[string]any{"id": " "}}, + {name: "slash", record: map[string]any{"id": "not/local"}}, + } { + t.Run(kind+"/"+test.name, func(t *testing.T) { + _, err := load(mapping("invalid resource ID", map[string]any{kind: []any{test.record}}), nil) + var loadErr *configkit.LoadError + if !errors.As(err, &loadErr) || len(loadErr.Diagnostics()) == 0 { + t.Fatalf("load error = %v, want a path-aware error", err) + } + }) + } + } +} + +func TestIdentifiedResourcesRejectDuplicateStrictPeers(t *testing.T) { + for _, kind := range []string{"backends", "profiles", "prompts"} { + for _, namespace := range []string{"", "*"} { + t.Run(kind+"/"+namespace, func(t *testing.T) { + values := func() map[string]any { + records := []any{map[string]any{"id": "duplicate"}} + if namespace == "" { + return map[string]any{kind: records} + } + return map[string]any{"collections": map[string]any{namespace: map[string]any{kind: records}}} + } + _, err := load(configkit.Input{Source: configkit.NoSource()}, []configkit.Underlay{{Inputs: []configkit.Input{ + mapping("first", values()), mapping("second", values()), + }}}) + var loadErr *configkit.LoadError + if !errors.As(err, &loadErr) { + t.Fatalf("load error = %v, want an upstream duplicate diagnostic", err) + } + for _, diagnostic := range loadErr.Diagnostics() { + if diagnostic.Code == configkit.CodeComposeConflict { + return + } + } + t.Fatalf("missing duplicate conflict: %+v", loadErr.Diagnostics()) + }) + } + } +} + +func TestFinalIdentitiesAreCheckedAfterApplicationResolution(t *testing.T) { + for _, kind := range []string{"backends", "profiles", "prompts"} { + for _, namespace := range []string{"", "*"} { + t.Run(kind+"/"+namespace, func(t *testing.T) { + definition := configurationDefinition(&loaded{}) + definition.Resolve = func(config *document, resolution *configkit.Resolution) error { + path := configkit.Keys(kind) + if namespace == "" { + switch kind { + case "backends": + config.Backends = []backendConfig{{ID: "same"}, {ID: "same"}} + case "profiles": + config.Profiles = []profileConfig{{ID: "same"}, {ID: "same"}} + case "prompts": + config.Prompts = []promptConfig{{ID: "same"}, {ID: "same"}} + } + } else { + collection := collectionConfig{} + switch kind { + case "backends": + collection.Backends = []backendConfig{{ID: "same"}, {ID: "same"}} + case "profiles": + collection.Profiles = []profileConfig{{ID: "same"}, {ID: "same"}} + case "prompts": + collection.Prompts = []promptConfig{{ID: "same"}, {ID: "same"}} + } + config.Collections = map[string]collectionConfig{namespace: collection} + path = configkit.Keys("collections", namespace, kind) + } + resolution.RecordNormalization(path, "supply resolved records") + return nil + } + input := configkit.Input{Source: configkit.NoSource()} + if namespace != "" { + input = mapping("existing collection", map[string]any{"collections": map[string]any{ + namespace: map[string]any{kind: []any{}}, + }}) + } + _, err := configkit.LoadLayered(input, configkit.LayeredOptions[document]{Definition: definition}) + var loadErr *configkit.LoadError + if !errors.As(err, &loadErr) { + t.Fatalf("load error = %v, want a final identity diagnostic", err) + } + for _, diagnostic := range loadErr.Diagnostics() { + if diagnostic.Code == configkit.CodeSequenceConflict { + return + } + } + t.Fatalf("missing final identity conflict: %+v", loadErr.Diagnostics()) + }) + } + } +} + +func TestIdentifiedResourceLayerOperations(t *testing.T) { + base := mapping("base profiles", map[string]any{"profiles": []any{ + map[string]any{"id": "first", "model": "original", "service_tier": "standard"}, + map[string]any{"id": "second", "model": "retained"}, + }}) + for _, test := range []struct { + name string + mode string + records []any + models map[string]string + }{ + {name: "whole record upsert", records: []any{map[string]any{"id": "first", "model": "updated"}}, models: map[string]string{"first": "updated", "second": "retained"}}, + {name: "empty upsert", records: []any{}, models: map[string]string{"first": "original", "second": "retained"}}, + {name: "append", mode: "append", records: []any{map[string]any{"id": "third", "model": "added"}}, models: map[string]string{"first": "original", "second": "retained", "third": "added"}}, + {name: "replace", mode: "replace", records: []any{map[string]any{"id": "third", "model": "replacement"}}, models: map[string]string{"third": "replacement"}}, + {name: "clear", mode: "replace", records: []any{}, models: map[string]string{}}, + } { + t.Run(test.name, func(t *testing.T) { + values := map[string]any{"profiles": test.records} + if test.mode != "" { + values["config"] = map[string]any{"sequences": []any{map[string]any{ + "destination": []any{"profiles"}, "mode": test.mode, + }}} + } + state, err := load(mapping("overlay profiles", values), []configkit.Underlay{{Inputs: []configkit.Input{base}}}) + if err != nil { + t.Fatal(err) + } + profiles := indexedProfiles(&state.result.Config) + if len(profiles) != len(test.models) { + t.Fatalf("profiles = %+v, want IDs and models %+v", profiles, test.models) + } + for id, model := range test.models { + profile, ok := profiles[id] + if !ok || profile.Model == nil || *profile.Model != model { + t.Fatalf("profile %q = %+v, want model %q", id, profile, model) + } + } + if test.name == "whole record upsert" && profiles["first"].ServiceTier != nil { + t.Fatalf("upsert retained a displaced field: %+v", profiles["first"]) + } + }) + } +} + +func TestIdentifiedResourceLayersRejectDisplacedParentAndDuplicateAppend(t *testing.T) { + base := mapping("base", map[string]any{"profiles": []any{ + map[string]any{"id": "shared", "model": "original"}, + }}) + for _, test := range []struct { + name string + values map[string]any + code configkit.Code + }{ + { + name: "self extension cannot recover displaced record", + values: map[string]any{"profiles": []any{ + map[string]any{"id": "shared", "extends": map[string]any{"id": "shared"}}, + }}, + code: configkit.CodeInheritanceCycle, + }, + { + name: "append cannot duplicate an identity", + values: map[string]any{ + "config": map[string]any{"sequences": []any{map[string]any{"destination": []any{"profiles"}, "mode": "append"}}}, + "profiles": []any{map[string]any{"id": "shared", "model": "other"}}, + }, + code: configkit.CodeSequenceConflict, + }, + } { + t.Run(test.name, func(t *testing.T) { + _, err := load(mapping("overlay", test.values), []configkit.Underlay{{Inputs: []configkit.Input{base}}}) + var loadErr *configkit.LoadError + if !errors.As(err, &loadErr) { + t.Fatalf("load error = %v, want structured diagnostic", err) + } + for _, diagnostic := range loadErr.Diagnostics() { + if diagnostic.Code == test.code { + return + } + } + t.Fatalf("missing %s diagnostic: %+v", test.code, loadErr.Diagnostics()) + }) + } +} + func TestLoadChecksOptionalConfigurationVersion(t *testing.T) { for _, test := range []struct { name string @@ -97,7 +357,7 @@ func TestLoadConsumesAcceptedUnderlayVersionAndValidatesHeaderlessPayload(t *tes } invalidFragment := mapping("headerless prompt fragment", map[string]any{ - "prompts": map[string]any{"incomplete": map[string]any{"description": "still validated"}}, + "prompts": []any{map[string]any{"id": "incomplete", "description": "still validated"}}, }) if _, err := load(configkit.Input{Source: configkit.NoSource()}, []configkit.Underlay{{Inputs: []configkit.Input{invalidFragment}}}); err == nil || !strings.Contains(err.Error(), "at least one message is required") { t.Fatalf("headerless fragment validation error = %v, want final prompt validation", err) @@ -106,30 +366,25 @@ func TestLoadConsumesAcceptedUnderlayVersionAndValidatesHeaderlessPayload(t *tes func TestLoadComposesLiteralKeysAndEffectivePresence(t *testing.T) { base := mapping("defaults", map[string]any{ - "version": int64(1), - "backends": map[string]any{ - "local.v1": map[string]any{"endpoint": "http://localhost:8080/v1", "concurrency_limit": 2}, - }, + "version": int64(2), + "backends": []any{map[string]any{"id": "local.v1", "endpoint": "http://localhost:8080/v1", "concurrency_limit": 2}}, "collections": map[string]any{"base": map[string]any{ "release_version": "", - "backends": map[string]any{ - "local.v1": map[string]any{"endpoint": "http://localhost:8080/v1", "concurrency_limit": 3}, - }, - "profiles": map[string]any{ - "one": map[string]any{"backend": "local.v1", "temperature": 0.8, "top_p": 0.9, "api_key_required": true, + "backends": []any{map[string]any{"id": "local.v1", "endpoint": "http://localhost:8080/v1", "concurrency_limit": 3}}, + "profiles": []any{ + map[string]any{"id": "one", "backend": "local.v1", "temperature": 0.8, "top_p": 0.9, "api_key_required": true, "extra_params": map[string]any{"provider": map[string]any{"first": 1, "second": 2}}}, - "child.v1": map[string]any{"extends": "one"}, - "namespaced": map[string]any{"backend": "base/local.v1", "model": "collection-model"}, + map[string]any{"id": "child.v1", "extends": map[string]any{"id": "one"}}, + map[string]any{"id": "namespaced", "backend": "base/local.v1", "model": "collection-model"}, }, }, "invoice": map[string]any{ "release_version": "r7", - "prompts": map[string]any{ - "summary": map[string]any{ - "version": "v1.0", + "prompts": []any{ + map[string]any{"id": "summary", "messages": []any{map[string]any{"role": "user", "content": "Summarize."}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, }, - "version": map[string]any{ + map[string]any{"id": "version", "messages": []any{map[string]any{"role": "user", "content": "Nested ID."}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, }, @@ -137,19 +392,23 @@ func TestLoadComposesLiteralKeysAndEffectivePresence(t *testing.T) { }}, }) overlay := mapping("operator", map[string]any{ - "backends": map[string]any{"local.v1": map[string]any{"queue_capacity": 0}}, - "collections": map[string]any{"base": map[string]any{"profiles": map[string]any{"child.v1": map[string]any{ + "backends": []any{map[string]any{"id": "local.v1", "endpoint": "http://localhost:8080/v1", "concurrency_limit": 2, "queue_capacity": 0}}, + "collections": map[string]any{"base": map[string]any{"profiles": []any{map[string]any{"id": "child.v1", "extends": map[string]any{"id": "one"}, "temperature": 0, "top_p": nil, "api_key_required": false, "extra_params": map[string]any{"provider": map[string]any{"second": 3}}, }}}}, }) - loaded, err := load(mapping("main", map[string]any{"version": "1"}), []configkit.Underlay{ + loaded, err := load(mapping("main", map[string]any{"version": "2"}), []configkit.Underlay{ {Inputs: []configkit.Input{base}}, {Inputs: []configkit.Input{overlay}}, }) if err != nil { + var loadErr *configkit.LoadError + if errors.As(err, &loadErr) { + t.Fatalf("load: %v; diagnostics: %+v", err, loadErr.Diagnostics()) + } t.Fatal(err) } - profile := loaded.result.Config.Collections["base"].Profiles["child.v1"] + profile := loaded.result.Config.Collections["base"].Profiles[1] if profile.Temperature == nil || *profile.Temperature != 0 || profile.TopP != nil || profile.APIKeyRequired == nil || *profile.APIKeyRequired { t.Fatalf("effective values were not retained: %+v", profile) } @@ -160,7 +419,7 @@ func TestLoadComposesLiteralKeysAndEffectivePresence(t *testing.T) { if provider["first"] != int64(1) || provider["second"] != int64(3) { t.Fatalf("recursive overlay failed: %+v", provider) } - backend := loaded.result.Config.Backends["local.v1"] + backend := loaded.result.Config.Backends[0] if backend.QueueCapacity == nil || *backend.QueueCapacity != 0 || loaded.backends["local.v1"]["queue_capacity"] != fieldValue { t.Fatalf("explicit zero queue capacity was lost: %+v", backend) } @@ -171,10 +430,10 @@ func TestLoadComposesLiteralKeysAndEffectivePresence(t *testing.T) { if !exists || collectionProfile.ID != "base/namespaced" || collectionProfile.ResolvedTarget == nil || collectionProfile.ResolvedTarget.BackendID != "base/local.v1" { t.Fatalf("collection profile snapshot = %+v, exists %t", collectionProfile, exists) } - if loaded.result.Config.Collections["invoice"].Prompts["summary"].Messages[0].Content == nil { + if loaded.result.Config.Collections["invoice"].Prompts[0].Messages[0].Content == nil { t.Fatal("literal prompt content was not decoded") } - if loaded.result.Config.Version == nil || *loaded.result.Config.Version != "1" || + if loaded.result.Config.Version == nil || *loaded.result.Config.Version != "2" || loaded.result.Config.Collections["base"].ReleaseVersion == nil || *loaded.result.Config.Collections["base"].ReleaseVersion != "" || loaded.result.Config.Collections["invoice"].ReleaseVersion == nil || *loaded.result.Config.Collections["invoice"].ReleaseVersion != "r7" { t.Fatalf("root or collection version metadata was not retained: %+v", loaded.result.Config) @@ -197,20 +456,20 @@ func TestLoadRejectsInvalidCollectionAndResourceKeys(t *testing.T) { }, { name: "root resource with separator", - values: map[string]any{"backends": map[string]any{ - "bad/name": map[string]any{"endpoint": "https://example.test/v1"}, + values: map[string]any{"backends": []any{ + map[string]any{"id": "bad/name", "endpoint": "https://example.test/v1"}, }}, - message: "backend key must be a nonblank local key without a slash", + message: "backend ID must be a nonblank local key without a slash", }, { name: "collection resource with separator", values: map[string]any{"collections": map[string]any{"good": map[string]any{ - "prompts": map[string]any{"bad/name": map[string]any{ + "prompts": []any{map[string]any{"id": "bad/name", "messages": []any{map[string]any{"role": "user", "content": "Hello."}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, }}, }}}, - message: "prompt key must be a nonblank local key without a slash", + message: "prompt ID must be a nonblank local key without a slash", }, } { t.Run(test.name, func(t *testing.T) { @@ -267,17 +526,17 @@ func TestLoadBackendSmokeTestsOmissionAndNull(t *testing.T) { {name: "null", smokeTests: nil, include: true, inherited: true}, } { t.Run(test.name, func(t *testing.T) { - backend := map[string]any{"endpoint": "https://backend.example/v1"} + backend := map[string]any{"id": "local", "endpoint": "https://backend.example/v1"} if test.include { backend["smoke_tests"] = test.smokeTests } main := mapping("backend", map[string]any{ - "backends": map[string]any{"local": backend}, + "backends": []any{backend}, }) var underlays []configkit.Underlay if test.inherited { underlays = []configkit.Underlay{{Inputs: []configkit.Input{mapping("defaults", map[string]any{ - "backends": map[string]any{"local": map[string]any{ + "backends": []any{map[string]any{"id": "local", "endpoint": "https://backend.example/v1", "smoke_tests": map[string]any{"default_profile": "inherited-profile"}, }}, @@ -287,7 +546,7 @@ func TestLoadBackendSmokeTestsOmissionAndNull(t *testing.T) { if err != nil { t.Fatal(err) } - if got := loaded.result.Config.Backends["local"].SmokeTests; got != nil { + if got := loaded.result.Config.Backends[0].SmokeTests; got != nil { t.Fatalf("smoke_tests = %+v, want nil", got) } selected, err := loaded.registry.GetBackend("local") @@ -303,7 +562,7 @@ func TestLoadBackendSmokeTestsOmissionAndNull(t *testing.T) { func TestLoadMergesBackendSmokeTestsAndRetainsFallbacks(t *testing.T) { base := mapping("defaults", map[string]any{ - "backends": map[string]any{"local": map[string]any{ + "backends": []any{map[string]any{"id": "catalog", "endpoint": "https://backend.example/v1", "smoke_tests": map[string]any{ "default_profile": "catalog-profile", @@ -316,7 +575,7 @@ func TestLoadMergesBackendSmokeTestsAndRetainsFallbacks(t *testing.T) { }}, }) overlay := mapping("operator", map[string]any{ - "backends": map[string]any{"local": map[string]any{ + "backends": []any{map[string]any{"id": "local", "extends": map[string]any{"id": "catalog"}, "smoke_tests": map[string]any{ "default_profile": "not-yet-listed", "authentication": map[string]any{"kind": " NONE ", "endpoint": nil}, @@ -360,37 +619,37 @@ func TestLoadRejectsInvalidFinalBackendSmokeTests(t *testing.T) { { name: "blank default profile", data: map[string]any{"default_profile": " \t "}, - path: "/backends/local/smoke_tests/default_profile", + path: "/backends/0/smoke_tests/default_profile", }, { name: "authentication kind required", data: map[string]any{"authentication": map[string]any{}}, - path: "/backends/local/smoke_tests/authentication/kind", + path: "/backends/0/smoke_tests/authentication/kind", }, { name: "unknown authentication kind", data: map[string]any{"authentication": map[string]any{"kind": "bearer"}}, - path: "/backends/local/smoke_tests/authentication/kind", + path: "/backends/0/smoke_tests/authentication/kind", }, { name: "non-http reachability URL", data: map[string]any{"reachability": map[string]any{"endpoint": "ftp://probe.example/ready"}}, - path: "/backends/local/smoke_tests/reachability/endpoint", + path: "/backends/0/smoke_tests/reachability/endpoint", }, { name: "blank authentication URL", data: map[string]any{"authentication": map[string]any{"kind": "none", "endpoint": " "}}, - path: "/backends/local/smoke_tests/authentication/endpoint", + path: "/backends/0/smoke_tests/authentication/endpoint", }, { name: "relative authentication URL", data: map[string]any{"authentication": map[string]any{"kind": "none", "endpoint": "/key"}}, - path: "/backends/local/smoke_tests/authentication/endpoint", + path: "/backends/0/smoke_tests/authentication/endpoint", }, } { t.Run(test.name, func(t *testing.T) { _, err := load(mapping("backend", map[string]any{ - "backends": map[string]any{"local": map[string]any{ + "backends": []any{map[string]any{"id": "local", "endpoint": "https://backend.example/v1", "smoke_tests": test.data, }}, @@ -455,33 +714,33 @@ func TestLoadRejectsStrictGroupConflict(t *testing.T) { func TestLoadValidatesOnlyFinalDomainValues(t *testing.T) { underlay := mapping("defaults", map[string]any{ - "backends": map[string]any{"local": map[string]any{"endpoint": "not a URL"}}, - "profiles": map[string]any{"partial": map[string]any{"backend": "local"}}, + "backends": []any{map[string]any{"id": "local", "endpoint": "not a URL"}}, + "profiles": []any{map[string]any{"id": "partial", "backend": "local"}}, }) - main := mapping("operator", map[string]any{"backends": map[string]any{"local": map[string]any{"endpoint": "http://localhost:8080/v1"}}}) + main := mapping("operator", map[string]any{"backends": []any{map[string]any{"id": "local", "endpoint": "http://localhost:8080/v1"}}}) loaded, err := load(main, []configkit.Underlay{{Inputs: []configkit.Input{underlay}}}) if err != nil { t.Fatalf("replaced lower-layer domain value should not fail: %v", err) } - if loaded.result.Config.Profiles["partial"].Model != nil { + if loaded.result.Config.Profiles[0].Model != nil { t.Fatal("unused partial profile unexpectedly gained a model") } _, err = load(configkit.Input{Source: configkit.NoSource()}, []configkit.Underlay{{Inputs: []configkit.Input{ - mapping("invalid", map[string]any{"backends": map[string]any{"unused": map[string]any{"endpoint": "not a URL"}}}), + mapping("invalid", map[string]any{"backends": []any{map[string]any{"id": "unused", "endpoint": "not a URL"}}}), }}}) var loadError *configkit.LoadError if !errors.As(err, &loadError) { t.Fatalf("expected path-aware effective backend failure, got %v", err) } - if diagnostics := loadError.Diagnostics(); len(diagnostics) != 1 || diagnostics[0].Path.String() != "/backends/unused/endpoint" { + if diagnostics := loadError.Diagnostics(); len(diagnostics) != 1 || diagnostics[0].Path.String() != "/backends/0/endpoint" { t.Fatalf("expected endpoint diagnostic, got %+v", diagnostics) } } func TestLoadValidatesOnlyFinalBackendSmokeTestValues(t *testing.T) { underlay := mapping("defaults", map[string]any{ - "backends": map[string]any{"local": map[string]any{ + "backends": []any{map[string]any{"id": "local", "endpoint": "https://backend.example/v1", "smoke_tests": map[string]any{ "default_profile": " ", @@ -491,7 +750,7 @@ func TestLoadValidatesOnlyFinalBackendSmokeTestValues(t *testing.T) { }}, }) main := mapping("operator", map[string]any{ - "backends": map[string]any{"local": map[string]any{ + "backends": []any{map[string]any{"id": "local", "endpoint": "https://backend.example/v1", "smoke_tests": map[string]any{ "default_profile": "dangling-profile", "reachability": map[string]any{"endpoint": nil}, @@ -514,7 +773,7 @@ func TestLoadValidatesOnlyFinalBackendSmokeTestValues(t *testing.T) { } func TestLoadKeepsSyntaxErrorsFromLowerTiers(t *testing.T) { - _, err := load(mapping("operator", map[string]any{"backends": map[string]any{"local": map[string]any{"endpoint": "http://localhost:8080"}}}), + _, err := load(mapping("operator", map[string]any{"backends": []any{map[string]any{"id": "local", "endpoint": "http://localhost:8080"}}}), []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Bytes("broken", []byte("backends: ["))}}}}) var loadError *configkit.LoadError if !errors.As(err, &loadError) { diff --git a/internal/configuration/targets_test.go b/internal/configuration/targets_test.go index 5d14da4..1a67440 100644 --- a/internal/configuration/targets_test.go +++ b/internal/configuration/targets_test.go @@ -13,6 +13,8 @@ import ( "gitea.maximumdirect.net/eric/promptkit/internal/domain" "gitea.maximumdirect.net/eric/promptkit/internal/llm" profilepkg "gitea.maximumdirect.net/eric/promptkit/internal/profile" + "gitea.maximumdirect.net/eric/promptkit/internal/prompt" + "gitea.maximumdirect.net/eric/promptkit/internal/usecase" ) func TestLoadIndexesIdentifiedTargets(t *testing.T) { @@ -40,6 +42,31 @@ func TestLoadIndexesIdentifiedTargets(t *testing.T) { } } +func TestRequestCompletesLoadedPartialProfile(t *testing.T) { + state, err := load(mapping("partial profile", map[string]any{ + "profiles": []any{map[string]any{"id": "partial"}}, + "prompts": []any{map[string]any{ + "id": "example", "messages": []any{map[string]any{"role": "user", "content": "hello"}}, + "output": map[string]any{"format": "text", "validation_mode": "basic"}, + }}, + }), nil) + if err != nil { + t.Fatal(err) + } + runner := usecase.NewRunner(state, state, nil, prompt.NewGoRenderer(), nil, nil, nil) + prepared, err := runner.Prepare(context.Background(), domain.RunRequest{ + PromptID: "example", ProfileID: "partial", + Execution: &domain.ExecutionTargetOverride{Endpoint: "https://example.invalid/v1", Model: "request-model"}, + }) + if err != nil { + t.Fatal(err) + } + if prepared.SelectedProfileID != "partial" || prepared.EffectiveModelParams.Endpoint != "https://example.invalid/v1" || + prepared.EffectiveModelParams.Model != "request-model" { + t.Fatalf("request did not complete the loaded profile: %+v", prepared) + } +} + func TestLoadBuildsEffectiveTargetsAfterInheritance(t *testing.T) { state, err := load(mapping("effective-targets", map[string]any{ "backends": []any{