
    |Wi-                       d Z ddlmZ ddlZddlmc mZ ddl	Z	ddl
Z
ddlZddlmZ ddlmZ ddlmZmZ ddlZ ee      j+                         j,                  d   Zedz  Z ee      ej4                  vr"ej4                  j7                  d ee             dd	Zd
ej:                  vr e       ej:                  d
<   ddlmZm Z  dZ!	 	 d	 	 	 	 	 ddZ"dddZ# G d d      Z$ G d d      Z%y)zTests for VLMClient.

All requests.post calls are mocked so these tests run without a real network
connection or the requests library installed (we inject a stub module).
    )annotationsN)Path)
ModuleType)	MagicMockpatch   srcc                 f    t        d      }  G d dt              }|| _        t               | _        | S )z=Build a minimal requests stub sufficient for VLMClient tests.requestsc                  "     e Zd Zdd fdZ xZS )&_make_fake_requests.<locals>.HTTPErrorc                D    t         |   t        |             || _        y N)super__init__strresponse)selfr   	__class__s     R/home/nelsen/Projects/kognitive/orchestrator/src/services/tests/test_vlm_client.pyr   z/_make_fake_requests.<locals>.HTTPError.__init__!   s    GS]+$DM    r   )r   objectreturnNone)__name__
__module____qualname__r   __classcell__)r   s   @r   	HTTPErrorr       s    	% 	%r   r   )r   	Exceptionr   r   post)faker   s     r   _make_fake_requestsr#      s/    j!D%I %
 DNDIKr   r   )	VLMClient	VLMConfigs               c                z    t               }| |_        ddd|iigi}t        |      |_        t               |_        |S )z:Build a mock requests.Response for a successful VLM reply.choicesmessagecontentreturn_value)r   status_codejsonraise_for_status)r,   r)   respbodys       r   _make_mock_responser1   6   sN    
 ;D"D 	G,-
D
 t,DI%KDKr   c                b    t               }| |_        t        t        d|              |_        |S )zABuild a mock requests.Response that raises on raise_for_status().zHTTP side_effect)r   r,   r    r.   )r,   r/   s     r   _make_error_responser5   H   s4    ;D"D%k]34D Kr   c                  X    e Zd Z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	Zdd
Zy)TestVLMConfigz6VLMConfig defaults and environment variable overrides.c                   |j                  dd       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}}y )NVLM_ENDPOINTFraisingz.https://modelapi.klass.dev/v1/chat/completions==z0%(py2)s
{%(py2)s = %(py0)s.endpoint
} == %(py5)sconfigpy0py2py5assert %(py7)spy7)delenvr%   endpoint
@pytest_ar_call_reprcompare@py_builtinslocals_should_repr_global_name	_safereprAssertionError_format_explanationr   monkeypatchr?   @py_assert1@py_assert4@py_assert3@py_format6@py_format8s           r   test_default_endpointz#TestVLMConfig.test_default_endpointX   s    >59R"RR"RRRRR"RRRRRRRvRRRvRRRRRR"RRRRRRRRr   c                   |j                  dd       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}}y )	Nr9   z&http://myhost:9999/v1/chat/completionsr<   r>   r?   r@   rD   rE   )setenvr%   rG   rH   rI   rJ   rK   rL   rM   rN   rO   rP   s           r   test_endpoint_from_envz$TestVLMConfig.test_endpoint_from_env]   s    >+STJ"JJ"JJJJJ"JJJJJJJvJJJvJJJJJJ"JJJJJJJJr   c                   |j                  dd       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}}y )N	VLM_MODELFr:   zQwen3.5-122B-A10Br<   z-%(py2)s
{%(py2)s = %(py0)s.model
} == %(py5)sr?   r@   rD   rE   )rF   r%   modelrH   rI   rJ   rK   rL   rM   rN   rO   rP   s           r   test_default_model_emptyz&TestVLMConfig.test_default_model_emptyb   s    ;6||222|22222|2222222v222v222|22222222222r   c                   |j                  dd       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}}y )	Nr\   zQwen-VL-72Br<   r]   r?   r@   rD   rE   )rY   r%   r^   rH   rI   rJ   rK   rL   rM   rN   rO   rP   s           r   test_model_from_envz!TestVLMConfig.test_model_from_envg   s    ;6||,},|},,,,|},,,,,,v,,,v,,,|,,,},,,,,,,r   c                   |j                  dd       |j                  dd       |j                  dd       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}}y )NMODEL_API_KEYFr:   OPENAI_API_KEYMODELAPI_KEYzsk-test-keyr<   z/%(py2)s
{%(py2)s = %(py0)s.api_key
} == %(py5)sr?   r@   rD   rE   rF   rY   r%   api_keyrH   rI   rJ   rK   rL   rM   rN   rO   rP   s           r   "test_api_key_from_modelapi_key_envz0TestVLMConfig.test_api_key_from_modelapi_key_envl   s    ?E:+U;>=9~~..~....~......v...v...~..........r   c                   |j                  dd       |j                  dd       |j                  dd       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}}y )Nre   Fr:   rd   rc   zsk-fallbackr<   rf   r?   r@   rD   rE   rg   rP   s           r   &test_api_key_fallback_to_model_api_keyz4TestVLMConfig.test_api_key_fallback_to_model_api_keys   s    >59+U;?M:~~..~....~......v...v...~..........r   c                   |j                  dd       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}}y )Nre   zenv-keyzexplicit-key)rh   r<   rf   r?   r@   rD   rE   )rY   r%   rh   rH   rI   rJ   rK   rL   rM   rN   rO   rP   s           r   +test_explicit_api_key_not_overridden_by_envz9TestVLMConfig.test_explicit_api_key_not_overridden_by_envz   s    >95>2~~//~////~//////v///v///~//////////r   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}}y )N   r<   z1%(py2)s
{%(py2)s = %(py0)s.timeout_s
} == %(py5)sr?   r@   rD   rE   )
r%   	timeout_srH   rI   rJ   rK   rL   rM   rN   rO   )r   r?   rR   rS   rT   rU   rV   s          r   test_default_timeoutz"TestVLMConfig.test_default_timeout   s{    %2%2%%%%2%%%%%%v%%%v%%%%%%2%%%%%%%r   c                   |j                  dd       |j                  dd       t        ddd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}||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                  |      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                  |      dz  }dd|iz  }t        t        j                  |            d x}x}}y )Nr9   Fr:   r\   z&http://custom:1234/v1/chat/completionsz
custom-vlmzmy-key<   )rG   r^   rh   rq   r<   r>   r?   r@   rD   rE   r]   rf   rp   )rF   r%   rG   rH   rI   rJ   rK   rL   rM   rN   rO   r^   rh   rq   rP   s           r   test_custom_valuesz TestVLMConfig.test_custom_values   s   >59;6=	
 J"JJ"JJJJJ"JJJJJJJvJJJvJJJJJJ"JJJJJJJJ||+|+||++++||++++++v+++v+++|+++|+++++++~~))~))))~))))))v)))v)))~))))))))))%2%2%%%%2%%%%%%v%%%v%%%%%%2%%%%%%%r   N)rQ   zpytest.MonkeyPatchr   r   r   r   )r   r   r   __doc__rW   rZ   r_   ra   ri   rk   rm   rr   ru    r   r   r7   r7   U   s4    @S
K
3
-
//0&&r   r7   c                  h    e Zd Z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	Zdd
ZddZddZy)TestVLMClientAnalyzeScenez$VLMClient.analyze_scene() behaviour.c                4    t        ddd      }t        |      S )N)https://fake-modelapi/v1/chat/completionstest-vlmzsk-test)rG   r^   rh   )r%   r$   )r   r?   s     r   _clientz!TestVLMClientAnalyzeScene._client   s"    @

   r   c                   | j                         }t        d      }ddl}t        j                  |d|      5  |j                  t        d      }d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}}y# 1 sw Y   xY w)u>   HTTP 200 with well-formed response → returns content string.z%Two workers unconscious near barrels.)r)   r   Nr!   r*   Describe the scene.r<   z%(py0)s == %(py3)sresultrA   py3assert %(py5)srC   )r~   r1   r   r   r   analyze_scene_SAMPLE_JPEGrH   rI   rJ   rK   rL   rM   rN   rO   	r   client	mock_respr   r   @py_assert2rR   @py_format4rU   s	            r   test_success_returns_model_textz9TestVLMClientAnalyzeScene.test_success_returns_model_text   s    '0WX	\\(FC 	O)),8MNF	O A@v@@@@@v@@@@@@@v@@@v@@@@@@@@@@@	O 	O   C??Dc                   | j                         }t               }ddl}t        j                  |d|      5 }|j                  t        d       ddd       j                  }|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}}y# 1 sw Y   xY w)z/analyze_scene posts to the configured endpoint.r   Nr!   r*   r   r|   r<   z%(py1)s == %(py4)spy1py4assert %(py6)spy6)r~   r1   r   r   r   r   r   	call_argsargsrH   rI   rM   rN   rO   )r   r   r   r   	mock_postr   @py_assert0rT   r   @py_format5@py_format7s              r   #test_success_sends_correct_endpointz=TestVLMClientAnalyzeScene.test_success_sends_correct_endpoint   s    ')	\\(FC 	Fy  /DE	F ''	~~a O$OO $OOOOO $OOOO OOO$OOOOOOOO		F 	Fs   C--C6c                   | j                         }t               }ddl}t        j                  |d|      5 }|j                  t        d       ddd       j                  j                  d   }|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}}y# 1 sw Y   xY w)z0Authorization header is set from config.api_key.r   Nr!   r*   r   headersAuthorizationzBearer sk-testr<   r   r   r   r   )r~   r1   r   r   r   r   r   r   kwargsrH   rI   rM   rN   rO   )r   r   r   r   r   r   r   rT   r   r   r   s              r   test_success_sends_auth_headerz8TestVLMClientAnalyzeScene.test_success_sends_auth_header   s    ')	\\(FC 	Fy  /DE	F %%,,Y7';+;;'+;;;;;'+;;;;';;;+;;;;;;;;		F 	Fs   C00C9c                   | j                         }i dfd}ddl}t        j                  |d|      5  |j	                  t
        d       ddd       d   d   d	   }t        d
 |D              }|d   d   }|j                  }d} ||      }	|	sddt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      t        j                  |	      dz  }
t        t        j                  |
            dx}x}}	|j                  dd      d   }t!        j"                  |      }|t
        k(  }|st        j$                  d|fd|t
        f      dt        j                         v st        j                  |      rt        j                  |      nddt        j                         v st        j                  t
              rt        j                  t
              nddz  }dd|iz  }t        t        j                  |            d}y# 1 sw Y   xY w)z@The JPEG bytes appear in the payload as a valid base64 data URL.urlc                :    j                  |       t               S r   updater1   )r   r-   r   timeoutcaptured_payloads       r   	fake_postzLTestVLMClientAnalyzeScene.test_base64_encoding_is_correct.<locals>.fake_post   s    ##D)&((r   r   Nr!   r3   zCheck for hazards.messagesr)   c              3  2   K   | ]  }|d    dk(  s|  yw)type	image_urlNrx   .0ps     r   	<genexpr>zLTestVLMClientAnalyzeScene.test_base64_encoding_is_correct.<locals>.<genexpr>   s     Oai;6N!O   r   zdata:image/jpeg;base64,zLassert %(py6)s
{%(py6)s = %(py2)s
{%(py2)s = %(py0)s.startswith
}(%(py4)s)
}data_url)rA   rB   r   r   ,   r<   )z%(py0)s == %(py2)sdecodedr   )rA   rB   zassert %(py4)sr   
r   r   r-   dictr   r   r   intr   r   )r~   r   r   r   r   r   next
startswithrJ   rK   rH   rL   rM   rN   rO   splitbase64	b64decoderI   )r   r   r   r   content_parts
image_partr   rR   rT   @py_assert5r   b64_partr   @py_format3r   r   s                  @r   test_base64_encoding_is_correctz9TestVLMClientAnalyzeScene.test_base64_encoding_is_correct   s   !#	) 	\\(F	B 	E  /CD	E )4Q7	BO]OO
";/6""=#<="#<========x===x==="===#<==========>>#q)!,""8,,&&&&w,&&&&&&w&&&w&&&&&&,&&&,&&&&&&&	E 	Es   IIc                	   | j                         }i d%fd}ddl}t        j                  |d|      5  |j	                  t
        d       ddd       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}||k(  }|slt        j                  d	|fd
||f      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}d   }	t        |	      }d}
||
k(  }|st        j                  d	|fd||
f      dt        j                         v st        j                  t              rt        j                  t              nd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}}
|	d   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 ch c]  }|d   	 }}ddh}||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        d$ |D              }|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}}y# 1 sw Y   wxY wc c}w )&z9Payload matches the OpenAI vision chat completion format.c                :    j                  |       t               S r   r   )r   r-   r   r   captureds       r   r   zCTestVLMClientAnalyzeScene.test_payload_structure.<locals>.fake_post   s    OOD!&((r   r   Nr!   r3   zAny casualties?r^   r}   r<   r   r   r   r   temperaturegffffff?
max_tokensi   r   r   )z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py6)slen)rA   r   r   r   zassert %(py8)spy8roleuserr)   r   r   textr   typesr   r   rC   c              3  2   K   | ]  }|d    dk(  s|  yw)r   r   Nrx   r   s     r   r   zCTestVLMClientAnalyzeScene.test_payload_structure.<locals>.<genexpr>   s     CqqyF/BCr   r   )r~   r   r   r   r   r   rH   rI   rM   rN   rO   r   rJ   rK   rL   r   )r   r   r   r   r   rT   r   r   r   r   r   rS   @py_format9r)   r   r   rR   r   rU   	text_partr   s                       @r   test_payload_structurez0TestVLMClientAnalyzeScene.test_payload_structure   s+   	) 	\\(F	B 	B  /@A	B  .J. J.... J... ...J.......&-#-&#----&#---&---#-------%--%----%---%----------J'8}!!}!!!!}!!!!!!s!!!s!!!!!!8!!!8!!!}!!!!!!!!!!{6",f,"f,,,,"f,,,",,,f,,,,,,,1+i($+,q6,,$f--u-----u-------u---u-----------CGCC	 5$55 $55555 $5555 555$55555555!	B 	B -s   S9SSc                   | j                         }t        d      }ddl}t        j                  |d|      5  |j                  t        d      }d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}}y# 1 sw Y   xY w)uC   Non-200 response → analyze_scene returns empty string (no raise).  )r,   r   Nr!   r*   r    r<   r   r   r   r   rC   )r~   r5   r   r   r   r   r   rH   rI   rJ   rK   rL   rM   rN   rO   r   s	            r   $test_http_error_returns_empty_stringz>TestVLMClientAnalyzeScene.test_http_error_returns_empty_string   s    (S9	\\(FC 	O)),8MNF	O v|vvv	O 	Or   c                   | j                         }ddl}t        j                  |dt	        d            5  |j                  t        d      }d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}}y# 1 sw Y   xY w)uC   Connection error → analyze_scene returns empty string (no raise).r   Nr!   refusedr3   r   r   r<   r   r   r   r   rC   )r~   r   r   r   ConnectionRefusedErrorr   r   rH   rI   rJ   rK   rL   rM   rN   rO   )r   r   r   r   r   rR   r   rU   s           r   +test_network_exception_returns_empty_stringzETestVLMClientAnalyzeScene.test_network_exception_returns_empty_string  s    \\(F8Ny8YZ 	O)),8MNF	O v|vvv	O 	Os   C<<Dc                   | j                         }ddl}t        j                  |d      5 }|j	                  dd      }d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}}j                          y# 1 sw Y   xY w)uA   Empty bytes → returns empty string without making an HTTP call.r   Nr!   r   r   r   r<   r   r   r   r   rC   )r~   r   r   r   r   rH   rI   rJ   rK   rL   rM   rN   rO   assert_not_called	r   r   r   r   r   r   rR   r   rU   s	            r   *test_empty_jpeg_bytes_returns_empty_stringzDTestVLMClientAnalyzeScene.test_empty_jpeg_bytes_returns_empty_string  s    \\(F+ 	Fy))#/DEF	F v|vvv##%		F 	Fs   C==Dc                   | j                         }ddl}t        j                  |d      5 }|j	                  t
        d      }d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}}j                          y# 1 sw Y   xY w)uB   Empty prompt → returns empty string without making an HTTP call.r   Nr!   r   r<   r   r   r   r   rC   )r~   r   r   r   r   r   rH   rI   rJ   rK   rL   rM   rN   rO   r   r   s	            r   &test_empty_prompt_returns_empty_stringz@TestVLMClientAnalyzeScene.test_empty_prompt_returns_empty_string  s    \\(F+ 	<y)),;F	< v|vvv##%		< 	<s   DD
c                V   | j                         }t               }t               |_        t        ddi      |_        ddl}t        j                  |d|      5  |j                  t        d      }d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}}y# 1 sw Y   xY w)u8   Response missing 'choices' key → returns empty string.
unexpectedformatr*   r   Nr!   r   r   r<   r   r   r   r   rC   )r~   r   r.   r-   r   r   r   r   r   rH   rI   rJ   rK   rL   rM   rN   rO   r   s	            r   ,test_malformed_response_returns_empty_stringzFTestVLMClientAnalyzeScene.test_malformed_response_returns_empty_string!  s    K	%.[	""x0HI	\\(FC 	O)),8MNF	O v|vvv	O 	Os   DD(N)r   r$   rv   )r   r   r   rw   r~   r   r   r   r   r   r   r   r   r   r   rx   r   r   rz   rz      s>    .!	A
P
<',68		&	&r   rz   )r   r   )   zScene analysis result.)r,   r   r)   r   r   r   )r   )r,   r   r   r   )&rw   
__future__r   builtinsrJ   _pytest.assertion.rewrite	assertionrewriterH   r   r-   syspathlibr   r   r   unittest.mockr   r   pytest__file__resolveparentsROOTSRCr   pathinsertr#   modulesservices.vlm_clientr$   r%   r   r1   r5   r7   rz   rx   r   r   <module>r      s    #     
   *  H~''*
Uls8388HHOOAs3x  S[[ 13CKK
 4
 2 + $;& ;&BV Vr   