Files
bookhoard/internal/services/worker_active_test.go
T
John O'Keefe fd6f0e3353 fix(web): scan progress — correct percentages, no layout shift, refresh-survivable
Bug: the admin 'Scanning Libraries' widget fed the server's 0-1
progress straight into percent strings and bar widths, so the overall
and per-library percentages capped at '1%' and the bars never filled.
Both now scale by 100 (matching the setup page, which was already
correct).

Bug: scan state only reached clients via websocket events, so a page
opened (or refreshed) mid-scan showed nothing until the next tick —
and the admin widget never re-attached at all. The worker now exposes
active jobs (with their library_id so rows keep their names), served
as GET /api/scanner/active (admin, re-attaches the widget + polling)
and GET /api/scanner/active-summary (any user, restores the sidebar
indicator on load). Idle hides both, exactly as before.

Annoyance: the sidebar's 'Scanning… N%' was a block element between
the logo and the nav, shoving every menu entry down while a scan ran.
The percent now lives inside the fixed-height logo row next to the
spinner (spinner last, so growing digits extend leftward); nothing
below it moves.
2026-10-15 08:29:04 -04:00

61 lines
1.8 KiB
Go

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))
}
}