Expose canonical paths on command invocations

This commit is contained in:
2026-10-19 19:33:49 +00:00
parent 75fc45f072
commit 451a0c483f
5 changed files with 128 additions and 3 deletions

View File

@@ -232,7 +232,7 @@ func (c typedCommand[I]) prepare(flagValues [][]string, operands []string, enfor
if c.privateRun != nil {
return c.privateRun(current.commandContext(), input)
}
return c.spec.Run(Invocation{Context: current.ctx, Streams: current.streams}, input)
return c.spec.Run(current.invocation(), input)
}, nil
}

View File

@@ -1,6 +1,6 @@
# Application-owned aliases implementation plan
Status: in progress; Stages 1–8 completed.
Status: in progress; Stages 1–9 completed.
## Authority, baseline, and scope
@@ -495,6 +495,13 @@ and canonical forms reach the same binding/callback once; repeated calls and
declaration reuse do not leak invocation values; zero Invocation accessors
are empty. Existing cancellation and domain outcome checks remain green.
**Completed:** Invocation now retains the entered and canonical route paths
alongside the caller's context and streams. Public typed handlers and private
callbacks receive the same resolved identity. Tests cover canonical and alias
calls with operands, repeated calls, declaration reuse across Apps, private
callback plumbing, and zero-value accessors. Verified with the root, ckconfig,
and ckprompt suites under the local workspace.
## Stage 10: Add alias-aware help and discovery views
**Work**

View File

@@ -62,11 +62,20 @@ type commandExecution struct {
func (current *execution) commandContext() commandExecution {
return commandExecution{
invocation: Invocation{Context: current.ctx, Streams: current.streams},
invocation: current.invocation(),
renderHelpTargetsFunc: current.renderHelpTargets,
}
}
func (current *execution) invocation() Invocation {
return Invocation{
Context: current.ctx,
Streams: current.streams,
enteredPath: current.route.enteredPath.String(),
canonicalPath: current.route.canonicalPath.String(),
}
}
func (current commandExecution) renderHelpTargets(targets []string) Outcome {
return current.renderHelpTargetsFunc(targets)
}

View File

@@ -34,4 +34,15 @@ type Invocation struct {
Context context.Context
// Streams are the caller-selected output destinations.
Streams Streams
enteredPath string
canonicalPath string
}
// EnteredPath returns the command path used to invoke the selected command.
// It is empty for an Invocation that was not created by command execution.
func (inv Invocation) EnteredPath() string { return inv.enteredPath }
// CanonicalPath returns the selected command's canonical path.
// It is empty for an Invocation that was not created by command execution.
func (inv Invocation) CanonicalPath() string { return inv.canonicalPath }

View File

@@ -190,6 +190,104 @@ func TestExecuteDispatchesEnteredAliasToCanonicalRunner(t *testing.T) {
}
}
func TestInvocationReportsEnteredAndCanonicalPathsPerCall(t *testing.T) {
type input struct {
label Supplied[string]
values Supplied[[]string]
}
type observed struct {
entered string
canonical string
label string
values []string
}
var calls []observed
command := NewCommand(CommandSpec[input]{
Name: "run",
Flags: []Flag[input]{ValueFlag(FlagMeta{Name: "label"}, (*string)(nil),
func(value string) (string, error) { return value, nil },
func(target *input, label Supplied[string]) { target.label = label },
)},
Arguments: []Argument[input]{RepeatArgument(ArgumentMeta{Name: "value"}, 0, -1,
func(value string) (string, error) { return value, nil },
func(target *input, values Supplied[[]string]) { target.values = values },
)},
Run: func(inv Invocation, value input) Outcome {
calls = append(calls, observed{
entered: inv.EnteredPath(),
canonical: inv.CanonicalPath(),
label: value.label.Value,
values: append([]string(nil), value.values.Value...),
})
return Success()
},
})
shortcut := Alias("shortcut", "workspace run")
app, err := New(Identity{Name: "Example"}, Group("workspace", "Workspace", command), shortcut)
if err != nil {
t.Fatal(err)
}
for _, args := range [][]string{{"workspace", "run", "--label=first", "first"}, {"shortcut", "--label=second", "second"}, {"workspace", "run"}} {
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.Kind)
}
}
if len(calls) != 3 {
t.Fatalf("handler calls = %d, want 3", len(calls))
}
want := []observed{
{entered: "workspace run", canonical: "workspace run", label: "first", values: []string{"first"}},
{entered: "shortcut", canonical: "workspace run", label: "second", values: []string{"second"}},
{entered: "workspace run", canonical: "workspace run"},
}
if !reflect.DeepEqual(calls, want) {
t.Fatalf("handler invocations = %#v, want %#v", calls, want)
}
secondApp, err := New(Identity{Name: "Another"}, Group("workspace", "Workspace", command), shortcut)
if err != nil {
t.Fatal(err)
}
if outcome := secondApp.Execute(context.Background(), []string{"shortcut"}, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeSuccess {
t.Fatalf("reused declaration execution = %v, want success", outcome.Kind)
}
if got := calls[3]; got.entered != "shortcut" || got.canonical != "workspace run" || len(got.values) != 0 {
t.Fatalf("reused declaration invocation = %#v", got)
}
var zero Invocation
if zero.EnteredPath() != "" || zero.CanonicalPath() != "" {
t.Fatalf("zero Invocation paths = (%q, %q), want empty paths", zero.EnteredPath(), zero.CanonicalPath())
}
}
func TestPrivateCommandCallbacksReceiveResolvedInvocationPaths(t *testing.T) {
var calls []Invocation
command := Command{record: &commandRecord{
name: "inspect",
payload: typedCommand[struct{}]{
spec: CommandSpec[struct{}]{Name: "inspect"},
privateRun: func(exec commandExecution, _ struct{}) Outcome {
calls = append(calls, exec.invocation)
return Success()
},
},
}}
app, err := New(Identity{Name: "Example"}, command, Alias("shortcut", "inspect"))
if err != nil {
t.Fatal(err)
}
for _, args := range [][]string{{"inspect"}, {"shortcut"}} {
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.Kind)
}
}
if len(calls) != 2 || calls[0].EnteredPath() != "inspect" || calls[0].CanonicalPath() != "inspect" || calls[1].EnteredPath() != "shortcut" || calls[1].CanonicalPath() != "inspect" {
t.Fatalf("private callback invocations = %#v", calls)
}
}
func TestExecuteAppliesCanonicalCommandFlagAliasesAfterRouting(t *testing.T) {
type input struct{ color string }
var got string