package scheduler import ( "bytes" "os" "path/filepath" "strings" "testing" "time" "git.toki-labs.com/toki/alt/packages/domain/market" ) func testCapability() market.ProviderCapability { return market.ProviderCapability{ Provider: market.ProviderKIS, Rules: []market.ProviderCapabilityRule{ { Market: market.MarketKR, Venue: market.VenueKRX, AssetTypes: map[market.AssetType]bool{ market.AssetTypeEquity: true, }, Timeframes: map[market.Timeframe]bool{ market.TimeframeDaily: true, }, }, }, } } func TestValidateConfigReportsNextWindowAndRejectedCombination(t *testing.T) { cfg, err := LoadConfig(strings.NewReader(` schedules: - name: kr-core-daily provider: kis selector: kind: watchlist market: KR venue: KRX name: kr-core symbols: ["005930", "000660"] timeframe: daily cadence: daily timezone: Asia/Seoul backfill_window: 3d parallelism_limit: 2 - name: kr-core-monthly provider: kis selector: kind: watchlist market: KR venue: KRX name: kr-core symbols: ["005930"] timeframe: monthly cadence: daily timezone: Asia/Seoul backfill_window: 3d parallelism_limit: 1 `)) if err != nil { t.Fatalf("LoadConfig: %v", err) } now := time.Date(2026, 6, 21, 10, 30, 0, 0, time.UTC) result := ValidateConfig(cfg, ValidationOptions{ Now: now, Capabilities: CapabilityMap(testCapability()), }) if len(result.Items) != 2 { t.Fatalf("items = %d, want 2", len(result.Items)) } valid := result.Items[0] if valid.Status != "valid" { t.Fatalf("first status = %q, want valid: %+v", valid.Status, valid) } if valid.NextRun != "2026-06-22T00:00:00+09:00" { t.Errorf("next run = %q, want 2026-06-22T00:00:00+09:00", valid.NextRun) } if valid.Cadence != "24h0m0s" || valid.BackfillWindow != "72h0m0s" { t.Errorf("durations = %s/%s, want 24h0m0s/72h0m0s", valid.Cadence, valid.BackfillWindow) } rejected := result.Items[1] if rejected.Status != "rejected" { t.Fatalf("second status = %q, want rejected", rejected.Status) } if !strings.Contains(rejected.Reason, "unsupported timeframe") { t.Errorf("rejection reason %q missing unsupported timeframe", rejected.Reason) } } func TestWriteTextAndJSONL(t *testing.T) { validation := Validation{Items: []ValidationItem{ { Schedule: "kr-core-daily", Status: "valid", Provider: "kis", Selector: "kr-core", Timeframe: "daily", Cadence: "24h0m0s", Timezone: "Asia/Seoul", BackfillWindow: "72h0m0s", ParallelismLimit: 2, NextRun: "2026-06-22T00:00:00+09:00", }, {Schedule: "bad", Status: "rejected", Reason: "unsupported provider/timeframe combination"}, }} var text bytes.Buffer if err := WriteText(&text, validation); err != nil { t.Fatalf("WriteText: %v", err) } for _, want := range []string{ "schedule=kr-core-daily status=valid", "next_run=2026-06-22T00:00:00+09:00", "schedule=bad status=rejected", "unsupported provider/timeframe combination", } { if !strings.Contains(text.String(), want) { t.Errorf("text output %q missing %q", text.String(), want) } } var jsonl bytes.Buffer if err := WriteJSONL(&jsonl, validation); err != nil { t.Fatalf("WriteJSONL: %v", err) } if !strings.Contains(jsonl.String(), `"schedule":"kr-core-daily"`) { t.Errorf("jsonl output %q missing valid schedule", jsonl.String()) } if !strings.Contains(jsonl.String(), `"status":"rejected"`) { t.Errorf("jsonl output %q missing rejected status", jsonl.String()) } } func TestRunCheckReturnsRejectedWhenAnyScheduleIsRejected(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "schedule.yaml") if err := os.WriteFile(path, []byte(` schedules: - name: unsupported provider: kis selector: kind: watchlist market: KR venue: KRX name: kr-core symbols: ["005930"] timeframe: minute_1 cadence: 24h timezone: Asia/Seoul backfill_window: 48h parallelism_limit: 1 `), 0644); err != nil { t.Fatalf("write fixture: %v", err) } var stdout, stderr bytes.Buffer code := RunCheck([]string{"--file", path, "--output", "jsonl", "--at", "2026-06-21T10:30:00Z"}, &stdout, &stderr, func() time.Time { return time.Date(2026, 6, 21, 10, 30, 0, 0, time.UTC) }, CapabilityMap(testCapability())) if code != exitRejected { t.Fatalf("exit code = %d, want %d (stderr=%q stdout=%q)", code, exitRejected, stderr.String(), stdout.String()) } if !strings.Contains(stdout.String(), `"status":"rejected"`) { t.Errorf("stdout %q missing rejected JSONL", stdout.String()) } }