Separate comparison side selectors

This commit is contained in:
2026-11-07 04:36:05 +00:00
parent da120c2dd4
commit 85ae55bf42
15 changed files with 169 additions and 99 deletions

View File

@@ -465,10 +465,11 @@ configuration inspection API. PromptKit owns the meaning of that native
comparison; CommandKit uses the existing `config diff` preparation and report
path.
`--config` and `--overlay` select the left side. `--against-config` and
`--against-overlay` independently override the right side. When a right-side
field is omitted, it inherits the corresponding left value and supplied state;
an explicit empty string overrides it and remains supplied. Equal selections are
allowed; their equality alone does not cause CommandKit to skip the provider.
`--against-overlay` select the right side. An omitted field keeps that side's
ordinary application loading behavior and unsupplied state; an explicit empty
string remains supplied. Application selectors affect the left side, while
declared `--against-*` counterparts affect the right. Equal selections are
allowed and do not cause CommandKit to skip the provider.
Automatic output is YAML. The shared `--format` also accepts YAML, JSON, and
text; text uses the same structured representation. For example:

View File

@@ -237,8 +237,10 @@ Use `ckconfig.Command(ckconfig.Suite[I, S]{...})` when the application needs
multiple named scopes, selector flags, scope-aware input checks, or shared
acquisition state or a custom comparison provider. A declared `Diff` capability
uses `Scope.Compare`; `ckconfig.FromConfigKit` and `AppCommand` provide the
ConfigKit-backed implementation. Each comparison side gets its own initialized
and checked input, while state is shared by the invocation. Factory results
ConfigKit-backed implementation. `Selector.AgainstName` opts a selector into an
independent right-side flag. Each comparison side gets its own initialized and
checked input, with only its supplied selectors applied; state is shared by the
invocation. Factory results
remain caller-owned, so separate initialization calls do not guarantee distinct
objects. See
[ckconfig.Suite's GoDoc](../../integrations/ckconfig/contracts.go) for factory

View File

@@ -1,6 +1,6 @@
# Shared reporting and PromptKit v0.20.0 implementation plan
Status: implementation in progress; Stages 1–14 complete.
Status: implementation in progress; Stages 1–15 complete.
## Scope, authority, and execution
@@ -539,6 +539,12 @@ failure, independently supplied diagnostics, and metadata/help without loading.
## Stage 15: Independent comparison selector inputs
**Complete.** Comparison sides now capture and apply independently declared
selectors and configuration selections. `FlagValues.ApplySuppliedTo` was added
to core because ordinary captured replay also applies flag defaults; this keeps
each side's factory defaults intact while retaining required checks and ordered
occurrences. Root, ckconfig, and ckprompt workspace suites passed with Go 1.27.0.
Replace inheritance in [input.go](../../integrations/ckconfig/input.go).
Add `Selector.AgainstName string`; an empty name declares no right-side flag.
For a declared counterpart use Stage 6's `RenameFlag` to retain domain parsing

View File

@@ -76,6 +76,17 @@ func CaptureFlags[Outer, I any](flags []Flag[I], target func(*Outer) *FlagValues
// occurrence values in command-line order. Repeated core-owned values are copied
// for each application.
func (values FlagValues[I]) ApplyTo(target *I, selected ...Flag[I]) error {
return values.applyTo(target, false, selected...)
}
// ApplySuppliedTo applies only explicitly supplied selected flags, preserving
// the destination's existing values for omitted flags. It checks selected
// bindings and required values like ApplyTo and preserves occurrence order.
func (values FlagValues[I]) ApplySuppliedTo(target *I, selected ...Flag[I]) error {
return values.applyTo(target, true, selected...)
}
func (values FlagValues[I]) applyTo(target *I, suppliedOnly bool, selected ...Flag[I]) error {
if target == nil {
return fmt.Errorf("%w: destination is nil", ErrFlagWiring)
}
@@ -92,6 +103,9 @@ func (values FlagValues[I]) ApplyTo(target *I, selected ...Flag[I]) error {
if binding.meta.Required && !prepared.supplied {
return fmt.Errorf("flag --%s: %w", binding.meta.Name, ErrFlagInput)
}
if suppliedOnly && !prepared.supplied {
continue
}
preparedValues = append(preparedValues, prepared)
}
var ordered []preparedOccurrence[I]

View File

@@ -10,57 +10,6 @@ import (
configkit "go.maximumdirect.net/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},
@@ -125,7 +74,6 @@ func TestComparisonPreparation(t *testing.T) {
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)

View File

@@ -62,7 +62,7 @@ func TestComparisonPreparesEqualSelectionsWithSeparateTypedInputs(t *testing.T)
}
return struct{}{}
},
Selectors: []Selector[*input]{{Binding: enabled}, {Binding: limit}, {Binding: tags}},
Selectors: []Selector[*input]{{Binding: enabled, AgainstName: "against-enabled"}, {Binding: limit, AgainstName: "against-limit"}, {Binding: tags, AgainstName: "against-tag"}},
CheckInput: func(scope string, selection Selection, value *input) error {
checkCalls++
preparedInputs = append(preparedInputs, value)
@@ -91,7 +91,7 @@ func TestComparisonPreparesEqualSelectionsWithSeparateTypedInputs(t *testing.T)
app := newConfigTestApp(t, Command(suite))
outcome := app.Execute(context.Background(), []string{
"config", "diff", "--config", "same.yml", "--against-config", "same.yml", "--overlay=",
"--enabled=false", "--limit=0", "--tag", "alpha", "--tag", "beta",
"--enabled=false", "--limit=0", "--tag", "alpha", "--tag", "beta", "--against-enabled=false", "--against-limit=0", "--against-tag", "alpha", "--against-tag", "beta", "--against-overlay=",
}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess {
t.Fatalf("valid comparison outcome = %#v", outcome)
@@ -99,7 +99,7 @@ func TestComparisonPreparesEqualSelectionsWithSeparateTypedInputs(t *testing.T)
if inputCalls != 2 || checkCalls != 2 || stateCalls != 1 {
t.Fatalf("preparation calls input=%d check=%d state=%d", inputCalls, checkCalls, stateCalls)
}
if parseCalls != 3 {
if parseCalls != 6 {
t.Fatalf("typed values were converted %d times, want once per supplied occurrence", parseCalls)
}
wantSelection := Selection{
@@ -161,7 +161,7 @@ func TestComparisonProviderReceivesCheckedPairAndSharedState(t *testing.T) {
t.Fatalf("comparison outcome = %#v", outcome)
}
left := Selection{Config: commandkit.Supplied[string]{Value: "before.yml", Set: true}}
right := Selection{Config: left.Config, Overlay: commandkit.Supplied[string]{Set: true}}
right := Selection{Overlay: commandkit.Supplied[string]{Set: true}}
if request.Left.Selection != left || request.Right.Selection != right {
t.Fatalf("provider selections = %#v", request)
}

View File

@@ -141,6 +141,9 @@ type Selector[I any] struct {
Binding commandkit.Flag[I]
// Scopes lists the scope names that receive this flag's value.
Scopes []string
// AgainstName optionally declares the long name of an independent
// right-side comparison flag using the same parser and setter.
AgainstName string
}
// Suite declares the typed input, shared state, selectors, and configuration

View File

@@ -14,7 +14,7 @@ import (
"go.yaml.in/yaml/v4"
)
func TestConfigDiffExecutesWithDefaultAndExplicitYAMLAndInheritedSelections(t *testing.T) {
func TestConfigDiffExecutesWithDefaultAndExplicitYAMLAndIndependentSelections(t *testing.T) {
type settings struct {
Greeting string `yaml:"greeting"`
}
@@ -32,7 +32,7 @@ func TestConfigDiffExecutesWithDefaultAndExplicitYAMLAndInheritedSelections(t *t
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 {
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.Set {
t.Fatalf("comparison selections = %#v", selections)
}
var document yaml.Node
@@ -256,7 +256,7 @@ func TestConfigDiffRepeatedInvocationsPrepareFreshInputsAndState(t *testing.T) {
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" {
if observed[0].left.Label != "current" || observed[0].right.Label != "" || observed[1].left.Label != "current" || observed[1].right.Label != "" {
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 {

View File

@@ -23,8 +23,9 @@ type scopeInventory struct {
}
type preparedScope[I, S any] struct {
scope Scope[I, S]
bindings []commandkit.Flag[I]
scope Scope[I, S]
bindings []commandkit.Flag[I]
againstBindings []commandkit.Flag[I]
}
type preparedSuite[I, S any] struct {
@@ -34,6 +35,7 @@ type preparedSuite[I, S any] struct {
scopes []*preparedScope[I, S]
byName map[string]*preparedScope[I, S]
bindingsByOperation map[Operation][]commandkit.Flag[I]
againstBindings []commandkit.Flag[I]
scopeChoices string
operations []Operation
inventory scopeInventory
@@ -90,9 +92,13 @@ func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] {
bindingsByOperation: make(map[Operation][]commandkit.Flag[I]),
}
selectors := make([]Selector[I], len(suite.Selectors))
against := make([]commandkit.Flag[I], len(suite.Selectors))
for index, selector := range suite.Selectors {
selector.Scopes = append([]string(nil), selector.Scopes...)
selectors[index] = selector
if selector.AgainstName != "" {
against[index] = commandkit.RenameFlag(selector.Binding, selector.AgainstName)
}
}
seenNames := make(map[string]struct{}, len(suite.Scopes))
@@ -158,12 +164,18 @@ func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] {
for scopeIndex := range prepared.scopes {
if len(prepared.scopes[scopeIndex].scope.Operations) > 0 {
prepared.scopes[scopeIndex].bindings = append(prepared.scopes[scopeIndex].bindings, selector.Binding)
if selector.AgainstName != "" && containsOperation(prepared.scopes[scopeIndex].scope.Operations, Diff) {
prepared.scopes[scopeIndex].againstBindings = append(prepared.scopes[scopeIndex].againstBindings, against[index])
}
}
}
} else {
for _, name := range uniqueScopes {
if scope, exists := prepared.byName[name]; exists && len(scope.scope.Operations) > 0 {
scope.bindings = append(scope.bindings, selector.Binding)
if selector.AgainstName != "" && containsOperation(scope.scope.Operations, Diff) {
scope.againstBindings = append(scope.againstBindings, against[index])
}
}
}
}
@@ -176,10 +188,13 @@ func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] {
break
}
}
for _, selector := range selectors {
for index, selector := range selectors {
for _, scope := range prepared.scopes {
if containsOperation(scope.scope.Operations, operation) && selectorApplies(selector.Scopes, scope.scope.Name) {
prepared.bindingsByOperation[operation] = append(prepared.bindingsByOperation[operation], selector.Binding)
if operation == Diff && selector.AgainstName != "" {
prepared.againstBindings = append(prepared.againstBindings, against[index])
}
break
}
}

View File

@@ -0,0 +1,79 @@
package ckconfig
import (
"context"
"io"
"reflect"
"testing"
commandkit "go.maximumdirect.net/commandkit"
configkit "go.maximumdirect.net/configkit"
)
func TestComparisonSidesKeepIndependentSelectionsAndDefaults(t *testing.T) {
type input struct {
Label commandkit.Supplied[string]
Tags []string
}
label := commandkit.ValueFlag(commandkit.FlagMeta{Name: "label"}, (*string)(nil), parseConfigString, func(i *input, value commandkit.Supplied[string]) { i.Label = value })
tags := commandkit.RepeatFlag(commandkit.FlagMeta{Name: "tag"}, []string(nil), parseConfigString, func(i *input, value commandkit.Supplied[[]string]) { i.Tags = value.Value })
var pair ComparisonRequest[input]
calls := 0
suite := Suite[input, struct{}]{
NewInput: func() input {
return input{Label: commandkit.Supplied[string]{Value: "factory"}, Tags: []string{"factory"}}
},
Selectors: []Selector[input]{{Binding: label, AgainstName: "against-label"}, {Binding: tags, AgainstName: "against-tag"}},
Scopes: []Scope[input, struct{}]{{Name: "app", Operations: []Operation{Diff}, Compare: func(_ context.Context, request ComparisonRequest[input], _ struct{}) (commandkit.Acquired[[]configkit.Difference], error) {
calls++
pair = request
return commandkit.Acquired[[]configkit.Difference]{}, nil
}}},
}
app := newConfigTestApp(t, Command(suite))
tests := []struct {
args []string
left, right Selection
leftLabel, rightLabel commandkit.Supplied[string]
leftTags, rightTags []string
}{
{leftLabel: commandkit.Supplied[string]{Value: "factory"}, rightLabel: commandkit.Supplied[string]{Value: "factory"}, leftTags: []string{"factory"}, rightTags: []string{"factory"}},
{args: []string{"--config=left", "--overlay=base", "--label=", "--tag=a", "--tag=b"}, left: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}, Overlay: commandkit.Supplied[string]{Value: "base", Set: true}}, leftLabel: commandkit.Supplied[string]{Set: true}, rightLabel: commandkit.Supplied[string]{Value: "factory"}, leftTags: []string{"a", "b"}, rightTags: []string{"factory"}},
{args: []string{"--against-config=right", "--against-label=r", "--against-tag=x"}, right: Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}, leftLabel: commandkit.Supplied[string]{Value: "factory"}, rightLabel: commandkit.Supplied[string]{Value: "r", Set: true}, leftTags: []string{"factory"}, rightTags: []string{"x"}},
{args: []string{"--config=left", "--against-overlay=", "--label=l", "--against-label=r", "--tag=a", "--against-tag=b"}, left: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}, right: Selection{Overlay: commandkit.Supplied[string]{Set: true}}, leftLabel: commandkit.Supplied[string]{Value: "l", Set: true}, rightLabel: commandkit.Supplied[string]{Value: "r", Set: true}, leftTags: []string{"a"}, rightTags: []string{"b"}},
}
for _, test := range tests {
result := app.Execute(context.Background(), append([]string{"config", "diff"}, test.args...), commandkit.Streams{Out: io.Discard, Err: io.Discard})
if result.Kind != commandkit.OutcomeSuccess || pair.Left.Selection != test.left || pair.Right.Selection != test.right || pair.Left.Input.Label != test.leftLabel || pair.Right.Input.Label != test.rightLabel || !reflect.DeepEqual(pair.Left.Input.Tags, test.leftTags) || !reflect.DeepEqual(pair.Right.Input.Tags, test.rightTags) {
t.Fatalf("args=%v outcome=%#v pair=%#v", test.args, result, pair)
}
}
if calls != len(tests) {
t.Fatalf("provider calls = %d", calls)
}
}
func TestComparisonRequiredCounterpartsAndCollisions(t *testing.T) {
flag := commandkit.ValueFlag(commandkit.FlagMeta{Name: "choice", Required: true}, (*string)(nil), parseConfigString, func(i *string, value commandkit.Supplied[string]) { *i = value.Value })
calls := 0
scope := Scope[string, struct{}]{Name: "app", Operations: []Operation{Diff}, Compare: func(context.Context, ComparisonRequest[string], struct{}) (commandkit.Acquired[[]configkit.Difference], error) {
calls++
return commandkit.Acquired[[]configkit.Difference]{}, nil
}}
suite := Suite[string, struct{}]{Selectors: []Selector[string]{{Binding: flag, AgainstName: "against-choice"}}, Scopes: []Scope[string, struct{}]{scope}}
app := newConfigTestApp(t, Command(suite))
for _, args := range [][]string{{"--choice=x"}, {"--against-choice=x"}} {
result := app.Execute(context.Background(), append([]string{"config", "diff"}, args...), commandkit.Streams{Out: io.Discard, Err: io.Discard})
if result.Kind != commandkit.OutcomeUsage || calls != 0 {
t.Fatalf("missing side %v: %#v", args, result)
}
}
result := app.Execute(context.Background(), []string{"config", "diff", "--choice=x", "--against-choice=y"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if result.Kind != commandkit.OutcomeSuccess || calls != 1 {
t.Fatalf("complete sides: %#v", result)
}
suite.Selectors[0].AgainstName = "against-config"
if _, err := commandkit.New(commandkit.Identity{Name: "app"}, Command(suite)); err == nil {
t.Fatal("conflicting counterpart accepted")
}
}

View File

@@ -9,12 +9,13 @@ import (
)
type configCommandInput[I, S any] struct {
selection Selection
against Selection
scope commandkit.Supplied[string]
path commandkit.Supplied[string]
values commandkit.FlagValues[I]
plan *configOperationPlan[I, S]
selection Selection
against Selection
scope commandkit.Supplied[string]
path commandkit.Supplied[string]
values commandkit.FlagValues[I]
againstValues commandkit.FlagValues[I]
plan *configOperationPlan[I, S]
}
type selectedScopeInput[I, S any] struct {
@@ -85,6 +86,14 @@ func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operatio
}
return &(*input).values
})...)
if operation == Diff {
flags = append(flags, commandkit.CaptureFlags(prepared.againstBindings, func(input **configCommandInput[I, S]) *commandkit.FlagValues[I] {
if input == nil || *input == nil {
return nil
}
return &(*input).againstValues
})...)
}
name, summary := operationDeclaration(operation)
return commandkit.NewCommand(commandkit.CommandSpec[*configCommandInput[I, S]]{
@@ -169,13 +178,12 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
scopes: make([]selectedScopeInput[I, S], 0, len(selected)),
}
if operation == Diff {
input.against = inheritComparisonSelection(input.selection, input.against)
scope := selected[0]
left, err := prepareScopeInput(prepared, input.values, scope, input.selection)
left, err := prepareScopeInput(prepared, input.values, scope, scope.bindings, input.selection, true)
if err != nil {
return fmt.Errorf("configuration scope %q left input: %w", scope.scope.Name, err)
}
right, err := prepareScopeInput(prepared, input.values, scope, input.against)
right, err := prepareScopeInput(prepared, input.againstValues, scope, scope.againstBindings, input.against, true)
if err != nil {
return fmt.Errorf("configuration scope %q right input: %w", scope.scope.Name, err)
}
@@ -190,7 +198,7 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
return nil
}
for _, scope := range selected {
value, err := prepareScopeInput(prepared, input.values, scope, input.selection)
value, err := prepareScopeInput(prepared, input.values, scope, scope.bindings, input.selection, false)
if err != nil {
return fmt.Errorf("configuration scope %q: %w", scope.scope.Name, err)
}
@@ -200,14 +208,18 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
return nil
}
func prepareScopeInput[I, S any](prepared *preparedSuite[I, S], values commandkit.FlagValues[I], scope *preparedScope[I, S], selection Selection) (I, error) {
func prepareScopeInput[I, S any](prepared *preparedSuite[I, S], values commandkit.FlagValues[I], scope *preparedScope[I, S], bindings []commandkit.Flag[I], selection Selection, suppliedOnly bool) (I, error) {
var value I
if prepared.newInput != nil {
value = prepared.newInput()
}
// Availability was checked before this helper, so every binding for the
// selected scope belongs to the operation's captured union.
if err := values.ApplyTo(&value, scope.bindings...); err != nil {
apply := values.ApplyTo
if suppliedOnly {
apply = values.ApplySuppliedTo
}
if err := apply(&value, bindings...); err != nil {
return value, err
}
if prepared.checkInput != nil {
@@ -218,16 +230,6 @@ func prepareScopeInput[I, S any](prepared *preparedSuite[I, S], values commandki
return value, nil
}
func inheritComparisonSelection(left, right Selection) Selection {
if !right.Config.Set {
right.Config = left.Config
}
if !right.Overlay.Set {
right.Overlay = left.Overlay
}
return right
}
func joinOperations(operations []Operation) string {
values := make([]string, len(operations))
for index, operation := range operations {

View File

@@ -64,7 +64,7 @@ func TestPublicConfigDiffUsesPromptKitPairAndPreparedInputs(t *testing.T) {
events = append(events, "state")
return &compositionState{token: len(events)}
},
Selectors: []ckconfig.Selector[*compositionInput]{{Binding: flag, Scopes: []string{"app", "promptkit"}}},
Selectors: []ckconfig.Selector[*compositionInput]{{Binding: flag, AgainstName: "against-campaign", Scopes: []string{"app", "promptkit"}}},
CheckInput: func(scope string, selection ckconfig.Selection, input *compositionInput) error {
if scope == "promptkit" {
events = append(events, "check")
@@ -90,7 +90,7 @@ func TestPublicConfigDiffUsesPromptKitPairAndPreparedInputs(t *testing.T) {
rightSet bool
}{
{name: "right selection is explicit", rightArgs: []string{"--against-config", "after.yml"}, rightValue: "after.yml", rightSet: true},
{name: "right selection inherits", rightValue: "before.yml", rightSet: true},
{name: "right selection remains omitted"},
{name: "empty right selection is supplied", rightArgs: []string{"--against-config", ""}, rightSet: true},
} {
t.Run(test.name, func(t *testing.T) {
@@ -100,7 +100,7 @@ func TestPublicConfigDiffUsesPromptKitPairAndPreparedInputs(t *testing.T) {
loadedInputs = nil
loadedStates = nil
loadedSelections = nil
args := []string{"config", "diff", "--scope", "promptkit", "--config", "before.yml", "--campaign", "spring"}
args := []string{"config", "diff", "--scope", "promptkit", "--config", "before.yml", "--campaign", "spring", "--against-campaign", "spring"}
args = append(args, test.rightArgs...)
var output bytes.Buffer
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: io.Discard})
@@ -153,7 +153,7 @@ func TestPublicConfigDiffUsesPromptKitPairAndPreparedInputs(t *testing.T) {
}
var diagnostics bytes.Buffer
failure := app.Execute(context.Background(), []string{"config", "diff", "--scope", "promptkit", "--config", "bad-left.yml", "--against-config", "right.yml", "--campaign", "spring"}, commandkit.Streams{Out: io.Discard, Err: &diagnostics})
failure := app.Execute(context.Background(), []string{"config", "diff", "--scope", "promptkit", "--config", "bad-left.yml", "--against-config", "right.yml", "--campaign", "spring", "--against-campaign", "spring"}, commandkit.Streams{Out: io.Discard, Err: &diagnostics})
if failure.Kind != commandkit.OutcomeOperational || !strings.Contains(diagnostics.String(), `Configuration scope "promptkit" (left):`) {
t.Fatalf("installed load failure = %#v, diagnostics %q", failure, diagnostics.String())
}

View File

@@ -12,7 +12,7 @@ go run . config scopes
go run . config validate --scope all --campaign spring --limit=0
go run . config diff --scope app --config fixtures/before.yml --against-config fixtures/after.yml
go run . config diff --scope app --config fixtures/before.yml --against-config fixtures/after.yml --campaign spring --limit=0
go run . config diff --scope promptkit --config fixtures/before.yml --against-config fixtures/after.yml --campaign spring
go run . config diff --scope promptkit --config fixtures/before.yml --against-config fixtures/after.yml --campaign spring --against-campaign spring
go run . promptkit list prompts --config fixtures/before.yml --campaign spring
go run . promptkit list profiles --config fixtures/after.yml --campaign autumn --format yaml
go run . promptkit list backends --campaign spring

View File

@@ -234,8 +234,8 @@ func newAppWithEngineLoader(
return nil
},
Selectors: []ckconfig.Selector[*inspectionInput]{
{Binding: campaignSelector(), Scopes: []string{"app", "promptkit"}},
{Binding: limitFlag, Scopes: []string{"app"}},
{Binding: campaignSelector(), AgainstName: "against-campaign", Scopes: []string{"app", "promptkit"}},
{Binding: limitFlag, AgainstName: "against-limit", Scopes: []string{"app"}},
},
Scopes: []ckconfig.Scope[*inspectionInput, *runCache]{appScope, engineScope},
})

View File

@@ -297,7 +297,7 @@ func TestPromptKitScopeDiffAndCatalogsUseSelectedCampaign(t *testing.T) {
t.Fatal(err)
}
var output bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "diff", "--scope", "promptkit", "--config", "fixtures/before.yml", "--against-config", "fixtures/after.yml", "--campaign", "spring"}, commandkit.Streams{Out: &output, Err: io.Discard})
outcome := app.Execute(context.Background(), []string{"config", "diff", "--scope", "promptkit", "--config", "fixtures/before.yml", "--against-config", "fixtures/after.yml", "--campaign", "spring", "--against-campaign", "spring"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess {
t.Fatalf("PromptKit diff outcome = %#v", outcome)
}