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
This commit is contained in:
parent
01d62ffa13
commit
3e0f3a5a64
33 changed files with 7084 additions and 65 deletions
|
|
@ -2,6 +2,7 @@ package engine
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -83,3 +84,223 @@ func TestManagerShutdown(t *testing.T) {
|
|||
mgr.Shutdown(ctx)
|
||||
// Should not hang
|
||||
}
|
||||
|
||||
func TestManagerDefaultConcurrency(t *testing.T) {
|
||||
reporter := NewProgressReporter(
|
||||
agent.NewClient("http://localhost", "test", "test"),
|
||||
1*time.Second,
|
||||
)
|
||||
mgr := NewManager(ManagerConfig{MaxConcurrent: 0}, reporter)
|
||||
if cap(mgr.sem) != 3 {
|
||||
t.Errorf("default MaxConcurrent should be 3, got %d", cap(mgr.sem))
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerGetTask(t *testing.T) {
|
||||
reporter := NewProgressReporter(
|
||||
agent.NewClient("http://localhost", "test", "test"),
|
||||
1*time.Second,
|
||||
)
|
||||
mgr := NewManager(ManagerConfig{MaxConcurrent: 2}, reporter)
|
||||
|
||||
// No task added
|
||||
if task := mgr.GetTask("nonexistent"); task != nil {
|
||||
t.Error("expected nil for nonexistent task")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerActiveTasks(t *testing.T) {
|
||||
reporter := NewProgressReporter(
|
||||
agent.NewClient("http://localhost", "test", "test"),
|
||||
1*time.Second,
|
||||
)
|
||||
mgr := NewManager(ManagerConfig{MaxConcurrent: 2}, reporter)
|
||||
|
||||
tasks := mgr.ActiveTasks()
|
||||
if len(tasks) != 0 {
|
||||
t.Errorf("expected 0 active tasks, got %d", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerSubmitCompletesWithValidFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// Create a file that verify() will accept
|
||||
filePath := dir + "/movie.mkv"
|
||||
os.WriteFile(filePath, make([]byte, 1024), 0o644)
|
||||
|
||||
reporter := &mockStatusReporter{}
|
||||
pr := &ProgressReporter{
|
||||
reporter: reporter,
|
||||
interval: 100 * time.Millisecond,
|
||||
latest: make(map[string]*Task),
|
||||
lastReported: make(map[string]TaskStatus),
|
||||
}
|
||||
|
||||
dl := &resultMockDownloader{
|
||||
method: MethodTorrent,
|
||||
result: &Result{
|
||||
FilePath: filePath,
|
||||
FileName: "movie.mkv",
|
||||
Method: MethodTorrent,
|
||||
Size: 1024,
|
||||
},
|
||||
}
|
||||
|
||||
mgr := NewManager(ManagerConfig{
|
||||
MaxConcurrent: 2,
|
||||
OutputDir: dir,
|
||||
}, pr, dl)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
go pr.Run(ctx)
|
||||
|
||||
mgr.Submit(ctx, agent.Task{
|
||||
ID: "task-complete-test1",
|
||||
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
||||
Title: "Test Movie",
|
||||
PreferredMethod: "torrent",
|
||||
})
|
||||
|
||||
mgr.Wait()
|
||||
cancel()
|
||||
|
||||
// Task should have completed successfully
|
||||
// (we can't check directly since it's removed from active map after processing)
|
||||
}
|
||||
|
||||
func TestManagerCancelTask(t *testing.T) {
|
||||
reporter := NewProgressReporter(
|
||||
agent.NewClient("http://localhost", "test", "test"),
|
||||
1*time.Second,
|
||||
)
|
||||
|
||||
dl := &slowMockDownloader{method: MethodTorrent}
|
||||
mgr := NewManager(ManagerConfig{
|
||||
MaxConcurrent: 2,
|
||||
OutputDir: t.TempDir(),
|
||||
}, reporter, dl)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
go reporter.Run(ctx)
|
||||
|
||||
mgr.Submit(ctx, agent.Task{
|
||||
ID: "task-cancel-test12",
|
||||
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
||||
Title: "Cancel Me",
|
||||
PreferredMethod: "torrent",
|
||||
})
|
||||
|
||||
// Give it time to start
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
mgr.CancelTask("task-cancel-test12")
|
||||
mgr.Wait()
|
||||
}
|
||||
|
||||
func TestManagerPauseTask(t *testing.T) {
|
||||
reporter := NewProgressReporter(
|
||||
agent.NewClient("http://localhost", "test", "test"),
|
||||
1*time.Second,
|
||||
)
|
||||
|
||||
dl := &slowMockDownloader{method: MethodTorrent}
|
||||
mgr := NewManager(ManagerConfig{
|
||||
MaxConcurrent: 2,
|
||||
OutputDir: t.TempDir(),
|
||||
}, reporter, dl)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
go reporter.Run(ctx)
|
||||
|
||||
mgr.Submit(ctx, agent.Task{
|
||||
ID: "task-pause-test123",
|
||||
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
||||
Title: "Pause Me",
|
||||
PreferredMethod: "torrent",
|
||||
})
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
mgr.PauseTask("task-pause-test123")
|
||||
mgr.Wait()
|
||||
}
|
||||
|
||||
func TestManagerCancelAndDeleteFiles(t *testing.T) {
|
||||
reporter := NewProgressReporter(
|
||||
agent.NewClient("http://localhost", "test", "test"),
|
||||
1*time.Second,
|
||||
)
|
||||
|
||||
dl := &slowMockDownloader{method: MethodTorrent}
|
||||
mgr := NewManager(ManagerConfig{
|
||||
MaxConcurrent: 2,
|
||||
OutputDir: t.TempDir(),
|
||||
}, reporter, dl)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
go reporter.Run(ctx)
|
||||
|
||||
mgr.Submit(ctx, agent.Task{
|
||||
ID: "task-delfile-test12",
|
||||
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
||||
Title: "Delete Me",
|
||||
PreferredMethod: "torrent",
|
||||
})
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
mgr.CancelAndDeleteFiles("task-delfile-test12")
|
||||
mgr.Wait()
|
||||
}
|
||||
|
||||
func TestManagerCancelNonexistent(t *testing.T) {
|
||||
reporter := NewProgressReporter(
|
||||
agent.NewClient("http://localhost", "test", "test"),
|
||||
1*time.Second,
|
||||
)
|
||||
mgr := NewManager(ManagerConfig{MaxConcurrent: 2}, reporter)
|
||||
// Should not panic
|
||||
mgr.CancelTask("nonexistent")
|
||||
mgr.PauseTask("nonexistent")
|
||||
mgr.CancelAndDeleteFiles("nonexistent")
|
||||
}
|
||||
|
||||
// resultMockDownloader returns a configurable result
|
||||
type resultMockDownloader struct {
|
||||
method DownloadMethod
|
||||
result *Result
|
||||
}
|
||||
|
||||
func (m *resultMockDownloader) Method() DownloadMethod { return m.method }
|
||||
func (m *resultMockDownloader) Available(_ context.Context, _ *Task) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
func (m *resultMockDownloader) Download(_ context.Context, _ *Task, _ string, _ chan<- Progress) (*Result, error) {
|
||||
return m.result, nil
|
||||
}
|
||||
func (m *resultMockDownloader) Pause(_ string) error { return nil }
|
||||
func (m *resultMockDownloader) Cancel(_ string) error { return nil }
|
||||
func (m *resultMockDownloader) Shutdown(_ context.Context) error { return nil }
|
||||
|
||||
// slowMockDownloader blocks until context is cancelled
|
||||
type slowMockDownloader struct {
|
||||
method DownloadMethod
|
||||
}
|
||||
|
||||
func (m *slowMockDownloader) Method() DownloadMethod { return m.method }
|
||||
func (m *slowMockDownloader) Available(_ context.Context, _ *Task) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
func (m *slowMockDownloader) Download(ctx context.Context, _ *Task, _ string, _ chan<- Progress) (*Result, error) {
|
||||
<-ctx.Done()
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
func (m *slowMockDownloader) Pause(_ string) error { return nil }
|
||||
func (m *slowMockDownloader) Cancel(_ string) error { return nil }
|
||||
func (m *slowMockDownloader) Shutdown(_ context.Context) error { return nil }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue