Files
commandkit/config_test.go

220 lines
9.6 KiB
Go
Raw Normal View History

package commandkit
import (
"bytes"
"context"
"reflect"
"strconv"
"strings"
"testing"
)
func TestConfigScopeDiscoveryOrdersAndDeduplicatesCapabilities(t *testing.T) {
app, err := New(Identity{Name: "example"})
if err != nil {
t.Fatal(err)
}
newContextCalls := 0
acquireCalls := 0
suite := ConfigSuite[struct{}]{
NewContext: func() struct{} { newContextCalls++; return struct{}{} },
Scopes: []ConfigScope[struct{}]{
{Name: "zeta", Operations: []ConfigOperation{ConfigExplain, ConfigShow, ConfigExplain}, Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) {
acquireCalls++
return Acquired[ConfigView]{}, nil
}},
{Name: "promptkit", Operations: []ConfigOperation{ConfigSources}, Acquire: noopConfigAcquire[struct{}]},
{Name: "app", Operations: []ConfigOperation{ConfigShow, ConfigValidate, ConfigShow}, Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) {
acquireCalls++
return Acquired[ConfigView]{}, nil
}},
{Name: "metadata", Summary: "No provider is required."},
},
}
normalized, err := normalizeConfigSuite(suite)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(normalized.Scopes[0].Operations, []ConfigOperation{ConfigShow, ConfigExplain}) || !reflect.DeepEqual(normalized.Scopes[2].Operations, []ConfigOperation{ConfigValidate, ConfigShow}) {
t.Fatalf("normalized capabilities = %#v", normalized.Scopes)
}
if err := RegisterConfig(app, suite); err != nil {
t.Fatal(err)
}
var textOutput bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "scopes"}, Streams{Out: &textOutput})
if outcome.Kind != OutcomeSuccess {
t.Fatalf("text inventory outcome = %#v", outcome)
}
2026-10-07 13:55:43 +00:00
wantText := "app: validate, show\npromptkit:\nmetadata:\nzeta: show\n"
if textOutput.String() != wantText {
t.Fatalf("text inventory = %q, want %q", textOutput.String(), wantText)
}
var yamlOutput bytes.Buffer
outcome = app.Execute(context.Background(), []string{"config", "scopes", "--format", "yaml"}, Streams{Out: &yamlOutput})
if outcome.Kind != OutcomeSuccess {
t.Fatalf("YAML inventory outcome = %#v", outcome)
}
if got := yamlOutput.String(); !strings.Contains(got, "scopes:") || strings.Index(got, "name: app") > strings.Index(got, "name: promptkit") || !strings.Contains(got, "operations: []") {
t.Fatalf("YAML inventory = %q", got)
}
outcome = app.Execute(context.Background(), []string{"config", "scopes", "--help"}, Streams{})
if outcome.Kind != OutcomeSuccess {
t.Fatalf("scope help outcome = %#v", outcome)
}
outcome = app.Execute(context.Background(), []string{"config", "scopes", "--format", "json"}, Streams{})
if outcome.Kind != OutcomeUsage {
t.Fatalf("invalid format outcome = %#v", outcome)
}
if newContextCalls != 0 || acquireCalls != 0 {
t.Fatalf("discovery called runtime callbacks: NewContext=%d Acquire=%d", newContextCalls, acquireCalls)
}
}
func TestConfigInputDefaultsToAppAndRetainsAbsentSelections(t *testing.T) {
suite := ConfigSuite[struct{}]{Scopes: []ConfigScope[struct{}]{{Name: "app"}}}
prepared, err := prepareInput(CommandSpec[configInput[struct{}]]{Flags: configInputFlags(suite, "yaml")}, make([][]string, 4), nil, true)
if err != nil {
t.Fatal(err)
}
input := prepared.build()
if input.Scope != "app" || input.Format != "yaml" || input.Selection.Config.Set || input.Selection.Overlay.Set {
t.Fatalf("configuration defaults = %#v", input)
}
}
func TestRegisterConfigRejectsInvalidSuitesAndCollisions(t *testing.T) {
tests := []struct {
name string
scope ConfigScope[struct{}]
want string
}{
{name: "empty name", scope: ConfigScope[struct{}]{}, want: "name must not be empty"},
{name: "all name", scope: ConfigScope[struct{}]{Name: "all"}, want: "reserved"},
{name: "unknown operation", scope: ConfigScope[struct{}]{Name: "app", Operations: []ConfigOperation{"diff"}}, want: "unknown operation"},
{name: "missing acquisition", scope: ConfigScope[struct{}]{Name: "app", Operations: []ConfigOperation{ConfigValidate}}, want: "without an acquisition"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
app, _ := New(Identity{Name: "example"})
err := RegisterConfig(app, ConfigSuite[struct{}]{Scopes: []ConfigScope[struct{}]{test.scope}})
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("RegisterConfig error = %v, want %q", err, test.want)
}
})
}
app, _ := New(Identity{Name: "example"})
custom, err := NewCommand(CommandSpec[struct{}]{Name: "config", Run: func(Invocation, struct{}) Outcome { return Success() }})
if err != nil {
t.Fatal(err)
}
if err := app.Register(custom); err != nil {
t.Fatal(err)
}
if err := RegisterConfig(app, ConfigSuite[struct{}]{}); err == nil {
t.Fatal("replaced a pre-existing config command")
}
}
func TestConfigSelectorPreflightAndScopedContextProjection(t *testing.T) {
type selectorInput struct {
Count Supplied[int]
Enabled Supplied[bool]
Other Supplied[int]
Runtime string
}
baseContextCalls := 0
checkCalls := 0
var checked selectorInput
countDefault := 9
suite := ConfigSuite[selectorInput]{
NewContext: func() selectorInput { baseContextCalls++; return selectorInput{Runtime: "invocation cache"} },
Selectors: []Selector[selectorInput]{
{Binding: ValueFlag[selectorInput](FlagMeta{Name: "count"}, &countDefault, strconv.Atoi, func(input *selectorInput, value Supplied[int]) { input.Count = value }), Scopes: []string{"app"}},
{Binding: BoolFlag[selectorInput](FlagMeta{Name: "enabled"}, true, func(input *selectorInput, value Supplied[bool]) { input.Enabled = value }), Scopes: []string{"promptkit"}},
{Binding: ValueFlag[selectorInput](FlagMeta{Name: "other"}, nil, strconv.Atoi, func(input *selectorInput, value Supplied[int]) { input.Other = value })},
},
CheckInput: func(_ Selection, input selectorInput) error { checkCalls++; checked = input; return nil },
Scopes: []ConfigScope[selectorInput]{
{Name: "app", Operations: []ConfigOperation{ConfigValidate}, Acquire: noopConfigAcquire[selectorInput]},
{Name: "promptkit", Operations: []ConfigOperation{ConfigValidate}, Acquire: noopConfigAcquire[selectorInput]},
},
}
normalized, err := normalizeConfigSuite(suite)
if err != nil {
t.Fatal(err)
}
spec := CommandSpec[configInput[selectorInput]]{Flags: configInputFlags(normalized, "text")}
prepared, err := prepareInput(spec, [][]string{{"all"}, {"text"}, {""}, nil, {"0"}, {"false"}, nil}, nil, true)
if err != nil {
t.Fatal(err)
}
input := prepared.build()
scopeNames, err := preflightConfig(normalized, input, ConfigValidate)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(scopeNames, []string{"app", "promptkit"}) || checkCalls != 1 || checked.Count != (Supplied[int]{Value: 0, Set: true}) || checked.Enabled != (Supplied[bool]{Value: false, Set: true}) {
t.Fatalf("preflight result=%v checked=%#v calls=%d", scopeNames, checked, checkCalls)
}
if !input.Selection.Config.Set || input.Selection.Config.Value != "" || input.Selection.Overlay.Set {
t.Fatalf("configuration selection presence = %#v", input.Selection)
}
if baseContextCalls != 0 {
t.Fatalf("NewContext called during preflight %d times", baseContextCalls)
}
contexts := contextsForScopes(normalized, input, scopeNames)
if baseContextCalls != 1 {
t.Fatalf("NewContext calls = %d, want one", baseContextCalls)
}
if contexts["app"].Count != (Supplied[int]{Value: 0, Set: true}) || contexts["app"].Enabled != (Supplied[bool]{}) || contexts["app"].Other != (Supplied[int]{}) {
t.Fatalf("app selectors = %#v", contexts["app"])
}
if contexts["promptkit"].Count != (Supplied[int]{}) || contexts["promptkit"].Enabled != (Supplied[bool]{Value: false, Set: true}) || contexts["promptkit"].Runtime != "invocation cache" {
t.Fatalf("promptkit selectors = %#v", contexts["promptkit"])
}
}
func TestConfigPreflightRejectsUnsupportedAggregateAndInapplicableSelector(t *testing.T) {
type selectorInput struct{ Region Supplied[string] }
newContextCalls := 0
suite := ConfigSuite[selectorInput]{
NewContext: func() selectorInput { newContextCalls++; return selectorInput{} },
Selectors: []Selector[selectorInput]{{Binding: ValueFlag[selectorInput](FlagMeta{Name: "region"}, nil, parseString, func(input *selectorInput, value Supplied[string]) { input.Region = value }), Scopes: []string{"promptkit"}}},
Scopes: []ConfigScope[selectorInput]{
{Name: "app", Operations: []ConfigOperation{ConfigValidate}, Acquire: noopConfigAcquire[selectorInput]},
{Name: "promptkit", Operations: []ConfigOperation{ConfigShow}, Acquire: noopConfigAcquire[selectorInput]},
},
}
normalized, err := normalizeConfigSuite(suite)
if err != nil {
t.Fatal(err)
}
prepared, err := prepareInput(CommandSpec[configInput[selectorInput]]{Flags: configInputFlags(normalized, "text")}, [][]string{{"all"}, {"text"}, nil, nil, nil}, nil, true)
if err != nil {
t.Fatal(err)
}
if _, err := preflightConfig(normalized, prepared.build(), ConfigValidate); err == nil || !strings.Contains(err.Error(), "does not support validate") {
t.Fatalf("unsupported aggregate error = %v", err)
}
if newContextCalls != 0 {
t.Fatalf("unsupported aggregate created context %d times", newContextCalls)
}
prepared, err = prepareInput(CommandSpec[configInput[selectorInput]]{Flags: configInputFlags(normalized, "text")}, [][]string{{"app"}, {"text"}, nil, nil, {"west"}}, nil, true)
if err != nil {
t.Fatal(err)
}
if _, err := preflightConfig(normalized, prepared.build(), ConfigValidate); err == nil || !strings.Contains(err.Error(), "not applicable") {
t.Fatalf("inapplicable selector error = %v", err)
}
}
func noopConfigAcquire[C any](context.Context, ConfigRequest, C) (Acquired[ConfigView], error) {
return Acquired[ConfigView]{}, nil
}