Add opt-in configuration overrides
This commit is contained in:
11
docs/cli.md
11
docs/cli.md
@@ -306,6 +306,17 @@ runtime context or acquire configuration. It reports the intersection between
|
|||||||
declared scope capabilities and installed handlers, so an operation is listed
|
declared scope capabilities and installed handlers, so an operation is listed
|
||||||
only when the corresponding command is available.
|
only when the corresponding command is available.
|
||||||
|
|
||||||
|
Scopes can opt in to transient overrides. For an opted-in scope, acquisition
|
||||||
|
commands accept repeatable `--set POINTER=JSON`; comparison also accepts
|
||||||
|
`--against-set POINTER=JSON` for the right side. Each assignment is passed to
|
||||||
|
the loader in order, with `--set` or `--against-set` as its source label.
|
||||||
|
The pointer and value are split at the first `=`, so pointer keys containing
|
||||||
|
`=` cannot be addressed by these flags; JSON strings can contain `=`. Values
|
||||||
|
use JSON types, including null, false, zero, and empty containers. ConfigKit
|
||||||
|
owns override path validation and conflict handling. A selected scope must opt
|
||||||
|
in before receiving overrides; for `--scope all`, every selected scope must
|
||||||
|
support them.
|
||||||
|
|
||||||
Applications can add `ckprompt.ConfigScope(loadEngine)` to the ConfigKit suite
|
Applications can add `ckprompt.ConfigScope(loadEngine)` to the ConfigKit suite
|
||||||
to expose PromptKit engine configuration through these handlers. The scope
|
to expose PromptKit engine configuration through these handlers. The scope
|
||||||
declares validate, show, sources, explain, and native diff. It does not install
|
declares validate, show, sources, explain, and native diff. It does not install
|
||||||
|
|||||||
@@ -255,6 +255,10 @@ owns operator syntax and report details. `Scope.Features` declares optional mode
|
|||||||
`View.Metadata` and `View.Findings` carry native path metadata and diagnostic
|
`View.Metadata` and `View.Findings` carry native path metadata and diagnostic
|
||||||
records when available. `Findings.Messages` contains presentations associated
|
records when available. `Findings.Messages` contains presentations associated
|
||||||
with those records, while `Acquired.Diagnostics` carries independent messages.
|
with those records, while `Acquired.Diagnostics` carries independent messages.
|
||||||
|
Set `Scope.Features.Overrides` only when the loader passes
|
||||||
|
`Selection.Overrides` to ConfigKit as part of its normal load. The integration
|
||||||
|
parses CLI records but cannot add them to an already-loaded result. The
|
||||||
|
[CLI reference](../cli.md#configuration-scopes) describes the operator syntax.
|
||||||
|
|
||||||
## PromptKit configuration, definitions, and requirements
|
## PromptKit configuration, definitions, and requirements
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Shared reporting and PromptKit v0.20.0 implementation plan
|
# Shared reporting and PromptKit v0.20.0 implementation plan
|
||||||
|
|
||||||
Status: implementation in progress; Stages 1–15 complete.
|
Status: implementation in progress; Stages 1–16 complete.
|
||||||
|
|
||||||
## Scope, authority, and execution
|
## Scope, authority, and execution
|
||||||
|
|
||||||
@@ -571,6 +571,11 @@ model. Explicit-empty custom collections remain application-defined.
|
|||||||
|
|
||||||
## Stage 16: Opt-in transient override acquisition
|
## Stage 16: Opt-in transient override acquisition
|
||||||
|
|
||||||
|
**Complete.** Opted-in loaders receive ordered, independently copied native
|
||||||
|
override records with safe flag labels. The root, ckconfig, and ckprompt
|
||||||
|
workspace suites passed with Go 1.27.0, including scope preflight, comparison
|
||||||
|
separation, native provenance, and load-error propagation.
|
||||||
|
|
||||||
Add `Selection.Overrides []configkit.Override`. Preserve ordered records; nil
|
Add `Selection.Overrides []configkit.Override`. Preserve ordered records; nil
|
||||||
means no CLI records. A loader opting in via `scope.Features.Overrides = true`
|
means no CLI records. A loader opting in via `scope.Features.Overrides = true`
|
||||||
reads those records during its ordinary load and supplies them to ConfigKit.
|
reads those records during its ordinary load and supplies them to ConfigKit.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ func TestFromConfigKitCompareReturnsNativeChangesAndLoadsEqualSelections(t *test
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if len(calls) != 2 || calls[0].selection != selection || calls[1].selection != selection {
|
if len(calls) != 2 || !reflect.DeepEqual(calls[0].selection, selection) || !reflect.DeepEqual(calls[1].selection, selection) {
|
||||||
t.Fatalf("loader calls = %#v", calls)
|
t.Fatalf("loader calls = %#v", calls)
|
||||||
}
|
}
|
||||||
if calls[0].input.Label != "before" || calls[1].input.Label != "after" || calls[0].state != state || calls[1].state != state || *state != 2 {
|
if calls[0].input.Label != "before" || calls[1].input.Label != "after" || calls[0].state != state || calls[1].state != state || *state != 2 {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package ckconfig
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ func TestComparisonPreparation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
if want := (Selection{
|
if want := (Selection{
|
||||||
Overlay: commandkit.Supplied[string]{Value: "candidate", Set: true},
|
Overlay: commandkit.Supplied[string]{Value: "candidate", Set: true},
|
||||||
}); input.against != want {
|
}); !reflect.DeepEqual(input.against, want) {
|
||||||
t.Fatalf("resolved right selection = %#v, want %#v", input.against, want)
|
t.Fatalf("resolved right selection = %#v, want %#v", input.against, want)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -106,7 +107,7 @@ func TestComparisonPreparesEqualSelectionsWithSeparateTypedInputs(t *testing.T)
|
|||||||
Config: commandkit.Supplied[string]{Value: "same.yml", Set: true},
|
Config: commandkit.Supplied[string]{Value: "same.yml", Set: true},
|
||||||
Overlay: commandkit.Supplied[string]{Set: true},
|
Overlay: commandkit.Supplied[string]{Set: true},
|
||||||
}
|
}
|
||||||
if len(selections) != 2 || selections[0] != wantSelection || selections[1] != wantSelection {
|
if len(selections) != 2 || !reflect.DeepEqual(selections[0], wantSelection) || !reflect.DeepEqual(selections[1], wantSelection) {
|
||||||
t.Fatalf("side selections = %#v, want equal %#v values", selections, wantSelection)
|
t.Fatalf("side selections = %#v, want equal %#v values", selections, wantSelection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,7 +116,7 @@ func TestComparisonProviderReceivesCheckedPairAndSharedState(t *testing.T) {
|
|||||||
type input struct{ ID int }
|
type input struct{ ID int }
|
||||||
type state struct{ Label string }
|
type state struct{ Label string }
|
||||||
inputCalls, checkCalls, stateCalls, compareCalls, acquireCalls := 0, 0, 0, 0, 0
|
inputCalls, checkCalls, stateCalls, compareCalls, acquireCalls := 0, 0, 0, 0, 0
|
||||||
checked := make(map[Selection]input)
|
var checked []input
|
||||||
shared := &state{Label: "shared"}
|
shared := &state{Label: "shared"}
|
||||||
var request ComparisonRequest[input]
|
var request ComparisonRequest[input]
|
||||||
app := newConfigTestApp(t, Command(Suite[input, *state]{
|
app := newConfigTestApp(t, Command(Suite[input, *state]{
|
||||||
@@ -135,7 +136,7 @@ func TestComparisonProviderReceivesCheckedPairAndSharedState(t *testing.T) {
|
|||||||
if scope != "app" {
|
if scope != "app" {
|
||||||
return errors.New("unexpected scope")
|
return errors.New("unexpected scope")
|
||||||
}
|
}
|
||||||
checked[selection] = value
|
checked = append(checked, value)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Scopes: []Scope[input, *state]{{
|
Scopes: []Scope[input, *state]{{
|
||||||
@@ -162,10 +163,10 @@ func TestComparisonProviderReceivesCheckedPairAndSharedState(t *testing.T) {
|
|||||||
}
|
}
|
||||||
left := Selection{Config: commandkit.Supplied[string]{Value: "before.yml", Set: true}}
|
left := Selection{Config: commandkit.Supplied[string]{Value: "before.yml", Set: true}}
|
||||||
right := Selection{Overlay: commandkit.Supplied[string]{Set: true}}
|
right := Selection{Overlay: commandkit.Supplied[string]{Set: true}}
|
||||||
if request.Left.Selection != left || request.Right.Selection != right {
|
if !reflect.DeepEqual(request.Left.Selection, left) || !reflect.DeepEqual(request.Right.Selection, right) {
|
||||||
t.Fatalf("provider selections = %#v", request)
|
t.Fatalf("provider selections = %#v", request)
|
||||||
}
|
}
|
||||||
if len(checked) != 2 || request.Left.Input != checked[left] || request.Right.Input != checked[right] {
|
if len(checked) != 2 || request.Left.Input != checked[0] || request.Right.Input != checked[1] {
|
||||||
t.Fatalf("provider inputs = %#v, checked = %#v", request, checked)
|
t.Fatalf("provider inputs = %#v, checked = %#v", request, checked)
|
||||||
}
|
}
|
||||||
if inputCalls != 2 || checkCalls != 2 || stateCalls != 1 || compareCalls != 1 || acquireCalls != 0 {
|
if inputCalls != 2 || checkCalls != 2 || stateCalls != 1 || compareCalls != 1 || acquireCalls != 0 {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ type Selection struct {
|
|||||||
Config commandkit.Supplied[string]
|
Config commandkit.Supplied[string]
|
||||||
// Overlay is the caller-supplied overlay path, if present.
|
// Overlay is the caller-supplied overlay path, if present.
|
||||||
Overlay commandkit.Supplied[string]
|
Overlay commandkit.Supplied[string]
|
||||||
|
// Overrides contains ordered CLI override records, or nil when none were supplied.
|
||||||
|
Overrides []configkit.Override
|
||||||
}
|
}
|
||||||
|
|
||||||
// Operation identifies a supported standard configuration operation.
|
// Operation identifies a supported standard configuration operation.
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func TestFromConfigKitPassesInvocationValuesAndNativeFacets(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if gotContext != ctx || gotSelection != selection || gotInput.Label != "typed" || gotState != stateValue || stateValue.Calls != 1 {
|
if gotContext != ctx || !reflect.DeepEqual(gotSelection, selection) || gotInput.Label != "typed" || gotState != stateValue || stateValue.Calls != 1 {
|
||||||
t.Fatalf("loader arguments/context/state = %v, %#v, %#v, %p, %d", gotContext, gotSelection, gotInput, gotState, stateValue.Calls)
|
t.Fatalf("loader arguments/context/state = %v, %#v, %#v, %p, %d", gotContext, gotSelection, gotInput, gotState, stateValue.Calls)
|
||||||
}
|
}
|
||||||
if acquired.Value.Effective != result || acquired.Value.Sources != result || acquired.Value.Explanation != result || acquired.Value.Metadata == nil || acquired.Value.Findings == nil {
|
if acquired.Value.Effective != result || acquired.Value.Sources != result || acquired.Value.Explanation != result || acquired.Value.Metadata == nil || acquired.Value.Findings == nil {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func runConfigExplanation[I, S any](invocation commandkit.Invocation, plan *conf
|
|||||||
}
|
}
|
||||||
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
|
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
|
||||||
Operation: Explain,
|
Operation: Explain,
|
||||||
Selection: plan.selection,
|
Selection: selected.selection,
|
||||||
}, selected.input, state)
|
}, selected.input, state)
|
||||||
queueConfigAcquisition(reporter, acquired)
|
queueConfigAcquisition(reporter, acquired)
|
||||||
if acquireErr != nil {
|
if acquireErr != nil {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func TestComparisonSidesKeepIndependentSelectionsAndDefaults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
result := app.Execute(context.Background(), append([]string{"config", "diff"}, test.args...), commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
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) {
|
if result.Kind != commandkit.OutcomeSuccess || !reflect.DeepEqual(pair.Left.Selection, test.left) || !reflect.DeepEqual(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)
|
t.Fatalf("args=%v outcome=%#v pair=%#v", test.args, result, pair)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type configCommandInput[I, S any] struct {
|
|||||||
type selectedScopeInput[I, S any] struct {
|
type selectedScopeInput[I, S any] struct {
|
||||||
scope *preparedScope[I, S]
|
scope *preparedScope[I, S]
|
||||||
input I
|
input I
|
||||||
|
selection Selection
|
||||||
}
|
}
|
||||||
|
|
||||||
type preparedComparison[I, S any] struct {
|
type preparedComparison[I, S any] struct {
|
||||||
@@ -31,7 +32,6 @@ type preparedComparison[I, S any] struct {
|
|||||||
type configOperationPlan[I, S any] struct {
|
type configOperationPlan[I, S any] struct {
|
||||||
prepared *preparedSuite[I, S]
|
prepared *preparedSuite[I, S]
|
||||||
operation Operation
|
operation Operation
|
||||||
selection Selection
|
|
||||||
scopeName string
|
scopeName string
|
||||||
path configkit.Path
|
path configkit.Path
|
||||||
scopes []selectedScopeInput[I, S]
|
scopes []selectedScopeInput[I, S]
|
||||||
@@ -76,6 +76,18 @@ func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operatio
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if supportsOverrides(prepared, operation) {
|
||||||
|
flags = append(flags, commandkit.RepeatFlag(commandkit.FlagMeta{Name: "set", Usage: "Override POINTER=JSON (pointer keys cannot contain =)."}, []configkit.Override(nil), overrideParser("--set"),
|
||||||
|
func(input **configCommandInput[I, S], value commandkit.Supplied[[]configkit.Override]) {
|
||||||
|
(*input).selection.Overrides = value.Value
|
||||||
|
}))
|
||||||
|
if operation == Diff {
|
||||||
|
flags = append(flags, commandkit.RepeatFlag(commandkit.FlagMeta{Name: "against-set", Usage: "Right-side override POINTER=JSON (pointer keys cannot contain =)."}, []configkit.Override(nil), overrideParser("--against-set"),
|
||||||
|
func(input **configCommandInput[I, S], value commandkit.Supplied[[]configkit.Override]) {
|
||||||
|
(*input).against.Overrides = value.Value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
if operation == Explain {
|
if operation == Explain {
|
||||||
flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: "path", Usage: "JSON Pointer path to explain.", Required: true}, (*string)(nil), parseConfigString,
|
flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: "path", Usage: "JSON Pointer path to explain.", Required: true}, (*string)(nil), parseConfigString,
|
||||||
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).path = value }))
|
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).path = value }))
|
||||||
@@ -155,6 +167,9 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
|||||||
if !containsOperation(scope.scope.Operations, operation) {
|
if !containsOperation(scope.scope.Operations, operation) {
|
||||||
return fmt.Errorf("configuration scope %q does not support %s (available: %s)", scope.scope.Name, operation, joinOperations(scope.scope.Operations))
|
return fmt.Errorf("configuration scope %q does not support %s (available: %s)", scope.scope.Name, operation, joinOperations(scope.scope.Operations))
|
||||||
}
|
}
|
||||||
|
if (len(input.selection.Overrides) > 0 || len(input.against.Overrides) > 0) && !scope.scope.Features.Overrides {
|
||||||
|
return fmt.Errorf("configuration scope %q does not support overrides", scope.scope.Name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var path configkit.Path
|
var path configkit.Path
|
||||||
@@ -172,13 +187,14 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
|||||||
plan := &configOperationPlan[I, S]{
|
plan := &configOperationPlan[I, S]{
|
||||||
prepared: prepared,
|
prepared: prepared,
|
||||||
operation: operation,
|
operation: operation,
|
||||||
selection: input.selection,
|
|
||||||
scopeName: scopeName,
|
scopeName: scopeName,
|
||||||
path: path,
|
path: path,
|
||||||
scopes: make([]selectedScopeInput[I, S], 0, len(selected)),
|
scopes: make([]selectedScopeInput[I, S], 0, len(selected)),
|
||||||
}
|
}
|
||||||
if operation == Diff {
|
if operation == Diff {
|
||||||
scope := selected[0]
|
scope := selected[0]
|
||||||
|
input.selection = cloneSelection(input.selection)
|
||||||
|
input.against = cloneSelection(input.against)
|
||||||
left, err := prepareScopeInput(prepared, input.values, scope, scope.bindings, input.selection, true)
|
left, err := prepareScopeInput(prepared, input.values, scope, scope.bindings, input.selection, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("configuration scope %q left input: %w", scope.scope.Name, err)
|
return fmt.Errorf("configuration scope %q left input: %w", scope.scope.Name, err)
|
||||||
@@ -198,11 +214,12 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, scope := range selected {
|
for _, scope := range selected {
|
||||||
value, err := prepareScopeInput(prepared, input.values, scope, scope.bindings, input.selection, false)
|
selection := cloneSelection(input.selection)
|
||||||
|
value, err := prepareScopeInput(prepared, input.values, scope, scope.bindings, selection, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("configuration scope %q: %w", scope.scope.Name, err)
|
return fmt.Errorf("configuration scope %q: %w", scope.scope.Name, err)
|
||||||
}
|
}
|
||||||
plan.scopes = append(plan.scopes, selectedScopeInput[I, S]{scope: scope, input: value})
|
plan.scopes = append(plan.scopes, selectedScopeInput[I, S]{scope: scope, input: value, selection: selection})
|
||||||
}
|
}
|
||||||
input.plan = plan
|
input.plan = plan
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
65
integrations/ckconfig/overrides.go
Normal file
65
integrations/ckconfig/overrides.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package ckconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
commandkit "go.maximumdirect.net/commandkit"
|
||||||
|
configkit "go.maximumdirect.net/configkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func supportsOverrides[I, S any](prepared *preparedSuite[I, S], operation Operation) bool {
|
||||||
|
for _, scope := range prepared.scopes {
|
||||||
|
if scope.scope.Features.Overrides && containsOperation(scope.scope.Operations, operation) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func overrideParser(flag string) func(string) (configkit.Override, error) {
|
||||||
|
return func(raw string) (configkit.Override, error) {
|
||||||
|
pointer, jsonText, ok := strings.Cut(raw, "=")
|
||||||
|
if !ok {
|
||||||
|
return configkit.Override{}, fmt.Errorf("override requires POINTER=JSON")
|
||||||
|
}
|
||||||
|
path, err := configkit.ParsePath(pointer)
|
||||||
|
if err != nil {
|
||||||
|
return configkit.Override{}, fmt.Errorf("invalid override path: %w", err)
|
||||||
|
}
|
||||||
|
value, err := commandkit.ParseCLIJSON(jsonText)
|
||||||
|
if err != nil {
|
||||||
|
return configkit.Override{}, fmt.Errorf("invalid override JSON: %w", err)
|
||||||
|
}
|
||||||
|
return configkit.Override{Path: path, Value: value, Flag: flag}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneSelection(selection Selection) Selection {
|
||||||
|
if selection.Overrides != nil {
|
||||||
|
selection.Overrides = append([]configkit.Override(nil), selection.Overrides...)
|
||||||
|
for index := range selection.Overrides {
|
||||||
|
selection.Overrides[index].Value = cloneCLIValue(selection.Overrides[index].Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selection
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneCLIValue(value any) any {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case []any:
|
||||||
|
copy := make([]any, len(value))
|
||||||
|
for index, element := range value {
|
||||||
|
copy[index] = cloneCLIValue(element)
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
case map[string]any:
|
||||||
|
copy := make(map[string]any, len(value))
|
||||||
|
for key, element := range value {
|
||||||
|
copy[key] = cloneCLIValue(element)
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
94
integrations/ckconfig/overrides_test.go
Normal file
94
integrations/ckconfig/overrides_test.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package ckconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
commandkit "go.maximumdirect.net/commandkit"
|
||||||
|
configkit "go.maximumdirect.net/configkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOverrideAssignmentsAreCopiedPerScopeAndPreservedInOrder(t *testing.T) {
|
||||||
|
var received []Selection
|
||||||
|
makeScope := func(name string) Scope[struct{}, struct{}] {
|
||||||
|
return Scope[struct{}, struct{}]{Name: name, Operations: []Operation{Validate}, Features: ScopeFeatures{Overrides: true}, Acquire: func(_ context.Context, req Request, _, _ struct{}) (commandkit.Acquired[View], error) {
|
||||||
|
received = append(received, cloneSelection(req.Selection))
|
||||||
|
if name == "app" {
|
||||||
|
req.Selection.Overrides[0].Value.(map[string]any)["numbers"].([]any)[0] = int64(999)
|
||||||
|
}
|
||||||
|
return commandkit.Acquired[View]{}, nil
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{makeScope("app"), makeScope("other")}}))
|
||||||
|
result := app.Execute(context.Background(), []string{"config", "validate", "--scope=all", "--set=/nested={\"numbers\":[1,2.5],\"v\":\"a=b\"}", "--set=/nested=null", "--set=/false=false", "--set=/zero=0", "--set=/empty=\"\""}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||||
|
if result.Kind != commandkit.OutcomeSuccess || len(received) != 2 || !reflect.DeepEqual(received[0].Overrides, received[1].Overrides) || len(received[0].Overrides) != 5 {
|
||||||
|
t.Fatalf("result=%#v selections=%#v", result, received)
|
||||||
|
}
|
||||||
|
first := received[1].Overrides
|
||||||
|
object, ok := first[0].Value.(map[string]any)
|
||||||
|
if !ok || !reflect.DeepEqual(object["numbers"], []any{int64(1), float64(2.5)}) || object["v"] != "a=b" || first[0].Flag != "--set" || first[1].Path.String() != "/nested" || first[1].Value != nil || first[2].Value != false || first[3].Value != int64(0) || first[4].Value != "" {
|
||||||
|
t.Fatalf("records=%#v", first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOverridePreflightAndComparisonSides(t *testing.T) {
|
||||||
|
calls, factories := 0, 0
|
||||||
|
var pair ComparisonRequest[struct{}]
|
||||||
|
supported := Scope[struct{}, struct{}]{Name: "app", Operations: []Operation{Validate, Diff}, Features: ScopeFeatures{Overrides: true}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
||||||
|
calls++
|
||||||
|
return commandkit.Acquired[View]{}, nil
|
||||||
|
}, Compare: func(_ context.Context, value ComparisonRequest[struct{}], _ struct{}) (commandkit.Acquired[[]configkit.Difference], error) {
|
||||||
|
calls++
|
||||||
|
pair = value
|
||||||
|
return commandkit.Acquired[[]configkit.Difference]{}, nil
|
||||||
|
}}
|
||||||
|
unsupported := Scope[struct{}, struct{}]{Name: "other", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
||||||
|
calls++
|
||||||
|
return commandkit.Acquired[View]{}, nil
|
||||||
|
}}
|
||||||
|
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{NewInput: func() struct{} { factories++; return struct{}{} }, Scopes: []Scope[struct{}, struct{}]{supported, unsupported}}))
|
||||||
|
for _, args := range [][]string{{"config", "validate", "--scope=other", "--set=/x=1"}, {"config", "validate", "--scope=all", "--set=/x=1"}, {"config", "validate", "--set=/bad"}, {"config", "validate", "--set=/bad={"}, {"config", "validate", "--set=bad=1"}} {
|
||||||
|
result := app.Execute(context.Background(), args, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||||
|
if result.Kind != commandkit.OutcomeUsage || calls != 0 || factories != 0 {
|
||||||
|
t.Fatalf("preflight %v: %#v, calls=%d factories=%d", args, result, calls, factories)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result := app.Execute(context.Background(), []string{"config", "diff", "--set=/x=1", "--against-set=/x=2", "--against-set=/y=[]"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||||
|
if result.Kind != commandkit.OutcomeSuccess || len(pair.Left.Selection.Overrides) != 1 || len(pair.Right.Selection.Overrides) != 2 || pair.Left.Selection.Overrides[0].Flag != "--set" || pair.Right.Selection.Overrides[0].Flag != "--against-set" || pair.Left.Selection.Overrides[0].Value != int64(1) || pair.Right.Selection.Overrides[0].Value != int64(2) {
|
||||||
|
t.Fatalf("pair=%#v outcome=%#v", pair, result)
|
||||||
|
}
|
||||||
|
result = app.Execute(context.Background(), []string{"config", "diff", "--set=/x=1"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||||
|
if result.Kind != commandkit.OutcomeSuccess || pair.Right.Selection.Overrides != nil {
|
||||||
|
t.Fatalf("omitted right = %#v", pair.Right.Selection)
|
||||||
|
}
|
||||||
|
without := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{unsupported}}))
|
||||||
|
if result := without.Execute(context.Background(), []string{"config", "validate", "--set=/x=1"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}); result.Kind != commandkit.OutcomeUsage {
|
||||||
|
t.Fatalf("unavailable flag: %#v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOverrideNativeLoadErrorAndProvenance(t *testing.T) {
|
||||||
|
type settings struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
}
|
||||||
|
scope := FromConfigKit("app", "", func(_ context.Context, selection Selection, _, _ struct{}) (*configkit.Result[settings], error) {
|
||||||
|
return configkit.Load(configkit.Mapping("input", map[string]any{"name": "before"}), configkit.Options[settings]{Overrides: selection.Overrides})
|
||||||
|
})
|
||||||
|
scope.Features.Overrides = true
|
||||||
|
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}}))
|
||||||
|
var out bytes.Buffer
|
||||||
|
result := app.Execute(context.Background(), []string{"config", "explain", "--path=/name", "--set=/name=\"after\""}, commandkit.Streams{Out: &out, Err: io.Discard})
|
||||||
|
if result.Kind != commandkit.OutcomeSuccess || !strings.Contains(out.String(), "--set") {
|
||||||
|
t.Fatalf("provenance: %#v %q", result, out.String())
|
||||||
|
}
|
||||||
|
result = app.Execute(context.Background(), []string{"config", "validate", "--set==1"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||||
|
var loadErr *configkit.LoadError
|
||||||
|
if result.Kind != commandkit.OutcomeOperational || !errors.As(result.Cause, &loadErr) {
|
||||||
|
t.Fatalf("native invalid root override: %#v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ func runConfigShow[I, S any](invocation commandkit.Invocation, plan *configOpera
|
|||||||
if selected.scope.scope.Acquire == nil {
|
if selected.scope.scope.Acquire == nil {
|
||||||
return commandkit.Failure(fmt.Sprintf("Configuration scope %q has no acquisition callback.", selected.scope.scope.Name))
|
return commandkit.Failure(fmt.Sprintf("Configuration scope %q has no acquisition callback.", selected.scope.scope.Name))
|
||||||
}
|
}
|
||||||
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{Operation: Show, Selection: plan.selection}, selected.input, state)
|
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{Operation: Show, Selection: selected.selection}, selected.input, state)
|
||||||
if acquireErr != nil {
|
if acquireErr != nil {
|
||||||
return configAcquisitionFailure(reporter, "show", selected.scope.scope.Name, acquired, acquireErr)
|
return configAcquisitionFailure(reporter, "show", selected.scope.scope.Name, acquired, acquireErr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOp
|
|||||||
}
|
}
|
||||||
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
|
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
|
||||||
Operation: Sources,
|
Operation: Sources,
|
||||||
Selection: plan.selection,
|
Selection: selected.selection,
|
||||||
}, selected.input, state)
|
}, selected.input, state)
|
||||||
queueConfigAcquisition(reporter, acquired)
|
queueConfigAcquisition(reporter, acquired)
|
||||||
if acquireErr != nil {
|
if acquireErr != nil {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *confi
|
|||||||
outcome = commandkit.Failure(fmt.Sprintf("Configuration scope %q has no acquisition callback.", name))
|
outcome = commandkit.Failure(fmt.Sprintf("Configuration scope %q has no acquisition callback.", name))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
acquired, err := selected.scope.scope.Acquire(invocation.Context, Request{Operation: Validate, Selection: plan.selection}, selected.input, state)
|
acquired, err := selected.scope.scope.Acquire(invocation.Context, Request{Operation: Validate, Selection: selected.selection}, selected.input, state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status := "failed"
|
status := "failed"
|
||||||
if invocation.Context.Err() != nil {
|
if invocation.Context.Err() != nil {
|
||||||
|
|||||||
@@ -150,14 +150,14 @@ func TestWithSmokeTestingForwardsAuthoritativeNativeRequestAndRetainsReport(t *t
|
|||||||
suite := WithSmokeTesting(Suite[input, state]{},
|
suite := WithSmokeTesting(Suite[input, state]{},
|
||||||
func(ctx context.Context, selection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
func(ctx context.Context, selection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
||||||
loaderCalls++
|
loaderCalls++
|
||||||
if ctx.Value(contextKey{}) != "smoke invocation" || selection != wantSelection || gotInput != wantInput || gotState != wantState {
|
if ctx.Value(contextKey{}) != "smoke invocation" || !reflect.DeepEqual(selection, wantSelection) || gotInput != wantInput || gotState != wantState {
|
||||||
t.Errorf("loader args = ctx=%v selection=%#v input=%#v state=%#v", ctx.Value(contextKey{}), selection, gotInput, gotState)
|
t.Errorf("loader args = ctx=%v selection=%#v input=%#v state=%#v", ctx.Value(contextKey{}), selection, gotInput, gotState)
|
||||||
}
|
}
|
||||||
return engine, nil
|
return engine, nil
|
||||||
},
|
},
|
||||||
func(ctx context.Context, request SmokeRequest, gotInput input, gotState state) (promptkit.SmokeTestRequest, error) {
|
func(ctx context.Context, request SmokeRequest, gotInput input, gotState state) (promptkit.SmokeTestRequest, error) {
|
||||||
builderCalls++
|
builderCalls++
|
||||||
if ctx.Value(contextKey{}) != "smoke invocation" || request.Selection != wantSelection || request.Request.Kind != promptkit.SmokeTestAuthentication || gotInput != wantInput || gotState != wantState {
|
if ctx.Value(contextKey{}) != "smoke invocation" || !reflect.DeepEqual(request.Selection, wantSelection) || request.Request.Kind != promptkit.SmokeTestAuthentication || gotInput != wantInput || gotState != wantState {
|
||||||
t.Errorf("builder args = ctx=%v request=%#v input=%#v state=%#v", ctx.Value(contextKey{}), request, gotInput, gotState)
|
t.Errorf("builder args = ctx=%v request=%#v input=%#v state=%#v", ctx.Value(contextKey{}), request, gotInput, gotState)
|
||||||
}
|
}
|
||||||
return built, nil
|
return built, nil
|
||||||
@@ -431,7 +431,7 @@ func TestConfigScopeCompareUsesNativeDiffAndLoadsBothEqualSelections(t *testing.
|
|||||||
if !reflect.DeepEqual(got.Value, want) {
|
if !reflect.DeepEqual(got.Value, want) {
|
||||||
t.Fatalf("adapter differences = %#v, want native PromptKit result %#v", got.Value, want)
|
t.Fatalf("adapter differences = %#v, want native PromptKit result %#v", got.Value, want)
|
||||||
}
|
}
|
||||||
if len(calls) != 2 || calls[0].selection != selection || calls[1].selection != selection || calls[0].input.label != "before" || calls[1].input.label != "after" || calls[0].state != state || calls[1].state != state {
|
if len(calls) != 2 || !reflect.DeepEqual(calls[0].selection, selection) || !reflect.DeepEqual(calls[1].selection, selection) || calls[0].input.label != "before" || calls[1].input.label != "after" || calls[0].state != state || calls[1].state != state {
|
||||||
t.Fatalf("loader calls = %#v", calls)
|
t.Fatalf("loader calls = %#v", calls)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -659,7 +659,7 @@ func TestFromEngineListsNativeCatalogsWithInvocationValues(t *testing.T) {
|
|||||||
loaderCalls := 0
|
loaderCalls := 0
|
||||||
suite := FromEngine(func(ctx context.Context, selection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
suite := FromEngine(func(ctx context.Context, selection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
||||||
loaderCalls++
|
loaderCalls++
|
||||||
if ctx.Value(contextKey{}) != "catalog invocation" || selection != wantSelection || gotInput != wantInput || gotState != wantState {
|
if ctx.Value(contextKey{}) != "catalog invocation" || !reflect.DeepEqual(selection, wantSelection) || gotInput != wantInput || gotState != wantState {
|
||||||
t.Errorf("loader args = ctx=%v selection=%#v input=%#v state=%#v", ctx.Value(contextKey{}), selection, gotInput, gotState)
|
t.Errorf("loader args = ctx=%v selection=%#v input=%#v state=%#v", ctx.Value(contextKey{}), selection, gotInput, gotState)
|
||||||
}
|
}
|
||||||
return engine, nil
|
return engine, nil
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ type inspectionInput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type applicationCacheKey struct {
|
type applicationCacheKey struct {
|
||||||
selection ckconfig.Selection
|
config commandkit.Supplied[string]
|
||||||
|
overlay commandkit.Supplied[string]
|
||||||
campaign commandkit.Supplied[string]
|
campaign commandkit.Supplied[string]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ func loadApplicationConfig(ctx context.Context, selection ckconfig.Selection, in
|
|||||||
if input != nil {
|
if input != nil {
|
||||||
campaignInput = input.Campaign
|
campaignInput = input.Campaign
|
||||||
}
|
}
|
||||||
key := applicationCacheKey{selection: selection, campaign: campaignInput}
|
key := applicationCacheKey{config: selection.Config, overlay: selection.Overlay, campaign: campaignInput}
|
||||||
if cache != nil {
|
if cache != nil {
|
||||||
if cached, ok := cache.applications[key]; ok {
|
if cached, ok := cache.applications[key]; ok {
|
||||||
return cached, nil
|
return cached, nil
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ func TestBuildValidationRequestDefaultsOnlyAbsentVariables(t *testing.T) {
|
|||||||
if !reflect.DeepEqual(got.Inputs, inputs) || !reflect.DeepEqual(seed.Request.Vars, explicit) {
|
if !reflect.DeepEqual(got.Inputs, inputs) || !reflect.DeepEqual(seed.Request.Vars, explicit) {
|
||||||
t.Fatalf("seed bindings changed: built=%#v seed=%#v", got.Inputs, explicit)
|
t.Fatalf("seed bindings changed: built=%#v seed=%#v", got.Inputs, explicit)
|
||||||
}
|
}
|
||||||
key := applicationCacheKey{selection: seed.Selection, campaign: input.Campaign}
|
key := applicationCacheKey{config: seed.Selection.Config, overlay: seed.Selection.Overlay, campaign: input.Campaign}
|
||||||
cached := cache.applications[key]
|
cached := cache.applications[key]
|
||||||
if cached == nil {
|
if cached == nil {
|
||||||
t.Fatal("builder did not load application configuration into invocation state")
|
t.Fatal("builder did not load application configuration into invocation state")
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func TestSmokeTestCommandRetainsSingletonValuesAndResetsInvocations(t *testing.T
|
|||||||
Overlay: commandkit.Supplied[string]{Value: "final-overlay.yml", Set: true},
|
Overlay: commandkit.Supplied[string]{Value: "final-overlay.yml", Set: true},
|
||||||
}
|
}
|
||||||
got := requests[0]
|
got := requests[0]
|
||||||
if got.selection != wantSelection || got.input.campaign != "final" || got.request.BackendID != "final-backend" || got.request.PromptID != "final-prompt" || got.request.ProfileID != "final-profile" {
|
if !reflect.DeepEqual(got.selection, wantSelection) || got.input.campaign != "final" || got.request.BackendID != "final-backend" || got.request.PromptID != "final-prompt" || got.request.ProfileID != "final-profile" {
|
||||||
t.Fatalf("last supplied values = %#v", got)
|
t.Fatalf("last supplied values = %#v", got)
|
||||||
}
|
}
|
||||||
if got.request.Execution == nil || got.request.Execution.Model != "final-model" || got.request.Execution.Temperature == nil || *got.request.Execution.Temperature != 0 || got.request.TransientRetries == nil || *got.request.TransientRetries != 0 {
|
if got.request.Execution == nil || got.request.Execution.Model != "final-model" || got.request.Execution.Temperature == nil || *got.request.Execution.Temperature != 0 || got.request.TransientRetries == nil || *got.request.TransientRetries != 0 {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
@@ -61,7 +62,7 @@ func TestWithRequestValidationPreservesSuiteAndValidatesSeedWithoutBuilder(t *te
|
|||||||
loaderCalls := 0
|
loaderCalls := 0
|
||||||
suite = WithRequestValidation(suite, func(gotContext context.Context, gotSelection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
suite = WithRequestValidation(suite, func(gotContext context.Context, gotSelection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
||||||
loaderCalls++
|
loaderCalls++
|
||||||
if gotContext != ctx || gotContext.Value(contextKey{}) != "invocation" || gotSelection != selection || gotInput.value != "input" || gotState.value != "state" {
|
if gotContext != ctx || gotContext.Value(contextKey{}) != "invocation" || !reflect.DeepEqual(gotSelection, selection) || gotInput.value != "input" || gotState.value != "state" {
|
||||||
t.Errorf("loader args = (%v, %#v, %#v, %#v)", gotContext, gotSelection, gotInput, gotState)
|
t.Errorf("loader args = (%v, %#v, %#v, %#v)", gotContext, gotSelection, gotInput, gotState)
|
||||||
}
|
}
|
||||||
return engine, nil
|
return engine, nil
|
||||||
@@ -138,7 +139,7 @@ func TestWithRequestValidationDelegatesNativeOutcomesWithoutModelCalls(t *testin
|
|||||||
engine := validationTestEngine(t, adapterPrompt, promptkit.WithLLMClient(client))
|
engine := validationTestEngine(t, adapterPrompt, promptkit.WithLLMClient(client))
|
||||||
selection := ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "selected.yml", Set: true}}
|
selection := ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "selected.yml", Set: true}}
|
||||||
suite := WithRequestValidation(Suite[struct{}, struct{}]{}, func(_ context.Context, gotSelection ckconfig.Selection, _, _ struct{}) (*promptkit.Engine, error) {
|
suite := WithRequestValidation(Suite[struct{}, struct{}]{}, func(_ context.Context, gotSelection ckconfig.Selection, _, _ struct{}) (*promptkit.Engine, error) {
|
||||||
if gotSelection != selection {
|
if !reflect.DeepEqual(gotSelection, selection) {
|
||||||
t.Errorf("loader selection = %#v, want %#v", gotSelection, selection)
|
t.Errorf("loader selection = %#v, want %#v", gotSelection, selection)
|
||||||
}
|
}
|
||||||
return engine, nil
|
return engine, nil
|
||||||
@@ -190,13 +191,13 @@ func TestWithRequestValidationBuilderOutputReachesNativeValidation(t *testing.T)
|
|||||||
builderCalls, loaderCalls := 0, 0
|
builderCalls, loaderCalls := 0, 0
|
||||||
suite := WithRequestValidation(Suite[input, state]{}, func(gotContext context.Context, gotSelection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
suite := WithRequestValidation(Suite[input, state]{}, func(gotContext context.Context, gotSelection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
||||||
loaderCalls++
|
loaderCalls++
|
||||||
if gotContext != ctx || gotContext.Value(contextKey{}) != "validation invocation" || gotSelection != selection || gotInput != wantInput || gotState != wantState {
|
if gotContext != ctx || gotContext.Value(contextKey{}) != "validation invocation" || !reflect.DeepEqual(gotSelection, selection) || gotInput != wantInput || gotState != wantState {
|
||||||
t.Errorf("loader args = (%v, %#v, %#v, %#v)", gotContext, gotSelection, gotInput, gotState)
|
t.Errorf("loader args = (%v, %#v, %#v, %#v)", gotContext, gotSelection, gotInput, gotState)
|
||||||
}
|
}
|
||||||
return engine, nil
|
return engine, nil
|
||||||
}, func(gotContext context.Context, request ValidationRequest, gotInput input, gotState state) (promptkit.RunRequest, error) {
|
}, func(gotContext context.Context, request ValidationRequest, gotInput input, gotState state) (promptkit.RunRequest, error) {
|
||||||
builderCalls++
|
builderCalls++
|
||||||
if gotContext != ctx || gotContext.Value(contextKey{}) != "validation invocation" || request.Selection != selection || request.Request.PromptID != seed.PromptID || request.Request.Vars["topic"] != "seed topic" || gotInput != wantInput || gotState != wantState {
|
if gotContext != ctx || gotContext.Value(contextKey{}) != "validation invocation" || !reflect.DeepEqual(request.Selection, selection) || request.Request.PromptID != seed.PromptID || request.Request.Vars["topic"] != "seed topic" || gotInput != wantInput || gotState != wantState {
|
||||||
t.Errorf("builder args = (%v, %#v, %#v, %#v)", gotContext, request, gotInput, gotState)
|
t.Errorf("builder args = (%v, %#v, %#v, %#v)", gotContext, request, gotInput, gotState)
|
||||||
}
|
}
|
||||||
built := request.Request
|
built := request.Request
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func TestValidationCommandComposesWithRequirementsShow(t *testing.T) {
|
|||||||
NewState: func() state { return state{sequence: 1} },
|
NewState: func() state { return state{sequence: 1} },
|
||||||
Selectors: []commandkit.Flag[input]{campaignFlag},
|
Selectors: []commandkit.Flag[input]{campaignFlag},
|
||||||
CheckInput: func(selection ckconfig.Selection, value input) error {
|
CheckInput: func(selection ckconfig.Selection, value input) error {
|
||||||
if selection != wantSelection || !reflect.DeepEqual(value, wantInput) {
|
if !reflect.DeepEqual(selection, wantSelection) || !reflect.DeepEqual(value, wantInput) {
|
||||||
t.Errorf("checked input = (%#v, %#v)", selection, value)
|
t.Errorf("checked input = (%#v, %#v)", selection, value)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -70,7 +70,7 @@ func TestValidationCommandComposesWithRequirementsShow(t *testing.T) {
|
|||||||
if outcome.Kind != commandkit.OutcomeSuccess || outcome.Status != 0 || validateCalls != 1 {
|
if outcome.Kind != commandkit.OutcomeSuccess || outcome.Status != 0 || validateCalls != 1 {
|
||||||
t.Fatalf("validation outcome = (%#v, %q), calls=%d", outcome, output.String(), validateCalls)
|
t.Fatalf("validation outcome = (%#v, %q), calls=%d", outcome, output.String(), validateCalls)
|
||||||
}
|
}
|
||||||
if received.Selection != wantSelection || received.Request.PromptID != "briefing" || received.Request.ProfileID != "editorial" || received.Request.SessionID != "session-1" {
|
if !reflect.DeepEqual(received.Selection, wantSelection) || received.Request.PromptID != "briefing" || received.Request.ProfileID != "editorial" || received.Request.SessionID != "session-1" {
|
||||||
t.Errorf("validation request = %#v", received)
|
t.Errorf("validation request = %#v", received)
|
||||||
}
|
}
|
||||||
if received.Request.Vars["topic"] != "roadmap" || len(received.Request.Vars) != 1 {
|
if received.Request.Vars["topic"] != "roadmap" || len(received.Request.Vars) != 1 {
|
||||||
|
|||||||
Reference in New Issue
Block a user