package ckconfig import ( "context" "io" "strings" "testing" commandkit "gitea.maximumdirect.net/eric/CommandKit" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) func TestComparisonSelectionInheritsFieldsIndependently(t *testing.T) { leftConfig := commandkit.Supplied[string]{Value: "before.yml", Set: true} leftOverlay := commandkit.Supplied[string]{Value: "base", Set: true} tests := []struct { name string left Selection right Selection want Selection }{ { name: "omitted fields stay omitted", }, { name: "both fields inherit supplied state", left: Selection{Config: leftConfig, Overlay: leftOverlay}, want: Selection{Config: leftConfig, Overlay: leftOverlay}, }, { name: "explicit empty config overrides while overlay inherits", left: Selection{Config: leftConfig, Overlay: leftOverlay}, right: Selection{Config: commandkit.Supplied[string]{Set: true}}, want: Selection{Config: commandkit.Supplied[string]{Set: true}, Overlay: leftOverlay}, }, { name: "explicit overlay overrides while config inherits", left: Selection{Config: leftConfig, Overlay: leftOverlay}, right: Selection{Overlay: commandkit.Supplied[string]{Value: "candidate", Set: true}}, want: Selection{Config: leftConfig, Overlay: commandkit.Supplied[string]{Value: "candidate", Set: true}}, }, { name: "explicit empty overlay remains supplied", left: Selection{Config: leftConfig, Overlay: leftOverlay}, right: Selection{Overlay: commandkit.Supplied[string]{Set: true}}, want: Selection{Config: leftConfig, Overlay: commandkit.Supplied[string]{Set: true}}, }, { name: "both explicit right fields replace left", left: Selection{Config: leftConfig, Overlay: leftOverlay}, right: Selection{Config: commandkit.Supplied[string]{Value: "after.yml", Set: true}, Overlay: commandkit.Supplied[string]{Value: "new", Set: true}}, want: Selection{Config: commandkit.Supplied[string]{Value: "after.yml", Set: true}, Overlay: commandkit.Supplied[string]{Value: "new", Set: true}}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if got := inheritComparisonSelection(test.left, test.right); got != test.want { t.Fatalf("inherited selection = %#v, want %#v", got, test.want) } }) } } func TestComparisonHelpLimitsFlagsAndDefaultsFormatToYAML(t *testing.T) { scope := Scope[struct{}, struct{}]{ Name: "app", Operations: []Operation{Diff, Show, Explain, Validate, Sources}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) { return commandkit.Acquired[View]{}, nil }, Compare: func(context.Context, ComparisonRequest[struct{}], struct{}) (commandkit.Acquired[[]configkit.Difference], error) { return commandkit.Acquired[[]configkit.Difference]{}, nil }, } app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}})) help := func(args ...string) string { t.Helper() var output strings.Builder app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: io.Discard}) return output.String() } diffHelp := help("config", "diff", "--help") if !strings.Contains(diffHelp, "--against-config") || !strings.Contains(diffHelp, "--against-overlay") { t.Fatalf("diff help omits right-side selection flags:\n%s", diffHelp) } if !strings.Contains(diffHelp, "default: yaml") { t.Fatalf("diff help does not show the YAML default:\n%s", diffHelp) } for _, operation := range []string{"validate", "show", "sources", "explain"} { operationHelp := help("config", operation, "--help") if strings.Contains(operationHelp, "--against-config") || strings.Contains(operationHelp, "--against-overlay") { t.Errorf("%s exposes comparison-only flags:\n%s", operation, operationHelp) } if operation == "show" && !strings.Contains(operationHelp, "default: yaml") { t.Errorf("show format default changed:\n%s", operationHelp) } if operation != "show" && !strings.Contains(operationHelp, "default: text") { t.Errorf("%s format default changed:\n%s", operation, operationHelp) } if operation != "explain" && strings.Contains(operationHelp, "--path") { t.Errorf("%s exposes the explain-only path flag:\n%s", operation, operationHelp) } } explainHelp := help("config", "explain", "--help") if !strings.Contains(explainHelp, "--path") { t.Fatalf("explain flags changed:\n%s", explainHelp) } } func TestComparisonPreparationAcceptsOnlyYAML(t *testing.T) { inputCalls, stateCalls := 0, 0 prepared := captureSuite(Suite[struct{}, struct{}]{ NewInput: func() struct{} { inputCalls++; return struct{}{} }, NewState: func() struct{} { stateCalls++; return struct{}{} }, Scopes: []Scope[struct{}, struct{}]{{ Name: "app", Operations: []Operation{Diff}, Compare: func(context.Context, ComparisonRequest[struct{}], struct{}) (commandkit.Acquired[[]configkit.Difference], error) { return commandkit.Acquired[[]configkit.Difference]{}, nil }, }}, }) input := &configCommandInput[struct{}, struct{}]{ selection: Selection{Config: commandkit.Supplied[string]{Value: "before.yml", Set: true}}, against: Selection{Overlay: commandkit.Supplied[string]{Value: "candidate", Set: true}}, format: commandkit.Supplied[string]{Value: "yaml", Set: true}, } if err := prepareConfigOperation(prepared, Diff, input); err != nil { t.Fatalf("explicit YAML format rejected: %v", err) } if input.plan == nil || input.plan.format != "yaml" { t.Fatalf("comparison plan = %#v", input.plan) } if input.plan.scopeName != "app" { t.Fatalf("comparison default scope = %q, want app", input.plan.scopeName) } if want := (Selection{ Config: commandkit.Supplied[string]{Value: "before.yml", Set: true}, Overlay: commandkit.Supplied[string]{Value: "candidate", Set: true}, }); input.against != want { t.Fatalf("resolved right selection = %#v, want %#v", input.against, want) } for _, format := range []string{"text", "json"} { input := &configCommandInput[struct{}, struct{}]{format: commandkit.Supplied[string]{Value: format}} if err := prepareConfigOperation(prepared, Diff, input); err == nil { t.Errorf("comparison format %q was accepted", format) } } if inputCalls != 2 || stateCalls != 0 { t.Fatalf("comparison preparation calls input/state: %d/%d", inputCalls, stateCalls) } } func TestComparisonAvailabilityPreflightPrecedesFactories(t *testing.T) { inputCalls, stateCalls, acquireCalls, compareCalls := 0, 0, 0, 0 prepared := captureSuite(Suite[struct{}, struct{}]{ NewInput: func() struct{} { inputCalls++; return struct{}{} }, NewState: func() struct{} { stateCalls++; return struct{}{} }, Scopes: []Scope[struct{}, struct{}]{ {Name: "app", Operations: []Operation{Diff}, Compare: func(context.Context, ComparisonRequest[struct{}], struct{}) (commandkit.Acquired[[]configkit.Difference], error) { compareCalls++ return commandkit.Acquired[[]configkit.Difference]{}, nil }}, {Name: "plain", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) { acquireCalls++ return commandkit.Acquired[View]{}, nil }}, }, }) for _, scope := range []string{"missing", "all", "plain"} { input := &configCommandInput[struct{}, struct{}]{ scope: commandkit.Supplied[string]{Value: scope, Set: true}, format: commandkit.Supplied[string]{Value: "yaml"}, } if err := prepareConfigOperation(prepared, Diff, input); err == nil { t.Errorf("comparison scope %q was accepted", scope) } } if inputCalls != 0 || stateCalls != 0 || acquireCalls != 0 || compareCalls != 0 { t.Fatalf("failed preflight invoked suite work: input=%d state=%d acquire=%d compare=%d", inputCalls, stateCalls, acquireCalls, compareCalls) } }