Add opt-in configuration overrides

This commit is contained in:
2026-11-07 04:39:34 +00:00
parent 85ae55bf42
commit aeb70e5873
22 changed files with 238 additions and 35 deletions

View File

@@ -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
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
to expose PromptKit engine configuration through these handlers. The scope
declares validate, show, sources, explain, and native diff. It does not install

View File

@@ -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
records when available. `Findings.Messages` contains presentations associated
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

View File

@@ -1,6 +1,6 @@
# 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
@@ -571,6 +571,11 @@ model. Explicit-empty custom collections remain application-defined.
## 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
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.

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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

View 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
}
}

View 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)
}
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 {