Check source versions against original document controls

This commit is contained in:
2026-11-02 00:37:45 +00:00
parent 1fd0adc571
commit ad5b859337
5 changed files with 102 additions and 46 deletions

View File

@@ -139,6 +139,8 @@ defaults and resolution must not gain a new schema/key rejection.
## Stage 4 — Verify source-version and acquisition integration
**Status:** Complete. Focused source-version and declaration-overlap tests pass.
**Owners:** [source_version.go](../../internal/engine/source_version.go),
`processAcquiredInputVersion` in acquisition, import processing, and existing
source-version/item-policy tests.

View File

@@ -205,7 +205,7 @@ func processAcquiredInputVersion(parsed parsedSource, request inputAcquisitionRe
hasResult := false
updateMainProcessing := false
switch request.directiveContext {
case rootDirectiveContextSource:
case rootDirectiveContextSource, rootDirectiveContextLayered:
if state != nil {
processed, diagnostics := state.processWithDirectiveContext(
parsed.document, request.role, request.input.destination, sourceID,
@@ -226,30 +226,6 @@ func processAcquiredInputVersion(parsed parsedSource, request inputAcquisitionRe
}
}
hasResult = true
case rootDirectiveContextLayered:
extracted, diagnostics := extractRootDirectives(parsed.document, request.directiveContext)
if len(diagnostics) != 0 {
return nil, diagnostics
}
result = sourceVersionProcessingResult{
payload: extracted.body, imports: extracted.imports, sequences: extracted.sequences,
directiveContext: request.directiveContext,
}
hasResult = true
if state != nil && state.mode != sourceVersionModeDefault {
processed, diagnostics := state.process(
extracted.body, request.role, request.input.destination,
sourceID, request.input.placementMode,
)
if len(diagnostics) != 0 {
return nil, diagnostics
}
processed.imports = extracted.imports
processed.sequences = append(cloneSequenceDeclarations(extracted.sequences), processed.sequences...)
processed.directiveContext = request.directiveContext
result = processed
updateMainProcessing = request.role == sourceVersionMain
}
default:
if state != nil && state.mode != sourceVersionModeDefault {
processed, diagnostics := state.process(

View File

@@ -458,7 +458,7 @@ func TestItemSourceVersionProcessingConsumesDeclarationsBeforeComposition(t *tes
t.Parallel()
root := parseItemPolicyDocument(t, "item", `version: v1
configkit:
config:
sequences:
- destination: [prompts]
key: id
@@ -483,7 +483,7 @@ prompts:
if _, ok := documentAtPath(processed.payload, Keys("version")); ok {
t.Fatalf("consumed version header remains in item payload: %#v", processed.payload)
}
if _, ok := documentAtPath(processed.payload, Keys("configkit")); ok {
if _, ok := documentAtPath(processed.payload, Keys("config")); ok {
t.Fatalf("consumed controls remain in item payload: %#v", processed.payload)
}
}

View File

@@ -126,13 +126,13 @@ func TestParseSequenceDeclarationRejectsMalformedForms(t *testing.T) {
}
func TestSequenceDeclarationsDoNotTriggerVersionOverlapWhenHeaderIsAbsent(t *testing.T) {
root := parseRootDirectiveDocument(t, `configkit:
root := parseRootDirectiveDocument(t, `config:
sequences:
- destination: [prompts]
key: id
version: v1
`)
presentPath, err := ParsePath("/configkit/sequences/0/destination")
presentPath, err := ParsePath("/config/sequences/0/destination")
if err != nil {
t.Fatal(err)
}
@@ -140,7 +140,7 @@ version: v1
if !sourceVersionHeaderControlOverlap(root, present) {
t.Fatal("present version header beneath a sequence control did not overlap")
}
absentPath, err := ParsePath("/configkit/sequences/1/destination")
absentPath, err := ParsePath("/config/sequences/1/destination")
if err != nil {
t.Fatal(err)
}

View File

@@ -24,7 +24,7 @@ func TestDefaultMainVersionWarningSurvivesLayeredLoading(t *testing.T) {
}{
{name: "ordinary", ordinary: true},
{name: "layered", lower: "{}"},
{name: "later structural failure", body: "configkit:\n sequences: [{destination: [items], key: id}]\nitems: []\n", lower: "items: null", failure: true},
{name: "later structural failure", body: "config:\n sequences: [{destination: [items], key: id}]\nitems: []\n", lower: "items: null", failure: true},
{name: "later decoding failure", body: "count: invalid\n", lower: "{}", failure: true},
} {
t.Run(test.name, func(t *testing.T) {
@@ -433,8 +433,8 @@ func TestSourceVersionExtractionRejectsStructuralOverlap(t *testing.T) {
wantCode Code
}{
{
name: "inside import declaration", input: "configkit:\n imports:\n - path: child.yml\n",
path: Keys("configkit", "imports").Index(0).Key("path"), wantCode: CodeVersionError,
name: "inside import declaration", input: "config:\n imports:\n - path: child.yml\n",
path: Keys("config", "imports").Index(0).Key("path"), wantCode: CodeVersionError,
},
{
name: "extends control", input: "settings:\n extends: parent\n",
@@ -445,8 +445,8 @@ func TestSourceVersionExtractionRejectsStructuralOverlap(t *testing.T) {
path: Keys("settings", "extends", "version"), wantCode: CodeVersionError,
},
{
name: "malformed directive cannot be consumed", input: "configkit:\n imports: true\n",
path: Keys("configkit", "imports"), wantCode: CodeImportInvalid,
name: "malformed directive cannot be consumed", input: "config:\n imports: true\n",
path: Keys("config", "imports"), wantCode: CodeImportInvalid,
},
}
for _, test := range tests {
@@ -471,7 +471,7 @@ func TestSourceVersionExtractionRejectsStructuralOverlap(t *testing.T) {
})
}
unrelatedRoot := sourceVersionTestDocument(t, "configkit:\n imports:\n - path: child.yml\nmetadata:\n version: 4\n")
unrelatedRoot := sourceVersionTestDocument(t, "config:\n imports:\n - path: child.yml\nmetadata:\n version: 4\n")
unrelatedResult := processSourceVersion(unrelatedRoot, &VersionRule{
Path: Keys("metadata", "version"), Check: func(Version, *Validation) error { return nil },
}, sourceVersionContribution, Path{}, sourceVersionModeIfPresent)
@@ -489,6 +489,84 @@ func TestSourceVersionExtractionRejectsStructuralOverlap(t *testing.T) {
}
}
func TestSourceVersionAllowsNestedConfigData(t *testing.T) {
root := sourceVersionTestDocument(t, "service: {config: {version: 4, imports: ordinary}}\n")
called := false
result := processSourceVersion(root, &VersionRule{
Path: Keys("service", "config", "version"),
Check: func(version Version, _ *Validation) error {
called = version.Present && version.Text == "4"
return nil
},
}, sourceVersionContribution, Path{}, sourceVersionModeIfPresent)
if len(result.diagnostics) != 0 || !called || result.payload == nil {
t.Fatalf("nested version result = %#v, checked = %t", result, called)
}
if _, found := documentAtPath(result.payload, Keys("service", "config", "version")); found {
t.Fatal("accepted contribution header was not consumed")
}
}
func TestSourceVersionMountedConfigKeepsLocalAndEffectivePaths(t *testing.T) {
type config struct {
Config struct {
Version string `yaml:"version"`
} `yaml:"config"`
}
checks := 0
definition := Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: func(version Version, _ *Validation) error {
checks++
if !version.Present || version.Text != "4" {
return errors.New("unexpected version")
}
return nil
}}}
main := Input{Source: Bytes("main", []byte("version: 4\n")), Destination: Keys("config")}
loaded, err := LoadLayered(main, testSecretBinding(), LayeredOptions[config]{Definition: definition, SourceVersionMode: sourceVersionModeRequired})
if err != nil || loaded == nil || loaded.Config.Config.Version != "4" || checks != 1 {
t.Fatalf("mounted version = %#v, checks %d, error %v", loaded, checks, err)
}
_, err = LoadLayered(main, testSecretBinding(), LayeredOptions[config]{Definition: definition, SourceVersionMode: sourceVersionModeRequired,
Overrides: []Override{{Path: Keys("config", "version"), Value: "5", Flag: "--version"}},
})
var loadErr *LoadError
if !errors.As(err, &loadErr) {
t.Fatalf("protected override error = %v", err)
}
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeOverrideVersion, "/config/version")
}
func TestSourceVersionLayeredRejectsControlHeaderBeforeChecking(t *testing.T) {
called := false
definition := Definition[map[string]any]{Version: &VersionRule{
Path: Keys("config", "imports").Index(0).Key("path"),
Check: func(Version, *Validation) error { called = true; return nil },
}}
mainPath := writeImportFile(t, t.TempDir(), "main.yml", "config: {imports: [{path: child.yml}]}\n")
_, err := LoadLayered(Input{Source: File(mainPath), Imports: true},
testSecretBinding(), LayeredOptions[map[string]any]{Definition: definition, SourceVersionMode: sourceVersionModeIfPresent})
var loadErr *LoadError
if !errors.As(err, &loadErr) || called {
t.Fatalf("overlap error = %v, checker called = %t", err, called)
}
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeVersionError, "/config/imports/0/path")
}
func TestSourceVersionDefaultModeChecksAssembledConfigPath(t *testing.T) {
checked := false
definition := Definition[map[string]any]{Version: &VersionRule{
Path: Keys("config", "version"), Check: func(version Version, _ *Validation) error {
checked = version.Present && version.Text == "4"
return nil
},
}}
loaded, err := LoadLayered(Input{Source: Bytes("main", []byte("version: 4\n")), Destination: Keys("config")},
testSecretBinding(), LayeredOptions[map[string]any]{Definition: definition})
if err != nil || loaded == nil || !checked {
t.Fatalf("assembled version = %#v, checked = %t, error = %v", loaded, checked, err)
}
}
func TestSourceVersionDefaultModeDoesNotCheckOrConsume(t *testing.T) {
t.Parallel()
@@ -858,13 +936,13 @@ func TestSourceVersionImportsCheckNestedAndGlobSourcesBeforeComposition(t *testi
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `version: main
configkit:
config:
imports:
- glob: fragments/*.yml
destination: [groups]
`)
firstPath := writeImportFile(t, directory, "fragments/a.yml", `version: first
configkit:
config:
imports:
- path: nested/child.yml
destination: [nested]
@@ -935,7 +1013,7 @@ func TestSourceVersionImportsRejectHiddenAndMissingDeclarations(t *testing.T) {
t.Run("rejected child is not hidden by the main declaration", func(t *testing.T) {
t.Parallel()
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "version: main\nconfigkit: {imports: [{path: child.yml}]}\n")
mainPath := writeImportFile(t, directory, "main.yml", "version: main\nconfig: {imports: [{path: child.yml}]}\n")
writeImportFile(t, directory, "child.yml", "version: rejected\n")
checks := 0
_, err := Load(File(mainPath), testSecretBinding(), Options[sourceVersionNullConfig]{
@@ -963,7 +1041,7 @@ func TestSourceVersionImportsRejectHiddenAndMissingDeclarations(t *testing.T) {
t.Run("required child omission reports its nearest source location", func(t *testing.T) {
t.Parallel()
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "version: main\nconfigkit: {imports: [{path: child.yml}]}\n")
mainPath := writeImportFile(t, directory, "main.yml", "version: main\nconfig: {imports: [{path: child.yml}]}\n")
writeImportFile(t, directory, "child.yml", "name: child\n")
checks := 0
_, err := Load(File(mainPath), testSecretBinding(), Options[sourceVersionNullConfig]{
@@ -986,7 +1064,7 @@ func TestSourceVersionImportsRejectHiddenAndMissingDeclarations(t *testing.T) {
t.Parallel()
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `version: main
configkit:
config:
imports:
- path: missing.yml
`)
@@ -1144,7 +1222,7 @@ func TestSourceVersionUnderlayImportsKeepLocalHeadersAndMountedDestinations(t *t
directory := t.TempDir()
underlayPath := writeImportFile(t, directory, "underlay.yml", `meta: {version: peer-root}
configkit:
config:
imports:
- path: nested.yml
destination: [nested]
@@ -1296,7 +1374,7 @@ func TestSourceVersionSelectedOverlayConsumesHeaderAndRetainsWarning(t *testing.
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: main-v}\nsettings: {kept: base}\nitems: [{id: base}]\n")
overlayPath := writeImportFile(t, directory, "overlays/testing.yml", `schema: {version: overlay-v}
configkit:
config:
sequences:
- destination: [items]
key: id
@@ -1327,7 +1405,7 @@ items: [{id: overlay}]
if loaded.Config["schema"].(map[string]any)["version"] != "main-v" || loaded.Config["settings"].(map[string]any)["changed"] != "overlay" {
t.Fatalf("overlay payload = %#v", loaded.Config)
}
if _, found := loaded.Config["configkit"]; found || len(loaded.Config["items"].([]any)) != 2 {
if _, found := loaded.Config["config"]; found || len(loaded.Config["items"].([]any)) != 2 {
t.Fatalf("overlay declarations or collection values = %#v", loaded.Config)
}
diagnostics := loaded.Inspection.Diagnostics()
@@ -1395,7 +1473,7 @@ func TestSourceVersionSelectedOverlayRequiresAndChecksItsDeclaration(t *testing.
t.Run("structural validation precedes declaration processing", func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: main-v}\n")
writeImportFile(t, directory, "overlays/testing.yml", "configkit: {imports: true}\nschema: {version: overlay-v}\n")
writeImportFile(t, directory, "overlays/testing.yml", "config: {imports: true}\nschema: {version: overlay-v}\n")
checks := 0
loaded, err := Load(File(mainPath), testSecretBinding(), Options[map[string]any]{
Overlay: "testing", SourceVersionMode: sourceVersionModeRequired,
@@ -1513,7 +1591,7 @@ func TestSourceVersionImportAccountingIncludesConsumedHeaderNodes(t *testing.T)
t.Parallel()
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: child.yml}]}\n")
mainPath := writeImportFile(t, directory, "main.yml", "config: {imports: [{path: child.yml}]}\n")
childPath := writeImportFile(t, directory, "child.yml", "version: accepted\nname: child\n")
main := parseImportMain(t, mainPath)
child, diagnostics := parseSource(File(childPath), "source-child", defaultParseLimits())