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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -168,6 +168,8 @@ syntax section. Presence-sensitive cases belong to Stage 3.
|
||||
|
||||
## Stage 3 — Preserve Presence And Existing Domain Validation
|
||||
|
||||
> Completion: complete.
|
||||
|
||||
Goal: ensure inherited input remains explicit through Notarius normalization,
|
||||
defaulting, and validator-chain conversion.
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
@@ -131,3 +134,200 @@ pipelines:
|
||||
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