feat(01-03): add deterministic rich CLI output

- Inject bonfire.Output from stdin/stdout/stderr with stdlib widgets and x/term
- Degrade spinner, progress, table and prompts without a TTY or color
- Reject plugin command names that are not namespace:verb
This commit is contained in:
Jakub Zych
2026-09-16 13:54:44 +02:00
parent 9b1a25dc94
commit a6004e490f
16 changed files with 884 additions and 78 deletions

View File

@@ -18,17 +18,13 @@ import (
func TestGreeterHelloPrintsLayeredConfig(t *testing.T) {
got := runHello(t, nil)
want := "name=hello-app posts_per_page=10 debug=false extra=hello-from-optional events=ok collected=greeter handled=true"
if got != want {
t.Fatalf("output = %q, want %q", got, want)
}
assertGreeting(t, got, want)
}
func TestGreeterHelloSeesDevelopmentOverlay(t *testing.T) {
got := runHello(t, map[string]string{"SUMMER_ENV": "development"})
want := "name=hello-dev posts_per_page=10 debug=true extra=hello-from-optional events=ok collected=greeter handled=true"
if got != want {
t.Fatalf("output = %q, want %q", got, want)
}
assertGreeting(t, got, want)
}
func TestGreeterHelloSeesEnvVarOverrideOfPluginDefault(t *testing.T) {
@@ -36,9 +32,7 @@ func TestGreeterHelloSeesEnvVarOverrideOfPluginDefault(t *testing.T) {
"SUMMER_GOLEM15__HELLO__POSTS_PER_PAGE": "25",
})
want := "name=hello-app posts_per_page=25 debug=false extra=hello-from-optional events=ok collected=greeter handled=true"
if got != want {
t.Fatalf("output = %q, want %q", got, want)
}
assertGreeting(t, got, want)
}
func runHello(t *testing.T, env map[string]string) string {
@@ -56,9 +50,7 @@ func runHello(t *testing.T, env map[string]string) string {
func TestGreeterRunsWithoutOptionalPlugin(t *testing.T) {
got := runHelloIDs(t, []string{"golem15.hello", "golem15.greeter"}, nil)
want := "name=hello-app posts_per_page=10 debug=false extra= events=ok collected=greeter handled=true"
if got != want {
t.Fatalf("output = %q, want %q", got, want)
}
assertGreeting(t, got, want)
}
func runHelloIDs(t *testing.T, ids []string, env map[string]string) string {
@@ -82,7 +74,10 @@ func runHelloIDs(t *testing.T, ids []string, env map[string]string) string {
}
}
var buf bytes.Buffer
root := bonfire.NewRoot("hello", commands, &buf)
root, err := bonfire.NewRoot("hello", commands, &buf)
if err != nil {
t.Fatalf("NewRoot: %v", err)
}
root.SetArgs([]string{"greeter:hello"})
if err := root.Execute(); err != nil {
t.Fatalf("execute: %v", err)
@@ -104,9 +99,27 @@ func TestBuiltBinaryGreeterHello(t *testing.T) {
if err != nil {
t.Fatalf("bin/hello greeter:hello: %v\n%s", err, out)
}
if got := strings.TrimSpace(string(out)); got != "name=hello-app posts_per_page=10 debug=false extra=hello-from-optional events=ok collected=greeter handled=true" {
t.Fatalf("output = %q, want layered production config with optional extra and events", got)
got := strings.TrimSpace(string(out))
want := "name=hello-app posts_per_page=10 debug=false extra=hello-from-optional events=ok collected=greeter handled=true"
assertGreeting(t, got, want)
}
func assertGreeting(t *testing.T, got, want string) {
t.Helper()
if !strings.Contains(got, "plugin\tstatus") || !strings.Contains(got, "golem15.greeter\tactive") {
t.Fatalf("missing non-tty table:\n%s", got)
}
if lastLine(got) != want {
t.Fatalf("greeting = %q, want %q\nfull:\n%s", lastLine(got), want, got)
}
}
func lastLine(s string) string {
s = strings.TrimSpace(s)
if i := strings.LastIndex(s, "\n"); i >= 0 {
return s[i+1:]
}
return s
}
func runSummerBuild(t *testing.T) string {