feat(cli): upgrade command, rich status, and version cache
- Replace `upgrade` stub with real command (alias for `self-update`)
- Also register `update` as alias: `unarr update` works too
- Rewrite `status` to show full config, disk usage, daemon state, and
update availability with colored sections
- Add version check cache (1h TTL) so `status` is instant on repeat runs
- Guard against division by zero on empty filesystems
- Guard against negative durations from clock skew
- Guard against stale PID via heartbeat recency check (2 min)
- Add comprehensive test coverage across agent, engine, upgrade, usenet,
arr, library, mediaserver, and UI packages
- Improve Makefile coverage target to exclude cmd/ glue code
- Fix stream handler resource cleanup and ffprobe error handling
2026-03-31 22:05:43 +02:00
|
|
|
package sentry
|
|
|
|
|
|
2026-05-27 16:50:16 +02:00
|
|
|
import (
|
2026-05-27 17:03:26 +02:00
|
|
|
"errors"
|
2026-05-27 16:50:16 +02:00
|
|
|
"fmt"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
feat(cli): upgrade command, rich status, and version cache
- Replace `upgrade` stub with real command (alias for `self-update`)
- Also register `update` as alias: `unarr update` works too
- Rewrite `status` to show full config, disk usage, daemon state, and
update availability with colored sections
- Add version check cache (1h TTL) so `status` is instant on repeat runs
- Guard against division by zero on empty filesystems
- Guard against negative durations from clock skew
- Guard against stale PID via heartbeat recency check (2 min)
- Add comprehensive test coverage across agent, engine, upgrade, usenet,
arr, library, mediaserver, and UI packages
- Improve Makefile coverage target to exclude cmd/ glue code
- Fix stream handler resource cleanup and ffprobe error handling
2026-03-31 22:05:43 +02:00
|
|
|
|
|
|
|
|
func TestEnvironment(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
version string
|
|
|
|
|
want string
|
|
|
|
|
}{
|
|
|
|
|
{"", "development"},
|
|
|
|
|
{"dev", "development"},
|
|
|
|
|
{"0.1.0-dev", "development"},
|
|
|
|
|
{"1.0.0", "production"},
|
|
|
|
|
{"0.3.5", "production"},
|
|
|
|
|
{"2.0.0-beta", "production"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.version, func(t *testing.T) {
|
|
|
|
|
got := environment(tt.version)
|
|
|
|
|
if got != tt.want {
|
|
|
|
|
t.Errorf("environment(%q) = %q, want %q", tt.version, got, tt.want)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestInitNoOp(t *testing.T) {
|
|
|
|
|
// With empty dsn (default in tests), Init should be a no-op
|
|
|
|
|
Init("1.0.0")
|
|
|
|
|
// Should not panic
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCloseNoOp(t *testing.T) {
|
|
|
|
|
// Close should be safe to call without Init
|
|
|
|
|
Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCaptureErrorNil(t *testing.T) {
|
|
|
|
|
// Should not panic with nil error
|
|
|
|
|
CaptureError(nil, "test")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSetUser(t *testing.T) {
|
|
|
|
|
// Should not panic without initialization
|
|
|
|
|
SetUser("agent-123")
|
|
|
|
|
}
|
2026-05-27 16:50:16 +02:00
|
|
|
|
2026-05-27 17:03:26 +02:00
|
|
|
func TestShouldSkipSentryDaemonNotRunning(t *testing.T) {
|
|
|
|
|
// String must stay in sync with agent.ErrDaemonNotRunning. If that sentinel
|
|
|
|
|
// is reworded, this test fails loudly so the marker can be updated.
|
|
|
|
|
err := errors.New("daemon does not appear to be running (state file not found)")
|
|
|
|
|
if !shouldSkipSentry(err) {
|
|
|
|
|
t.Error("ErrDaemonNotRunning message should be skipped")
|
2026-05-27 16:50:16 +02:00
|
|
|
}
|
2026-05-27 17:03:26 +02:00
|
|
|
wrapped := fmt.Errorf("read daemon state: %w", err)
|
|
|
|
|
if !shouldSkipSentry(wrapped) {
|
|
|
|
|
t.Error("wrapped ErrDaemonNotRunning message should be skipped")
|
2026-05-27 16:50:16 +02:00
|
|
|
}
|
|
|
|
|
}
|