
    [?i4                     ~   d Z ddlZddlmc mZ ddlZddlm	Z	m
Z
mZ ddlZddlmZ ddlmZmZmZmZmZmZ  ej        ddddd	gid
                                          Zedfdedede	fdZdefdZdefdZdefdZd Z ej!        j"        d&d            Z#ej!        j"        d&d            Z$ej!        j"        d&d            Z%ej!        j"        d&d            Z&ej!        j"        d&d            Z'ej!        j"        d&d            Z(d&dZ)d&dZ*d&dZ+d&dZ,d&dZ-d&dZ.d&dZ/d&d Z0d&d!Z1d&d"Z2d&d#Z3d&d$Z4d&d%Z5dS )'z.Tests for openclaw-proxy WebSocket middleware.    N)	AsyncMock	MagicMockpatch)
TestClient)SessionStateappprocess_sttcall_openclawprocess_ttsMAX_AUDIO_BUFFER_BYTESokpayloads   你好！我在这儿。)textmediaUrlstatusresultstdout
returncodereturnc                 L    t                      }| df|j        _        ||_        |S )z7Build a mock asyncio.Process with preset stdout/stderr.    )r   communicatereturn_valuer   )r   r   procs      0/root/projects/openclaw-proxy/tests/test_main.py_make_mock_processr      s(    
 ;;D%+SMD! DOKr   c                  0    t          j        ddd          S )Nhardware_eventkey_downtypeactionjsondumps r   r   _key_down_msgr)   (   s    :/:FFGGGr   c                  0    t          j        ddd          S )Nr    key_upr"   r%   r(   r   r   _key_up_msgr,   ,   s    :/8DDEEEr   c                  0    t          j        ddd          S )Nr    	wake_wordr"   r%   r(   r   r   _wake_word_msgr/   0   s    :/;GGHHHr   c                      t          dt          d          t          dt          d          t          dt          d          fS )z<Context manager that patches STT/LLM/TTS pipeline functions.zmain.process_sttu   测试语音输入)new_callabler   zmain.call_openclawr   zmain.process_tts   )r   r   r(   r   r   _pipeline_patchesr3   4   sc     	"-	
 	
 	

 	 "3	
 	
 	

 	"(	
 	
 	
 r   c                    K   t                      } ddi| j        _        t          d|           5  t	          d           d{V }ddd           n# 1 swxY w Y   d}||k    }|st          j        d|fd	||f          d
t          j                    v st          j	        |          rt          j
        |          nd
t          j
        |          dz  }dd|iz  }t          t          j        |                    dx}}| j                                         | j        j        \  }}|d         }|j        }d}	 ||	          }
|
ssdt          j
        |          t          j
        |          t          j
        |	          t          j
        |
          dz  }t          t          j        |                    dx}x}x}	}
ddi}||k    }|st          j        d|fd	||f          dt          j                    v st          j	        |          rt          j
        |          ndt          j
        |          dz  }dd|iz  }t          t          j        |                    dx}}dS )zLSTT should write audio to temp file, call Whisper, and return stripped text.r   u    你好世界 zmain._get_whisper_modelr   P                                                                                   Nu   你好世界==z%(py0)s == %(py3)sr   py0py3assert %(py5)spy5r   z.opuszJassert %(py7)s
{%(py7)s = %(py3)s
{%(py3)s = %(py1)s.endswith
}(%(py5)s)
})py1r<   r>   py7languagezhkwargs)r   
transcriber   r   r	   
@pytest_ar_call_reprcompare@py_builtinslocals_should_repr_global_name	_safereprAssertionError_format_explanationassert_called_once	call_argsendswith)
mock_modelr   @py_assert2@py_assert1@py_format4@py_format6argsrC   @py_assert0@py_assert4@py_assert6@py_format8s               r   test_process_stt_calls_whisperrZ   L   s
      J*02B)CJ&	(z	B	B	B 1 1"<000000001 1 1 1 1 1 1 1 1 1 1 1 1 1 1##6^##########6^############6#####6######^#########################,,...(2LD&7$7$G$G$$$$$$$$7$$$$$$$$G$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $''6'''''''''''6'''''''''''''6'''''6''''''''''''''''''''''''''''''''''s   AAAc                    K   t                      } t          d|           5  t          dd           d{V }ddd           n# 1 swxY w Y   d}||k    }|st          j        d|fd||f          d	t          j                    v st          j        |          rt          j        |          nd	t          j        |          d
z  }dd|iz  }t          t          j
        |                    dx}}dS )z>call_openclaw should parse the CLI JSON and return reply text.#main.asyncio.create_subprocess_execr5      你好test-sessionNr   r7   r9   r   r:   r=   r>   )r   r   r
   rE   rF   rG   rH   rI   rJ   rK   rL   )	mock_procr   rQ   rR   rS   rT   s         r   $test_call_openclaw_parses_cli_outputr`   Z   sh      #$$I	49	M	M	M ? ?$X~>>>>>>>>? ? ? ? ? ? ? ? ? ? ? ? ? ? ?//6///////////6/////////////6/////6//////////////////////////////////s   AA	A	c                  *  K   t          dd          } d| j        _        t          d|           5  t	          j        t          d          5  t          d	d
           d{V  ddd           n# 1 swxY w Y   ddd           dS # 1 swxY w Y   dS )z@call_openclaw should raise RuntimeError when CLI exits non-zero.r      )r   r   )r   s
   some errorr\   r5   zOpenClaw CLI errormatchr]   r^   N)r   r   r   r   pytestraisesRuntimeErrorr
   )r_   s    r   (test_call_openclaw_raises_on_cli_failurerh   c   s*      ##!<<<I)=I&	49	M	M	M : :]</CDDD 	: 	:.999999999	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	:: : : : : : : : : : : : : : : : : :s5   BA0$B0A4	4B7A4	8BBBc                  f  K   t          j        ddg id                                          } t          |           }t	          d|          5  t          j        t          d          5  t          d	d
           d{V  ddd           n# 1 swxY w Y   ddd           dS # 1 swxY w Y   dS )z:call_openclaw should raise RuntimeError on empty payloads.r   r   r   )r   r\   r5   zempty payloadsrc   r]   r^   N)	r&   r'   encoder   r   re   rf   rg   r
   )emptyr_   s     r   +test_call_openclaw_raises_on_empty_payloadsrl   m   sE      J$:r2BCCDDKKMME"%000I	49	M	M	M : :]</?@@@ 	: 	:.999999999	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	:: : : : : : : : : : : : : : : : : :s6   B&+BB&B	B&B	B&&B*-B*c                    K   dd} t                      }| |_        t                      }d|j        _        d|_        t          d|          5  t          d|          5  t          d	           d{V }ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   d
}||k    }|st          j	        d|fd||f          dt          j                    v st          j        |          rt          j        |          ndt          j        |          dz  }dd|iz  }t          t          j        |                    dx}}|j                            d           dS )zATTS should collect MP3 from edge-tts, convert to Opus via ffmpeg.r   Nc                 :   K   dddW V  dddW V  dd dW V  d S )Naudios	   mp3-part1r#   datas	   mp3-part2WordBoundaryr(   r(   r   r   mock_streamz8test_process_tts_returns_opus_bytes.<locals>.mock_stream{   sQ      555555555555%t44444444r   )	   fake-opusr   r   main.edge_tts.Communicater5   r\      测试文本rt   r7   r9   r   r:   r=   r>   s   mp3-part1mp3-part2)inputr   N)r   streamr   r   r   r   r   r   rE   rF   rG   rH   rI   rJ   rK   rL   assert_called_once_with)rs   	mock_commr_   r   rQ   rR   rS   rT   s           r   #test_process_tts_returns_opus_bytesr|   w   s#     5 5 5 5
 I"II)<I&I 	)	BBB3 33)LLL3 3 #>22222222	3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 "!6\!!!!!!!!!!6\!!!!!!!!!!!!6!!!!!6!!!!!!\!!!!!!!!!!!!!!!!!!!!!!!!!118M1NNNNNs6   B B6BB	B	B	
BB Bc                    K   dd} t                      }| |_        t                      }d|j        _        d|_        t          d|          5  t          d|          5  t          j        t          d	
          5  t          d           d{V  ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   ddd           dS # 1 swxY w Y   dS )z9TTS should raise RuntimeError when ffmpeg exits non-zero.r   Nc                    K   dddW V  d S )Nro   s   mp3-datarp   r(   r(   r   r   rs   z>test_process_tts_raises_on_ffmpeg_failure.<locals>.mock_stream   s#      44444444r   )r   s   ffmpeg error msgrb   ru   r5   r\   zffmpeg TTS conversion errorrc   rv   rx   )r   ry   r   r   r   r   r   re   rf   rg   r   )rs   r{   r_   s      r   )test_process_tts_raises_on_ffmpeg_failurer      s     5 5 5 5 I"II)CI&I 	)	BBB. .3)LLL. . ]</LMMM 	. 	.n---------	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	.	. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .sZ   C B5<BB5B""B5%B"&B5)C5B9	9C<B9	=CCCc                     t                      } | j        }d}||k    }|st          j        d|fd||f          dt	          j                    v st          j        |           rt          j        |           ndt          j        |          t          j        |          dz  }dd|iz  }t          t          j	        |                    dx}x}}| j
        }g }||k    }|st          j        d|fd	||f          dt	          j                    v st          j        |           rt          j        |           ndt          j        |          t          j        |          dz  }dd|iz  }t          t          j	        |                    dx}x}}| j        } |            }d
}||k    }|st          j        d|fd||f          dt	          j                    v st          j        |           rt          j        |           ndt          j        |          t          j        |          t          j        |          dz  }dd|iz  }t          t          j	        |                    dx}x}x}}dS )z7SessionState should start idle with empty audio_chunks.idler7   )z.%(py2)s
{%(py2)s = %(py0)s.status
} == %(py5)sstater;   py2r>   assert %(py7)sr@   N)z4%(py2)s
{%(py2)s = %(py0)s.audio_chunks
} == %(py5)sr   )zJ%(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.buffer_size
}()
} == %(py7)sr;   r   py4r@   assert %(py9)spy9)r   r   rE   rF   rG   rH   rI   rJ   rK   rL   audio_chunksbuffer_size)	r   rR   rW   @py_assert3rT   rY   rX   @py_assert5@py_format10s	            r   test_session_state_defaultsr      s   NNE<!6!<6!!!!!!!!!!<6!!!!!!!!!!!!5!!!!!5!!!!!!<!!!!6!!!!!!!!!!!!!!!!!!!!!!!!!!!########################5#####5#######################################!#!##########!############5#####5##############!###############################r   c                     t          t                    } |                     d          }|j        }d}||k    }|st	          j        d|fd||f          dt          j                    v st	          j        |          rt	          j	        |          ndt	          j	        |          t	          j	        |          dz  }dd|iz  }t          t	          j        |                    d	x}x}}|j        } |            }d
di}||k    }|st	          j        d|fd||f          dt          j                    v st	          j        |          rt	          j	        |          ndt	          j	        |          t	          j	        |          t	          j	        |          dz  }dd|iz  }	t          t	          j        |	                    d	x}x}x}}d	S )z-GET /health should return 200 with ok status.z/health   r7   )z3%(py2)s
{%(py2)s = %(py0)s.status_code
} == %(py5)srespr   r   r@   Nr   r   )zC%(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.json
}()
} == %(py7)sr   r   r   )r   r   getstatus_coderE   rF   rG   rH   rI   rJ   rK   rL   r&   )
clientr   rR   rW   r   rT   rY   rX   r   r   s
             r   test_health_endpointr      s	   __F::i  D"s"s""""""""""s""""""""""""4"""""4""""""""""s"""""""""""""""""""""""""""9*99;;*8T**;***********;*************4*****4******9****;************************************r   c                      t          t                    } |                     d          5  	 ddd           dS # 1 swxY w Y   dS )zAClient should be able to connect to /chat and cleanly disconnect./chatN)r   r   websocket_connect)r   s    r   %test_websocket_connect_and_disconnectr      s    __F		!	!'	*	*                   s   9= =c                  (   t          t                    } |                     d          5 }|                    t	                                 t          j        |                                          }|d         }d}||k    }|slt          j	        d|fd||f          t          j
        |          t          j
        |          dz  }dd|iz  }t          t          j        |                    d	x}x}}|d
         }d}||k    }|slt          j	        d|fd||f          t          j
        |          t          j
        |          dz  }dd|iz  }t          t          j        |                    d	x}x}}d	d	d	           d	S # 1 swxY w Y   d	S )z=Sending a wake_word JSON should get back a 'listening' state.r   r#   r   r7   z%(py1)s == %(py4)sr?   r   assert %(py6)spy6Nr   	listeningr   r   r   	send_textr/   r&   loadsreceive_textrE   rF   rJ   rK   rL   r   wsr   rV   r   rQ   @py_format5@py_format7s           r   #test_websocket_text_frame_wake_wordr      s   __F		!	!'	*	* -b
^%%&&&z"//++,,F|&w&|w&&&&&&&&&&|w&&&&&|&&&&w&&&&&&&&&&&&&&&&&&&&&&&&&&&H~,,~,,,,,,,,,,~,,,,,~,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,	- - - - - - - - - - - - - - - - - -s   EFFFc                  J   t          t                    } |                     d          5 }|                    d           |                    t	                                 t          j        |                                          }|d         }d}||k    }|slt          j	        d|fd||f          t          j
        |          t          j
        |          dz  }dd	|iz  }t          t          j        |                    d
x}x}}d
d
d
           d
S # 1 swxY w Y   d
S )z/Malformed JSON should not crash the connection.r   znot-valid-json{{{r   r   r7   r   r   r   r   Nr   r   s           r   (test_websocket_text_frame_malformed_jsonr      sk   __F		!	!'	*	* -b
()))
^%%&&&z"//++,,H~,,~,,,,,,,,,,~,,,,,~,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,	- - - - - - - - - - - - - - - - - -s   C!DDDc                  h   t          t                    } |                     d          5 }|                    t	                                 t          j        |                                          }ddd}||k    }|st          j	        d|fd||f          dt          j                    v st          j        |          rt          j        |          ndt          j        |          dz  }d	d
|iz  }t          t          j        |                    dx}}ddd           dS # 1 swxY w Y   dS )zFkey_down should transition to listening and return state notification.r   r   r   r#   r   r7   r9   r   r:   r=   r>   N)r   r   r   r   r)   r&   r   r   rE   rF   rG   rH   rI   rJ   rK   rL   )r   r   r   rQ   rR   rS   rT   s          r   test_key_down_starts_listeningr      s   __F		!	!'	*	* @b
]__%%%z"//++,, ';???t???????????t?????????????t?????t????????????????????????????????@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @s   C0D''D+.D+c                     t                      \  } }}t          t                    }| 5  |5  |5  |                    d          5 }|                    t                                 |                                 |                    d           |                    t                                 t          j
        |                                          }ddd}||k    }|st          j        d|fd||f          dt          j                    v st          j        |          rt          j        |          ndt          j        |          d	z  }d
d|iz  }	t#          t          j        |	                    dx}}t          j
        |                                          }
|
d         }d}||k    }|slt          j        d|fd||f          t          j        |          t          j        |          dz  }dd|iz  }t#          t          j        |                    dx}x}}|
d         }d}||k    }|slt          j        d|fd||f          t          j        |          t          j        |          dz  }dd|iz  }t#          t          j        |                    dx}x}}|                                }d}||k    }|st          j        d|fd||f          dt          j                    v st          j        |          rt          j        |          ndt          j        |          d	z  }d
d|iz  }	t#          t          j        |	                    dx}}ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   ddd           dS # 1 swxY w Y   dS )uL   key_down → binary → key_up should trigger full STT→LLM→TTS pipeline.r   r6   r   thinkingr   r7   r9   msg1r:   r=   r>   Nr#   
text_replyr   r   r   r   r   r   r2   ro   r3   r   r   r   r   r)   r   
send_bytesr,   r&   r   rE   rF   rG   rH   rI   rJ   rK   rL   receive_bytesp_sttp_llmp_ttsr   r   r   rQ   rR   rS   rT   msg2rV   r   r   r   ro   s                   r   test_key_up_triggers_pipeliner      s   +--E5%__F	 , , , ,u , ,%%g.. 	,"LL)))OOMM,'''LL''' :boo//00D$+zBBB4BBBBBBBBBBB4BBBBBBBBBBBBB4BBBBB4BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB :boo//00D</</<<//////////<</////<////<///////////////////////////<=#==<#===========<#======<====#============================ $$&&E++5O++++++++++5O++++++++++++5+++++5++++++O+++++++++++++++++++++++++'	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	,, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,|   N8N N	L!M2	&N	2M66N	9M6:N	=N 	NN NN N8 N$	$N8'N$	(N88N<?N<c                     t                      \  } }}t          t                    }| 5 }|5  |5  |                    d          5 }|                    t                                 |                                 |                    t                                 |                                 |                    t                                 t          j        |                                          }|d         }d}||k    }	|	slt          j        d|	fd||f          t          j        |          t          j        |          dz  }
dd|
iz  }t          t          j        |                    d	x}x}	}d	d	d	           n# 1 swxY w Y   d	d	d	           n# 1 swxY w Y   d	d	d	           n# 1 swxY w Y   d	d	d	           d	S # 1 swxY w Y   d	S )
uI   key_down → key_up with no audio should skip pipeline, connection alive.r   r   r   r7   r   r   r   r   N)r3   r   r   r   r   r)   r   r,   assert_not_calledr/   r&   r   rE   rF   rJ   rK   rL   r   r   r   r   mock_sttr   r   rV   r   rQ   r   r   s               r   'test_key_up_empty_buffer_skips_pipeliner      s   +--E5%__F	 1(E 1 15 1 1%%g.. 	1"LL)))OOLL''' &&((( LL))***:boo//00D>0[0>[0000000000>[00000>0000[000000000000000000000000000	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	11 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1s|   GF5FD6F	;FFFFFF5F""F5%F"&F5)G5F9	9G<F9	=GGGc                  2   t                      \  } }}t          t                    }| 5 }|5  |5  |                    d          5 }|                    d           |                                 |                    t                                 t          j	        |
                                          }|d         }d}||k    }	|	slt          j        d|	fd||f          t          j        |          t          j        |          dz  }
dd	|
iz  }t          t          j        |                    d
x}x}	}d
d
d
           n# 1 swxY w Y   d
d
d
           n# 1 swxY w Y   d
d
d
           n# 1 swxY w Y   d
d
d
           d
S # 1 swxY w Y   d
S )z?Binary frame in idle state should be dropped, connection alive.r   r6   r   r   r7   r   r   r   r   N)r3   r   r   r   r   r   r   r/   r&   r   r   rE   rF   rJ   rK   rL   r   s               r   'test_binary_frame_while_idle_is_droppedr     s   +--E5%__F	 
1(E 
1 
15 
1 
1%%g.. 		1"MM,''' &&((( LL))***:boo//00D>0[0>[0000000000>[00000>0000[000000000000000000000000000		1 		1 		1 		1 		1 		1 		1 		1 		1 		1 		1 		1 		1 		1 		1
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1s|   FE4EC5E	:EE

EE
EE4E!!E4$E!%E4(F4E8	8F;E8	<FFFc            	         t                      \  } }}t          t                    }| 5 }|5  |5  |                    d          5 }|                    t                                 |                                 |                    d           |                    d           |                    d           |                    t                                 |                                 |                                 |	                                 |
                    d           ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   ddd           dS # 1 swxY w Y   dS )z9Multiple binary frames should be concatenated before STT.r   s
   s   s   s<   Nr3   r   r   r   r   r)   r   r   r,   r   rz   r   r   r   r   r   r   s         r   #test_binary_frames_are_concatenatedr   !  sc   +--E5%__F	 (E  5  %%g.. 	"LL)))OOMM,'''MM,'''MM,'''LL''' OOOO ,,:  !	 	 	 	 	 	 	 	 	 	 	 	 	 	 	                                               s|   E>E&EC'D8	,E8D<<E?D< EE&EE&EE&E>&E*	*E>-E*	.E>>FFc            	      V   t                      \  } }}t          t                    }| 5 }|5  |5  |                    d          5 }|                    t                                 |                                 |                    d           |                    t                                 |                                 |                    d           |                    t                                 |                                 |                                 |	                                 |
                    d           ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   ddd           dS # 1 swxY w Y   dS )uF   Second key_down should reset buffer — only second chunk reaches STT.r   s2   s   Nr   r   s         r   $test_repeated_key_down_resets_bufferr   ;  sp   +--E5%__F	 ;(E ; ;5 ; ;%%g.. 	;"LL)))OOMM,''' LL)))OOMM,'''LL'''OOOO ,,\:::#	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	;; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ;s|   FFE/DE	E/EE/E E/#F/E33F6E37F:FF
	
FF
	FF"%F"c                  J   t                      \  } }}t          t                    }| 5 }|5  |5  |                    d          5 }|                    t                                 |                                 |                    t                                 t          j	        |
                                          }|d         }d}||k    }	|	slt          j        d|	fd||f          t          j        |          t          j        |          dz  }
dd|
iz  }t          t          j        |                    d	x}x}	}d	d	d	           n# 1 swxY w Y   d	d	d	           n# 1 swxY w Y   d	d	d	           n# 1 swxY w Y   d	d	d	           d	S # 1 swxY w Y   d	S )
z9key_up in idle state should be ignored, connection alive.r   r   r   r7   r   r   r   r   N)r3   r   r   r   r   r,   r   r/   r&   r   r   rE   rF   rJ   rK   rL   r   s               r   'test_key_up_without_key_down_is_ignoredr   T  s   +--E5%__F	 
1(E 
1 
15 
1 
1%%g.. 		1"LL''' &&((( LL))***:boo//00D>0[0>[0000000000>[00000>0000[000000000000000000000000000		1 		1 		1 		1 		1 		1 		1 		1 		1 		1 		1 		1 		1 		1 		1
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1 
1s|   FF E)DE	E)EE)EE)F )E--F 0E-1F 4F F	FF	FFFc                     t                      \  } }}t          t                    }| 5  |5  |5  |                    d          5 }|                    t                                 |                                 |                    d           |                    t                                 t          j
        |                                          }ddd}||k    }|st          j        d|fd||f          dt          j                    v st          j        |          rt          j        |          ndt          j        |          d	z  }d
d|iz  }	t#          t          j        |	                    dx}}t          j
        |                                          }
|
d         }d}||k    }|slt          j        d|fd||f          t          j        |          t          j        |          dz  }dd|iz  }t#          t          j        |                    dx}x}}|
d         }d}||k    }|slt          j        d|fd||f          t          j        |          t          j        |          dz  }dd|iz  }t#          t          j        |                    dx}x}}|                                }d}||k    }|st          j        d|fd||f          dt          j                    v st          j        |          rt          j        |          ndt          j        |          d	z  }d
d|iz  }	t#          t          j        |	                    dx}}ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   ddd           dS # 1 swxY w Y   dS )u   key_down → binary → key_up should trigger STT → LLM → TTS pipeline.

    Expected response sequence:
      1. {"type": "state", "status": "thinking"}
      2. {"type": "text_reply", "text": "..."}
      3. binary bytes (TTS audio)
    r   r6   r   r   r   r7   r9   r   r:   r=   r>   Nr#   r   r   r   r   r   r   r   r2   ro   r   r   s                   r   )test_websocket_binary_frame_full_pipeliner   e  s    ,--E5%__F	 , , , ,u , ,%%g.. 	,"LL)))OOMM,'''LL''' :boo//00D$+zBBB4BBBBBBBBBBB4BBBBBBBBBBBBB4BBBBB4BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB :boo//00D</</<<//////////<</////<////<///////////////////////////<=#==<#===========<#======<====#============================ $$&&E++5O++++++++++5O++++++++++++5+++++5++++++O+++++++++++++++++++++++++'	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	,, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,r   rx   )6__doc__builtinsrG   _pytest.assertion.rewrite	assertionrewriterE   r&   unittest.mockr   r   r   re   starlette.testclientr   mainr   r   r	   r
   r   r   r'   rj   MOCK_OPENCLAW_OUTPUTbytesintr   strr)   r,   r/   r3   markasynciorZ   r`   rh   rl   r|   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r(   r   r   <module>r      s   4 4               5 5 5 5 5 5 5 5 5 5  + + + + + +                "tz8dKKL# #  
 
688  )     Hs H H H HFS F F F FI I I I I  0 
( 
( 
( 
( 0 0 0 0 : : : : : : : : O O O O2 . . . .0$ $ $ $+ + + +   - - - -- - - -@ @ @ @, , , ,61 1 1 1(1 1 1 1"   4; ; ; ;21 1 1 1", , , , , ,r   