Verify typed alias composition in integrations

This commit is contained in:
2026-10-19 19:53:33 +00:00
parent f34ab12ca5
commit c3bb3941d3
4 changed files with 221 additions and 1 deletions

View File

@@ -1,7 +1,9 @@
package commandkit package commandkit
import ( import (
"context"
"errors" "errors"
"io"
"reflect" "reflect"
"strconv" "strconv"
"testing" "testing"
@@ -245,3 +247,90 @@ func TestIncompletePreparationSkipsMissingRequirementsAndInputFactory(t *testing
t.Fatalf("preparation invoked NewInput %d times", factoryCalls) t.Fatalf("preparation invoked NewInput %d times", factoryCalls)
} }
} }
func TestExecuteBindsMixedFlagSpellingsWithFreshPresence(t *testing.T) {
type input struct {
count Supplied[int]
enabled Supplied[bool]
message Supplied[string]
tags Supplied[[]string]
required Supplied[string]
instance int
}
type observed struct {
count Supplied[int]
enabled Supplied[bool]
message Supplied[string]
tags Supplied[[]string]
required Supplied[string]
instance int
}
defaultCount := 9
defaultMessage := "fallback"
defaultTags := []string{"base"}
countParses, inputFactories := 0, 0
var calls []observed
command := NewCommand(CommandSpec[input]{
Name: "run",
NewInput: func() input {
inputFactories++
return input{instance: inputFactories}
},
Flags: []Flag[input]{
ValueFlag(FlagMeta{Name: "count"}, &defaultCount, func(value string) (int, error) {
countParses++
return strconv.Atoi(value)
}, func(target *input, value Supplied[int]) { target.count = value }),
BoolFlag(FlagMeta{Name: "enabled"}, true, func(target *input, value Supplied[bool]) { target.enabled = value }),
ValueFlag(FlagMeta{Name: "message"}, &defaultMessage, func(value string) (string, error) { return value, nil }, func(target *input, value Supplied[string]) { target.message = value }),
RepeatFlag(FlagMeta{Name: "tag"}, defaultTags, func(value string) (string, error) { return value, nil }, func(target *input, value Supplied[[]string]) { target.tags = value }),
ValueFlag(FlagMeta{Name: "required", Required: true}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(target *input, value Supplied[string]) { target.required = value }),
},
Run: func(_ Invocation, value input) Outcome {
value.tags.Value = append([]string(nil), value.tags.Value...)
calls = append(calls, observed(value))
return Success()
},
})
app, err := New(Identity{Name: "Example"}, command,
Alias("shortcut", "run"),
FlagAliasFor("run", "--number", "--count"),
FlagAliasFor("run", "--active", "--enabled"),
FlagAliasFor("run", "--text", "--message"),
FlagAliasFor("run", "--label", "--tag"),
)
if err != nil {
t.Fatal(err)
}
for _, args := range [][]string{
{"shortcut", "--number=0", "--count=5", "--active=false", "--enabled=true", "--text=", "--message=last", "--label=a", "--tag=b", "--required=first"},
{"run", "--number=0", "--active=false", "--text=", "--label=c", "--required=second"},
} {
if outcome := app.Execute(context.Background(), args, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeSuccess {
t.Fatalf("Execute(%q) = %#v, want success", args, outcome)
}
}
if countParses != 3 {
t.Fatalf("count conversions = %d, want every one of three occurrences", countParses)
}
if len(calls) != 2 || calls[0].count != (Supplied[int]{Value: 5, Set: true}) || calls[0].enabled != (Supplied[bool]{Value: true, Set: true}) || calls[0].message != (Supplied[string]{Value: "last", Set: true}) || !reflect.DeepEqual(calls[0].tags, Supplied[[]string]{Value: []string{"a", "b"}, Set: true}) || calls[0].required != (Supplied[string]{Value: "first", Set: true}) {
t.Fatalf("first mixed-spelling invocation = %#v", calls)
}
if calls[1].count != (Supplied[int]{Value: 0, Set: true}) || calls[1].enabled != (Supplied[bool]{Value: false, Set: true}) || calls[1].message != (Supplied[string]{Value: "", Set: true}) || !reflect.DeepEqual(calls[1].tags, Supplied[[]string]{Value: []string{"c"}, Set: true}) || calls[1].required != (Supplied[string]{Value: "second", Set: true}) || calls[0].instance == calls[1].instance {
t.Fatalf("second mixed-spelling invocation = %#v", calls[1])
}
if outcome := app.Execute(context.Background(), []string{"shortcut", "--count=7"}, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeUsage {
t.Fatalf("missing required flag outcome = %#v, want usage", outcome)
}
if inputFactories != 2 || len(calls) != 2 {
t.Fatalf("missing required flag created or ran input: factories=%d calls=%d", inputFactories, len(calls))
}
if outcome := app.Execute(context.Background(), []string{"shortcut", "--required=defaults"}, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeSuccess {
t.Fatalf("defaulted invocation = %#v, want success", outcome)
}
if len(calls) != 3 || calls[2].count != (Supplied[int]{Value: 9}) || calls[2].enabled != (Supplied[bool]{Value: true}) || calls[2].message != (Supplied[string]{Value: "fallback"}) || !reflect.DeepEqual(calls[2].tags, Supplied[[]string]{Value: []string{"base"}}) || calls[2].required != (Supplied[string]{Value: "defaults", Set: true}) || calls[2].instance == calls[1].instance {
t.Fatalf("omitted values reused prior invocation state: %#v", calls[2])
}
}

View File

@@ -1,6 +1,6 @@
# Application-owned aliases implementation plan # Application-owned aliases implementation plan
Status: in progress; Stages 1–12 completed. Status: in progress; Stages 1–13 completed.
## Authority, baseline, and scope ## Authority, baseline, and scope
@@ -621,6 +621,17 @@ suite passes with `go test ./... -count=1`.
local workspace. Do not claim nested modules' pinned core v0.2.0 graph validates local workspace. Do not claim nested modules' pinned core v0.2.0 graph validates
new alias APIs. See Stage 17 for independent local-source graph verification. new alias APIs. See Stage 17 for independent local-source graph verification.
**Completed:** Root binding coverage now verifies every singleton occurrence
is converted, the last value wins, repeated values retain order, explicit
false/zero/empty values remain present, required selectors are checked, and
each invocation receives fresh defaults and input state across canonical and
alternate spellings. Integration tests exercise scoped selector aliases on
installed PromptKit collection and prompt-inspection commands, the `smoke`
shortcut and returned report, failed operand-alias registration with later
canonical and shortcut calls, and global help aliases for both standard
families. Verified with the root, ckconfig, and ckprompt suites under the local
three-module workspace.
## Stage 14: Update maintained consumer examples ## Stage 14: Update maintained consumer examples
**Work** **Work**

View File

@@ -225,6 +225,51 @@ func TestRequiredSelectorAppliesOnlyToItsSelectedScope(t *testing.T) {
} }
} }
func TestConfigAliasesForwardSelectedTenantAndGlobalHelp(t *testing.T) {
type input struct{ Tenant commandkit.Supplied[string] }
flag := commandkit.ValueFlag(commandkit.FlagMeta{Name: "tenant"}, (*string)(nil),
func(value string) (string, error) { return value, nil },
func(target *input, value commandkit.Supplied[string]) { target.Tenant = value })
var acquired []commandkit.Supplied[string]
command := Command(Suite[input, struct{}]{
Selectors: []Selector[input]{{Binding: flag, Scopes: []string{"promptkit"}}},
Scopes: []Scope[input, struct{}]{
{
Name: "promptkit",
Operations: []Operation{Validate},
Acquire: func(_ context.Context, _ Request, value input, _ struct{}) (commandkit.Acquired[View], error) {
acquired = append(acquired, value.Tenant)
return commandkit.Acquired[View]{}, nil
},
},
},
})
app, err := commandkit.New(commandkit.Identity{Name: "example"},
command,
commandkit.Alias("check-config", "config validate"),
commandkit.FlagAliasFor("config validate", "--tenant-id", "--tenant"),
commandkit.FlagAlias("--hlep", "--help"),
)
if err != nil {
t.Fatal(err)
}
var output bytes.Buffer
outcome := app.Execute(context.Background(), []string{"check-config", "--hlep"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "validate") || len(acquired) != 0 {
t.Fatalf("global help alias outcome/output/acquisitions = (%#v, %q, %#v)", outcome, output.String(), acquired)
}
outcome = app.Execute(context.Background(), []string{"check-config", "--scope", "promptkit", "--tenant-id=acme"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || len(acquired) != 1 || acquired[0] != (commandkit.Supplied[string]{Value: "acme", Set: true}) {
t.Fatalf("aliased selector outcome/acquisitions = (%#v, %#v)", outcome, acquired)
}
outcome = app.Execute(context.Background(), []string{"config", "validate", "--scope", "promptkit", "--tenant=canonical"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || len(acquired) != 2 || acquired[1] != (commandkit.Supplied[string]{Value: "canonical", Set: true}) {
t.Fatalf("canonical selector outcome/acquisitions = (%#v, %#v)", outcome, acquired)
}
}
func TestShowAcquiresAllViewsBeforeRenderingAndFramesNativeYAML(t *testing.T) { func TestShowAcquiresAllViewsBeforeRenderingAndFramesNativeYAML(t *testing.T) {
var output bytes.Buffer var output bytes.Buffer
var acquisitionOutputLengths []int var acquisitionOutputLengths []int

View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"io" "io"
"strings"
"testing" "testing"
"testing/fstest" "testing/fstest"
@@ -66,6 +67,80 @@ func TestPublicDefinitionCommandsCaptureSelectorHandles(t *testing.T) {
} }
} }
func TestPromptKitAliasesComposeWithInstalledOperations(t *testing.T) {
type input struct{ Campaign commandkit.Supplied[string] }
selector := commandkit.ValueFlag(commandkit.FlagMeta{Name: "campaign"}, (*string)(nil),
func(value string) (string, error) { return value, nil },
func(target *input, value commandkit.Supplied[string]) { target.Campaign = value })
var collectionInput input
var inspectionInput input
var inspectedID string
var smokeInput input
var smokeKind promptkit.SmokeTestKind
callbacks := 0
suite := ckprompt.Suite[input, struct{}]{
Selectors: []commandkit.Flag[input]{selector},
ListCollections: func(_ context.Context, _ ckconfig.Selection, value input, _ struct{}) (commandkit.Acquired[[]promptkit.CollectionInfo], error) {
callbacks++
collectionInput = value
return commandkit.Acquired[[]promptkit.CollectionInfo]{Value: []promptkit.CollectionInfo{{Namespace: "spring"}}}, nil
},
InspectPrompt: func(_ context.Context, request ckprompt.PromptRequest, value input, _ struct{}) (commandkit.Acquired[*promptkit.PromptInspection], error) {
callbacks++
inspectionInput, inspectedID = value, request.ID
return commandkit.Acquired[*promptkit.PromptInspection]{Value: &promptkit.PromptInspection{PromptID: request.ID}}, nil
},
SmokeTest: func(_ context.Context, request ckprompt.SmokeRequest, value input, _ struct{}) (commandkit.Acquired[*promptkit.SmokeTestReport], error) {
callbacks++
smokeInput, smokeKind = value, request.Request.Kind
return commandkit.Acquired[*promptkit.SmokeTestReport]{Value: &promptkit.SmokeTestReport{Kind: request.Request.Kind, Supported: true, Completed: true}}, nil
},
}
app, err := commandkit.New(commandkit.Identity{Name: "example"},
ckprompt.Command(suite),
commandkit.Alias("collect", "promptkit list collections"),
commandkit.Alias("inspect", "promptkit show prompt"),
commandkit.Alias("smoke", "promptkit test"),
commandkit.FlagAliasFor("promptkit list collections", "--camp", "--campaign"),
commandkit.FlagAliasFor("promptkit show prompt", "--camp", "--campaign"),
commandkit.FlagAlias("--hlep", "--help"),
)
if err != nil {
t.Fatal(err)
}
var output bytes.Buffer
outcome := app.Execute(context.Background(), []string{"collect", "--camp=spring"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || !collectionInput.Campaign.Set || collectionInput.Campaign.Value != "spring" || !strings.Contains(output.String(), "spring") {
t.Fatalf("collection shortcut outcome/input/report = (%#v, %#v, %q)", outcome, collectionInput, output.String())
}
output.Reset()
outcome = app.Execute(context.Background(), []string{"inspect", "briefing", "--camp", "spring"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || !inspectionInput.Campaign.Set || inspectionInput.Campaign.Value != "spring" || inspectedID != "briefing" {
t.Fatalf("inspection shortcut outcome/input/id = (%#v, %#v, %q)", outcome, inspectionInput, inspectedID)
}
output.Reset()
outcome = app.Execute(context.Background(), []string{"smoke", "--hlep"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "--help") || callbacks != 2 {
t.Fatalf("global help alias outcome/report/callbacks = (%#v, %q, %d)", outcome, output.String(), callbacks)
}
if err := app.Register(commandkit.Alias("promptkit test text-inference", "promptkit test authentication")); err == nil {
t.Fatal("accepted a command alias that maps one smoke operand to another")
}
output.Reset()
outcome = app.Execute(context.Background(), []string{"promptkit", "test", "authentication", "--campaign=spring"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || smokeKind != promptkit.SmokeTestAuthentication || !smokeInput.Campaign.Set || smokeInput.Campaign.Value != "spring" || !strings.Contains(output.String(), "kind: authentication") || !strings.Contains(output.String(), "completed: true") {
t.Fatalf("canonical smoke outcome/request/report = (%#v, %#v, %q)", outcome, smokeKind, output.String())
}
output.Reset()
outcome = app.Execute(context.Background(), []string{"smoke", "text-inference", "--campaign=spring"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || smokeKind != promptkit.SmokeTestKind("text-inference") || callbacks != 4 || !strings.Contains(output.String(), "kind: text-inference") {
t.Fatalf("smoke shortcut outcome/kind/report/callbacks = (%#v, %#v, %q, %d)", outcome, smokeKind, output.String(), callbacks)
}
}
func TestPublicConfigurationAndDefinitionFamiliesRemainIndependent(t *testing.T) { func TestPublicConfigurationAndDefinitionFamiliesRemainIndependent(t *testing.T) {
inputs, err := promptkit.FilesystemInputs("composition fixture", fstest.MapFS{ inputs, err := promptkit.FilesystemInputs("composition fixture", fstest.MapFS{
"config/prompts.yml": &fstest.MapFile{Data: []byte(`prompts: "config/prompts.yml": &fstest.MapFile{Data: []byte(`prompts: