
    Si@                       U d Z ddlmZ ddlmZ ddlZddlZddlZddl	Z	ddl
Z
dZ	  ej                  de      Z ej                  e      Zej                   j#                  e       dZe
j.                  j1                  edud	e 
      Zh dZh dZddZ G d d      Z G d d      Z G d d      Z G d d      Z  G d d      Z! G d d      Z" G d d      Z#y# e$ r#Z e	j*                  d      ZeZded<   Y dZ[dZ[ww xY w)u	  
image-skill-router.py 유닛 테스트

테스트 범위:
  - route_skill: 작업 유형별 스킬 라우팅
  - route_skill: urgent=True 오버라이드
  - route_skill: count >= 10 강제 라우팅
  - route_skill: 알 수 없는 작업 유형 처리
  - route_skill: 출력 dict 구조 검증
  - get_skill_recommendation: 작업 설명 키워드 기반 스킬 추천
  - CLI 통합: subprocess 기반 end-to-end 실행

외부 의존성:
  - subprocess → CLI 통합 테스트
  - 나머지는 모듈 직접 호출
    )annotationsNz//home/jay/workspace/tools/image-skill-router.pyimage_skill_routerimage_skill_router_stubzException | None_IMPORT_ERRORu(   image-skill-router.py 임포트 실패: )reason>   gemini-imagehybrid-imagesatori-cardnews>   r   warnings
input_typealternativesrecommended_skillc                .    t        j                  | fi |S )u   route_skill 호출 헬퍼.)r   route_skill)	task_typekwargss     T/home/jay/workspace/.worktrees/task-2117-dev1/tools/tests/test_image_skill_router.py_router   <   s    )))>v>>    c                  Z    e Zd ZdZej
                  j                  dg d      dd       Zy)TestBasicRoutingu4   각 작업 유형별 올바른 스킬 추천 검증.ztask_type, expected_skill))   광고 배너r	   )   카드뉴스r
   )   SNS 메인 이미지r   )u   블로그 썸네일r	   )u   A/B 테스트 변형r
   )   프리미엄 브랜딩r   )   인포그래픽r
   )	   비교표r
   )u   Meta 배너r	   )u   Google 정적 배너r
   )u   네이버 GFA Smartr
   )u   카카오 비즈보드r
   c                P    t        |      }|d   |k(  sJ d|d|d|d          y)u9   작업 유형에 따라 올바른 스킬이 추천된다.r   
task_type=z: expected z, got Nr   )selfr   expected_skillresults       r   test_routing_by_task_typez*TestBasicRouting.test_routing_by_task_typeI   sN    & 	")*n< 	
 &&)7J0K/NP	
<r   N)r   strr"   r%   returnNone)__name__
__module____qualname____doc__pytestmarkparametrizer$    r   r   r   r   F   s.    >[[#	
"
#"
r   r   c                  8    e Zd ZdZddZddZddZddZddZy)	TestUrgentModeu@   urgent=True 시 satori-cardnews 강제 및 경고 포함 검증.c                2    t        dd      }|d   dk(  sJ y)u;   urgent=True 이면 추천 스킬이 satori-cardnews 이다.r   Turgentr   r
   Nr    r!   r#   s     r   test_urgent_true_returns_satoriz.TestUrgentMode.test_urgent_true_returns_satorik   s#    5)*.????r   c                x    t        dd      }t        |d   t              sJ t        |d         dkD  sJ d       y)u@   urgent=True 이면 warnings 리스트에 경고가 포함된다.r   Tr3   r   r   u%   urgent 모드 경고가 없습니다.N)r   
isinstancelistlenr5   s     r   !test_urgent_true_includes_warningz0TestUrgentMode.test_urgent_true_includes_warningp   sA    5&,d3336*%&*S,SS*r   c                f    t        d      }|d   dk(  sJ d       t        dd      }|d   dk(  sJ y)	uZ   원래 gemini-image가 추천될 작업도 urgent=True면 satori로 오버라이드된다.r   r   r   uZ   이 테스트는 '프리미엄 브랜딩'이 gemini-image를 반환해야 성립합니다.Tr3   r
   Nr    r!   normal_resulturgent_results      r   +test_urgent_overrides_gemini_recommendationz:TestUrgentMode.test_urgent_overrides_gemini_recommendationv   sQ     7801^C 	
h	
C
 7E015FFFFr   c                f    t        d      }|d   dk(  sJ d       t        dd      }|d   dk(  sJ y)	uZ   원래 hybrid-image가 추천될 작업도 urgent=True면 satori로 오버라이드된다.r   r   r	   uQ   이 테스트는 '광고 배너'이 hybrid-image를 반환해야 성립합니다.Tr3   r
   Nr    r=   s      r   +test_urgent_overrides_hybrid_recommendationz:TestUrgentMode.test_urgent_overrides_hybrid_recommendation   sM    /01^C 	
_	
C t<015FFFFr   c                2    t        dd      }|d   dk(  sJ y)u?   urgent=False(기본값)이면 강제 오버라이드가 없다.r   Fr3   r   r	   Nr    r5   s     r   'test_urgent_false_does_not_force_satoriz6TestUrgentMode.test_urgent_false_does_not_force_satori   s"    6)*n<<<r   Nr&   r'   )	r(   r)   r*   r+   r6   r;   r@   rB   rD   r/   r   r   r1   r1   h   s"    J@
T
GG=r   r1   c                  8    e Zd ZdZddZddZddZddZddZy)	TestBulkGenerationu>   count 값에 따른 satori 강제 및 정상 라우팅 검증.c                2    t        dd      }|d   dk(  sJ y)u3   count >= 10 이면 satori-cardnews가 강제된다.r   
   countr   r
   Nr    r5   s     r   test_count_10_forces_satoriz.TestBulkGeneration.test_count_10_forces_satori   s#    r2)*.????r   c                2    t        dd      }|d   dk(  sJ y)uB   count=100 같은 대량 생성도 satori-cardnews를 반환한다.r   d   rJ   r   r
   Nr    r5   s     r   test_count_100_forces_satoriz/TestBulkGeneration.test_count_100_forces_satori   s$    0<)*.????r   c                2    t        dd      }|d   dk(  sJ y)u7   count=9 이면 정상 라우팅 결과를 반환한다.r   	   rJ   r   r	   Nr    r5   s     r   test_count_9_normal_routingz.TestBulkGeneration.test_count_9_normal_routing   s"    q1)*n<<<r   c                2    t        dd      }|d   dk(  sJ y)uA   count=1(기본값)이면 정상 라우팅 결과를 반환한다.r      rJ   r   r
   Nr    r5   s     r   test_count_1_normal_routingz.TestBulkGeneration.test_count_1_normal_routing   s#    a0)*.????r   c                .    t        d      }|d   dk(  sJ y)uA   count를 명시하지 않으면 정상 라우팅이 적용된다.r   r   r   Nr    r5   s     r   !test_count_default_normal_routingz4TestBulkGeneration.test_count_default_normal_routing   s!    ./)*n<<<r   NrE   )	r(   r)   r*   r+   rL   rO   rR   rU   rW   r/   r   r   rG   rG      s!    H@
@
=
@
=r   rG   c                  0    e Zd ZdZddZddZddZddZy)TestUnknownTaskTypeu6   알 수 없는 작업 유형 입력 시 처리 검증.c                H    t        d      }t        |t              sJ d       y)u;   알 수 없는 작업 유형에서도 dict를 반환한다.u*   완전히 알 수 없는 작업 유형 xyzu   dict를 반환해야 합니다.N)r   r8   dictr5   s     r   #test_unknown_task_type_returns_dictz7TestUnknownTaskType.test_unknown_task_type_returns_dict   s#    DE&$'J)JJ'r   c                P    t        d      }|d   }||t        v sJ d|d       yy)u]   알 수 없는 작업 유형이면 recommended_skill이 None이거나 유효한 스킬이다.u   알수없음abcXYZ123r   Nu   반환된 스킬 u-   이 None도 유효한 스킬도 아닙니다.)r   VALID_SKILLS)r!   r#   skills      r   2test_unknown_task_type_returns_none_or_valid_skillzFTestUnknownTaskType.test_unknown_task_type_returns_none_or_valid_skill   sC    /0*+} 5 	
y(UV	
5 5}r   c                j    	 t        d      }t        |t              sJ y# t        t        f$ r Y yw xY w)u3   빈 문자열 작업 유형도 처리 가능하다. N)r   r8   r[   
ValueErrorKeyErrorr5   s     r   test_empty_string_task_typez/TestUnknownTaskType.test_empty_string_task_type   s7    	BZFfd+++H% 		s     22c                    	 t        d      }t        |t              r-t        t	        |j                               z
  }|r
J d|        yy# t        t        t        f$ r Y yw xY w)u^   알 수 없는 작업 유형에서 dict를 반환할 경우 필수 키가 모두 포함된다.u   알수없음XYZ   필수 키 누락: N)	r   r8   r[   REQUIRED_KEYSsetkeysrc   rd   
SystemExit)r!   r#   missings      r   8test_unknown_task_type_has_required_keys_if_returns_dictzLTestUnknownTaskType.test_unknown_task_type_has_required_keys_if_returns_dict   sh    	-.F&$''#fkkm*<<"C&9'$CC{7 ( Hj1 		s   AA A"!A"NrE   )r(   r)   r*   r+   r\   r`   re   rm   r/   r   r   rY   rY      s    @K

r   rY   c                      e Zd ZdZe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
Zej                  j!                  dg d      dd       Zy)TestOutputStructureu8   route_skill 반환 dict의 구조 및 값 타입 검증.c                    t        d      S )u+   테스트용 샘플 결과 (광고 배너).r   r    )r!   s    r   sample_resultz!TestOutputStructure.sample_result   s     o&&r   c                \    t         t        |j                               z
  }|r
J d|        y)u1   반환 dict에 필수 키가 모두 존재한다.rg   N)rh   ri   rj   )r!   rq   rl   s      r   test_required_keys_presentz.TestOutputStructure.test_required_keys_present   s1    #m&8&8&:";;;1';;{7r   c                    |d   t         v sJ y)u7   recommended_skill 값이 유효한 스킬 이름이다.r   Nr^   r!   rq   s     r   test_recommended_skill_is_validz3TestOutputStructure.test_recommended_skill_is_valid   s    01\AAAr   c                ^    t        |d   t              sJ t        |d         dkD  sJ d       y)u   reason 값이 문자열이다.r   r   u!   reason이 빈 문자열입니다.N)r8   r%   r:   rv   s     r   test_reason_is_stringz)TestOutputStructure.test_reason_is_string   s5    -13777=*+a/T1TT/r   c                .    t        |d   t              sJ y)u$   alternatives 값이 리스트이다.r   Nr8   r9   rv   s     r   test_alternatives_is_listz-TestOutputStructure.test_alternatives_is_list   s    -7>>>r   c                <    |d   D ]  }|t         v rJ d|        y)u<   alternatives의 각 항목이 유효한 스킬 이름이다.r   u#   유효하지 않은 대안 스킬: Nru   )r!   rq   alts      r   'test_alternatives_contains_valid_skillsz;TestOutputStructure.test_alternatives_contains_valid_skills   s3     0 	VC,&U*McW(UU&	Vr   c                .    t        |d   t              sJ y)u    warnings 값이 리스트이다.r   Nr{   rv   s     r   test_warnings_is_listz)TestOutputStructure.test_warnings_is_list   s    -
3T:::r   c                .    t        |d   t              sJ y)u"   input_type 값이 문자열이다.r   N)r8   r%   rv   s     r   test_input_type_is_stringz-TestOutputStructure.test_input_type_is_string  s    -5s;;;r   c                &    |d   |d   vsJ d       y)u<   recommended_skill이 alternatives에 포함되지 않는다.r   r   uB   추천된 스킬이 대안 목록에도 포함되어 있습니다.Nr/   rv   s     r   *test_recommended_skill_not_in_alternativesz>TestOutputStructure.test_recommended_skill_not_in_alternatives  s%    01~9VV 	
P	
Vr   r   )r   r   r   r   r   c                x    t        |      }t        t        |j                               z
  }|rJ d|d|        y)u@   다양한 작업 유형에서 모두 필수 키가 존재한다.r   u   : 필수 키 누락: N)r   rh   ri   rj   )r!   r   r#   rl   s       r   &test_all_task_types_have_required_keysz:TestOutputStructure.test_all_task_types_have_required_keys  s@     	"#fkkm"44Tj5J7)TT{7r   N)r&   r[   )rq   r[   r&   r'   )r   r%   r&   r'   )r(   r)   r*   r+   r,   fixturerq   rs   rw   ry   r|   r   r   r   r   r-   r.   r   r/   r   r   ro   ro      sq    B^^' '<
BU
?V
;<
 [[aU	Ur   ro   c                  8    e Zd ZdZddZddZddZddZddZy)	TestGetSkillRecommendationuN   get_skill_recommendation 함수: 작업 설명 키워드 기반 추천 검증.c                <    t         j                  d      }|dk(  sJ y)u/   '메타 광고 배너 제작' → hybrid-image.u   메타 광고 배너 제작r	   Nr   get_skill_recommendationr5   s     r   (test_meta_ad_banner_returns_hybrid_imagezCTestGetSkillRecommendation.test_meta_ad_banner_returns_hybrid_image!  s!    #<<=Z['''r   c                <    t         j                  d      }|dk(  sJ y)u1   '보험 카드뉴스 제작' → satori-cardnews.u   보험 카드뉴스 제작r
   Nr   r5   s     r   &test_insurance_cardnews_returns_satorizATestGetSkillRecommendation.test_insurance_cardnews_returns_satori&  s"    #<<=YZ****r   c                <    t         j                  d      }|dk(  sJ y)u(   'SNS 홍보 이미지' → gemini-image.u   SNS 홍보 이미지r   Nr   r5   s     r   #test_sns_promo_image_returns_geminiz>TestGetSkillRecommendation.test_sns_promo_image_returns_gemini+  s!    #<<=ST'''r   c                R    t         j                  d      }t        |t              sJ y)u   반환값이 문자열이다.r   N)r   r   r8   r%   r5   s     r   test_return_type_is_stringz5TestGetSkillRecommendation.test_return_type_is_string0  s"    #<<_M&#&&&r   c                B    t         j                  d      }|t        v sJ y)u+   반환값이 유효한 스킬 이름이다.u   카드뉴스 만들기N)r   r   r^   r5   s     r    test_return_value_is_valid_skillz;TestGetSkillRecommendation.test_return_value_is_valid_skill5  s!    #<<=UV%%%r   NrE   )	r(   r)   r*   r+   r   r   r   r   r   r/   r   r   r   r     s    X(
+
(
'
&r   r   c                  L    e Zd ZdZeZd
dZddZddZddZ	ddZ
ddZddZy	)TestCLIIntegrationu;   subprocess로 CLI 직접 실행하여 통합 동작 검증.c                t    t         j                  | j                  g|z   }t        j                  |fdddd|S )u9   CLI를 subprocess로 실행하고 결과를 반환한다.T   )capture_outputtexttimeout)sys
executable_SCRIPT
subprocessrun)r!   argsr   cmds       r   _runzTestCLIIntegration._runE  sE    ~~t||,t3~~
	

 
 	
r   c                    | j                  ddg      }|j                  dk(  s.J d|j                   d|j                   d|j                          y)u>   --type '광고 배너' 실행 시 exit code 0을 반환한다.--typer   r   
exit code 	
stdout: 	
stderr: N)r   
returncodestdoutstderrr5   s     r   test_basic_run_exits_zeroz,TestCLIIntegration.test_basic_run_exits_zeroP  sU    Ho67  A% 	
**+:fmm_Jv}}o^	
%r   c                   | j                  g d      }|j                  dk(  s!J d|j                   d|j                          	 t        j                  |j
                        }t        t              sJ d       y# t        j                  $ r/}t        j                  d| d|j
                         Y d}~Ud}~ww xY w)	u;   --json 플래그 사용 시 출력이 유효한 JSON이다.r   r   --jsonr   r   r   u   JSON 파싱 실패: r   Nu'   JSON 최상위가 dict여야 합니다.)r   r   r   jsonloadsr   JSONDecodeErrorr,   failr8   r[   )r!   r#   parsedes       r   test_json_output_is_parseablez0TestCLIIntegration.test_json_output_is_parseableW  s    @A  A% 	
**+:fmm_E	
%	OZZ.F &$'R)RR' ## 	OKK.qcFMM;LMNN	Os   A< <B>%B99B>c                    | j                  g d      }|j                  dk(  sJ t        j                  |j                        }t
        t        |j                               z
  }|r
J d|        y)u3   --json 출력에 필수 키가 모두 포함된다.r   r   u"   JSON 출력에 필수 키 누락: N)r   r   r   r   r   rh   ri   rj   )r!   r#   r   rl   s       r   "test_json_output_has_required_keysz5TestCLIIntegration.test_json_output_has_required_keysc  sc    @A  A%%%FMM*#fkkm"44J@	JJ{7r   c                    | j                  g d      }|j                  dk(  s!J d|j                   d|j                          t        j                  |j
                        }|d   dk(  sJ d|d   d       y	)
u>   --urgent 플래그 사용 시 satori-cardnews가 추천된다.)r   r   z--urgentr   r   r   r   r   r
   u%   urgent 모드에서 satori가 아닌 u
    추천됨N)r   r   r   r   r   r   r!   r#   r   s      r   "test_urgent_flag_recommends_satoriz5TestCLIIntegration.test_urgent_flag_recommends_satorik  s    LM  A% 	
**+:fmm_E	
% FMM*)*.?? 	
3F;N4O3RR\]	
?r   c                R    | j                  g       }|j                  dk7  sJ d       y)u>   인수 없이 실행하면 non-zero exit code를 반환한다.r   u@   인수 없이 실행했는데 exit code 0이 반환됐습니다.N)r   r   r5   s     r   test_no_args_exits_nonzeroz-TestCLIIntegration.test_no_args_exits_nonzerov  s-    2  A% 	
N	
%r   c                    | j                  g d      }|j                  dk(  sJ t        j                  |j                        }|d   dk(  sJ y)uK   CLI에서 '카드뉴스' 유형 실행 시 satori-cardnews가 추천된다.)r   r   r   r   r   r
   N)r   r   r   r   r   r   s      r   ,test_cardnews_type_recommends_satori_via_cliz?TestCLIIntegration.test_cardnews_type_recommends_satori_via_cli}  sL    ?@  A%%%FMM*)*.????r   N)r   z	list[str]r&   zsubprocess.CompletedProcessrE   )r(   r)   r*   r+   _MODULE_PATHr   r   r   r   r   r   r   r   r/   r   r   r   r   @  s0    EG	


SK	

@r   r   )r   r%   r&   r[   )$r+   
__future__r   importlib.utilutil_ilur   r   r   typesr,   r   spec_from_file_location_specmodule_from_specr   loaderexec_moduler   	Exceptionexc
ModuleType__annotations__r-   skipif
pytestmarkr^   rh   r   r   r1   rG   rY   ro   r   r   r/   r   r   <module>r      s7  " #    
   A(D(()=|LE...u5	LL/0
 M[[5m_E   
 C Z?
 
D'= '=^= =D   P5U 5Uz& &DB@ B@w  *)))*CD&)M#))*s   A C D#DD