Files
configkit/internal/engine/history_budget_test.go

283 lines
11 KiB
Go

package engine
import (
"testing"
"go.maximumdirect.net/configkit/internal/document"
)
type historyBudgetSimpleConfig struct {
Value string
}
type historyBudgetRecord struct {
Value string
Credential Secret
}
type historyBudgetLayeredConfig struct {
Parent historyBudgetRecord
Child historyBudgetRecord
}
func retainedInspectionNodeCountForTest(inspection *Inspection) int {
seen := make(map[*document.Node]struct{})
model := inspection.history
stack := []*document.Node{model.baseline, model.final, inspection.final}
for _, payload := range model.sourcePayloads {
if payload.kind == historySourceApplicationPayload {
stack = append(stack, payload.application.value)
}
}
for _, payload := range model.inheritancePayloads {
for _, operation := range payload.operations {
stack = append(stack, operation.parent, operation.effective)
}
}
for _, payload := range model.cliPayloads {
if payload.kind == historySourceApplicationPayload {
stack = append(stack, payload.application.value)
}
}
if model.preCLICheckpoint != nil {
stack = append(stack, model.preCLICheckpoint.root)
}
if model.postCLICheckpoint != nil {
stack = append(stack, model.postCLICheckpoint.root)
}
if model.resolutionPayload != nil {
for _, snapshot := range model.resolutionPayload.snapshots {
stack = append(stack, snapshot.before, snapshot.after)
}
}
for len(stack) != 0 {
last := len(stack) - 1
node := stack[last]
stack = stack[:last]
if node == nil {
continue
}
if _, exists := seen[node]; exists {
continue
}
seen[node] = struct{}{}
for _, entry := range node.Mapping {
stack = append(stack, entry.Value)
}
stack = append(stack, node.Sequence...)
}
return len(seen)
}
func TestLoadHistoryBudgetUsesExactRetainedNodeCount(t *testing.T) {
load := func(historyLimit int, validations *int) (*Loaded[historyBudgetSimpleConfig], error) {
return loadWithSecretLookupLimit(
Bytes("main", []byte("value: retained\n")),
testSecretBinding(),
func(string) (string, bool) { return "", false },
historyLimit,
Options[historyBudgetSimpleConfig]{Definition: Definition[historyBudgetSimpleConfig]{
Validate: func(*historyBudgetSimpleConfig, *Validation) { *validations++ },
}},
)
}
var baselineValidations int
baseline, err := load(10000, &baselineValidations)
if err != nil {
t.Fatalf("baseline load: %v", err)
}
want := retainedInspectionNodeCountForTest(baseline.Inspection)
if want == 0 {
t.Fatal("independent retained-tree count was empty")
}
var exactValidations int
loaded, err := load(want, &exactValidations)
if err != nil || loaded == nil {
t.Fatalf("load at exact retained-node count %d: result=%v error=%v", want, loaded, err)
}
if got := retainedInspectionNodeCountForTest(loaded.Inspection); got != want {
t.Fatalf("retained node count = %d, want %d", got, want)
}
if exactValidations != 1 {
t.Fatalf("validation calls at exact count = %d, want 1", exactValidations)
}
var rejectedValidations int
loaded, err = load(want-1, &rejectedValidations)
if loaded != nil || !hasHistoryLimitDiagnostic(t, err) {
t.Fatalf("load below retained-node count: result=%v error=%v", loaded, err)
}
if rejectedValidations != 0 {
t.Fatalf("validation ran before history rejection: %d calls", rejectedValidations)
}
}
func TestHistoryNodeBudgetIncludesFinalInspectionProjection(t *testing.T) {
final := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{
Key: "value", Value: &document.Node{Kind: document.String, Text: "final"},
}}}
builder := newHistoryBuilder()
model := builder.freeze()
inspection := &Inspection{history: model, metadata: Metadata{historyModel: model}, final: final}
counter := newRetainedNodeCounter(2)
admitted := addInspectionHistory(counter, inspection)
if !admitted || counter.count != 2 {
t.Fatalf("final inspection projection was not counted exactly: admitted=%t count=%d", admitted, counter.count)
}
counter = newRetainedNodeCounter(1)
if addInspectionHistory(counter, inspection) {
t.Fatal("final inspection projection did not consume the node budget")
}
}
func TestLayeredHistoryBudgetRejectsBeforeSecretsAndValidation(t *testing.T) {
type callbackCounts struct {
lookups int
validations int
}
makeRequest := func(counts *callbackCounts) loadRequest[historyBudgetLayeredConfig] {
return loadRequest[historyBudgetLayeredConfig]{
main: loadInput{source: Bytes("main", []byte("child:\n extends: parent\n"))},
underlays: []loadGroup{{inputs: []loadInput{{source: Bytes("underlay", []byte(
"parent:\n value: inherited\n credential: HISTORY_BUDGET_SECRET\n",
))}}}},
overrides: []Override{{Path: Keys("child", "value"), Value: "cli", Flag: "--child-value"}},
definition: Definition[historyBudgetLayeredConfig]{
Resolve: func(configuration *historyBudgetLayeredConfig, resolution *Resolution) error {
configuration.Child.Value += "-resolved"
resolution.RecordNormalization(Keys("child", "value"), "normalize child")
resolution.RecordDerivedDefault(Keys("child"), "record enclosing child state")
return nil
},
Validate: func(*historyBudgetLayeredConfig, *Validation) { counts.validations++ },
},
}
}
lookup := func(counts *callbackCounts) secretLookup {
return func(name string) (string, bool) {
counts.lookups++
if name != "HISTORY_BUDGET_SECRET" {
t.Errorf("secret lookup name = %q", name)
}
return "resolved-secret", true
}
}
baselineCounts := &callbackCounts{}
baseline, err := loadGroupedWithSecretLookupLimit(makeRequest(baselineCounts), testSecretBinding(), lookup(baselineCounts), 10000)
if err != nil {
t.Fatalf("baseline grouped load: %v", err)
}
want := retainedInspectionNodeCountForTest(baseline.Inspection)
if want == 0 {
t.Fatal("independent retained-tree count was empty")
}
if baseline.Inspection.history == nil || baseline.Inspection.metadata.historyModel != baseline.Inspection.history || len(baseline.Inspection.history.inheritancePayloads) == 0 {
t.Fatal("layered load did not publish its shared inheritance evidence owner")
}
resolution := baseline.Inspection.history.resolutionPayload
if resolution == nil || len(resolution.snapshots) != 2 || len(resolution.replayGroups) == 0 {
t.Fatalf("layered load did not retain overlapping original resolution evidence: %#v", resolution)
}
resolutionPaths := make(map[string]bool, len(resolution.snapshots))
for _, snapshot := range resolution.snapshots {
resolutionPaths[snapshot.change.Path.String()] = true
}
if !resolutionPaths["/child"] || !resolutionPaths["/child/value"] {
t.Fatalf("layered load resolution snapshots have paths %v, want /child and /child/value", resolutionPaths)
}
group := resolution.replayGroups[0]
groupRecords := make(map[int]bool, len(group.originalRecordIDs))
for _, id := range group.originalRecordIDs {
groupRecords[id] = true
}
if len(groupRecords) != 2 || !groupRecords[0] || !groupRecords[1] || group.change.Path.String() != "/child" {
t.Fatalf("layered load did not group both overlapping original records: %#v", group)
}
exactCounts := &callbackCounts{}
loaded, err := loadGroupedWithSecretLookupLimit(makeRequest(exactCounts), testSecretBinding(), lookup(exactCounts), want)
if err != nil || loaded == nil {
t.Fatalf("grouped load at exact retained-node count %d: result=%v error=%v", want, loaded, err)
}
if got := retainedInspectionNodeCountForTest(loaded.Inspection); got != want {
t.Fatalf("grouped retained node count = %d, want %d", got, want)
}
if loaded.Config.Child.Value != "cli-resolved" || exactCounts.lookups != 1 || exactCounts.validations != 1 {
t.Fatalf("grouped lifecycle result=%#v callbacks=%#v", loaded.Config, exactCounts)
}
rejectedCounts := &callbackCounts{}
loaded, err = loadGroupedWithSecretLookupLimit(makeRequest(rejectedCounts), testSecretBinding(), lookup(rejectedCounts), want-1)
if loaded != nil || !hasHistoryLimitDiagnostic(t, err) {
t.Fatalf("grouped load below retained-node count: result=%v error=%v", loaded, err)
}
if rejectedCounts.lookups != 0 || rejectedCounts.validations != 0 {
t.Fatalf("callbacks ran before history rejection: %#v", rejectedCounts)
}
}
func TestPreparedInheritanceEvidenceSharesOwnerAndDetachesPublicViews(t *testing.T) {
input := parseCompositionDocument(t, "inheritance", "parent: {value: shared}\nchild: {extends: parent}\n")
expanded, diagnostics := expandInheritanceWithOrigins(input, nil, defaultProjectionLimits())
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
baseline := &document.Node{Kind: document.Mapping}
builder := newHistoryBuilder()
builder.observeBaseline(baseline)
builder.setScratch(input)
builder.observeInheritanceEvidence(expanded.operations)
builder.observePreCLICheckpoint(expanded.root, expanded.origins)
builder.observePostCLICheckpoint(expanded.root, expanded.origins)
builder.prefixReady = true
metadata := Metadata{historyBuilder: builder}
resolved := resolvedLoad[historyBudgetSimpleConfig]{
metadata: metadata, final: expanded.root,
}
counter := newRetainedNodeCounter(100)
inspection, ok := prepareInspectionHistory(resolved, counter)
if !ok {
t.Fatal("preparation rejected the shared evidence owner")
}
if inspection.history == nil || inspection.metadata.historyModel != inspection.history || len(inspection.history.inheritancePayloads) != 1 {
t.Fatalf("history owners = inspection %p, metadata %p, inheritance payloads %d", inspection.history, inspection.metadata.historyModel, len(inspection.history.inheritancePayloads))
}
path := Keys("child", "value")
first := inspection.Metadata().History(path)
if len(first) == 0 {
t.Fatal("metadata projection omitted inherited history")
}
first[0].Origin.Locations[0].SourceID = "mutated metadata result"
first[0].Dependencies[0].segments[0].key = "mutated metadata dependency"
if explanation, found := inspection.Explain(path); !found || len(explanation.History) == 0 || explanation.History[0].Origin.Locations[0].SourceID == "mutated metadata result" {
t.Fatalf("metadata result mutation leaked into explanation: %#v, %v", explanation, found)
}
explanation, _ := inspection.Explain(path)
explanation.History[0].Origin.Locations[0].SourceID = "mutated explanation result"
second := inspection.Metadata().History(path)
if second[0].Origin.Locations[0].SourceID == "mutated explanation result" || second[0].Dependencies[0].String() == "/mutated metadata dependency" {
t.Fatalf("explanation mutation leaked into metadata: %#v", second)
}
}
func hasHistoryLimitDiagnostic(t *testing.T, err error) bool {
t.Helper()
if err == nil {
return false
}
loadErr, ok := err.(*LoadError)
if !ok {
t.Fatalf("error type = %T, want *LoadError", err)
}
for _, diagnostic := range loadErr.Diagnostics() {
if diagnostic.Code == CodeHistoryLimitExceeded {
return true
}
}
return false
}