Add opt-in configuration overrides
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -66,7 +67,7 @@ func TestFromConfigKitCompareReturnsNativeChangesAndLoadsEqualSelections(t *test
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
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 (
|
||||
"context"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -75,7 +76,7 @@ func TestComparisonPreparation(t *testing.T) {
|
||||
}
|
||||
if want := (Selection{
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -106,7 +107,7 @@ func TestComparisonPreparesEqualSelectionsWithSeparateTypedInputs(t *testing.T)
|
||||
Config: commandkit.Supplied[string]{Value: "same.yml", 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)
|
||||
}
|
||||
}
|
||||
@@ -115,7 +116,7 @@ func TestComparisonProviderReceivesCheckedPairAndSharedState(t *testing.T) {
|
||||
type input struct{ ID int }
|
||||
type state struct{ Label string }
|
||||
inputCalls, checkCalls, stateCalls, compareCalls, acquireCalls := 0, 0, 0, 0, 0
|
||||
checked := make(map[Selection]input)
|
||||
var checked []input
|
||||
shared := &state{Label: "shared"}
|
||||
var request ComparisonRequest[input]
|
||||
app := newConfigTestApp(t, Command(Suite[input, *state]{
|
||||
@@ -135,7 +136,7 @@ func TestComparisonProviderReceivesCheckedPairAndSharedState(t *testing.T) {
|
||||
if scope != "app" {
|
||||
return errors.New("unexpected scope")
|
||||
}
|
||||
checked[selection] = value
|
||||
checked = append(checked, value)
|
||||
return nil
|
||||
},
|
||||
Scopes: []Scope[input, *state]{{
|
||||
@@ -162,10 +163,10 @@ func TestComparisonProviderReceivesCheckedPairAndSharedState(t *testing.T) {
|
||||
}
|
||||
left := Selection{Config: commandkit.Supplied[string]{Value: "before.yml", 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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
if inputCalls != 2 || checkCalls != 2 || stateCalls != 1 || compareCalls != 1 || acquireCalls != 0 {
|
||||
|
||||
@@ -15,6 +15,8 @@ type Selection struct {
|
||||
Config commandkit.Supplied[string]
|
||||
// Overlay is the caller-supplied overlay path, if present.
|
||||
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.
|
||||
|
||||
@@ -92,7 +92,7 @@ func TestFromConfigKitPassesInvocationValuesAndNativeFacets(t *testing.T) {
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
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{
|
||||
Operation: Explain,
|
||||
Selection: plan.selection,
|
||||
Selection: selected.selection,
|
||||
}, selected.input, state)
|
||||
queueConfigAcquisition(reporter, acquired)
|
||||
if acquireErr != nil {
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestComparisonSidesKeepIndependentSelectionsAndDefaults(t *testing.T) {
|
||||
}
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,9 @@ type configCommandInput[I, S any] struct {
|
||||
}
|
||||
|
||||
type selectedScopeInput[I, S any] struct {
|
||||
scope *preparedScope[I, S]
|
||||
input I
|
||||
scope *preparedScope[I, S]
|
||||
input I
|
||||
selection Selection
|
||||
}
|
||||
|
||||
type preparedComparison[I, S any] struct {
|
||||
@@ -31,7 +32,6 @@ type preparedComparison[I, S any] struct {
|
||||
type configOperationPlan[I, S any] struct {
|
||||
prepared *preparedSuite[I, S]
|
||||
operation Operation
|
||||
selection Selection
|
||||
scopeName string
|
||||
path configkit.Path
|
||||
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 {
|
||||
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 }))
|
||||
@@ -155,6 +167,9 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
||||
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))
|
||||
}
|
||||
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
|
||||
@@ -172,13 +187,14 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
||||
plan := &configOperationPlan[I, S]{
|
||||
prepared: prepared,
|
||||
operation: operation,
|
||||
selection: input.selection,
|
||||
scopeName: scopeName,
|
||||
path: path,
|
||||
scopes: make([]selectedScopeInput[I, S], 0, len(selected)),
|
||||
}
|
||||
if operation == Diff {
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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
|
||||
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 {
|
||||
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 {
|
||||
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{
|
||||
Operation: Sources,
|
||||
Selection: plan.selection,
|
||||
Selection: selected.selection,
|
||||
}, selected.input, state)
|
||||
queueConfigAcquisition(reporter, acquired)
|
||||
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))
|
||||
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 {
|
||||
status := "failed"
|
||||
if invocation.Context.Err() != nil {
|
||||
|
||||
@@ -150,14 +150,14 @@ func TestWithSmokeTestingForwardsAuthoritativeNativeRequestAndRetainsReport(t *t
|
||||
suite := WithSmokeTesting(Suite[input, state]{},
|
||||
func(ctx context.Context, selection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
||||
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)
|
||||
}
|
||||
return engine, nil
|
||||
},
|
||||
func(ctx context.Context, request SmokeRequest, gotInput input, gotState state) (promptkit.SmokeTestRequest, error) {
|
||||
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)
|
||||
}
|
||||
return built, nil
|
||||
@@ -431,7 +431,7 @@ func TestConfigScopeCompareUsesNativeDiffAndLoadsBothEqualSelections(t *testing.
|
||||
if !reflect.DeepEqual(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)
|
||||
}
|
||||
}
|
||||
@@ -659,7 +659,7 @@ func TestFromEngineListsNativeCatalogsWithInvocationValues(t *testing.T) {
|
||||
loaderCalls := 0
|
||||
suite := FromEngine(func(ctx context.Context, selection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
||||
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)
|
||||
}
|
||||
return engine, nil
|
||||
|
||||
@@ -32,8 +32,9 @@ type inspectionInput struct {
|
||||
}
|
||||
|
||||
type applicationCacheKey struct {
|
||||
selection ckconfig.Selection
|
||||
campaign commandkit.Supplied[string]
|
||||
config commandkit.Supplied[string]
|
||||
overlay commandkit.Supplied[string]
|
||||
campaign commandkit.Supplied[string]
|
||||
}
|
||||
|
||||
// runCache belongs to one invocation and is shared only where the application
|
||||
@@ -80,7 +81,7 @@ func loadApplicationConfig(ctx context.Context, selection ckconfig.Selection, in
|
||||
if input != nil {
|
||||
campaignInput = input.Campaign
|
||||
}
|
||||
key := applicationCacheKey{selection: selection, campaign: campaignInput}
|
||||
key := applicationCacheKey{config: selection.Config, overlay: selection.Overlay, campaign: campaignInput}
|
||||
if cache != nil {
|
||||
if cached, ok := cache.applications[key]; ok {
|
||||
return cached, nil
|
||||
|
||||
@@ -266,7 +266,7 @@ func TestBuildValidationRequestDefaultsOnlyAbsentVariables(t *testing.T) {
|
||||
if !reflect.DeepEqual(got.Inputs, inputs) || !reflect.DeepEqual(seed.Request.Vars, 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]
|
||||
if cached == nil {
|
||||
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},
|
||||
}
|
||||
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)
|
||||
}
|
||||
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"
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
@@ -61,7 +62,7 @@ func TestWithRequestValidationPreservesSuiteAndValidatesSeedWithoutBuilder(t *te
|
||||
loaderCalls := 0
|
||||
suite = WithRequestValidation(suite, func(gotContext context.Context, gotSelection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
||||
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)
|
||||
}
|
||||
return engine, nil
|
||||
@@ -138,7 +139,7 @@ func TestWithRequestValidationDelegatesNativeOutcomesWithoutModelCalls(t *testin
|
||||
engine := validationTestEngine(t, adapterPrompt, promptkit.WithLLMClient(client))
|
||||
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) {
|
||||
if gotSelection != selection {
|
||||
if !reflect.DeepEqual(gotSelection, selection) {
|
||||
t.Errorf("loader selection = %#v, want %#v", gotSelection, selection)
|
||||
}
|
||||
return engine, nil
|
||||
@@ -190,13 +191,13 @@ func TestWithRequestValidationBuilderOutputReachesNativeValidation(t *testing.T)
|
||||
builderCalls, loaderCalls := 0, 0
|
||||
suite := WithRequestValidation(Suite[input, state]{}, func(gotContext context.Context, gotSelection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
||||
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)
|
||||
}
|
||||
return engine, nil
|
||||
}, func(gotContext context.Context, request ValidationRequest, gotInput input, gotState state) (promptkit.RunRequest, error) {
|
||||
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)
|
||||
}
|
||||
built := request.Request
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestValidationCommandComposesWithRequirementsShow(t *testing.T) {
|
||||
NewState: func() state { return state{sequence: 1} },
|
||||
Selectors: []commandkit.Flag[input]{campaignFlag},
|
||||
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)
|
||||
}
|
||||
return nil
|
||||
@@ -70,7 +70,7 @@ func TestValidationCommandComposesWithRequirementsShow(t *testing.T) {
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || outcome.Status != 0 || validateCalls != 1 {
|
||||
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)
|
||||
}
|
||||
if received.Request.Vars["topic"] != "roadmap" || len(received.Request.Vars) != 1 {
|
||||
|
||||
Reference in New Issue
Block a user