package socket import ( "testing" "time" altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1" "git.toki-labs.com/toki/alt/services/worker/internal/scheduler" ) // TestSchedulerHandlersRegisterWhenRefreshStatusIsSet verifies that the // SchedulerRefreshStatusRequest handler is registered only when the // RefreshStatus dependency is wired, and that the handler returns all entries // (success, stale, error) with the expected protobuf fields. func TestSchedulerHandlersRegisterWhenRefreshStatusIsSet(t *testing.T) { store := scheduler.NewRefreshStatusStore() store.SetScheduleConfig(scheduler.Config{ Schedules: []scheduler.ScheduleConfig{ { Name: "kr-daily", Provider: "kis", Selector: scheduler.SelectorConfig{ Kind: "watchlist", Market: "KR", Venue: "KRX", Name: "kr-core", Symbols: []string{"005930"}, }, Timeframe: "daily", Cadence: "daily", Timezone: "Asia/Seoul", BackfillWindow: "3d", ParallelismLimit: 2, }, }, }) store.Record("kr-daily", scheduler.RefreshStatus{ Schedule: "kr-daily", Status: "success", ImportedBarCount: 120, MissingCount: 0, }) installed := marketHandlers(Deps{RefreshStatus: store}) // Verify the SchedulerRefreshStatusRequest handler is present. found := false for _, h := range installed { if h.requestType == "alt.v1.SchedulerRefreshStatusRequest" { found = true if h.register == nil { t.Error("SchedulerRefreshStatusRequest handler has nil register") } break } } if !found { t.Error("marketHandlers should include SchedulerRefreshStatusRequest handler when RefreshStatus is set") } // Verify All() returns the entry we recorded. all := store.All() if len(all) != 1 { t.Fatalf("expected 1 entry from store.All(), got %d", len(all)) } if all[0].Schedule != "kr-daily" || all[0].Status != "success" { t.Errorf("unexpected entry: %+v", all[0]) } // Verify Get() by name works. entry, ok := store.Get("kr-daily") if !ok { t.Error("store.Get('kr-daily') should return ok=true") } if entry.Schedule != "kr-daily" { t.Errorf("expected schedule 'kr-daily', got %q", entry.Schedule) } } func TestSchedulerHandlersOmittedWhenRefreshStatusIsNil(t *testing.T) { installed := marketHandlers(Deps{}) for _, h := range installed { if h.requestType == "alt.v1.SchedulerRefreshStatusRequest" { t.Error("marketHandlers should NOT include SchedulerRefreshStatusRequest handler when RefreshStatus is nil") } } } func TestSchedulerRefreshStatusResponseFields(t *testing.T) { store := scheduler.NewRefreshStatusStore() store.SetScheduleConfig(scheduler.Config{ Schedules: []scheduler.ScheduleConfig{ { Name: "kr-daily", Provider: "kis", BackfillWindow: "3d", Cadence: "daily", Timeframe: "daily", Timezone: "Asia/Seoul", ParallelismLimit: 2, Selector: scheduler.SelectorConfig{ Kind: "watchlist", Market: "KR", Venue: "KRX", Name: "kr-core", Symbols: []string{"005930"}, }, }, }, }) store.Record("kr-daily", scheduler.RefreshStatus{ Schedule: "kr-daily", Status: "success", LastSuccess: time.UnixMilli(1700000000000), ImportedBarCount: 120, MissingCount: 0, GapCount: 0, DuplicateCount: 0, ProviderDelay: 1, }) deps := Deps{RefreshStatus: store} resp, err := handleSchedulerRefreshStatus(deps, &altv1.SchedulerRefreshStatusRequest{}) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(resp.GetEntries()) != 1 { t.Fatalf("expected 1 entry, got %d", len(resp.GetEntries())) } entry := resp.GetEntries()[0] if entry.GetSchedule() != "kr-daily" { t.Errorf("schedule = %q, want %q", entry.GetSchedule(), "kr-daily") } if entry.GetStatus() != "success" { t.Errorf("status = %q, want %q", entry.GetStatus(), "success") } if entry.GetImportedBarCount() != 120 { t.Errorf("imported_bar_count = %d, want 120", entry.GetImportedBarCount()) } if entry.GetMissingCount() != 0 { t.Errorf("missing_count = %d, want 0", entry.GetMissingCount()) } if entry.GetGapCount() != 0 { t.Errorf("gap_count = %d, want 0", entry.GetGapCount()) } if entry.GetDuplicateCount() != 0 { t.Errorf("duplicate_count = %d, want 0", entry.GetDuplicateCount()) } if entry.GetProviderDelayDays() != 1 { t.Errorf("provider_delay_days = %d, want 1", entry.GetProviderDelayDays()) } } func TestSchedulerRefreshStatusZeroTimestamps(t *testing.T) { // Test 1: Zero LastSuccess and zero NextRun with NO config. // When config is absent, enrichWithNextRun does not compute NextRun, // so both timestamps remain zero and unixMilliMs converts them to 0. store := scheduler.NewRefreshStatusStore() // Do NOT set schedule config — NextRun stays zero. store.Record("kr-daily", scheduler.RefreshStatus{ Schedule: "kr-daily", Status: "stale", LastSuccess: time.Time{}, // zero time NextRun: time.Time{}, // zero time (no config to enrich) ImportedBarCount: 0, MissingCount: 0, GapCount: 0, DuplicateCount: 0, ProviderDelay: 0, }) resp, err := handleSchedulerRefreshStatus(Deps{RefreshStatus: store}, &altv1.SchedulerRefreshStatusRequest{}) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(resp.GetEntries()) != 1 { t.Fatalf("expected 1 entry, got %d", len(resp.GetEntries())) } entry := resp.GetEntries()[0] if entry.GetLastSuccessUnixMs() != 0 { t.Errorf("LastSuccessUnixMs = %d, want 0 for zero time", entry.GetLastSuccessUnixMs()) } if entry.GetNextRunUnixMs() != 0 { t.Errorf("NextRunUnixMs = %d, want 0 for zero time (no config)", entry.GetNextRunUnixMs()) } // Test 2: Non-zero LastSuccess with config. // When config is set, NextRun gets enriched by enrichWithNextRun, // so only LastSuccess is tested for non-zero preservation. store2 := scheduler.NewRefreshStatusStore() store2.SetScheduleConfig(scheduler.Config{ Schedules: []scheduler.ScheduleConfig{ { Name: "kr-daily", Provider: "kis", BackfillWindow: "3d", Cadence: "daily", Timeframe: "daily", Timezone: "Asia/Seoul", ParallelismLimit: 2, Selector: scheduler.SelectorConfig{ Kind: "watchlist", Market: "KR", Venue: "KRX", Name: "kr-core", Symbols: []string{"005930"}, }, }, }, }) nonZeroSuccess := time.UnixMilli(1700000000000) store2.Record("kr-daily", scheduler.RefreshStatus{ Schedule: "kr-daily", Status: "success", LastSuccess: nonZeroSuccess, NextRun: time.Time{}, // will be enriched by enrichWithNextRun ImportedBarCount: 120, MissingCount: 0, GapCount: 0, DuplicateCount: 0, ProviderDelay: 1, }) resp2, err := handleSchedulerRefreshStatus(Deps{RefreshStatus: store2}, &altv1.SchedulerRefreshStatusRequest{}) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(resp2.GetEntries()) != 1 { t.Fatalf("expected 1 entry, got %d", len(resp2.GetEntries())) } entry2 := resp2.GetEntries()[0] if entry2.GetLastSuccessUnixMs() != 1700000000000 { t.Errorf("LastSuccessUnixMs = %d, want 1700000000000", entry2.GetLastSuccessUnixMs()) } // NextRun is enriched by enrichWithNextRun — must NOT be zero. if entry2.GetNextRunUnixMs() == 0 { t.Errorf("NextRunUnixMs = 0, expected enriched non-zero value") } if entry2.GetImportedBarCount() != 120 { t.Errorf("imported_bar_count = %d, want 120", entry2.GetImportedBarCount()) } if entry2.GetProviderDelayDays() != 1 { t.Errorf("provider_delay_days = %d, want 1", entry2.GetProviderDelayDays()) } // Test 3: Error status with zero timestamps and NO config. store3 := scheduler.NewRefreshStatusStore() // Do NOT set schedule config. store3.Record("kr-daily", scheduler.RefreshStatus{ Schedule: "kr-daily", Status: "error", LastSuccess: time.Time{}, // zero - never succeeded LastError: "API key expired", NextRun: time.Time{}, // zero ImportedBarCount: 0, MissingCount: 0, GapCount: 0, DuplicateCount: 0, ProviderDelay: 0, }) resp3, err := handleSchedulerRefreshStatus(Deps{RefreshStatus: store3}, &altv1.SchedulerRefreshStatusRequest{}) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(resp3.GetEntries()) != 1 { t.Fatalf("expected 1 entry, got %d", len(resp3.GetEntries())) } entry3 := resp3.GetEntries()[0] if entry3.GetStatus() != "error" { t.Errorf("status = %q, want %q", entry3.GetStatus(), "error") } if entry3.GetLastSuccessUnixMs() != 0 { t.Errorf("LastSuccessUnixMs = %d, want 0 for zero time on error status", entry3.GetLastSuccessUnixMs()) } if entry3.GetNextRunUnixMs() != 0 { t.Errorf("NextRunUnixMs = %d, want 0 for zero time on error status (no config)", entry3.GetNextRunUnixMs()) } if entry3.GetLastError() != "API key expired" { t.Errorf("last_error = %q, want %q", entry3.GetLastError(), "API key expired") } } func TestSchedulerCapabilitiesAddedWhenRefreshStatusIsWired(t *testing.T) { capsNo := capabilitiesForDeps(Deps{}) capsYes := capabilitiesForDeps(Deps{ RefreshStatus: scheduler.NewRefreshStatusStore(), }) foundNo := false for _, c := range capsNo { if c == "scheduler" { foundNo = true } } if foundNo { t.Error("capabilities should NOT include 'scheduler' when RefreshStatus is nil") } foundYes := false for _, c := range capsYes { if c == "scheduler" { foundYes = true break } } if !foundYes { t.Error("capabilities should include 'scheduler' when RefreshStatus is wired") } }