alt/services/worker/internal/jobs/backtest_starter_test.go

117 lines
3.8 KiB
Go

package jobs
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
"git.toki-labs.com/toki/alt/packages/domain/backtest"
"git.toki-labs.com/toki/alt/packages/domain/market"
"git.toki-labs.com/toki/alt/services/worker/internal/storage"
)
func sampleSpec() backtest.RunSpec {
return backtest.RunSpec{
StrategyID: "strat-abc",
Market: market.MarketKR,
Timeframe: market.TimeframeDaily,
From: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
To: time.Date(2026, 5, 15, 0, 0, 0, 0, time.UTC),
}
}
func TestBacktestStarterRecordsPendingAndLaunches(t *testing.T) {
runner := NewRunner()
store := &stubBacktestRunStore{runs: make(map[backtest.RunID]backtest.Run)}
var executedPayload json.RawMessage
runner.Register(KindRunBacktest, func(ctx context.Context, payload json.RawMessage) error {
executedPayload = payload
return nil
})
fixed := time.Date(2026, 5, 30, 12, 0, 0, 0, time.UTC)
starter := NewBacktestStarter(runner, store,
WithStarterNow(func() time.Time { return fixed }),
WithStarterIDGenerator(func() string { return "run-fixed" }),
WithStarterLaunch(func(fn func()) { fn() }), // run synchronously for determinism
)
specWithSelector := sampleSpec()
specWithSelector.Selector = backtest.InputSelector{
InstrumentIDs: []market.InstrumentID{"KR:005930"},
Symbols: []string{"005930"},
}
run, err := starter.StartBacktest(context.Background(), specWithSelector)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if run.ID != "run-fixed" {
t.Errorf("expected generated id run-fixed, got %q", run.ID)
}
if run.Status != backtest.RunStatusPending {
t.Errorf("expected pending status, got %q", run.Status)
}
if !run.CreatedAt.Equal(fixed) || !run.UpdatedAt.Equal(fixed) {
t.Errorf("expected timestamps %v, got created=%v updated=%v", fixed, run.CreatedAt, run.UpdatedAt)
}
// Pending run is persisted before execution so it is immediately listable.
if len(store.upsertCalls) != 1 || store.upsertCalls[0].Status != backtest.RunStatusPending {
t.Fatalf("expected one pending upsert, got %+v", store.upsertCalls)
}
// The launched job carries a decodable RunBacktestPayload for the same run.
if executedPayload == nil {
t.Fatal("expected the runner to be invoked with a payload")
}
decoded, err := DecodeRunBacktestPayload(executedPayload)
if err != nil {
t.Fatalf("payload should decode: %v", err)
}
if decoded.RunID != "run-fixed" {
t.Errorf("payload run id mismatch: %q", decoded.RunID)
}
if decoded.StrategyID != "strat-abc" || decoded.Market != "KR" || decoded.Timeframe != "1d" {
t.Errorf("payload spec mismatch: %+v", decoded)
}
if len(decoded.Selector.InstrumentIDs) != 1 || decoded.Selector.InstrumentIDs[0] != "KR:005930" {
t.Errorf("payload selector instrument_ids mismatch: %+v", decoded.Selector.InstrumentIDs)
}
if len(decoded.Selector.Symbols) != 1 || decoded.Selector.Symbols[0] != "005930" {
t.Errorf("payload selector symbols mismatch: %+v", decoded.Selector.Symbols)
}
}
func TestBacktestStarterPersistErrorStops(t *testing.T) {
runner := NewRunner()
launched := false
runner.Register(KindRunBacktest, func(ctx context.Context, payload json.RawMessage) error {
launched = true
return nil
})
store := &failingRunStore{err: errors.New("db down")}
starter := NewBacktestStarter(runner, store,
WithStarterLaunch(func(fn func()) { fn() }),
)
if _, err := starter.StartBacktest(context.Background(), sampleSpec()); err == nil {
t.Fatal("expected persist error to propagate")
}
if launched {
t.Error("execution must not launch when the pending run cannot be persisted")
}
}
type failingRunStore struct {
err error
}
func (f *failingRunStore) UpsertRun(ctx context.Context, run backtest.Run) error { return f.err }
func (f *failingRunStore) GetRun(ctx context.Context, id backtest.RunID) (backtest.Run, error) {
return backtest.Run{}, storage.ErrRunNotFound
}