Update the composed PromptKit consumer

This commit is contained in:
2026-11-07 05:01:30 +00:00
parent 42af1da142
commit 1289868ed0
4 changed files with 236 additions and 30 deletions

View File

@@ -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–21 complete. Status: implementation in progress; Stages 1–22 complete.
## Scope, authority, and execution ## Scope, authority, and execution
@@ -784,6 +784,10 @@ do not initialize application runtime state.
## Stage 22: Composed consumer and application-owned flag reuse ## Stage 22: Composed consumer and application-owned flag reuse
**Complete.** The composed consumer exercises request preview, namespaces,
reusable flags, independent overrides, and failure evidence with invocation-local
caching. The root, ckconfig, and ckprompt workspace suites passed with Go 1.27.0.
Update the maintained [composed consumer](../../integrations/ckprompt/examples/composed) Update the maintained [composed consumer](../../integrations/ckprompt/examples/composed)
to install `ckprompt.Declarations`, explicit empty native namespace defaults to install `ckprompt.Declarations`, explicit empty native namespace defaults
(enabling root defaults plus opt-in selectors), request preparation, extended (enabling root defaults plus opt-in selectors), request preparation, extended

View File

@@ -1,7 +1,9 @@
# Composed consumer # Composed consumer
This example is part of the ckprompt module and uses core v0.3.0, ckconfig This example is part of the ckprompt module and uses core v0.3.0, ckconfig
v0.3.0, ConfigKit v1.1.0, and PromptKit v0.18.0. It requires Go 1.27.0. v0.3.0, ConfigKit v1.1.0, and PromptKit v0.20.0. It requires Go 1.27.0.
The current work uses the aligned local workspace until the required core and
ckconfig releases are published.
This example composes application configuration, PromptKit configuration, This example composes application configuration, PromptKit configuration,
PromptKit comparison and catalogs, definition and requirements inspection, and PromptKit comparison and catalogs, definition and requirements inspection, and
@@ -12,6 +14,7 @@ go run . config scopes
go run . config validate --scope all --campaign spring --limit=0 go run . config validate --scope all --campaign spring --limit=0
go run . config diff --scope app --config fixtures/before.yml --against-config fixtures/after.yml go run . config diff --scope app --config fixtures/before.yml --against-config fixtures/after.yml
go run . config diff --scope app --config fixtures/before.yml --against-config fixtures/after.yml --campaign spring --limit=0 go run . config diff --scope app --config fixtures/before.yml --against-config fixtures/after.yml --campaign spring --limit=0
go run . config diff --scope app --campaign spring --limit=0 --against-campaign autumn --against-limit=0 --set '/campaigns/spring/model="left"' --against-set '/campaigns/spring/model="right"' --details
go run . config diff --scope promptkit --config fixtures/before.yml --against-config fixtures/after.yml --campaign spring --against-campaign spring go run . config diff --scope promptkit --config fixtures/before.yml --against-config fixtures/after.yml --campaign spring --against-campaign spring
go run . promptkit list prompts --config fixtures/before.yml --campaign spring go run . promptkit list prompts --config fixtures/before.yml --campaign spring
go run . promptkit list profiles --config fixtures/after.yml --campaign autumn --format yaml go run . promptkit list profiles --config fixtures/after.yml --campaign autumn --format yaml
@@ -22,6 +25,8 @@ go run . list collections --campaign spring
go run . pk list bundles --campaign spring go run . pk list bundles --campaign spring
go run . promptkit list prompts --namespace field-notes --campaign spring go run . promptkit list prompts --namespace field-notes --campaign spring
go run . promptkit show prompt briefing --campaign spring --format yaml go run . promptkit show prompt briefing --campaign spring --format yaml
go run . promptkit show prompt briefing --campaign spring --prompt-namespace field-notes
go run . promptkit show prepared briefing --campaign spring --input topic=sprint --profile spring --extra-params '{"campaign":"preview"}'
go run . pk show prompt briefing --campaign spring --fmt yaml go run . pk show prompt briefing --campaign spring --fmt yaml
go run . promptkit show profile spring --campaign spring go run . promptkit show profile spring --campaign spring
go run . promptkit requirements show briefing --campaign spring go run . promptkit requirements show briefing --campaign spring
@@ -34,6 +39,7 @@ go run . smoke text_inference --campaign spring --llm-prompt briefing --llm-prof
go run . promptkit test all --campaign spring --llm-prompt briefing --llm-profile spring go run . promptkit test all --campaign spring --llm-prompt briefing --llm-profile spring
go run . pk --hlep go run . pk --hlep
go run . session restore session-42 --campaign spring go run . session restore session-42 --campaign spring
go run . request inspect --llm-prompt briefing --llm-profile= --llm-model example --llm-input-text topic=sprint --format json
``` ```
`newApp` installs application-owned command and flag aliases alongside the `newApp` installs application-owned command and flag aliases alongside the
@@ -51,8 +57,11 @@ Bash, then load a shell script as described in the
in `composed-example config show --config=fixtures/`. The already-entered `pk` in `composed-example config show --config=fixtures/`. The already-entered `pk`
alias selects the PromptKit command context without loading an engine. alias selects the PromptKit command context without loading an engine.
The composition calls `ckconfig.Command(suite)` and `ckprompt.Command(definitions)` The composition installs `ckconfig.Command(suite)` and the complete
directly in a `[]commandkit.Declaration` with root-installed routing rules. `ckprompt.Declarations(definitions)` in a `[]commandkit.Declaration` with
root-installed routing rules. The PromptKit declarations include scoped aliases;
the application-owned `request inspect` command reuses the flag collections
with their canonical names and does not install those aliases.
Group children remain canonical `Command` values. For command-only slices, Group children remain canonical `Command` values. For command-only slices,
convert each `Command` to `Declaration` before appending aliases. The convert each `Command` to `Declaration` before appending aliases. The
[consumer guide](../../../../docs/consumers/guide.md#application-owned-aliases) [consumer guide](../../../../docs/consumers/guide.md#application-owned-aliases)
@@ -79,10 +88,13 @@ Prompt inspection uses an ID only. The default PromptKit bundle also contributes
profiles and a backend to the engine inventory and source reports. The synthetic `field-notes` collection profiles and a backend to the engine inventory and source reports. The synthetic `field-notes` collection
provides one backend, profile, and prompt for collection discovery and native provides one backend, profile, and prompt for collection discovery and native
namespace filtering. The example's shared run cache keys successful results by namespace filtering. The example's shared run cache keys successful results by
the complete supplied configuration/overlay selection and the supplied the complete supplied configuration/overlay selection, CLI overrides, and the
campaign, including presence. The campaign override is applied through ConfigKit supplied campaign, including presence. The campaign override and application
before loading. These are choices for this example's loader and do not define a scope's `--set` records are applied through ConfigKit before loading. The
CommandKit cache policy. PromptKit configuration scope does not enable `--set` because its released
configuration loader has no override handoff. An aggregate operation therefore
rejects overrides when that scope is selected. These are choices for this
example's loaders and do not define a CommandKit cache policy.
Smoke testing is explicitly wired with `ckprompt.WithSmokeTesting` and its Smoke testing is explicitly wired with `ckprompt.WithSmokeTesting` and its
optional request builder is nil, so CommandKit forwards the selected native optional request builder is nil, so CommandKit forwards the selected native
@@ -114,17 +126,19 @@ not execute the prompt or establish input, credential, or execution readiness.
See the [CLI reference](../../../../docs/cli.md#promptkit-requirements) for the See the [CLI reference](../../../../docs/cli.md#promptkit-requirements) for the
command and report contract. command and report contract.
The validation commands exercise concrete PromptKit request preparation. The validation and preview commands exercise concrete PromptKit request preparation.
The first uses the builder's campaign-specific default topic variable and the The first uses the builder's campaign-specific default topic variable and the
operator's topic file. The second also selects the notes branch and supplies its operator's topic file. The second also selects the notes branch and supplies its
file. Both succeed and report only `prompt_id` and `valid: true`; they do not file. Validation reports only `prompt_id` and `valid: true`; preview returns the
execute the prompt. The final command intentionally fails because the prompt's prepared request, including rendered messages and effective provider settings.
Neither executes the prompt. The validation command without inputs intentionally
fails because the prompt's
required `topic` artifact is not supplied. The builder defaults variables from required `topic` artifact is not supplied. The builder defaults variables from
the selected campaign but leaves artifact bindings to the operator. The the selected campaign but leaves artifact bindings to the operator. The
`requirements show` commands remain discovery reports: partial discovery can `requirements show` commands remain discovery reports: partial discovery can
succeed even when a concrete request would fail validation. succeed even when a concrete request would fail validation.
Run `GOWORK=off go test ./...` from this example directory to check it against Use the aligned local workspace to test the three current sources, as described in
the module's declared dependencies. A local workspace can instead select the the [developer guide](../../../../docs/development.md#documentation-validation).
three current sources, as described in the Once released dependency requirements are aligned, `GOWORK=off go test ./...`
[developer guide](../../../../docs/development.md#documentation-validation). from this example directory checks it against its declared dependencies.

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"reflect"
"strconv" "strconv"
"go.maximumdirect.net/ckconfig" "go.maximumdirect.net/ckconfig"
@@ -31,16 +32,16 @@ type inspectionInput struct {
Limit commandkit.Supplied[int] Limit commandkit.Supplied[int]
} }
type applicationCacheKey struct { type applicationCacheEntry struct {
config commandkit.Supplied[string] selection ckconfig.Selection
overlay commandkit.Supplied[string] campaign commandkit.Supplied[string]
campaign commandkit.Supplied[string] result *configkit.Result[appConfiguration]
} }
// runCache belongs to one invocation and is shared only where the application // runCache belongs to one invocation and is shared only where the application
// chooses to pass it to its loaders. // chooses to pass it to its loaders.
type runCache struct { type runCache struct {
applications map[applicationCacheKey]*configkit.Result[appConfiguration] applications []applicationCacheEntry
llmClient promptkit.LLMClient llmClient promptkit.LLMClient
} }
@@ -60,6 +61,12 @@ type restoreInput struct {
Session string Session string
} }
type requestInputs struct {
Execution ckprompt.ExecutionInput `json:"execution"`
Resources ckprompt.ResourceInput `json:"resources"`
Inputs ckprompt.PromptInputs `json:"inputs"`
}
type session struct { type session struct {
ID string ID string
Campaign string Campaign string
@@ -81,10 +88,11 @@ func loadApplicationConfig(ctx context.Context, selection ckconfig.Selection, in
if input != nil { if input != nil {
campaignInput = input.Campaign campaignInput = input.Campaign
} }
key := applicationCacheKey{config: selection.Config, overlay: selection.Overlay, campaign: campaignInput}
if cache != nil { if cache != nil {
if cached, ok := cache.applications[key]; ok { for _, cached := range cache.applications {
return cached, nil if reflect.DeepEqual(cached.selection, selection) && cached.campaign == campaignInput {
return cached.result, nil
}
} }
} }
source := configkit.Mapping("composed application settings", map[string]any{ source := configkit.Mapping("composed application settings", map[string]any{
@@ -105,12 +113,10 @@ func loadApplicationConfig(ctx context.Context, selection ckconfig.Selection, in
if campaignInput.Set { if campaignInput.Set {
options.Overrides = []configkit.Override{{Path: configkit.Keys("default_campaign"), Value: campaignInput.Value, Flag: "--campaign"}} options.Overrides = []configkit.Override{{Path: configkit.Keys("default_campaign"), Value: campaignInput.Value, Flag: "--campaign"}}
} }
options.Overrides = append(options.Overrides, selection.Overrides...)
result, err := configkit.Load(source, options) result, err := configkit.Load(source, options)
if err == nil && result != nil && cache != nil { if err == nil && result != nil && cache != nil {
if cache.applications == nil { cache.applications = append(cache.applications, applicationCacheEntry{selection: selection, campaign: campaignInput, result: result})
cache.applications = make(map[applicationCacheKey]*configkit.Result[appConfiguration])
}
cache.applications[key] = result
} }
return result, err return result, err
} }
@@ -224,6 +230,7 @@ func newAppWithEngineLoader(
stateFactory := func() *runCache { return &runCache{llmClient: client} } stateFactory := func() *runCache { return &runCache{llmClient: client} }
appScope := ckconfig.FromConfigKit("app", "Application settings.", loadApplicationConfig) appScope := ckconfig.FromConfigKit("app", "Application settings.", loadApplicationConfig)
appScope.Features.Overrides = true
engineScope := ckprompt.ConfigScope(engineLoader) engineScope := ckprompt.ConfigScope(engineLoader)
configCommand := ckconfig.Command(ckconfig.Suite[*inspectionInput, *runCache]{ configCommand := ckconfig.Command(ckconfig.Suite[*inspectionInput, *runCache]{
NewInput: inputFactory, NewInput: inputFactory,
@@ -246,11 +253,28 @@ func newAppWithEngineLoader(
definitions.NewState = stateFactory definitions.NewState = stateFactory
definitions.Selectors = []commandkit.Flag[*inspectionInput]{campaignSelector()} definitions.Selectors = []commandkit.Flag[*inspectionInput]{campaignSelector()}
definitions = ckprompt.WithRequestValidation(definitions, engineLoader, buildValidationRequest) definitions = ckprompt.WithRequestValidation(definitions, engineLoader, buildValidationRequest)
definitions = ckprompt.WithRequestPreparation(definitions, engineLoader,
func(ctx context.Context, seed ckprompt.PreparationRequest, input *inspectionInput, cache *runCache) (promptkit.RunRequest, error) {
return buildValidationRequest(ctx, ckprompt.ValidationRequest{Selection: seed.Selection, Request: seed.Request, Namespaces: seed.Namespaces}, input, cache)
})
definitions = ckprompt.WithSmokeTesting(definitions, engineLoader, nil) definitions = ckprompt.WithSmokeTesting(definitions, engineLoader, nil)
definitions = ckprompt.WithNamespaces(definitions, promptkit.Namespaces{})
if smokeTest != nil { if smokeTest != nil {
definitions.SmokeTest = smokeTest definitions.SmokeTest = smokeTest
} }
definitionDeclarations := ckprompt.Declarations(definitions) definitionDeclarations := ckprompt.Declarations(definitions)
resources := ckprompt.NewResourceFlags(func(input *requestInputs) *ckprompt.ResourceInput { return &input.Resources })
execution := ckprompt.NewExecutionFlags(func(input *requestInputs) *ckprompt.ExecutionInput { return &input.Execution })
inputs := ckprompt.NewPromptInputFlags(func(input *requestInputs) *ckprompt.PromptInputs { return &input.Inputs })
requestFlags := append([]commandkit.Flag[requestInputs]{resources.Prompt, resources.Profile}, execution.All()...)
requestFlags = append(requestFlags, inputs.All()...)
requestInspect := commandkit.NewCommand(commandkit.CommandSpec[requestInputs]{
Name: "inspect", Summary: "Inspect application-owned request inputs.", Flags: requestFlags,
Run: func(invocation commandkit.Invocation, input requestInputs) commandkit.Outcome {
_ = invocation.Reporter().Report(commandkit.Report{Value: input})
return commandkit.Success()
},
})
store := map[string]session{ store := map[string]session{
"session-42": {ID: "session-42", Campaign: "spring", Summary: "A synthetic planning session."}, "session-42": {ID: "session-42", Campaign: "spring", Summary: "A synthetic planning session."},
@@ -294,6 +318,7 @@ func newAppWithEngineLoader(
configCommand, configCommand,
commandkit.Completion("composed-example"), commandkit.Completion("composed-example"),
commandkit.Group("session", "Manage saved sessions.", restore), commandkit.Group("session", "Manage saved sessions.", restore),
commandkit.Group("request", "Inspect application-owned request inputs.", requestInspect),
commandkit.Alias("list collections", "promptkit list collections"), commandkit.Alias("list collections", "promptkit list collections"),
commandkit.Alias("promptkit list bundles", "promptkit list collections"), commandkit.Alias("promptkit list bundles", "promptkit list collections"),
commandkit.Alias("pk", "promptkit"), commandkit.Alias("pk", "promptkit"),

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@@ -101,6 +102,159 @@ func TestApplicationCacheDoesNotRetainFailedLoads(t *testing.T) {
} }
} }
func TestApplicationCacheDistinguishesOverrides(t *testing.T) {
cache := &runCache{}
selection := func(value string) ckconfig.Selection {
return ckconfig.Selection{Overrides: []configkit.Override{{Path: configkit.Keys("campaigns", "spring", "model"), Value: value, Flag: "--set"}}}
}
left, err := loadApplicationConfig(context.Background(), selection("left"), nil, cache)
if err != nil {
t.Fatal(err)
}
right, err := loadApplicationConfig(context.Background(), selection("right"), nil, cache)
if err != nil {
t.Fatal(err)
}
again, err := loadApplicationConfig(context.Background(), selection("left"), nil, cache)
if err != nil {
t.Fatal(err)
}
if left == right || left != again || left.Config.Campaigns["spring"].Model != "left" || right.Config.Campaigns["spring"].Model != "right" {
t.Fatalf("override selections reused the wrong result: left=%#v right=%#v", left.Config, right.Config)
}
}
func TestComposedIndependentOverridesAndFailureEvidence(t *testing.T) {
app, err := newApp()
if err != nil {
t.Fatal(err)
}
var output bytes.Buffer
args := []string{"config", "diff", "--scope", "app", "--campaign", "spring", "--limit=0", "--against-campaign", "autumn", "--against-limit=0", "--set", `/campaigns/spring/model="left"`, "--against-set", `/campaigns/spring/model="right"`, "--details", "--format", "json"}
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess {
t.Fatalf("comparison: %#v, %s", outcome, output.String())
}
var report struct {
Differences []struct {
Path string `json:"path"`
Before struct {
Value any `json:"value"`
} `json:"before"`
After struct {
Value any `json:"value"`
} `json:"after"`
} `json:"differences"`
}
if err := json.Unmarshal(output.Bytes(), &report); err != nil {
t.Fatal(err)
}
changes := map[string][2]any{}
for _, difference := range report.Differences {
changes[difference.Path] = [2]any{difference.Before.Value, difference.After.Value}
}
if changes["/campaigns/spring/model"] != [2]any{"left", "right"} || changes["/default_campaign"] != [2]any{"spring", "autumn"} {
t.Fatalf("independent comparison: %#v", changes)
}
output.Reset()
outcome = app.Execute(context.Background(), []string{"config", "sources", "--scope", "app", "--config", "fixtures/missing.yml", "--format", "json"}, commandkit.Streams{Out: &output, Err: io.Discard})
var evidence struct {
Sources []any `json:"sources"`
}
if err := json.Unmarshal(output.Bytes(), &evidence); err != nil {
t.Fatal(err)
}
if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause == nil || len(evidence.Sources) == 0 {
t.Fatalf("failed load evidence: %#v, %s", outcome, output.String())
}
outcome = app.Execute(context.Background(), []string{"config", "validate", "--scope", "app", "--campaign", "spring", "--limit=0", "--set", `/default_campaign="autumn"`}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause == nil {
t.Fatalf("overlapping native overrides: %#v", outcome)
}
loads := 0
loader := func(ctx context.Context, selection ckconfig.Selection, input *inspectionInput, cache *runCache) (*promptkit.Engine, error) {
loads++
return loadPromptEngine(ctx, selection, input, cache)
}
app, err = newAppWithEngineLoader(&exampleLLMClient{}, loader, nil)
if err != nil {
t.Fatal(err)
}
outcome = app.Execute(context.Background(), []string{"config", "validate", "--scope", "all", "--set", `/default_campaign="spring"`}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeUsage || loads != 0 {
t.Fatalf("unsupported aggregate override: %#v", outcome)
}
}
func TestComposedPreviewAndReusableFlags(t *testing.T) {
calls := 0
app, err := newAppWithLLMClient(&exampleLLMClient{calls: &calls})
if err != nil {
t.Fatal(err)
}
var output bytes.Buffer
args := []string{"promptkit", "show", "prepared", "briefing", "--campaign", "spring", "--llm-input-file", "topic=fixtures/topic.txt", "--input", "topic=sprint", "--profile", "spring", "--extra-params", `{"campaign":"preview"}`, "--format", "json"}
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess {
t.Fatalf("preview: %#v, %s", outcome, output.String())
}
var report promptkit.PreparedRun
if err := json.Unmarshal(output.Bytes(), &report); err != nil {
t.Fatal(err)
}
input := &inspectionInput{Campaign: commandkit.Supplied[string]{Value: "spring", Set: true}}
engine, err := loadPromptEngine(context.Background(), ckconfig.Selection{}, input, nil)
if err != nil {
t.Fatal(err)
}
request, err := buildValidationRequest(context.Background(), ckprompt.ValidationRequest{Request: promptkit.RunRequest{PromptID: "briefing", ProfileID: "spring", Inputs: map[string]promptkit.ArtifactRef{"topic": promptkit.Inline("sprint")}, Execution: &promptkit.ExecutionTargetOverride{ExtraParams: map[string]any{"campaign": "preview"}}}}, input, nil)
if err != nil {
t.Fatal(err)
}
native, err := engine.Prepare(context.Background(), request)
if err != nil {
t.Fatal(err)
}
if report.PromptID != native.PromptID || !reflect.DeepEqual(report.Messages, native.Messages) || !reflect.DeepEqual(report.EffectiveModelParams, native.EffectiveModelParams) || calls != 0 {
t.Fatalf("preview diverged from native result or called model: %#v", report)
}
output.Reset()
outcome = app.Execute(context.Background(), []string{"promptkit", "show", "prompt", "briefing", "--campaign", "spring", "--prompt-namespace", "field-notes", "--format", "json"}, commandkit.Streams{Out: &output, Err: io.Discard})
var namespaced promptkit.PromptInspection
if err := json.Unmarshal(output.Bytes(), &namespaced); err != nil {
t.Fatal(err)
}
view, err := engine.WithNamespaces(promptkit.Namespaces{Prompt: "field-notes"})
if err != nil {
t.Fatal(err)
}
wantPrompt, err := view.InspectPrompt(context.Background(), "briefing")
if err != nil {
t.Fatal(err)
}
if outcome.Kind != commandkit.OutcomeSuccess || namespaced.PromptID != wantPrompt.PromptID {
t.Fatalf("namespace selection: %#v, %#v", outcome, namespaced)
}
output.Reset()
args = []string{"request", "inspect", "--llm-prompt", "briefing", "--llm-profile=", "--llm-model", "sample", "--llm-input-text", "topic=hello", "--format", "json"}
outcome = app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: io.Discard})
var owned requestInputs
if err := json.Unmarshal(output.Bytes(), &owned); err != nil {
t.Fatal(err)
}
if outcome.Kind != commandkit.OutcomeSuccess || !owned.Resources.Profile.Set || owned.Resources.Profile.Value != "" || owned.Execution.Execution.Model != "sample" || owned.Inputs.Inputs["topic"] != promptkit.Inline("hello") {
t.Fatalf("application flags: %#v, %#v", outcome, owned)
}
outcome = app.Execute(context.Background(), []string{"request", "inspect", "--model", "sample"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeUsage {
t.Fatalf("unexpected application alias: %#v", outcome)
}
}
func TestComposedPromptKitAndDomainCommandsRemainAvailable(t *testing.T) { func TestComposedPromptKitAndDomainCommandsRemainAvailable(t *testing.T) {
app, err := newApp() app, err := newApp()
if err != nil { if err != nil {
@@ -266,15 +420,19 @@ 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{config: seed.Selection.Config, overlay: seed.Selection.Overlay, campaign: input.Campaign} var cached *configkit.Result[appConfiguration]
cached := cache.applications[key] for _, entry := range cache.applications {
if reflect.DeepEqual(entry.selection, seed.Selection) && entry.campaign == input.Campaign {
cached = entry.result
}
}
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")
} }
if _, err := loadPromptEngine(context.Background(), seed.Selection, input, cache); err != nil { if _, err := loadPromptEngine(context.Background(), seed.Selection, input, cache); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if cache.applications[key] != cached { if len(cache.applications) != 1 || cache.applications[0].result != cached {
t.Fatal("engine loader did not reuse the builder's application configuration") t.Fatal("engine loader did not reuse the builder's application configuration")
} }
@@ -518,6 +676,11 @@ func TestComposedAliasesReachCanonicalCommands(t *testing.T) {
if outcome.Kind != commandkit.OutcomeSuccess || output.Len() != 0 || engineLoads != 0 || clientCalls != 0 { if outcome.Kind != commandkit.OutcomeSuccess || output.Len() != 0 || engineLoads != 0 || clientCalls != 0 {
t.Fatalf("quiet aliased help outcome/output/engine loads/client calls = (%#v, %q, %d, %d)", outcome, output.String(), engineLoads, clientCalls) t.Fatalf("quiet aliased help outcome/output/engine loads/client calls = (%#v, %q, %d, %d)", outcome, output.String(), engineLoads, clientCalls)
} }
output.Reset()
outcome = app.Execute(context.Background(), []string{"version"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || engineLoads != 0 || clientCalls != 0 {
t.Fatalf("version outcome/engine loads/client calls = (%#v, %d, %d)", outcome, engineLoads, clientCalls)
}
for _, args := range [][]string{ for _, args := range [][]string{
{"list", "collections", "--campaign", "spring"}, {"list", "collections", "--campaign", "spring"},