alt/apps/cli/internal/operator/scenario_test.go
toki 473934212c refactor: operator workflow G07 completed and archived
- Move G07 import CLI scenario docs to archive
- Update operator client, runner, scenario implementations
- Add kis_daily_import_smoke test fixtures
- Update handoff and output tests
2026-06-03 21:26:06 +09:00

465 lines
11 KiB
Go

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