alt/services/worker/internal/scheduler/config_test.go

170 lines
4.6 KiB
Go

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())
}
}