
    i`=              
          d Z ddlZddlZddlZddlZddlZddlZej                  j                  dej                  j                  ej                  j                  ej                  j                  e                         ddlZg dZg 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   keyword_cluster.py 유닛 테스트 (TDD - RED→GREEN).

샘플 데이터 기반 테스트: 보험 도메인 검색어 클러스터링 기능 검증.
    N)   보험료 계산u   보험료 할인   월납 보험료   보험 비용u   납입 방법   보험 종류   보험이란u
   보험 뜻   보험 기초u   보험 설명   보험 가입   가입 절차   청약 서류u   보험 심사u   가입 방법   보험 추천   보험 비교u   보험 순위   보험 후기u   보험 평판   연금 보험   변액 보험   저축 보험u   보험 수익률u   적금 보험)r   r   r	   r   r   r   c                   $    e Zd Zd Zd Zd Zd Zy)TestTokenizeKoreanc                 @    t        j                  d      }|g dk(  sJ y)u6   공백 기반 토큰화가 정상 동작해야 한다.u   보험료 계산 방법)	   보험료u   계산u   방법Nkctokenize_koreanselftokenss     E/home/jay/workspace/tools/geo-analytics/tests/test_keyword_cluster.pytest_basic_tokenizationz*TestTokenizeKorean.test_basic_tokenizationH   s"    ##$=>::::    c                 >    t        j                  d      }|dgk(  sJ y)u2   단어 하나도 리스트로 반환해야 한다.   보험Nr   r   s     r   test_single_wordz#TestTokenizeKorean.test_single_wordM   s"    ##H-(###r   c                 <    t        j                  d      }|g k(  sJ y)u6   빈 문자열은 빈 리스트를 반환해야 한다. Nr   r   s     r   test_empty_stringz$TestTokenizeKorean.test_empty_stringR   s    ##B'||r   c                 R    t        j                  d      }d|v sJ d|v sJ d|v sJ y)u2   여러 공백도 올바르게 처리해야 한다.u   보험  종류  비교r    u   종류u   비교Nr   r   s     r   test_extra_spacesz$TestTokenizeKorean.test_extra_spacesW   s=    ##$<=6!!!6!!!6!!!r   N)__name__
__module____qualname__r   r!   r$   r&    r   r   r   r   G   s    ;
$

"r   r   c                   0    e Zd Zd Zd Zd Zd Zd Zd Zy)TestLoadKeywordsFromCsvc                     |dz  }|j                  dd       t        j                  t        |            }|g dk(  sJ y)uM   헤더 없이 키워드만 있는 CSV를 읽어야 한다 (샘플 데이터).zsample_queries.csvu-   보험료 계산
보험 종류
보험 가입
utf-8encoding)r   r   r	   N
write_textr   load_keywords_from_csvstrr   tmp_pathcsv_filekeywordss       r   test_load_single_column_csvz3TestLoadKeywordsFromCsv.test_load_single_column_csve   sE    22NY`a,,S];QQQQr   c                 @   |dz  }t        t        |      ddd      5 }t        j                  |ddg      }|j	                          |j                  d	d
ddddg       ddd       t        j                  t        |            }d	|v sJ d|v sJ y# 1 sw Y   4xY w)uK   'query' 컬럼 헤더가 있는 CSV를 읽어야 한다 (샘플 데이터).zsample_ga4_queries.csvwr#   r.   newliner0   querysessions
fieldnamesr   100)r>   r?   r   80Nopenr4   csv
DictWriterwriteheader	writerowsr   r3   r   r6   r7   fwriterr8   s         r   &test_load_csv_with_header_query_columnz>TestLoadKeywordsFromCsv.test_load_csv_with_header_query_columnl   s    66#h-b7C 	q^^A7J2GHF ,%@)t< 	 ,,S];!X---(***	 	s   ABBc                 ,   |dz  }t        t        |      ddd      5 }t        j                  |ddg      }|j	                          |j                  d	d
dg       ddd       t        j                  t        |            }d	|v sJ y# 1 sw Y   .xY w)uP   'searchTerm' 컬럼 헤더가 있는 CSV를 읽어야 한다 (샘플 데이터).zsample_searchterm.csvr;   r#   r.   r<   
searchTermcountr@   r   50)rO   rP   NrD   rJ   s         r   $test_load_csv_with_searchterm_columnz<TestLoadKeywordsFromCsv.test_load_csv_with_searchterm_columnz   s    55#h-b7C 	q^^A<2IJF .> 	 ,,S];(***	 	s   ?B

Bc                     t        j                  t              5  t        j                  d       ddd       y# 1 sw Y   yxY w)uJ   존재하지 않는 파일은 FileNotFoundError를 발생시켜야 한다.z/nonexistent/path/queries.csvN)pytestraisesFileNotFoundErrorr   r3   r   s    r   !test_load_nonexistent_file_raisesz9TestLoadKeywordsFromCsv.test_load_nonexistent_file_raises   s7    ]],- 	G%%&EF	G 	G 	Gs	   9Ac                 ~    |dz  }|j                  dd       t        j                  t        |            }|g k(  sJ y)u7   빈 CSV 파일은 빈 리스트를 반환해야 한다.z	empty.csvr#   r.   r/   Nr1   r5   s       r   !test_empty_csv_returns_empty_listz9TestLoadKeywordsFromCsv.test_empty_csv_returns_empty_list   s?    k)B1,,S];2~~r   c                     |dz  }|j                  dd       t        j                  t        |            }|j	                  d      dk(  sJ y)u>   중복 키워드는 제거되어야 한다 (샘플 데이터).zsample_dup.csvu0   보험료 계산
보험료 계산
보험 종류
r.   r/   r      N)r2   r   r3   r4   rP   r5   s       r   test_deduplicationz*TestLoadKeywordsFromCsv.test_deduplication   sS    ..AG 	 	
 ,,S];~~01Q666r   N)	r'   r(   r)   r9   rM   rR   rX   rZ   r]   r*   r   r   r,   r,   d   s"    R+
+G
7r   r,   c                   6    e Zd Zd Zd Zd Zd Zd Zd Zd Z	y)	TestAssignClusterLabelc                 @    t        j                  g d      }|dk(  sJ y)uQ   보험료/비용 키워드는 COST 라벨을 받아야 한다 (샘플 데이터).)r   r   r   COSTNr   assign_cluster_labelr   labels     r   test_cost_cluster_labelz.TestAssignClusterLabel.test_cost_cluster_label   s    ''(abr   c                 @    t        j                  g d      }|dk(  sJ y)uV   보험 종류/뜻 키워드는 LEARNING 라벨을 받아야 한다 (샘플 데이터).)r   r   r   LEARNINGNrb   rd   s     r   test_learning_cluster_labelz2TestAssignClusterLabel.test_learning_cluster_label   s!    ''(Z[
"""r   c                 @    t        j                  g d      }|dk(  sJ y)uQ   가입/절차 키워드는 PROCESS 라벨을 받아야 한다 (샘플 데이터).)r	   r
   r   PROCESSNrb   rd   s     r   test_process_cluster_labelz1TestAssignClusterLabel.test_process_cluster_label   s!    ''([\	!!!r   c                 @    t        j                  g d      }|dk(  sJ y)uO   추천/비교 키워드는 TRUST 라벨을 받아야 한다 (샘플 데이터).)r   r   r   TRUSTNrb   rd   s     r   test_trust_cluster_labelz/TestAssignClusterLabel.test_trust_cluster_label   s!    ''([\r   c                 @    t        j                  g d      }|dk(  sJ y)uT   연금/투자 키워드는 INVESTMENT 라벨을 받아야 한다 (샘플 데이터).)r   r   r   
INVESTMENTNrb   rd   s     r   test_investment_cluster_labelz4TestAssignClusterLabel.test_investment_cluster_label   s!    ''([\$$$r   c                 @    t        j                  g d      }|dk(  sJ y)uG   프리셋과 무관한 키워드는 UNKNOWN 라벨을 받아야 한다.)zxyz abczfoo barztest 123UNKNOWNNrb   rd   s     r   test_unknown_cluster_labelz1TestAssignClusterLabel.test_unknown_cluster_label   s!    ''(JK	!!!r   c                 <    t        j                  g       }|dk(  sJ y)u:   빈 키워드 리스트는 UNKNOWN을 반환해야 한다.rt   Nrb   rd   s     r   #test_empty_keywords_returns_unknownz:TestAssignClusterLabel.test_empty_keywords_returns_unknown   s     ''+	!!!r   N)
r'   r(   r)   rf   ri   rl   ro   rr   ru   rw   r*   r   r   r_   r_      s%    
#
"
 
%
"
"r   r_   c                   N    e 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y)TestClusterKeywordsc                 ^    t        j                  t        d      }d|v sJ d|v sJ d|v sJ y)uL   결과 딕셔너리는 필수 키를 포함해야 한다 (샘플 데이터).   
n_clustersclusterstotal_keywordssilhouette_scoreNr   cluster_keywordsSAMPLE_KEYWORDSr   results     r   $test_returns_dict_with_required_keysz8TestClusterKeywords.test_returns_dict_with_required_keys   s>    $$_CV###6)))!V+++r   c                 `    t        j                  t        d      }t        |d         dk(  sJ y)u[   요청한 클러스터 수만큼 클러스터가 생성되어야 한다 (샘플 데이터).r{   r|   r~   Nr   r   r   lenr   s     r   test_cluster_countz&TestClusterKeywords.test_cluster_count   s,    $$_C6*%&!+++r   c                 h    t        j                  t        d      }|d   t        t              k(  sJ y)uQ   total_keywords가 입력 키워드 수와 일치해야 한다 (샘플 데이터).r{   r|   r   Nr   r   s     r   test_total_keywords_countz-TestClusterKeywords.test_total_keywords_count   s-    $$_C&'3+????r   c                     t        j                  t        d      }h d}|d   D ]N  }|j                  |j	                               r#J d|j                  d       d||j	                         z
           y)	uP   각 클러스터는 필수 필드를 모두 가져야 한다 (샘플 데이터).r{   r|   >   idsizere   r8   representative_keywordpillar_document_suggestionr~   u   클러스터 r   u   에 필드 누락: N)r   r   r   issubsetkeysget)r   r   required_fieldsclusters       r   %test_each_cluster_has_required_fieldsz9TestClusterKeywords.test_each_cluster_has_required_fields   s|    $$_C
 j) 	G"++GLLN; D 122E"W\\^346;	r   c                 x    t        j                  t        d      }t        d |d   D              }||d   k(  sJ y)ua   모든 클러스터의 키워드 합이 total_keywords와 일치해야 한다 (샘플 데이터).r{   r|   c              3   &   K   | ]	  }|d      yw)r   Nr*   ).0cs     r   	<genexpr>zMTestClusterKeywords.test_cluster_keywords_sum_equals_total.<locals>.<genexpr>   s     :!AfI:s   r~   r   N)r   r   r   sum)r   r   totals      r   &test_cluster_keywords_sum_equals_totalz:TestClusterKeywords.test_cluster_keywords_sum_equals_total   s=    $$_C:vj'9::/0000r   c                 `    t        j                  t        d      }d|d   cxk  rdk  sJ  J y)uF   silhouette_score는 -1에서 1 사이여야 한다 (샘플 데이터).r{   r|   g      r   g      ?Nr   r   s     r   test_silhouette_score_rangez/TestClusterKeywords.test_silhouette_score_range   s3    $$_Cv018S88888r   c                     ddl m} t        |j                               dhz  }t	        j
                  t        d      }|d   D ]  }|d   |v rJ  y)	uS   클러스터 라벨은 프리셋 또는 UNKNOWN이어야 한다 (샘플 데이터).r   )INSURANCE_CLUSTER_PRESETSrt   r{   r|   r~   re   N)configr   setr   r   r   r   )r   r   valid_labelsr   r   s        r   %test_label_is_valid_preset_or_unknownz9TestClusterKeywords.test_label_is_valid_preset_or_unknown   sY    4499;<	{J$$_Cj) 	4G7#|333	4r   c                 f    t        j                  t        d      }|d   D ]  }|d   |d   v rJ  y)ue   representative_keyword는 해당 클러스터의 keywords 안에 있어야 한다 (샘플 데이터).r{   r|   r~   r   r8   Nr   r   r   r   s      r   &test_representative_keyword_in_clusterz:TestClusterKeywords.test_representative_keyword_in_cluster   sC    $$_Cj) 	LG34
8KKKK	Lr   c                     t        j                  t        d      }t        |d         dk(  sJ |d   t        t              k(  sJ y)uB   소규모 샘플 데이터도 클러스터링 가능해야 한다.   r|   r~   r   N)r   r   SAMPLE_KEYWORDS_SMALLr   r   s     r   test_small_datasetz&TestClusterKeywords.test_small_dataset  sG    $$%:qI6*%&!+++&'3/D+EEEEr   c                 r    ddg}t        j                  |d      }t        |d         t        |      k  sJ y)uJ   클러스터 수가 키워드 수보다 클 경우 조정되어야 한다.r   r   r{   r|   r~   N)r   r   r   r   r8   r   s      r   *test_cluster_count_capped_at_keyword_countz>TestClusterKeywords.test_cluster_count_capped_at_keyword_count	  s:    1$$X!<6*%&#h-777r   c                     t        j                  t        d      }t        j                  |d      }t        j
                  |      }d|v sJ y)u@   결과는 JSON 직렬화 가능해야 한다 (샘플 데이터).r{   r|   F)ensure_asciir~   N)r   r   r   jsondumpsloads)r   r   json_strparseds       r   test_json_serializablez*TestClusterKeywords.test_json_serializable  s@    $$_C::f59H%V###r   N)r'   r(   r)   r   r   r   r   r   r   r   r   r   r   r   r*   r   r   ry   ry      s<    ,,
@
19
4LF8$r   ry   c                   $    e Zd Zd Zd Zd Zd Zy)TestEdgeCasesc                 z    	 t        j                  dgd      }t        |d         dk(  sJ y# t        $ r Y yw xY w)uO   키워드 1개는 ValueError 또는 단일 클러스터를 반환해야 한다.r   r\   r|   r~   N)r   r   r   
ValueErrorr   s     r   %test_single_keyword_raises_or_returnsz3TestEdgeCases.test_single_keyword_raises_or_returns  sE    	((+1EFvj)*a/// 		s   +. 	::c                     t        j                  t              5  t        j                  g d       ddd       y# 1 sw Y   yxY w)u@   빈 키워드 리스트는 ValueError를 발생시켜야 한다.r   r|   N)rT   rU   r   r   r   rW   s    r   &test_empty_keywords_raises_value_errorz4TestEdgeCases.test_empty_keywords_raises_value_error%  s3    ]]:& 	2q1	2 	2 	2s	   ;Ac                 X    dgdz  dgdz  z   }t        j                  |d      }d|v sJ y)uI   중복 키워드 입력도 처리 가능해야 한다 (샘플 데이터).r   
   r      r|   r~   N)r   r   r   s      r   test_duplicate_keywords_handledz-TestEdgeCases.test_duplicate_keywords_handled*  s<    &'",/@2/EE$$X!<V###r   c                 x    t        j                  t        d      }|d   D ]  }t        |d   t              rJ  y)uK   pillar_document_suggestion은 문자열이어야 한다 (샘플 데이터).r{   r|   r~   r   N)r   r   r   
isinstancer4   r   s      r   )test_pillar_document_suggestion_is_stringz7TestEdgeCases.test_pillar_document_suggestion_is_string0  s@    $$_Cj) 	JGg&BCSIII	Jr   N)r'   r(   r)   r   r   r   r   r*   r   r   r   r     s    2
$Jr   r   c                       e Zd Zd Zd Zd Zy)TestBuildReportc                     t        j                  t        d      }t        |dz        }t        j                  ||       t
        j                  j                  |      sJ y)uF   build_report는 JSON 파일을 생성해야 한다 (샘플 데이터).r{   r|   sample_report.jsonN)r   r   r   r4   build_reportospathexists)r   r6   cluster_resultoutput_paths       r   "test_build_report_writes_json_filez2TestBuildReport.test_build_report_writes_json_file=  sH    ,,_K(%99:
4ww~~k***r   c                    t        j                  t        d      }t        |dz        }t        j                  ||       t        |d      5 }t        j                  |      }ddd       dv sJ d|v sJ d	|v sJ y# 1 sw Y   xY w)
uF   생성된 파일은 유효한 JSON이어야 한다 (샘플 데이터).r{   r|   r   r.   r/   Nr~   r   r   )r   r   r   r4   r   rE   r   load)r   r6   r   r   rK   r   s         r   $test_build_report_content_valid_jsonz4TestBuildReport.test_build_report_content_valid_jsonD  s    ,,_K(%99:
4+0 	"AYYq\F	"V###6)))!V+++		" 	"s   A>>Bc                     t        j                  t        d      }t        j                  |d      }|t	        |t
              sJ yy)u`   output_path가 None이면 파일 생성 없이 결과를 반환해야 한다 (샘플 데이터).r{   r|   N)r   )r   r   r   r   r   dict)r   r   r   s      r   +test_build_report_returns_none_when_no_pathz;TestBuildReport.test_build_report_returns_none_when_no_pathO  s=    ,,_KTB~FD!999!9~r   N)r'   r(   r)   r   r   r   r*   r   r   r   r   <  s    +	,:r   r   c                       e Zd Zd Zd Zy)TestGa4Optionalc                 Z    ddl m}  |       rt        j                  d        |       rJ y)uF   GA4 미설정 시 is_ga4_configured()는 False를 반환해야 한다.r   is_ga4_configuredB   GA4가 설정된 환경에서는 이 테스트를 스킵합니다.N)r   r   rT   skipr   r   s     r   %test_ga4_not_configured_returns_falsez5TestGa4Optional.test_ga4_not_configured_returns_false\  s)    , KK\]$&&&&r   c                     ddl m}  |       rt        j                  d       t        j                  t
              5  t        j                  dd       ddd       y# 1 sw Y   yxY w)uO   GA4 미설정 시 fetch_ga4_keywords는 RuntimeError를 발생시켜야 한다.r   r   r   123456   )property_iddaysN)r   r   rT   r   rU   RuntimeErrorr   fetch_ga4_keywordsr   s     r   2test_fetch_ga4_keywords_raises_when_not_configuredzBTestGa4Optional.test_fetch_ga4_keywords_raises_when_not_configurede  sM    ,KK\]]]<( 	A!!hR@	A 	A 	As   AA&N)r'   r(   r)   r   r   r*   r   r   r   r   [  s    'Ar   r   )__doc__r   r   systempfilerF   rT   r   insertdirnameabspath__file__keyword_clusterr   r   r   r   r,   r_   ry   r   r   r   r*   r   r   <module>r      s   
  	 
  
  277??277??277??83L#MN O B " ":57 57z"" ""TI$ I$bJ J@: :>A Ar   