Retain import inclusion graph during acquisition

This commit is contained in:
2026-10-19 17:34:21 +00:00
parent 1aa121df71
commit fbe9ae6d8b
5 changed files with 377 additions and 38 deletions

View File

@@ -656,6 +656,22 @@ func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) {
}
})
t.Run("cycle through an item scope", func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports:
- path: item.yml
destination: [items]
placement: sequence-item
`)
writeImportFile(t, directory, "item.yml", "configkit: {imports: [{path: main.yml}]}\n")
loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportCycle)
diagnostic := loadError.Diagnostics()[0]
if diagnostic.Primary == nil || len(diagnostic.Related) != 1 || !strings.Contains(diagnostic.Message, "main.yml") || !strings.Contains(diagnostic.Message, "item.yml") {
t.Fatalf("nested item cycle diagnostic = %#v", diagnostic)
}
})
t.Run("diamond", func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: left.yml}, {path: right.yml}]\n")
@@ -686,6 +702,61 @@ func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) {
})
}
func TestItemAcquisitionRetainsInclusionGraphAndScopeOwnership(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports:
- path: item.yml
destination: [items]
placement: sequence-item
`)
itemPath := writeImportFile(t, directory, "item.yml", "configkit: {imports: [{path: helper.yml}]}\nname: item\n")
helperPath := writeImportFile(t, directory, "helper.yml", "id: supplied\n")
input, diagnostics := assembleBaseInput[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
if len(diagnostics) != 0 {
t.Fatalf("assemble input: %#v", diagnostics)
}
graph := input.composition.importGraph
if len(graph.nodes) != 3 || len(graph.edges) != 2 {
t.Fatalf("inclusion graph has %d nodes and %d edges, want 3 and 2", len(graph.nodes), len(graph.edges))
}
for index, want := range []struct {
path string
role importContributionRole
}{
{path: canonicalTestPath(t, mainPath), role: importRoleMain},
{path: canonicalTestPath(t, itemPath), role: importRoleItem},
{path: canonicalTestPath(t, helperPath), role: importRoleMapping},
} {
node := graph.nodes[index]
if node.canonical != want.path || node.role != want.role || node.sourceID != "source-"+strconv.Itoa(index+1) {
t.Errorf("graph node %d = %#v, want %s, %q, source-%d", index, node, want.role, want.path, index+1)
}
}
rootScope := input.composition.acquisition[0]
if rootScope.owner != graph.nodes[0].key || len(rootScope.contributions) != 2 {
t.Fatalf("root acquisition scope = %#v", rootScope)
}
item := rootScope.contributions[1]
if item.identity != graph.nodes[1].key || item.role != importRoleItem || item.scopeOwner != rootScope.owner || item.nested == nil {
t.Fatalf("outer item contribution = %#v", item)
}
itemScope := item.nested.acquisition[0]
if itemScope.owner != item.identity || len(itemScope.contributions) != 2 {
t.Fatalf("nested item scope = %#v", itemScope)
}
for _, contribution := range itemScope.contributions {
if contribution.scopeOwner != item.identity {
t.Errorf("nested contribution scope owner = %#v, want item %v", contribution.scopeOwner, item.identity)
}
}
if graph.edges[0].importer != graph.nodes[0].key || graph.edges[0].imported != graph.nodes[1].key || graph.edges[0].location.SourceID != "source-1" ||
graph.edges[1].importer != graph.nodes[1].key || graph.edges[1].imported != graph.nodes[2].key || graph.edges[1].location.SourceID != "source-2" {
t.Fatalf("inclusion edges = %#v", graph.edges)
}
}
func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
t.Skip("initial filesystem contract covers Linux and macOS")