Interpret backend and profile settings without changing source values
This commit is contained in:
@@ -570,6 +570,14 @@ inspection does not acquire synthesized whole-list origins/defaults. Record
|
||||
unrelated old-fixture failures for their assigned stages rather than restoring
|
||||
mutating resolution to satisfy them.
|
||||
|
||||
Completion: backend and profile normalization and defaults now operate on
|
||||
detached interpreted records during validation, with presence captured from the
|
||||
unchanged ConfigKit tree. Runtime indexes consume those interpreted values.
|
||||
Focused real-loader tests passed for padded endpoints, nested diagnostics,
|
||||
profile settings, omitted/null queue defaults, inherited targets, and preserved
|
||||
composed values and origins on Go 1.27.0. Prompt interpretation follows in the
|
||||
next assignment; older fixtures remain assigned to later integration work.
|
||||
|
||||
## Stage 12: Recover Prompt Interpretation And Preserve Inspection Origins
|
||||
|
||||
Scope: prompt interpretation/materialization in `load.go`/`prompts.go` and the
|
||||
|
||||
46
internal/configuration/interpretation_test.go
Normal file
46
internal/configuration/interpretation_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package configuration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestTargetInterpretationPreservesComposedNestedValues(t *testing.T) {
|
||||
state, err := load(mapping("padded target", map[string]any{
|
||||
"backends": []any{map[string]any{
|
||||
"id": "local", "endpoint": " HTTPS://example.invalid/v1 ", "concurrency_limit": 1,
|
||||
"smoke_tests": map[string]any{
|
||||
"reachability": map[string]any{"endpoint": " https://example.invalid/health "},
|
||||
"authentication": map[string]any{"kind": " NONE ", "endpoint": " https://example.invalid/key "},
|
||||
},
|
||||
}},
|
||||
"profiles": []any{map[string]any{"id": "partial", "backend": "local", "service_tier": " standard ", "api_key_env": " TOKEN "}},
|
||||
}), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backend, err := state.GetBackend("local")
|
||||
if err != nil || backend.Endpoint != "https://example.invalid/v1" || backend.QueueCapacity != 1024 ||
|
||||
backend.SmokeTests == nil || *backend.SmokeTests.Reachability.Endpoint != "https://example.invalid/health" ||
|
||||
*backend.SmokeTests.Authentication.Kind != "none" || *backend.SmokeTests.Authentication.Endpoint != "https://example.invalid/key" {
|
||||
t.Fatalf("interpreted backend = %+v, %v", backend, err)
|
||||
}
|
||||
profile, err := state.GetProfile(context.Background(), "partial")
|
||||
if err != nil || profile.ResolvedTarget.ServiceTier != "standard" || profile.ResolvedTarget.APIKeyEnv != "TOKEN" {
|
||||
t.Fatalf("interpreted profile = %+v, %v", profile, err)
|
||||
}
|
||||
composed := state.result.Config.Backends[0]
|
||||
if *composed.Endpoint != " HTTPS://example.invalid/v1 " || composed.QueueCapacity != nil ||
|
||||
*composed.SmokeTests.Reachability.Endpoint != " https://example.invalid/health " ||
|
||||
*composed.SmokeTests.Authentication.Kind != " NONE " || *composed.SmokeTests.Authentication.Endpoint != " https://example.invalid/key " ||
|
||||
*state.result.Config.Profiles[0].ServiceTier != " standard " || *state.result.Config.Profiles[0].APIKeyEnv != " TOKEN " {
|
||||
t.Fatalf("composed values were mutated: %+v", state.result.Config)
|
||||
}
|
||||
path := configkit.Keys("backends").Index(0).Key("smoke_tests").Key("authentication").Key("endpoint")
|
||||
field, found := state.result.Metadata().Lookup(path)
|
||||
if !found || len(field.Origin.Locations) == 0 || field.Origin.Operation == configkit.ChangeNormalization {
|
||||
t.Fatalf("nested field origin changed: %+v", field)
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,7 @@ const (
|
||||
|
||||
type loaded struct {
|
||||
result *configkit.Result[document]
|
||||
interpreted document
|
||||
backends map[string]map[string]fieldState
|
||||
profiles map[string]map[string]fieldState
|
||||
promptMetadata map[string][]promptInputPresence
|
||||
@@ -221,6 +222,36 @@ func Load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
|
||||
return load(main, underlays)
|
||||
}
|
||||
|
||||
func configurationDefinition(state *loaded) configkit.Definition[document] {
|
||||
return configkit.Definition[document]{
|
||||
SequenceIdentities: []configkit.SequenceIdentity{
|
||||
configkit.SequenceIdentityAt(configkit.Keys("backends"), "id"),
|
||||
configkit.SequenceIdentityAt(configkit.Keys("profiles"), "id"),
|
||||
configkit.SequenceIdentityAt(configkit.Keys("prompts"), "id"),
|
||||
configkit.SequenceIdentityMatching(configkit.SequenceSelector{configkit.SequenceLiteralKey("collections"), configkit.SequenceAnyMappingKey(), configkit.SequenceLiteralKey("backends")}, "id"),
|
||||
configkit.SequenceIdentityMatching(configkit.SequenceSelector{configkit.SequenceLiteralKey("collections"), configkit.SequenceAnyMappingKey(), configkit.SequenceLiteralKey("profiles")}, "id"),
|
||||
configkit.SequenceIdentityMatching(configkit.SequenceSelector{configkit.SequenceLiteralKey("collections"), configkit.SequenceAnyMappingKey(), configkit.SequenceLiteralKey("prompts")}, "id"),
|
||||
},
|
||||
Version: &configkit.VersionRule{
|
||||
Path: configkit.Keys("version"),
|
||||
Check: checkConfigurationVersion,
|
||||
},
|
||||
Sensitive: secretProviderPath,
|
||||
Resolve: func(config *document, resolution *configkit.Resolution) error {
|
||||
for _, resources := range resourceSets(config) {
|
||||
captureFields(resources.backends, func(v backendConfig) string { return v.ID }, resources.bases.backends, resources.namespace, backendFields, resolution, state.backends)
|
||||
captureFields(resources.profiles, func(v profileConfig) string { return v.ID }, resources.bases.profiles, resources.namespace, profileFields, resolution, state.profiles)
|
||||
capturePromptMetadata(resources.prompts, resources.bases.prompts, resources.namespace, resolution, state.promptMetadata)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Validate: func(config *document, validation *configkit.Validation) {
|
||||
state.interpreted = interpretTargets(config, validation)
|
||||
validateEffective(&state.interpreted, validation)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// load performs one ConfigKit lifecycle over the selected source tiers.
|
||||
func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) {
|
||||
state := &loaded{
|
||||
@@ -231,35 +262,7 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
|
||||
result, err := configkit.LoadLayered(main, configkit.LayeredOptions[document]{
|
||||
Underlays: underlays,
|
||||
SourceVersionMode: configkit.SourceVersionModeIfPresent,
|
||||
Definition: configkit.Definition[document]{
|
||||
SequenceIdentities: []configkit.SequenceIdentity{
|
||||
configkit.SequenceIdentityAt(configkit.Keys("backends"), "id"),
|
||||
configkit.SequenceIdentityAt(configkit.Keys("profiles"), "id"),
|
||||
configkit.SequenceIdentityAt(configkit.Keys("prompts"), "id"),
|
||||
configkit.SequenceIdentityMatching(configkit.SequenceSelector{configkit.SequenceLiteralKey("collections"), configkit.SequenceAnyMappingKey(), configkit.SequenceLiteralKey("backends")}, "id"),
|
||||
configkit.SequenceIdentityMatching(configkit.SequenceSelector{configkit.SequenceLiteralKey("collections"), configkit.SequenceAnyMappingKey(), configkit.SequenceLiteralKey("profiles")}, "id"),
|
||||
configkit.SequenceIdentityMatching(configkit.SequenceSelector{configkit.SequenceLiteralKey("collections"), configkit.SequenceAnyMappingKey(), configkit.SequenceLiteralKey("prompts")}, "id"),
|
||||
},
|
||||
Version: &configkit.VersionRule{
|
||||
Path: configkit.Keys("version"),
|
||||
Check: checkConfigurationVersion,
|
||||
},
|
||||
Sensitive: secretProviderPath,
|
||||
Resolve: func(config *document, resolution *configkit.Resolution) error {
|
||||
for _, resources := range resourceSets(config) {
|
||||
captureFields(resources.backends, func(v backendConfig) string { return v.ID }, resources.bases.backends, resources.namespace, backendFields, resolution, state.backends)
|
||||
captureFields(resources.profiles, func(v profileConfig) string { return v.ID }, resources.bases.profiles, resources.namespace, profileFields, resolution, state.profiles)
|
||||
capturePromptMetadata(resources.prompts, resources.bases.prompts, resources.namespace, resolution, state.promptMetadata)
|
||||
resolveBackends(resources.backends, resources.bases.backends, resolution)
|
||||
resolveProfiles(resources.profiles, resources.bases.profiles, resolution)
|
||||
resolvePrompts(resources.prompts, resources.bases.prompts, resolution)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Validate: func(config *document, validation *configkit.Validation) {
|
||||
validateEffective(config, validation)
|
||||
},
|
||||
},
|
||||
Definition: configurationDefinition(state),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load configuration: %w", err)
|
||||
@@ -271,7 +274,7 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
|
||||
if err := state.buildExecutionProfiles(); err != nil {
|
||||
return nil, fmt.Errorf("load configuration: %w", err)
|
||||
}
|
||||
state.backendIDs = sortedKeys(indexedBackends(&result.Config))
|
||||
state.backendIDs = sortedKeys(indexedBackends(&state.interpreted))
|
||||
state.profileIDs = sortedKeys(state.indexedProfiles)
|
||||
for _, id := range sortedKeys(state.prompts) {
|
||||
state.promptIDs = append(state.promptIDs, domain.PromptIdentity{ID: id})
|
||||
@@ -386,48 +389,71 @@ func normalizeString(value **string, path configkit.Path, resolution *configkit.
|
||||
return false
|
||||
}
|
||||
|
||||
func resolveBackends(backends []backendConfig, basePath configkit.Path, resolution *configkit.Resolution) {
|
||||
func interpretTargets(config *document, validation *configkit.Validation) document {
|
||||
interpreted := *config
|
||||
interpreted.Backends = interpretBackends(config.Backends, configkit.Keys("backends"), validation)
|
||||
interpreted.Profiles = interpretProfiles(config.Profiles, configkit.Keys("profiles"), validation)
|
||||
if config.Collections != nil {
|
||||
interpreted.Collections = make(map[string]collectionConfig, len(config.Collections))
|
||||
for namespace, original := range config.Collections {
|
||||
collection := original
|
||||
base := configkit.Keys("collections", namespace)
|
||||
collection.Backends = interpretBackends(original.Backends, base.Key("backends"), validation)
|
||||
collection.Profiles = interpretProfiles(original.Profiles, base.Key("profiles"), validation)
|
||||
interpreted.Collections[namespace] = collection
|
||||
}
|
||||
}
|
||||
return interpreted
|
||||
}
|
||||
|
||||
func interpretBackends(source []backendConfig, basePath configkit.Path, validation *configkit.Validation) []backendConfig {
|
||||
if source == nil {
|
||||
return nil
|
||||
}
|
||||
backends := append([]backendConfig{}, source...)
|
||||
for index := range backends {
|
||||
backend := &backends[index]
|
||||
base := basePath.Index(index)
|
||||
if backend.Endpoint != nil {
|
||||
endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*backend.Endpoint)
|
||||
if err != nil {
|
||||
resolution.Error(base.Key("endpoint"), err.Error())
|
||||
validation.Error(base.Key("endpoint"), err.Error())
|
||||
} else {
|
||||
normalizeString(&backend.Endpoint, base.Key("endpoint"), resolution, endpoint)
|
||||
backend.Endpoint = &endpoint
|
||||
}
|
||||
}
|
||||
if backend.APIKeyEnv != nil {
|
||||
normalizeString(&backend.APIKeyEnv, base.Key("api_key_env"), resolution, strings.TrimSpace(*backend.APIKeyEnv))
|
||||
value := strings.TrimSpace(*backend.APIKeyEnv)
|
||||
backend.APIKeyEnv = &value
|
||||
}
|
||||
if smokeTests := backend.SmokeTests; smokeTests != nil {
|
||||
if backend.SmokeTests != nil {
|
||||
smokeTests := *backend.SmokeTests
|
||||
backend.SmokeTests = &smokeTests
|
||||
smokePath := base.Key("smoke_tests")
|
||||
if smokeTests.Reachability != nil {
|
||||
normalizeDiagnosticEndpoint(&smokeTests.Reachability.Endpoint, smokePath.Key("reachability").Key("endpoint"), resolution)
|
||||
reachability := *smokeTests.Reachability
|
||||
smokeTests.Reachability = &reachability
|
||||
normalizeDiagnosticEndpoint(&reachability.Endpoint, smokePath.Key("reachability").Key("endpoint"), validation)
|
||||
}
|
||||
if smokeTests.Authentication != nil {
|
||||
if smokeTests.Authentication.Kind != nil {
|
||||
normalizeString(&smokeTests.Authentication.Kind,
|
||||
smokePath.Key("authentication").Key("kind"), resolution,
|
||||
strings.ToLower(strings.TrimSpace(*smokeTests.Authentication.Kind)))
|
||||
authentication := *smokeTests.Authentication
|
||||
smokeTests.Authentication = &authentication
|
||||
if authentication.Kind != nil {
|
||||
kind := strings.ToLower(strings.TrimSpace(*authentication.Kind))
|
||||
authentication.Kind = &kind
|
||||
}
|
||||
normalizeDiagnosticEndpoint(&smokeTests.Authentication.Endpoint,
|
||||
smokePath.Key("authentication").Key("endpoint"), resolution)
|
||||
normalizeDiagnosticEndpoint(&authentication.Endpoint, smokePath.Key("authentication").Key("endpoint"), validation)
|
||||
}
|
||||
}
|
||||
if backend.ConcurrencyLimit != nil && *backend.ConcurrencyLimit > 0 && backend.QueueCapacity == nil {
|
||||
capacity := 1024
|
||||
if resolution.IsExplicit(base.Key("queue_capacity")) {
|
||||
configkit.Normalize(resolution, base.Key("queue_capacity"), &backend.QueueCapacity, &capacity, "select default queue capacity")
|
||||
} else {
|
||||
configkit.Default(resolution, base.Key("queue_capacity"), &backend.QueueCapacity, &capacity, "select default queue capacity")
|
||||
}
|
||||
backend.QueueCapacity = &capacity
|
||||
}
|
||||
}
|
||||
return backends
|
||||
}
|
||||
|
||||
func normalizeDiagnosticEndpoint(value **string, path configkit.Path, resolution *configkit.Resolution) {
|
||||
func normalizeDiagnosticEndpoint(value **string, path configkit.Path, validation *configkit.Validation) {
|
||||
if value == nil || *value == nil {
|
||||
return
|
||||
}
|
||||
@@ -436,37 +462,36 @@ func normalizeDiagnosticEndpoint(value **string, path configkit.Path, resolution
|
||||
if normalized == "" || err != nil || request.URL == nil ||
|
||||
(strings.ToLower(request.URL.Scheme) != "http" && strings.ToLower(request.URL.Scheme) != "https") ||
|
||||
request.URL.Hostname() == "" {
|
||||
resolution.Error(path, "diagnostic endpoint must be an absolute HTTP(S) URL with a host")
|
||||
validation.Error(path, "diagnostic endpoint must be an absolute HTTP(S) URL with a host")
|
||||
return
|
||||
}
|
||||
normalizeString(value, path, resolution, normalized)
|
||||
*value = &normalized
|
||||
}
|
||||
|
||||
func resolveProfiles(profiles []profileConfig, basePath configkit.Path, resolution *configkit.Resolution) {
|
||||
func interpretProfiles(source []profileConfig, basePath configkit.Path, validation *configkit.Validation) []profileConfig {
|
||||
if source == nil {
|
||||
return nil
|
||||
}
|
||||
profiles := append([]profileConfig{}, source...)
|
||||
for index := range profiles {
|
||||
profile := &profiles[index]
|
||||
base := basePath.Index(index)
|
||||
if profile.Endpoint != nil && *profile.Endpoint != "" {
|
||||
endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*profile.Endpoint)
|
||||
if err != nil {
|
||||
resolution.Error(base.Key("endpoint"), err.Error())
|
||||
validation.Error(base.Key("endpoint"), err.Error())
|
||||
} else {
|
||||
normalizeString(&profile.Endpoint, base.Key("endpoint"), resolution, endpoint)
|
||||
profile.Endpoint = &endpoint
|
||||
}
|
||||
}
|
||||
for _, field := range []struct {
|
||||
name string
|
||||
value **string
|
||||
}{
|
||||
{"service_tier", &profile.ServiceTier},
|
||||
{"reasoning_effort", &profile.ReasoningEffort},
|
||||
{"api_key_env", &profile.APIKeyEnv},
|
||||
} {
|
||||
if *field.value != nil {
|
||||
normalizeString(field.value, base.Key(field.name), resolution, strings.TrimSpace(**field.value))
|
||||
for _, value := range []**string{&profile.ServiceTier, &profile.ReasoningEffort, &profile.APIKeyEnv} {
|
||||
if *value != nil {
|
||||
normalized := strings.TrimSpace(**value)
|
||||
*value = &normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
return profiles
|
||||
}
|
||||
|
||||
func validateEffective(config *document, validation *configkit.Validation) {
|
||||
|
||||
@@ -229,7 +229,7 @@ func TestLoadRejectsInvalidCollectionAndResourceKeys(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRecordsBackendNormalizationAndNullDefault(t *testing.T) {
|
||||
func TestLoadInterpretsBackendNormalizationAndNullDefault(t *testing.T) {
|
||||
loaded, err := load(mapping("backend", map[string]any{"backends": []any{
|
||||
map[string]any{"id": "local", "endpoint": " HTTP://localhost:8080/v1 ", "api_key_env": " TOKEN_NAME ",
|
||||
"concurrency_limit": 2, "queue_capacity": nil},
|
||||
@@ -237,18 +237,22 @@ func TestLoadRecordsBackendNormalizationAndNullDefault(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backend := loaded.result.Config.Backends[0]
|
||||
if backend.Endpoint == nil || *backend.Endpoint != "http://localhost:8080/v1" ||
|
||||
backend.APIKeyEnv == nil || *backend.APIKeyEnv != "TOKEN_NAME" ||
|
||||
backend.QueueCapacity == nil || *backend.QueueCapacity != 1024 {
|
||||
backend, err := loaded.GetBackend("local")
|
||||
if err != nil || backend.Endpoint != "http://localhost:8080/v1" ||
|
||||
backend.APIKeyEnv != "TOKEN_NAME" || backend.QueueCapacity != 1024 {
|
||||
t.Fatalf("backend normalization/default was not applied: %+v", backend)
|
||||
}
|
||||
composed := loaded.result.Config.Backends[0]
|
||||
if composed.Endpoint == nil || *composed.Endpoint != " HTTP://localhost:8080/v1 " ||
|
||||
composed.APIKeyEnv == nil || *composed.APIKeyEnv != " TOKEN_NAME " || composed.QueueCapacity != nil {
|
||||
t.Fatalf("composed backend was changed: %+v", composed)
|
||||
}
|
||||
if loaded.backends["local"]["queue_capacity"] != fieldNull {
|
||||
t.Fatalf("explicit null was not retained: %+v", loaded.backends["local"])
|
||||
}
|
||||
field, found := loaded.result.Metadata().Lookup(configkit.Keys("backends").Index(0).Key("endpoint"))
|
||||
if !found || field.Origin.Operation != configkit.ChangeNormalization {
|
||||
t.Fatalf("endpoint normalization was not recorded at the list entry: %+v", field)
|
||||
if !found || field.Origin.Operation == configkit.ChangeNormalization || len(field.Origin.Locations) == 0 {
|
||||
t.Fatalf("endpoint origin was not preserved: %+v", field)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,8 +419,8 @@ func TestLoadDefaultsAbsentQueueCapacity(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backend := loaded.result.Config.Backends[0]
|
||||
if backend.APIKeyEnv != nil || backend.QueueCapacity == nil || *backend.QueueCapacity != 1024 || loaded.backends["local"]["queue_capacity"] != fieldAbsent {
|
||||
backend, err := loaded.GetBackend("local")
|
||||
if err != nil || backend.APIKeyEnv != "" || backend.QueueCapacity != 1024 || loaded.result.Config.Backends[0].QueueCapacity != nil || loaded.backends["local"]["queue_capacity"] != fieldAbsent {
|
||||
t.Fatalf("absent queue capacity did not receive its default: %+v", backend)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
)
|
||||
|
||||
func (state *loaded) buildExecutionProfiles() error {
|
||||
backendConfigs := indexedBackends(&state.result.Config)
|
||||
profileConfigs := indexedProfiles(&state.result.Config)
|
||||
backendConfigs := indexedBackends(&state.interpreted)
|
||||
profileConfigs := indexedProfiles(&state.interpreted)
|
||||
definitions := make(map[string]domain.Backend, len(backendConfigs))
|
||||
for _, id := range sortedKeys(backendConfigs) {
|
||||
config := backendConfigs[id]
|
||||
|
||||
Reference in New Issue
Block a user