Add a central job registry that library scans and search index builds report into, so background work is visible instead of buried in the settings page. - backend/jobs: registry with per-job ring-buffer logs, capability-driven controls, and one coalesced JobsChanged snapshot at 4Hz - pause survives restart via a job_state table; a paused scan is adopted back on launch and skipped by the soft scan - top-bar indicator, popover, details drawer and a Jobs page replacing the config page's scan UI; per-library start/stop retained - scan timing breakdown moves into the job log, Full rescan to the Jobs page; delete the orphaned library-manager component Also add cmd/indexbuild and cmd/indexexport so the explore index can be built once centrally rather than by every install, which today streams ~205GB from the ListenBrainz spark dump on first run. indexbuild picks build/refresh/rebuild from index state; the Gitea workflow runs it on push, weekly, or manually and publishes only when content changed. fresh-install no longer defaults YJ_HOME under /tmp: it is tmpfs on most distros, and the import needs ~6GB of real disk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
104 lines
2.2 KiB
Go
104 lines
2.2 KiB
Go
package main
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// stubState lets the decision table run without a database. It mirrors
|
|
// the three pieces of index state `decide` consults.
|
|
type stubState struct {
|
|
complete bool
|
|
last time.Time
|
|
}
|
|
|
|
func (s stubState) IndexImportComplete() bool { return s.complete }
|
|
func (s stubState) IndexLastImported() time.Time { return s.last }
|
|
|
|
func TestDecide(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
rebuildAfter = 90 * 24 * time.Hour
|
|
refreshAfter = 7 * 24 * time.Hour
|
|
)
|
|
|
|
tests := []struct {
|
|
name string
|
|
mode mode
|
|
state stubState
|
|
want mode
|
|
}{
|
|
{
|
|
name: "first run with no import builds",
|
|
mode: modeAuto,
|
|
state: stubState{complete: false},
|
|
want: modeBuild,
|
|
},
|
|
{
|
|
name: "partial import resumes as a build",
|
|
mode: modeAuto,
|
|
state: stubState{complete: false, last: time.Now().Add(-time.Hour)},
|
|
want: modeBuild,
|
|
},
|
|
{
|
|
name: "recent import refreshes",
|
|
mode: modeAuto,
|
|
state: stubState{complete: true, last: time.Now().Add(-24 * time.Hour)},
|
|
want: modeRefresh,
|
|
},
|
|
{
|
|
name: "import just under the rebuild age still refreshes",
|
|
mode: modeAuto,
|
|
state: stubState{complete: true, last: time.Now().Add(-89 * 24 * time.Hour)},
|
|
want: modeRefresh,
|
|
},
|
|
{
|
|
name: "import past the rebuild age rebuilds",
|
|
mode: modeAuto,
|
|
state: stubState{complete: true, last: time.Now().Add(-91 * 24 * time.Hour)},
|
|
want: modeRebuild,
|
|
},
|
|
{
|
|
name: "unreadable timestamp rebuilds rather than wedging",
|
|
mode: modeAuto,
|
|
state: stubState{complete: true},
|
|
want: modeRebuild,
|
|
},
|
|
{
|
|
name: "explicit mode overrides state",
|
|
mode: modeRefresh,
|
|
state: stubState{complete: false},
|
|
want: modeRefresh,
|
|
},
|
|
{
|
|
name: "explicit rebuild overrides a fresh import",
|
|
mode: modeRebuild,
|
|
state: stubState{complete: true, last: time.Now()},
|
|
want: modeRebuild,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, why := decideFrom(
|
|
opts{
|
|
mode: tt.mode,
|
|
rebuildAfter: rebuildAfter,
|
|
refreshAfter: refreshAfter,
|
|
},
|
|
tt.state,
|
|
)
|
|
if got != tt.want {
|
|
t.Errorf("decide = %q (%s), want %q", got, why, tt.want)
|
|
}
|
|
|
|
if why == "" {
|
|
t.Error("expected a non-empty reason")
|
|
}
|
|
})
|
|
}
|
|
}
|