Assemble collection resources by qualified ID

This commit is contained in:
2026-10-17 06:03:28 +00:00
parent a10a9053b7
commit 473fc1f6d7
12 changed files with 387 additions and 154 deletions

View File

@@ -23,13 +23,19 @@ func TestConfigurationInspectionReportsLiteralKeysAndRedactedHistory(t *testing.
"endpoint": "https://example.invalid/v1", "api_key_env": "OPTIONAL_KEY", "endpoint": "https://example.invalid/v1", "api_key_env": "OPTIONAL_KEY",
"extra_params": map[string]any{"token": "private-sentinel", "visible": "public-value"}, "extra_params": map[string]any{"token": "private-sentinel", "visible": "public-value"},
}}, }},
"profiles": map[string]any{ "collections": map[string]any{"literal": map[string]any{
"base.v1": map[string]any{"backend": "local", "model": "default-model"}, "backends": map[string]any{"local": map[string]any{
"literal/id": map[string]any{"extends": "base.v1"}, "endpoint": "https://example.invalid/v1",
}, "extra_params": map[string]any{"token": "namespaced-private-sentinel"},
}},
"profiles": map[string]any{
"base.v1": map[string]any{"backend": "local", "model": "default-model"},
"id": map[string]any{"extends": "base.v1"},
},
}},
})}}}, })}}},
{Inputs: []configkit.Input{{Source: configkit.Mapping("operator", map[string]any{ {Inputs: []configkit.Input{{Source: configkit.Mapping("operator", map[string]any{
"profiles": map[string]any{"base.v1": map[string]any{"model": "operator-model"}}, "collections": map[string]any{"literal": map[string]any{"profiles": map[string]any{"base.v1": map[string]any{"model": "operator-model"}}}},
})}}}, })}}},
}, },
}}) }})
@@ -40,17 +46,17 @@ func TestConfigurationInspectionReportsLiteralKeysAndRedactedHistory(t *testing.
if err != nil { if err != nil {
t.Fatalf("inspect configuration: %v", err) t.Fatalf("inspect configuration: %v", err)
} }
modelPath := configkit.Keys("profiles", "base.v1", "model") modelPath := configkit.Keys("collections", "literal", "profiles", "base.v1", "model")
model, found := view.Explain(modelPath) model, found := view.Explain(modelPath)
if !found || !strings.Contains(model.Effective.YAML, "operator-model") || len(model.History) < 2 { if !found || !strings.Contains(model.Effective.YAML, "operator-model") || len(model.History) < 2 {
t.Fatalf("layered model explanation = (%+v, %t)", model, found) t.Fatalf("layered model explanation = (%+v, %t)", model, found)
} }
inheritedPath := configkit.Keys("profiles", "literal/id", "model") inheritedPath := configkit.Keys("collections", "literal", "profiles", "id", "model")
inherited, found := view.Explain(inheritedPath) inherited, found := view.Explain(inheritedPath)
if !found || !strings.Contains(inherited.Effective.YAML, "operator-model") || len(inherited.History) == 0 || !view.Metadata().IsExplicit(inheritedPath) { if !found || !strings.Contains(inherited.Effective.YAML, "operator-model") || len(inherited.History) == 0 || !view.Metadata().IsExplicit(inheritedPath) {
t.Fatalf("literal-key inheritance explanation = (%+v, %t)", inherited, found) t.Fatalf("literal-key inheritance explanation = (%+v, %t)", inherited, found)
} }
if inheritedPath.String() != "/profiles/literal~1id/model" { if inheritedPath.String() != "/collections/literal/profiles/id/model" {
t.Fatalf("literal-key path = %q", inheritedPath) t.Fatalf("literal-key path = %q", inheritedPath)
} }
secretPath := configkit.Keys("backends", "local", "extra_params", "token") secretPath := configkit.Keys("backends", "local", "extra_params", "token")
@@ -58,6 +64,11 @@ func TestConfigurationInspectionReportsLiteralKeysAndRedactedHistory(t *testing.
if !found || !secret.Effective.Redacted || strings.Contains(secret.Effective.YAML, "private-sentinel") { if !found || !secret.Effective.Redacted || strings.Contains(secret.Effective.YAML, "private-sentinel") {
t.Fatalf("credential-shaped provider value was not redacted: (%+v, %t)", secret, found) t.Fatalf("credential-shaped provider value was not redacted: (%+v, %t)", secret, found)
} }
namespacedSecretPath := configkit.Keys("collections", "literal", "backends", "local", "extra_params", "token")
namespacedSecret, found := view.Explain(namespacedSecretPath)
if !found || !namespacedSecret.Effective.Redacted || strings.Contains(namespacedSecret.Effective.YAML, "namespaced-private-sentinel") {
t.Fatalf("namespaced provider value was not redacted: (%+v, %t)", namespacedSecret, found)
}
var effective bytes.Buffer var effective bytes.Buffer
if err := view.WriteEffective(&effective); err != nil { if err := view.WriteEffective(&effective); err != nil {
t.Fatalf("write effective configuration: %v", err) t.Fatalf("write effective configuration: %v", err)
@@ -66,7 +77,7 @@ func TestConfigurationInspectionReportsLiteralKeysAndRedactedHistory(t *testing.
t.Fatalf("unexpected effective report: %s", effective.String()) t.Fatalf("unexpected effective report: %s", effective.String())
} }
var explanation bytes.Buffer var explanation bytes.Buffer
if err := view.WriteExplanation(&explanation, secretPath); err != nil || strings.Contains(explanation.String(), "private-sentinel") { if err := view.WriteExplanation(&explanation, namespacedSecretPath); err != nil || strings.Contains(explanation.String(), "namespaced-private-sentinel") {
t.Fatalf("secret explanation = (%q, %v)", explanation.String(), err) t.Fatalf("secret explanation = (%q, %v)", explanation.String(), err)
} }

View File

@@ -508,6 +508,14 @@ release metadata, sibling inheritance, headerless collection fragments, resource
named `version` in nested mappings, and header/declaration distinctions. named `version` in nested mappings, and header/declaration distinctions.
Run `GOWORK=off go test ./internal/configuration .`. Run `GOWORK=off go test ./internal/configuration .`.
Completion: final root and collection resources are interpreted from their
effective ConfigKit paths and indexed under full runtime IDs without re-keying
the retained result. Collection namespaces and local keys are validated;
collection release labels, sibling inheritance, inherited relative content,
presence/null handling, and namespaced secret redaction are covered. Capacity
and profile snapshots use full backend identities. Validation passed:
`GOWORK=off go test ./...`, `GOWORK=off go vet ./...`, and `git diff --check`.
## Stage 9: Make Application Tests Independent Of Bundled Catalogs ## Stage 9: Make Application Tests Independent Of Bundled Catalogs
Scope: owned fixtures and examples that currently assume automatic provider data. Scope: owned fixtures and examples that currently assume automatic provider data.

View File

@@ -66,6 +66,11 @@ func secretProviderPath(path configkit.Path) bool {
} }
func providerExtraParamsPath(parts []string) bool { func providerExtraParamsPath(parts []string) bool {
return len(parts) >= 4 && parts[1] != "" && if len(parts) >= 4 && parts[1] != "" &&
(parts[0] == "backends" || parts[0] == "profiles") && parts[2] == "extra_params" (parts[0] == "backends" || parts[0] == "profiles") && parts[2] == "extra_params" {
return true
}
return len(parts) >= 6 && parts[0] == "collections" && parts[1] != "" &&
(parts[2] == "backends" || parts[2] == "profiles") &&
parts[3] != "" && parts[4] == "extra_params"
} }

View File

@@ -14,8 +14,11 @@ func TestSecretProviderPathRecognizesResourceExtraParams(t *testing.T) {
}{ }{
{name: "backend nested secret", path: configkit.Keys("backends", "provider", "extra_params", "nested", "Token"), want: true}, {name: "backend nested secret", path: configkit.Keys("backends", "provider", "extra_params", "nested", "Token"), want: true},
{name: "profile nested secret", path: configkit.Keys("profiles", "assistant", "extra_params", "authorization"), want: true}, {name: "profile nested secret", path: configkit.Keys("profiles", "assistant", "extra_params", "authorization"), want: true},
{name: "collection backend nested secret", path: configkit.Keys("collections", "provider", "backends", "default", "extra_params", "nested", "Token"), want: true},
{name: "collection profile secret", path: configkit.Keys("collections", "assistant", "profiles", "model", "extra_params", "authorization"), want: true},
{name: "unrelated nested mapping", path: configkit.Keys("application", "backends", "provider", "extra_params", "token"), want: false}, {name: "unrelated nested mapping", path: configkit.Keys("application", "backends", "provider", "extra_params", "token"), want: false},
{name: "extra params outside resource map", path: configkit.Keys("backends", "provider", "options", "extra_params", "token"), want: false}, {name: "extra params outside resource map", path: configkit.Keys("backends", "provider", "options", "extra_params", "token"), want: false},
{name: "collection extra params outside resource map", path: configkit.Keys("collections", "provider", "settings", "backends", "default", "extra_params", "token"), want: false},
{name: "missing resource key", path: configkit.Keys("backends", "", "extra_params", "token"), want: false}, {name: "missing resource key", path: configkit.Keys("backends", "", "extra_params", "token"), want: false},
{name: "ordinary provider value", path: configkit.Keys("profiles", "assistant", "extra_params", "provider", "mode"), want: false}, {name: "ordinary provider value", path: configkit.Keys("profiles", "assistant", "extra_params", "provider", "mode"), want: false},
} }

View File

@@ -15,16 +15,11 @@ func TestLoadedListsSortedEffectiveIdentities(t *testing.T) {
"backends": map[string]any{ "backends": map[string]any{
"local": map[string]any{"endpoint": "http://localhost:8080/v1"}, "local": map[string]any{"endpoint": "http://localhost:8080/v1"},
}, },
"profiles": map[string]any{ "collections": map[string]any{
"z/profile": map[string]any{"backend": "local", "model": "z-model"}, "z": map[string]any{"profiles": map[string]any{"profile": map[string]any{"backend": "local", "model": "z-model"}}, "prompts": map[string]any{"prompt": listingPromptDefinition()}},
"partial/profile": map[string]any{"backend": "local"}, "partial": map[string]any{"profiles": map[string]any{"profile": map[string]any{"backend": "local"}}},
"a/profile": map[string]any{"backend": "local", "model": "a-model"}, "a": map[string]any{"profiles": map[string]any{"profile": map[string]any{"backend": "local", "model": "a-model"}}, "prompts": map[string]any{"prompt": listingPromptDefinition()}},
" z/profile ": map[string]any{"backend": "local", "model": "spaced-model"}, " z": map[string]any{"profiles": map[string]any{"profile ": map[string]any{"backend": "local", "model": "spaced-model"}}, "prompts": map[string]any{"prompt ": listingPromptDefinition()}},
},
"prompts": map[string]any{
"z/prompt": listingPromptDefinition(),
"a/prompt": listingPromptDefinition(),
" z/prompt ": listingPromptDefinition(),
}, },
}), nil) }), nil)
if err != nil { if err != nil {

View File

@@ -17,10 +17,18 @@ import (
) )
type document struct { type document struct {
Version *string `yaml:"version"` Version *string `yaml:"version"`
Backends map[string]backendConfig `yaml:"backends"` Backends map[string]backendConfig `yaml:"backends"`
Profiles map[string]profileConfig `yaml:"profiles"` Profiles map[string]profileConfig `yaml:"profiles"`
Prompts map[string]promptConfig `yaml:"prompts"` Prompts map[string]promptConfig `yaml:"prompts"`
Collections map[string]collectionConfig `yaml:"collections"`
}
type collectionConfig struct {
ReleaseVersion *string `yaml:"release_version"`
Backends map[string]backendConfig `yaml:"backends"`
Profiles map[string]profileConfig `yaml:"profiles"`
Prompts map[string]promptConfig `yaml:"prompts"`
} }
type backendConfig struct { type backendConfig struct {
@@ -126,6 +134,15 @@ type resourceBases struct {
prompts configkit.Path prompts configkit.Path
} }
type resourceSet struct {
isCollection bool
namespace string
bases resourceBases
backends map[string]backendConfig
profiles map[string]profileConfig
prompts map[string]promptConfig
}
func rootResourceBases() resourceBases { func rootResourceBases() resourceBases {
return resourceBases{ return resourceBases{
backends: configkit.Keys("backends"), backends: configkit.Keys("backends"),
@@ -134,6 +151,61 @@ func rootResourceBases() resourceBases {
} }
} }
func resourceSets(config *document) []resourceSet {
sets := make([]resourceSet, 0, len(config.Collections)+1)
sets = append(sets, resourceSet{
bases: rootResourceBases(), backends: config.Backends, profiles: config.Profiles, prompts: config.Prompts,
})
for _, namespace := range sortedKeys(config.Collections) {
collection := config.Collections[namespace]
base := configkit.Keys("collections", namespace)
sets = append(sets, resourceSet{
isCollection: true,
namespace: namespace,
bases: resourceBases{
backends: base.Key("backends"),
profiles: base.Key("profiles"),
prompts: base.Key("prompts"),
},
backends: collection.Backends,
profiles: collection.Profiles,
prompts: collection.Prompts,
})
}
return sets
}
func indexedResourceID(namespace, localKey string) string {
qualified, valid := domain.QualifyLocalResourceID(namespace, localKey)
if valid {
return qualified
}
if namespace == "" {
return localKey
}
return namespace + "/" + localKey
}
func indexedBackends(config *document) map[string]backendConfig {
backends := make(map[string]backendConfig)
for _, resources := range resourceSets(config) {
for localKey, definition := range resources.backends {
backends[indexedResourceID(resources.namespace, localKey)] = definition
}
}
return backends
}
func indexedProfiles(config *document) map[string]profileConfig {
profiles := make(map[string]profileConfig)
for _, resources := range resourceSets(config) {
for localKey, definition := range resources.profiles {
profiles[indexedResourceID(resources.namespace, localKey)] = definition
}
}
return profiles
}
var envName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) var envName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
var backendFields = []string{"endpoint", "api_key_env", "concurrency_limit", "queue_capacity", "extra_params", "smoke_tests"} var backendFields = []string{"endpoint", "api_key_env", "concurrency_limit", "queue_capacity", "extra_params", "smoke_tests"}
@@ -146,7 +218,6 @@ func Load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
// load performs one ConfigKit lifecycle over the selected source tiers. // load performs one ConfigKit lifecycle over the selected source tiers.
func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) { func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) {
bases := rootResourceBases()
state := &loaded{ state := &loaded{
backends: make(map[string]map[string]fieldState), backends: make(map[string]map[string]fieldState),
profiles: make(map[string]map[string]fieldState), profiles: make(map[string]map[string]fieldState),
@@ -162,16 +233,18 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
}, },
Secret: secretProviderPath, Secret: secretProviderPath,
Resolve: func(config *document, resolution *configkit.Resolution) error { Resolve: func(config *document, resolution *configkit.Resolution) error {
captureFields(config.Backends, bases.backends, backendFields, resolution, state.backends) for _, resources := range resourceSets(config) {
captureFields(config.Profiles, bases.profiles, profileFields, resolution, state.profiles) captureFields(resources.backends, resources.bases.backends, resources.namespace, backendFields, resolution, state.backends)
capturePromptMetadata(config.Prompts, bases.prompts, resolution, state.promptMetadata) captureFields(resources.profiles, resources.bases.profiles, resources.namespace, profileFields, resolution, state.profiles)
resolveBackends(config.Backends, bases.backends, resolution) capturePromptMetadata(resources.prompts, resources.bases.prompts, resources.namespace, resolution, state.promptMetadata)
resolveProfiles(config.Profiles, bases.profiles, resolution) resolveBackends(resources.backends, resources.bases.backends, resolution)
resolvePrompts(config.Prompts, bases.prompts, resolution) resolveProfiles(resources.profiles, resources.bases.profiles, resolution)
resolvePrompts(resources.prompts, resources.bases.prompts, resolution)
}
return nil return nil
}, },
Validate: func(config *document, validation *configkit.Validation) { Validate: func(config *document, validation *configkit.Validation) {
validateEffective(config, bases, validation) validateEffective(config, validation)
}, },
}, },
}) })
@@ -179,8 +252,10 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
return nil, fmt.Errorf("load configuration: %w", err) return nil, fmt.Errorf("load configuration: %w", err)
} }
state.result = result state.result = result
if err := state.materializePrompts(result.Config.Prompts, bases.prompts); err != nil { for _, resources := range resourceSets(&result.Config) {
return nil, fmt.Errorf("load configuration: %w", err) if err := state.materializePrompts(resources.prompts, resources.bases.prompts, resources.namespace); err != nil {
return nil, fmt.Errorf("load configuration: %w", err)
}
} }
if err := state.buildExecutionProfiles(); err != nil { if err := state.buildExecutionProfiles(); err != nil {
return nil, fmt.Errorf("load configuration: %w", err) return nil, fmt.Errorf("load configuration: %w", err)
@@ -207,7 +282,7 @@ type promptInputPresence struct {
description fieldState description fieldState
} }
func capturePromptMetadata(prompts map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution, destination map[string][]promptInputPresence) { func capturePromptMetadata(prompts map[string]promptConfig, basePath configkit.Path, namespace string, resolution *configkit.Resolution, destination map[string][]promptInputPresence) {
for _, id := range sortedKeys(prompts) { for _, id := range sortedKeys(prompts) {
inputs := prompts[id].Inputs inputs := prompts[id].Inputs
states := make([]promptInputPresence, len(inputs)) states := make([]promptInputPresence, len(inputs))
@@ -219,7 +294,7 @@ func capturePromptMetadata(prompts map[string]promptConfig, basePath configkit.P
description: captureFieldState(path.Key("description"), resolution), description: captureFieldState(path.Key("description"), resolution),
} }
} }
destination[id] = states destination[indexedResourceID(namespace, id)] = states
} }
} }
@@ -235,7 +310,7 @@ func captureFieldState(path configkit.Path, resolution *configkit.Resolution) fi
} }
} }
func captureFields[T any](entries map[string]T, basePath configkit.Path, fields []string, resolution *configkit.Resolution, destination map[string]map[string]fieldState) { func captureFields[T any](entries map[string]T, basePath configkit.Path, namespace string, fields []string, resolution *configkit.Resolution, destination map[string]map[string]fieldState) {
for id := range entries { for id := range entries {
states := make(map[string]fieldState, len(fields)) states := make(map[string]fieldState, len(fields))
for _, field := range fields { for _, field := range fields {
@@ -249,7 +324,7 @@ func captureFields[T any](entries map[string]T, basePath configkit.Path, fields
states[field] = fieldValue states[field] = fieldValue
} }
} }
destination[id] = states destination[indexedResourceID(namespace, id)] = states
} }
} }
@@ -355,83 +430,111 @@ func resolveProfiles(profiles map[string]profileConfig, basePath configkit.Path,
} }
} }
func validateEffective(config *document, bases resourceBases, validation *configkit.Validation) { func validateEffective(config *document, validation *configkit.Validation) {
for _, id := range sortedKeys(config.Backends) { sets := resourceSets(config)
backend := config.Backends[id] backendIDs := make(map[string]struct{})
base := bases.backends.Key(id) for _, resources := range sets {
if strings.TrimSpace(id) == "" { if resources.isCollection && !domain.IsValidResourceNamespace(resources.namespace) {
validation.Error(base, "backend ID must not be blank") validation.Error(configkit.Keys("collections", resources.namespace), "collection namespace must be nonblank and contain no slash")
} }
if backend.Endpoint == nil || *backend.Endpoint == "" { for localKey := range resources.backends {
validation.Error(base.Key("endpoint"), "endpoint is required") backendIDs[indexedResourceID(resources.namespace, localKey)] = struct{}{}
} }
if backend.APIKeyEnv != nil && *backend.APIKeyEnv != "" && !envName.MatchString(*backend.APIKeyEnv) { }
validation.Error(base.Key("api_key_env"), "environment variable name is invalid") for _, resources := range sets {
} for _, id := range sortedKeys(resources.backends) {
if backend.ConcurrencyLimit != nil && *backend.ConcurrencyLimit < 0 { backend := resources.backends[id]
validation.Error(base.Key("concurrency_limit"), "concurrency limit must not be negative") base := resources.bases.backends.Key(id)
} if !domain.IsValidLocalResourceKey(id) {
if backend.QueueCapacity != nil { validation.Error(base, "backend key must be a nonblank local key without a slash")
if *backend.QueueCapacity < 0 {
validation.Error(base.Key("queue_capacity"), "queue capacity must not be negative")
} }
if backend.ConcurrencyLimit == nil || *backend.ConcurrencyLimit <= 0 { if backend.Endpoint == nil || *backend.Endpoint == "" {
validation.Error(base.Key("queue_capacity"), "queue capacity requires a positive concurrency limit") validation.Error(base.Key("endpoint"), "endpoint is required")
} else if *backend.QueueCapacity > int(^uint(0)>>1)-*backend.ConcurrencyLimit {
validation.Error(base.Key("queue_capacity"), "total capacity overflows int")
} }
} if backend.APIKeyEnv != nil && *backend.APIKeyEnv != "" && !envName.MatchString(*backend.APIKeyEnv) {
validateExtraParams(backend.ExtraParams, base.Key("extra_params"), true, validation) validation.Error(base.Key("api_key_env"), "environment variable name is invalid")
if smokeTests := backend.SmokeTests; smokeTests != nil {
smokePath := base.Key("smoke_tests")
if smokeTests.DefaultProfile != nil && strings.TrimSpace(*smokeTests.DefaultProfile) == "" {
validation.Error(smokePath.Key("default_profile"), "default profile must not be blank")
} }
if authentication := smokeTests.Authentication; authentication != nil { if backend.ConcurrencyLimit != nil && *backend.ConcurrencyLimit < 0 {
kindPath := smokePath.Key("authentication").Key("kind") validation.Error(base.Key("concurrency_limit"), "concurrency limit must not be negative")
if authentication.Kind == nil { }
validation.Error(kindPath, "authentication kind is required") if backend.QueueCapacity != nil {
} else if *authentication.Kind != "none" && *authentication.Kind != "openrouter_key" { if *backend.QueueCapacity < 0 {
validation.Error(kindPath, "authentication kind must be none or openrouter_key") validation.Error(base.Key("queue_capacity"), "queue capacity must not be negative")
}
if backend.ConcurrencyLimit == nil || *backend.ConcurrencyLimit <= 0 {
validation.Error(base.Key("queue_capacity"), "queue capacity requires a positive concurrency limit")
} else if *backend.QueueCapacity > int(^uint(0)>>1)-*backend.ConcurrencyLimit {
validation.Error(base.Key("queue_capacity"), "total capacity overflows int")
}
}
validateExtraParams(backend.ExtraParams, base.Key("extra_params"), true, validation)
if smokeTests := backend.SmokeTests; smokeTests != nil {
smokePath := base.Key("smoke_tests")
if smokeTests.DefaultProfile != nil {
if strings.TrimSpace(*smokeTests.DefaultProfile) == "" {
validation.Error(smokePath.Key("default_profile"), "default profile must not be blank")
} else if !domain.IsValidResourceID(*smokeTests.DefaultProfile) {
validation.Error(smokePath.Key("default_profile"), "default profile reference is malformed")
}
}
if authentication := smokeTests.Authentication; authentication != nil {
kindPath := smokePath.Key("authentication").Key("kind")
if authentication.Kind == nil {
validation.Error(kindPath, "authentication kind is required")
} else if *authentication.Kind != "none" && *authentication.Kind != "openrouter_key" {
validation.Error(kindPath, "authentication kind must be none or openrouter_key")
}
} }
} }
} }
} for _, id := range sortedKeys(resources.profiles) {
for _, id := range sortedKeys(config.Profiles) { profile := resources.profiles[id]
profile := config.Profiles[id] base := resources.bases.profiles.Key(id)
base := bases.profiles.Key(id) if !domain.IsValidLocalResourceKey(id) {
if strings.TrimSpace(id) == "" { validation.Error(base, "profile key must be a nonblank local key without a slash")
validation.Error(base, "profile ID must not be blank") }
if profile.Backend != nil && *profile.Backend != "" {
if !domain.IsValidResourceID(*profile.Backend) {
validation.Error(base.Key("backend"), "backend reference is malformed")
} else if _, exists := backendIDs[*profile.Backend]; !exists {
validation.Error(base.Key("backend"), "backend is not defined")
}
}
if profile.Model != nil && *profile.Model != "" && strings.TrimSpace(*profile.Model) == "" {
validation.Error(base.Key("model"), "model must not be blank")
}
if profile.Temperature != nil {
if err := domain.ValidateExecutionTargetSettings(domain.ExecutionTarget{Temperature: *profile.Temperature}); err != nil {
validation.Error(base.Key("temperature"), err.Error())
}
}
if profile.TopP != nil {
if err := domain.ValidateExecutionTargetSettings(domain.ExecutionTarget{TopP: *profile.TopP}); err != nil {
validation.Error(base.Key("top_p"), err.Error())
}
}
if profile.MaxTokens != nil {
if err := domain.ValidateExecutionTargetSettings(domain.ExecutionTarget{MaxTokens: *profile.MaxTokens}); err != nil {
validation.Error(base.Key("max_tokens"), err.Error())
}
}
if profile.TimeoutSeconds != nil {
if err := domain.ValidateExecutionTargetSettings(domain.ExecutionTarget{TimeoutSeconds: *profile.TimeoutSeconds}); err != nil {
validation.Error(base.Key("timeout_seconds"), err.Error())
}
}
validateExtraParams(profile.ExtraParams, base.Key("extra_params"), false, validation)
} }
if profile.Backend != nil && *profile.Backend != "" { for _, id := range sortedKeys(resources.prompts) {
if _, exists := config.Backends[*profile.Backend]; !exists { prompt := resources.prompts[id]
validation.Error(base.Key("backend"), "backend is not defined") base := resources.bases.prompts.Key(id)
if !domain.IsValidLocalResourceKey(id) {
validation.Error(base, "prompt key must be a nonblank local key without a slash")
}
if prompt.DefaultProfile != nil && *prompt.DefaultProfile != "" && !domain.IsValidResourceID(*prompt.DefaultProfile) {
validation.Error(base.Key("default_profile"), "default profile reference is malformed")
} }
} }
if profile.Model != nil && *profile.Model != "" && strings.TrimSpace(*profile.Model) == "" {
validation.Error(base.Key("model"), "model must not be blank")
}
if profile.Temperature != nil {
if err := domain.ValidateExecutionTargetSettings(domain.ExecutionTarget{Temperature: *profile.Temperature}); err != nil {
validation.Error(base.Key("temperature"), err.Error())
}
}
if profile.TopP != nil {
if err := domain.ValidateExecutionTargetSettings(domain.ExecutionTarget{TopP: *profile.TopP}); err != nil {
validation.Error(base.Key("top_p"), err.Error())
}
}
if profile.MaxTokens != nil {
if err := domain.ValidateExecutionTargetSettings(domain.ExecutionTarget{MaxTokens: *profile.MaxTokens}); err != nil {
validation.Error(base.Key("max_tokens"), err.Error())
}
}
if profile.TimeoutSeconds != nil {
if err := domain.ValidateExecutionTargetSettings(domain.ExecutionTarget{TimeoutSeconds: *profile.TimeoutSeconds}); err != nil {
validation.Error(base.Key("timeout_seconds"), err.Error())
}
}
validateExtraParams(profile.ExtraParams, base.Key("extra_params"), false, validation)
} }
} }

View File

@@ -85,39 +85,55 @@ func TestLoadConsumesAcceptedUnderlayVersionAndValidatesHeaderlessPayload(t *tes
func TestLoadComposesLiteralKeysAndEffectivePresence(t *testing.T) { func TestLoadComposesLiteralKeysAndEffectivePresence(t *testing.T) {
base := mapping("defaults", map[string]any{ base := mapping("defaults", map[string]any{
"version": int64(1),
"backends": map[string]any{ "backends": map[string]any{
"local.v1": map[string]any{"endpoint": "http://localhost:8080/v1", "concurrency_limit": 2}, "local.v1": map[string]any{"endpoint": "http://localhost:8080/v1", "concurrency_limit": 2},
}, },
"profiles": map[string]any{ "collections": map[string]any{"base": map[string]any{
"base/one": map[string]any{"backend": "local.v1", "temperature": 0.8, "top_p": 0.9, "api_key_required": true, "release_version": "",
"extra_params": map[string]any{"provider": map[string]any{"first": 1, "second": 2}}}, "backends": map[string]any{
"child.v1": map[string]any{"extends": "base/one"}, "local.v1": map[string]any{"endpoint": "http://localhost:8080/v1", "concurrency_limit": 3},
}, },
"prompts": map[string]any{"invoice/summary": map[string]any{ "profiles": map[string]any{
"version": "v1.0", "one": map[string]any{"backend": "local.v1", "temperature": 0.8, "top_p": 0.9, "api_key_required": true,
"messages": []any{map[string]any{"role": "user", "content": "Summarize."}}, "extra_params": map[string]any{"provider": map[string]any{"first": 1, "second": 2}}},
"output": map[string]any{"format": "text", "validation_mode": "basic"}, "child.v1": map[string]any{"extends": "one"},
"namespaced": map[string]any{"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",
"messages": []any{map[string]any{"role": "user", "content": "Summarize."}},
"output": map[string]any{"format": "text", "validation_mode": "basic"},
},
"version": map[string]any{
"messages": []any{map[string]any{"role": "user", "content": "Nested ID."}},
"output": map[string]any{"format": "text", "validation_mode": "basic"},
},
},
}}, }},
}) })
overlay := mapping("operator", map[string]any{ overlay := mapping("operator", map[string]any{
"backends": map[string]any{"local.v1": map[string]any{"queue_capacity": 0}}, "backends": map[string]any{"local.v1": map[string]any{"queue_capacity": 0}},
"profiles": map[string]any{"child.v1": map[string]any{ "collections": map[string]any{"base": map[string]any{"profiles": map[string]any{"child.v1": map[string]any{
"temperature": 0, "top_p": nil, "api_key_required": false, "temperature": 0, "top_p": nil, "api_key_required": false,
"extra_params": map[string]any{"provider": map[string]any{"second": 3}}, "extra_params": map[string]any{"provider": map[string]any{"second": 3}},
}}, }}}},
}) })
loaded, err := load(configkit.Input{Source: configkit.NoSource()}, []configkit.Underlay{ loaded, err := load(mapping("main", map[string]any{"version": "1"}), []configkit.Underlay{
{Inputs: []configkit.Input{base}}, {Inputs: []configkit.Input{overlay}}, {Inputs: []configkit.Input{base}}, {Inputs: []configkit.Input{overlay}},
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
profile := loaded.result.Config.Profiles["child.v1"] profile := loaded.result.Config.Collections["base"].Profiles["child.v1"]
if profile.Temperature == nil || *profile.Temperature != 0 || profile.TopP != nil || profile.APIKeyRequired == nil || *profile.APIKeyRequired { if profile.Temperature == nil || *profile.Temperature != 0 || profile.TopP != nil || profile.APIKeyRequired == nil || *profile.APIKeyRequired {
t.Fatalf("effective values were not retained: %+v", profile) t.Fatalf("effective values were not retained: %+v", profile)
} }
if loaded.profiles["child.v1"]["temperature"] != fieldValue || loaded.profiles["child.v1"]["top_p"] != fieldNull || loaded.profiles["child.v1"]["api_key_required"] != fieldValue { if loaded.profiles["base/child.v1"]["temperature"] != fieldValue || loaded.profiles["base/child.v1"]["top_p"] != fieldNull || loaded.profiles["base/child.v1"]["api_key_required"] != fieldValue {
t.Fatalf("effective presence was lost: %+v", loaded.profiles["child.v1"]) t.Fatalf("effective presence was lost: %+v", loaded.profiles["base/child.v1"])
} }
provider := profile.ExtraParams["provider"].(map[string]any) provider := profile.ExtraParams["provider"].(map[string]any)
if provider["first"] != int64(1) || provider["second"] != int64(3) { if provider["first"] != int64(1) || provider["second"] != int64(3) {
@@ -127,9 +143,69 @@ func TestLoadComposesLiteralKeysAndEffectivePresence(t *testing.T) {
if backend.QueueCapacity == nil || *backend.QueueCapacity != 0 || loaded.backends["local.v1"]["queue_capacity"] != fieldValue { if backend.QueueCapacity == nil || *backend.QueueCapacity != 0 || loaded.backends["local.v1"]["queue_capacity"] != fieldValue {
t.Fatalf("explicit zero queue capacity was lost: %+v", backend) t.Fatalf("explicit zero queue capacity was lost: %+v", backend)
} }
if loaded.result.Config.Prompts["invoice/summary"].Messages[0].Content == nil || loaded.result.Config.Prompts["invoice/summary"].Version == nil || *loaded.result.Config.Prompts["invoice/summary"].Version != "v1.0" { if _, err := loaded.registry.GetBackend("base/local.v1"); err != nil {
t.Fatalf("collection backend was not indexed by its full ID: %v", err)
}
collectionProfile, exists := loaded.indexedProfiles["base/namespaced"]
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 || loaded.result.Config.Collections["invoice"].Prompts["summary"].Version == nil || *loaded.result.Config.Collections["invoice"].Prompts["summary"].Version != "v1.0" {
t.Fatal("literal prompt ID and version were not decoded") t.Fatal("literal prompt ID and version were not decoded")
} }
if loaded.result.Config.Version == nil || *loaded.result.Config.Version != "1" ||
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)
}
if _, _, err := loaded.prompt("invoice/version"); err != nil {
t.Fatalf("resource named version was not indexed under its qualified identity: %v", err)
}
}
func TestLoadRejectsInvalidCollectionAndResourceKeys(t *testing.T) {
for _, test := range []struct {
name string
values map[string]any
message string
}{
{
name: "namespace with separator",
values: map[string]any{"collections": map[string]any{"bad/name": map[string]any{}}},
message: "collection namespace must be nonblank and contain no slash",
},
{
name: "root resource with separator",
values: map[string]any{"backends": map[string]any{
"bad/name": map[string]any{"endpoint": "https://example.test/v1"},
}},
message: "backend key 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{
"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",
},
} {
t.Run(test.name, func(t *testing.T) {
_, err := load(mapping("invalid names", test.values), nil)
var loadErr *configkit.LoadError
if err == nil || !errors.As(err, &loadErr) {
t.Fatalf("load error = %v, want ConfigKit validation error", err)
}
for _, diagnostic := range loadErr.Diagnostics() {
if diagnostic.Message == test.message {
return
}
}
t.Fatalf("load diagnostics = %+v, want message %q", loadErr.Diagnostics(), test.message)
})
}
} }
func TestLoadRecordsBackendNormalizationAndNullDefault(t *testing.T) { func TestLoadRecordsBackendNormalizationAndNullDefault(t *testing.T) {

View File

@@ -81,8 +81,10 @@ func resolvePrompts(prompts map[string]promptConfig, basePath configkit.Path, re
} }
} }
func (state *loaded) materializePrompts(prompts map[string]promptConfig, basePath configkit.Path) error { func (state *loaded) materializePrompts(prompts map[string]promptConfig, basePath configkit.Path, namespace string) error {
state.prompts = make(map[string]*promptEntry, len(prompts)) if state.prompts == nil {
state.prompts = make(map[string]*promptEntry)
}
var metadata *configkit.Metadata var metadata *configkit.Metadata
lookupOrigin := func(path configkit.Path) (configkit.FieldInfo, bool) { lookupOrigin := func(path configkit.Path) (configkit.FieldInfo, bool) {
if metadata == nil { if metadata == nil {
@@ -96,11 +98,12 @@ func (state *loaded) materializePrompts(prompts map[string]promptConfig, basePat
if strings.TrimSpace(id) == "" { if strings.TrimSpace(id) == "" {
return fmt.Errorf("%s: prompt ID must not be blank", promptBase) return fmt.Errorf("%s: prompt ID must not be blank", promptBase)
} }
entry, err := state.materializePrompt(id, prompts[id].Version, prompts[id], promptBase, lookupOrigin) fullID := indexedResourceID(namespace, id)
entry, err := state.materializePrompt(fullID, prompts[id].Version, prompts[id], promptBase, lookupOrigin)
if err != nil { if err != nil {
return err return err
} }
state.prompts[id] = entry state.prompts[fullID] = entry
} }
return nil return nil
} }

View File

@@ -16,8 +16,8 @@ import (
func TestLoadCapturesInheritedImportedContent(t *testing.T) { func TestLoadCapturesInheritedImportedContent(t *testing.T) {
files := fstest.MapFS{ files := fstest.MapFS{
"app/main.yaml": {Data: []byte("configkit:\n imports:\n - path: ../catalog/base.yaml\nprompts:\n child:\n extends: base\n")}, "app/main.yaml": {Data: []byte("configkit:\n imports:\n - path: ../catalog/base.yaml\ncollections:\n docs:\n prompts:\n child:\n extends: base\n")},
"catalog/base.yaml": {Data: []byte("prompts:\n base:\n version: \"v1\"\n messages:\n - role: ' USER '\n cache_control:\n type: ' ephemeral '\n ttl: ' 1h '\n content_file: ./body.md\n output:\n format: text\n validation_mode: basic\n")}, "catalog/base.yaml": {Data: []byte("collections:\n docs:\n release_version: r1\n prompts:\n base:\n version: \"v1\"\n messages:\n - role: ' USER '\n cache_control:\n type: ' ephemeral '\n ttl: ' 1h '\n content_file: ./body.md\n output:\n format: text\n validation_mode: basic\n")},
"catalog/body.md": {Data: []byte("Captured {{.name}}")}, "catalog/body.md": {Data: []byte("Captured {{.name}}")},
} }
namespace := configkit.NewFilesystem("embedded", files) namespace := configkit.NewFilesystem("embedded", files)
@@ -32,7 +32,7 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) {
} }
t.Fatal(err) t.Fatal(err)
} }
definition, inspection, err := state.prompt("child") definition, inspection, err := state.prompt("docs/child")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -43,7 +43,7 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) {
if message.Role != "user" || message.CacheControl == nil || message.CacheControl.Type != "ephemeral" || message.CacheControl.TTL != "1h" { if message.Role != "user" || message.CacheControl == nil || message.CacheControl.Type != "ephemeral" || message.CacheControl.TTL != "1h" {
t.Fatalf("message metadata was not normalized: %+v", message) t.Fatalf("message metadata was not normalized: %+v", message)
} }
field, ok := state.result.Metadata().Lookup(configkit.Keys("prompts", "child", "messages").Index(0).Key("content_file")) field, ok := state.result.Metadata().Lookup(configkit.Keys("collections", "docs", "prompts", "child", "messages").Index(0).Key("content_file"))
if !ok || len(field.Origin.Locations) != 1 { if !ok || len(field.Origin.Locations) != 1 {
t.Fatalf("content origin was lost during normalization: %+v", field) t.Fatalf("content origin was lost during normalization: %+v", field)
} }
@@ -57,12 +57,12 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) {
t.Fatalf("content origin = %q, want catalog/base.yaml", originPath) t.Fatalf("content origin = %q, want catalog/base.yaml", originPath)
} }
files["catalog/body.md"].Data = []byte("Changed") files["catalog/body.md"].Data = []byte("Changed")
again, againInspection, err := state.prompt("child") again, againInspection, err := state.prompt("docs/child")
if err != nil || again.Templates[0].Content != "Captured {{.name}}" || againInspection.PromptHash != inspection.PromptHash { if err != nil || again.Templates[0].Content != "Captured {{.name}}" || againInspection.PromptHash != inspection.PromptHash {
t.Fatalf("captured definition changed after source mutation: %+v, %+v, %v", again, againInspection, err) t.Fatalf("captured definition changed after source mutation: %+v, %+v, %v", again, againInspection, err)
} }
definition.Templates[0].Content = "caller mutation" definition.Templates[0].Content = "caller mutation"
third, _, err := state.prompt("child") third, _, err := state.prompt("docs/child")
if err != nil || third.Templates[0].Content != "Captured {{.name}}" { if err != nil || third.Templates[0].Content != "Captured {{.name}}" {
t.Fatalf("caller mutation escaped into index: %+v, %v", third, err) t.Fatalf("caller mutation escaped into index: %+v, %v", third, err)
} }

View File

@@ -12,9 +12,11 @@ import (
) )
func (state *loaded) buildExecutionProfiles() error { func (state *loaded) buildExecutionProfiles() error {
definitions := make(map[string]domain.Backend, len(state.result.Config.Backends)) backendConfigs := indexedBackends(&state.result.Config)
for _, id := range sortedKeys(state.result.Config.Backends) { profileConfigs := indexedProfiles(&state.result.Config)
config := state.result.Config.Backends[id] definitions := make(map[string]domain.Backend, len(backendConfigs))
for _, id := range sortedKeys(backendConfigs) {
config := backendConfigs[id]
definition := domain.Backend{ID: id, ExtraParams: config.ExtraParams} definition := domain.Backend{ID: id, ExtraParams: config.ExtraParams}
if config.SmokeTests != nil { if config.SmokeTests != nil {
smokeTests := &domain.BackendSmokeTestConfig{DefaultProfile: config.SmokeTests.DefaultProfile} smokeTests := &domain.BackendSmokeTestConfig{DefaultProfile: config.SmokeTests.DefaultProfile}
@@ -49,9 +51,9 @@ func (state *loaded) buildExecutionProfiles() error {
return err return err
} }
state.registry = registry state.registry = registry
state.indexedProfiles = make(map[string]domain.ExecutionProfile, len(state.result.Config.Profiles)) state.indexedProfiles = make(map[string]domain.ExecutionProfile, len(profileConfigs))
for _, id := range sortedKeys(state.result.Config.Profiles) { for _, id := range sortedKeys(profileConfigs) {
config := state.result.Config.Profiles[id] config := profileConfigs[id]
fields := state.profiles[id] fields := state.profiles[id]
target := defaults.ExecutionTargetDefault() target := defaults.ExecutionTargetDefault()
if config.Backend != nil && *config.Backend != "" { if config.Backend != nil && *config.Backend != "" {

View File

@@ -38,3 +38,27 @@ func resourceNamespaceFromID(id string) (string, bool) {
namespace, _, _ := strings.Cut(id, "/") namespace, _, _ := strings.Cut(id, "/")
return namespace, true return namespace, true
} }
// IsValidResourceID reports whether id is an unqualified resource ID or one
// valid namespace/local-ID pair.
func IsValidResourceID(id string) bool { return validResourceID(id) }
// IsValidResourceNamespace reports whether namespace is nonblank and contains
// no namespace separator.
func IsValidResourceNamespace(namespace string) bool { return validResourceNamespace(namespace) }
// IsValidLocalResourceKey reports whether key is a nonblank key for a
// namespace-local resource mapping.
func IsValidLocalResourceKey(key string) bool { return validLocalResourceKey(key) }
// QualifyLocalResourceID combines a validated namespace and local mapping key.
// An empty namespace leaves a valid unqualified key unchanged.
func QualifyLocalResourceID(namespace, key string) (string, bool) {
if !validLocalResourceKey(key) || (namespace != "" && !validResourceNamespace(namespace)) {
return "", false
}
if namespace == "" {
return key, true
}
return namespace + "/" + key, true
}

View File

@@ -111,23 +111,26 @@ func TestListIdentitiesPreserveContextErrorsWithoutPartialResults(t *testing.T)
func TestListIdentitiesUseEffectiveLayeredMembership(t *testing.T) { func TestListIdentitiesUseEffectiveLayeredMembership(t *testing.T) {
underlay := configkit.Input{Source: configkit.Mapping("consumer defaults", map[string]any{ underlay := configkit.Input{Source: configkit.Mapping("consumer defaults", map[string]any{
"profiles": map[string]any{ "collections": map[string]any{"shared": map[string]any{
"base/profile": map[string]any{"endpoint": "https://example.test/v1", "model": "base-model"}, "profiles": map[string]any{
"child/profile": map[string]any{"extends": "base/profile"}, "base": map[string]any{"endpoint": "https://example.test/v1", "model": "base-model"},
"overlay/profile": map[string]any{"endpoint": "https://example.test/v1", "model": "old-model"}, "child": map[string]any{"extends": "base"},
}, "overlay": map[string]any{"endpoint": "https://example.test/v1", "model": "old-model"},
},
"prompts": map[string]any{
"prompt": promptDefinitionWithVersion("v1", listingContractPrompt()),
},
}},
"prompts": map[string]any{ "prompts": map[string]any{
"shared/prompt": promptDefinitionWithVersion("v1", listingContractPrompt()),
"underlay-only": promptDefinitionWithVersion("v1", listingContractPrompt()), "underlay-only": promptDefinitionWithVersion("v1", listingContractPrompt()),
}, },
})} })}
main := configkit.Input{Source: configkit.Mapping("application overlay", map[string]any{ main := configkit.Input{Source: configkit.Mapping("application overlay", map[string]any{
"profiles": map[string]any{ "collections": map[string]any{"shared": map[string]any{"profiles": map[string]any{
"overlay/profile": map[string]any{"model": "effective-model"}, "overlay": map[string]any{"model": "effective-model"},
}, }}},
"prompts": map[string]any{ "prompts": map[string]any{
"shared/prompt": promptDefinitionWithVersion("v2", listingContractPrompt()), "main-only": promptDefinitionWithVersion("v3", listingContractPrompt()),
"main-only": promptDefinitionWithVersion("v3", listingContractPrompt()),
}, },
})} })}
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{ engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
@@ -143,24 +146,24 @@ func TestListIdentitiesUseEffectiveLayeredMembership(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
for _, id := range []string{ for _, id := range []string{
"base/profile", "child/profile", "overlay/profile", "shared/base", "shared/child", "shared/overlay",
"promptkit-openrouter-standard", "promptkit-openrouter-auto", "promptkit-openrouter-standard", "promptkit-openrouter-auto",
} { } {
if !containsListingProfile(profiles, id) { if !containsListingProfile(profiles, id) {
t.Errorf("profile listing %#v does not include %q", profiles, id) t.Errorf("profile listing %#v does not include %q", profiles, id)
} }
} }
for _, id := range []string{"base/profile", "child/profile", "overlay/profile"} { for _, id := range []string{"shared/base", "shared/child", "shared/overlay"} {
if got := countListingProfile(profiles, id); got != 1 { if got := countListingProfile(profiles, id); got != 1 {
t.Errorf("profile %q appears %d times, want 1", id, got) t.Errorf("profile %q appears %d times, want 1", id, got)
} }
} }
overlay, err := engine.InspectProfile(context.Background(), "overlay/profile") overlay, err := engine.InspectProfile(context.Background(), "shared/overlay")
if err != nil || overlay == nil || overlay.EffectiveModelParams.Model != "effective-model" { if err != nil || overlay == nil || overlay.EffectiveModelParams.Model != "effective-model" {
t.Fatalf("overlay profile did not resolve to its effective definition: (%+v, %v)", overlay, err) t.Fatalf("overlay profile did not resolve to its effective definition: (%+v, %v)", overlay, err)
} }
child, err := engine.InspectProfile(context.Background(), "child/profile") child, err := engine.InspectProfile(context.Background(), "shared/child")
if err != nil || child == nil || child.EffectiveModelParams.Model != "base-model" { if err != nil || child == nil || child.EffectiveModelParams.Model != "base-model" {
t.Fatalf("inherited child profile did not resolve its effective definition: (%+v, %v)", child, err) t.Fatalf("inherited child profile did not resolve its effective definition: (%+v, %v)", child, err)
} }
@@ -171,13 +174,13 @@ func TestListIdentitiesUseEffectiveLayeredMembership(t *testing.T) {
} }
wantPrompts := []promptkit.PromptIdentity{ wantPrompts := []promptkit.PromptIdentity{
{ID: "main-only", Version: stringPointerForTest("v3")}, {ID: "main-only", Version: stringPointerForTest("v3")},
{ID: "shared/prompt", Version: stringPointerForTest("v2")}, {ID: "shared/prompt", Version: stringPointerForTest("v1")},
{ID: "underlay-only", Version: stringPointerForTest("v1")}, {ID: "underlay-only", Version: stringPointerForTest("v1")},
} }
if !reflect.DeepEqual(prompts, wantPrompts) { if !reflect.DeepEqual(prompts, wantPrompts) {
t.Fatalf("effective prompt identities = %#v, want %#v", prompts, wantPrompts) t.Fatalf("effective prompt identities = %#v, want %#v", prompts, wantPrompts)
} }
if got := countListingPrompt(prompts, "shared/prompt", "v2"); got != 1 { if got := countListingPrompt(prompts, "shared/prompt", "v1"); got != 1 {
t.Fatalf("overlaid prompt identity appears %d times, want 1", got) t.Fatalf("overlaid prompt identity appears %d times, want 1", got)
} }
} }