package operator import ( "path/filepath" "strings" "testing" ) func fixture(t *testing.T, name string) string { t.Helper() return filepath.Join("..", "..", "testdata", "operator", name) } func TestLoadScenarioValidatesExample(t *testing.T) { sc, err := LoadScenario(fixture(t, "api_connection_smoke.yaml")) if err != nil { t.Fatalf("LoadScenario returned error: %v", err) } if sc.Name != "api_connection_smoke" { t.Errorf("name = %q, want api_connection_smoke", sc.Name) } if len(sc.Steps) != 1 { t.Fatalf("steps = %d, want 1", len(sc.Steps)) } if sc.Steps[0].Action != ActionHello { t.Errorf("step action = %q, want %q", sc.Steps[0].Action, ActionHello) } } func TestLoadScenarioRejectsUnknownAction(t *testing.T) { data := []byte("name: unknown_action\ntimeout: 1s\nsteps:\n - id: unknown\n action: send_unsupported_request\n expect:\n status: error\n") _, err := ParseScenario(data) if err == nil { t.Fatal("expected error for unknown action, got nil") } if !strings.Contains(err.Error(), "unknown action") { t.Errorf("error = %v, want it to mention unknown action", err) } } func TestLoadScenarioRejectsMissingStepID(t *testing.T) { data := []byte("name: missing_id\ntimeout: 1s\nsteps:\n - action: hello\n") _, err := ParseScenario(data) if err == nil { t.Fatal("expected error for missing step id, got nil") } if !strings.Contains(err.Error(), "id is required") { t.Errorf("error = %v, want it to mention id is required", err) } } func TestParseScenarioRejectsInvalidDuration(t *testing.T) { data := []byte("name: invalid_timeout\ntimeout: not-a-duration\nsteps:\n - id: hello\n action: hello\n") _, err := ParseScenario(data) if err == nil { t.Fatal("expected error for invalid timeout, got nil") } if !strings.Contains(err.Error(), "invalid timeout") { t.Errorf("error = %v, want it to mention invalid timeout", err) } } func TestValidateRejectsUnknownExpectStatus(t *testing.T) { data := []byte("name: bad_expect\ntimeout: 1s\nsteps:\n - id: hello\n action: hello\n expect:\n status: maybe\n") _, err := ParseScenario(data) if err == nil { t.Fatal("expected error for unknown expect.status, got nil") } if !strings.Contains(err.Error(), "expect.status") { t.Errorf("error = %v, want it to mention expect.status", err) } } func TestValidateRejectsErrorCodeWithoutErrorStatus(t *testing.T) { data := []byte("name: bad_code\ntimeout: 1s\nsteps:\n - id: hello\n action: hello\n expect:\n status: ok\n error_code: invalid_request\n") _, err := ParseScenario(data) if err == nil { t.Fatal("expected error for error_code without error status, got nil") } if !strings.Contains(err.Error(), "error_code") { t.Errorf("error = %v, want it to mention error_code", err) } } func TestInvalidRequestMatrixDryRunCoverage(t *testing.T) { tests := []struct { name string yaml string expectedErr string }{ { name: "missing_run_id", yaml: ` name: missing_run_id timeout: 1s steps: - id: step1 action: get_backtest_result request: {} `, expectedErr: "requires request.run_id", }, { name: "missing_strategy_id", yaml: ` name: missing_strategy_id timeout: 1s steps: - id: step1 action: start_backtest request: market: kr timeframe: daily from_unix_ms: 100 to_unix_ms: 200 `, expectedErr: "requires request.strategy_id", }, { name: "empty_compare_id", yaml: ` name: empty_compare_id timeout: 1s steps: - id: step1 action: compare_backtest_runs request: run_ids: [""] `, expectedErr: "cannot contain empty or blank values", }, { name: "invalid_market", yaml: ` name: invalid_market timeout: 1s steps: - id: step1 action: start_backtest request: strategy_id: strat1 market: invalid_market timeframe: daily from_unix_ms: 100 to_unix_ms: 200 `, expectedErr: "unsupported market", }, { name: "invalid_timeframe", yaml: ` name: invalid_timeframe timeout: 1s steps: - id: step1 action: start_backtest request: strategy_id: strat1 market: kr timeframe: invalid_timeframe from_unix_ms: 100 to_unix_ms: 200 `, expectedErr: "unsupported timeframe", }, { name: "invalid_date_range", yaml: ` name: invalid_date_range timeout: 1s steps: - id: step1 action: start_backtest request: strategy_id: strat1 market: kr timeframe: daily from_unix_ms: 200 to_unix_ms: 100 `, expectedErr: "cannot be after request.to_unix_ms", }, { name: "import_missing_provider", yaml: ` name: import_missing_provider timeout: 1s steps: - id: step1 action: import_daily_bars request: selector_kind: watchlist symbols: ["005930"] from_yyyymmdd: "20240527" to_yyyymmdd: "20240528" `, expectedErr: "requires request.provider", }, { name: "import_invalid_provider", yaml: ` name: import_invalid_provider timeout: 1s steps: - id: step1 action: import_daily_bars request: provider: invalid selector_kind: watchlist symbols: ["005930"] from_yyyymmdd: "20240527" to_yyyymmdd: "20240528" `, expectedErr: "unsupported provider", }, { name: "import_missing_selector_kind", yaml: ` name: import_missing_selector_kind timeout: 1s steps: - id: step1 action: import_daily_bars request: provider: kis symbols: ["005930"] from_yyyymmdd: "20240527" to_yyyymmdd: "20240528" `, expectedErr: "requires request.selector_kind", }, { name: "import_invalid_selector_kind", yaml: ` name: import_invalid_selector_kind timeout: 1s steps: - id: step1 action: import_daily_bars request: provider: kis selector_kind: invalid symbols: ["005930"] from_yyyymmdd: "20240527" to_yyyymmdd: "20240528" `, expectedErr: "unsupported selector_kind", }, { name: "import_missing_symbols", yaml: ` name: import_missing_symbols timeout: 1s steps: - id: step1 action: import_daily_bars request: provider: kis selector_kind: watchlist from_yyyymmdd: "20240527" to_yyyymmdd: "20240528" `, expectedErr: "requires request.symbols", }, { name: "import_empty_symbol", yaml: ` name: import_empty_symbol timeout: 1s steps: - id: step1 action: import_daily_bars request: provider: kis selector_kind: watchlist symbols: ["005930", " "] from_yyyymmdd: "20240527" to_yyyymmdd: "20240528" `, expectedErr: "symbols cannot contain empty or blank values", }, { name: "import_invalid_market", yaml: ` name: import_invalid_market timeout: 1s steps: - id: step1 action: import_daily_bars request: provider: kis selector_kind: watchlist symbols: ["005930"] market: invalid from_yyyymmdd: "20240527" to_yyyymmdd: "20240528" `, expectedErr: "unsupported market", }, { name: "import_invalid_venue", yaml: ` name: import_invalid_venue timeout: 1s steps: - id: step1 action: import_daily_bars request: provider: kis selector_kind: watchlist symbols: ["005930"] venue: invalid from_yyyymmdd: "20240527" to_yyyymmdd: "20240528" `, expectedErr: "unsupported venue", }, { name: "import_invalid_from_yyyymmdd", yaml: ` name: import_invalid_from_yyyymmdd timeout: 1s steps: - id: step1 action: import_daily_bars request: provider: kis selector_kind: watchlist symbols: ["005930"] from_yyyymmdd: "2024-05" to_yyyymmdd: "20240528" `, expectedErr: "must be in YYYYMMDD format", }, { name: "import_invalid_to_yyyymmdd", yaml: ` name: import_invalid_to_yyyymmdd timeout: 1s steps: - id: step1 action: import_daily_bars request: provider: kis selector_kind: watchlist symbols: ["005930"] from_yyyymmdd: "20240527" to_yyyymmdd: "20240532" `, expectedErr: "must be in YYYYMMDD format", }, { name: "import_reversed_dates", yaml: ` name: import_reversed_dates timeout: 1s steps: - id: step1 action: import_daily_bars request: provider: kis selector_kind: watchlist symbols: ["005930"] from_yyyymmdd: "20240528" to_yyyymmdd: "20240527" `, expectedErr: "cannot be after request.to_yyyymmdd", }, { name: "import_missing_from_yyyymmdd", yaml: ` name: import_missing_from_yyyymmdd timeout: 1s steps: - id: step1 action: import_daily_bars request: provider: kis selector_kind: watchlist symbols: ["005930"] to_yyyymmdd: "20240528" `, expectedErr: "requires request.from_yyyymmdd", }, { name: "import_missing_to_yyyymmdd", yaml: ` name: import_missing_to_yyyymmdd timeout: 1s steps: - id: step1 action: import_daily_bars request: provider: kis selector_kind: watchlist symbols: ["005930"] from_yyyymmdd: "20240527" `, expectedErr: "requires request.to_yyyymmdd", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := ParseScenario([]byte(tt.yaml)) if err == nil { t.Fatalf("expected error containing %q, got nil", tt.expectedErr) } if !strings.Contains(err.Error(), tt.expectedErr) { t.Errorf("error = %v, want it to contain %q", err, tt.expectedErr) } }) } } func TestValidateImportDailyBars(t *testing.T) { yamlText := ` name: valid_import timeout: 5s steps: - id: step1 action: import_daily_bars request: provider: kis selector_kind: watchlist market: kr venue: krx symbols: ["005930"] from_yyyymmdd: "20240527" to_yyyymmdd: "20240528" expect: status: ok ` sc, err := ParseScenario([]byte(yamlText)) if err != nil { t.Fatalf("unexpected error parsing valid import scenario: %v", err) } if len(sc.Steps) != 1 { t.Fatalf("steps = %d, want 1", len(sc.Steps)) } step := sc.Steps[0] if step.Action != ActionImportDailyBars { t.Errorf("action = %q, want %q", step.Action, ActionImportDailyBars) } if step.Request.Provider != "kis" { t.Errorf("provider = %q, want kis", step.Request.Provider) } if step.Request.SelectorKind != "watchlist" { t.Errorf("selector_kind = %q, want watchlist", step.Request.SelectorKind) } if step.Request.Market != "kr" { t.Errorf("market = %q, want kr", step.Request.Market) } if step.Request.Venue != "krx" { t.Errorf("venue = %q, want krx", step.Request.Venue) } if len(step.Request.Symbols) != 1 || step.Request.Symbols[0] != "005930" { t.Errorf("symbols = %v, want [005930]", step.Request.Symbols) } if step.Request.FromYYYYMMDD != "20240527" { t.Errorf("from_yyyymmdd = %q, want 20240527", step.Request.FromYYYYMMDD) } if step.Request.ToYYYYMMDD != "20240528" { t.Errorf("to_yyyymmdd = %q, want 20240528", step.Request.ToYYYYMMDD) } }