Files
yellowjacket/cmd/indexbuild/decide_test.go
T
yonluandClaude Opus 5 01bc5f2094 feat(jobs): surface background jobs with progress, logs and controls
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>
2026-07-25 14:42:22 -04:00

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