// SPDX-FileCopyrightText: (C) Copyright 2026 Pim van Pelt // SPDX-License-Identifier: Apache-2.0 package cli import ( "context" "strings" "testing" ) // buildFixtureB constructs the same tree shape as buildFixture (tree_test.go) // but via the Builder, so the two are interchangeable. dynInstances, dynEvpns, // dynWatchOpts, runNoop and runQuit are defined in tree_test.go (same package). func buildFixtureB() *Node[fakeClient] { b := For[fakeClient]() // Circular watch-options slot: built then wired to itself. watchOpt := b.Slot("", "", dynWatchOpts, runNoop) watchOpt.Children = []*Node[fakeClient]{watchOpt} return b.Root( b.Dir("show", "show state", b.Cmd("instance", "list fleet members (add for one)", runNoop, b.Slot("", "show one member", dynInstances, runNoop, b.Cmd("evpn", "list the member's EVPNs", runNoop, b.Slot("", "show one EVPN", dynEvpns, runNoop))))), b.Dir("watch", "stream events", b.Cmd("events", "watch events", runNoop, watchOpt)), b.Cmd("quit", "exit", runQuit), ) } // TestBuilderMatchesLiteral checks the Builder produces a tree that Walks // identically to the struct-literal fixture: same runnable nodes, captured // args, prefix abbreviation, and circular-slot handling. func TestBuilderMatchesLiteral(t *testing.T) { root := buildFixtureB() cases := []struct { toks []string runnable bool help string args []string remaining int }{ {[]string{"show", "instance"}, true, "list fleet members (add for one)", nil, 0}, {[]string{"show", "instance", "host1"}, true, "show one member", []string{"host1"}, 0}, {[]string{"show", "instance", "host1", "evpn", "blue"}, true, "show one EVPN", []string{"host1", "blue"}, 0}, {[]string{"sh", "inst"}, true, "list fleet members (add for one)", nil, 0}, {[]string{"watch", "events", "num", "5", "crud"}, true, "", []string{"num", "5", "crud"}, 0}, {[]string{"show", "bogus"}, false, "", nil, 1}, } for _, tc := range cases { node, args, remaining := Walk(root, tc.toks) if len(remaining) != tc.remaining { t.Errorf("Walk(%v) remaining=%v, want %d", tc.toks, remaining, tc.remaining) continue } if tc.remaining > 0 { continue } if got := node.Run != nil; got != tc.runnable { t.Errorf("Walk(%v) runnable=%v, want %v", tc.toks, got, tc.runnable) } if tc.runnable && node.Help != tc.help { t.Errorf("Walk(%v) help=%q, want %q", tc.toks, node.Help, tc.help) } if strings.Join(args, ",") != strings.Join(tc.args, ",") { t.Errorf("Walk(%v) args=%v, want %v", tc.toks, args, tc.args) } } } // TestBuilderDynamicCandidates checks a Builder-built slot still resolves // context-dependent candidates through its Dynamic function. func TestBuilderDynamicCandidates(t *testing.T) { root := buildFixtureB() client := fakeClient{ instances: []string{"host1", "host2"}, evpns: map[string][]string{"host1": {"blue", "red"}}, } got := Candidates(context.Background(), client, root, []string{"show", "instance", "host1", "evpn"}, "") if len(got) != 2 || got[0].Word != "blue" || got[1].Word != "red" { t.Errorf("Candidates(... host1 evpn) = %v, want [blue red]", got) } }