package services import ( "testing" ) // GetActiveScanJobs backs the refresh-survival contract: a client that // loads the page mid-scan must be able to discover pending/running jobs // (with their library) rather than only learning about scans from // websocket events emitted after the page opened. func TestGetActiveScanJobs(t *testing.T) { w := NewWorker(1, nil) seed := map[string]*JobResult{ "running-scan": {JobID: "running-scan", Status: JobStatusRunning, LibraryID: "lib-1", Progress: 0.42}, "pending-scan": {JobID: "pending-scan", Status: JobStatusPending, LibraryID: "lib-2"}, "done-scan": {JobID: "done-scan", Status: JobStatusCompleted, LibraryID: "lib-3"}, "failed-scan": {JobID: "failed-scan", Status: JobStatusFailed}, "cancelled": {JobID: "cancelled", Status: JobStatusCancelled}, } w.mu.Lock() w.results = seed w.mu.Unlock() active := w.GetActiveScanJobs() if len(active) != 2 { t.Fatalf("expected 2 active jobs, got %d (%+v)", len(active), active) } seen := map[string]JobResult{} for _, job := range active { seen[job.JobID] = job } running, ok := seen["running-scan"] if !ok { t.Fatal("running scan missing from active jobs") } if running.LibraryID != "lib-1" { t.Errorf("expected library_id lib-1, got %q", running.LibraryID) } if running.Progress != 0.42 { t.Errorf("expected progress 0.42, got %v", running.Progress) } if _, ok := seen["pending-scan"]; !ok { t.Error("pending scan missing from active jobs") } for _, id := range []string{"done-scan", "failed-scan", "cancelled"} { if _, ok := seen[id]; ok { t.Errorf("terminal job %q must not appear as active", id) } } } func TestGetActiveScanJobsEmpty(t *testing.T) { w := NewWorker(1, nil) if jobs := w.GetActiveScanJobs(); len(jobs) != 0 { t.Fatalf("expected no active jobs, got %d", len(jobs)) } } // HasActiveScanForLibrary is the enqueue-coalescing predicate (plan // Part 2 §5): a pending/running SCAN for the same library makes the // next poll tick / watcher event a no-op. Only scans coalesce — an // import or convert sharing the library id must not block one. func TestHasActiveScanForLibrary(t *testing.T) { w := NewWorker(1, nil) seed := map[string]*JobResult{ "running-scan": {JobID: "running-scan", Status: JobStatusRunning, LibraryID: "lib-1", JobType: JobTypeScan}, "pending-scan": {JobID: "pending-scan", Status: JobStatusPending, LibraryID: "lib-2", JobType: JobTypeScan}, "done-scan": {JobID: "done-scan", Status: JobStatusCompleted, LibraryID: "lib-3", JobType: JobTypeScan}, "failed-scan": {JobID: "failed-scan", Status: JobStatusFailed, LibraryID: "lib-4", JobType: JobTypeScan}, "import-lib1": {JobID: "import-lib1", Status: JobStatusRunning, LibraryID: "lib-1", JobType: JobTypeImport}, "no-library": {JobID: "no-library", Status: JobStatusRunning, JobType: JobTypeScan}, } w.mu.Lock() w.results = seed w.mu.Unlock() cases := []struct { library string want bool }{ {"lib-1", true}, // running scan {"lib-2", true}, // pending scan {"lib-3", false}, // completed = terminal {"lib-4", false}, // failed = terminal {"lib-9", false}, // unknown library {"", false}, // jobs without a library never coalesce } for _, c := range cases { if got := w.HasActiveScanForLibrary(c.library); got != c.want { t.Errorf("HasActiveScanForLibrary(%q) = %v, want %v", c.library, got, c.want) } } // A nil worker (tests, early startup) must not panic. var nilWorker *Worker if nilWorker.HasActiveScanForLibrary("lib-1") { t.Error("nil worker must report no active scans") } }