e030cd28e9
Builder (cli.For[C]): Root/Dir/Cmd/SlotDir/Slot construct the tree without repeating the [C] type parameter at every node; returns plain *Node[C] so it interoperates with struct-literal construction. App[C]: collapses the per-binary main.go — standard flags (-color/-json/-version, -server when configured), mode-aware color defaults, version banner, client connect, and the one-shot-vs-shell split — into one Main(). Transport-agnostic via a Connect callback, so it never assumes gRPC. Makefile: `make check` = fixstyle vet lint test (the pre-commit gate), plus build and a linux+openbsd cross target. The example now dogfoods both Builder and App. Tests cover the builder tree and App's one-shot dispatch / -version / nil-Connect paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
87 lines
3.1 KiB
Go
87 lines
3.1 KiB
Go
// SPDX-FileCopyrightText: (C) Copyright 2026 Pim van Pelt <pim@ipng.ch>
|
|
// 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("<opts>", "", dynWatchOpts, runNoop)
|
|
watchOpt.Children = []*Node[fakeClient]{watchOpt}
|
|
|
|
return b.Root(
|
|
b.Dir("show", "show state",
|
|
b.Cmd("instance", "list fleet members (add <id> for one)", runNoop,
|
|
b.Slot("<id>", "show one member", dynInstances, runNoop,
|
|
b.Cmd("evpn", "list the member's EVPNs", runNoop,
|
|
b.Slot("<evpn>", "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 <id> 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 <id> 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)
|
|
}
|
|
}
|