package configkit_test import ( "bytes" "errors" "strings" "testing" "testing/fstest" configkit "go.maximumdirect.net/configkit" ) func TestFacadeCompositionConflictKeepsClaimantOrder(t *testing.T) { filesystem := configkit.NewFilesystem("bundle", fstest.MapFS{ "a.yml": &fstest.MapFile{Data: []byte(`config: imports: - path: z.yml `)}, "b.yml": &fstest.MapFile{Data: []byte("x: b\n")}, "c.yml": &fstest.MapFile{Data: []byte("x: c\n")}, "z.yml": &fstest.MapFile{Data: []byte("x: z\n")}, }) main := configkit.Mapping("main", map[string]any{ "config": map[string]any{"imports": []any{ map[string]any{"path": "a.yml"}, map[string]any{"path": "b.yml"}, map[string]any{"path": "c.yml"}, }}, }) _, err := configkit.Load[map[string]any](main, configkit.Options[map[string]any]{ Imports: true, Filesystem: &configkit.FilesystemContext{ FS: &filesystem, Root: ".", BaseDir: ".", }, }) var loadError *configkit.LoadError if !errors.As(err, &loadError) { t.Fatalf("Load error = %v, want composition LoadError", err) } diagnostics := loadError.Diagnostics() if len(diagnostics) != 1 { t.Fatalf("diagnostics = %#v, want one composition conflict", diagnostics) } diagnostic := diagnostics[0] if diagnostic.Code != configkit.CodeComposeConflict || diagnostic.Path.String() != "/x" || diagnostic.Primary == nil { t.Fatalf("composition diagnostic = %#v", diagnostic) } pathsByID := make(map[string]string) var acquisitionOrder []string for _, source := range loadError.Sources() { pathsByID[source.ID] = source.Path for _, name := range []string{"z.yml", "b.yml", "c.yml"} { if source.Path == name || strings.HasSuffix(source.Path, "/"+name) { acquisitionOrder = append(acquisitionOrder, strings.TrimSuffix(name, ".yml")) break } } } if strings.Join(acquisitionOrder, ",") != "z,b,c" { t.Fatalf("claimant acquisition order = %v, want z,b,c", acquisitionOrder) } claimant := func(location configkit.Location) string { for _, name := range []string{"b.yml", "c.yml", "z.yml"} { if pathsByID[location.SourceID] == name || strings.HasSuffix(pathsByID[location.SourceID], "/"+name) { return strings.TrimSuffix(name, ".yml") } } return "" } if got := claimant(*diagnostic.Primary); got != "b" { t.Fatalf("primary claimant = %q, want b (sources: %#v)", got, pathsByID) } if len(diagnostic.Related) != 2 || claimant(diagnostic.Related[0]) != "c" || claimant(diagnostic.Related[1]) != "z" { t.Fatalf("related claimants = %#v, want c then z", diagnostic.Related) } var output bytes.Buffer if err := configkit.WriteDiagnostics(&output, diagnostics); err != nil { t.Fatal(err) } positions := []int{ strings.Index(output.String(), diagnostic.Primary.SourceID), strings.Index(output.String(), diagnostic.Related[0].SourceID), strings.Index(output.String(), diagnostic.Related[1].SourceID), } if positions[0] < 0 || positions[1] <= positions[0] || positions[2] <= positions[1] { t.Fatalf("rendered claimant order = %v in %q", positions, output.String()) } diagnostics[0].Related[0].SourceID = "mutated" if got := claimant(loadError.Diagnostics()[0].Related[0]); got != "c" { t.Fatalf("LoadError.Diagnostics retained caller mutation; first claimant = %q", got) } }