diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 94cfa27..123af19 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -98,7 +98,7 @@ invocation metadata were added. ## R-2: Retain discovery-prefix context in usage diagnostics -Status: open. Priority: P3. Category: presentation consistency. +Status: complete. Priority: P3. Category: presentation consistency. **Evidence:** Discovery parsing uses the backend root with only trailing flag tokens, so parser failures have an empty canonical path. @@ -130,6 +130,14 @@ identify `demo list` in usage. Repeat through a composed group shortcut after R-1. Check identifying fragments rather than exact formatting, and confirm that ordinary root and canonical command diagnostics retain their existing behavior. +**Completed:** Usage presentation now uses the selected discovery prefix rather +than asking the canonical backend for its usage. Regressions first failed for +all three error forms at both direct and chained prefixes, and now retain the +entered prefix and flag spelling without inventing a canonical identity. +Usage status 2, completed diagnostic delivery, metadata-only failures, and +ordinary root/canonical usage remain covered. The complete offline root suite +passed on Go 1.26.8. + ## R-3: Preserve empty and dash operands before child-name tokens Status: open. Priority: P2. Category: inherited parser/routing inconsistency. diff --git a/execute.go b/execute.go index 75c0f2e..0ca7120 100644 --- a/execute.go +++ b/execute.go @@ -194,7 +194,13 @@ func (current *execution) usageLine(path []string) string { func (current *execution) present(outcome Outcome, path []string) Outcome { usageLine := func(path []string) string { return current.usageLine(path) } - if current.route.kind == routeCanonical && !current.route.enteredPath.Equal(current.route.canonicalPath) { + switch { + case current.route.kind == routeDiscovery: + path = clonePath(current.route.enteredPath) + usageLine = func([]string) string { + return fmt.Sprintf("Usage: %s %s [command]", current.app.identity.Name, current.route.enteredPath.String()) + } + case current.route.kind == routeCanonical && !current.route.enteredPath.Equal(current.route.canonicalPath): path = clonePath(current.route.enteredPath) usageLine = func([]string) string { return current.routeUsageLine() } } diff --git a/route_test.go b/route_test.go index 3f17465..e235025 100644 --- a/route_test.go +++ b/route_test.go @@ -11,6 +11,57 @@ import ( "testing" ) +func TestDiscoveryUsageRetainsEnteredPrefix(t *testing.T) { + initialized := false + inspect := NewCommand(CommandSpec[struct{}]{ + Name: "inspect", NewInput: func() struct{} { initialized = true; return struct{}{} }, + Run: func(Invocation, struct{}) Outcome { t.Fatal("usage failure executed a handler"); return Success() }, + }) + app, err := New(Identity{Name: "Demo"}, inspect, Group("box", "Box"), + Alias("list things", "inspect"), Alias("p", "q"), Alias("q", "box"), Alias("q admin pick", "inspect"), + FlagAlias("--hlep", "--help")) + if err != nil { + t.Fatal(err) + } + for _, prefix := range []string{"list", "p admin"} { + for _, test := range []struct { + name string + tail []string + want string + }{ + {name: "bare", want: "incomplete command path"}, + {name: "unknown flag", tail: []string{"--missing"}, want: "--missing"}, + {name: "invalid help", tail: []string{"--hlep=bad"}, want: "for --hlep"}, + } { + t.Run(prefix+"/"+test.name, func(t *testing.T) { + var output, diagnostics bytes.Buffer + outcome := app.Execute(context.Background(), append(strings.Fields(prefix), test.tail...), Streams{Out: &output, Err: &diagnostics}) + if outcome.Kind != OutcomeUsage || outcome.Status != 2 || outcome.DiagnosticDelivery != DeliveryComplete || output.Len() != 0 { + t.Fatalf("outcome = %#v, output %q", outcome, output.String()) + } + for _, fragment := range []string{test.want, "Usage: Demo " + prefix} { + if !strings.Contains(diagnostics.String(), fragment) { + t.Errorf("diagnostics %q omit %q", diagnostics.String(), fragment) + } + } + if strings.Contains(diagnostics.String(), "Alias for:") { + t.Errorf("discovery was assigned a canonical identity: %q", diagnostics.String()) + } + }) + } + } + for _, prefix := range []string{"", "inspect"} { + var diagnostics bytes.Buffer + outcome := app.Execute(context.Background(), append(strings.Fields(prefix), "--missing"), Streams{Err: &diagnostics}) + if outcome.Kind != OutcomeUsage || !strings.Contains(diagnostics.String(), "Usage: Demo "+prefix) || strings.Contains(diagnostics.String(), "Alias for:") { + t.Errorf("ordinary usage for %q = %#v, diagnostics %q", prefix, outcome, diagnostics.String()) + } + } + if initialized { + t.Fatal("usage initialized command input") + } +} + func TestParserFailuresRetainAliasUsageAndFlagSpellings(t *testing.T) { built := 0 command := NewCommand(CommandSpec[struct{}]{