package ckconfig import ( "context" "errors" "io" "strings" "testing" commandkit "gitea.maximumdirect.net/eric/CommandKit" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) func TestComparisonCapabilitiesAreIndependentAndCaptured(t *testing.T) { type input struct{ Value string } var inputCalls, stateCalls, acquireCalls, compareCalls int compare := func(context.Context, ComparisonRequest[input], struct{}) (commandkit.Acquired[[]configkit.Difference], error) { compareCalls++ return commandkit.Acquired[[]configkit.Difference]{}, nil } operations := []Operation{Diff} suite := Suite[input, struct{}]{ NewInput: func() input { inputCalls++; return input{} }, NewState: func() struct{} { stateCalls++; return struct{}{} }, Scopes: []Scope[input, struct{}]{ {Name: "app", Operations: operations, Compare: compare}, {Name: "promptkit", Operations: []Operation{Diff, Show, Validate}, Acquire: func(context.Context, Request, input, struct{}) (commandkit.Acquired[View], error) { acquireCalls++ return commandkit.Acquired[View]{}, nil }, Compare: compare}, {Name: "alpha"}, {Name: "plain", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, input, struct{}) (commandkit.Acquired[View], error) { acquireCalls++ return commandkit.Acquired[View]{}, nil }}, }, } captured := captureSuite(suite) declaration := Command(suite) // Capability metadata and callback references are copied during capture. suite.Scopes[0].Operations[0] = Show suite.Scopes[0].Compare = nil app := newConfigTestApp(t, declaration) var output strings.Builder outcome := app.Execute(context.Background(), []string{"config", "scopes"}, commandkit.Streams{Out: &output, Err: io.Discard}) if outcome.Kind != commandkit.OutcomeSuccess { t.Fatalf("scope discovery outcome = %#v", outcome) } if got, want := output.String(), "app: diff\npromptkit: validate, show, diff\nalpha: \nplain: validate\n"; got != want { t.Fatalf("scope discovery = %q, want %q", got, want) } // A family-wide diff command is available, but unsupported scopes fail before // suite factories or either provider runs. outcome = app.Execute(context.Background(), []string{"config", "diff", "--scope", "plain"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}) if outcome.Kind != commandkit.OutcomeUsage { t.Fatalf("unsupported scope outcome = %#v", outcome) } if inputCalls != 0 || stateCalls != 0 || acquireCalls != 0 || compareCalls != 0 { t.Fatalf("discovery or preflight invoked suite work: input=%d state=%d acquire=%d compare=%d", inputCalls, stateCalls, acquireCalls, compareCalls) } if captured.byName["app"].scope.Compare == nil { t.Fatal("captured comparison callback was cleared by a source edit") } if _, err := captured.byName["app"].scope.Compare(context.Background(), ComparisonRequest[input]{}, struct{}{}); err != nil || compareCalls != 1 { t.Fatalf("captured comparison callback result=%v calls=%d", err, compareCalls) } withoutComparison := newConfigTestApp(t, Command(Suite[input, struct{}]{Scopes: []Scope[input, struct{}]{{ Name: "plain", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, input, struct{}) (commandkit.Acquired[View], error) { return commandkit.Acquired[View]{}, nil }, }}})) outcome = withoutComparison.Execute(context.Background(), []string{"config", "diff"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}) if outcome.Kind != commandkit.OutcomeUsage { t.Fatalf("family without comparison exposed diff: %#v", outcome) } } func TestComparisonCallbackWiringAndRetainedAdapterIssues(t *testing.T) { newApp := func(scope Scope[struct{}, struct{}]) error { _, err := commandkit.New(commandkit.Identity{Name: "example"}, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}})) return err } if err := newApp(Scope[struct{}, struct{}]{Name: "app", Operations: []Operation{Diff}}); err == nil { t.Fatalf("missing comparison callback error = %v", err) } if err := newApp(Scope[struct{}, struct{}]{Name: "app", Operations: []Operation{Validate}}); err == nil { t.Fatalf("missing ordinary acquisition callback error = %v", err) } scope := FromConfigKit[struct{}, struct{}, struct{}]("app", "Application configuration", nil) scope.Operations = []Operation{Diff} scope.Compare = func(context.Context, ComparisonRequest[struct{}], struct{}) (commandkit.Acquired[[]configkit.Difference], error) { return commandkit.Acquired[[]configkit.Difference]{}, nil } retained := scope.issues[0] if err := newApp(scope); err == nil || !errors.Is(err, retained) { t.Fatalf("narrowed adapter lost its retained loader issue: %v", err) } }