diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 238f4bb..4c19dbd 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -187,7 +187,7 @@ passed on Go 1.26.8. ## R-5: Include alias routes in help for groups without canonical children -Status: open. Priority: P2. Category: help discovery gap. +Status: complete. Priority: P2. Category: help discovery gap. **Evidence:** [routeHelpAliases](../../help_view.go#L48) returns immediately when the selected canonical node has no canonical children. Alias records are @@ -206,6 +206,13 @@ commands and routing records; do not install fake children to fill the view. its explicit alias routes and their resolved destinations, while groups with no such routes retain their ordinary help behavior. +**Completed:** Group alias discovery no longer depends on canonical child +count. A new regression failed for canonical and shortcut help before the +correction, and now both help forms list the separate alias route. An unrelated +empty group's help remains unchanged and the complete route still executes. +No synthetic command children were introduced. The complete offline root suite +passed on Go 1.26.8. + ## R-6: Keep the flag interpretation pass consistent with value consumption Status: open. Priority: P2. Category: canonical/alias parsing divergence. diff --git a/help_view.go b/help_view.go index d934ebf..cccc9af 100644 --- a/help_view.go +++ b/help_view.go @@ -64,7 +64,7 @@ func routeHelpAliases(snapshot *routingSnapshot, route routeResult) []commandmet return append([]commandmeta.CommandAlias(nil), snapshot.commandAliases...) } node := traceCanonicalPath(snapshot.canonical, route.canonicalPath).exact - if node == nil || len(node.children) == 0 { + if node == nil { return nil } diff --git a/route_test.go b/route_test.go index 6cdaebe..f6e3378 100644 --- a/route_test.go +++ b/route_test.go @@ -214,6 +214,29 @@ func TestDiscoveryHelpThroughExpandedGroupPath(t *testing.T) { } } +func TestEmptyCanonicalGroupHelpListsAliasRoutes(t *testing.T) { + app, err := New(Identity{Name: "Demo"}, Group("box", "Box"), Group("quiet", "Quiet"), + simpleRunnable("inspect"), Alias("box pick", "inspect"), Alias("pk", "box")) + if err != nil { + t.Fatal(err) + } + for _, args := range [][]string{{"box", "--help"}, {"help", "box"}, {"pk", "--help"}, {"help", "pk"}} { + var output bytes.Buffer + outcome := app.Execute(context.Background(), args, Streams{Out: &output, Err: io.Discard}) + if outcome.Kind != OutcomeSuccess || !strings.Contains(output.String(), "pick → inspect") { + t.Errorf("Execute(%q) = %v, help %q", args, outcome.Kind, output.String()) + } + } + var output bytes.Buffer + outcome := app.Execute(context.Background(), []string{"quiet", "--help"}, Streams{Out: &output, Err: io.Discard}) + if outcome.Kind != OutcomeSuccess || strings.Contains(output.String(), "Command Aliases:") { + t.Fatalf("unaliased group help = %v, %q", outcome.Kind, output.String()) + } + if outcome := app.Execute(context.Background(), []string{"box", "pick"}, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeSuccess { + t.Fatal("alias route became unavailable") + } +} + func routeTestApp(t *testing.T, extra ...Declaration) *App { t.Helper() collections := simpleRunnable("collections")