
    %~i                        d 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
Z
ddlmZ ddlmZmZmZ ddlmZmZ ddlmZ h dZd Zd	 Zd
 Z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!d%de"de"fdZ# G d d      Z$ G d d       Z% G d! d"      Z& G d# d$      Z'y)&uK   
knowledge_extractor_v2 단위 테스트
테스터: 모리건 (개발3팀)
    N)patch)_cleanup_checkpoints_llm_refine_thread_splitsextract_knowledge_v2ChatMessageMessageType)InsightType>   idtagstypetitleanswerexpertsummarycategoryquestion
confidence
key_points
raw_threadsource_chatsource_dateparticipantsrelated_topicsc            	          t        ddddt        j                        t        ddddt        j                        t        dd	dd
t        j                        gS )u)   #궁금증 태그가 있는 Q&A 메시지
2025-12-0317:00u   질문자/인카/서울uI   #궁금증
• 유형 : 보상
광응고술이 수술에 해당하나요?datetimeusercontentr   17:05   이해철/프라임/부산u?   네 수술에 해당합니다. 약관이 개정되었습니다.z17:06uQ   레이저를 이용한 광응고술도 수술약관에 명시되어 있습니다.r   r	   MESSAGE     r/home/jay/projects/insuwiki/.worktrees/task-2064-dev6/scripts/kakao_knowledge/tests/test_knowledge_extractor_v2.py_make_qa_messagesr*   /   si     	*a$$	
 	-U$$	
 	-g$$	
 r(   c            	          t        ddddt        j                        t        ddddt        j                        t        dd	dd
t        j                        gS )u1   전문가 의견 공유 메시지 (태그 없음)r   z21:42r$   u   다이어트 약, 이제 정말 주의하셔야 합니다.
비만약 처방받은 사실을 고지하지 않았다는 이유로 보험사에서 면책한 사례가 나왔습니다.r   z21:44   박유진/인카/서울u4   비만약은 건강이음에도 안뜨지 않아요?z21:45u>   건강이음은 투약처방일수까지 알 수 없습니다.r%   r'   r(   r)   _make_expert_opinion_messagesr-   J   sl     	- L$$	
 	*J$$	
 	-T$$	
 r(   c            	          t        ddddt        j                        t        ddddt        j                        t        ddd	d
t        j                        gS )u   경고/주의 유형 메시지
2025-12-0410:00u   전문가A/프라임/부산uV   주의하세요! 이번 달부터 고지의무 위반 심사가 강화되었습니다.r   z10:02uE   특히 비만약 관련 고지 누락은 절대 하면 안 됩니다.10:05u   설계사B/KB/대전u;   감사합니다. 고객들한테 꼭 안내해야겠네요.r%   r'   r(   r)   _make_warning_messagesr2   e   si     	.l$$	
 	.[$$	
 	'Q$$	
 r(   c            	          t        ddddt        j                        t        ddddt        j                        t        dd	d
dt        j                        gS )u+   노이즈 메시지만 (인사, 환영 등)r   z17:38u   오픈채팅봇u5   📚 한 발 앞서가는 설계사, 환영합니다.r   z17:40u   새회원/인카/서울   안녕하세요z17:41u   기존회원/프라임/부산u   안녕하세요!)r   r	   BOTr&   r'   r(   r)   _make_noise_messagesr6      sg     	"K	
 	*%$$	
 	0&$$	
 r(   c                   L    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)TestRuleBasedExtractionu.   규칙 기반 추출 (use_llm=False) 테스트c                    t        g d      }g }||k(  }|st        j                  d|fd||f      dt        j                         v st        j
                  |      rt        j                  |      ndt        j                  |      dz  }t        j                  d      dz   d	|iz  }t        t        j                  |            d
x}}y
)u0   빈 메시지 리스트 → 빈 리스트 반환Fuse_llm==)z%(py0)s == %(py3)sresultpy0py3u=   빈 메시지 입력 시 빈 리스트가 반환되어야 함
>assert %(py5)spy5N)
r   
@pytest_ar_call_reprcompare@py_builtinslocals_should_repr_global_name	_saferepr_format_assertmsgAssertionError_format_explanation)selfr>   @py_assert2@py_assert1@py_format4@py_format6s         r)   &test_empty_messages_returns_empty_listz>TestRuleBasedExtraction.test_empty_messages_returns_empty_list   so    %b%8\v|\\\v\\\\\\v\\\v\\\\\\\\\\\\\r(   c                    t        ddddt        j                        g}t        |d      }t	        |      }d}||k(  }|st        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  }t        j                  d      dz   d|iz  }t        t        j                  |            dx}x}}y)uQ   단일 메시지 스레드 → 인사이트 제외 (최소 2개 메시지 필요)r   r      A/인카/서울u.   #궁금증
• 유형 : 보상
질문입니다r   Fr:   r   r<   z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py6)slenr>   r@   py1rA   py6u>   단일 메시지 스레드는 결과에서 제외되어야 함
>assert %(py8)spy8Nr   r	   r&   r   rV   rD   rE   rF   rG   rH   rI   rJ   rK   rL   rM   messagesr>   rN   @py_assert5@py_assert4@py_format7@py_format9s           r)   #test_single_message_thread_excludedz;TestRuleBasedExtraction.test_single_message_thread_excluded   s     !&J ((
 &h>6{aaa{aaaa{aaaaaaasaaasaaaaaa6aaa6aaa{aaaaaaa!aaaaaaaar(   c                 &   t        t               d      }t        |      }d}||k\  }|st        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  }t        j                  d	      d
z   d|iz  }t        t        j                  |            dx}x}}|D cg c]  }|d   	 }}d}	|	|v }|st        j                  d|fd|	|f      t        j                  |	      dt        j                         v st        j                  |      rt        j                  |      nddz  }
t        j                  d|       dz   d|
iz  }t        t        j                  |            dx}	}yc c}w )u7   #궁금증 태그 Q&A → qa 유형 인사이트 추출Fr:      >=z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} >= %(py6)srV   r>   rW   uE   #궁금증 태그 메시지에서 인사이트가 추출되어야 함rZ   r[   Nr   qainz%(py1)s in %(py3)stypesrX   rA   u4   qa 유형 인사이트가 없음. 추출된 타입: rB   rC   r   r*   rV   rD   rE   rF   rG   rH   rI   rJ   rK   rL   )rM   r>   rN   r_   r`   ra   rb   rrm   @py_assert0rP   rQ   s               r)   $test_question_tag_creates_qa_insightz<TestRuleBasedExtraction.test_question_tag_creates_qa_insight   s3   %&7&95I6{hah{ahhh{ahhhhhhshhhshhhhhh6hhh6hhh{hhhahhh!hhhhhhhh$*+q6++\tu}\\\tu\\\t\\\\\\u\\\u\\\\ TUZT[\\\\\\\ ,s   Hc                 ^   t        ddddt        j                        t        ddddt        j                        g}t        |d	
      }t	        |      dkD  r|D cg c]  }|d   	 }}d}||v }|st        j                  d|fd||f      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      nddz  }t        j                  d|       dz   d|iz  }t        t        j                  |            dx}}yyc c}w )u1   '질문 드립니다' 패턴 → qa 유형 추출r   r   rT   u3   질문 드립니다. 실비 청구 관련해서요.r   17:02   B/프라임/부산uL   실비 청구는 진단서와 영수증을 함께 제출하시면 됩니다.Fr:   r   r   ri   rj   rl   rm   rn   uW   '질문 드립니다' 패턴에서 qa 유형이 추출되어야 함. 추출된 타입: rB   rC   Nr   r	   r&   r   rV   rD   rE   rI   rF   rG   rH   rJ   rK   rL   	rM   r^   r>   rp   rm   rq   rN   rP   rQ   s	            r)   (test_question_pattern_creates_qa_insightz@TestRuleBasedExtraction.test_question_pattern_creates_qa_insight   s?    !&M (( !)f ((
  &h>v;?(./1QvY/E/qq_p_pqq qgpgp q qjpjpq qXpXp q qgpgp q q_p_phinhopq q q]p]pq q /   D*c                    t        t               d      }t        |      }d}||k\  }|st        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  }t        j                  d	      d
z   d|iz  }t        t        j                  |            dx}x}}|D ]  }t        t        |j                               z
  }| }	|	s~t        j                  d|       dz   ddt        j                         v st        j                  |      rt        j                  |      ndiz  }
t        t        j                  |
            d}	 y)u2   모든 필수 키가 결과에 존재해야 한다Fr:   re   rf   rh   rV   r>   rW   u(   결과가 비어있어 키 검증 불가rZ   r[   N   필수 키 누락: 
>assert not %(py0)sr@   missing)r   r*   rV   rD   rE   rF   rG   rH   rI   rJ   rK   rL   REQUIRED_V2_KEYSsetkeys)rM   r>   rN   r_   r`   ra   rb   entryr}   rO   @py_format2s              r)   test_all_required_keys_presentz6TestRuleBasedExtraction.test_all_required_keys_present   s   %&7&95I6{KaK{aKKK{aKKKKKKsKKKsKKKKKK6KKK6KKK{KKKaKKK!KKKKKKKK 	@E&UZZ\)::G;?;??"5gY ???????w???w??????	@r(   c                 V   t        t               d      }t        |      }d}||k\  }|st        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  }t        j                  d	      d
z   d|iz  }t        t        j                  |            dx}x}}|D ]  }|d   j                  d      }t        |      }d}||k(  }|st        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  }t        j                  d|d    d      d
z   d|iz  }t        t        j                  |            dx}x}}|d   }	|	j                  } |       }|s|t        j                  d|d          dz   t        j                  |	      t        j                  |      t        j                  |      d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t        j                  |      t        j                  |      t        j                  |      dz  }t        j                  d|d          dz   d|iz  }t        t        j                  |            dx}x}x}} y)u/   id 포맷: 'insight-001' 형식이어야 한다Fr:   re   rf   rh   rV   r>   rW   u'   결과가 비어있어 id 검증 불가rZ   r[   Nr   -   r<   rU   partsu   id 형식 오류: u1    (하이픈으로 분리된 2부분이어야 함)u&   id 숫자 부분이 숫자가 아님: zD
>assert %(py5)s
{%(py5)s = %(py3)s
{%(py3)s = %(py1)s.isdigit
}()
})rX   rA   rC      )z0%(py4)s
{%(py4)s = %(py0)s(%(py2)s)
} == %(py7)s)r@   py2py4py7u'   id 숫자 부분이 3자리가 아님: z
>assert %(py9)spy9)r   r*   rV   rD   rE   rF   rG   rH   rI   rJ   rK   rL   splitisdigit)rM   r>   rN   r_   r`   ra   rb   r   r   rq   rQ   rO   @py_assert3@py_assert6@py_format8@py_format10s                   r)   test_id_format_insight_prefixz5TestRuleBasedExtraction.test_id_format_insight_prefix   s	   %&7&95I6{JaJ{aJJJ{aJJJJJJsJJJsJJJJJJ6JJJ6JJJ{JJJaJJJ!JJJJJJJJ 	\E$K%%c*EE
ccac cQbQbcac c\b\bc cJbJb c cYbYb c c\b\bc cJbJb c cYbYb c cYbYb c cYbYb  c cQbQb#E$K=0abc c cObObc c 8Z8##Z#%Z%ZZ)OPUVWPXz'ZZZZ8ZZZ#ZZZ%ZZZZZZQx[3x=[A[=A%[[[=A[[[[[[3[[[3[[[x[[[=[[[A[[[)PQVWXQYPZ'[[[[[[[[	\r(   c                    t        t               d      }t        |      }d}||k\  }|st        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  }t        j                  d	      d
z   d|iz  }t        t        j                  |            dx}x}}h d}|D ]  }|d   }	|	|v }|st        j                  d|fd|	|f      t        j                  |	      dt        j                         v st        j                  |      rt        j                  |      nddz  }
t        j                  d|d          dz   d|
iz  }t        t        j                  |            dx}	} y)u9   confidence 값은 high/medium/low 중 하나여야 한다Fr:   re   rf   rh   rV   r>   rW   u/   결과가 비어있어 confidence 검증 불가rZ   r[   N>   lowhighmediumr   rj   rl   valid_confidencern   u$   유효하지 않은 confidence 값: rB   rC   ro   )rM   r>   rN   r_   r`   ra   rb   r   r   rq   rP   rQ   s               r)   test_confidence_is_valid_valuez6TestRuleBasedExtraction.test_confidence_is_valid_value   s   %&7&95I6{RaR{aRRR{aRRRRRRsRRRsRRRRRR6RRR6RRR{RRRaRRR!RRRRRRRR4 	LEl#L#'77L:K:KL#'7L LBK) $L LEKVL L3K3K (8L LBK) (8L L:K:K5eL6I5JKL L L8K8KL L	Lr(   c                 \   t        t               d      }t        |      }d}||k\  }|st        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  }t        j                  d	      d
z   d|iz  }t        t        j                  |            dx}x}}t        D ch c]  }|j                   }}|D ]  }	|	d   }
|
|v }|st        j                  d|fd|
|f      t        j                  |
      dt        j                         v st        j                  |      rt        j                  |      nddz  }t        j                  d|	d    d|       dz   d|iz  }t        t        j                  |            dx}
} yc c}w )u8   type 값은 InsightType enum 값 중 하나여야 한다Fr:   re   rf   rh   rV   r>   rW   u)   결과가 비어있어 type 검증 불가rZ   r[   Nr   rj   rl   valid_typesrn   u   유효하지 않은 type 값: u   . 허용값: rB   rC   )r   r*   rV   rD   rE   rF   rG   rH   rI   rJ   rK   rL   r
   value)rM   r>   rN   r_   r`   ra   rb   er   r   rq   rP   rQ   s                r)   test_type_is_valid_insight_typez7TestRuleBasedExtraction.test_type_is_valid_insight_type   s   %&7&95I6{LaL{aLLL{aLLLLLLsLLLsLLLLLL6LLL6LLL{LLLaLLL!LLLLLLLL(341qww44 	ZEfZ,ZHYHYZZ ZPYPY Z ZSYSYZ ZAYAY "-Z ZPYPY "-Z ZHYHY/fmK=YZ Z ZFYFYZ Z	Z 5s   H)c                 $   t        ddddt        j                        t        ddddt        j                        g}t        |d	
      }t	        |      }d}||k\  }|st        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  }t        j                  d      dz   d|iz  }t        t        j                  |            dx}x}}|D ]  }dj                  |j                  dg             }	d}
|
|	v}|st        j                  d|fd|
|	f      t        j                  |
      dt        j                         v st        j                  |	      rt        j                  |	      nddz  }t        j                  d      dz   d|iz  }t        t        j                  |            dx}
}d}
|
|	v }|st        j                  d |fd!|
|	f      t        j                  |
      dt        j                         v st        j                  |	      rt        j                  |	      nddz  }t        j                  d"      dz   d|iz  }t        t        j                  |            dx}
} y)#u)   전화번호가 마스킹되어야 한다r   r   rT   uJ   #궁금증
• 유형 : 보상
문의사항 있으시면 연락 주세요.r   r#   ru   u+   문의는 010-1234-5678로 연락주세요.Fr:   re   rf   rh   rV   r>   rW   u;   전화번호 마스킹 테스트를 위한 결과가 없음rZ   r[   N
r   z010-1234-5678)not in)z%(py1)s not in %(py3)sraw_textrn   u0   전화번호가 마스킹되지 않고 노출됨rB   rC   z***-****-****rj   rl   u6   전화번호 마스킹 형식(***-****-****)이 없음)r   r	   r&   r   rV   rD   rE   rF   rG   rH   rI   rJ   rK   rL   joinget)rM   r^   r>   rN   r_   r`   ra   rb   r   r   rq   rP   rQ   s                r)   test_phone_number_maskedz0TestRuleBasedExtraction.test_phone_number_masked   s1    !&f (( !)E ((
  &h>6{^a^{a^^^{a^^^^^^s^^^s^^^^^^6^^^6^^^{^^^a^^^!^^^^^^^^ 	HEyy<!<=H"f?(2fff?(fff?ffffff(fff(ffff4fffffffH8+H6G6GH8H H>Gi  H HAGH H/G/G $,H H>Gi $,H H6G6GGH H H4G4GH H	Hr(   c                    t        t               d      }t        |      }d}||k(  }|st        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  }t        j                  d	|       d
z   d|iz  }t        t        j                  |            dx}x}}y)ui   노이즈 메시지(인사, 봇 메시지)만 있는 경우 인사이트를 추출하지 않아야 한다Fr:   r   r<   rU   rV   r>   rW   uH   노이즈 메시지에서 인사이트가 추출되면 안 됨. 결과: rZ   r[   N)r   r6   rV   rD   rE   rF   rG   rH   rI   rJ   rK   rL   )rM   r>   rN   r_   r`   ra   rb   s          r)    test_noise_messages_filtered_outz8TestRuleBasedExtraction.test_noise_messages_filtered_out  sC   %&:&<eLK	_	_1	_ 	_M^M^	_1	_ 	_X^X^	_ 	_F^F^ 	_ 	_U^U^ 	_ 	_X^X^	_ 	_F^F^ 	_ 	_U^U^ 	_ 	_U^U^ 	_ 	_U^U^ 	_ 	_M^M^UV\U]^	_ 	_ 	_K^K^	_ 	_ 	_r(   N)__name__
__module____qualname____doc__rR   rc   rr   rx   r   r   r   r   r   r   r'   r(   r)   r8   r8      sA    8]
b]q2@
\LZH6_r(   r8   c                   >    e Zd ZdZdedee   fdZd Zd Z	d Z
d Zy	)
TestCategoryMappingu$   카테고리 확장 매핑 테스트keyword_contentreturnc                     t        dddd| dt        j                        t        dddd	t        j                        g}t        |d
      S )Nr   r   rT   u   #궁금증
• 유형 : u   
관련 질문입니다.r   r#   ru   u   답변입니다.Fr:   )r   r	   r&   r   )rM   r   r^   s      r)   _extract_for_contentz(TestCategoryMapping._extract_for_content)  s_    !&3O3DD]^ (( !)* ((
  $He<<r(   c                     | j                  d      }t        |      }d}||k\  }|st        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  }t        j                  d      d	z   d
|iz  }t        t        j                  |            dx}x}}|d   d   }d}||v }|st        j                  d|fd||f      t        j                  |      dt	        j
                         v st        j                  |      rt        j                  |      nddz  }	t        j                  d|       dz   d|	iz  }
t        t        j                  |
            dx}}y)u8   '보상' 키워드 → 보상 관련 카테고리 매핑u   보상re   rf   rh   rV   r>   rW   u3   '보상' 키워드 메시지에서 결과가 없음rZ   r[   Nr   r   rj   rl   rn   u1   카테고리에 '보상'이 포함되지 않음: rB   rC   r   rV   rD   rE   rF   rG   rH   rI   rJ   rK   rL   rM   r>   rN   r_   r`   ra   rb   r   rq   rP   rQ   s              r)   +   test_보상_keyword_maps_to_보상_categoryu?   TestCategoryMapping.test_보상_keyword_maps_to_보상_category<  &   **846{VaV{aVVV{aVVVVVVsVVVsVVVVVV6VVV6VVV{VVVaVVV!VVVVVVVV!9Z(cx8#cccx8cccxcccccc8ccc8cccc'XYaXb%cccccccr(   c                     | j                  d      }t        |      }d}||k\  }|st        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  }t        j                  d      d	z   d
|iz  }t        t        j                  |            dx}x}}|d   d   }d}||v }|st        j                  d|fd||f      t        j                  |      dt	        j
                         v st        j                  |      rt        j                  |      nddz  }	t        j                  d|       dz   d|	iz  }
t        t        j                  |
            dx}}y)u0   '고지' 키워드 → 고지의무 카테고리u   고지의무re   rf   rh   rV   r>   rW   u9   '고지의무' 키워드 메시지에서 결과가 없음rZ   r[   Nr   r   u   고지rj   rl   rn   u1   카테고리에 '고지'가 포함되지 않음: rB   rC   r   r   s              r)   1   test_고지_keyword_maps_to_고지의무_categoryuE   TestCategoryMapping.test_고지_keyword_maps_to_고지의무_categoryC  s&   **>:6{\a\{a\\\{a\\\\\\s\\\s\\\\\\6\\\6\\\{\\\a\\\!\\\\\\\\!9Z(cx8#cccx8cccxcccccc8ccc8cccc'XYaXb%cccccccr(   c                     | j                  d      }t        |      }d}||k\  }|st        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  }t        j                  d      d	z   d
|iz  }t        t        j                  |            dx}x}}|d   d   }d}||v }|st        j                  d|fd||f      t        j                  |      dt	        j
                         v st        j                  |      rt        j                  |      nddz  }	t        j                  d|       dz   d|	iz  }
t        t        j                  |
            dx}}y)u0   '약관' 키워드 → 약관해석 카테고리u   약관re   rf   rh   rV   r>   rW   u3   '약관' 키워드 메시지에서 결과가 없음rZ   r[   Nr   r   rj   rl   rn   u1   카테고리에 '약관'이 포함되지 않음: rB   rC   r   r   s              r)   1   test_약관_keyword_maps_to_약관해석_categoryuE   TestCategoryMapping.test_약관_keyword_maps_to_약관해석_categoryJ  r   r(   c                     | j                  d      }t        |      }d}||k\  }|st        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  }t        j                  d      d	z   d
|iz  }t        t        j                  |            dx}x}}|d   d   }d}||v }|st        j                  d|fd||f      t        j                  |      dt	        j
                         v st        j                  |      rt        j                  |      nddz  }	t        j                  d|       dz   d|	iz  }
t        t        j                  |
            dx}}y)u0   '상품' 키워드 → 상품비교 카테고리u   상품re   rf   rh   rV   r>   rW   u3   '상품' 키워드 메시지에서 결과가 없음rZ   r[   Nr   r   rj   rl   rn   u1   카테고리에 '상품'이 포함되지 않음: rB   rC   r   r   s              r)   1   test_상품_keyword_maps_to_상품비교_categoryuE   TestCategoryMapping.test_상품_keyword_maps_to_상품비교_categoryQ  r   r(   N)r   r   r   r   strlistdictr   r   r   r   r   r'   r(   r)   r   r   &  s4    .=C =DJ =&ddddr(   r   c                   (    e Zd ZdZd Zd Zd Zd Zy)TestInsightTypeDetectionu$   인사이트 유형 감지 테스트c                 ^   t        ddddt        j                        t        ddddt        j                        g}t        |d	
      }t	        |      dkD  r|D cg c]  }|d   	 }}d}||v }|st        j                  d|fd||f      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      nddz  }t        j                  d|       dz   d|iz  }t        t        j                  |            dx}}yyc c}w )u0   '제 경험상' 패턴 → expert_opinion 유형r   r   r$   uJ   제 경험상 비만약 처방 이력은 반드시 고지해야 합니다.r   rt   r,   u(   그렇군요. 꼭 기억하겠습니다.Fr:   r   r   expert_opinionrj   rl   rm   rn   uL   '제 경험상' 패턴에서 expert_opinion이 추출되어야 함. 실제: rB   rC   Nrv   rw   s	            r)   0   test_경험_공유_패턴_creates_expert_opinionuI   TestInsightTypeDetection.test_경험_공유_패턴_creates_expert_opiniona  s@    !1d (( !.B ((
  &h>v;?(./1QvY/E/ f E)fTeTef Ef f\e\e !f f_e_ef fMeMe %*f f\e\e %*f fTeTe]^c]def f fReRef f /ry   c                 ^   t        ddddt        j                        t        ddddt        j                        g}t        |d	
      }t	        |      dkD  r|D cg c]  }|d   	 }}d}||v }|st        j                  d|fd||f      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      nddz  }t        j                  d|       dz   d|iz  }t        t        j                  |            dx}}yyc c}w )u<   '실제로 이런 사례가' 패턴 → case_analysis 유형r   r   r$   u   실제로 이런 사례가 있었습니다. 비만약 복용 후 보험 가입을 시도한 고객이 면책당한 사례입니다.r   17:03r,   u   정말 조심해야겠네요.Fr:   r   r   case_analysisrj   rl   rm   rn   uX   '실제로 이런 사례가' 패턴에서 case_analysis가 추출되어야 함. 실제: rB   rC   Nrv   rw   s	            r)   /   test_사례_공유_패턴_creates_case_analysisuH   TestInsightTypeDetection.test_사례_공유_패턴_creates_case_analysisz  sC    !1 ^ (( !.7 ((
  &h>v;?(./1QvY/E/r5(r`q`qr5r rhqhq  r rkqkqr rYqYq $)r rhqhq $)r r`q`qijoipqr r r^q^qr r /ry   c                    t        t               d      }t        |      dkD  r|D cg c]  }|d   	 }}d}||v }|st        j                  d|fd||f      t        j
                  |      dt        j                         v st        j                  |      rt        j
                  |      ndd	z  }t        j                  d
|       dz   d|iz  }t        t        j                  |            dx}}yyc c}w )u+   '주의하세요' 패턴 → warning 유형Fr:   r   r   warningrj   rl   rm   rn   uG   '주의하세요' 패턴에서 warning이 추출되어야 함. 실제: rB   rC   N)r   r2   rV   rD   rE   rI   rF   rG   rH   rJ   rK   rL   )rM   r>   rp   rm   rq   rN   rP   rQ   s           r)   )   test_주의_경고_패턴_creates_warninguB   TestInsightTypeDetection.test_주의_경고_패턴_creates_warning  s    %&<&>Nv;?(./1QvY/E/aU"aO`O`aUa aW`W` a aZ`Z`a aH`H` #a aW`W` #a aO`O`XY^X_`a a aM`M`a a /s   C6c           	         t        ddddt        j                        t        ddddt        j                        t        dd	d
dt        j                        g}t        |d      }t	        |      dkD  r|D cg c]  }|d   	 }}d}||v }|st        j                  d|fd||f      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      nddz  }t        j                  d|       dz   d|iz  }t        t        j                  |            dx}}yyc c}w )u9   약관 해석 논쟁 → regulation_interpretation 유형r   r   rT   uW   약관 해석이 궁금합니다. 레이저 시술이 수술 약관에 포함되나요?r   r   r$   u}   약관 조항을 보면 레이저를 이용한 수술도 포함됩니다. 단, 약관 개정일 이후부터 적용됩니다.r#      C/KB/대전uJ   약관마다 다를 수 있으니 각 사마다 확인이 필요합니다.Fr:   r   r   regulation_interpretationrj   rl   rm   rn   uU   약관 해석 논쟁에서 regulation_interpretation이 추출되어야 함. 실제: rB   rC   Nrv   rw   s	            r)   ;   test_약관_해석_논쟁_creates_regulation_interpretationuT   TestInsightTypeDetection.test_약관_해석_논쟁_creates_regulation_interpretation  sb    !&q (( !1 X (( !"d ((
. &h>v;?(./1QvY/E/+o+u4o]n]no+uo oenen ,o ohnhno oVnVn 05o oenen 05o o]n]nfglfmno o o[n[no o /s   9EN)r   r   r   r   r   r   r   r   r'   r(   r)   r   r   ^  s    .f2r2aor(   r   c                   "    e Zd ZdZd Zd Zd Zy)TestThreadSplittingu!   스레드 분리 개선 테스트c           
      p   t        ddddt        j                        t        ddddt        j                        t        dd	d
dt        j                        t        ddddt        j                        g}t        |d      }t	        |      }d}||k(  }|st        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  }t        j                  dt	        |             dz   d|iz  }t        t        j                  |            dx}x}}y)u,   15분 초과 gap → 새 스레드로 분리r   r   rT   u/   #궁금증
• 유형 : 보상
질문1입니다r   r#   ru   u   답변1입니다z17:21r   u/   #궁금증
• 유형 : 약관
질문2입니다z17:25u   답변2입니다Fr:   r   r<   rU   rV   r>   rW   u?   15분 gap으로 2개 스레드가 생성되어야 함. 실제: rZ   r[   Nr\   r]   s           r)   !test_15min_gap_creates_new_threadz5TestThreadSplitting.test_15min_gap_creates_new_thread  s    !&K (( !)* (( !"K (( !)* ((/
> &h>K	[	[1	[ 	[IZIZ	[1	[ 	[TZTZ	[ 	[BZBZ 	[ 	[QZQZ 	[ 	[TZTZ	[ 	[BZBZ 	[ 	[QZQZ 	[ 	[QZQZ 	[ 	[QZQZ 	[ 	[IZIZLSQW[MZ	[ 	[ 	[GZGZ	[ 	[ 	[r(   c           
      p   t        ddddt        j                        t        ddddt        j                        t        d	d
ddt        j                        t        d	dddt        j                        g}t        |d      }t	        |      }d}||k(  }|st        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  }t        j                  dt	        |             dz   d|iz  }t        t        j                  |            dx}x}}y)u)   날짜 변경 → 새 스레드로 분리r   z23:50rT   &   #궁금증
• 유형 : 보상
질문1r   z23:55ru      답변1r/   z00:05r   ,   #궁금증
• 유형 : 고지의무
질문2z00:10   답변2Fr:   r   r<   rU   rV   r>   rW   uC   날짜 변경으로 2개 스레드가 생성되어야 함. 실제: rZ   r[   Nr\   r]   s           r)   #test_date_change_creates_new_threadz7TestThreadSplitting.test_date_change_creates_new_thread  s    !&B (( !)! (( !"H (( !)! ((/
> &h>K	_	_1	_ 	_M^M^	_1	_ 	_X^X^	_ 	_F^F^ 	_ 	_U^U^ 	_ 	_X^X^	_ 	_F^F^ 	_ 	_U^U^ 	_ 	_U^U^ 	_ 	_U^U^ 	_ 	_M^M^PQTU[Q\P]^	_ 	_ 	_K^K^	_ 	_ 	_r(   c           
      p   t        ddddt        j                        t        ddddt        j                        t        dd	d
dt        j                        t        ddddt        j                        g}t        |d      }t	        |      }d}||k(  }|st        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  }t        j                  dt	        |             dz   d|iz  }t        t        j                  |            dx}x}}y)u3   같은 시간대 #궁금증 2번 → 2개 스레드r   r   rT   r   r   rt   ru   r   r   r   r   r#   r   Fr:   r   r<   rU   rV   r>   rW   uP   #궁금증 태그가 2번 나오면 2개 결과가 생성되어야 함. 실제: rZ   r[   Nr\   r]   s           r)   <test_two_question_tags_in_same_timeframe_creates_two_threadszPTestThreadSplitting.test_two_question_tags_in_same_timeframe_creates_two_threads  s    !&B (( !)! (( !"H (( !)! ((-
< &h>K	l	l1	l 	lZkZk	l1	l 	lekek	l 	lSkSk 	l 	lbkbk 	l 	lekek	l 	lSkSk 	l 	lbkbk 	l 	lbkbk 	l 	lbkbk 	l 	lZkZk]^abh^i]jk	l 	l 	lXkXk	l 	l 	lr(   N)r   r   r   r   r   r   r   r'   r(   r)   r   r     s    +$[L$_L#lr(   r   c                   :    e Zd ZdZd	dedee   fdZd Zd Z	d Z
y)
TestBatchProcessingu   배치 처리 테스트countr   c                 B   g }t        |      D ]  }d|dz  z   }|dz  dz  }|dd|d}|dz  dk(  r6|j                  t        d|d	| d
d| dt        j                               Z|j                  t        d|d| dd| dt        j                                |S )u   대량 메시지 생성 헬퍼
   r   <   02d:   r   r   u	   질문자   /인카/서울u%   #궁금증
• 유형 : 보상
질문u	   입니다r   u	   답변자u   /프라임/부산u   답변)rangeappendr   r	   r&   )rM   r   r^   ihourminutetime_strs          r)   _make_bulk_messagesz'TestBatchProcessing._make_bulk_messages?  s    u 	Ab>D!er\Fs1VCL1H1uz)%(>:"I!I V(00 )%(+<="(9 5(00	0 r(   c                    | j                  d      }	 t        |dd      }t        t              }|s!t        j                  d	      d
z   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dt        j                         v st        j                  t              rt        j
                  t              ndt        j
                  |      dz  }t        t        j                  |            d}y# t        $ rb}d}|sQt        j                  d|       dz   dt        j
                  |      iz  }t        t        j                  |            d}Y d}~d}~ww xY w)uH   대량 메시지(100개) 처리 시 에러 없이 완료되어야 한다d   F2   r;   
batch_sizeu+   대량 메시지 처리 중 에러 발생: 
>assert %(py1)srX   N   결과가 리스트여야 함7
>assert %(py4)s
{%(py4)s = %(py0)s(%(py1)s, %(py2)s)
}
isinstancer>   r   r@   rX   r   r   )r   r   	ExceptionrD   rJ   rI   rK   rL   r   r   rF   rG   rH   )rM   r^   r>   r   rq   r   r   @py_format5s           r)   test_bulk_messages_no_errorz/TestBatchProcessing.test_bulk_messages_no_error\  s   ++C0	L)(EbQF &$'H'HH)HHHHHHHzHHHzHHHHHH&HHH&HHHHHH$HHH$HHH'HHHHHH  	LK5KKGsKKKK5KKKKKK	Ls   E 	G!AF>>Gc                    | j                  d      }t        |dd      }t        |t              }|s-t	        j
                  dt        |             dz   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d	t        j                         v st	        j                  t              rt	        j                  t              nd	t	        j                  |      d
z  }t        t	        j                  |            d}y)u8   대량 메시지 처리 결과가 리스트여야 한다r   Fr   r   u&   결과 타입이 리스트가 아님: r   r   r>   r   r   N)r   r   r   r   rD   rJ   r   rF   rG   rH   rI   rK   rL   )rM   r^   r>   r   r   s        r)   test_bulk_messages_returns_listz3TestBatchProcessing.test_bulk_messages_returns_liste  s    ++C0%h"M&$'`'``+QRVW]R^Q_)```````z```z``````&```&``````$```$```'``````r(   c                 t   t               }t        j                         5 }t        |d|      }t	        |      dkD  rit        j                  |      }d}|sNt        j                  d      dz   dt        j                  |      iz  }t        t        j                  |            d}ddd       y# 1 sw Y   yxY w)	u=   output_dir 지정 시 중간 파일이 생성되어야 한다Fr;   
output_dirr   Tu   output_dir 지정 처리 완료r   rX   N)r*   tempfileTemporaryDirectoryr   rV   oslistdirrD   rJ   rI   rK   rL   )rM   r^   tmpdirr>   filesrq   r   s          r)   *test_output_dir_creates_intermediate_filesz>TestBatchProcessing.test_output_dir_creates_intermediate_filesk  s    $&((* 	?f)(EfUF6{Q

6*>t>>>>>>t>>>>>	? 	? 	?s   BB..B7N)r   )r   r   r   r   intr   r   r   r   r   r  r'   r(   r)   r   r   <  s.    ! tK7H :Ia
?r(   r   c                   "    e Zd ZdZd Zd Zd Zy)TestLlmPathWithMocku"   LLM 경로 테스트 (mock 사용)c                 N   dddt         t           dt        dt        j                  t           ffd}t        d|      5  t        t               d	
      }ddd       t        t               }|s!t        j                  d      dz   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dt        j                         v st        j                  t               rt        j                  t               ndt        j                  |      dz  }t        t        j                   |            d}y# 1 sw Y   AxY w)u.   Stage 1 Haiku mock → has_insight=True 판별zT{"has_insight": true, "insight_types": ["qa", "expert_opinion"], "noise_reason": ""}u  {"title": "테스트", "type": "qa", "category": "보상/일반", "summary": "요약", "key_points": ["포인트1"], "expert": "전문가", "confidence": "high", "related_topics": ["보상"], "tags": ["#테스트"], "question": "질문", "answer": "답변"}cmdkwargsr   c                 t    t        |       dkD  r| d   nd}|dk(  r}n}t        j                  | d|d      S N   haikur    args
returncodestdoutstderrrV   
subprocessCompletedProcessr
  r  	model_argr  stage1_jsonstage2_jsons       r)   _side_effectzQTestLlmPathWithMock.test_stage1_haiku_mock_has_insight_true.<locals>._side_effect  H     #&c(Q,AGIG#$$..Qvb r(   5kakao_knowledge.knowledge_extractor_v2.subprocess.runside_effectTr:   Nu3   LLM mock 경로에서 결과가 리스트여야 함r   r   r>   r   r   r   r   objectr  r  r   r   r*   r   rD   rJ   rF   rG   rH   rI   rK   rL   rM   r  r>   r   r   r  r  s        @@r)   'test_stage1_haiku_mock_has_insight_truez;TestLlmPathWithMock.test_stage1_haiku_mock_has_insight_true  s   l Z
	c
	&,
	((-
	 C$
 	 *!#F		 &$'^'^^)^^^^^^^z^^^z^^^^^^&^^^&^^^^^^$^^^$^^^'^^^^^^	 	   FF$c                 N   dddt         t           dt        dt        j                  t           ffd}t        d|      5  t        t               d	
      }ddd       t        t               }|s!t        j                  d      dz   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dt        j                         v st        j                  t               rt        j                  t               ndt        j                  |      dz  }t        t        j                   |            d}y# 1 sw Y   AxY w)u(   Stage 2 Sonnet mock → InsightV2 추출zB{"has_insight": true, "insight_types": ["qa"], "noise_reason": ""}u  {"title": "광응고술 수술 해당 여부", "type": "qa", "category": "보상/일반", "summary": "광응고술은 수술약관에 해당합니다.", "key_points": ["약관 개정", "레이저 수술 포함"], "expert": "이해철/프라임/부산", "confidence": "high", "related_topics": ["보상", "수술"], "tags": ["#광응고술"], "question": "광응고술이 수술에 해당하나요?", "answer": "네 수술에 해당합니다."}r
  r  r   c                 t    t        |       dkD  r| d   nd}|dk(  r}n}t        j                  | d|d      S r  r  r  s       r)   r  zRTestLlmPathWithMock.test_stage2_sonnet_mock_extracts_insight.<locals>._side_effect  r  r(   r  r   Tr:   Nu7   Stage 2 mock 경로에서 결과가 리스트여야 함r   r   r>   r   r   r"  r$  s        @@r)   (test_stage2_sonnet_mock_extracts_insightz<TestLlmPathWithMock.test_stage2_sonnet_mock_extracts_insight  s    Q 	: 	
	c
	&,
	((-
	 C$
 	 *!#F		 &$'b'bb)bbbbbbbzbbbzbbbbbb&bbb&bbbbbb$bbb$bbb'bbbbbb	 	r&  c                 :   dt         t           dt        dt        j                  t           fd}t        d|      5  t        t               d      }t        |t               }|s!t        j                  d	      d
z   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dt        j                         v st        j                  t               rt        j                  t               ndt        j                  |      dz  }t        t        j                   |            d}ddd       y# 1 sw Y   yxY w)u<   CLI 실패 시 규칙 기반 fallback이 동작해야 한다r
  r  r   c                 4    t        j                  | ddd      S )Nre   r  u%   Claude CLI 오류: connection refusedr  )r  r  )r
  r  s     r)   r  zSTestLlmPathWithMock.test_api_failure_falls_back_to_rule_based.<locals>._side_effect  s#     ..>	 r(   r  r   Tr:   u5   CLI 실패 시 fallback 결과가 리스트여야 함r   r   r>   r   r   Nr"  )rM   r  r>   r   r   s        r)   )test_api_failure_falls_back_to_rule_basedz=TestLlmPathWithMock.test_api_failure_falls_back_to_rule_based  s   	c	&,	((-	 C$
 	e *!#F fd+d+dd-ddddddd:ddd:ddddddfdddfdddddddddddddd+dddddd	e 	e 	es   EFFN)r   r   r   r   r%  r)  r,  r'   r(   r)   r  r  }  s    ,_6$cLer(   r  c                   :    e Zd ZdZd Zd Zd Zd Zd Zd Z	d Z
y	)
TestReturnValueStructureu   반환값 구조 검증c                    t        t               d      }|D ]E  }t        |t              }|s-t	        j
                  dt        |             dz   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dt        j                         v st	        j                  t              rt	        j                  t              ndt	        j                  |      dz  }t        t	        j                  |            d	}H y	)
u3   결과의 각 항목이 dict 타입이어야 한다Fr:   u!   결과 항목이 dict가 아님: r   r   r   r   r   N)r   r*   r   r   rD   rJ   r   rF   rG   rH   rI   rK   rL   )rM   r>   r   r   r   s        r)   test_result_entries_are_dictsz6TestReturnValueStructure.test_result_entries_are_dicts  s    %&7&95I 	^EeT*]*]].OPTUZP[},]]]]]]]:]]]:]]]]]]e]]]e]]]]]]T]]]T]]]*]]]]]]	^r(   c                    t        t               d      }|D ]  }|j                  }d} ||      }t        |t              }|sxt        j                  dt        |j                  d                   dz   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                  |      t        j                  |      dt        j                         v st        j                  t              rt        j                  t              ndt        j                  |      d	z  }t        t        j                  |            d
x}x}x}} y
)u2   결과의 key_points 값이 리스트여야 한다Fr:   r   u#   key_points가 리스트가 아님: p
>assert %(py10)s
{%(py10)s = %(py0)s(%(py7)s
{%(py7)s = %(py3)s
{%(py3)s = %(py1)s.get
}(%(py5)s)
}, %(py8)s)
}r   r   r   r@   rX   rA   rC   r   r[   py10Nr   r*   r   r   r   rD   rJ   r   rF   rG   rH   rI   rK   rL   rM   r>   r   rN   r`   r   @py_assert9@py_format11s           r)   !test_key_points_is_list_in_resultz:TestReturnValueStructure.test_key_points_is_list_in_result     %&7&95I 	UE		U&U,'U:' U  UCTCT4T%))L:Q5R4STU UNTfU U<T<T  U UKT9  U UNTfU U<T<T U UKT9 U UKT9 U UKT9 'U UKT9 (U UNTfU U<T<T *.U UKT9 *.U UKT9 U U UATATU U U	Ur(   c                    t        t               d      }|D ]  }|j                  }d} ||      }t        |t              }|sxt        j                  dt        |j                  d                   dz   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                  |      t        j                  |      dt        j                         v st        j                  t              rt        j                  t              ndt        j                  |      d	z  }t        t        j                  |            d
x}x}x}} y
)u2   결과의 raw_thread 값이 리스트여야 한다Fr:   r   u#   raw_thread가 리스트가 아님: r2  r   r   r   r3  Nr5  r6  s           r)   !test_raw_thread_is_list_in_resultz:TestReturnValueStructure.test_raw_thread_is_list_in_result  r:  r(   c                    t        t               d      }|D ]  }|j                  }d} ||      }t        |t              }|sxt        j                  dt        |j                  d                   dz   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                  |      t        j                  |      dt        j                         v st        j                  t              rt        j                  t              ndt        j                  |      d	z  }t        t        j                  |            d
x}x}x}} y
)u4   결과의 participants 값이 리스트여야 한다Fr:   r   u%   participants가 리스트가 아님: r2  r   r   r   r3  Nr5  r6  s           r)   #test_participants_is_list_in_resultz<TestReturnValueStructure.test_participants_is_list_in_result  s   %&7&95I 	YE		Y(Y.)Y:)4 Y  YGXGX6tEIIn<U7V6WXY YRXRXY Y@X@X  Y YOXy  Y YRXRXY Y@X@X Y YOXy Y YOXy Y YOXy )Y YOXy *Y YRXRXY Y@X@X ,0Y YOXy ,0Y YOXy Y Y YEXEXY Y Y	Yr(   c           	         t        t               dd      }|D ])  }|j                  }d} ||      }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }t        j                  d	|j                  d             d
z   d|iz  }	t        t        j                  |	            dx}x}x}x}}, y)u<   source_chat 파라미터가 결과에 반영되어야 한다Fu#   보험설계사_테스트_채팅방)r;   r   r   r<   )zI%(py6)s
{%(py6)s = %(py2)s
{%(py2)s = %(py0)s.get
}(%(py4)s)
} == %(py9)sr   r@   r   r   rY   r   u.   source_chat이 결과에 반영되지 않음: 
>assert %(py11)spy11N)r   r*   r   rD   rE   rF   rG   rH   rI   rJ   rK   rL   )
rM   r>   r   rO   r   r_   @py_assert8@py_assert7r   @py_format12s
             r)   test_source_chat_propagatedz4TestReturnValueStructure.test_source_chat_propagated  sf   %=

  	[E		['[-([,Q[(,QQ[IZIZ[(,Q[ [TZTZ[ [BZBZ [ [QZQZ [ [QZQZ [ [QZQZ ([ [QZQZ )[ [QZQZ -R[ [IZIZ?		-@X?YZ[ [ [GZGZ[ [ [	[r(   c           
         t        ddddt        j                        t        ddddt        j                        t        dd	d
dt        j                        t        ddddt        j                        g}t        |d      }t	        |      dk\  r^t        |d   d   j                  d      d         }t        |d   d   j                  d      d         }d}||z   }||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      nddt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }t        j                  d|d   d    d|d   d          dz   d|iz  }	t        t        j                  |	            dx}x}}yy)u@   여러 인사이트의 id가 순차적으로 증가해야 한다r   r   rT   r   r   rt   ru   r   r   r   u&   #궁금증
• 유형 : 약관
질문2r#   r   Fr:   r   r   r   r   re   r<   )z%(py0)s == (%(py2)s + %(py4)s)
second_num	first_numr@   r   r   u%   id 순번이 순차적이지 않음: u    → z
>assert %(py7)sr   N)r   r	   r&   r   rV   r  r   rD   rE   rF   rG   rH   rI   rJ   rK   rL   )
rM   r^   r>   rI  rH  r   r_   rO   rQ   r   s
             r)   #test_consecutive_ids_are_sequentialz<TestReturnValueStructure.test_consecutive_ids_are_sequential  s    !&B (( !)! (( !"B (( !)! ((-
< &h>v;!F1IdO11#6q9:IVAYt_2237:;J*+_'!m_m+_ _M^M^_m_ _X^X^_ _F^F^ _ _U^U^ _ _X^X^_ _F^F^ (_ _U^U^ (_ _U^U^ +,_ _M^M^6vay6GuVTUYW[_L]^_ _ _K^K^_ _ _ r(   c                    t        t               d      }t        |t              }|s!t	        j
                  d      dz   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dt        j                         v st	        j                  t              rt	        j                  t              ndt	        j                  |      dz  }t        t	        j                  |            d	}y	)
uD   전문가 의견 메시지에서도 결과가 반환되어야 한다Fr:   r   r   r   r>   r   r   N)r   r-   r   r   rD   rJ   rF   rG   rH   rI   rK   rL   )rM   r>   r   r   s       r)   +test_expert_opinion_messages_return_resultszDTestReturnValueStructure.test_expert_opinion_messages_return_results3  s    %&C&EuU&$'H'HH)HHHHHHHzHHHzHHHHHH&HHH&HHHHHH$HHH$HHH'HHHHHHr(   N)r   r   r   r   r0  r9  r<  r>  rF  rK  rM  r'   r(   r)   r.  r.    s0    !^UUY
[&_PIr(   r.  nstart_minutec                     ddl m}m} ddlm} g }t        |       D ]:  }|j                   |dd||z   dz  dd| d	d
| d|j                               <  |       }||_        d|d|_	        |S )u2   N개 메시지가 있는 ThreadV2를 생성한다.r   r   ThreadV2r   17:r   r      유저r      보험 관련 메시지 :   입니다. 보상 청구에 대해 질문이 있습니다.r   z2025-12-03 17:)
kakao_knowledge.modelsr   r	   kakao_knowledge.models_v2rR  r   r   r&   r^   
start_time)rN  rO  r   r	   rR  msgsr   ts           r)   _make_thread_with_n_messagesr\  ?  s    ?2D1X 	
!L1,2378aS/21#5op ((	
	
 	
AAJ#L#56ALHr(   c                   4    e Zd ZdZd Zd Zd Zd Zd Zd Z	y)	TestLlmRefineThreadSplitsu\   _llm_refine_thread_splits 병합+분리 양방향 지원 테스트 (개발1팀 아르고스)c                 
   t        d      }ddddgdgi}t        dt        j                  |            5  t	        |g      }d	d	d	       t              }d
}||k(  }|st        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  }t        j                  dt        |             dz   d|iz  }t        t        j                  |            d	x}x}}|d   }	|	j                  }
t        |
      }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  t
              rt        j                  t
              ndt        j                  |	      t        j                  |
      t        j                  |      t        j                  |      dz  }t        j                  dt        |d   j                               dz   d|iz  }t        t        j                  |            d	x}	x}
x}x}}|d   }	|	j                  }
t        |
      }d
}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  t
              rt        j                  t
              ndt        j                  |	      t        j                  |
      t        j                  |      t        j                  |      dz  }t        j                  dt        |d   j                               dz   d|iz  }t        t        j                  |            d	x}	x}
x}x}}|d   }	|	j                  }
t        |
      }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  t
              rt        j                  t
              ndt        j                  |	      t        j                  |
      t        j                  |      t        j                  |      dz  }t        j                  dt        |d   j                               dz   d|iz  }t        t        j                  |            d	x}	x}
x}x}}y	# 1 sw Y   xY w)uJ   split_at이 [5, 8]이면 12개 메시지를 3개 스레드로 분리한다   threadsFr      merge_with_prevsplit_at3kakao_knowledge.knowledge_extractor_v2._call_claudereturn_valueNr   r<   rU   rV   r>   rW   uA   split_at=[5,8]로 3개 스레드가 생성되어야 함. 실제: rZ   r[   r   zN%(py6)s
{%(py6)s = %(py0)s(%(py4)s
{%(py4)s = %(py2)s.messages
})
} == %(py9)sr@  u+   첫 번째 스레드 메시지 수 오류: rA  rB  re   u+   두 번째 스레드 메시지 수 오류: r   r  u+   세 번째 스레드 메시지 수 오류: r\  r   jsondumpsr   rV   rD   rE   rF   rG   rH   rI   rJ   rK   rL   r^   rM   threadllm_responser>   rN   r_   r`   ra   rb   rO   r   rC  rD  r   rE  s                  r)   -test_split_at_divides_thread_into_sub_threadszGTestLlmRefineThreadSplits.test_split_at_divides_thread_into_sub_threadsX  sh   -b1$)1v>
 AL1
 	9 /x8F		9 K	]	]1	] 	]K\K\	]1	] 	]V\V\	] 	]D\D\ 	] 	]S\S\ 	] 	]V\V\	] 	]D\D\ 	] 	]S\S\ 	] 	]S\S\ 	] 	]S\S\ 	] 	]K\K\NsSY{m\	] 	] 	]I\I\	] 	] q		S""	S"#	S'(	S#q(	S 	SARAR	S#q	S 	SLRF	S 	S:R:R 	S 	SIR 	S 	SIR 	S 	SIR #	S 	SIR $	S 	SIR ()	S 	SARAR8VAY=O=O9P8QR	S 	S 	S?R?R	S 	S 	S q		S""	S"#	S'(	S#q(	S 	SARAR	S#q	S 	SLRF	S 	S:R:R 	S 	SIR 	S 	SIR 	S 	SIR #	S 	SIR $	S 	SIR ()	S 	SARAR8VAY=O=O9P8QR	S 	S 	S?R?R	S 	S 	S q		S""	S"#	S'(	S#q(	S 	SARAR	S#q	S 	SLRF	S 	S:R:R 	S 	SIR 	S 	SIR 	S 	SIR #	S 	SIR $	S 	SIR ()	S 	SARAR8VAY=O=O9P8QR	S 	S 	S?R?R	S 	S 	S	9 	9s   T33T=c                    t        dd      }t        dd      }ddg ddg dgi}t        dt        j                  |      	      5  t	        ||g      }d
d
d
       t              }d}||k(  }|st        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  }t        j                  dt        |             dz   d|iz  }	t        t        j                  |	            d
x}x}}|d   }
|
j                  }t        |      }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  t
              rt        j                  t
              ndt        j                  |
      t        j                  |      t        j                  |      t        j                  |      dz  }t        j                  dt        |d   j                               dz   d|iz  }t        t        j                  |            d
x}
x}x}x}}y
# 1 sw Y   zxY w)uA   merge_with_prev=true 이면 두 스레드가 1개로 합쳐진다r   r   rO  ra  Frc  Trf  rg  Nre   r<   rU   rV   r>   rW   uA   merge_with_prev=true로 1개 스레드가 되어야 함. 실제: rZ   r[      ri  r@  u4   병합 후 메시지 수가 6개여야 함. 실제: rA  rB  rj  rM   thread1thread2ro  r>   rN   r_   r`   ra   rb   rO   r   rC  rD  r   rE  s                   r)    test_merge_with_prev_still_worksz:TestLlmRefineThreadSplits.test_merge_with_prev_still_workst  s   .qqA.qqA$)r:$(b9
 AL1
 	C //ABF		C K	]	]1	] 	]K\K\	]1	] 	]V\V\	] 	]D\D\ 	] 	]S\S\ 	] 	]V\V\	] 	]D\D\ 	] 	]S\S\ 	] 	]S\S\ 	] 	]S\S\ 	] 	]K\K\NsSY{m\	] 	] 	]I\I\	] 	] q		\""	\"#	\'(	\#q(	\ 	\J[J[	\#q	\ 	\U[U[	\ 	\C[C[ 	\ 	\R[R[ 	\ 	\R[R[ 	\ 	\R[R[ #	\ 	\R[R[ $	\ 	\R[R[ ()	\ 	\J[J[A#fQiFXFXBYAZ[	\ 	\ 	\H[H[	\ 	\ 	\	C 	Cs   KKc                     t        dd      }t        dd      }ddg dddd	gdgi}t        d
t        j                  |            5  t	        ||g      }ddd       t              }d}||k(  }|st        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  }t        j                  dt        |             dz   d|iz  }	t        t        j                  |	            dx}x}}|d   }
|
j                  }t        |      }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  t
              rt        j                  t
              ndt        j                  |
      t        j                  |      t        j                  |      t        j                  |      dz  }t        j                  dt        |d   j                               dz   d|iz  }t        t        j                  |            dx}
x}x}x}}|d   }
|
j                  }t        |      }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  t
              rt        j                  t
              ndt        j                  |
      t        j                  |      t        j                  |      t        j                  |      dz  }t        j                  dt        |d   j                               dz   d|iz  }t        t        j                  |            dx}
x}x}x}}|d   }
|
j                  }t        |      }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  t
              rt        j                  t
              ndt        j                  |
      t        j                  |      t        j                  |      t        j                  |      dz  }t        j                  dt        |d   j                               dz   d|iz  }t        t        j                  |            dx}
x}x}x}}|d   }
|
j                  }t        |      }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  t
              rt        j                  t
              ndt        j                  |
      t        j                  |      t        j                  |      t        j                  |      dz  }t        j                  dt        |d   j                               dz   d|iz  }t        t        j                  |            dx}
x}x}x}}y# 1 sw Y   .xY w)uR   병합 없이 둘째 스레드를 split_at=[4,7]로 3분할 → 총 4개 스레드r   r   rr  r   ra  Frc  r     rf  rg  Nr<   rU   rV   r>   rW   uP   첫째 스레드 그대로 + 둘째 3분할 = 4개 스레드여야 함. 실제: rZ   r[   ri  r@  u'   첫째 스레드 메시지 수 오류: rA  rB  re   u(   분리 첫 번째 메시지 수 오류: r   u(   분리 두 번째 메시지 수 오류: u(   분리 세 번째 메시지 수 오류: rj  rt  s                   r)   test_merge_and_split_combinedz7TestLlmRefineThreadSplits.test_merge_and_split_combined  s   .qqA.rB$)r:$)1v>
 AL1
 	C //ABF		C K	l	l1	l 	lZkZk	l1	l 	lekek	l 	lSkSk 	l 	lbkbk 	l 	lekek	l 	lSkSk 	l 	lbkbk 	l 	lbkbk 	l 	lbkbk 	l 	lZkZk]^abh^i]jk	l 	l 	lXkXk	l 	l q		O""	O"#	O'(	O#q(	O 	O=N=N	O#q	O 	OHN	O 	O6N6N 	O 	OENY 	O 	OENY 	O 	OENY #	O 	OENY $	O 	OENY ()	O 	O=N=N4S9K9K5L4MN	O 	O 	O;N;N	O 	O 	O q		P""	P"#	P'(	P#q(	P 	P>O>O	P#q	P 	PIO	P 	P7O7O 	P 	PFOi 	P 	PFOi 	P 	PFOi #	P 	PFOi $	P 	PFOi ()	P 	P>O>O5c&):L:L6M5NO	P 	P 	P<O<O	P 	P 	P q		P""	P"#	P'(	P#q(	P 	P>O>O	P#q	P 	PIO	P 	P7O7O 	P 	PFOi 	P 	PFOi 	P 	PFOi #	P 	PFOi $	P 	PFOi ()	P 	P>O>O5c&):L:L6M5NO	P 	P 	P<O<O	P 	P 	P q		P""	P"#	P'(	P#q(	P 	P>O>O	P#q	P 	PIO	P 	P7O7O 	P 	PFOi 	P 	PFOi 	P 	PFOi #	P 	PFOi $	P 	PFOi ()	P 	P>O>O5c&):L:L6M5NO	P 	P 	P<O<O	P 	P 	P%	C 	Cs   	ZZc                    t        d      }ddg dgi}t        dt        j                  |            5  t	        |g      }ddd       t              }d}||k(  }|st        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  }t        j                  dt        |             dz   d|iz  }t        t        j                  |            dx}x}}|d   }	|	j                  }
t        |
      }d}||k(  }|st        j                  d	|fd||f      dt        j                         v st        j                  t
              rt        j                  t
              ndt        j                  |	      t        j                  |
      t        j                  |      t        j                  |      dz  }t        j                  dt        |d   j                               dz   d|iz  }t        t        j                  |            dx}	x}
x}x}}y# 1 sw Y   zxY w)uC   split_at=[]이면 스레드가 변경 없이 그대로 유지된다r   ra  Frc  rf  rg  Nre   r<   rU   rV   r>   rW   u>   split_at=[]이면 1개 스레드 그대로여야 함. 실제: rZ   r[   r   ri  r@  u/   메시지 수가 변경되면 안 됨. 실제: rA  rB  rj  rm  s                  r)   test_empty_split_at_no_changez7TestLlmRefineThreadSplits.test_empty_split_at_no_change  s   -a0$)r:
 AL1
 	9 /x8F		9 K	Z	Z1	Z 	ZHYHY	Z1	Z 	ZSYSY	Z 	ZAYAY 	Z 	ZPYPY 	Z 	ZSYSY	Z 	ZAYAY 	Z 	ZPYPY 	Z 	ZPYPY 	Z 	ZPYPY 	Z 	ZHYHYKCPVK=Y	Z 	Z 	ZFYFY	Z 	Z q		W""	W"#	W'(	W#q(	W 	WEVEV	W#q	W 	WPVPV	W 	W>V>V 	W 	WMVY 	W 	WMVY 	W 	WMVY #	W 	WMVY $	W 	WMVY ()	W 	WEVEV<SASAS=T<UV	W 	W 	WCVCV	W 	W 	W	9 	9s   J99Kc                    t        dd      }t        dd      }t        dt        d            5  t        ||g      }ddd       t	              }d	}||k(  }|st        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  }t        j                  dt	        |             dz   d|iz  }t        t        j                  |            dx}x}}y# 1 sw Y   >xY w)uG   LLM 호출 실패 시 원래 스레드 목록을 그대로 반환한다r   r   rr  r  rf  u   LLM 연결 실패r   Nr   r<   rU   rV   r>   rW   uE   LLM 실패 시 원래 2개 스레드가 반환되어야 함. 실제: rZ   r[   )r\  r   r   r   rV   rD   rE   rF   rG   rH   rI   rJ   rK   rL   )	rM   ru  rv  r>   rN   r_   r`   ra   rb   s	            r)   )test_llm_failure_returns_original_threadszCTestLlmRefineThreadSplits.test_llm_failure_returns_original_threads  s   .qqA.qqAA!"56
 	C //ABF		C K	a	a1	a 	aO`O`	a1	a 	aZ`Z`	a 	aH`H` 	a 	aW`W` 	a 	aZ`Z`	a 	aH`H` 	a 	aW`W` 	a 	aW`W` 	a 	aW`W` 	a 	aO`O`RSVW]S^R_`	a 	a 	aM`M`	a 	a 	a	C 	Cs   E;;Fc                    ddl m} g }t        d      D ];  }|j                  t	        dd|dd| dd	| d
t
        j                               = t        ddgd      D ]:  \  }}|j                  t	        dd|dd| d|t
        j                               < t        dd      D ];  }|j                  t	        dd|dd| dd	| d
t
        j                               =  |       }||_        d|_	        ddddgdgi}t        dt        j                  |            5  t        |g      }ddd       t              }d}	||	k  }
|
st        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  }t        j*                  dt        |             dz   d|iz  }t-        t        j.                  |            dx}x}
}	y# 1 sw Y   >xY w) uT   분리 후 메시지 3개 미만 + 보험 키워드 없는 스레드는 제거된다r   rQ  r   r   rS  r   rT  r   rU  rV  r   r4   u   감사합니다)startr   rb  z2025-12-03 17:00ra  Frc  rf  rg  N)<)z/%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} < %(py6)srV   r>   rW   uN   노이즈 스레드 필터링 후 결과가 3개 미만이어야 함. 실제: rZ   r[   )rX  rR  r   r   r   r	   r&   	enumerater^   rY  r   rk  rl  r   rV   rD   rE   rF   rG   rH   rI   rJ   rK   rL   )rM   rR  rZ  r   r"   rn  ro  r>   rN   r_   r`   ra   rb   s                r)   'test_noise_threads_filtered_after_splitzATestLlmRefineThreadSplits.test_noise_threads_filtered_after_split  s   6 q 		AKK%qg!!N36qc9st$,,		 $%68I$JRST 		JAwKK%qg!!N3#$,,		 q! 		AKK%qg!!N36qc9st$,,		 . $)1v>
 AL1
 	9 /x8F		9 K	j	j!O	j 	jXiXi	j!	j 	jcici	j 	jQiQi 	j 	j`i`i 	j 	jcici	j 	jQiQi 	j 	j`i`i 	j 	j`i`i 	j 	j`i`i 	j 	jXiXi[\_`f\g[hi	j 	j 	jViVi	j 	j 	j	9 	9s   'I00I:N)
r   r   r   r   rp  rw  rz  r|  r~  r  r'   r(   r)   r^  r^  U  s*    fS8\0PBW,a9jr(   r^  c                   @    e Zd ZdZd Zd Zd Zd Zd Zd Z	d Z
d	 Zy
)TestMonthFilteringuL   --month 파라미터로 특정 월의 메시지만 필터링하는 테스트c                    t        ddddt        j                        t        ddddt        j                        t        d	d
ddt        j                        t        d	dddt        j                        t        ddddt        j                        t        ddddt        j                        t        ddddt        j                        t        ddddt        j                        gS )u%   여러 달에 걸친 메시지 생성z
2026-02-28r   rT   u3   #궁금증
• 유형 : 보상
2월 질문입니다r   r#   ru   u"   2월 답변입니다 보상 관련z
2026-03-01r0   r   u9   #궁금증
• 유형 : 고지의무
3월 질문입니다r1   u(   3월 답변입니다 고지의무 관련z
2026-03-15z14:00u   D/인카/서울u1   #궁금증
• 유형 : 약관
3월 중순 질문z14:05u    3월 중순 답변 약관 관련z
2026-04-01z09:00u   E/KB/대전u3   #궁금증
• 유형 : 상품
4월 질문입니다z09:05u"   4월 답변입니다 상품 관련r%   )rM   s    r)   _make_multi_month_messagesz-TestMonthFiltering._make_multi_month_messages  s    !&O (( !)< (( !"U (( !)B (( !&M (( !): (( !"O (( !)< ((e9
 9	
r(   c                    | j                         }t        |dd      }t        |      }d}||k\  }|st        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  }t        j                  d
      dz   d|iz  }t        t        j                  |            dx}x}}|D ]  }|d   }	|	j                  }d} ||      }
|
st        j                  d|d          dz   t        j                  |	      t        j                  |      t        j                  |      t        j                  |
      dz  }t        t        j                  |            dx}	x}x}}
 y)u,   month='2026-03' → 3월 메시지만 처리F2026-03r;   monthre   rf   rh   rV   r>   rW   u8   3월 메시지에서 인사이트가 추출되어야 함rZ   r[   Nr   u!   3월 이외 날짜가 포함됨: N
>assert %(py7)s
{%(py7)s = %(py3)s
{%(py3)s = %(py1)s.startswith
}(%(py5)s)
}rX   rA   rC   r   r  r   rV   rD   rE   rF   rG   rH   rI   rJ   rK   rL   
startswithrM   r^   r>   rN   r_   r`   ra   rb   r   rq   r   r   s               r)   -test_month_filter_returns_only_matching_monthz@TestMonthFiltering.test_month_filter_returns_only_matching_monthS  s   224%hYO6{[a[{a[[[{a[[[[[[s[[[s[[[[[[6[[[6[[[{[[[a[[[![[[[[[[[ 	JE' J'22 JJ2 J  J8I8I253G2HIJ J@I	 ( J J@I	 3 J J@I	 J J@I	 J J J6I6IJ J J	Jr(   c           	         | j                         }t        |dd      }t        |d      }t        |      }t        |      }||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                  |      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                  |      d
z  }t        j                  dt        |       dt        |             dz   d|iz  }t        t        j                  |            dx}x}}y)u@   month='' (기본값) → 전체 메시지 처리 (하위 호환)Fr  r  r:   r<   )zN%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py8)s
{%(py8)s = %(py5)s(%(py6)s)
}rV   
result_allresult_default)r@   rX   rA   rC   rY   r[   u3   빈 month와 기본값이 같은 결과여야 함: z vs z
>assert %(py10)sr4  Nr  r   rV   rD   rE   rF   rG   rH   rI   rJ   rK   rL   )	rM   r^   r  r  rN   rD  r`   rb   r8  s	            r)    test_no_month_filter_returns_allz3TestMonthFiltering.test_no_month_filter_returns_all^  s   224)(EL
-hF: 	l##
 	l #
 
 	l 	lZkZk	l #
 	l 	lekek	l 	lSkSk  	l 	lbkbk  	l 	lekek	l 	lSkSk  	l 	lbkbk  	l 	lbkbk  	l 	lekek	l 	lSkSk #& 	l 	lbkbk #& 	l 	lekek	l 	lSkSk 	l 	lbkbk 	l 	lbkbk#
 	l 	lZkZk@Z@QQUVYZhViUjk	l 	l 	lXkXk	l 	l 	lr(   c                    | j                         }t        |dd      }t        |      }d}||k(  }|st        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  }t        j                  d
t        |             dz   d|iz  }t        t        j                  |            dx}x}}y)u2   매칭되는 월이 없으면 빈 리스트 반환Fz2025-01r  r   r<   rU   rV   r>   rW   u7   매칭 메시지가 없으면 빈 리스트여야 함: rZ   r[   Nr  r]   s           r)   &test_month_filter_no_matching_messagesz9TestMonthFiltering.test_month_filter_no_matching_messagesg  s    224%hYO6{hah{ahhh{ahhhhhhshhhshhhhhh6hhh6hhh{hhhahhh#Z[^_e[fZg!hhhhhhhhr(   c                    | j                         }t        |dd      }t        |      }d}||k\  }|st        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  }t        j                  d
      dz   d|iz  }t        t        j                  |            dx}x}}|D ]  }t        t        |j                               z
  }	|	 }
|
s~t        j                  d|	       dz   ddt        j                         v st        j                  |	      rt        j                  |	      ndiz  }t        t        j                  |            d}
 y)uA   month 필터링 후에도 InsightV2 구조가 유지되어야 함Fr  r  re   rf   rh   rV   r>   rW   u+   결과가 비어있어 구조 검증 불가rZ   r[   Nr{   r|   r@   r}   )r  r   rV   rD   rE   rF   rG   rH   rI   rJ   rK   rL   r~   r   r   )rM   r^   r>   rN   r_   r`   ra   rb   r   r}   rO   r   s               r)   -test_month_filter_preserves_insight_structurez@TestMonthFiltering.test_month_filter_preserves_insight_structurem  s'   224%hYO6{NaN{aNNN{aNNNNNNsNNNsNNNNNN6NNN6NNN{NNNaNNN!NNNNNNNN 	@E&UZZ\)::G;?;??"5gY ???????w???w??????	@r(   c                    | j                         }t        |dd      }t        |      }d}||k\  }|st        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  }t        j                  d
      dz   d|iz  }t        t        j                  |            dx}x}}|D ]  }|d   }	|	j                  }d} ||      }
|
st        j                  d|d          dz   t        j                  |	      t        j                  |      t        j                  |      t        j                  |
      dz  }t        t        j                  |            dx}	x}x}}
t        |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  }t        j                  d|d          dz   d|iz  }t        t        j                  |            dx}} y)u7   month='2026-03-H1' → 3월 1~15일 메시지만 처리Fz
2026-03-H1r  re   rf   rh   rV   r>   rW   uB   3월 상반기 메시지에서 인사이트가 추출되어야 함rZ   r[   Nr   r     3월 이외 날짜: r  r  rb  r      )<=)z%(py0)s <= %(py3)sdayr?   u)   상반기(1~15일) 이외 날짜 포함: rB   rC   )r  r   rV   rD   rE   rF   rG   rH   rI   rJ   rK   rL   r  r  )rM   r^   r>   rN   r_   r`   ra   rb   r   rq   r   r   r  rO   rP   rQ   s                   r)   test_month_h1_filterz'TestMonthFiltering.test_month_h1_filterv  s   224%h\R6{eae{aeee{aeeeeeeseeeseeeeee6eee6eee{eeeaeee!eeeeeeee 	aE'l'22l9l29=l=llAUV[\iVjUk?llll'lll2lll9lll=lllllleM*1R01C`3"9```3"``````3```3```"``` I%P]J^I_````````	ar(   c                    | j                         }t        |dd      }t        |      }d}||k(  }|st        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  }t        j                  d
t        |             dz   d|iz  }t        t        j                  |            dx}x}}y)u8   month='2026-03-H2' → 3월 16~31일 메시지만 처리Fz
2026-03-H2r  r   r<   rU   rV   r>   rW   u?   3월 하반기 메시지가 없으므로 빈 결과여야 함: rZ   r[   Nr  r]   s           r)   test_month_h2_filterz'TestMonthFiltering.test_month_h2_filter  s    224%h\R6{pap{appp{appppppspppspppppp6ppp6ppp{pppappp#bcfgmcnbo!ppppppppr(   c                    | j                         }t        |dd      }t        |      }d}||k\  }|st        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  }t        j                  d
      dz   d|iz  }t        t        j                  |            dx}x}}|D ]  }|d   }	|	j                  }d} ||      }
|
st        j                  d|d          dz   t        j                  |	      t        j                  |      t        j                  |      t        j                  |
      dz  }t        t        j                  |            dx}	x}x}}
 y)uJ   month='2026-03' (기존 형식) → 여전히 3월 전체 메시지 처리Fr  r  re   rf   rh   rV   r>   rW   u+   기존 형식은 여전히 동작해야 함rZ   r[   Nr   r  r  r  r  r  s               r)   #test_month_full_backward_compatiblez6TestMonthFiltering.test_month_full_backward_compatible  sJ   224%hYO6{NaN{aNNN{aNNNNNNsNNNsNNNNNN6NNN6NNN{NNNaNNN!NNNNNNNN 	mE'l'22l9l29=l=llAUV[\iVjUk?llll'lll2lll9lll=llllll	mr(   N)r   r   r   r   r  r  r  r  r  r  r  r  r'   r(   r)   r  r    s5    V;
z	Jli@aqmr(   r  c                       e Zd ZdZd Zy)TestLLMPreCallLogginguJ   LLM 호출 직전에 _add_log + _write_progress가 호출되는지 검증.c                 4   ddl m} |j                  j                          t	        ddddt
        j                        t	        ddd	d
t
        j                        g}t        j                  ddd      5 }|j                  }ddd       	 t        j                  |d      5 }t        j                  ddg dgi      t        j                  ddgdd      t        j                  dddddgdg dgddd
      g|_        t        |d       ddd       d j!                  |j                        }d!}||v }|st#        j$                  d"|fd#||f      t#        j&                  |      d$t)        j*                         v st#        j,                  |      rt#        j&                  |      nd$d%z  }	t#        j.                  d&|j                         d'z   d(|	iz  }
t1        t#        j2                  |
            dx}}t5              5 }t        j6                  |      }ddd       d)}|v }|st#        j$                  d"|fd#||f      t#        j&                  |      d*t)        j*                         v st#        j,                  |      rt#        j&                  |      nd*d%z  }	d+d(|	iz  }
t1        t#        j2                  |
            dx}}t9        j:                  |       y# 1 sw Y   ZxY w# 1 sw Y   xY w# 1 sw Y   xY w# t9        j:                         w xY w),uK   배치 LLM 호출 전 '분석 요청 중' 로그가 기록되는지 확인.r   Nr   r0   u	   홍길동u)   #궁금증 실손보험 청구 방법은?r   z10:01u	   이영희uU   실손보험은 병원 영수증과 진단서를 보험사에 제출하면 됩니다.wz.jsonF)modesuffixdelete_call_claudera  rc  Tri   r  )has_insightinsight_typesnoise_reasonu   실손보험 청구u   보험청구u   실손보험 청구 절차u   영수증 제출g?u   실손u   실손보험 청구?u   영수증과 진단서 제출)
r   r   r   r   r   r   r   r   r   r   )r;   progress_file u   LLM 분석 요청 중rj   rl   	logs_textrn   u2   'LLM 분석 요청 중' 로그가 없음. 로그: rB   rC   statusprogress_datazassert %(py5)s)&kakao_knowledge.knowledge_extractor_v2knowledge_extractor_v2_recent_logsclearr   r	   r&   r   NamedTemporaryFilenamer   r#  rk  rl  r!  r   r   rD   rE   rI   rF   rG   rH   rJ   rK   rL   openloadr  unlink)rM   modr^   fr  mock_clauder  rq   rN   rP   rQ   r  s               r)   test_llm_pre_call_log_messagez3TestLLMPreCallLogging.test_llm_pre_call_log_message  s   < 	  ! C (( ! o ((
" ((c'%P 	#TUFFM	#,	%c>2 k
 JJ	SU,V+WXYJJttf^`abJJ!6 $$2#?'9&:&)*,!)
$:"A  +'( % "//< !1!12I* *i7  *i  I +  v   /8  I /8    ESEUEUDVW    
 m$ - $		!- ,8},,,,8},,,8,,,,,,},,,},,,,,,, IIm$_	# 	# H- - IIm$sK   5KL  !A*K'C6L  K4B-L  K$'K1,L  4K=9L   LN)r   r   r   r   r  r'   r(   r)   r  r    s    TG%r(   r  c                   (    e Zd ZdZd Zd Zd Zd Zy)TestCheckpointCleanupu6   체크포인트 정리 기능 테스트 (아르고스)c                    |dz  }|dz  }|j                  dd       |j                  dd       |j                  } |       }|st        j                  d      dz   dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      d	z  }t        t        j                  |            d
x}}|j                  } |       }|st        j                  d      dz   dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      d	z  }t        t        j                  |            d
x}}t        t        |             |j                  } |       }| }|st        j                  d      dz   dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      d	z  }t        t        j                  |            d
x}x}}|j                  } |       }| }|st        j                  d      dz   dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      d	z  }t        t        j                  |            d
x}x}}y
)um   output_dir에 체크포인트 파일이 있을 때 _cleanup_checkpoints 호출 시 두 파일이 삭제된다checkpoint_threads.jsoncheckpoint_refined_threads.json{}utf-8encoding:   사전 조건: checkpoint_threads.json이 존재해야 함C
>assert %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.exists
}()
}cp1rJ  NuB   사전 조건: checkpoint_refined_threads.json이 존재해야 함cp2u.   checkpoint_threads.json이 삭제되어야 함G
>assert not %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.exists
}()
}u6   checkpoint_refined_threads.json이 삭제되어야 함)
write_textexistsrD   rJ   rF   rG   rH   rI   rK   rL   r   r   )	rM   tmp_pathr  r  rO   r   r   r_   rQ   s	            r)   %test_cleanup_removes_checkpoint_filesz;TestCheckpointCleanup.test_cleanup_removes_checkpoint_files  s   22::tg.tg.zzYz|Y|YYYYYYYYYsYYYsYYYzYYY|YYYYYYzzaz|a|aaaaaaaaasaaasaaazaaa|aaaaaaS]+::Q:<Q<QQQ!QQQQQQQ3QQQ3QQQ:QQQ<QQQQQQ::Y:<Y<YYY!YYYYYYY3YYY3YYY:YYY<YYYYYYr(   c                    	 t        t        |             y# t        $ ra}d}|sQt        j                  d|       dz   dt        j
                  |      iz  }t        t        j                  |            d}Y d}~yd}~ww xY w)ur   체크포인트 파일이 없는 빈 디렉토리에서 _cleanup_checkpoints 호출 시 에러 없이 완료된다FuG   체크포인트 파일이 없어도 에러 없이 완료되어야 함: r   rX   N)r   r   r   rD   rJ   rI   rK   rL   )rM   r  excrq   r   s        r)   'test_cleanup_nonexistent_files_no_errorz=TestCheckpointCleanup.test_cleanup_nonexistent_files_no_error  s]    	j X/ 	ji5iicdgchiiii5iiiiii	js    	BAA<<Bc                    |dz  }|dz  }|j                  dd       |j                  } |       }|st        j                  d      dz   dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      d	z  }t        t        j                  |            d
x}}|j                  } |       }| }|st        j                  d      dz   dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      d	z  }t        t        j                  |            d
x}x}}	 t        t        |             |j                  } |       }| }|st        j                  d      dz   dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      d	z  }t        t        j                  |            d
x}x}}y
# t        $ rb}	d}
|
sQt        j                  d|	       dz   dt        j                  |
      iz  }t        t        j                  |            d
}
Y d
}	~	(d
}	~	ww xY w)um   checkpoint_threads.json만 존재할 때 존재하는 파일만 삭제되고 에러가 발생하지 않는다r  r  r  r  r  r  r  r  rJ  Nu?   사전 조건: checkpoint_refined_threads.json은 없어야 함r  r  Fu>   부분 파일만 있어도 에러 없이 완료되어야 함: r   rX   u;   존재했던 checkpoint_threads.json은 삭제되어야 함)r  r  rD   rJ   rF   rG   rH   rI   rK   rL   r   r   r   )rM   r  r  r  rO   r   r   r_   rQ   r  rq   r   s               r)   test_cleanup_partial_filesz0TestCheckpointCleanup.test_cleanup_partial_files   s   22::tg.zzYz|Y|YYYYYYYYYsYYYsYYYzYYY|YYYYYY::b:<b<bbb!bbbbbbb3bbb3bbb:bbb<bbbbbb	a X/ ::^:<^<^^^!^^^^^^^3^^^3^^^:^^^<^^^^^^  	a`5``Z[^Z_````5``````	as   I2 2	K;AKKc                 ^   t               }t        |dt        |             |dz  }|dz  }|j                  } |       }| }|st	        j
                  d      dz   dt        j                         v st	        j                  |      rt	        j                  |      ndt	        j                  |      t	        j                  |      dz  }t        t	        j                  |            d	x}x}}|j                  } |       }| }|st	        j
                  d
      dz   dt        j                         v st	        j                  |      rt	        j                  |      ndt	        j                  |      t	        j                  |      dz  }t        t	        j                  |            d	x}x}}y	)uv   extract_knowledge_v2(use_llm=False, output_dir=...) 완료 후 체크포인트 파일이 남아있지 않아야 한다Fr   r  r  uR   extract_knowledge_v2 완료 후 checkpoint_threads.json이 남아있으면 안 됨r  r  rJ  NuZ   extract_knowledge_v2 완료 후 checkpoint_refined_threads.json이 남아있으면 안 됨r  )r*   r   r   r  rD   rJ   rF   rG   rH   rI   rK   rL   )	rM   r  r^   r  r  rO   r   r_   rQ   s	            r)   *test_rule_based_extract_cleans_checkpointsz@TestCheckpointCleanup.test_rule_based_extract_cleans_checkpoints  s   $&XuXO22:::: 	
:< 	
< 	
 	
  a	
 	
	6	
 	
   	
 	
 		  	
 	
 		  	
 	
 		   	
 	
 	
 	
 	
 :: 	
:< 	
< 	
 	
  i	
 	
	6	
 	
   	
 	
 		  	
 	
 		  	
 	
 		   	
 	
 	
 	
 	
 	
r(   N)r   r   r   r   r  r  r  r  r'   r(   r)   r  r    s    @Zj_ 
r(   r  )r   )(r   builtinsrF   _pytest.assertion.rewrite	assertionrewriterD   rk  r  r  r   unittest.mockr   r  r   r   r   rW  r   r	   rX  r
   r~   r*   r-   r2   r6   r8   r   r   r   r   r  r.  r  r\  r^  r  r  r  r'   r(   r)   <module>r     s   
   	   J J < 1 0666@~_ ~_L0d 0dp\o \oHrl rlt9? 9?BYe YeBYI YIBC s ,vj vj|zm zmDJ% J%d6
 6
r(   