feat: Validate + keypress subpackage; RFC-style design.md (v1.2.0)
Validate(root): optional startup/test check for tree authoring faults — >1 slot child per node, empty word, duplicate sibling words, dead-end node — traversing circular slots without looping (#3). keypress subpackage: WaitForKey(ctx, cancel) cancels a context on any keystroke for watch-style streaming commands, with per-GOOS cbreak (linux TCGETS/TCSETS, BSD TIOCGETA/TIOCSETA) and a non-tty/unsupported fallback that just waits on ctx. Lifts the last OpenBSD-specific bit out of evpnc/maglevc's watch.go (#6). docs: replace PROPOSAL.md with an RFC-2119 design.md (FR/NFR for the library). Example now dogfoods Validate (a unit test) and keypress (a bounded `watch` command). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
// SPDX-FileCopyrightText: (C) Copyright 2026 Pim van Pelt <pim@ipng.ch>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestValidateClean checks that a well-formed tree (the builder fixture, which
|
||||
// includes a circular slot) validates without error.
|
||||
func TestValidateClean(t *testing.T) {
|
||||
if err := Validate(buildFixtureB()); err != nil {
|
||||
t.Errorf("Validate(clean tree) = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateFaults checks each fault class is reported.
|
||||
func TestValidateFaults(t *testing.T) {
|
||||
dyn := func(context.Context, fakeClient, []string) []string { return nil }
|
||||
|
||||
root := &Node[fakeClient]{Children: []*Node[fakeClient]{
|
||||
// two slot children under one node
|
||||
{Word: "twoslots", Help: "", Children: []*Node[fakeClient]{
|
||||
{Word: "<a>", Dynamic: dyn, Run: runNoop},
|
||||
{Word: "<b>", Dynamic: dyn, Run: runNoop},
|
||||
}},
|
||||
// duplicate fixed words
|
||||
{Word: "dup", Children: []*Node[fakeClient]{
|
||||
{Word: "x", Run: runNoop},
|
||||
{Word: "x", Run: runNoop},
|
||||
}},
|
||||
// dead end: neither runs nor has children
|
||||
{Word: "deadend"},
|
||||
// empty word
|
||||
{Word: "", Run: runNoop},
|
||||
}}
|
||||
|
||||
err := Validate(root)
|
||||
if err == nil {
|
||||
t.Fatal("Validate(broken tree) = nil, want errors")
|
||||
}
|
||||
msg := err.Error()
|
||||
for _, want := range []string{
|
||||
"only the first is reachable",
|
||||
`duplicate child word "x"`,
|
||||
"dead end",
|
||||
"empty Word",
|
||||
} {
|
||||
if !strings.Contains(msg, want) {
|
||||
t.Errorf("Validate missing %q in:\n%s", want, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user