@@ -965,3 +965,149 @@ def test_cash_out_invalid_state_error(self, tcp_client: socket.socket) -> None:
965965 ["current_state" ],
966966 ErrorCode .INVALID_GAME_STATE .value ,
967967 )
968+
969+
970+ class TestRearrangeHand :
971+ """Tests for the rearrange_hand API endpoint."""
972+
973+ @pytest .fixture (autouse = True )
974+ def setup_and_teardown (
975+ self , tcp_client : socket .socket
976+ ) -> Generator [dict , None , None ]:
977+ """Start a run, reach SELECTING_HAND phase, yield initial state, then clean up."""
978+ # Begin a run and select the first blind to obtain an initial hand
979+ send_and_receive_api_message (
980+ tcp_client ,
981+ "start_run" ,
982+ {
983+ "deck" : "Red Deck" ,
984+ "stake" : 1 ,
985+ "challenge" : None ,
986+ "seed" : "TESTSEED" ,
987+ },
988+ )
989+ game_state = send_and_receive_api_message (
990+ tcp_client , "skip_or_select_blind" , {"action" : "select" }
991+ )
992+ assert game_state ["state" ] == State .SELECTING_HAND .value
993+ yield game_state
994+ send_and_receive_api_message (tcp_client , "go_to_menu" , {})
995+
996+ # ------------------------------------------------------------------
997+ # Success scenario
998+ # ------------------------------------------------------------------
999+
1000+ def test_rearrange_hand_success (
1001+ self , tcp_client : socket .socket , setup_and_teardown : dict
1002+ ) -> None :
1003+ """Reverse the hand order and verify the API response reflects it."""
1004+ initial_state = setup_and_teardown
1005+ initial_cards = initial_state ["hand" ]["cards" ]
1006+ hand_size : int = len (initial_cards )
1007+
1008+ # Reverse order indices (API expects zero-based indices)
1009+ new_order = list (range (hand_size - 1 , - 1 , - 1 ))
1010+
1011+ final_state = send_and_receive_api_message (
1012+ tcp_client ,
1013+ "rearrange_hand" ,
1014+ {"action" : "rearrange_hand" , "cards" : new_order },
1015+ )
1016+
1017+ # Ensure we remain in selecting hand state
1018+ assert final_state ["state" ] == State .SELECTING_HAND .value
1019+
1020+ # Compare card_key ordering to make sure it's reversed
1021+ initial_keys = [card ["config" ]["card_key" ] for card in initial_cards ]
1022+ final_keys = [
1023+ card ["config" ]["card_key" ] for card in final_state ["hand" ]["cards" ]
1024+ ]
1025+ assert final_keys == list (reversed (initial_keys ))
1026+
1027+ def test_rearrange_hand_noop (
1028+ self , tcp_client : socket .socket , setup_and_teardown : dict
1029+ ) -> None :
1030+ """Sending indices in current order should leave the hand unchanged."""
1031+ initial_state = setup_and_teardown
1032+ initial_cards = initial_state ["hand" ]["cards" ]
1033+ hand_size : int = len (initial_cards )
1034+
1035+ # Existing order indices (0-based)
1036+ current_order = list (range (hand_size ))
1037+
1038+ final_state = send_and_receive_api_message (
1039+ tcp_client ,
1040+ "rearrange_hand" ,
1041+ {"action" : "rearrange_hand" , "cards" : current_order },
1042+ )
1043+
1044+ assert final_state ["state" ] == State .SELECTING_HAND .value
1045+
1046+ initial_keys = [card ["config" ]["card_key" ] for card in initial_cards ]
1047+ final_keys = [
1048+ card ["config" ]["card_key" ] for card in final_state ["hand" ]["cards" ]
1049+ ]
1050+ assert final_keys == initial_keys
1051+
1052+ # ------------------------------------------------------------------
1053+ # Validation / error scenarios
1054+ # ------------------------------------------------------------------
1055+
1056+ def test_rearrange_hand_invalid_number_of_cards (
1057+ self , tcp_client : socket .socket , setup_and_teardown : dict
1058+ ) -> None :
1059+ """Providing an index list with the wrong length should error."""
1060+ hand_size = len (setup_and_teardown ["hand" ]["cards" ])
1061+ invalid_order = list (range (hand_size - 1 )) # one short
1062+
1063+ response = send_and_receive_api_message (
1064+ tcp_client ,
1065+ "rearrange_hand" ,
1066+ {"action" : "rearrange_hand" , "cards" : invalid_order },
1067+ )
1068+
1069+ assert_error_response (
1070+ response ,
1071+ "Invalid number of cards to rearrange" ,
1072+ ["cards_count" , "valid_range" ],
1073+ ErrorCode .PARAMETER_OUT_OF_RANGE .value ,
1074+ )
1075+
1076+ def test_rearrange_hand_out_of_range_index (
1077+ self , tcp_client : socket .socket , setup_and_teardown : dict
1078+ ) -> None :
1079+ """Including an index >= hand size should error."""
1080+ hand_size = len (setup_and_teardown ["hand" ]["cards" ])
1081+ order = list (range (hand_size ))
1082+ order [- 1 ] = hand_size # out-of-range zero-based index
1083+
1084+ response = send_and_receive_api_message (
1085+ tcp_client ,
1086+ "rearrange_hand" ,
1087+ {"action" : "rearrange_hand" , "cards" : order },
1088+ )
1089+
1090+ assert_error_response (
1091+ response ,
1092+ "Card index out of range" ,
1093+ ["index" , "max_index" ],
1094+ ErrorCode .PARAMETER_OUT_OF_RANGE .value ,
1095+ )
1096+
1097+ def test_rearrange_hand_invalid_state (self , tcp_client : socket .socket ) -> None :
1098+ """Calling rearrange_hand outside of SELECTING_HAND should error."""
1099+ # Ensure we're in MENU state
1100+ send_and_receive_api_message (tcp_client , "go_to_menu" , {})
1101+
1102+ response = send_and_receive_api_message (
1103+ tcp_client ,
1104+ "rearrange_hand" ,
1105+ {"action" : "rearrange_hand" , "cards" : [0 ]},
1106+ )
1107+
1108+ assert_error_response (
1109+ response ,
1110+ "Cannot rearrange hand when not selecting hand" ,
1111+ ["current_state" ],
1112+ ErrorCode .INVALID_GAME_STATE .value ,
1113+ )
0 commit comments