Skip to content
Merged
90 changes: 82 additions & 8 deletions command/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@ type MockHandler struct {
clearCalled bool
quitCalled bool
historyCalled bool
settingCalled bool
versionCalled bool
migrateCalled bool
resetCalled bool

searchArgs []string
getArgs string
upsertArgs string
deleteArgs string
searchArgs []string
getArgs string
upsertArgs string
deleteArgs string
settingArgs []string
}

func (m *MockHandler) HandleList(scanner *bufio.Scanner) {
Expand Down Expand Up @@ -75,23 +80,24 @@ func (m *MockHandler) HandleHistory() {
}

func (m *MockHandler) HandleSetting(scanner *bufio.Scanner, args []string) {
// Not used in command tests
m.settingCalled = true
m.settingArgs = args
}

func (m *MockHandler) HandleUnknown(command string) {
// Not used in command tests
}

func (m *MockHandler) HandleVersion() {
// Not used in command tests
m.versionCalled = true
}

func (m *MockHandler) HandleMigrate(scanner *bufio.Scanner) {
// Not used in command tests
m.migrateCalled = true
}

func (m *MockHandler) HandleReset(scanner *bufio.Scanner) {
// Not used in command tests
m.resetCalled = true
}

func TestNewCommandRegistry(t *testing.T) {
Expand Down Expand Up @@ -529,4 +535,72 @@ func TestCommandInterfaceCompliance(t *testing.T) {
var _ Command = &ClearCommand{}
var _ Command = &QuitCommand{}
var _ Command = &HistoryCommand{}
var _ Command = &SettingCommand{}
var _ Command = &VersionCommand{}
var _ Command = &MigrateCommand{}
var _ Command = &ResetCommand{}
}

func TestSettingCommand_Execute(t *testing.T) {
mockHandler := &MockHandler{}
command := &SettingCommand{Handler: mockHandler}

scanner := bufio.NewScanner(strings.NewReader(""))
quit := command.Execute(scanner, []string{"randomizeinterval", "true"})

if quit {
t.Error("SettingCommand should not return quit=true")
}

if !mockHandler.settingCalled {
t.Error("Handler.HandleSetting should have been called")
}
}

func TestVersionCommand_Execute(t *testing.T) {
mockHandler := &MockHandler{}
command := &VersionCommand{Handler: mockHandler}

scanner := bufio.NewScanner(strings.NewReader(""))
quit := command.Execute(scanner, []string{})

if quit {
t.Error("VersionCommand should not return quit=true")
}

if !mockHandler.versionCalled {
t.Error("Handler.HandleVersion should have been called")
}
}

func TestMigrateCommand_Execute(t *testing.T) {
mockHandler := &MockHandler{}
command := &MigrateCommand{Handler: mockHandler}

scanner := bufio.NewScanner(strings.NewReader(""))
quit := command.Execute(scanner, []string{})

if quit {
t.Error("MigrateCommand should not return quit=true")
}

if !mockHandler.migrateCalled {
t.Error("Handler.HandleMigrate should have been called")
}
}

func TestResetCommand_Execute(t *testing.T) {
mockHandler := &MockHandler{}
command := &ResetCommand{Handler: mockHandler}

scanner := bufio.NewScanner(strings.NewReader(""))
quit := command.Execute(scanner, []string{})

if quit {
t.Error("ResetCommand should not return quit=true")
}

if !mockHandler.resetCalled {
t.Error("Handler.HandleReset should have been called")
}
}
113 changes: 113 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,116 @@ func TestResetToDefaults(t *testing.T) {
config.PageSize = originalPageSize
config.MaxDelta = originalMaxDelta
}

func TestGetSettingsRegistry(t *testing.T) {
fileUtil := &MockFileUtil{}
config, err := NewConfig(fileUtil)
if err != nil {
t.Fatalf("Failed to create config: %v", err)
}

registry := config.GetSettingsRegistry()

// Verify registry is not empty
if len(registry) == 0 {
t.Error("Expected non-empty settings registry")
}

// Verify known settings exist (these are the configurable settings)
expectedSettings := []string{"randomizeinterval", "overduepenalty", "overduelimit"}
for _, name := range expectedSettings {
if _, exists := registry[name]; !exists {
t.Errorf("Expected setting %q to exist in registry", name)
}
}
}

func TestGetSettingValue(t *testing.T) {
fileUtil := &MockFileUtil{}
config, err := NewConfig(fileUtil)
if err != nil {
t.Fatalf("Failed to create config: %v", err)
}

// Set a known value
config.RandomizeInterval = true

// Test getting a valid setting
value, err := config.GetSettingValue("randomizeinterval")
if err != nil {
t.Fatalf("Failed to get setting value: %v", err)
}
if value != true {
t.Errorf("Expected RandomizeInterval to be true, got %v", value)
}

// Test case insensitivity
value, err = config.GetSettingValue("RandomizeInterval")
if err != nil {
t.Fatalf("Failed to get setting value with mixed case: %v", err)
}
if value != true {
t.Errorf("Expected RandomizeInterval to be true, got %v", value)
}

// Test getting an unknown setting
_, err = config.GetSettingValue("unknownsetting")
if err == nil {
t.Error("Expected error for unknown setting")
}
}

func TestSetSettingValue(t *testing.T) {
fileUtil := &MockFileUtil{}
config, err := NewConfig(fileUtil)
if err != nil {
t.Fatalf("Failed to create config: %v", err)
}

// Test setting a valid int value
if err := config.SetSettingValue("overduelimit", 15); err != nil {
t.Fatalf("Failed to set setting value: %v", err)
}
if config.OverdueLimit != 15 {
t.Errorf("Expected OverdueLimit to be 15, got %d", config.OverdueLimit)
}

// Test setting a valid bool value
if err := config.SetSettingValue("randomizeinterval", false); err != nil {
t.Fatalf("Failed to set bool setting: %v", err)
}
if config.RandomizeInterval != false {
t.Error("Expected RandomizeInterval to be false")
}

// Test setting an unknown setting
if err := config.SetSettingValue("unknownsetting", 123); err == nil {
t.Error("Expected error for unknown setting")
}
}

func TestGetSettingInfo(t *testing.T) {
fileUtil := &MockFileUtil{}
config, err := NewConfig(fileUtil)
if err != nil {
t.Fatalf("Failed to create config: %v", err)
}

// Test getting info for a valid setting
info, err := config.GetSettingInfo("overduelimit")
if err != nil {
t.Fatalf("Failed to get setting info: %v", err)
}
if info.Name != "OverdueLimit" {
t.Errorf("Expected Name to be 'OverdueLimit', got %q", info.Name)
}
if info.Type != "int" {
t.Errorf("Expected Type to be 'int', got %q", info.Type)
}

// Test getting info for an unknown setting
_, err = config.GetSettingInfo("unknownsetting")
if err == nil {
t.Error("Expected error for unknown setting")
}
}
62 changes: 62 additions & 0 deletions core/model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package core

import "testing"

func TestPlatformString(t *testing.T) {
tests := []struct {
platform Platform
expected string
}{
{PlatformLeetCode, "LeetCode"},
{PlatformHackerRank, "HackerRank"},
{Platform("unknown"), "unknown"},
}

for _, tt := range tests {
t.Run(string(tt.platform), func(t *testing.T) {
if got := tt.platform.String(); got != tt.expected {
t.Errorf("Platform.String() = %q, want %q", got, tt.expected)
}
})
}
}

func TestActionTypeString(t *testing.T) {
tests := []struct {
action ActionType
expected string
}{
{ActionAdd, "Add"},
{ActionUpdate, "Update"},
{ActionDelete, "Delete"},
{ActionType("unknown"), ""},
}

for _, tt := range tests {
t.Run(string(tt.action), func(t *testing.T) {
if got := tt.action.String(); got != tt.expected {
t.Errorf("ActionType.String() = %q, want %q", got, tt.expected)
}
})
}
}

func TestActionTypePastTenseString(t *testing.T) {
tests := []struct {
action ActionType
expected string
}{
{ActionAdd, "Added"},
{ActionUpdate, "Updated"},
{ActionDelete, "Deleted"},
{ActionType("unknown"), ""},
}

for _, tt := range tests {
t.Run(string(tt.action), func(t *testing.T) {
if got := tt.action.PastTenseString(); got != tt.expected {
t.Errorf("ActionType.PastTenseString() = %q, want %q", got, tt.expected)
}
})
}
}
28 changes: 26 additions & 2 deletions core/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,29 @@ type Scheduler interface {
CalculatePriorityScore(q *Question) float64
}

// Rand abstracts random number generation for testability.
type Rand interface {
// IntN returns a random int in [0, n).
IntN(n int) int
}

// DefaultRand uses the global math/rand/v2 functions.
type DefaultRand struct{}

func (DefaultRand) IntN(n int) int { return rand.IntN(n) }

// FixedRand returns a fixed value for deterministic tests.
type FixedRand struct {
Value int
}

func (f FixedRand) IntN(_ int) int { return f.Value }

// SM2Scheduler implements the spaced repetition scheduling logic
type SM2Scheduler struct {
cfg *config.Config
Clock clock.Clock
Rand Rand

// Interval settings (in days)
maxInterval int
Expand All @@ -42,9 +61,14 @@ type SM2Scheduler struct {
}

func NewSM2Scheduler(cfg *config.Config, clock clock.Clock) *SM2Scheduler {
return NewSM2SchedulerWithRand(cfg, clock, DefaultRand{})
}

func NewSM2SchedulerWithRand(cfg *config.Config, clock clock.Clock, rand Rand) *SM2Scheduler {
return &SM2Scheduler{
cfg: cfg,
Clock: clock,
Rand: rand,

// Interval settings (in days)
maxInterval: 90,
Expand Down Expand Up @@ -157,8 +181,8 @@ func (s SM2Scheduler) setNextReview(q *Question, date time.Time, intervalDays in
// Randomize interval to avoid over-fitting to a specific date
if s.cfg.RandomizeInterval {
// Randomize between -1 and +2 days
// rand.IntN(4) produces 0,1,2,3; subtracting 1 gives -1,0,1,2
intervalDays += rand.IntN(4) - 1
// IntN(4) produces 0,1,2,3; subtracting 1 gives -1,0,1,2
intervalDays += s.Rand.IntN(4) - 1
}

// Secure bounds
Expand Down
Loading