homelab: scaffold unified CLI (registry, manifest, claim/release) in infra/cli
Begin evolving the existing infra/cli into the agent-facing "homelab" CLI decided in the design/grilling session: one composable, JSON-capable surface for the operations agents run over and over (mined from 51k commands across 2,225 past sessions; the infra inner-loop is ~29% of them). v0.1 targets that loop — work/tf/claim — and ships here, in place, in infra/cli. This first slice: - command registry + dispatcher (longest-prefix verb matching) and a `manifest`/`manifest --json` progressive-discovery entrypoint; every verb declares a read|write tier so write-gating can be added later (everything is allowed for now). - claim/release verbs wrapping the existing presence script (not reimplemented), with label-taxonomy validation. - main() front-dispatches the homelab verb surface but falls through to the legacy webhook -use-case path verbatim, so the in-cluster infra-cli image is unaffected. - fix a pre-existing vet error (glog.Infof missing format directive) that blocked `go test`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
70e217db24
commit
ed6f22fd53
9 changed files with 426 additions and 3 deletions
73
cli/command_test.go
Normal file
73
cli/command_test.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tracer bullet: the dispatcher must route `homelab <path...> <args...>` to the
|
||||
// command whose Path is the longest matching prefix of the input tokens, and
|
||||
// hand the command the remaining args.
|
||||
func TestDispatchRoutesToLongestPrefixMatch(t *testing.T) {
|
||||
var gotArgs []string
|
||||
ran := ""
|
||||
reg := []Command{
|
||||
{Path: []string{"claim"}, Tier: TierWrite, Summary: "claim a resource",
|
||||
Run: func(a []string) error { ran = "claim"; gotArgs = a; return nil }},
|
||||
{Path: []string{"tf", "plan"}, Tier: TierRead, Summary: "plan a stack",
|
||||
Run: func(a []string) error { ran = "tf plan"; gotArgs = a; return nil }},
|
||||
}
|
||||
|
||||
if err := dispatch(reg, []string{"tf", "plan", "vault", "--json"}); err != nil {
|
||||
t.Fatalf("dispatch returned error: %v", err)
|
||||
}
|
||||
if ran != "tf plan" {
|
||||
t.Fatalf("routed to %q, want %q", ran, "tf plan")
|
||||
}
|
||||
if want := []string{"vault", "--json"}; !reflect.DeepEqual(gotArgs, want) {
|
||||
t.Fatalf("command got args %v, want %v", gotArgs, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchUnknownCommandErrors(t *testing.T) {
|
||||
reg := []Command{{Path: []string{"claim"}, Run: func(a []string) error { return nil }}}
|
||||
if err := dispatch(reg, []string{"bogus"}); err == nil {
|
||||
t.Fatal("expected error for unknown command, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// The manifest is the progressive-discovery entrypoint: one line per command
|
||||
// showing the full verb path, its tier, and summary, sorted for stable output.
|
||||
func TestManifestTextListsEveryCommandWithTier(t *testing.T) {
|
||||
reg := []Command{
|
||||
{Path: []string{"tf", "plan"}, Tier: TierRead, Summary: "plan a stack"},
|
||||
{Path: []string{"claim"}, Tier: TierWrite, Summary: "claim a resource"},
|
||||
}
|
||||
out := manifestText(reg)
|
||||
for _, want := range []string{"claim", "tf plan", "read", "write", "plan a stack", "claim a resource"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("manifest text missing %q\n---\n%s", want, out)
|
||||
}
|
||||
}
|
||||
// sorted: claim (c) must appear before tf plan (t)
|
||||
if strings.Index(out, "claim") > strings.Index(out, "tf plan") {
|
||||
t.Errorf("manifest not sorted by path:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestJSONIsParsableAndTagged(t *testing.T) {
|
||||
reg := []Command{{Path: []string{"tf", "apply"}, Tier: TierWrite, Summary: "apply a stack"}}
|
||||
out, err := manifestJSON(reg)
|
||||
if err != nil {
|
||||
t.Fatalf("manifestJSON error: %v", err)
|
||||
}
|
||||
var got []map[string]string
|
||||
if err := json.Unmarshal([]byte(out), &got); err != nil {
|
||||
t.Fatalf("manifest JSON not parsable: %v\n%s", err, out)
|
||||
}
|
||||
if len(got) != 1 || got[0]["command"] != "tf apply" || got[0]["tier"] != "write" {
|
||||
t.Fatalf("unexpected manifest JSON: %v", got)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue