Preserve inherited presence and validation
This commit is contained in:
@@ -121,6 +121,14 @@ sequence, so a step cannot extend another step. Replacing `steps` or a validator
|
|||||||
chain supplies the complete replacement sequence; entries are not appended or
|
chain supplies the complete replacement sequence; entries are not appended or
|
||||||
merged by identifier.
|
merged by identifier.
|
||||||
|
|
||||||
|
Inherited values count as explicit input for Notarius's existing
|
||||||
|
presence-sensitive rules: zero, false, and empty-list overrides stay explicit
|
||||||
|
instead of restoring a default. Null also replaces the inherited value, but
|
||||||
|
each field's ordinary type and validation rules still apply; null is not
|
||||||
|
universally valid. Omitting `extends` preserves a relationship supplied by a
|
||||||
|
lower-priority source, while `extends: null` cancels that relationship. It
|
||||||
|
does not independently clear other inherited fields.
|
||||||
|
|
||||||
`extends` is now a reserved key in every input mapping, including dynamic
|
`extends` is now a reserved key in every input mapping, including dynamic
|
||||||
module options and named maps. Existing ordinary data or entry names using that
|
module options and named maps. Existing ordinary data or entry names using that
|
||||||
key must be renamed along with any consumer that reads them. Configuration
|
key must be renamed along with any consumer that reads them. Configuration
|
||||||
|
|||||||
@@ -168,6 +168,8 @@ syntax section. Presence-sensitive cases belong to Stage 3.
|
|||||||
|
|
||||||
## Stage 3 — Preserve Presence And Existing Domain Validation
|
## Stage 3 — Preserve Presence And Existing Domain Validation
|
||||||
|
|
||||||
|
> Completion: complete.
|
||||||
|
|
||||||
Goal: ensure inherited input remains explicit through Notarius normalization,
|
Goal: ensure inherited input remains explicit through Notarius normalization,
|
||||||
defaulting, and validator-chain conversion.
|
defaulting, and validator-chain conversion.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
@@ -131,3 +134,200 @@ pipelines:
|
|||||||
t.Fatalf("Resolve() error = %v", err)
|
t.Fatalf("Resolve() error = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInheritedPresenceOverridesRemainExplicit(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
loaded, err := loadConfigSource(configkit.Bytes("presence.yml", []byte(`version: 5
|
||||||
|
pipelines:
|
||||||
|
parent:
|
||||||
|
input: {module: input}
|
||||||
|
structured_output_repair_attempts: 3
|
||||||
|
chunk:
|
||||||
|
module: generic
|
||||||
|
validators: []
|
||||||
|
artifacts:
|
||||||
|
score:
|
||||||
|
extract:
|
||||||
|
module: extract
|
||||||
|
validators: [{module: configured-a}]
|
||||||
|
child:
|
||||||
|
extends: parent
|
||||||
|
structured_output_repair_attempts: 0
|
||||||
|
artifacts:
|
||||||
|
score:
|
||||||
|
extract:
|
||||||
|
validators: []
|
||||||
|
`)), LoadOptions{})
|
||||||
|
if err != nil {
|
||||||
|
var loadError *configkit.LoadError
|
||||||
|
if errors.As(err, &loadError) {
|
||||||
|
t.Fatalf("loadConfigSource() diagnostics = %#v", loadError.Diagnostics())
|
||||||
|
}
|
||||||
|
t.Fatalf("loadConfigSource() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := loaded.Config.Pipelines["parent"]
|
||||||
|
child := loaded.Config.Pipelines["child"]
|
||||||
|
if parent.Chunk.Module != "generic" || !parent.Chunk.Validators.Set || parent.Chunk.Validators.Validators == nil || len(parent.Chunk.Validators.Validators) != 0 {
|
||||||
|
t.Fatalf("parent empty chunk validators = %#v", parent.Chunk)
|
||||||
|
}
|
||||||
|
if !parent.Artifacts["score"].Extract.Validators.Set || len(parent.Artifacts["score"].Extract.Validators.Validators) != 1 ||
|
||||||
|
parent.Artifacts["score"].Extract.Validators.Validators[0].Module != "configured-a" {
|
||||||
|
t.Fatalf("parent extraction validators = %#v", parent.Artifacts["score"].Extract.Validators)
|
||||||
|
}
|
||||||
|
if parent.StructuredOutputRepairAttempts == nil || *parent.StructuredOutputRepairAttempts != 3 {
|
||||||
|
t.Fatalf("parent repair budget = %#v", parent.StructuredOutputRepairAttempts)
|
||||||
|
}
|
||||||
|
if child.StructuredOutputRepairAttempts == nil || *child.StructuredOutputRepairAttempts != 0 {
|
||||||
|
t.Fatalf("child zero repair budget = %#v", child.StructuredOutputRepairAttempts)
|
||||||
|
}
|
||||||
|
extract := child.Artifacts["score"].Extract
|
||||||
|
if !extract.Validators.Set || extract.Validators.Validators == nil || len(extract.Validators.Validators) != 0 {
|
||||||
|
t.Fatalf("child empty extraction validators = %#v", extract.Validators)
|
||||||
|
}
|
||||||
|
if !child.Chunk.Validators.Set || child.Chunk.Validators.Validators == nil || len(child.Chunk.Validators.Validators) != 0 {
|
||||||
|
t.Fatalf("inherited empty chunk validators = %#v", child.Chunk.Validators)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved, err := loaded.Config.Resolve(ResolveInput{PipelineID: "child", Catalog: effectiveCatalog(t)})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Resolve(child) error = %v", err)
|
||||||
|
}
|
||||||
|
lanes := resolved.ResolvedPipeline.AllArtifactLanes()
|
||||||
|
if len(lanes) != 1 || lanes[0].Extract.StructuredOutputRepairAttempts == nil || *lanes[0].Extract.StructuredOutputRepairAttempts != 0 {
|
||||||
|
t.Fatalf("resolved child repair budget = %#v", lanes)
|
||||||
|
}
|
||||||
|
if !resolved.ResolvedPipeline.Chunk.Validators.Set || len(resolved.ResolvedPipeline.Chunk.Validators.Validators) != 0 ||
|
||||||
|
!lanes[0].Extract.Validators.Set || len(lanes[0].Extract.Validators.Validators) != 0 {
|
||||||
|
t.Fatalf("resolved empty validator overrides = chunk %#v, extraction %#v", resolved.ResolvedPipeline.Chunk.Validators, lanes[0].Extract.Validators)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInheritedFalseOptionRetainsExplicitMetadata(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
loaded, err := loadConfigSource(configkit.Bytes("false-option.yml", []byte(`version: 5
|
||||||
|
pipelines:
|
||||||
|
parent:
|
||||||
|
input: {module: input}
|
||||||
|
output:
|
||||||
|
module: json
|
||||||
|
options:
|
||||||
|
evidence_context: {enabled: false}
|
||||||
|
child:
|
||||||
|
extends: parent
|
||||||
|
`)), LoadOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("loadConfigSource() error = %v", err)
|
||||||
|
}
|
||||||
|
options := loaded.Config.Pipelines["child"].Output.Options["evidence_context"].(map[string]any)
|
||||||
|
if enabled, ok := options["enabled"].(bool); !ok || enabled {
|
||||||
|
t.Fatalf("inherited evidence_context.enabled = %#v", options["enabled"])
|
||||||
|
}
|
||||||
|
path := configkit.Keys("pipelines", "child", "output", "options", "evidence_context", "enabled")
|
||||||
|
info, ok := loaded.result.Metadata().Lookup(path)
|
||||||
|
if !ok || !info.Explicit || info.Null || len(info.Origin.Inheritance) == 0 {
|
||||||
|
t.Fatalf("inherited false metadata = %#v, found=%v", info, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInheritedAndDirectNullRepairBudgetsRemainInvalid(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
source string
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "child null",
|
||||||
|
source: `version: 5
|
||||||
|
pipelines:
|
||||||
|
parent: {input: {module: input}, structured_output_repair_attempts: 2}
|
||||||
|
child:
|
||||||
|
extends: parent
|
||||||
|
structured_output_repair_attempts: null
|
||||||
|
`,
|
||||||
|
path: "/pipelines/child/structured_output_repair_attempts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inherited null",
|
||||||
|
source: `version: 5
|
||||||
|
pipelines:
|
||||||
|
parent: {input: {module: input}, structured_output_repair_attempts: null}
|
||||||
|
child: {extends: parent}
|
||||||
|
`,
|
||||||
|
path: "/pipelines/parent/structured_output_repair_attempts",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
_, err := loadConfigSource(configkit.Bytes("null.yml", []byte(test.source)), LoadOptions{})
|
||||||
|
assertDocumentDiagnosticAt(t, err, test.path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOverlayAndOverridesComposeBeforeInheritance(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
root := t.TempDir()
|
||||||
|
overlays := filepath.Join(root, "overlays")
|
||||||
|
if err := os.MkdirAll(overlays, 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
main := filepath.Join(root, "config.yml")
|
||||||
|
if err := os.WriteFile(main, []byte(`version: 5
|
||||||
|
pipelines:
|
||||||
|
parent:
|
||||||
|
input: {module: input}
|
||||||
|
structured_output_repair_attempts: 1
|
||||||
|
inherited:
|
||||||
|
extends: parent
|
||||||
|
overridden:
|
||||||
|
extends: parent
|
||||||
|
structured_output_repair_attempts: 0
|
||||||
|
cancelled:
|
||||||
|
extends: parent
|
||||||
|
input: {module: input}
|
||||||
|
`), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(overlays, "development.yml"), []byte(`pipelines:
|
||||||
|
parent:
|
||||||
|
structured_output_repair_attempts: 2
|
||||||
|
cancelled:
|
||||||
|
extends: null
|
||||||
|
`), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded, err := Load(main, LoadOptions{Overlay: "development"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error = %v", err)
|
||||||
|
}
|
||||||
|
if got := *loaded.Config.Pipelines["inherited"].StructuredOutputRepairAttempts; got != 2 {
|
||||||
|
t.Fatalf("overlay-inherited repair budget = %d, want 2", got)
|
||||||
|
}
|
||||||
|
if got := *loaded.Config.Pipelines["overridden"].StructuredOutputRepairAttempts; got != 0 {
|
||||||
|
t.Fatalf("child replacement repair budget = %d, want 0", got)
|
||||||
|
}
|
||||||
|
if loaded.Config.Pipelines["cancelled"].StructuredOutputRepairAttempts != nil {
|
||||||
|
t.Fatalf("cancelled child inherited parent settings: %#v", loaded.Config.Pipelines["cancelled"])
|
||||||
|
}
|
||||||
|
|
||||||
|
withOverride, err := Load(main, LoadOptions{
|
||||||
|
Overlay: "development",
|
||||||
|
Overrides: []configkit.Override{{
|
||||||
|
Path: configkit.Keys("pipelines", "parent", "structured_output_repair_attempts"),
|
||||||
|
Value: 3,
|
||||||
|
Flag: "--repair-budget",
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() with explicit override error = %v", err)
|
||||||
|
}
|
||||||
|
if got := *withOverride.Config.Pipelines["inherited"].StructuredOutputRepairAttempts; got != 3 {
|
||||||
|
t.Fatalf("override-inherited repair budget = %d, want 3", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user