fix(cli): normalize error prefix
- commit
- 23e3a7075804f42a957f9d5ab3eaed8c425cdcf1
- author
- brice · · 2026-07-18 13:56 UTC
- parent
- 15085f3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +72 -1
M main.go +13 -1
| @@ -3,13 +3,25 @@ package main | |||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | 5 | "os" | |
| 6 | + | "strings" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/vev/internal/app" | |
| 8 | 9 | ) | |
| 9 | 10 | ||
| 10 | 11 | func main() { | |
| 11 | 12 | if err := app.Run(os.Args[1:]); err != nil { | |
| 12 | - | fmt.Fprintln(os.Stderr, "vev:", err) | |
| 13 | + | fmt.Fprintln(os.Stderr, cliError(err)) | |
| 13 | 14 | os.Exit(1) | |
| 14 | 15 | } | |
| 15 | 16 | } | |
| 17 | + | ||
| 18 | + | // cliError adds the sole user-facing CLI prefix. Errors may already include | |
| 19 | + | // it when they originate from a runtime component, so remove any such prefix | |
| 20 | + | // before rendering. | |
| 21 | + | func cliError(err error) string { | |
| 22 | + | message := err.Error() | |
| 23 | + | for strings.HasPrefix(message, "vev:") { | |
| 24 | + | message = strings.TrimSpace(strings.TrimPrefix(message, "vev:")) | |
| 25 | + | } | |
| 26 | + | return "vev: " + message | |
| 27 | + | } | |
| @@ -3,13 +3,25 @@ package main | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | "os" | 5 | "os" | ||
| 6 | + | "strings" | |||
| 6 | 7 | ||||
| 7 | "github.com/bnema/vev/internal/app" | 8 | "github.com/bnema/vev/internal/app" | ||
| 8 | ) | 9 | ) | ||
| 9 | 10 | ||||
| 10 | func main() { | 11 | func main() { | ||
| 11 | if err := app.Run(os.Args[1:]); err != nil { | 12 | if err := app.Run(os.Args[1:]); err != nil { | ||
| 12 | - | fmt.Fprintln(os.Stderr, "vev:", err) | 13 | + | fmt.Fprintln(os.Stderr, cliError(err)) |
| 13 | os.Exit(1) | 14 | os.Exit(1) | ||
| 14 | } | 15 | } | ||
| 15 | } | 16 | } | ||
| 17 | + | ||||
| 18 | + | // cliError adds the sole user-facing CLI prefix. Errors may already include | |||
| 19 | + | // it when they originate from a runtime component, so remove any such prefix | |||
| 20 | + | // before rendering. | |||
| 21 | + | func cliError(err error) string { | |||
| 22 | + | message := err.Error() | |||
| 23 | + | for strings.HasPrefix(message, "vev:") { | |||
| 24 | + | message = strings.TrimSpace(strings.TrimPrefix(message, "vev:")) | |||
| 25 | + | } | |||
| 26 | + | return "vev: " + message | |||
| 27 | + | } | |||
A main_test.go +59 -0
| @@ -0,0 +1,59 @@ | |||
| 1 | + | package main | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/vev/internal/app" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func TestCLIError(t *testing.T) { | |
| 11 | + | tests := []struct { | |
| 12 | + | name string | |
| 13 | + | err error | |
| 14 | + | want string | |
| 15 | + | }{ | |
| 16 | + | { | |
| 17 | + | name: "usage error", | |
| 18 | + | err: runError([]string{"unknown"}), | |
| 19 | + | want: `vev: unknown command "unknown"`, | |
| 20 | + | }, | |
| 21 | + | { | |
| 22 | + | name: "domain validation error", | |
| 23 | + | err: runError([]string{"new", "invalid name"}), | |
| 24 | + | want: "vev: invalid session name: must match [A-Za-z0-9][A-Za-z0-9._-]{0,63}", | |
| 25 | + | }, | |
| 26 | + | { | |
| 27 | + | name: "daemon error", | |
| 28 | + | err: errors.New("vev: daemon listen: address already in use"), | |
| 29 | + | want: "vev: daemon listen: address already in use", | |
| 30 | + | }, | |
| 31 | + | { | |
| 32 | + | name: "session error", | |
| 33 | + | err: errors.New("vev: no such session: work"), | |
| 34 | + | want: "vev: no such session: work", | |
| 35 | + | }, | |
| 36 | + | { | |
| 37 | + | name: "repeated prefix", | |
| 38 | + | err: errors.New("vev: vev: message"), | |
| 39 | + | want: "vev: message", | |
| 40 | + | }, | |
| 41 | + | { | |
| 42 | + | name: "prefix whitespace", | |
| 43 | + | err: errors.New("vev:\t message"), | |
| 44 | + | want: "vev: message", | |
| 45 | + | }, | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | for _, tt := range tests { | |
| 49 | + | t.Run(tt.name, func(t *testing.T) { | |
| 50 | + | if got := cliError(tt.err); got != tt.want { | |
| 51 | + | t.Errorf("cliError(%v) = %q, want %q", tt.err, got, tt.want) | |
| 52 | + | } | |
| 53 | + | }) | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | func runError(args []string) error { | |
| 58 | + | return app.Run(args) | |
| 59 | + | } | |
| @@ -0,0 +1,59 @@ | |||||
| 1 | + | package main | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/vev/internal/app" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func TestCLIError(t *testing.T) { | |||
| 11 | + | tests := []struct { | |||
| 12 | + | name string | |||
| 13 | + | err error | |||
| 14 | + | want string | |||
| 15 | + | }{ | |||
| 16 | + | { | |||
| 17 | + | name: "usage error", | |||
| 18 | + | err: runError([]string{"unknown"}), | |||
| 19 | + | want: `vev: unknown command "unknown"`, | |||
| 20 | + | }, | |||
| 21 | + | { | |||
| 22 | + | name: "domain validation error", | |||
| 23 | + | err: runError([]string{"new", "invalid name"}), | |||
| 24 | + | want: "vev: invalid session name: must match [A-Za-z0-9][A-Za-z0-9._-]{0,63}", | |||
| 25 | + | }, | |||
| 26 | + | { | |||
| 27 | + | name: "daemon error", | |||
| 28 | + | err: errors.New("vev: daemon listen: address already in use"), | |||
| 29 | + | want: "vev: daemon listen: address already in use", | |||
| 30 | + | }, | |||
| 31 | + | { | |||
| 32 | + | name: "session error", | |||
| 33 | + | err: errors.New("vev: no such session: work"), | |||
| 34 | + | want: "vev: no such session: work", | |||
| 35 | + | }, | |||
| 36 | + | { | |||
| 37 | + | name: "repeated prefix", | |||
| 38 | + | err: errors.New("vev: vev: message"), | |||
| 39 | + | want: "vev: message", | |||
| 40 | + | }, | |||
| 41 | + | { | |||
| 42 | + | name: "prefix whitespace", | |||
| 43 | + | err: errors.New("vev:\t message"), | |||
| 44 | + | want: "vev: message", | |||
| 45 | + | }, | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | for _, tt := range tests { | |||
| 49 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 50 | + | if got := cliError(tt.err); got != tt.want { | |||
| 51 | + | t.Errorf("cliError(%v) = %q, want %q", tt.err, got, tt.want) | |||
| 52 | + | } | |||
| 53 | + | }) | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | func runError(args []string) error { | |||
| 58 | + | return app.Run(args) | |||
| 59 | + | } | |||