Check source versions in selected overlays
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Implement shared source version checks
|
||||
|
||||
Status: Stages 1–6 complete; selected overlay integration has not started.
|
||||
Status: Stages 1–7 complete; public load-wide mode integration has not started.
|
||||
|
||||
Audience: a Luna coding agent implementing one numbered stage per prompt, in
|
||||
order. The [feature roadmap](source-version-checks.md) owns target behavior and
|
||||
@@ -416,6 +416,27 @@ Validate: overlay/override/inheritance/version tests and everyday check. Done:
|
||||
all document roles are integrated privately; synthetic operations are exempt
|
||||
from header checks, not from existing protection.
|
||||
|
||||
Completion evidence: threaded the shared version state through ordinary and
|
||||
layered selected-overlay loading. After parse and overlay-directive validation,
|
||||
the selected overlay is checked as a non-main source; accepted headers are
|
||||
removed from its owned payload before effective-path protection and application.
|
||||
The original parsed node still supplies raw byte/node charges, and source
|
||||
identity, filesystem namespace, closure, overlay-control checks, and override
|
||||
protection remain on their existing paths. Overlay warnings are accumulated with
|
||||
the other per-load diagnostics. Added coverage for accepted, required-missing,
|
||||
and rejected overlay declarations; structural validation before callbacks;
|
||||
warning retention; CLI overrides without document-header checks; mounted-main
|
||||
protection after header consumption; and sequence deletion retaining original
|
||||
evidence coordinates while later payload indices shift. Existing default overlay
|
||||
version/directive/accounting coverage remains passing. These checks passed with
|
||||
Go `go1.26.8 linux/amd64`:
|
||||
|
||||
- `GOTOOLCHAIN=local GOPROXY=off go test ./internal/engine -run 'SourceVersion|Version|Import|Underlay|Overlay|Override|Inheritance|Destination' -count=1`
|
||||
- `GOTOOLCHAIN=local ./scripts/check.sh`
|
||||
- `git diff --check`
|
||||
|
||||
No environmental limitation affected validation; checks ran on Linux/amd64.
|
||||
|
||||
## Stage 8 — Expose the public load-wide mode
|
||||
|
||||
1. Add root mode/constants/option fields and both conversions from decision 1.
|
||||
|
||||
@@ -142,9 +142,9 @@ func applyInputLayers[T any](source Source, options Options[T], input assembledI
|
||||
}
|
||||
assembly := newAssemblyAccumulator(composition)
|
||||
if activeOverlay(options.Overlay) {
|
||||
input.sources, diagnostics = loadSelectedOverlayInto(
|
||||
input.sources, diagnostics = loadSelectedOverlayIntoWithVersion(
|
||||
source, options.Filesystem, input.parsed, assembly, input.sources,
|
||||
options.Overlay, options.Definition.Version, defaultImportLimits(),
|
||||
options.Overlay, options.Definition.Version, defaultImportLimits(), input.versionState,
|
||||
)
|
||||
if len(diagnostics) != 0 {
|
||||
return input, diagnostics
|
||||
|
||||
@@ -126,7 +126,7 @@ func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, a
|
||||
selection := Source{kind: sourceBytes}
|
||||
selected := parsedSource{document: &document.Node{Kind: document.Mapping}, source: &SourceInfo{}}
|
||||
var diagnostics []Diagnostic
|
||||
input.sources, diagnostics = loadSelectedOverlayInto(selection, request.overlayFilesystem, selected, assembly, input.sources, request.overlay, request.protectionVersion(), defaultImportLimits())
|
||||
input.sources, diagnostics = loadSelectedOverlayIntoWithVersion(selection, request.overlayFilesystem, selected, assembly, input.sources, request.overlay, request.protectionVersion(), defaultImportLimits(), input.versionState)
|
||||
if len(diagnostics) != 0 {
|
||||
return input, diagnostics
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ func loadSelectedOverlayDocument(source Source, context *FilesystemContext, main
|
||||
}
|
||||
|
||||
func loadSelectedOverlayInto(source Source, context *FilesystemContext, main parsedSource, base *assemblyAccumulator, sources []SourceInfo, name string, rule *VersionRule, limits importLimits) ([]SourceInfo, []Diagnostic) {
|
||||
return loadSelectedOverlayIntoWithVersion(source, context, main, base, sources, name, rule, limits, nil)
|
||||
}
|
||||
|
||||
func loadSelectedOverlayIntoWithVersion(source Source, context *FilesystemContext, main parsedSource, base *assemblyAccumulator, sources []SourceInfo, name string, rule *VersionRule, limits importLimits, versionState *sourceVersionState) ([]SourceInfo, []Diagnostic) {
|
||||
if nativeContext(source, context) {
|
||||
access, basePath, mainPath, diagnostic := nativeFilesystemAccess(source, context)
|
||||
if diagnostic != nil {
|
||||
@@ -46,7 +50,7 @@ func loadSelectedOverlayInto(source Source, context *FilesystemContext, main par
|
||||
if diagnostic := validateNativeDirectories(access, basePath, mainPath); diagnostic != nil {
|
||||
return cloneSourceInfos(sources), []Diagnostic{*diagnostic}
|
||||
}
|
||||
return loadSelectedOverlayWithAccess(access, base, sources, name, rule, limits)
|
||||
return loadSelectedOverlayWithAccessAndVersion(access, base, sources, name, rule, limits, versionState)
|
||||
}
|
||||
rootPath, _, _, diagnostic := importFilesystem(source, context, main)
|
||||
if diagnostic != nil {
|
||||
@@ -58,10 +62,14 @@ func loadSelectedOverlayInto(source Source, context *FilesystemContext, main par
|
||||
value.Primary = nil
|
||||
return cloneSourceInfos(sources), []Diagnostic{value}
|
||||
}
|
||||
return loadSelectedOverlayWithAccess(access, base, sources, name, rule, limits)
|
||||
return loadSelectedOverlayWithAccessAndVersion(access, base, sources, name, rule, limits, versionState)
|
||||
}
|
||||
|
||||
func loadSelectedOverlayWithAccess(access *filesystemAccess, base *assemblyAccumulator, sources []SourceInfo, name string, rule *VersionRule, limits importLimits) ([]SourceInfo, []Diagnostic) {
|
||||
return loadSelectedOverlayWithAccessAndVersion(access, base, sources, name, rule, limits, nil)
|
||||
}
|
||||
|
||||
func loadSelectedOverlayWithAccessAndVersion(access *filesystemAccess, base *assemblyAccumulator, sources []SourceInfo, name string, rule *VersionRule, limits importLimits, versionState *sourceVersionState) ([]SourceInfo, []Diagnostic) {
|
||||
closed := false
|
||||
defer func() {
|
||||
if !closed {
|
||||
@@ -126,7 +134,15 @@ func loadSelectedOverlayWithAccess(access *filesystemAccess, base *assemblyAccum
|
||||
if diagnostics := overlayDirectiveDiagnostics(overlayRoot, Path{}); len(diagnostics) != 0 {
|
||||
return sources, diagnostics
|
||||
}
|
||||
if versionNode := fragmentVersionClaim(overlayRoot, rule); versionNode != nil {
|
||||
overlayPayload := overlayRoot
|
||||
if versionState != nil && versionState.mode != sourceVersionModeDefault {
|
||||
processed, failures := versionState.process(overlayRoot, sourceVersionContribution, Path{}, sourceID)
|
||||
if len(failures) != 0 {
|
||||
return sources, failures
|
||||
}
|
||||
overlayPayload = processed.payload
|
||||
}
|
||||
if versionNode := fragmentVersionClaim(overlayPayload, rule); versionNode != nil {
|
||||
location := publicLocation(versionNode.Location)
|
||||
return sources, []Diagnostic{{
|
||||
Code: CodeOverlayVersion, Severity: SeverityError, Path: rule.Path.clone(),
|
||||
@@ -139,7 +155,7 @@ func loadSelectedOverlayWithAccess(access *filesystemAccess, base *assemblyAccum
|
||||
return sources, []Diagnostic{importDiagnostic(CodeImportRead, "configuration root could not be closed after reading the selected overlay", nil)}
|
||||
}
|
||||
|
||||
if diagnostics := base.applyOverlay(overlayRoot, nil, nil, LayerOverlay); len(diagnostics) != 0 {
|
||||
if diagnostics := base.applyOverlay(overlayPayload, nil, nil, LayerOverlay); len(diagnostics) != 0 {
|
||||
return sources, diagnostics
|
||||
}
|
||||
base.current.inputBytes += int64(len(data))
|
||||
|
||||
@@ -974,6 +974,201 @@ func TestSourceVersionUnderlaysProtectRemainingMountedPayload(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceVersionSelectedOverlayConsumesHeaderAndRetainsWarning(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: main-v}\nsettings: {kept: base}\n")
|
||||
overlayPath := writeImportFile(t, directory, "overlays/testing.yml", "schema: {version: overlay-v}\nsettings: {changed: overlay}\n")
|
||||
var checked []string
|
||||
loaded, err := Load(File(mainPath), testSecretBinding(), Options[map[string]any]{
|
||||
Overlay: "testing", SourceVersionMode: sourceVersionModeRequired,
|
||||
Definition: Definition[map[string]any]{Version: &VersionRule{
|
||||
Path: Keys("schema", "version"),
|
||||
Check: func(version Version, validation *Validation) error {
|
||||
checked = append(checked, version.Text)
|
||||
if version.Text == "overlay-v" {
|
||||
validation.Warning(Path{}, "overlay accepted with advisory")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if len(checked) != 2 || checked[0] != "main-v" || checked[1] != "overlay-v" {
|
||||
t.Fatalf("checked source declarations = %#v", checked)
|
||||
}
|
||||
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)
|
||||
}
|
||||
diagnostics := loaded.Inspection.Diagnostics()
|
||||
if len(diagnostics) != 1 || diagnostics[0].Code != CodeVersionWarning {
|
||||
t.Fatalf("overlay warning was not retained: %#v", diagnostics)
|
||||
}
|
||||
sources := loaded.Inspection.Sources()
|
||||
if len(sources) != 2 || sources[1].Path != filepath.Clean(overlayPath) || sources[1].Layer != LayerOverlay {
|
||||
t.Fatalf("overlay source records = %#v", sources)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceVersionSelectedOverlayRequiresAndChecksItsDeclaration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("required absence", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: main-v}\n")
|
||||
writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: overlay}\n")
|
||||
loaded, err := Load(File(mainPath), testSecretBinding(), Options[map[string]any]{
|
||||
Overlay: "testing", SourceVersionMode: sourceVersionModeRequired,
|
||||
Definition: Definition[map[string]any]{Version: &VersionRule{
|
||||
Path: Keys("schema", "version"), Check: func(Version, *Validation) error { return nil },
|
||||
}},
|
||||
})
|
||||
if err == nil || loaded != nil {
|
||||
t.Fatalf("Load = %#v, %v, want required overlay declaration error", loaded, err)
|
||||
}
|
||||
loadErr, ok := err.(*LoadError)
|
||||
if !ok || len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeVersionError {
|
||||
t.Fatalf("missing overlay diagnostics = %#v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("checker rejection", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: main-v}\n")
|
||||
writeImportFile(t, directory, "overlays/testing.yml", "schema: {version: rejected-overlay}\nsettings: {changed: hidden}\n")
|
||||
var checked []string
|
||||
loaded, err := Load(File(mainPath), testSecretBinding(), Options[map[string]any]{
|
||||
Overlay: "testing", SourceVersionMode: sourceVersionModeIfPresent,
|
||||
Definition: Definition[map[string]any]{Version: &VersionRule{
|
||||
Path: Keys("schema", "version"),
|
||||
Check: func(version Version, _ *Validation) error {
|
||||
checked = append(checked, version.Text)
|
||||
if version.Text == "rejected-overlay" {
|
||||
return errors.New("overlay rejected")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}},
|
||||
})
|
||||
if err == nil || loaded != nil {
|
||||
t.Fatalf("Load = %#v, %v, want overlay rejection", loaded, err)
|
||||
}
|
||||
loadErr, ok := err.(*LoadError)
|
||||
if !ok || len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeVersionHookFailed {
|
||||
t.Fatalf("overlay rejection diagnostics = %#v", err)
|
||||
}
|
||||
if len(checked) != 2 || checked[0] != "main-v" || checked[1] != "rejected-overlay" {
|
||||
t.Fatalf("checked source declarations = %#v", checked)
|
||||
}
|
||||
})
|
||||
|
||||
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")
|
||||
checks := 0
|
||||
loaded, err := Load(File(mainPath), testSecretBinding(), Options[map[string]any]{
|
||||
Overlay: "testing", SourceVersionMode: sourceVersionModeRequired,
|
||||
Definition: Definition[map[string]any]{Version: &VersionRule{
|
||||
Path: Keys("schema", "version"), Check: func(Version, *Validation) error { checks++; return nil },
|
||||
}},
|
||||
})
|
||||
if err == nil || loaded != nil {
|
||||
t.Fatalf("Load = %#v, %v, want structural overlay rejection", loaded, err)
|
||||
}
|
||||
loadErr, ok := err.(*LoadError)
|
||||
if !ok || len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeOverlayDirective {
|
||||
t.Fatalf("structural overlay diagnostics = %#v", err)
|
||||
}
|
||||
if checks != 1 {
|
||||
t.Fatalf("version checker calls = %d, want main only", checks)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSourceVersionOverlayLeavesCLIOverridesHeaderFree(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
checks := 0
|
||||
loaded, err := Load(Bytes("main", []byte("schema: {version: main-v}\nsettings: {base: value}\n")), testSecretBinding(), Options[map[string]any]{
|
||||
SourceVersionMode: sourceVersionModeRequired,
|
||||
Definition: Definition[map[string]any]{Version: &VersionRule{
|
||||
Path: Keys("schema", "version"),
|
||||
Check: func(Version, *Validation) error {
|
||||
checks++
|
||||
return nil
|
||||
},
|
||||
}},
|
||||
Overrides: []Override{{Path: Keys("settings", "cli"), Value: "supplied", Flag: "--settings.cli"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Load with CLI override: %v", err)
|
||||
}
|
||||
if checks != 1 || loaded.Config["settings"].(map[string]any)["cli"] != "supplied" {
|
||||
t.Fatalf("checker count/configuration = %d, %#v", checks, loaded.Config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceVersionOverlayProtectsMountedMainAndPreservesSequenceCoordinates(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("mounted main ownership", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "meta: {version: main-v}\n")
|
||||
writeImportFile(t, directory, "overlays/testing.yml", "meta: {version: overlay-v}\nservice: {meta: {version: payload}}\n")
|
||||
_, err := LoadLayered(Input{Source: File(mainPath), Destination: Keys("service")}, testSecretBinding(), LayeredOptions[map[string]any]{
|
||||
Overlay: "testing", OverlayFilesystem: &FilesystemContext{BaseDir: directory, Root: directory},
|
||||
SourceVersionMode: sourceVersionModeRequired,
|
||||
Definition: Definition[map[string]any]{Version: &VersionRule{
|
||||
Path: Keys("meta", "version"), Check: func(Version, *Validation) error { return nil },
|
||||
}},
|
||||
})
|
||||
loadErr, ok := err.(*LoadError)
|
||||
if !ok || len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeOverlayVersion {
|
||||
t.Fatalf("mounted overlay ownership error = %#v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sequence location remains source-local after deletion", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "base: true\n")
|
||||
writeImportFile(t, directory, "overlays/testing.yml", "versions:\n - before\n - overlay-version\n - after\n")
|
||||
main, diagnostics := parseSource(File(mainPath), "source-main", defaultParseLimits())
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatalf("parse main: %#v", diagnostics)
|
||||
}
|
||||
localRule := &VersionRule{Path: Keys("versions").Index(1), Check: func(version Version, _ *Validation) error {
|
||||
if version.Text != "overlay-version" {
|
||||
t.Errorf("checked version = %#v", version)
|
||||
}
|
||||
return nil
|
||||
}}
|
||||
effectiveRule := effectiveSourceVersionRule(localRule, Keys("service"), sourceVersionModeRequired)
|
||||
state := &sourceVersionState{localRule: localRule, effectiveRule: effectiveRule, mode: sourceVersionModeRequired}
|
||||
base, compositionDiagnostics := composeDocuments(&document.Node{Kind: document.Mapping})
|
||||
if len(compositionDiagnostics) != 0 {
|
||||
t.Fatalf("compose base: %#v", compositionDiagnostics)
|
||||
}
|
||||
assembly := newAssemblyAccumulator(base)
|
||||
_, diagnostics = loadSelectedOverlayIntoWithVersion(
|
||||
File(mainPath), nil, main, assembly, sourceInfos(main.source), "testing", effectiveRule, defaultImportLimits(), state,
|
||||
)
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatalf("load selected overlay: %#v", diagnostics)
|
||||
}
|
||||
if len(state.evidence) != 1 || state.evidence[0].resolvedPath.String() != "/versions/1" || state.evidence[0].location == nil || state.evidence[0].location.Line != 3 {
|
||||
t.Fatalf("sequence evidence lost original coordinates: %#v", state.evidence)
|
||||
}
|
||||
versions, found := documentAtPath(assembly.current.root, Keys("versions"))
|
||||
if !found || versions.Kind != document.Sequence || len(versions.Sequence) != 2 || versions.Sequence[0].Text != "before" || versions.Sequence[1].Text != "after" {
|
||||
t.Fatalf("remaining overlay sequence = %#v", versions)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSourceVersionImportAccountingIncludesConsumedHeaderNodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user