package vocab import "testing" // TestLadderProgression walks a card up the Leitner ladder on repeated "good" // reviews: 1 → 3 → 7 → 16 → 35 days, then geometric growth by ease. func TestLadderProgression(t *testing.T) { s := State{Ease: startEase} want := []int{1, 3, 7, 16, 35} for i, w := range want { s = s.next(GradeGood) if s.Interval != w { t.Fatalf("rung %d: interval=%d, want %d", i, s.Interval, w) } if s.Reps != i+1 { t.Fatalf("rung %d: reps=%d, want %d", i, s.Reps, i+1) } } // Past the ladder it grows by ease (35 * 2.5 = 87.5 → 88). s = s.next(GradeGood) if s.Interval != 88 { t.Fatalf("post-ladder interval=%d, want 88", s.Interval) } } // TestAgainStepsBackGently proves a lapse resets to a 1-day interval and counts // a lapse, but only nudges ease down (never below the floor) — no harsh wipe. func TestAgainStepsBackGently(t *testing.T) { s := State{Reps: 4, Interval: 35, Ease: startEase} s = s.next(GradeAgain) if s.Interval != 1 { t.Fatalf("again interval=%d, want 1", s.Interval) } if s.Reps != 0 { t.Fatalf("again reps=%d, want 0", s.Reps) } if s.Lapses != 1 { t.Fatalf("again lapses=%d, want 1", s.Lapses) } if s.Ease != startEase+easeAgainDelta { t.Fatalf("again ease=%v, want %v", s.Ease, startEase+easeAgainDelta) } // Ease never drops below the floor no matter how many lapses. low := State{Ease: minEase} for i := 0; i < 5; i++ { low = low.next(GradeAgain) } if low.Ease < minEase { t.Fatalf("ease fell below floor: %v", low.Ease) } } // TestCapsBoundGrowth proves runaway "easy" grading can't push ease or the // interval past their ceilings, so a word always resurfaces within a year. func TestCapsBoundGrowth(t *testing.T) { s := State{Ease: startEase} for i := 0; i < 40; i++ { s = s.next(GradeEasy) if s.Ease > maxEase { t.Fatalf("step %d: ease %v exceeded cap %v", i, s.Ease, maxEase) } if s.Interval > maxInterval { t.Fatalf("step %d: interval %d exceeded cap %d", i, s.Interval, maxInterval) } } // After enough "easy" reviews it should be pinned at the ceilings. if s.Interval != maxInterval { t.Fatalf("interval should saturate at %d, got %d", maxInterval, s.Interval) } if s.Ease != maxEase { t.Fatalf("ease should saturate at %v, got %v", maxEase, s.Ease) } } // TestEasyAdvancesFurther proves "easy" both bumps ease and lands a longer // interval than a plain "good" at the same rung. func TestEasyAdvancesFurther(t *testing.T) { base := State{Reps: 2, Interval: 7, Ease: startEase} good := base.next(GradeGood) easy := base.next(GradeEasy) if easy.Interval <= good.Interval { t.Fatalf("easy interval %d should exceed good interval %d", easy.Interval, good.Interval) } if easy.Ease <= startEase { t.Fatalf("easy should raise ease, got %v", easy.Ease) } }