Build identified record inheritance graphs

This commit is contained in:
2026-10-23 20:23:09 +00:00
parent 36ff9170b7
commit 9d4d2e2a17
3 changed files with 366 additions and 1 deletions

View File

@@ -0,0 +1,200 @@
package engine
import (
"strings"
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
func parseIdentifiedInheritanceSequence(t *testing.T, sourceID, input string) *document.Node {
t.Helper()
var indented strings.Builder
indented.WriteString("jobs:\n")
for _, line := range strings.Split(strings.TrimSuffix(input, "\n"), "\n") {
indented.WriteString(" ")
indented.WriteString(line)
indented.WriteByte('\n')
}
root := parseItemPolicyDocument(t, sourceID, indented.String())
sequence, found := documentAtPath(root, Keys("jobs"))
if !found || sequence.Kind != document.Sequence {
t.Fatalf("identified sequence missing from test document: %#v", root)
}
return sequence
}
func TestIdentifiedInheritanceGraphSchedulesForwardReferencesInPlace(t *testing.T) {
sequence := parseIdentifiedInheritanceSequence(t, "identified-graph", `- id: 2
tenant: acme
extends: {id: 1.0, tenant: acme}
- id: 1e0
tenant: acme
extends: {id: 0, tenant: acme}
- id: 0
tenant: acme
- id: 3
tenant: acme
extends: null
`)
path := Keys("jobs")
graph, diagnostics := analyzeIdentifiedInheritanceGraph(sequence, path, []string{"tenant", "id"})
if len(diagnostics) != 0 {
t.Fatalf("analyze identified graph: %#v", diagnostics)
}
if len(graph.edges) != 2 {
t.Fatalf("edges = %#v, want the two links in the inheritance chain", graph.edges)
}
order, diagnostics := graph.schedule()
if len(diagnostics) != 0 {
t.Fatalf("schedule identified graph: %#v", diagnostics)
}
if len(order) != 4 || order[0] != 2 || order[1] != 1 || order[2] != 0 {
t.Fatalf("schedule = %v, want the forward parent chain before its children", order)
}
if graph.nodes[0].path.String() != "/jobs/0" || graph.nodes[0].item != sequence.Sequence[0] ||
!graph.nodes[3].cancelled || len(graph.nodes[3].directivePath.segments) == 0 {
t.Fatalf("graph nodes changed authored positions or cancellation: %#v", graph.nodes)
}
if _, found := graph.byIdentity[graph.nodes[1].tuple]; !found {
t.Fatal("identity index does not contain the parent record")
}
}
func TestIdentifiedInheritanceRequiresChildIdentityBeforeParentLookup(t *testing.T) {
sequence := parseIdentifiedInheritanceSequence(t, "missing-child-field", `- id: child
extends: {id: parent, tenant: acme}
- id: parent
tenant: acme
`)
_, diagnostics := analyzeIdentifiedInheritanceGraph(sequence, Keys("jobs"), []string{"id", "tenant"})
if len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceInvalid || diagnostics[0].Path.String() != "/jobs/0/tenant" {
t.Fatalf("missing child identity diagnostics = %#v", diagnostics)
}
}
func TestIdentifiedInheritanceRejectsMalformedAndMissingReferences(t *testing.T) {
tests := []struct {
name string
value string
wantCode Code
}{
{name: "string shorthand", value: "extends: parent\n", wantCode: CodeInheritanceInvalid},
{name: "sequence shorthand", value: "extends: [{id: parent, tenant: acme}]\n", wantCode: CodeInheritanceInvalid},
{name: "missing component", value: "extends: {id: parent}\n", wantCode: CodeInheritanceInvalid},
{name: "extra component", value: "extends: {id: parent, tenant: acme, extra: value}\n", wantCode: CodeInheritanceInvalid},
{name: "mapping component", value: "extends: {id: {nested: value}, tenant: acme}\n", wantCode: CodeInheritanceInvalid},
{name: "missing parent", value: "extends: {id: missing, tenant: acme}\n", wantCode: CodeInheritanceMissing},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
sequence := parseIdentifiedInheritanceSequence(t, "bad-reference", "- id: child\n tenant: acme\n "+test.value)
_, diagnostics := analyzeIdentifiedInheritanceGraph(sequence, Keys("jobs"), []string{"id", "tenant"})
if len(diagnostics) != 1 || diagnostics[0].Code != test.wantCode || diagnostics[0].Path.String() != "/jobs/0/extends" && diagnostics[0].Code != CodeInheritanceInvalid {
t.Fatalf("diagnostics = %#v", diagnostics)
}
if diagnostics[0].Code == CodeInheritanceMissing && diagnostics[0].Path.String() != "/jobs/0/extends" {
t.Fatalf("missing-parent path = %s", diagnostics[0].Path)
}
})
}
}
func TestIdentifiedInheritanceGraphFindsSelfAndLongerCycles(t *testing.T) {
for _, input := range []string{
`- id: self
tenant: acme
extends: {id: self, tenant: acme}
`,
`- id: alpha
tenant: acme
extends: {id: beta, tenant: acme}
- id: beta
tenant: acme
extends: {id: gamma, tenant: acme}
- id: gamma
tenant: acme
extends: {id: alpha, tenant: acme}
`,
} {
graph, diagnostics := analyzeIdentifiedInheritanceGraph(parseIdentifiedInheritanceSequence(t, "cycle", input), Keys("jobs"), []string{"id", "tenant"})
if len(diagnostics) != 0 {
t.Fatalf("analyze cycle graph: %#v", diagnostics)
}
_, diagnostics = graph.schedule()
if len(diagnostics) != 1 || diagnostics[0].Code != CodeInheritanceCycle || diagnostics[0].Path.String() == "" {
t.Fatalf("cycle diagnostics = %#v", diagnostics)
}
if diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "cycle" {
t.Fatalf("cycle primary location = %#v", diagnostics[0].Primary)
}
}
}
func TestIdentifiedInheritanceScopesCannotReferenceOtherCollections(t *testing.T) {
other := parseIdentifiedInheritanceSequence(t, "other-collection", `- id: parent
tenant: acme
`)
_, diagnostics := analyzeIdentifiedInheritanceGraph(other, Keys("other"), []string{"id", "tenant"})
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
first := parseIdentifiedInheritanceSequence(t, "first-collection", `- id: child
tenant: acme
extends: {id: parent, tenant: acme}
`)
_, diagnostics = analyzeIdentifiedInheritanceGraph(first, Keys("first"), []string{"id", "tenant"})
if len(diagnostics) != 1 || diagnostics[0].Code != CodeInheritanceMissing {
t.Fatalf("cross-collection reference diagnostics = %#v", diagnostics)
}
}
func TestIdentifiedInheritanceAcceptsStrictlyComposedParentReference(t *testing.T) {
left := parseCompositionDocument(t, "reference-left", "extends: {id: parent}\n")
right := parseCompositionDocument(t, "reference-right", "extends: {tenant: acme}\n")
reference, diagnostics := composeDocuments(left, right)
if len(diagnostics) != 0 {
t.Fatalf("compose disjoint reference helpers: %#v", diagnostics)
}
child, diagnostics := composeDocuments(parseCompositionDocument(t, "child-fields", "id: child\ntenant: acme\n"), reference.root)
if len(diagnostics) != 0 {
t.Fatalf("compose child fields and reference: %#v", diagnostics)
}
parent := parseCompositionDocument(t, "parent", "id: parent\ntenant: acme\n")
sequence := &document.Node{Kind: document.Sequence, Sequence: []*document.Node{child.root, parent}}
graph, diagnostics := analyzeIdentifiedInheritanceGraph(sequence, Keys("jobs"), []string{"id", "tenant"})
if len(diagnostics) != 0 {
t.Fatalf("analyze composed reference: %#v", diagnostics)
}
if len(graph.edges) != 1 || graph.edges[0] != (dependencyEdge{prerequisite: 1, dependent: 0}) {
t.Fatalf("composed reference edge = %#v", graph.edges)
}
_, diagnostics = composeDocuments(
parseCompositionDocument(t, "overlap-a", "extends: {id: first}\n"),
parseCompositionDocument(t, "overlap-b", "extends: {id: second}\n"),
)
if len(diagnostics) != 1 || diagnostics[0].Code != CodeComposeConflict {
t.Fatalf("overlapping reference fields should conflict during strict composition: %#v", diagnostics)
}
}
func TestIdentifiedInheritanceValidatesOnlySurvivingRelationships(t *testing.T) {
lower := parseIdentifiedInheritanceSequence(t, "superseded", `- id: shared
extends: {id: absent, tenant: acme}
`)
higher := parseIdentifiedInheritanceSequence(t, "cancelled", `- id: shared
extends: null
`)
resolution := collectionPolicyResolution{
operation: sequenceModeUpsert, effectiveIdentityFields: []string{"id"},
}
combined, diagnostics := combineSequence(lower, higher, resolution, sequenceCombinationContext{destination: Keys("jobs")})
if len(diagnostics) != 0 {
t.Fatalf("combine surviving reference: %#v", diagnostics)
}
graph, diagnostics := analyzeIdentifiedInheritanceGraph(combined.value, Keys("jobs"), []string{"id"})
if len(diagnostics) != 0 || len(graph.edges) != 0 || !graph.nodes[0].cancelled {
t.Fatalf("superseded missing reference was validated: graph=%#v diagnostics=%#v", graph, diagnostics)
}
}