package options_test import ( "context" "encoding/json" "sort" "testing" "time" decimal "github.com/alpacahq/alpacadecimal" "github.com/google/go-cmp/cmp" "s1d3sw1ped/robinhood-agentic-mcp/client" "s1d3sw1ped/robinhood-agentic-mcp/internal/rhntest" "s1d3sw1ped/robinhood-agentic-mcp/options" ) func TestOptions_toolNames(t *testing.T) { t.Parallel() qty := decimal.NewFromInt(1) px := decimal.RequireFromString("1.5") stop := decimal.RequireFromString("1.4") start := time.Date(2026, 8, 18, 13, 30, 0, 0, time.UTC) end := time.Date(2026, 8, 18, 20, 0, 0, 0, time.UTC) leg := options.Leg{ OptionID: "opt-1", Side: client.Buy, PositionEffect: "open", RatioQuantity: 1, } legsWire := []map[string]any{ {"option_id": "opt-1", "side": "buy", "position_effect": "open", "ratio_quantity": 1}, } tests := []struct { name string call func(*options.Client) error wantName string wantArgs map[string]any }{ { name: "Chains", call: func(c *options.Client) error { _, err := c.Chains(context.Background(), options.ChainsRequest{ IDs: "chain-1", UnderlyingSymbol: "AAPL", }) return err }, wantName: "get_option_chains", wantArgs: map[string]any{"ids": "chain-1", "underlying_symbol": "AAPL"}, }, { name: "Instruments", call: func(c *options.Client) error { _, err := c.Instruments(context.Background(), options.InstrumentsRequest{ ChainID: "chain-1", ChainSymbol: "AAPL", ExpirationDates: "2026-09-18", StrikePrice: "150.0000", Type: "call", State: "active", Tradability: "tradable", IDs: "opt-1", Cursor: "c1", }) return err }, wantName: "get_option_instruments", wantArgs: map[string]any{ "chain_id": "chain-1", "chain_symbol": "AAPL", "expiration_dates": "2026-09-18", "strike_price": "150.0000", "type": "call", "state": "active", "tradability": "tradable", "ids": "opt-1", "cursor": "c1", }, }, { name: "Quotes", call: func(c *options.Client) error { _, err := c.Quotes(context.Background(), options.QuotesRequest{ InstrumentIDs: []string{"opt-1", "opt-2"}, }) return err }, wantName: "get_option_quotes", wantArgs: map[string]any{"instrument_ids": []string{"opt-1", "opt-2"}}, }, { name: "Positions", call: func(c *options.Client) error { _, err := c.Positions(context.Background(), options.PositionsRequest{ AccountNumber: "acct-1", Nonzero: true, ChainIDs: "chain-1", OptionIDs: "opt-1", Type: "long", OptionType: "call", ExpirationDate: "2026-09-18", ExpirationDateLTE: "2026-12-31", ExpirationDateGTE: "2026-01-01", Cursor: "c1", }) return err }, wantName: "get_option_positions", wantArgs: map[string]any{ "account_number": "acct-1", "nonzero": true, "chain_ids": "chain-1", "option_ids": "opt-1", "type": "long", "option_type": "call", "expiration_date": "2026-09-18", "expiration_date_lte": "2026-12-31", "expiration_date_gte": "2026-01-01", "cursor": "c1", }, }, { name: "Orders", call: func(c *options.Client) error { _, err := c.Orders(context.Background(), options.OrdersRequest{ AccountNumber: "acct-1", OrderID: "o1", State: "filled", CreatedAtGTE: "2026-08-18", ChainIDs: "chain-1", UnderlyingType: "equity", PlacedAgent: "agentic", Cursor: "c1", }) return err }, wantName: "get_option_orders", wantArgs: map[string]any{ "account_number": "acct-1", "order_id": "o1", "state": "filled", "created_at_gte": "2026-08-18", "chain_ids": "chain-1", "underlying_type": "equity", "placed_agent": "agentic", "cursor": "c1", }, }, { name: "Historicals", call: func(c *options.Client) error { _, err := c.Historicals(context.Background(), options.HistoricalsRequest{ InstrumentIDs: []string{"opt-1"}, StartTime: start, EndTime: end, Interval: "minute", Bounds: "regular", }) return err }, wantName: "get_option_historicals", wantArgs: map[string]any{ "instrument_ids": []string{"opt-1"}, "start_time": "2026-08-18T13:30:00Z", "end_time": "2026-08-18T20:00:00Z", "interval": "minute", "bounds": "regular", }, }, { name: "ReviewOrder", call: func(c *options.Client) error { _, err := c.ReviewOrder(context.Background(), options.PlaceOrderRequest{ AccountNumber: "acct", Legs: []options.Leg{leg}, Type: client.Limit, Quantity: &qty, Price: &px, TimeInForce: client.GFD, MarketHours: client.RegularHours, ChainSymbol: "AAPL", UnderlyingType: "equity", RefID: "buy:opt", }) return err }, wantName: "review_option_order", wantArgs: map[string]any{ "account_number": "acct", "legs": legsWire, "type": "limit", "quantity": "1", "price": "1.5", "time_in_force": "gfd", "market_hours": "regular_hours", "chain_symbol": "AAPL", "underlying_type": "equity", }, }, { name: "PlaceOrder", call: func(c *options.Client) error { _, err := c.PlaceOrder(context.Background(), options.PlaceOrderRequest{ AccountNumber: "acct", Legs: []options.Leg{leg}, Direction: "debit", Type: client.StopLimit, Quantity: &qty, Price: &px, StopPrice: &stop, TimeInForce: client.GTC, MarketHours: client.RegularCurbHours, RefID: "buy:opt", }) return err }, wantName: "place_option_order", wantArgs: map[string]any{ "account_number": "acct", "legs": legsWire, "direction": "debit", "type": "stop_limit", "quantity": "1", "price": "1.5", "stop_price": "1.4", "time_in_force": "gtc", "market_hours": "regular_curb_hours", "ref_id": "buy:opt", }, }, { name: "CancelOrder", call: func(c *options.Client) error { return c.CancelOrder(context.Background(), options.CancelOrderRequest{ AccountNumber: "acct", OrderID: "o1", }) }, wantName: "cancel_option_order", wantArgs: map[string]any{"account_number": "acct", "order_id": "o1"}, }, { name: "ReplaceOrder", call: func(c *options.Client) error { _, err := c.ReplaceOrder(context.Background(), options.ReplaceOrderRequest{ AccountNumber: "acct", OrderID: "o1", Legs: []options.Leg{leg}, Type: client.Limit, Quantity: &qty, Price: &px, RefID: "repl:opt", }) return err }, wantName: "replace_option_order", wantArgs: map[string]any{ "account_number": "acct", "order_id": "o1", "legs": legsWire, "type": "limit", "quantity": "1", "price": "1.5", "ref_id": "repl:opt", }, }, { name: "Exercise", call: func(c *options.Client) error { _, err := c.Exercise(context.Background(), options.ExerciseRequest{ AccountNumber: "acct", OptionID: "opt-1", Quantity: 2, RefID: "ex:opt", Reason: "buying_stocks", AllowShorts: true, }) return err }, wantName: "exercise_option", wantArgs: map[string]any{ "account_number": "acct", "option_id": "opt-1", "quantity": 2, "ref_id": "ex:opt", "reason": "buying_stocks", "allow_shorts": true, }, }, { name: "CancelExercise", call: func(c *options.Client) error { return c.CancelExercise(context.Background(), options.CancelExerciseRequest{ AccountNumber: "acct", OptionID: "opt-1", }) }, wantName: "cancel_option_exercise", wantArgs: map[string]any{"account_number": "acct", "option_id": "opt-1"}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() var gotName string var gotArgs map[string]any c := options.New(client.Func(func(ctx context.Context, name string, args map[string]any) (json.RawMessage, error) { gotName, gotArgs = name, args return json.RawMessage(`{"id":"o1"}`), nil })) if err := tc.call(c); err != nil { t.Fatal(err) } if gotName != tc.wantName { t.Fatalf("%s %+v", gotName, gotArgs) } if diff := cmp.Diff(tc.wantArgs, gotArgs); diff != "" { t.Fatal(diff) } if tc.name == "ReviewOrder" { if _, ok := gotArgs["ref_id"]; ok { t.Fatalf("ref_id on review: %+v", gotArgs) } } if tc.name == "PlaceOrder" { if _, ok := gotArgs["idempotency_key"]; ok { t.Fatalf("idempotency_key on place: %+v", gotArgs) } if _, ok := gotArgs["chain_symbol"]; ok { t.Fatalf("chain_symbol on place: %+v", gotArgs) } } }) } } func TestTools(t *testing.T) { t.Parallel() want := []string{ "cancel_option_exercise", "cancel_option_order", "exercise_option", "get_option_chains", "get_option_historicals", "get_option_instruments", "get_option_orders", "get_option_positions", "get_option_quotes", "place_option_order", "replace_option_order", "review_option_order", } got := append([]string(nil), options.Tools()...) sort.Strings(got) if diff := cmp.Diff(want, got); diff != "" { t.Fatal(diff) } } func TestQuotes_rhntest(t *testing.T) { t.Parallel() s := rhntest.New(t) s.Set("get_option_quotes", json.RawMessage(`{}`)) c := options.New(&client.Client{URL: s.URL}) _, err := c.Quotes(context.Background(), options.QuotesRequest{ InstrumentIDs: []string{"opt-1", "opt-2"}, }) if err != nil { t.Fatal(err) } if s.LastName() != "get_option_quotes" { t.Fatalf("%s", s.LastName()) } want := map[string]any{"instrument_ids": []any{"opt-1", "opt-2"}} if diff := cmp.Diff(want, s.LastArgs()); diff != "" { t.Fatal(diff) } } func TestPlaceOrder_rhntest(t *testing.T) { t.Parallel() s := rhntest.New(t) s.Set("place_option_order", json.RawMessage(`{"id":"o1"}`)) c := options.New(&client.Client{URL: s.URL}) qty := decimal.NewFromInt(1) px := decimal.RequireFromString("1.5") got, err := c.PlaceOrder(context.Background(), options.PlaceOrderRequest{ AccountNumber: "acct", Legs: []options.Leg{{ OptionID: "opt-1", Side: client.Buy, PositionEffect: "open", RatioQuantity: 1, }}, Type: client.Limit, Quantity: &qty, Price: &px, RefID: "buy:opt", }) if err != nil { t.Fatal(err) } if got.ID != "o1" { t.Fatalf("%+v", got) } if s.LastName() != "place_option_order" { t.Fatalf("%s", s.LastName()) } wantLegs := []any{ map[string]any{ "option_id": "opt-1", "side": "buy", "position_effect": "open", "ratio_quantity": float64(1), }, } if diff := cmp.Diff(wantLegs, s.LastArgs()["legs"]); diff != "" { t.Fatal(diff) } }