Use entry metadata for resolution presence
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Interface Contract Remediation Implementation Plan
|
# Interface Contract Remediation Implementation Plan
|
||||||
|
|
||||||
Status: pending. All stages below remain unimplemented.
|
Status: in progress. Stage 1 is complete; Stages 2–18 remain pending.
|
||||||
Baseline: `f6d54a094a2b5ada5748686620659399dce7ebf0`.
|
Baseline: `f6d54a094a2b5ada5748686620659399dce7ebf0`.
|
||||||
Audience: a Luna coding agent executing the stages sequentially.
|
Audience: a Luna coding agent executing the stages sequentially.
|
||||||
|
|
||||||
@@ -290,7 +290,9 @@ change only defect expectations and tests tied to retired representations.
|
|||||||
|
|
||||||
Finding 1. Owners: `resolution.go`, `load_metadata.go`, resolution/provenance tests,
|
Finding 1. Owners: `resolution.go`, `load_metadata.go`, resolution/provenance tests,
|
||||||
and one root regression in `facade_compatibility_test.go` or a focused new facade
|
and one root regression in `facade_compatibility_test.go` or a focused new facade
|
||||||
test file. Status: pending.
|
test file. Status: complete. Validation:
|
||||||
|
`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Resolution|Provenance|Default' -count=1`
|
||||||
|
and `GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test . -run 'TestFacadeResolutionPresenceUsesDynamicEntryShape' -count=1`.
|
||||||
|
|
||||||
1. Add a private entry-presence helper using `resolutionInput.lookup` and its
|
1. Add a private entry-presence helper using `resolutionInput.lookup` and its
|
||||||
`FieldInfo.Explicit`. Use it in `Resolution.IsExplicit` after existing
|
`FieldInfo.Explicit`. Use it in `Resolution.IsExplicit` after existing
|
||||||
|
|||||||
63
facade_resolution_presence_test.go
Normal file
63
facade_resolution_presence_test.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package configkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFacadeResolutionPresenceUsesDynamicEntryShape(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type configuration struct {
|
||||||
|
Shape any `yaml:"shape"`
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
keyPresent bool
|
||||||
|
}{
|
||||||
|
{name: "mapping key", input: "shape: {'0': input}\n", keyPresent: true},
|
||||||
|
{name: "sequence index", input: "shape: [input]\n", keyPresent: false},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
parsed, err := configkit.ParsePath("/shape/0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = configkit.Load(configkit.Bytes("dynamic.yml", []byte(test.input)), configkit.Options[configuration]{
|
||||||
|
Definition: configkit.Definition[configuration]{
|
||||||
|
Resolve: func(_ *configuration, resolution *configkit.Resolution) error {
|
||||||
|
check := func(path configkit.Path, want bool) error {
|
||||||
|
info, found := resolution.InputInfo(path)
|
||||||
|
explicit := resolution.IsExplicit(path)
|
||||||
|
if found != want || explicit != want || (found && info.Explicit != explicit) {
|
||||||
|
return errors.New("resolution presence disagrees with dynamic entry shape")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, checkPath := range []struct {
|
||||||
|
path configkit.Path
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{path: configkit.Keys("shape", "0"), want: test.keyPresent},
|
||||||
|
{path: configkit.Keys("shape").Index(0), want: !test.keyPresent},
|
||||||
|
{path: parsed, want: true},
|
||||||
|
{path: configkit.Keys("shape", "missing"), want: false},
|
||||||
|
} {
|
||||||
|
if err := check(checkPath.path, checkPath.want); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,6 @@ const closedCallbackContextMessage = "configkit: callback context is no longer a
|
|||||||
type resolutionState struct {
|
type resolutionState struct {
|
||||||
active bool
|
active bool
|
||||||
schema *schemaDescriptor
|
schema *schemaDescriptor
|
||||||
presence map[string]inputPresence
|
|
||||||
document *document.Node
|
document *document.Node
|
||||||
input resolutionInput
|
input resolutionInput
|
||||||
records map[string]*resolutionRecord
|
records map[string]*resolutionRecord
|
||||||
@@ -78,8 +77,12 @@ func (r *Resolution) IsExplicit(path Path) bool {
|
|||||||
if !state.validPath(path) {
|
if !state.validPath(path) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
_, explicit := state.presence[path.String()]
|
return state.inputExplicit(path)
|
||||||
return explicit
|
}
|
||||||
|
|
||||||
|
func (state *resolutionState) inputExplicit(path Path) bool {
|
||||||
|
info, found := state.input.lookup(path)
|
||||||
|
return found && info.Explicit
|
||||||
}
|
}
|
||||||
|
|
||||||
// InputInfo returns detached entry-state metadata for path. The entry state is
|
// InputInfo returns detached entry-state metadata for path. The entry state is
|
||||||
@@ -134,7 +137,7 @@ func Default[V any](resolution *Resolution, path Path, target *V, value V, reaso
|
|||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, explicit := state.presence[path.String()]; explicit {
|
if state.inputExplicit(path) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
*target = value
|
*target = value
|
||||||
@@ -282,7 +285,7 @@ func (state *resolutionState) addDiagnostic(code Code, path Path, message string
|
|||||||
|
|
||||||
func runResolution[T any](configuration *T, resolver func(*T, *Resolution) error, schema *schemaDescriptor, decoded decodedConfiguration, input *document.Node, entry resolutionInput) resolutionResult {
|
func runResolution[T any](configuration *T, resolver func(*T, *Resolution) error, schema *schemaDescriptor, decoded decodedConfiguration, input *document.Node, entry resolutionInput) resolutionResult {
|
||||||
state := &resolutionState{
|
state := &resolutionState{
|
||||||
active: true, schema: schema, presence: decoded.presence, document: input,
|
active: true, schema: schema, document: input,
|
||||||
input: entry,
|
input: entry,
|
||||||
records: make(map[string]*resolutionRecord),
|
records: make(map[string]*resolutionRecord),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,6 +311,85 @@ func TestResolutionRejectsInvalidProjectedValuesAndSuppressesReturnedErrors(t *t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolutionPresenceUsesEntryShapeAcrossDynamicChanges(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Dynamic any `yaml:"dynamic"`
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
keyPresent bool
|
||||||
|
}{
|
||||||
|
{name: "mapping to sequence", input: "dynamic: {'0': input}\n", keyPresent: true},
|
||||||
|
{name: "sequence to mapping", input: "dynamic: [input]\n", keyPresent: false},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
parsed, err := ParsePath("/dynamic/0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var callbackErr error
|
||||||
|
result, err := testLoad(Bytes("dynamic.yml", []byte(test.input)), Options[config]{Definition: Definition[config]{
|
||||||
|
Resolve: func(configuration *config, resolution *Resolution) error {
|
||||||
|
check := func(path Path, want bool) error {
|
||||||
|
info, found := resolution.InputInfo(path)
|
||||||
|
explicit := resolution.IsExplicit(path)
|
||||||
|
if found != want || explicit != want || (found && info.Explicit != explicit) {
|
||||||
|
return errors.New("resolution presence disagrees with entry shape")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
keyPath := Keys("dynamic", "0")
|
||||||
|
indexPath := Keys("dynamic").Index(0)
|
||||||
|
for _, checkPath := range []struct {
|
||||||
|
path Path
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{path: keyPath, want: test.keyPresent},
|
||||||
|
{path: indexPath, want: !test.keyPresent},
|
||||||
|
{path: parsed, want: true},
|
||||||
|
{path: Keys("dynamic", "missing"), want: false},
|
||||||
|
} {
|
||||||
|
if err := check(checkPath.path, checkPath.want); err != nil {
|
||||||
|
callbackErr = err
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolution.Record(Keys("dynamic"), ChangeNormalization, "change dynamic shape")
|
||||||
|
child := ""
|
||||||
|
if test.keyPresent {
|
||||||
|
configuration.Dynamic = []any{}
|
||||||
|
Default(resolution, indexPath, &child, "derived", "default new sequence child")
|
||||||
|
configuration.Dynamic = []any{child}
|
||||||
|
} else {
|
||||||
|
configuration.Dynamic = map[string]any{}
|
||||||
|
Default(resolution, keyPath, &child, "derived", "default new mapping child")
|
||||||
|
configuration.Dynamic = map[string]any{"0": child}
|
||||||
|
}
|
||||||
|
if child != "derived" {
|
||||||
|
callbackErr = errors.New("default was suppressed by the old container shape")
|
||||||
|
return callbackErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
if callbackErr != nil {
|
||||||
|
t.Fatal(callbackErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(result.resolution) != 2 || result.resolution[1].change.Origin.Operation != ChangeDerivedDefault {
|
||||||
|
t.Fatalf("resolution records = %#v", result.resolution)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolutionEntryQueriesExposeDetachedLoadInput(t *testing.T) {
|
func TestResolutionEntryQueriesExposeDetachedLoadInput(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user