Files
bookhoard/internal/services/media_scanner_ev_test.go
T
John O'Keefe 8733e83f1f style: apply gofmt across Go sources
Repo-wide gofmt sweep: const-block alignment, doc-comment spacing,
statement-after-brace fix in media.go, trailing blank lines. No
semantic changes — whitespace-insensitive diff is empty for every
file in this commit.
2026-10-25 11:05:37 -04:00

69 lines
2.0 KiB
Go

package services
// Unit tests for the extractor-version (ev) mechanism (faceted plan
// Phase 10): the wire normalization (absent = v1-or-unknown = 1) and the
// per-scan catch-up budget. The full scan-state machine (ev mismatch →
// re-extract once → stable) lives in cmd/server/tests
// (TestExtractorVersionReextract) because the store paths need a real DB.
import (
"testing"
)
func TestManifestEvNormalization(t *testing.T) {
if got := manifestEv(nil); got != 1 {
t.Errorf("nil manifest rides v1: got %d, want 1", got)
}
cases := []struct {
raw string
want int
}{
{`{"v":1}`, 1}, // old shape: ev absent → v1-or-unknown
{`{"v":1,"ev":0}`, 1}, // explicitly unset → same
{`{"v":1,"ev":1}`, 1}, // baseline parser stamp
{`{"v":1,"ev":2}`, 2}, // post-bump stamp
{`{"v":1,"entries":[],"ev":3}`, 3}, // sidecar-less ev home
}
for _, tc := range cases {
m := decodeSidecarManifest([]byte(tc.raw))
if m == nil {
t.Fatalf("%s must decode", tc.raw)
}
if got := manifestEv(m); got != tc.want {
t.Errorf("manifestEv(%s) = %d, want %d", tc.raw, got, tc.want)
}
}
}
func TestEvBudgetTake(t *testing.T) {
scanner := &MediaScanner{}
scanner.evBudgetRemaining.Store(2)
if !scanner.evBudgetTake() {
t.Error("budget 2: first take must pass")
}
if !scanner.evBudgetTake() {
t.Error("budget 1: second take must pass")
}
if scanner.evBudgetTake() {
t.Error("budget 0: third take must fail")
}
if got := scanner.evBudgetRemaining.Load(); got != 0 {
t.Errorf("budget must land at exactly 0, got %d", got)
}
}
func TestEvBudgetTake_FailsFastAtZero(t *testing.T) {
scanner := &MediaScanner{}
scanner.evBudgetRemaining.Store(0)
// Repeated takes at 0 must fail WITHOUT driving the counter negative
// (the CAS loop only fires when cur > 0).
for i := 0; i < 5; i++ {
if scanner.evBudgetTake() {
t.Fatalf("take %d must fail at budget 0", i+1)
}
}
if got := scanner.evBudgetRemaining.Load(); got != 0 {
t.Errorf("budget must stay 0, got %d", got)
}
}