Verify typed alias composition in integrations
This commit is contained in:
@@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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**
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user