package ckconfig import ( "bytes" "context" "errors" "fmt" "io" "strings" "testing" commandkit "gitea.maximumdirect.net/eric/CommandKit" configkit "gitea.maximumdirect.net/eric/ConfigKit" "go.yaml.in/yaml/v4" ) func TestConfigDiffExecutesWithDefaultAndExplicitYAMLAndInheritedSelections(t *testing.T) { type settings struct { Greeting string `yaml:"greeting"` } var selections []Selection app := newConfigTestApp(t, AppCommand(func(_ context.Context, selection Selection) (*configkit.Result[settings], error) { selections = append(selections, selection) value := "Hello" if selection.Config.Set && selection.Config.Value == "after.yml" { value = "Goodbye" } return configkit.Load(configkit.Mapping("selected configuration", map[string]any{"greeting": value}), configkit.Options[settings]{}) })) var output bytes.Buffer outcome := app.Execute(context.Background(), []string{"config", "diff", "--config", "before.yml", "--overlay", "base", "--against-config", "after.yml"}, commandkit.Streams{Out: &output, Err: io.Discard}) if outcome.Kind != commandkit.OutcomeSuccess { t.Fatalf("default YAML comparison outcome = %#v", outcome) } if len(selections) != 2 || !selections[0].Config.Set || selections[0].Config.Value != "before.yml" || !selections[1].Config.Set || selections[1].Config.Value != "after.yml" || selections[1].Overlay != selections[0].Overlay { t.Fatalf("comparison selections = %#v", selections) } var document yaml.Node if err := yaml.Unmarshal(output.Bytes(), &document); err != nil { t.Fatalf("default report is not YAML: %q: %v", output.String(), err) } changes := reportField(document.Content[0], "changes") if value := reportField(changes, "greeting"); value == nil || value.Value != "Goodbye" { t.Fatalf("public comparison output = %#v", document.Content[0]) } output.Reset() outcome = app.Execute(context.Background(), []string{"config", "diff", "--format", "yaml", "--config", "before.yml", "--against-config", "before.yml"}, commandkit.Streams{Out: &output, Err: io.Discard}) if outcome.Kind != commandkit.OutcomeSuccess { t.Fatalf("explicit YAML comparison outcome = %#v", outcome) } var equal yaml.Node if err := yaml.Unmarshal(output.Bytes(), &equal); err != nil { t.Fatalf("explicit YAML report is not YAML: %q: %v", output.String(), err) } if len(reportField(equal.Content[0], "additions").Content) != 0 || len(reportField(equal.Content[0], "changes").Content) != 0 || len(reportField(equal.Content[0], "deletions").Content) != 0 { t.Fatalf("equal comparison did not return empty categories: %q", output.String()) } loadsBeforeText := len(selections) output.Reset() outcome = app.Execute(context.Background(), []string{"config", "diff", "--format", "text"}, commandkit.Streams{Out: &output, Err: io.Discard}) if outcome.Kind != commandkit.OutcomeUsage || len(selections) != loadsBeforeText || output.Len() != 0 { t.Fatalf("text format outcome/work/output = %#v / %d / %q", outcome, len(selections)-loadsBeforeText, output.String()) } } func TestConfigDiffRunsComparisonOnlyScopeAndRejectsUnsupportedScopeBeforeWork(t *testing.T) { var inputCalls, stateCalls, acquireCalls, compareCalls int app := newConfigTestApp(t, Command(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{Show}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) { acquireCalls++ return commandkit.Acquired[View]{}, nil }}, }, })) var output bytes.Buffer outcome := app.Execute(context.Background(), []string{"config", "scopes"}, commandkit.Streams{Out: &output, Err: io.Discard}) if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "app: diff") { t.Fatalf("comparison-only discovery = %#v, %q", outcome, output.String()) } output.Reset() outcome = app.Execute(context.Background(), []string{"config", "diff", "--scope", "app"}, commandkit.Streams{Out: &output, Err: io.Discard}) if outcome.Kind != commandkit.OutcomeSuccess || compareCalls != 1 { t.Fatalf("comparison-only execution = %#v calls=%d", outcome, compareCalls) } inputsBefore, statesBefore := inputCalls, stateCalls output.Reset() outcome = app.Execute(context.Background(), []string{"config", "diff", "--scope", "plain"}, commandkit.Streams{Out: &output, Err: io.Discard}) if outcome.Kind != commandkit.OutcomeUsage || inputCalls != inputsBefore || stateCalls != statesBefore || acquireCalls != 0 || compareCalls != 1 || output.Len() != 0 { t.Fatalf("unsupported scope outcome/work/output = %#v / %d,%d,%d,%d / %q", outcome, inputCalls, stateCalls, acquireCalls, compareCalls, output.String()) } } func TestConfigDiffProviderFailureRetainsDiagnosticsAndCause(t *testing.T) { wantErr := errors.New("provider unavailable") diagnostic := commandkit.Diagnostic{Severity: commandkit.SeverityError, Write: func(writer io.Writer) error { _, err := io.WriteString(writer, "provider warning\n") return err }} app := newConfigTestApp(t, Command(Suite[struct{}, 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]{Diagnostics: []commandkit.Diagnostic{diagnostic}}, fmt.Errorf("comparison setup: %w", wantErr) }, }}})) var output bytes.Buffer outcome := app.Execute(context.Background(), []string{"config", "diff"}, commandkit.Streams{Out: &output, Err: io.Discard}) if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, wantErr) || outcome.ReportIncomplete || outcome.DeliveryError != nil || output.Len() != 0 || len(outcome.Diagnostics) < 1 || outcome.Diagnostics[0].Severity != commandkit.SeverityError { t.Fatalf("provider failure outcome/output = %#v / %q", outcome, output.String()) } } func TestConfigDiffReportPreparationFailureWritesNoReport(t *testing.T) { app := newConfigTestApp(t, Command(Suite[struct{}, 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]{Value: []configkit.Difference{{ Path: configkit.Keys("invalid"), Right: configkit.ValueSnapshot{Present: true, YAML: "bad: ["}, }}}, nil }, }}})) var output bytes.Buffer outcome := app.Execute(context.Background(), []string{"config", "diff"}, commandkit.Streams{Out: &output, Err: io.Discard}) if outcome.Kind != commandkit.OutcomeOperational || outcome.ReportIncomplete || outcome.DeliveryError != nil || output.Len() != 0 { t.Fatalf("report preparation failure outcome/output = %#v / %q", outcome, output.String()) } } func TestConfigDiffCancellationAfterAcquisitionRetainsDiagnosticsWithoutWriting(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) diagnostic := commandkit.Diagnostic{Severity: commandkit.SeverityWarning, Write: func(io.Writer) error { return nil }} app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{ Name: "app", Operations: []Operation{Diff}, Compare: func(context.Context, ComparisonRequest[struct{}], struct{}) (commandkit.Acquired[[]configkit.Difference], error) { cancel() return commandkit.Acquired[[]configkit.Difference]{Diagnostics: []commandkit.Diagnostic{diagnostic}}, nil }, }}})) defer cancel() var output bytes.Buffer outcome := app.Execute(ctx, []string{"config", "diff"}, commandkit.Streams{Out: &output, Err: io.Discard}) if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, context.Canceled) || outcome.ReportIncomplete || output.Len() != 0 || len(outcome.Diagnostics) < 1 || outcome.Diagnostics[0].Severity != commandkit.SeverityWarning { t.Fatalf("cancelled comparison outcome/output = %#v / %q", outcome, output.String()) } } func TestConfigDiffOutputErrorsCarryIncompleteDeliveryOutcome(t *testing.T) { app := newConfigTestApp(t, Command(Suite[struct{}, 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 }, }}})) wantErr := errors.New("output failed") for _, test := range []struct { name string writer io.Writer want error }{ {name: "writer error", writer: failingOutput{err: wantErr}, want: wantErr}, {name: "short write", writer: shortWriter{}, want: io.ErrShortWrite}, } { t.Run(test.name, func(t *testing.T) { outcome := app.Execute(context.Background(), []string{"config", "diff"}, commandkit.Streams{Out: test.writer, Err: io.Discard}) if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.DeliveryError, test.want) || !errors.Is(outcome.Cause, test.want) || !outcome.ReportIncomplete { t.Fatalf("delivery failure outcome = %#v", outcome) } }) } } func TestConfigDiffRepeatedInvocationsPrepareFreshInputsAndState(t *testing.T) { type input struct { ID int Label string } type state struct{ ID int } label := commandkit.ValueFlag(commandkit.FlagMeta{Name: "label"}, (*string)(nil), parseConfigString, func(target *input, value commandkit.Supplied[string]) { target.Label = value.Value }) var inputID, stateID int var observed []struct { left, right input state *state } app := newConfigTestApp(t, Command(Suite[input, *state]{ NewInput: func() input { inputID++; return input{ID: inputID} }, NewState: func() *state { stateID++; return &state{ID: stateID} }, Selectors: []Selector[input]{{Binding: label, Scopes: []string{"app"}}}, Scopes: []Scope[input, *state]{{Name: "app", Operations: []Operation{Diff}, Compare: func(_ context.Context, request ComparisonRequest[input], shared *state) (commandkit.Acquired[[]configkit.Difference], error) { observed = append(observed, struct { left, right input state *state }{request.Left.Input, request.Right.Input, shared}) left, err := configkit.Load[struct { Label string `yaml:"label"` }](configkit.Mapping("left", map[string]any{"label": request.Left.Input.Label})) if err != nil { return commandkit.Acquired[[]configkit.Difference]{}, err } right, err := configkit.Load[struct { Label string `yaml:"label"` }](configkit.Mapping("right", map[string]any{"label": request.Right.Input.Label + "-right"})) if err != nil { return commandkit.Acquired[[]configkit.Difference]{}, err } differences, err := configkit.Diff(left, right) return commandkit.Acquired[[]configkit.Difference]{Value: differences}, err }}}, })) for range 2 { outcome := app.Execute(context.Background(), []string{"config", "diff", "--label", "current"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}) if outcome.Kind != commandkit.OutcomeSuccess { t.Fatalf("comparison invocation outcome = %#v", outcome) } } if len(observed) != 2 || observed[0].left.ID == observed[0].right.ID || observed[1].left.ID == observed[1].right.ID || observed[0].left.ID == observed[1].left.ID || observed[0].right.ID == observed[1].right.ID { t.Fatalf("input instances were reused: %#v", observed) } if observed[0].left.Label != "current" || observed[0].right.Label != "current" || observed[1].left.Label != "current" || observed[1].right.Label != "current" { t.Fatalf("comparison inputs missed captured flag values: %#v", observed) } if observed[0].state == nil || observed[1].state == nil || observed[0].state == observed[1].state || observed[0].state.ID != 1 || observed[1].state.ID != 2 { t.Fatalf("state was not fresh per invocation: %#v", observed) } } func TestConfigDiffExecutesConcealedChangesAndEmptyResults(t *testing.T) { type settings struct { Secret string `yaml:"secret"` } app := newConfigTestApp(t, AppCommand(func(_ context.Context, selection Selection) (*configkit.Result[settings], error) { value := "same" if selection.Config.Set && selection.Config.Value == "after" { value = "changed-secret" } return configkit.Load(configkit.Mapping("selected", map[string]any{"secret": value}), configkit.Options[settings]{Definition: configkit.Definition[settings]{Secret: func(path configkit.Path) bool { return path.String() == "/secret" }}}) })) var output bytes.Buffer outcome := app.Execute(context.Background(), []string{"config", "diff", "--config", "before", "--against-config", "after"}, commandkit.Streams{Out: &output, Err: io.Discard}) if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "[REDACTED]") || strings.Contains(output.String(), "changed-secret") { t.Fatalf("concealed comparison outcome/report = %#v / %q", outcome, output.String()) } output.Reset() outcome = app.Execute(context.Background(), []string{"config", "diff", "--config", "before", "--against-config", "before"}, commandkit.Streams{Out: &output, Err: io.Discard}) if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "additions:") || !strings.Contains(output.String(), "changes:") || !strings.Contains(output.String(), "deletions:") { t.Fatalf("empty comparison outcome/report = %#v / %q", outcome, output.String()) } }