cli: maturity polish — color, error translation, tabwriter consistency
Adds three small but high-leverage presentation tweaks for v0.1: 1. internal/cli/style is a new ~70 LOC package with Pass/Fail/Warn/ Dim/Bold helpers. Each is TTY-gated and obeys NO_COLOR. No external dep. Wired into the doctor PASS/FAIL/WARN status, the "banger:" error prefix on stderr, and the dim 'ready in <elapsed>' line. 2. internal/cli/errors translates rpc.ErrorResponse into user-facing text. operation_failed becomes invisible (the message wins); not_found, already_exists, bad_request, bad_version, unauthorized, unknown_method get short labels; unknown codes pass through. The daemon-attached op_id lands in dim parens — paste into journalctl --grep to find the daemon log line that produced the failure. 3. Tabwriter config converges on (0, 8, 2, ' ', 0) across every list/table command. The vm prune confirmation table picked up the right config; system install + system status switched from bare "key: value\n" lines to tabular form. printVMSpecLine drops its Unicode middle dot for an ASCII '|' so terminals without UTF-8 render cleanly. Tests cover translateRPCError for every code, style helpers no-op on non-TTY and under NO_COLOR. Smoke status greps switch from "key: value" to "key value" to match the new format. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e47b8146dc
commit
71a332a6a1
11 changed files with 358 additions and 28 deletions
60
internal/cli/errors_test.go
Normal file
60
internal/cli/errors_test.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"banger/internal/rpc"
|
||||
)
|
||||
|
||||
// TestTranslateRPCError pins the user-facing error rendering for
|
||||
// every code the daemon emits today plus the catch-all unknown-code
|
||||
// path. Buffer is non-TTY so style helpers no-op and assertions
|
||||
// stay readable.
|
||||
func TestTranslateRPCError(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
cases := []struct {
|
||||
name string
|
||||
code string
|
||||
msg string
|
||||
opID string
|
||||
expect string
|
||||
}{
|
||||
{"operation_failed strips code", "operation_failed", "vm running", "", "vm running"},
|
||||
{"empty code drops prefix", "", "raw boom", "", "raw boom"},
|
||||
{"not_found", "not_found", `vm "x" not found`, "", `not found: vm "x" not found`},
|
||||
{"not_running", "not_running", "vm is not running", "", "not running: vm is not running"},
|
||||
{"already_exists", "already_exists", "image foo", "", "already exists: image foo"},
|
||||
{"bad_request", "bad_request", "missing rootfs", "", "bad request: missing rootfs"},
|
||||
{"bad_params", "bad_params", "invalid tap name", "", "bad request: invalid tap name"},
|
||||
{"bad_version", "bad_version", "unsupported version 99", "", "version mismatch: unsupported version 99"},
|
||||
{"unauthorized", "unauthorized", "uid 1000 not allowed", "", "unauthorized: uid 1000 not allowed"},
|
||||
{"unknown_method", "unknown_method", "no.such.method", "", "unknown method: no.such.method"},
|
||||
{"unknown code falls through", "weird_new_code", "boom", "", "weird_new_code: boom"},
|
||||
{"op_id appended in parens", "operation_failed", "boom", "op-deadbeef00ff", "boom (op-deadbeef00ff)"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := &rpc.ErrorResponse{Code: tc.code, Message: tc.msg, OpID: tc.opID}
|
||||
got := translateRPCError(&buf, err)
|
||||
if got != tc.expect {
|
||||
t.Errorf("got %q, want %q", got, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTranslateRPCErrorPassesThroughNonRPCErrors covers the dial
|
||||
// failure / decode failure paths where rpc.Call returns a plain Go
|
||||
// error rather than *rpc.ErrorResponse. The translator must not
|
||||
// hide the original message — that's the only signal an operator
|
||||
// has when the daemon is down.
|
||||
func TestTranslateRPCErrorPassesThroughNonRPCErrors(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
got := translateRPCError(&buf, errors.New("dial unix /run/banger/bangerd.sock: connect: no such file or directory"))
|
||||
if !strings.Contains(got, "no such file or directory") {
|
||||
t.Fatalf("plain error lost: got %q", got)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue