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>
60 lines
2.4 KiB
Go
60 lines
2.4 KiB
Go
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)
|
|
}
|
|
}
|