Files
bookhoard/internal/sync/queue_test.go
T
John O'Keefe e83c00f869 refactor(progress): ProgressService required at construction; delete the 9 unreachable erase-fallbacks
Batch H1 step 3a. The ceiling census (exhaustive UpdateUniversalProgress
grep) found 9 nil-guarded `if progressSvc != nil { funnel } else { raw
erase-write }` fallbacks — the survey's "four" under-counted kobo.go,
which has FOUR sites (Markup ReadingSync, Markup last-read-place,
AnalyticsGettests, SyncFromServer), plus one each in media.go, queue.go
and koreader.go. All unreachable: the service is created unconditionally
at main.go:127 and wired into every handler and the queue processor; the
test harness wires them too.

The erasure pattern is now unrepresentable:
- NewMediaHandler / NewKOReaderHandler / NewKoboHandler / both
  NewSyncQueueProcessor flavors take the ProgressService as a REQUIRED
  constructor arg; the SetProgressService setters are gone.
- Every fallback body (raw UpdateUniversalProgress with explicit-NULL
  pgtypes for viewport/scroll/panel/reading_mode/zoom and no
  audio_offset_seconds/context_text) is deleted; each write funnels
  through SaveProgress with the LWW seed, so fields the writer doesn't
  know survive.
- Removed the now-dead koreader pg-type helpers (textPtrToPgText,
  intPtrToPgInt4, int64PtrToPgInt8) and their self-tests.

Behavior deltas (stored-row content, no wire shape changes —
KOReader/Kobo response pins stayed green):
- KOReader fallback used to write explicit NULL viewports and drop the
  context_text the koplugin sends; the funneled mainline already maps
  book.ContextText.
- Queue flushes no longer overwrite chapter_progress with the book
  percentage (bogus data; preserved via the seed).
- A web PUT without percentage no longer writes 0% via the deleted
  fallback; percentage seeds from the stored row.

Contract suite: KOReader×4 + Kobo×3 pins green.
2026-10-16 17:28:06 -04:00

60 lines
1.8 KiB
Go

package sync
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCalculateNextRetry(t *testing.T) {
processor := NewSyncQueueProcessor(nil, nil)
tests := []struct {
name string
attempts int32
minDelay time.Duration
maxDelay time.Duration
}{
{"Attempt 0", 0, -100 * time.Millisecond, 1 * time.Second},
{"Attempt 1", 1, 59 * time.Second, 61 * time.Second},
{"Attempt 2", 2, 4*time.Minute + 59*time.Second, 5*time.Minute + 1*time.Second},
{"Attempt 3", 3, 14*time.Minute + 59*time.Second, 15*time.Minute + 1*time.Second},
{"Attempt 4", 4, 59*time.Minute + 59*time.Second, 60*time.Minute + 1*time.Second},
{"Attempt 5", 5, 23*time.Hour + 59*time.Minute, 24*time.Hour + 1*time.Minute},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
nextRetry := processor.calculateNextRetry(tt.attempts)
delay := nextRetry.Sub(time.Now())
assert.GreaterOrEqual(t, delay, tt.minDelay, "delay should be at least minDelay")
assert.LessOrEqual(t, delay, tt.maxDelay, "delay should be at most maxDelay")
})
}
}
func TestSyncTypeConstants(t *testing.T) {
assert.Equal(t, "progress", SyncTypeProgress)
assert.Equal(t, "note", SyncTypeNote)
assert.Equal(t, "highlight", SyncTypeHighlight)
assert.Equal(t, "bookmark", SyncTypeBookmark)
}
func TestSyncStatusConstants(t *testing.T) {
assert.Equal(t, "pending", SyncStatusPending)
assert.Equal(t, "processing", SyncStatusProcessing)
assert.Equal(t, "completed", SyncStatusCompleted)
assert.Equal(t, "failed", SyncStatusFailed)
}
func TestPriorityConstants(t *testing.T) {
assert.Equal(t, 1, PriorityUserInitiated)
assert.Equal(t, 2, PriorityBookCompletion)
assert.Equal(t, 3, PriorityCriticalNote)
assert.Equal(t, 5, PriorityPageTurn)
assert.Equal(t, 7, PriorityCheckpoint)
assert.Equal(t, 10, PriorityBackgroundSync)
}