alt/services/worker/internal/jobs/backtest_starter_test.go
toki ec938475e0 chore: socket rail implementation and task archival
- Add backtest and market socket handlers for API and Worker
- Add proto definitions for backtest and market services
- Update client socket integration and tests
- Generate protobuf code for Go and Dart
- Archive completed agent tasks under agent-task/archive/2026/05/
2026-05-30 22:57:18 +09:00

106 lines
3.3 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
)
run, err := starter.StartBacktest(context.Background(), sampleSpec())
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)
}
}
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
}