Project version protections through item placement

This commit is contained in:
2026-10-16 19:11:08 +00:00
parent 190431740a
commit c19825620c
10 changed files with 276 additions and 50 deletions

View File

@@ -1,6 +1,6 @@
# Implementation Plan: Sequence Placement and Composition # Implementation Plan: Sequence Placement and Composition
Status: implementation in progress; stages 1–9 completed. Status: implementation in progress; stages 1–10 completed.
## Purpose and authority ## Purpose and authority
@@ -595,6 +595,22 @@ Validation: source-version/version/inheritance/override/resolution tests,
`go test -race ./internal/engine`, and `./scripts/check.sh`. `go test -race ./internal/engine`, and `./scripts/check.sh`.
Completion: item placement changes coordinates, not version or structural policy. Completion: item placement changes coordinates, not version or structural policy.
Completion evidence: enabled source-version rules now derive their effective
paths through the main item's assigned index, while default-mode coordinates
remain unchanged. Structural import and underlay checks use assigned sequence
ranges so they protect the actual main item without rejecting unrelated peers;
source-local checking still runs once per original source, and projected header
evidence is used for sensitivity. Tests cover ordinary and layered placement,
protected versus peer indexes, nested projections, and redacted version
evidence. These checks passed with Go `go1.26.9 linux/amd64`:
- `GOTOOLCHAIN=local GOPROXY=off go test ./internal/engine -run 'SourceVersion|Version|Inheritance|Override|Resolution' -count=1`
- `GOTOOLCHAIN=local GOPROXY=off go test -race ./internal/engine`
- `GOTOOLCHAIN=local ./scripts/check.sh`
- `git diff --check`
No environmental limitation affected validation; checks ran on Linux/amd64.
## Stage 11 — Expose root-owned controls and detached placement evidence ## Stage 11 — Expose root-owned controls and detached placement evidence
Scope: root facade declarations, conversions, and public integration tests. Scope: root facade declarations, conversions, and public integration tests.

View File

@@ -193,6 +193,43 @@ func TestLayeredMountedVersionProtectionCoversInheritanceAndUpperTiers(t *testin
}) })
} }
func TestLayeredItemVersionProtectionTracksAssignedPeerIndexes(t *testing.T) {
t.Parallel()
definition := Definition[map[string]any]{
Version: &VersionRule{Path: Keys("schema", "version"), Check: func(Version, *Validation) error { return nil }},
sequenceRegistrations: []sequenceRegistration{{path: Keys("catalog", "entries"), identityField: "id"}},
}
item := func(label string, value map[string]any) Input {
return Input{Source: Mapping(label, value), Destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem}
}
options := func(inputs ...Input) LayeredOptions[map[string]any] {
return LayeredOptions[map[string]any]{Definition: definition, SourceVersionMode: sourceVersionModeIfPresent, Underlays: []Underlay{{Inputs: inputs}}}
}
main := item("main", map[string]any{"id": "main", "schema": map[string]any{"version": "v1"}})
t.Run("same assigned main path is protected", func(t *testing.T) {
_, err := LoadLayered(main, testSecretBinding(), options(item("underlay", map[string]any{"id": "underlay", "schema": nil})))
if got := groupedErrorCode(t, err); got != CodeUnderlayVersion {
t.Fatalf("protected item claim code = %s, want %s", got, CodeUnderlayVersion)
}
})
t.Run("appended peer does not claim main path", func(t *testing.T) {
loaded, err := LoadLayered(main, testSecretBinding(), options(
item("first", map[string]any{"id": "first", "schema": map[string]any{"other": "value"}}),
item("peer", map[string]any{"id": "peer", "schema": nil}),
))
if err != nil {
t.Fatalf("unrelated appended peer rejected: %v", err)
}
entries := layeredMapValue(t, testLoaded(loaded).Config, "catalog", "entries").([]any)
if len(entries) != 1 || entries[0].(map[string]any)["id"] != "main" {
t.Fatalf("main item projection = %#v", entries)
}
})
}
type destinationDecodeProbe string type destinationDecodeProbe string
var destinationDecodeProbeValues []string var destinationDecodeProbeValues []string

View File

@@ -163,26 +163,54 @@ func nestedDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic
} }
func fragmentVersionClaim(root *document.Node, rule *VersionRule) *document.Node { func fragmentVersionClaim(root *document.Node, rule *VersionRule) *document.Node {
return fragmentVersionClaimProjected(root, rule, -1, nil)
}
func fragmentVersionClaimProjected(root *document.Node, rule *VersionRule, sourceIndex int, ranges []sequenceRange) *document.Node {
if rule == nil { if rule == nil {
return nil return nil
} }
current := root current := root
currentPath := Path{}
if len(rule.Path.segments) == 0 { if len(rule.Path.segments) == 0 {
return current return current
} }
for _, segment := range rule.Path.segments { for _, segment := range rule.Path.segments {
if current.Kind != document.Mapping { switch current.Kind {
case document.Mapping:
if segment.kind == pathSegmentIndex {
return current
}
index := documentEntryIndex(current.Mapping, segment.key)
if index < 0 {
return nil
}
current = current.Mapping[index].Value
currentPath = currentPath.Key(segment.key)
case document.Sequence:
if segment.kind != pathSegmentIndex {
return current
}
index := segment.index
for _, assigned := range ranges {
if assigned.sourceIndex != sourceIndex || assigned.path.String() != currentPath.String() {
continue
}
if index < assigned.start || index >= assigned.end {
return nil
}
index -= assigned.start
break
}
if index < 0 || index >= len(current.Sequence) {
return nil
}
current = current.Sequence[index]
currentPath = currentPath.Index(index)
default:
// A scalar or null at an ancestor replaces the protected subtree.
return current return current
} }
if segment.kind == pathSegmentIndex {
return nil
}
key := segment.key
index := documentEntryIndex(current.Mapping, key)
if index < 0 {
return nil
}
current = current.Mapping[index].Value
} }
return current return current
} }

View File

@@ -230,6 +230,11 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
if len(failures) != 0 { if len(failures) != 0 {
return nil, cloneSourceInfos(state.sources), prependDiagnosticPaths(failures, destination.Index(0)) return nil, cloneSourceInfos(state.sources), prependDiagnosticPaths(failures, destination.Index(0))
} }
if versionState != nil {
if diagnostic := importedVersionClaim(documents, completed, versionState.localRule, 0); diagnostic != nil {
return nil, cloneSourceInfos(state.sources), []Diagnostic{*diagnostic}
}
}
completed.acquisition = []acquisitionScope{cloneAcquisitionScope(localScope)} completed.acquisition = []acquisitionScope{cloneAcquisitionScope(localScope)}
projectNestedEvidence(completed, localScope) projectNestedEvidence(completed, localScope)
_, placed, err := placeDocumentWithMode(completed.root, destination, placementModeSequenceItem, body.Location, maxDocumentDepth, maxDocumentNodes) _, placed, err := placeDocumentWithMode(completed.root, destination, placementModeSequenceItem, body.Location, maxDocumentDepth, maxDocumentNodes)
@@ -260,6 +265,11 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
documents = append(documents, contribution.document) documents = append(documents, contribution.document)
} }
composed, diagnostics := composeDocumentsWithPolicy(policy, documents...) composed, diagnostics := composeDocumentsWithPolicy(policy, documents...)
if composed != nil {
if diagnostic := importedVersionClaim(documents, composed, rule, 0); diagnostic != nil {
return nil, cloneSourceInfos(state.sources), []Diagnostic{*diagnostic}
}
}
if composed != nil { if composed != nil {
projectNestedEvidence(composed, state.scope) projectNestedEvidence(composed, state.scope)
projectAcquiredDestinations(composed, state.scope, state.sources) projectAcquiredDestinations(composed, state.scope, state.sources)
@@ -416,7 +426,11 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
var diagnostics []Diagnostic var diagnostics []Diagnostic
var processing *sourceVersionProcessingResult var processing *sourceVersionProcessingResult
if s.versionState != nil && s.versionState.mode != sourceVersionModeDefault { if s.versionState != nil && s.versionState.mode != sourceVersionModeDefault {
processed, failures := s.versionState.process(root, sourceVersionContribution, destination, sourceID) versionDestination := destination
if s.itemScope {
versionDestination = appendDestination(s.itemPrefix.Index(0), destination)
}
processed, failures := s.versionState.process(root, sourceVersionContribution, versionDestination, sourceID, declaration.placement)
if len(failures) != 0 { if len(failures) != 0 {
return failures return failures
} }
@@ -452,18 +466,6 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &location)} return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &location)}
} }
s.accounting.nodes += placementNodes s.accounting.nodes += placementNodes
protectionRule := s.rule
if s.versionState != nil && s.versionState.effectiveRule != nil {
protectionRule = s.versionState.effectiveRule
}
if versionNode := fragmentVersionClaim(placed, protectionRule); versionNode != nil {
versionLocation := publicLocation(versionNode.Location)
return []Diagnostic{{
Code: CodeImportVersion, Severity: SeverityError, Path: protectionRule.Path.clone(),
Message: "imported fragments cannot contribute to the schema-version path", Primary: &versionLocation,
}}
}
route := importRoute{canonical: canonical, locations: currentRoute.locations} route := importRoute{canonical: canonical, locations: currentRoute.locations}
s.scope.contributions = append(s.scope.contributions, acquiredContribution{ s.scope.contributions = append(s.scope.contributions, acquiredContribution{
source: cloneSourceInfo(info), payload: body, document: placed, source: cloneSourceInfo(info), payload: body, document: placed,
@@ -530,17 +532,6 @@ func (s *importTraversal) visitItemSource(canonical string, declaration importDe
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &declaration.location)} return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &declaration.location)}
} }
s.accounting.nodes += placementNodes s.accounting.nodes += placementNodes
protectionRule := s.rule
if s.versionState != nil && s.versionState.effectiveRule != nil {
protectionRule = s.versionState.effectiveRule
}
if versionNode := fragmentVersionClaim(placed, protectionRule); versionNode != nil {
versionLocation := publicLocation(versionNode.Location)
return []Diagnostic{{
Code: CodeImportVersion, Severity: SeverityError, Path: protectionRule.Path.clone(),
Message: "imported fragments cannot contribute to the schema-version path", Primary: &versionLocation,
}}
}
s.scope.contributions = append(s.scope.contributions, acquiredContribution{ s.scope.contributions = append(s.scope.contributions, acquiredContribution{
source: cloneSourceInfo(info), payload: completed.root, document: placed, source: cloneSourceInfo(info), payload: completed.root, document: placed,
destination: pl.clone(), relativeDestination: relativeDestination.clone(), processing: processing, destination: pl.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
@@ -549,6 +540,25 @@ func (s *importTraversal) visitItemSource(canonical string, declaration importDe
return nil return nil
} }
func importedVersionClaim(documents []*document.Node, composed *composedDocument, rule *VersionRule, mainIndex int) *Diagnostic {
if composed == nil || rule == nil {
return nil
}
for index, contribution := range documents {
if index == mainIndex {
continue
}
if node := fragmentVersionClaimProjected(contribution, rule, index, composed.sequenceRanges); node != nil {
location := publicLocation(node.Location)
return &Diagnostic{
Code: CodeImportVersion, Severity: SeverityError, Path: rule.Path.clone(),
Message: "imported fragments cannot contribute to the schema-version path", Primary: &location,
}
}
}
return nil
}
func prependDiagnosticPaths(diagnostics []Diagnostic, prefix Path) []Diagnostic { func prependDiagnosticPaths(diagnostics []Diagnostic, prefix Path) []Diagnostic {
cloned := append([]Diagnostic(nil), diagnostics...) cloned := append([]Diagnostic(nil), diagnostics...)
for index := range cloned { for index := range cloned {
@@ -647,6 +657,7 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc
} }
contributionPrefix = appendDestination(container, Path{}.Index(start)) contributionPrefix = appendDestination(container, Path{}.Index(start))
contributionInsideItem = true contributionInsideItem = true
projectSourceVersionEvidence(contribution.processing, contributionPrefix)
} else if contributionInsideItem { } else if contributionInsideItem {
exactDestination := appendDestination(contributionPrefix, contribution.destination) exactDestination := appendDestination(contributionPrefix, contribution.destination)
contribution.source.Destination = exactDestination.clone() contribution.source.Destination = exactDestination.clone()
@@ -656,6 +667,7 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc
sources[position].Destination = exactDestination sources[position].Destination = exactDestination
sources[position].destinationUnresolved = false sources[position].destinationUnresolved = false
} }
projectSourceVersionEvidence(contribution.processing, exactDestination)
} }
if contribution.nested != nil && contribution.nested.acquisition != nil { if contribution.nested != nil && contribution.nested.acquisition != nil {
for _, nestedScope := range contribution.nested.acquisition { for _, nestedScope := range contribution.nested.acquisition {
@@ -667,6 +679,12 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc
projectScope(composed, scope, Path{}, false, "") projectScope(composed, scope, Path{}, false, "")
} }
func projectSourceVersionEvidence(processing *sourceVersionProcessingResult, destination Path) {
if processing != nil && processing.evidence != nil {
processing.evidence.destination = destination.clone()
}
}
func (s *importTraversal) readImport(name, sourceID string, optional bool) ([]byte, bool, *Diagnostic) { func (s *importTraversal) readImport(name, sourceID string, optional bool) ([]byte, bool, *Diagnostic) {
remaining := s.limits.bytes - s.accounting.bytes remaining := s.limits.bytes - s.accounting.bytes
limit := min(int64(maxSourceBytes), remaining) limit := min(int64(maxSourceBytes), remaining)

View File

@@ -154,7 +154,7 @@ func loadWithSecretLookup[T any](source Source, secrets SecretBinding, lookup se
definition: definition, sequencePolicy: policy, overlay: options.Overlay, definition: definition, sequencePolicy: policy, overlay: options.Overlay,
overrides: options.Overrides, ordinary: true, overrides: options.Overrides, ordinary: true,
sourceVersionMode: options.SourceVersionMode, sourceVersionMode: options.SourceVersionMode,
effectiveVersion: effectiveSourceVersionRule(options.Definition.Version, Path{}, options.SourceVersionMode), effectiveVersion: effectiveSourceVersionRuleForPlacement(options.Definition.Version, options.destination, options.SourceVersionMode, options.placementMode),
} }
return executeLoadRequest(request, secrets, lookup) return executeLoadRequest(request, secrets, lookup)
} }

View File

@@ -111,7 +111,7 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
if parsed.source != nil { if parsed.source != nil {
sourceID = parsed.source.ID sourceID = parsed.source.ID
} }
processed, failures := versionState.process(parsed.document, role, destination, sourceID) processed, failures := versionState.process(parsed.document, role, destination, sourceID, options.placementMode)
if len(failures) != 0 { if len(failures) != 0 {
return input, failures return input, failures
} }

View File

@@ -49,7 +49,7 @@ func loadGroupedWithSecretLookup[T any](request loadRequest[T], secrets SecretBi
}}) }})
request.main.destination = mainOptions.destination request.main.destination = mainOptions.destination
diagnostics = append(diagnostics, mainDiagnostics...) diagnostics = append(diagnostics, mainDiagnostics...)
request.effectiveVersion = effectiveSourceVersionRule(request.definition.Version, request.main.destination, request.sourceVersionMode) request.effectiveVersion = effectiveSourceVersionRuleForPlacement(request.definition.Version, request.main.destination, request.sourceVersionMode, request.main.placementMode)
for groupIndex := range request.underlays { for groupIndex := range request.underlays {
for inputIndex := range request.underlays[groupIndex].inputs { for inputIndex := range request.underlays[groupIndex].inputs {
input := &request.underlays[groupIndex].inputs[inputIndex] input := &request.underlays[groupIndex].inputs[inputIndex]
@@ -180,13 +180,6 @@ func acquireUnderlayGroupWithPolicyVersion(group loadGroup, index int, rule *Ver
if acquired.document == nil { if acquired.document == nil {
continue continue
} }
if node := fragmentVersionClaim(acquired.document, rule); node != nil {
location := publicLocation(node.Location)
return nil, sources, contexts, []Diagnostic{{
Code: CodeUnderlayVersion, Severity: SeverityError, Path: rule.Path.clone(),
Message: "underlay inputs cannot contribute to the schema-version path", Primary: &location,
}}
}
scopes = append(scopes, cloneAcquisitionScopes(acquired.acquisition)...) scopes = append(scopes, cloneAcquisitionScopes(acquired.acquisition)...)
} }
var documents []*document.Node var documents []*document.Node
@@ -202,6 +195,17 @@ func acquireUnderlayGroupWithPolicyVersion(group loadGroup, index int, rule *Ver
if len(diagnostics) != 0 { if len(diagnostics) != 0 {
return nil, sources, contexts, diagnostics return nil, sources, contexts, diagnostics
} }
if composition != nil && rule != nil {
for sourceIndex, document := range documents {
if node := fragmentVersionClaimProjected(document, rule, sourceIndex, composition.sequenceRanges); node != nil {
location := publicLocation(node.Location)
return nil, sources, contexts, []Diagnostic{{
Code: CodeUnderlayVersion, Severity: SeverityError, Path: rule.Path.clone(),
Message: "underlay inputs cannot contribute to the schema-version path", Primary: &location,
}}
}
}
}
composition.inputBytes = accounting.bytes - initialBytes composition.inputBytes = accounting.bytes - initialBytes
composition.inputNodes = accounting.nodes - initialNodes composition.inputNodes = accounting.nodes - initialNodes
composition.acquisition = cloneAcquisitionScopes(scopes) composition.acquisition = cloneAcquisitionScopes(scopes)

View File

@@ -136,7 +136,7 @@ func loadSelectedOverlayWithAccessAndVersion(access *filesystemAccess, base *ass
} }
overlayPayload := overlayRoot overlayPayload := overlayRoot
if versionState != nil && versionState.mode != sourceVersionModeDefault { if versionState != nil && versionState.mode != sourceVersionModeDefault {
processed, failures := versionState.process(overlayRoot, sourceVersionContribution, Path{}, sourceID) processed, failures := versionState.process(overlayRoot, sourceVersionContribution, Path{}, sourceID, placementModeMapping)
if len(failures) != 0 { if len(failures) != 0 {
return sources, failures return sources, failures
} }

View File

@@ -91,11 +91,15 @@ func sourceVersionCheckForEvidence(evidence *sourceVersionEvidence, sensitivity
} }
} }
func (state *sourceVersionState) process(root *document.Node, role sourceVersionRole, destination Path, sourceID string) (sourceVersionProcessingResult, []Diagnostic) { func (state *sourceVersionState) process(root *document.Node, role sourceVersionRole, destination Path, sourceID string, placement placementMode) (sourceVersionProcessingResult, []Diagnostic) {
if state == nil || state.mode == sourceVersionModeDefault { if state == nil || state.mode == sourceVersionModeDefault {
return sourceVersionProcessingResult{}, nil return sourceVersionProcessingResult{}, nil
} }
result := processSourceVersion(root, state.localRule, role, destination, state.mode) projectedDestination := destination.clone()
if placement == placementModeSequenceItem {
projectedDestination = projectedDestination.Index(0)
}
result := processSourceVersion(root, state.localRule, role, projectedDestination, state.mode)
if result.evidence != nil { if result.evidence != nil {
result.evidence.sourceID = sourceID result.evidence.sourceID = sourceID
state.evidence = append(state.evidence, result.evidence) state.evidence = append(state.evidence, result.evidence)
@@ -119,6 +123,10 @@ type sourceVersionProcessingResult struct {
} }
func effectiveSourceVersionRule(local *VersionRule, destination Path, mode sourceVersionMode) *VersionRule { func effectiveSourceVersionRule(local *VersionRule, destination Path, mode sourceVersionMode) *VersionRule {
return effectiveSourceVersionRuleForPlacement(local, destination, mode, placementModeMapping)
}
func effectiveSourceVersionRuleForPlacement(local *VersionRule, destination Path, mode sourceVersionMode, placement placementMode) *VersionRule {
if local == nil { if local == nil {
return nil return nil
} }
@@ -126,9 +134,13 @@ func effectiveSourceVersionRule(local *VersionRule, destination Path, mode sourc
return local return local
} }
effective := *local effective := *local
prefix := destination.clone()
if placement == placementModeSequenceItem {
prefix = prefix.Index(0)
}
effective.Path = Path{ effective.Path = Path{
segments: append(clonePathSegments(destination.segments, len(local.Path.segments)), local.Path.segments...), segments: append(clonePathSegments(prefix.segments, len(local.Path.segments)), local.Path.segments...),
invalid: destination.invalid || local.Path.invalid, invalid: prefix.invalid || local.Path.invalid,
} }
return &effective return &effective
} }

View File

@@ -37,6 +37,40 @@ func TestSourceVersionAbsentPathKeepsUnresolvedSegmentIntent(t *testing.T) {
} }
} }
func TestFragmentVersionClaimUsesAssignedSequenceProjection(t *testing.T) {
t.Parallel()
fragment := sourceVersionTestDocument(t, "items: [{version: v1}]\n")
rangeForPeer := []sequenceRange{{sourceIndex: 1, path: Keys("items"), start: 1, end: 2}}
if node := fragmentVersionClaimProjected(fragment, &VersionRule{Path: Keys("items").Index(0).Key("version")}, 1, rangeForPeer); node != nil {
t.Fatalf("peer append claimed main index 0: %#v", node)
}
if node := fragmentVersionClaimProjected(fragment, &VersionRule{Path: Keys("items").Index(1).Key("version")}, 1, rangeForPeer); node == nil || node.Text != "v1" {
t.Fatalf("assigned peer index 1 claim = %#v, want its version node", node)
}
nested := sourceVersionTestDocument(t, "items: [{nested: [{version: nested-v}]}]\n")
nestedRanges := []sequenceRange{
{sourceIndex: 1, path: Keys("items"), start: 1, end: 2},
{sourceIndex: 1, path: Keys("items").Index(0).Key("nested"), start: 2, end: 3},
}
if node := fragmentVersionClaimProjected(nested, &VersionRule{Path: Keys("items").Index(1).Key("nested").Index(2).Key("version")}, 1, nestedRanges); node == nil || node.Text != "nested-v" {
t.Fatalf("nested assigned claim = %#v, want nested version", node)
}
ancestor := sourceVersionTestDocument(t, "items: {version: replacement}\n")
if node := fragmentVersionClaim(ancestor, &VersionRule{Path: Keys("items").Index(0).Key("version")}); node == nil || node.Kind != document.Mapping {
t.Fatalf("destructive sequence ancestor claim = %#v, want mapping node", node)
}
effective := effectiveSourceVersionRuleForPlacement(&VersionRule{Path: Keys("version")}, Keys("catalog", "entries"), sourceVersionModeRequired, placementModeSequenceItem)
if got, want := effective.Path.String(), "/catalog/entries/0/version"; got != want {
t.Fatalf("item effective version path = %q, want %q", got, want)
}
defaultRule := effectiveSourceVersionRuleForPlacement(&VersionRule{Path: Keys("version")}, Keys("catalog", "entries"), sourceVersionModeDefault, placementModeSequenceItem)
if got, want := defaultRule.Path.String(), "/version"; got != want {
t.Fatalf("default effective version path = %q, want %q", got, want)
}
}
type sourceVersionMountedService struct { type sourceVersionMountedService struct {
Version string `yaml:"version"` Version string `yaml:"version"`
} }
@@ -473,6 +507,83 @@ func TestSourceVersionLoadChecksMountedMainLocallyAndProtectsEffectivePath(t *te
} }
} }
func TestSourceVersionItemPlacementKeepsLocalChecksAndProtectsProjectedPath(t *testing.T) {
t.Parallel()
for _, test := range []struct {
name string
load func(Definition[map[string]any]) error
}{
{
name: "ordinary",
load: func(definition Definition[map[string]any]) error {
_, err := Load(Mapping("main", map[string]any{"version": "v1", "name": "ordinary"}), testSecretBinding(), Options[map[string]any]{
Definition: definition, SourceVersionMode: sourceVersionModeRequired,
destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem,
Overrides: []Override{{Path: Keys("catalog", "entries"), Value: []any{}, Flag: "--entries"}},
})
return err
},
},
{
name: "layered",
load: func(definition Definition[map[string]any]) error {
_, err := LoadLayered(Input{Source: Mapping("main", map[string]any{"version": "v1", "name": "layered"}), Destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem}, testSecretBinding(), LayeredOptions[map[string]any]{
Definition: definition, SourceVersionMode: sourceVersionModeRequired,
Overrides: []Override{{Path: Keys("catalog", "entries"), Value: []any{}, Flag: "--entries"}},
})
return err
},
},
} {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
checks := 0
definition := Definition[map[string]any]{Version: &VersionRule{
Path: Keys("version"),
Check: func(version Version, _ *Validation) error {
checks++
if !version.Present || version.Text != "v1" {
t.Errorf("source-local version = %#v, want v1", version)
}
return nil
},
}}
err := test.load(definition)
var loadErr *LoadError
if !errors.As(err, &loadErr) {
t.Fatalf("item placement override error = %T %v, want *LoadError", err, err)
}
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeOverrideVersion, "/catalog/entries")
if checks != 1 {
t.Fatalf("source-version checker called %d times, want once", checks)
}
})
}
}
func TestSourceVersionItemEvidenceUsesProjectedSensitivityPath(t *testing.T) {
t.Parallel()
const canary = "PROJECTED-VERSION-EVIDENCE-CANARY"
secretPath := Keys("catalog", "entries").Index(0).Key("version")
loaded, err := Load(Mapping("main", map[string]any{"version": canary}), testSecretBinding(), Options[map[string]any]{
Definition: Definition[map[string]any]{
Version: &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }},
Secret: func(path Path) bool { return path.String() == secretPath.String() },
},
SourceVersionMode: sourceVersionModeRequired,
destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem,
})
if err != nil {
t.Fatalf("item source-version load: %v", err)
}
sources := loaded.Inspection.Sources()
if len(sources) != 1 || sources[0].VersionCheck == nil || !sources[0].VersionCheck.Redacted || sources[0].VersionCheck.Version.Text != "" {
t.Fatalf("projected source-version evidence was not redacted: %#v", sources)
}
}
func TestSourceVersionLoadChecksNullAndNoSourceMain(t *testing.T) { func TestSourceVersionLoadChecksNullAndNoSourceMain(t *testing.T) {
t.Parallel() t.Parallel()