
    ˆj!                    (   d Z ddlmZ ddlZddlZddlZddlmZ ej                  j                  dej                  j                  ej                  j                  e                   ddlZddlmZmZmZmZ  ej&                  d      d        Z G d	 d
      Z G d d      Z G d d      Zd Zd Zd Zd Zd Zd Zd Zd Zd Z d Z!d Z"d Z#d Z$ejJ                  jM                  dg d      d        Z'd Z(d  Z)d! Z*d" Z+d# Z,d$ Z-d% Z.d& Z/y)'u  test_server.py - 아누 공용 임베딩 서비스 pytest.

검증 항목
- /health 응답 스키마
- /embed 가 정확히 768차원을 반환
- 결정론성: 같은 입력 → 같은 벡터
- 빈 입력/비문자열/배치초과 거부 (422)
- 차원 불변식 fail-closed: 768 이 아니면 500 (조용한 truncate/padding 금지)
- 모델이 서버 생애주기 동안 **1회만** 로드되는지
    )annotationsN)
TestClient)EMBEDDING_DIMMAX_BATCH_SIZEMAX_TEXT_CHARS
MODEL_NAMEmodule)scopec               #  p   K   t        t        j                        5 } |  ddd       y# 1 sw Y   yxY ww)uL   TestClient 를 context manager 로 사용 → lifespan(startup) 1회 실행.N)r   serverapp)test_clients    !services/embedding/test_server.pyclientr      s0      
FJJ	 ;  s   6*	636c                      e Zd ZdZddZd Zy)
_FakeModeluW   차원 위반을 재현하기 위한 가짜 모델 (정상 포맷의 잘못된 차원).c                    || _         y Ndim)selfr   s     r   __init__z_FakeModel.__init__$   s	        c                H    |D cg c]  }dg| j                   z   c}S c c}w Ng?r   r   textskwargs_s       r   encodez_FakeModel.encode'   s!    */0Q 000s   N)r   int)__name__
__module____qualname____doc__r   r     r   r   r   r   !   s    a1r   r   c                      e Zd Zd Zy)	_NaNModelc                N    |D cg c]  }t        d      gt        z   c}S c c}w )Nnan)floatr   r   s       r   r    z_NaNModel.encode,   s!    8=>1u.>>>s   "Nr"   r#   r$   r    r&   r   r   r(   r(   +   s    ?r   r(   c                      e Zd Zd Zy)_WrongCountModelc                    dgt         z  gS r   r   )r   r   r   s      r   r    z_WrongCountModel.encode1   s    %&&r   Nr,   r&   r   r   r.   r.   0   s    'r   r.   c                    | j                  d      }|j                  dk(  sJ |j                         }|d   dk(  sJ |d   t        k(  sJ |d   dk(  sJ |d   d	u sJ |d
   dk(  sJ y )Nz/health   statusokmodel	dimension   loadedTdevicecpu)getstatus_codejsonr   )r   respbodys      r   test_health_okr@   :   s    ::i Ds"""99;D>T!!!=J&&&###>T!!!>U"""r   c                     t         dk(  sJ y )Nr7   r0   r&   r   r   %test_health_dimension_is_768_constantrB   E   s    Cr   c                (   | j                  dddgdd      }|j                  dk(  sJ |j                         }|d   t        k(  sJ |d	   d
k(  sJ t	        |d         dk(  sJ |d   D ]&  }t	        |      d
k(  sJ t        d |D              r&J  y )N/embedu   안녕하세요u   실손의료비 보장Tr   	normalizer=   r2   r5   r6   r7   
embeddings   c              3  <   K   | ]  }t        |t                y wr   )
isinstancer+   .0vs     r   	<genexpr>z4test_embed_returns_768_dimensions.<locals>.<genexpr>W   s     5A:a'5s   )postr<   r=   r   lenall)r   r>   r?   vecs       r   !test_embed_returns_768_dimensionsrT   N   s    ;;x1BD\0]lp&q;rDs"""99;D=J&&&###tL!"a'''L! 63x3555556r   c                    | j                  ddgdd      }|j                  dk(  sJ |j                         d   d   }t        d	 |D              d
z  }t	        |dz
        dk  sJ y )NrD   u   보험 특약 설명TrE   rG   r2   rH   r   c              3  &   K   | ]	  }||z    y wr   r&   rL   s     r   rO   z<test_embed_normalize_true_gives_unit_norm.<locals>.<genexpr>^        "q1u"         ?      ?-C6?rP   r<   r=   sumabsr   r>   rS   norms       r   )test_embed_normalize_true_gives_unit_normra   Z   sr    ;;x1G0HW[&\;]Ds"""
))+l
#A
&C"c""c)Dtcz?T!!!r   c                    | j                  dddgi      }|j                  dk(  sJ |j                         d   d   }t        d |D              d	z  }t	        |d
z
        dk  sJ y)u?   normalize 를 생략하면 기본 True 로 동작해야 한다.rD   r   u   기본값 확인rG   r2   rH   r   c              3  &   K   | ]	  }||z    y wr   r&   rL   s     r   rO   z7test_embed_normalize_default_is_true.<locals>.<genexpr>g   rW   rX   rY   rZ   r[   Nr\   r_   s       r   $test_embed_normalize_default_is_truerd   b   sq    ;;xw1C0D&E;FDs"""
))+l
#A
&C"c""c)Dtcz?T!!!r   c                    dgdd}| j                  d|      j                         d   d   }| j                  d|      j                         d   d   }||k(  sJ y )Nu#   결정론성 확인 문장입니다TrE   rD   rG   rH   r   rP   r=   )r   payloadfirstseconds       r   /test_embed_deterministic_same_input_same_vectorrj   k   se    >?dSGKKwK/446|DQGE[[[0557EaHFF??r   c                t    | j                  dddgdd      }|j                         d   }|d   |d	   k7  sJ y )
NrD   u   암 진단비u   자동차 보험료TrE   rG   rH   r      rf   )r   r>   vecss      r   1test_embed_different_texts_give_different_vectorsrn   r   sF    ;;xBW0Xgk&l;mD99;|$D7d1gr   c                    dt         dz   z  }| j                  d|gdd      }|j                  dk(  sJ t        |j	                         d   d	         d
k(  sJ y )N   가i  rD   TrE   rG   r2   rH   r   r7   )r   rP   r<   rQ   r=   )r   	long_textr>   s      r   ,test_embed_truncates_long_text_without_errorrr   x   sa    $./I;;x$&O;PDs"""tyy{<(+,333r   c                R    | j                  dg dd      }|j                  dk(  sJ y )NrD   TrE   rG     rP   r<   r   r>   s     r   test_embed_empty_list_rejectedrw      s-    ;;x&F;GDs"""r   c                T    | j                  ddgdd      }|j                  dk(  sJ y )NrD   z   TrE   rG   rt   ru   rv   s     r    test_embed_blank_string_rejectedry      s/    ;;xd&K;LDs"""r   c                P    | j                  dddi      }|j                  dk(  sJ y )NrD   rF   TrG   rt   ru   rv   s     r   'test_embed_missing_texts_field_rejectedr{      s-    ;;x{D&9;:Ds"""r   c                T    | j                  ddgdd      }|j                  dk(  sJ y )NrD   {   TrE   rG   rt   ru   rv   s     r   #test_embed_non_string_item_rejectedr~      s/    ;;xD&I;JDs"""r   c                h    | j                  ddgt        dz   z  dd      }|j                  dk(  sJ y )NrD   rp   rl   TrE   rG   rt   )rP   r   r<   rv   s     r   $test_embed_batch_over_limit_rejectedr      s:    ;;x>A;M0N]a&b;cDs"""r   bad_dim)  i  i  i   c                    |j                  t        j                  dt        |             | j	                  ddgdd      }|j
                  dk(  sJ d|j                         d	   v sJ y
)uc   768 이 아닌 결과는 조용히 자르거나 패딩하지 않고 500 으로 실패해야 한다.r5   rD   u   차원 위반 확인TrE   rG        차원 불변식 위반detailN)setitemr   _stater   rP   r<   r=   )r   monkeypatchr   r>   s       r   'test_embed_wrong_dimension_fails_closedr      sh     w
70CD;;x1G0HW[&\;]Ds"""$		H(====r   c                    |j                  t        j                  dt                      | j	                  ddgdd      }|j
                  dk(  sJ y )Nr5   rD   u
   NaN 확인TrE   rG   r   )r   r   r   r(   rP   r<   r   r   r>   s      r   !test_embed_nan_values_fail_closedr      sG    w	<;;xT&R;SDs"""r   c                    |j                  t        j                  dt                      | j	                  dddgdd      }|j
                  dk(  sJ y )	Nr5   rD   abTrE   rG   r   )r   r   r   r.   rP   r<   r   s      r   &test_embed_count_mismatch_fails_closedr      sK    w0@0BC;;xc
&N;ODs"""r   c                 f    dgdz  g} t        j                  | d      }t        |d         dk(  sJ y )Ng        r7   rl   )expected_countr   )r   validate_embeddingsrQ   )rowsouts     r   *test_validate_embeddings_accepts_exact_768r      s7    ECK=D

$
$T!
<Cs1v;#r   c                    |j                  t        j                  dd       | j                  ddgdd      }|j                  dk(  sJ y)	uY   모델 미로딩 시 요청 시점 로딩으로 폴백하지 않고 503 을 반환한다.r5   NrD   u   미로딩 확인TrE   rG   i  )r   r   r   rP   r<   r   s      r   !test_model_not_loaded_returns_503r      sH    w5;;x1C0DSW&X;YDs"""r   c                     G d d      ddl }| j                  |dfd       t        j                  t        d      5  t        j                          ddd       y# 1 sw Y   yxY w)	uX   모델이 보고하는 차원이 768 이 아니면 기동 자체가 실패해야 한다.c                      e Zd Zd Zy)=test_load_model_rejects_wrong_dimension.<locals>._BadDimModelc                     y)Nr   r&   )r   s    r    get_sentence_embedding_dimensionz^test_load_model_rejects_wrong_dimension.<locals>._BadDimModel.get_sentence_embedding_dimension   s    r   N)r"   r#   r$   r   r&   r   r   _BadDimModelr      s    	r   r   r   NSentenceTransformerc                             S r   r&   )r   kr   s     r   <lambda>z9test_load_model_rejects_wrong_dimension.<locals>.<lambda>   s	    ln r   r   )match)sentence_transformerssetattrpytestraisesRuntimeErrorr   
load_model)r   r   r   s     @r   'test_load_model_rejects_wrong_dimensionr      s[      !46T 
|+D	E   s   AA(c                    t         j                  d   }|dk(  sJ d| d       t        d      D ]'  }| j                  dddgi	      j                  d
k(  r'J  t         j                  d   dk(  sJ y)uP   여러 요청을 보내도 모델 로드는 startup 시 1회뿐이어야 한다.
load_countrl   u3   startup 후 load_count 는 1 이어야 함 (actual=)   rD   r   u   로드 카운트 확인rG   r2   N)r   r   rangerP   r<   )r   beforer   s      r   test_model_loaded_exactly_oncer      s    ]]<(FQ;WMfXUVWW;1X e{{875N4O*P{Q]]adddde==&!+++r   c                    t         j                  d   }| j                  dddgi       t         j                  d   |u sJ y)uJ   요청 간 동일한 모델 객체(identity)가 재사용되어야 한다.r5   rD   r   u   동일 인스턴스 확인rG   N)r   r   rP   )r   rh   s     r   -test_model_instance_is_stable_across_requestsr      s@    MM'"E
KK*F)GHKI==!U***r   c                 ~    dD ]"  } t         j                  j                  | d       $ t        dk(  sJ t        dk(  sJ y)uW   외부 API 키 환경변수 없이도 모듈 상수가 유효하다 (외부 API 키 0).)GEMINI_API_KEYOPENAI_API_KEYANTHROPIC_API_KEYNzjhgan/ko-sroberta-multitaskr7   )osenvironpopr   r   )keys    r   !test_no_external_api_key_requiredr      s@    H "


sD!"6666Cr   )0r%   
__future__r   r   sysr   fastapi.testclientr   pathinsertdirnameabspath__file__r   settingsr   r   r   r   fixturer   r   r(   r.   r@   rB   rT   ra   rd   rj   rn   rr   rw   ry   r{   r~   r   markparametrizer   r   r   r   r   r   r   r   r   r&   r   r   <module>r      s  	 # 	 
  ) 277??277??8#<= >  N N h  1 1? ?
' '# 	6""4#
#
#
#
# $9:> ;>###*,+ r   