diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index eb2d22e..7312e38 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -270,7 +270,7 @@ root-scoped upper tiers. No limitations. ## Stage 5 — Verify accumulated resource accounting -Status: planned. Scope: limit regressions and any necessary narrow correction. +Status: complete. Scope: limit regressions and any necessary narrow correction. 1. Audit `placeDocument`, `destinationWrapperNodeCount`, `importAccounting`, and composition totals. Each body uses the full effective destination for @@ -290,6 +290,16 @@ Status: planned. Scope: limit regressions and any necessary narrow correction. Validate: focused import/destination/limit/override tests and everyday check. Done: accumulation cannot bypass current bounds or falsely charge absent input. +Evidence (2026-10-14): based on `430985c6293ca8f9151149f1af34481ac325eed3`, +using `go1.26.8 linux/amd64`. The focused engine import/glob/destination/grouped/ +overlay/override/history/inheritance suite and `./scripts/check.sh` passed. +Exact, below, and above node-budget fixtures cover nested empty imports and +multiple glob matches using their accumulated destinations. Additional cases +verify accumulated depth bounds and that absent optional path/glob contributions +at unused depth add no source or wrapper charge. Existing +`TestSelectedOverlayAndCLIUsePlacementNodeCharges` continues to cover later +overlay/CLI accounting. No limitations. + ## Stage 6 — Preserve diagnostics, source inspection, and resource contexts Status: planned. Scope: coordinates, detached records, and resource ownership. diff --git a/internal/engine/destination_accounting_test.go b/internal/engine/destination_accounting_test.go index 7bece4e..7814fb9 100644 --- a/internal/engine/destination_accounting_test.go +++ b/internal/engine/destination_accounting_test.go @@ -1,6 +1,8 @@ package engine import ( + "os" + "path/filepath" "testing" "testing/fstest" @@ -120,6 +122,207 @@ func TestImportedEmptyBodiesChargeEachDestinationWrapper(t *testing.T) { } } +func TestImportedEmptyBodiesChargeFullRelativeDestinations(t *testing.T) { + t.Parallel() + + directory := t.TempDir() + mainPath := writeImportFile(t, directory, "main.yml", `configkit: + imports: + - path: empty.yml + destination: [groups, empty] +`) + fragmentPath := writeImportFile(t, directory, "empty.yml", "{}\n") + main := parseImportMain(t, mainPath) + fragment, diagnostics := parseSource(File(fragmentPath), "source-2", defaultParseLimits()) + if len(diagnostics) != 0 { + t.Fatalf("empty fragment diagnostics = %#v", diagnostics) + } + entryDestination := Keys("collections", "entry") + importDestination := appendDestination(entryDestination, Keys("groups", "empty")) + wantNodes := countInputNodes(main.document) + countInputNodes(fragment.document) + + destinationWrapperNodeCount(entryDestination) + destinationWrapperNodeCount(importDestination) + limits := defaultImportLimits() + + for _, test := range []struct { + name string + delta int + wantError bool + }{ + {name: "below aggregate bound", delta: -1, wantError: true}, + {name: "at aggregate bound"}, + {name: "above aggregate bound", delta: 1}, + } { + t.Run(test.name, func(t *testing.T) { + currentLimits := limits + currentLimits.nodes = wantNodes + test.delta + accounting := &importAccounting{} + composed, sources, diagnostics := loadImportedDocumentsTracked( + File(mainPath), nil, main, entryDestination, nil, currentLimits, accounting, nil, nil, + ) + if test.wantError { + if composed != nil || len(diagnostics) != 1 || diagnostics[0].Code != CodeImportLimitExceeded { + t.Fatalf("below-bound relative placement = %#v, %#v", composed, diagnostics) + } + return + } + if len(diagnostics) != 0 || composed == nil || len(sources) != 2 { + t.Fatalf("bounded relative placement = %#v, %#v, %#v", composed, sources, diagnostics) + } + if composed.inputNodes != wantNodes || accounting.nodes != wantNodes { + t.Fatalf("relative placement node charge = %d/%d, want %d", composed.inputNodes, accounting.nodes, wantNodes) + } + if sources[1].Destination.String() != importDestination.String() { + t.Fatalf("empty import destination = %q, want %q", sources[1].Destination.String(), importDestination.String()) + } + if node, present := documentAtPath(composed.root, importDestination); !present || node.Kind != document.Mapping || len(node.Mapping) != 0 { + t.Fatalf("empty relative import body = %#v, present %v", node, present) + } + }) + } +} + +func TestGlobMatchesChargeTheirSharedRelativeDestination(t *testing.T) { + t.Parallel() + + directory := t.TempDir() + mainPath := writeImportFile(t, directory, "main.yml", `configkit: + imports: + - glob: fragments/*.yml + destination: [groups, empty] +`) + fragmentPaths := []string{ + writeImportFile(t, directory, "fragments/a.yml", "{}\n"), + writeImportFile(t, directory, "fragments/b.yml", "{}\n"), + writeImportFile(t, directory, "fragments/c.yml", "{}\n"), + } + main := parseImportMain(t, mainPath) + fragment, diagnostics := parseSource(File(fragmentPaths[0]), "source-fragment", defaultParseLimits()) + if len(diagnostics) != 0 { + t.Fatalf("glob fragment diagnostics = %#v", diagnostics) + } + entryDestination := Keys("collections", "entry") + importDestination := appendDestination(entryDestination, Keys("groups", "empty")) + wantNodes := countInputNodes(main.document) + len(fragmentPaths)*countInputNodes(fragment.document) + + destinationWrapperNodeCount(entryDestination) + len(fragmentPaths)*destinationWrapperNodeCount(importDestination) + limits := defaultImportLimits() + + for _, test := range []struct { + name string + delta int + wantError bool + }{ + {name: "below aggregate bound", delta: -1, wantError: true}, + {name: "at aggregate bound"}, + {name: "above aggregate bound", delta: 1}, + } { + t.Run(test.name, func(t *testing.T) { + currentLimits := limits + currentLimits.nodes = wantNodes + test.delta + accounting := &importAccounting{} + composed, sources, diagnostics := loadImportedDocumentsTracked( + File(mainPath), nil, main, entryDestination, nil, currentLimits, accounting, nil, nil, + ) + if test.wantError { + if composed != nil || len(diagnostics) != 1 || diagnostics[0].Code != CodeImportLimitExceeded { + t.Fatalf("below-bound glob placement = %#v, %#v", composed, diagnostics) + } + return + } + if len(diagnostics) != 0 || composed == nil || len(sources) != len(fragmentPaths)+1 { + t.Fatalf("bounded glob placement = %#v, %#v, %#v", composed, sources, diagnostics) + } + if composed.inputNodes != wantNodes || accounting.nodes != wantNodes { + t.Fatalf("glob relative placement node charge = %d/%d, want %d", composed.inputNodes, accounting.nodes, wantNodes) + } + for index, source := range sources[1:] { + if source.Destination.String() != importDestination.String() { + t.Errorf("glob source %d destination = %q, want %q", index, source.Destination.String(), importDestination.String()) + } + } + if node, present := documentAtPath(composed.root, importDestination); !present || node.Kind != document.Mapping || len(node.Mapping) != 0 { + t.Fatalf("empty glob contributions = %#v, present %v", node, present) + } + }) + } +} + +func TestRelativeImportDestinationUsesAccumulatedDocumentDepth(t *testing.T) { + t.Parallel() + + directory := t.TempDir() + mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: fragment.yml, destination: [extra]}]}\n") + writeImportFile(t, directory, "fragment.yml", "value: nested\n") + destination := Path{segments: make([]pathSegment, maxDocumentDepth)} + for index := range destination.segments { + destination.segments[index] = pathSegment{kind: pathSegmentKey, key: "entry"} + } + _, err := LoadLayered( + Input{Source: File(mainPath), Imports: true, Destination: destination}, testSecretBinding(), + LayeredOptions[map[string]any]{}, + ) + loadError, ok := err.(*LoadError) + if !ok || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != CodeImportLimitExceeded { + t.Fatalf("accumulated depth result = %v", err) + } + sources := loadError.Sources() + if len(sources) != 2 || len(sources[0].Destination.segments) != maxDocumentDepth || len(sources[1].Destination.segments) != maxDocumentDepth+1 { + t.Fatalf("depth failure source destinations = %#v", sources) + } +} + +func TestAbsentOptionalImportsDoNotChargeUnusedDeepDestinations(t *testing.T) { + t.Parallel() + + destination := Path{segments: make([]pathSegment, maxDocumentDepth)} + for index := range destination.segments { + destination.segments[index] = pathSegment{kind: pathSegmentKey, key: "entry"} + } + + for _, test := range []struct { + name string + body string + setup func(*testing.T, string) + }{ + { + name: "optional missing path", + body: "configkit: {imports: [{path: missing.yml, optional: true, destination: [unused]}]}\n", + }, + { + name: "optional unmatched glob", + body: "configkit: {imports: [{glob: empty/*.yml, optional: true, destination: [unused]}]}\n", + setup: func(t *testing.T, directory string) { + if err := os.Mkdir(filepath.Join(directory, "empty"), 0o755); err != nil { + t.Fatal(err) + } + }, + }, + } { + t.Run(test.name, func(t *testing.T) { + directory := t.TempDir() + if test.setup != nil { + test.setup(t, directory) + } + mainPath := writeImportFile(t, directory, "main.yml", test.body) + main := parseImportMain(t, mainPath) + limits := defaultImportLimits() + accounting := &importAccounting{} + composed, sources, diagnostics := loadImportedDocumentsTracked( + File(mainPath), nil, main, destination, nil, limits, accounting, nil, nil, + ) + if len(diagnostics) != 0 || composed == nil || len(sources) != 1 { + t.Fatalf("absent optional import = %#v, %#v, %#v", composed, sources, diagnostics) + } + wantNodes := countInputNodes(main.document) + destinationWrapperNodeCount(destination) + if composed.inputNodes != wantNodes || accounting.nodes != wantNodes { + t.Fatalf("absent optional node charge = %d/%d, want %d", composed.inputNodes, accounting.nodes, wantNodes) + } + if node, present := documentAtPath(composed.root, destination); !present || node.Kind != document.Mapping || len(node.Mapping) != 0 { + t.Fatalf("empty entry body = %#v, present %v", node, present) + } + }) + } +} + func TestNoSourceAndRootPlacementAddNoWrapperCharges(t *testing.T) { t.Parallel()