
    SiP                     \   d 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
mZ ddlZddlmZmZmZmZmZmZmZ ej*                  deeef   fd       Zej*                  d	eeef   deeef   fd
       Zej*                  deeef   dedefd       Zej*                  defd       Zej*                  defd       Zej*                  def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$ G d d       Z% G d! d"      Z&y)#u   
TDD RED 단계: eval runner 테스트 파일
run_evals.py 구현이 없으므로 모든 테스트는 ImportError 또는 FAIL 상태여야 합니다.
    N)Path)Any)	MagicMock	mock_openpatch)check_forbiddencheck_routingevaluate_responsegenerate_reportget_skill_listkeyword_match
load_evalsreturnc                      dddddgg dS )u;   단일 eval 케이스 픽스처 (evals.json 구조 반영).   uw   InsuWiki Google RSA 광고를 처음부터 만들어주세요. 헤드라인 10개와 설명 3개를 만들어주세요.u4  Google RSA 규격(헤드라인 30자 이하, 설명 90자 이하)을 준수하며 최소 10개의 헤드라인과 3개의 설명을 제공해야 합니다. 헤드라인은 독립적으로 의미가 통해야 하며 키워드 중심, 혜택 중심, CTA 중심 헤드라인이 고루 포함되어야 합니다.z/Checks all headlines are 30 characters or fewerz.Provides at least one keyword-focused headlineidpromptexpected_output
assertionsfiles r       Q/home/jay/workspace/.worktrees/task-2117-dev1/tools/eval-runner/test_run_evals.pysample_eval_caser      s/      LZ
 ><
  r   r   c                     d| ddddgg dgdS )u#   evals.json 전체 구조 픽스처.ad-creative   u0   Meta 피드 광고 소재를 만들어주세요.uw   Meta 광고 규격에 맞게 기본 텍스트, 헤드라인, 설명을 각 3가지 변형으로 제공해야 합니다.z@Checks primary text hooks appear within the first 125 charactersr   
skill_nameevalsr   )r   s    r   sample_evals_json_contentr"   2   s3     $L $]ab	
 r   r"   tmp_pathc                     |dz  dz  dz  }|j                  d       |dz  }|j                  t        j                  | d      d	
       |S )u:   임시 evals.json 파일 경로를 반환하는 픽스처.skillsr   r!   Tparents
evals.jsonFensure_asciiutf-8encoding)mkdir
write_textjsondumps)r"   r#   	skill_dir
evals_files       r   evals_json_pathr4   D   sV     8#m3g=IOODO!\)J$**%>US^efr   c                       	 y)u3   ad-creative 스킬의 boundary 문자열 픽스처.u   For campaign strategy, budget allocation, or platform selection, → paid-ads. For social media content strategy, → social-content.r   r   r   r   ad_creative_boundaryr6   N   s    	Ar   c                       	 y)u9   정상 통과 LLM 응답 픽스처 (헤드라인 포함).u$  Google RSA 헤드라인 목록:
1. 보험 약관 3분에 이해하기 (15자)
2. 내 보험 제대로 알고 있나요? (16자)
3. 복잡한 약관 쉽게 풀어드립니다 (16자)
설명: 보험 약관 핵심 내용만 쉽게 정리. 청구 누락 없이 내 보험을 100% 활용하세요.r   r   r   r   llm_response_passr8   W   s    	yr   c                       y)u8   실패하는 LLM 응답 픽스처 (헤드라인 없음).uY   죄송합니다. 광고 캠페인 예산 전략은 paid-ads 스킬을 이용해주세요.r   r   r   r   llm_response_failr:   c   s     gr   c                   f    e Zd ZdZdedeeef   ddfdZdeddfdZ	deddfd	Z
deddfd
ZddZy)TestLoadEvalsu.   evals.json 파일 로딩 및 파싱 테스트.r4   r"   r   Nc                    t        |j                  j                  j                        }t        d|      5  t        d      }ddd       d   dk(  sJ t	        |d   t
              sJ t        |d         dk(  sJ y# 1 sw Y   <xY w)uP   evals.json을 정상적으로 읽어 올바른 구조를 반환하는지 확인.run_evals.SKILLS_BASE_PATHr   Nr    r!   r   )strparentr   r   
isinstancelistlen)selfr4   r"   	base_pathresults        r   'test_load_evals_returns_valid_structurez5TestLoadEvals.test_load_evals_returns_valid_structureq   s     ..55<<=	/; 	/.F	/ l#}444&/40006'?#q(((	/ 	/s   A==Bc                     t        |j                  j                  j                        }t        d|      5  t        d      }ddd       d   d   }d|v sJ d|v sJ d|v sJ d	|v sJ d
|v sJ y# 1 sw Y   0xY w)u`   각 eval 케이스에 id, prompt, expected_output, assertions, files 필드가 있는지 확인.r>   r   Nr!   r   r   r   r   r   r   )r?   r@   r   r   )rD   r4   rE   rF   
first_evals        r   "test_load_evals_parses_eval_fieldsz0TestLoadEvals.test_load_evals_parses_eval_fields   s    
 ..55<<=	/; 	/.F	/ G_Q'
z!!!:%%% J...z)))*$$$	/ 	/s   A11A:r#   c                 v   |dz  dz  dz  }|j                  d       |dz  }|j                  dd	       t        |dz  d
z        }t        dt        |            5  t	        j
                  t        j                  t        f      5  t        d       ddd       ddd       y# 1 sw Y   xY w# 1 sw Y   yxY w)uH   잘못된 JSON 파일일 때 적절한 에러가 발생하는지 확인.r%   z	bad-skillr!   Tr&   r(   z{invalid json: }r+   r,   z..r>   N)
r.   r/   r?   r   pytestraisesr0   JSONDecodeError
ValueErrorr   )rD   r#   r2   bad_jsonrE   s        r   )test_load_evals_invalid_json_raises_errorz7TestLoadEvals.test_load_evals_invalid_json_raises_error   s    x'+5?	%|+.A8+d23	/X? 	( 4 4jAB (;'(	( 	(( (	( 	(s$   *B/B#B/#B,	(B//B8c                    |dz  dz  dz  }|j                  d       |dz  }|j                  t        j                  dg dd	      d
       t	        dt        |            5  t        d      }ddd       d   g k(  sJ y# 1 sw Y   xY w)uH   빈 evals 배열을 가진 evals.json을 처리할 수 있는지 확인.r%   zempty-skillr!   Tr&   r(   r   Fr)   r+   r,   r>   N)r.   r/   r0   r1   r   r?   r   )rD   r#   r2   empty_evalsrF   s        r   !test_load_evals_empty_evals_arrayz/TestLoadEvals.test_load_evals_empty_evals_array   s    x'-7'A	%,.JJmbAPUV 	 	

 /X? 	/.F	/ g"$$$	/ 	/s   #BBc                     t        dd      5  t        j                  t        t        t
        f      5  t        d       ddd       ddd       y# 1 sw Y   xY w# 1 sw Y   yxY w)uO   존재하지 않는 스킬명을 전달하면 에러가 발생하는지 확인.r>   z/nonexistent/pathznonexistent-skillN)r   rL   rM   FileNotFoundErrorOSErrorrO   r   rD   s    r   .test_load_evals_nonexistent_skill_raises_errorz<TestLoadEvals.test_load_evals_nonexistent_skill_raises_error   sZ    /1DE 	0 17JGH 0./0	0 	00 0	0 	0s!   %AAAA	AA$r   N)__name__
__module____qualname____doc__r   dictr?   r   rG   rJ   rQ   rT   rY   r   r   r   r<   r<   n   sp    8)) $(S>) 
	)%% 
% 
($ 
(4 
(%$ %4 %0r   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)	TestKeywordMatchu   keyword_match 함수 테스트.Nc                 D    d}d}t        ||      \  }}}|du sJ d|v sJ y)uK   한글 키워드가 LLM 응답에 포함되어 있을 때 pass=True 반환.uX   헤드라인 10개를 작성해 드리겠습니다: '보험 약관 3분에 이해하기'u7   최소 10개의 헤드라인을 제공해야 합니다.T   헤드라인Nr   rD   responseexpectedpassedmatchedmisseds         r   %test_korean_keyword_found_in_responsez6TestKeywordMatch.test_korean_keyword_found_in_response   s9    mL"/("C~~(((r   c                 X    d}d}t        ||      \  }}}|du sJ t        |      dkD  sJ y)u;   한글 키워드가 응답에 없을 때 pass=False 반환.u:   죄송합니다. 이 요청은 처리할 수 없습니다.u<   헤드라인 10개와 설명 3개를 제공해야 합니다.Fr   N)r   rC   re   s         r   )test_korean_keyword_missing_from_responsez:TestKeywordMatch.test_korean_keyword_missing_from_response   s;    OQ"/("C6{Qr   c                 8    d}d}t        ||      \  }}}|du sJ y)uh   부분 매칭이 지원되는지 확인 (예: '헤드라인 10' 중 '헤드라인'만 있어도 매칭).u'   다음은 헤드라인 목록입니다.u-   헤드라인 10개를 제공해야 합니다.TNrd   re   s         r   'test_partial_keyword_matching_supportedz8TestKeywordMatch.test_partial_keyword_matching_supported   s+    <B"/("C~~r   c                 `    d}d}t        ||      \  }}}|du sJ t        d |D              sJ y)u3   영문 키워드 대소문자 무시 매칭 확인.z*Here are the HEADLINES for google rsa ads.z%Provide headlines for Google RSA ads.Tc              3   B   K   | ]  }|j                         d k(    yw)	headlinesN)lower).0ks     r   	<genexpr>zJTestKeywordMatch.test_english_case_insensitive_matching.<locals>.<genexpr>   s     =1779+=s   N)r   anyre   s         r   &test_english_case_insensitive_matchingz7TestKeywordMatch.test_english_case_insensitive_matching   s?    ?:"/("C~~=W====r   c                 p    d}d}t        ||      \  }}}t        |t              sJ t        |      dkD  sJ y)u?   매칭되지 않은 키워드 목록이 반환되는지 확인.u$   여기 헤드라인만 있습니다.u?   헤드라인과 설명과 CTA를 모두 제공해야 합니다.r   N)r   rA   rB   rC   re   s         r   *test_keyword_match_returns_missed_keywordsz;TestKeywordMatch.test_keyword_match_returns_missed_keywords   s?    9T"/("C&$'''6{Qr   rZ   )	r[   r\   r]   r^   rk   rm   ro   rx   rz   r   r   r   ra   ra      s    ))>r   ra   c                   0    e Zd ZdZddZdeddfdZddZy)	TestCheckForbiddenu!   check_forbidden 함수 테스트.r   Nc                 V    d}d}t        ||      \  }}|du sJ t        |      dkD  sJ y)ua   boundary에 '→ paid-ads' 라우팅 지시가 있을 때, 응답에서 범위 밖 처리 감지.ue   이 요청은 paid-ads 스킬을 사용하여 처리해야 합니다. paid-ads로 라우팅합니다.u1   → paid-ads: 캠페인 전략, 타겟팅, 예산Fr   Nr   rC   rD   rf   boundaryrh   found_forbiddens        r   +test_forbidden_routing_instruction_detectedz>TestCheckForbidden.test_forbidden_routing_instruction_detected   s>    zF"1(H"E ?#a'''r   r6   c                 @    d}t        ||      \  }}|du sJ |g k(  sJ y)u>   정상 응답에 금지 내용이 없을 때 pass=True 반환.uR   헤드라인 10개를 작성해 드립니다. 1. 보험 약관 쉽게 이해하기TN)r   )rD   r6   rf   rh   r   s        r   ,test_no_forbidden_content_in_normal_responsez?TestCheckForbidden.test_no_forbidden_content_in_normal_response   s3    g"1(<P"Q~~"$$$r   c                 V    d}d}t        ||      \  }}|du sJ t        |      dkD  sJ y)uK   보험업법 위반 단정적 표현('무조건', '100%') 감지 테스트.u:   이 보험은 무조건 보장되며 100% 지급됩니다.u6   무조건, 반드시, 100% 등 단정적 표현 금지Fr   Nr~   r   s        r   +test_absolute_guarantee_expression_detectedz>TestCheckForbidden.test_absolute_guarantee_expression_detected  s<    OK"1(H"E?#a'''r   rZ   )r[   r\   r]   r^   r   r?   r   r   r   r   r   r|   r|      s$    +	(%QT %Y] %(r   r|   c                   0    e Zd ZdZdeddfdZdeddfdZy)TestCheckRoutingu   check_routing 함수 테스트.r6   r   Nc                 n    d}t        ||      \  }}t        |t              sJ t        |t              sJ y)u[   boundary에 '→ paid-ads' 지시가 있고 응답이 올바르게 위임할 때 pass=True.uf   캠페인 예산 전략은 paid-ads 스킬에서 담당합니다. 해당 스킬을 이용해주세요.N)r	   rA   boolr?   rD   r6   rf   rh   reasons        r   &test_routing_to_correct_skill_detectedz7TestCheckRouting.test_routing_to_correct_skill_detected  s8    {&x1EF&$'''&#&&&r   c                 @    d}t        ||      \  }}|du sJ |dk7  sJ y)uK   스킬 범위 밖 요청에 대해 LLM이 적절히 거부하는지 확인.uX   예산 전략: CPC 300원, 일예산 50만원으로 설정하세요. 최적 타겟은...F N)r	   r   s        r   &test_routing_refusal_when_out_of_scopez7TestCheckRouting.test_routing_refusal_when_out_of_scope  s3     n&x1EF||r   )r[   r\   r]   r^   r?   r   r   r   r   r   r   r     s-    )'3 'SW '3 SW r   r   c                       e Zd ZdZdeeef   deddfdZdeeef   deddfdZdeeef   deddfd	Z	deeef   deddfd
Z
y)TestEvaluateResponseu*   evaluate_response 함수 통합 테스트.r   r8   r   Nc                 d    t        ||      }t        |t              sJ d|v sJ d|v sJ d|v sJ y)uL   evaluate_response가 필수 키를 포함한 dict를 반환하는지 확인.rh   eval_iddetailsN)r
   rA   r_   rD   r   r8   rF   s       r   6test_evaluate_response_returns_dict_with_required_keyszKTestEvaluateResponse.test_evaluate_response_returns_dict_with_required_keys%  sL     ##46FG&$'''6!!!F"""F"""r   c                 .    t        ||      }|d   du sJ y)uE   헤드라인을 포함한 정상 응답에 대해 passed=True 반환.rh   TNr
   r   s       r    test_evaluate_response_pass_casez5TestEvaluateResponse.test_evaluate_response_pass_case2  s%     ##46FGh4'''r   r:   c                 .    t        ||      }|d   du sJ y)u9   키워드가 없는 응답에 대해 passed=False 반환.rh   FNr   )rD   r   r:   rF   s       r    test_evaluate_response_fail_casez5TestEvaluateResponse.test_evaluate_response_fail_case<  s%     ##46FGh5(((r   c                 6    t        ||      }|d   |d   k(  sJ y)u,   결과에 eval id가 포함되는지 확인.r   r   Nr   r   s       r   'test_evaluate_response_includes_eval_idz<TestEvaluateResponse.test_evaluate_response_includes_eval_idF  s+     ##46FGi $4T$::::r   )r[   r\   r]   r^   r_   r?   r   r   r   r   r   r   r   r   r   r   "  s    4#sCx.# # 
	#(sCx.( ( 
	()sCx.) ) 
	);sCx.; ; 
	;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)	TestCLIParsingu   CLI 인수 파싱 테스트.Nc                 n    ddl }ddlm}  |       }|j                  ddg      }|j                  dk(  sJ y)uB   --skill ad-creative 인수가 올바르게 파싱되는지 확인.r   Ncreate_argument_parser--skillr   )argparse	run_evalsr   
parse_argsskill)rD   r   r   parserargss        r   test_parse_skill_argumentz(TestCLIParsing.test_parse_skill_argumentY  s9     	5')  )]!;<zz]***r   c                 b    ddl m}  |       }|j                  dg      }|j                  du sJ y)u7   --all 플래그가 올바르게 파싱되는지 확인.r   r   z--allTN)r   r   r   allrD   r   r   r   s       r   test_parse_all_flagz"TestCLIParsing.test_parse_all_flage  s1    4')  '+xx4r   c                 d    ddl m}  |       }|j                  g d      }|j                  du sJ y)u;   --verbose 플래그가 올바르게 파싱되는지 확인.r   r   )r   r   z	--verboseTN)r   r   r   verboser   s       r   test_parse_verbose_flagz&TestCLIParsing.test_parse_verbose_flagn  0    4')  !HI||t###r   c                 d    ddl m}  |       }|j                  g d      }|j                  du sJ y)u;   --dry-run 플래그가 올바르게 파싱되는지 확인.r   r   )r   r   z	--dry-runTN)r   r   r   dry_runr   s       r   test_parse_dry_run_flagz&TestCLIParsing.test_parse_dry_run_flagw  r   r   c                     ddl m} t        j                  t        t
        f      5   |d       ddd       y# 1 sw Y   yxY w)uI   존재하지 않는 스킬명에 대해 에러가 발생하는지 확인.r   )validate_skill_nameznonexistent-skill-xyz-123N)r   r   rL   rM   rO   
SystemExit)rD   r   s     r   $test_invalid_skill_name_raises_errorz3TestCLIParsing.test_invalid_skill_name_raises_error  s4    1]]J
34 	= ;<	= 	= 	=s	   	8ArZ   )	r[   r\   r]   r^   r   r   r   r   r   r   r   r   r   r   V  s    &
+ $$=r   r   c                       e Zd ZdZej
                  deeee	f      fd       Z
deeee	f      deddfdZdeeee	f      deddfdZdeeee	f      deddfd	Zy)
TestResultSummaryu-   결과 요약 및 보고서 생성 테스트.r   c           	      N    dddi ddddi dddddd	gidd
ddi ddddddgidgS )u,   pass/fail이 섞인 결과 목록 픽스처.r   r   T)r   r    rh   r   r      Frj   rc      copywriting   CTAr   rX   s    r   mixed_resultszTestResultSummary.mixed_results  sa     $SUV$SUV%U]`n_oTpq$SUV%U]`e_fTgh
 	
r   r   r#   Nc                     t        |dz        }t        ||      }t        |t               sJ t        j                  |      }d|v sJ |d   d   d   }t        |dz
        dk  sJ y)u;   스킬별 pass rate가 올바르게 계산되는지 확인.report.jsonskill_resultsr   	pass_rategUUUUUU?{Gz?N)r?   r   rA   r0   loadsabs)rD   r   r#   report_pathreportreport_dataad_creative_rates          r    test_skill_pass_rate_calculationz2TestResultSummary.test_skill_pass_rate_calculation  sw    (]23 <&#&&&jj(+---&7F{S#e+,t333r   c                     t        |dz        }t        ||      }t        j                  |      }d|v sJ t	        |d   dz
        dk  sJ y)u8   전체 pass rate가 올바르게 계산되는지 확인.r   total_pass_rateg333333?r   N)r?   r   r0   r   r   )rD   r   r#   r   r   r   s         r    test_total_pass_rate_calculationz2TestResultSummary.test_total_pass_rate_calculation  sW    (]23 <jj( K///;01E9:TAAAr   c                     t        |dz        }t        ||      }t        j                  |      }d|v sJ t	        |d         dk(  sJ |d   D ]  }d|v sJ d|v rJ  y)u:   FAIL 케이스에 상세 정보가 포함되는지 확인.r   failed_casesr   r   r   N)r?   r   r0   r   rC   )rD   r   r#   r   r   r   	fail_cases          r   test_fail_cases_include_detailsz1TestResultSummary.test_fail_cases_include_details  s    (]23 <jj(,,,;~./1444$^4 	*I	)))	)))	*r   )r[   r\   r]   r^   rL   fixturerB   r_   r?   r   r   r   r   r   r   r   r   r   r   r     s    7^^
tDcN3 
 
	4d4S>>R 	4^b 	4gk 	4Bd4S>>R B^b Bgk B
*T$sCx.=Q 
*]a 
*fj 
*r   r   c                   @    e Zd ZdZdeddfdZdeddfdZdeddfdZy)
TestDryRunu   dry-run 모드 테스트.r4   r   Nc                 .   ddl m} t        |j                  j                  j                        }t	               }t        d|      5  t        d|      5   |dd       ddd       ddd       |j                          y# 1 sw Y   "xY w# 1 sw Y   &xY w)	u8   dry-run 모드에서 LLM API 호출이 없는지 확인.r   run_evals_for_skillr>   zrun_evals.call_llmr   Tr   N)r   r   r?   r@   r   r   assert_not_called)rD   r4   r   rE   mock_llm_calls        r   "test_dry_run_does_not_call_llm_apiz-TestDryRun.test_dry_run_does_not_call_llm_api  s    
 	2..55<<=	!/; 	A+]; A#M4@A	A 	'')A A	A 	As$   BA?B?B	BBc                     ddl m} t        |j                  j                  j                        }t	        d|      5   |dd      }ddd       J d|v sJ |d   du sJ y# 1 sw Y   xY w)	uC   dry-run 모드에서 evals 구조 검증이 수행되는지 확인.r   r   r>   r   Tr   Nr   r   r   r?   r@   r   rD   r4   r   rE   rF   s        r   &test_dry_run_validates_evals_structurez1TestDryRun.test_dry_run_validates_evals_structure  s    
 	2..55<<=	/; 	F(EF	F !!!F"""i D(((	F 	Fs   A##A,c                     ddl m} t        |j                  j                  j                        }t	        d|      5   |dd      }ddd       dv sJ |d   d	k(  sJ y# 1 sw Y   xY w)
uB   dry-run 모드에서 eval 케이스 수를 반환하는지 확인.r   r   r>   r   Tr   N
eval_countr   r   r   s        r   test_dry_run_returns_eval_countz*TestDryRun.test_dry_run_returns_eval_count  sv    
 	2..55<<=	/; 	F(EF	F v%%%l#q(((		F 	Fs   A  A))r[   r\   r]   r^   r   r   r   r   r   r   r   r   r     sJ    #** 
* )) 
)")) 
)r   r   c                   (    e Zd ZdZddZddZddZy)TestGetSkillListu    get_skill_list 함수 테스트.Nc                 <    t               }t        |t              sJ y)u6   get_skill_list가 리스트를 반환하는지 확인.N)r   rA   rB   rD   rF   s     r    test_get_skill_list_returns_listz1TestGetSkillList.test_get_skill_list_returns_list  s    !&$'''r   c                 $    t               }d|v sJ y)u7   ad-creative 스킬이 목록에 포함되는지 확인.r   N)r   r   s     r   (test_get_skill_list_includes_ad_creativez9TestGetSkillList.test_get_skill_list_includes_ad_creative  s    !&&&r   c                 8    t               }t        |      dkD  sJ y)u/   스킬 목록이 비어있지 않은지 확인.r   N)r   rC   r   s     r   )test_get_skill_list_returns_nonempty_listz:TestGetSkillList.test_get_skill_list_returns_nonempty_list  s    !6{Qr   rZ   )r[   r\   r]   r^   r   r   r   r   r   r   r   r     s    *(
'
r   r   )'r^   r0   ostempfilepathlibr   typingr   unittest.mockr   r   r   rL   r   r   r	   r
   r   r   r   r   r   r_   r?   r   r"   r4   r6   r8   r:   r<   ra   r|   r   r   r   r   r   r   r   r   r   <module>r      s  
  	    5 5    $sCx.  $ S#X 4S>  " tCH~  RV   c   3   g3 g g@0 @0P2 2j( (B ,,; ,;h/= /=n,* ,*h1) 1)r r   