From 49218a836bef1a340ef0c8c00f5c7529832b553b Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 15 Oct 2026 15:15:05 +0000 Subject: [PATCH] Add source-local version extraction --- docs/roadmap/implementation.md | 17 ++ internal/engine/source_version.go | 339 ++++++++++++++++++++++ internal/engine/source_version_test.go | 384 +++++++++++++++++++++++++ 3 files changed, 740 insertions(+) create mode 100644 internal/engine/source_version.go create mode 100644 internal/engine/source_version_test.go diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 9909a5a..1e79a9e 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -270,6 +270,23 @@ No environmental limitation affected validation; checks ran on Linux/amd64. Validate: helper/version-path/directive/inheritance-control tests and everyday check. Done: the processor is tested independently of public loading. +Completion evidence: added a private source-local processor that resolves the +configured path against the original document, separates imports, records +private path/ancestor/location evidence, and consumes accepted contribution +headers only from an owned payload copy. Missing and obstructed paths retain +their cause and nearest available location; required absence fails before the +checker, while optional main absence still invokes it. Structural directive +overlap is rejected before payload consumption. Focused coverage includes the +version vocabulary, malformed nodes, contextual numeric keys and indices, +empty ancestors, sequence deletion, and actual versus unrelated structural +overlap. These checks passed with Go `go1.26.8 linux/amd64`: + +- `GOTOOLCHAIN=local GOPROXY=off go test ./internal/engine -run 'SourceVersion|Version|Import|Underlay|Overlay|Destination|Inheritance'` +- `GOTOOLCHAIN=local ./scripts/check.sh` +- `git diff --check` + +No environmental limitation affected validation; checks ran on Linux/amd64. + ## Stage 4 — Add private mode state and main-document integration 1. Add the engine mode to ordinary/layered options and loadRequest. Validate it at diff --git a/internal/engine/source_version.go b/internal/engine/source_version.go new file mode 100644 index 0000000..58a51c5 --- /dev/null +++ b/internal/engine/source_version.go @@ -0,0 +1,339 @@ +package engine + +import "gitea.maximumdirect.net/eric/ConfigKit/internal/document" + +type sourceVersionMode uint8 + +const ( + sourceVersionModeDefault sourceVersionMode = iota + sourceVersionModeIfPresent + sourceVersionModeRequired +) + +type sourceVersionRole uint8 + +const ( + sourceVersionMain sourceVersionRole = iota + sourceVersionContribution +) + +type sourceVersionAncestor struct { + path Path + kind document.Kind +} + +type sourceVersionEvidence struct { + path Path + resolvedPath Path + destination Path + version Version + location *Location + ancestors []sourceVersionAncestor + absence sourceVersionPathAbsence + checked bool + redacted bool +} + +type sourceVersionProcessingResult struct { + payload *document.Node + imports []importDeclaration + evidence *sourceVersionEvidence + diagnostics []Diagnostic +} + +type sourceVersionPathAbsence uint8 + +const ( + sourceVersionPathPresent sourceVersionPathAbsence = iota + sourceVersionPathInvalid + sourceVersionMemberMissing + sourceVersionIndexUnavailable + sourceVersionPathObstructed +) + +type sourceVersionPath struct { + node *document.Node + present bool + absence sourceVersionPathAbsence + nearest document.Location + resolvedPath Path + ancestors []sourceVersionAncestor +} + +func processSourceVersion(root *document.Node, rule *VersionRule, role sourceVersionRole, destination Path, mode sourceVersionMode) sourceVersionProcessingResult { + if role != sourceVersionMain && role != sourceVersionContribution { + return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source role is invalid")}} + } + if mode != sourceVersionModeDefault && mode != sourceVersionModeIfPresent && mode != sourceVersionModeRequired { + return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source-version mode is invalid")}} + } + if mode != sourceVersionModeDefault && (rule == nil || rule.Check == nil || !rule.Path.valid()) { + return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source-version checking requires a valid version rule and checker")}} + } + if root == nil { + if role == sourceVersionMain && mode != sourceVersionModeDefault { + return sourceVersionProcessingResult{diagnostics: canonicalDiagnostics(reportVersion(rule, Version{}, nil))} + } + return sourceVersionProcessingResult{} + } + + payload, imports, diagnostics := splitImportDirectives(root) + if len(diagnostics) != 0 { + return sourceVersionProcessingResult{diagnostics: diagnostics} + } + result := sourceVersionProcessingResult{payload: payload, imports: imports} + if mode == sourceVersionModeDefault { + return result + } + + resolved := resolveSourceVersionPath(root, rule.Path) + evidence := &sourceVersionEvidence{ + path: rule.Path.clone(), resolvedPath: resolved.resolvedPath, + destination: destination.clone(), ancestors: resolved.ancestors, + absence: resolved.absence, + } + location := resolved.nearest + if resolved.present { + location = resolved.node.Location + } + if locationKnown(location) { + public := publicLocation(location) + evidence.location = &public + } + result.evidence = evidence + + interpreted := interpretVersion(rule, root) + evidence.version = interpreted.version + if sourceVersionControlOverlap(root, evidence.resolvedPath) { + evidence.redactText() + result.payload = nil + result.diagnostics = []Diagnostic{sourceVersionOverlapDiagnostic(rule, evidence)} + return result + } + if hasErrors(interpreted.diagnostics) { + evidence.redactText() + result.payload = nil + result.diagnostics = canonicalDiagnostics(interpreted.diagnostics) + return result + } + if !interpreted.version.Present { + if mode == sourceVersionModeRequired { + evidence.redactText() + result.payload = nil + result.diagnostics = []Diagnostic{sourceVersionMissingDiagnostic(rule, evidence)} + return result + } + if role == sourceVersionContribution { + return result + } + } + + evidence.checked = true + result.diagnostics = canonicalDiagnostics(reportVersion(rule, interpreted.version, root)) + if hasErrors(result.diagnostics) { + evidence.redactText() + result.payload = nil + return result + } + if role == sourceVersionContribution && interpreted.version.Present { + if !removeSourceVersionNode(result.payload, evidence.resolvedPath) { + evidence.redactText() + result.payload = nil + result.diagnostics = []Diagnostic{sourceVersionOptionsDiagnostic("selected source version could not be removed from its owned payload")} + return result + } + } + return result +} + +func sourceVersionOptionsDiagnostic(message string) Diagnostic { + return Diagnostic{Code: CodeOptionsInvalid, Severity: SeverityError, Message: message} +} + +func sourceVersionMissingDiagnostic(rule *VersionRule, evidence *sourceVersionEvidence) Diagnostic { + return Diagnostic{ + Code: CodeVersionError, Severity: SeverityError, Path: rule.Path.clone(), + Message: "required schema version is missing", Primary: cloneLocation(evidence.location), + } +} + +func sourceVersionOverlapDiagnostic(rule *VersionRule, evidence *sourceVersionEvidence) Diagnostic { + return Diagnostic{ + Code: CodeVersionError, Severity: SeverityError, Path: rule.Path.clone(), + Message: "schema version path overlaps a structural directive", Primary: cloneLocation(evidence.location), + } +} + +func (evidence *sourceVersionEvidence) redactText() { + if evidence == nil { + return + } + evidence.version.Text = "" + evidence.redacted = true +} + +func resolveSourceVersionPath(root *document.Node, path Path) sourceVersionPath { + resolved := sourceVersionPath{absence: sourceVersionPathInvalid} + if root == nil || !path.valid() { + return resolved + } + current := root + resolved.nearest = current.Location + resolved.ancestors = append(resolved.ancestors, sourceVersionAncestor{kind: current.Kind}) + for _, segment := range path.segments { + switch current.Kind { + case document.Mapping: + if segment.kind == pathSegmentIndex { + resolved.absence = sourceVersionPathObstructed + return resolved + } + resolved.resolvedPath = resolved.resolvedPath.Key(segment.key) + found := false + for _, entry := range current.Mapping { + if entry.Key == segment.key { + current = entry.Value + found = true + break + } + } + if !found { + resolved.absence = sourceVersionMemberMissing + return resolved + } + case document.Sequence: + index, ok := schemaIndex(segment) + if !ok { + resolved.absence = sourceVersionIndexUnavailable + return resolved + } + resolved.resolvedPath = resolved.resolvedPath.Index(index) + if index >= len(current.Sequence) { + resolved.absence = sourceVersionIndexUnavailable + return resolved + } + current = current.Sequence[index] + default: + resolved.absence = sourceVersionPathObstructed + return resolved + } + resolved.nearest = current.Location + resolved.ancestors = append(resolved.ancestors, sourceVersionAncestor{ + path: resolved.resolvedPath.clone(), kind: current.Kind, + }) + } + resolved.node = current + resolved.present = true + resolved.absence = sourceVersionPathPresent + return resolved +} + +func sourceVersionControlOverlap(root *document.Node, path Path) bool { + if root == nil || !path.valid() { + return false + } + if root.Kind == document.Mapping { + for _, entry := range root.Mapping { + if entry.Key == directiveKey && pathHasPrefix(path, Keys(directiveKey)) { + return true + } + } + } + return sourceVersionInheritanceOverlap(root, Path{}, path) +} + +func sourceVersionInheritanceOverlap(node *document.Node, current, path Path) bool { + if node == nil { + return false + } + if node.Kind == document.Mapping { + if _, found := inheritanceDirective(node); found { + controlPath := current.Key(inheritanceKey) + if pathHasPrefix(path, controlPath) { + return true + } + } + for _, entry := range node.Mapping { + if entry.Key == inheritanceKey { + continue + } + if sourceVersionInheritanceOverlap(entry.Value, current.Key(entry.Key), path) { + return true + } + } + } else if node.Kind == document.Sequence { + for index, child := range node.Sequence { + if sourceVersionInheritanceOverlap(child, current.Index(index), path) { + return true + } + } + } + return false +} + +func pathHasPrefix(path, prefix Path) bool { + if !path.valid() || !prefix.valid() || len(prefix.segments) > len(path.segments) { + return false + } + for index, segment := range prefix.segments { + candidate := path.segments[index] + if candidate.kind != segment.kind || candidate.key != segment.key || candidate.index != segment.index { + return false + } + } + return true +} + +func removeSourceVersionNode(root *document.Node, path Path) bool { + if root == nil || !path.valid() || len(path.segments) == 0 { + return false + } + current := root + for _, segment := range path.segments[:len(path.segments)-1] { + switch current.Kind { + case document.Mapping: + if segment.kind == pathSegmentIndex { + return false + } + found := false + for _, entry := range current.Mapping { + if entry.Key == segment.key { + current = entry.Value + found = true + break + } + } + if !found { + return false + } + case document.Sequence: + index, ok := schemaIndex(segment) + if !ok || index >= len(current.Sequence) { + return false + } + current = current.Sequence[index] + default: + return false + } + } + last := path.segments[len(path.segments)-1] + switch current.Kind { + case document.Mapping: + if last.kind == pathSegmentIndex { + return false + } + for index, entry := range current.Mapping { + if entry.Key == last.key { + current.Mapping = append(current.Mapping[:index], current.Mapping[index+1:]...) + return true + } + } + case document.Sequence: + index, ok := schemaIndex(last) + if !ok || index >= len(current.Sequence) { + return false + } + current.Sequence = append(current.Sequence[:index], current.Sequence[index+1:]...) + return true + } + return false +} diff --git a/internal/engine/source_version_test.go b/internal/engine/source_version_test.go new file mode 100644 index 0000000..174f23b --- /dev/null +++ b/internal/engine/source_version_test.go @@ -0,0 +1,384 @@ +package engine + +import ( + "testing" + + "gitea.maximumdirect.net/eric/ConfigKit/internal/document" +) + +func sourceVersionTestDocument(t *testing.T, input string) *document.Node { + t.Helper() + parsed, diagnostics := parseSource(Bytes("source-version.yml", []byte(input)), "source-version", defaultParseLimits()) + if len(diagnostics) != 0 { + t.Fatalf("parse diagnostics = %#v", diagnostics) + } + return parsed.document +} + +func requireSourceVersionDiagnostic(t *testing.T, diagnostics []Diagnostic, code Code, path string) Diagnostic { + t.Helper() + for _, diagnostic := range diagnostics { + if diagnostic.Code == code && diagnostic.Path.String() == path { + return diagnostic + } + } + t.Fatalf("diagnostics = %#v, want %s at %q", diagnostics, code, path) + return Diagnostic{} +} + +func TestSourceVersionExtractionPreservesVersionVocabulary(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + want Version + }{ + {name: "string", input: "version: \"app.v7\"\n", want: Version{Present: true, Text: "app.v7"}}, + {name: "integer", input: "version: 123456789012345678901234567890\n", want: Version{Present: true, Text: "123456789012345678901234567890"}}, + {name: "null", input: "version: null\n", want: Version{Present: true, Null: true}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + root := sourceVersionTestDocument(t, test.input) + var checked Version + rule := &VersionRule{Path: Keys("version"), Check: func(version Version, _ *Validation) error { + checked = version + return nil + }} + result := processSourceVersion(root, rule, sourceVersionContribution, Keys("collections", "weather"), sourceVersionModeIfPresent) + if len(result.diagnostics) != 0 || !result.evidence.checked || result.evidence.version != test.want || checked != test.want { + t.Fatalf("result = %#v, checked = %#v", result, checked) + } + if _, found := documentAtPath(result.payload, rule.Path); found { + t.Fatal("accepted non-main version remained in the payload") + } + if node, found := documentAtPath(root, rule.Path); !found || node == nil { + t.Fatal("source document was mutated while consuming its version") + } + if result.evidence.destination.String() != "/collections/weather" { + t.Fatalf("evidence destination = %q", result.evidence.destination.String()) + } + }) + } +} + +func TestSourceVersionExtractionHandlesMissingAndMalformedValues(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + path Path + mode sourceVersionMode + role sourceVersionRole + wantCode Code + wantPresent bool + wantCallback bool + wantAbsence sourceVersionPathAbsence + }{ + { + name: "missing mapping member is optional absence", input: "settings: {}\n", + path: Keys("settings", "version"), mode: sourceVersionModeIfPresent, + role: sourceVersionContribution, wantPresent: false, wantAbsence: sourceVersionMemberMissing, + }, + { + name: "missing below scalar is optional absence", input: "settings: scalar\n", + path: Keys("settings", "version"), mode: sourceVersionModeIfPresent, + role: sourceVersionContribution, wantPresent: false, wantAbsence: sourceVersionPathObstructed, + }, + { + name: "required absence reports nearest location", input: "settings: scalar\n", + path: Keys("settings", "version"), mode: sourceVersionModeRequired, + role: sourceVersionContribution, wantCode: CodeVersionError, wantAbsence: sourceVersionPathObstructed, + }, + { + name: "unavailable sequence index is absence", input: "versions: [1]\n", + path: Keys("versions").Index(3), mode: sourceVersionModeRequired, + role: sourceVersionContribution, wantCode: CodeVersionError, wantAbsence: sourceVersionIndexUnavailable, + }, + { + name: "required actual main absence", input: "name: demo\n", path: Keys("version"), + mode: sourceVersionModeRequired, role: sourceVersionMain, wantCode: CodeVersionError, wantAbsence: sourceVersionMemberMissing, + }, + { + name: "main optional absence invokes checker", input: "name: demo\n", path: Keys("version"), + mode: sourceVersionModeIfPresent, role: sourceVersionMain, wantCallback: true, wantAbsence: sourceVersionMemberMissing, + }, + {name: "boolean", input: "version: true\n", path: Keys("version"), mode: sourceVersionModeIfPresent, role: sourceVersionContribution, wantCode: CodeVersionType, wantPresent: true}, + {name: "fraction", input: "version: 4.5\n", path: Keys("version"), mode: sourceVersionModeIfPresent, role: sourceVersionContribution, wantCode: CodeVersionType, wantPresent: true}, + {name: "mapping", input: "version: {}\n", path: Keys("version"), mode: sourceVersionModeIfPresent, role: sourceVersionContribution, wantCode: CodeVersionType, wantPresent: true}, + {name: "sequence", input: "version: []\n", path: Keys("version"), mode: sourceVersionModeIfPresent, role: sourceVersionContribution, wantCode: CodeVersionType, wantPresent: true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + root := sourceVersionTestDocument(t, test.input) + called := false + rule := &VersionRule{Path: test.path, Check: func(Version, *Validation) error { + called = true + return nil + }} + result := processSourceVersion(root, rule, test.role, Path{}, test.mode) + if called != test.wantCallback { + t.Fatalf("checker called = %v, want %v", called, test.wantCallback) + } + if test.wantCode != "" { + diagnostic := requireSourceVersionDiagnostic(t, result.diagnostics, test.wantCode, test.path.String()) + if result.payload != nil { + t.Fatalf("failed processing returned payload %#v", result.payload) + } + if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-version" || diagnostic.Primary.Line != 1 { + t.Fatalf("diagnostic location = %#v", diagnostic.Primary) + } + } else if len(result.diagnostics) != 0 { + t.Fatalf("diagnostics = %#v", result.diagnostics) + } + if result.evidence == nil || result.evidence.version.Present != test.wantPresent { + t.Fatalf("evidence = %#v", result.evidence) + } + if result.evidence.absence != test.wantAbsence { + t.Fatalf("absence cause = %v, want %v", result.evidence.absence, test.wantAbsence) + } + if test.wantCode == CodeVersionType && (!result.evidence.redacted || result.evidence.version.Text != "") { + t.Fatalf("malformed value was retained: %#v", result.evidence) + } + if !test.wantCallback && result.evidence.checked { + t.Fatalf("evidence incorrectly records a callback: %#v", result.evidence) + } + }) + } +} + +func TestSourceVersionExtractionConsumesOnlySelectedNodes(t *testing.T) { + t.Parallel() + + destination := Keys("collections", "weather") + root := sourceVersionTestDocument(t, "settings:\n version: 8\n") + originalSettings, _ := documentAtPath(root, Keys("settings")) + result := processSourceVersion(root, &VersionRule{ + Path: Keys("settings", "version"), Check: func(Version, *Validation) error { return nil }, + }, sourceVersionContribution, destination, sourceVersionModeIfPresent) + settings, found := documentAtPath(result.payload, Keys("settings")) + if len(result.diagnostics) != 0 || !found || settings.Kind != document.Mapping || len(settings.Mapping) != 0 { + t.Fatalf("payload = %#v, diagnostics = %#v", result.payload, result.diagnostics) + } + if settings.Location != originalSettings.Location { + t.Fatalf("authored ancestor location changed: got %#v, want %#v", settings.Location, originalSettings.Location) + } + if original, found := documentAtPath(root, Keys("settings", "version")); !found || original.Text != "8" { + t.Fatalf("original source changed: %#v, found %v", original, found) + } + if len(result.evidence.ancestors) != 3 || result.evidence.ancestors[1].kind != document.Mapping { + t.Fatalf("ancestor shape = %#v", result.evidence.ancestors) + } + + sequenceRoot := sourceVersionTestDocument(t, "versions: [5, 7, 9]\n") + originalVersions, _ := documentAtPath(sequenceRoot, Keys("versions")) + originalLastLocation := originalVersions.Sequence[2].Location + sequencePath, err := ParsePath("/versions/1") + if err != nil { + t.Fatal(err) + } + sequenceResult := processSourceVersion(sequenceRoot, &VersionRule{ + Path: sequencePath, Check: func(version Version, _ *Validation) error { + if version.Text != "7" { + t.Fatalf("checked version = %#v", version) + } + return nil + }, + }, sourceVersionContribution, Path{}, sourceVersionModeRequired) + versions, found := documentAtPath(sequenceResult.payload, Keys("versions")) + if len(sequenceResult.diagnostics) != 0 || !found || versions.Kind != document.Sequence || len(versions.Sequence) != 2 || versions.Sequence[0].Text != "5" || versions.Sequence[1].Text != "9" { + t.Fatalf("sequence payload = %#v, diagnostics = %#v", sequenceResult.payload, sequenceResult.diagnostics) + } + if versions.Sequence[1].Location != originalLastLocation { + t.Fatalf("remaining sequence location changed: got %#v, want %#v", versions.Sequence[1].Location, originalLastLocation) + } + if len(sequenceResult.evidence.resolvedPath.segments) != 2 || sequenceResult.evidence.resolvedPath.segments[1].kind != pathSegmentIndex || sequenceResult.evidence.resolvedPath.segments[1].index != 1 { + t.Fatalf("original sequence path = %#v", sequenceResult.evidence.resolvedPath) + } + + itemRoot := sourceVersionTestDocument(t, "items:\n - name: first\n - version: 12\n") + itemResult := processSourceVersion(itemRoot, &VersionRule{ + Path: Keys("items").Index(1).Key("version"), Check: func(Version, *Validation) error { return nil }, + }, sourceVersionContribution, Path{}, sourceVersionModeIfPresent) + items, found := documentAtPath(itemResult.payload, Keys("items")) + if len(itemResult.diagnostics) != 0 || !found || len(items.Sequence) != 2 || items.Sequence[1].Kind != document.Mapping || len(items.Sequence[1].Mapping) != 0 { + t.Fatalf("sequence mapping payload = %#v, diagnostics = %#v", itemResult.payload, itemResult.diagnostics) + } + + emptySequenceRoot := sourceVersionTestDocument(t, "versions: [7]\n") + emptySequenceResult := processSourceVersion(emptySequenceRoot, &VersionRule{ + Path: Keys("versions").Index(0), Check: func(Version, *Validation) error { return nil }, + }, sourceVersionContribution, Path{}, sourceVersionModeIfPresent) + emptySequence, found := documentAtPath(emptySequenceResult.payload, Keys("versions")) + if len(emptySequenceResult.diagnostics) != 0 || !found || emptySequence.Kind != document.Sequence || len(emptySequence.Sequence) != 0 { + t.Fatalf("empty sequence payload = %#v, diagnostics = %#v", emptySequenceResult.payload, emptySequenceResult.diagnostics) + } +} + +func TestSourceVersionExtractionResolvesContextualPathSegments(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + path Path + wantRemainingPath bool + wantRemainingText string + }{ + {name: "literal numeric mapping key", input: "versions:\n \"0\": 3\n other: 4\n", path: Keys("versions", "0")}, + {name: "parsed numeric mapping key", input: "versions:\n \"0\": 3\n other: 4\n", path: mustParseSourceVersionPath(t, "/versions/0")}, + {name: "parsed sequence index", input: "versions: [3, 4]\n", path: mustParseSourceVersionPath(t, "/versions/0"), wantRemainingPath: true, wantRemainingText: "4"}, + {name: "explicit sequence index", input: "versions: [3, 4]\n", path: Keys("versions").Index(0), wantRemainingPath: true, wantRemainingText: "4"}, + {name: "escaped mapping key", input: "a/b:\n version: 3\n", path: mustParseSourceVersionPath(t, "/a~1b/version")}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + root := sourceVersionTestDocument(t, test.input) + result := processSourceVersion(root, &VersionRule{ + Path: test.path, Check: func(version Version, _ *Validation) error { + if version.Text != "3" { + t.Errorf("checked version = %#v", version) + } + return nil + }, + }, sourceVersionContribution, Path{}, sourceVersionModeIfPresent) + if len(result.diagnostics) != 0 { + t.Fatalf("diagnostics = %#v", result.diagnostics) + } + remaining, found := documentAtPath(result.payload, test.path) + if found != test.wantRemainingPath || found && remaining.Text != test.wantRemainingText { + t.Fatalf("remaining selected path = %#v, found = %v", remaining, found) + } + if !result.evidence.version.Present || result.evidence.version.Text != "3" { + t.Fatalf("evidence = %#v", result.evidence) + } + }) + } +} + +func TestSourceVersionExtractionRejectsStructuralOverlap(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + path Path + 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: "extends control", input: "settings:\n extends: parent\n", + path: Keys("settings", "extends"), wantCode: CodeVersionError, + }, + { + name: "invalid extends subtree", input: "settings:\n extends:\n version: 1\n", + 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, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + root := sourceVersionTestDocument(t, test.input) + called := false + result := processSourceVersion(root, &VersionRule{Path: test.path, Check: func(Version, *Validation) error { + called = true + return nil + }}, sourceVersionContribution, Path{}, sourceVersionModeIfPresent) + diagnostic := requireSourceVersionDiagnostic(t, result.diagnostics, test.wantCode, test.path.String()) + if called || result.payload != nil { + t.Fatalf("invalid overlap produced payload %#v or called checker %v", result.payload, called) + } + if test.wantCode == CodeVersionError && (result.evidence == nil || !result.evidence.redacted || result.evidence.version.Text != "") { + t.Fatalf("overlap evidence was not conservatively redacted: %#v", result.evidence) + } + if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-version" { + t.Fatalf("diagnostic location = %#v", diagnostic.Primary) + } + }) + } + + unrelatedRoot := sourceVersionTestDocument(t, "configkit:\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) + metadata, found := documentAtPath(unrelatedResult.payload, Keys("metadata")) + if len(unrelatedResult.diagnostics) != 0 || !found || len(metadata.Mapping) != 0 || len(unrelatedResult.imports) != 1 { + t.Fatalf("unrelated directive result = %#v", unrelatedResult) + } + + ordinaryRoot := sourceVersionTestDocument(t, "extends_version: 4\n") + ordinaryResult := processSourceVersion(ordinaryRoot, &VersionRule{ + Path: Keys("extends_version"), Check: func(Version, *Validation) error { return nil }, + }, sourceVersionContribution, Path{}, sourceVersionModeIfPresent) + if len(ordinaryResult.diagnostics) != 0 || ordinaryResult.payload == nil { + t.Fatalf("ordinary extends_version key was rejected: %#v", ordinaryResult) + } +} + +func TestSourceVersionDefaultModeDoesNotCheckOrConsume(t *testing.T) { + t.Parallel() + + root := sourceVersionTestDocument(t, "version: 2\n") + called := false + result := processSourceVersion(root, &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { + called = true + return nil + }}, sourceVersionContribution, Path{}, sourceVersionModeDefault) + if len(result.diagnostics) != 0 || called || result.evidence != nil { + t.Fatalf("default result = %#v, checker called = %v", result, called) + } + if node, found := documentAtPath(result.payload, Keys("version")); !found || node.Text != "2" { + t.Fatalf("default payload = %#v", result.payload) + } +} + +func TestSourceVersionNoSourceMainRetainsAbsentCallback(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + mode sourceVersionMode + }{ + {name: "if present", mode: sourceVersionModeIfPresent}, + {name: "required", mode: sourceVersionModeRequired}, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + called := false + result := processSourceVersion(nil, &VersionRule{ + Path: Keys("version"), + Check: func(version Version, _ *Validation) error { + called = true + if version != (Version{}) { + t.Errorf("NoSource version = %#v", version) + } + return nil + }, + }, sourceVersionMain, Path{}, test.mode) + if !called || len(result.diagnostics) != 0 || result.evidence != nil || result.payload != nil { + t.Fatalf("NoSource result = %#v, callback called = %v", result, called) + } + }) + } +} + +func mustParseSourceVersionPath(t *testing.T, input string) Path { + t.Helper() + path, err := ParsePath(input) + if err != nil { + t.Fatal(err) + } + return path +}