
    Si             	          d Z ddlZddlZddlZddlZddlZddlmZ ddlm	Z	m
Z
mZ ddlZd Z e         eej                  j                  dd            Zedz  d	z  Z ee      ej&                  vr"ej&                  j)                  d ee             d
ef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 G d d      Z ej@                         d        Z! ej@                         d        Z" G d d       Z# G d! d"      Z$ G d# d$      Z% G d% d&      Z& G d' d(      Z' G d) d*      Z( G d+ d,      Z) G d- d.      Z* G d/ d0      Z+ G d1 d2      Z, G d3 d4      Z- G d5 d6      Z. G d7 d8      Z/i d9d:d;d<d=d=d=d>d?d@d;dAd=d=d=d>dBdCd;dDd=d=d=d>dEdFd;dGd=d=d=d>dHdId;dJd=d=d=d>dKdLdMdNd=d=d=d>dOdPdMdAd=d=d=d>dQdRdMdDd=d=d=d>dSdTdMdUd=d=d=d>dVdWdMdJd=d=d=d>dXdYdZd[d=d=d=d>d\d]dZdAd=d=d=d>d^d_dZdDd=d=d=d>d`dadZdUd=d=d=d>dbdcdZdJd=d=d=d>dddedfdgd=d=d=d>dhdidjdkd=d=d=d>dldmdnd=d=d=d>dodpdqd=d=d=d>drZ0 G ds dt      Z1 G du dv      Z2y)wu  
test_group_chat.py

group_chat.py 단위 테스트 (아르고스 작성)

테스트 항목:
1. load_env_keys: 환경변수 로드 정상 작동
2. load_bot_token: 환경변수 / .env.keys 폴백 / RuntimeError
3. load_personas: 조직도 우선, personas.json 폴백, DEFAULT_PERSONAS 폴백
4. load_personas_from_org: 조직도 파싱 상세 검증
5. format_persona_tag: 태그 포맷 검증
6. EMOJI_MAP: 이모지 매핑 검증
7. read_trigger: 트리거 파일 읽기 + 삭제 확인
8. dump_session / load_session: 세션 상태 파일 덤프/복구
9. GroupChatSession.start: 입장 시퀀스 정상 동작 (send 호출 횟수)
10. GroupChatSession.end: 퇴장 시퀀스 + 세션 파일 정리
11. GroupChatSession.add_user_input: auto_turns 리셋, 히스토리 추가
12. select_next_speaker: 직전 발화자 제외, 폴백 로직
13. MAX_AUTO_TURNS 제한: auto_turns >= 6 시 대기
    N)Path)	MagicMock	mock_openpatchc                     dt         j                  vr_t        j                  d      } t	        t	        d            | _        t	        t	        d            | _        | t         j                  d<   yt         j                  d   }t        |d      st	        t	        d            |_        t        |d      st	        t	        d            |_        yy)u/   requests 스텁을 sys.modules에 등록한다.requestsT)okreturn_valuegetpostN)sysmodulestypes
ModuleTyper   r   r   hasattr)requests_stubstubs     F/home/jay/workspace/.worktrees/task-2117-dev1/tests/test_group_chat.py_inject_stubsr   '   s     $((4&I4FG%93EF"/J {{:&tU# i4.@ADHtV$!yD/ABDI %    WORKSPACE_ROOTz/home/jay/workspacememoryzorganization-structure.jsonreturnc                     t         j                         syt        t         d      5 } t        j                  |       }ddd       t               }j                  di       }|j                  di       j                  dg       D ]  }|j                  d      d	k7  r|j                  d
      dk(  r|j                  dg       D ]  }|j                  d      d	k7  r|j                  d      xs i }|j                  d      r0|d   dk7  r(|j                  d      d	k(  r|j                  |d          |j                  dg       D ]?  }|j                  d      dk(  r|j                  d      dv s,|j                  |d          A  |j                  d      xs i }|j                  d      rC|d   dk7  r;|j                  d      r*|j                  dd	      }|dv r|j                  |d          |j                  dg       D ]B  }|j                  d      dk(  r|j                  dd	      }	|	dv s/|j                  |d          D  |j                  di       j                  dg       D ]  }
|
j                  d      d	k7  r|
j                  d      xs i }|j                  d      r/|d   dk7  r'|j                  d      dv r|j                  |d          |
j                  dg       D ]B  }|j                  d      dk(  r|j                  dd	      }	|	dv s/|j                  |d          D  t        |      S # 1 sw Y   xY w)uu   organization-structure.json에서 load_personas_from_org와 동일한 로직으로 활성 인원 수를 계산한다.utf-8encodingN	structurecolumnsteamsstatusactiveteam_idzdevelopment-office	sub_teamsleadidanumembers)r$   	availablenamerowscenters)	ORG_STRUCTURE_PATHexistsopenjsonloadsetr   addlen)forgseenr    teamsub_teamr'   memberlead_statusmember_statuscenters              r   _count_expected_org_personasr@   E   s   $$&	 7	3 qiilDR(I i,00"= +88H)88I"66 HH["5 
/<<)X5||F+1r88D>d4jE&9dhhx>PT\>\HHT$Z(&ll9b9 /Fzz$'50 zz(+/FF.	/
/ 88F#)rDxx~$t*"5$((6:J"hhx:"99HHT$Z(((9b1 +::d#u, &

8X > $;;HHVD\*+-+< --+//	2> '::h8+zz&!'R88D>d4jE1dhhx6HLc6cHHT$Z jjB/ 	'Fzz$5("JJx:M 77&	'' t9c s   L>>Mc                      t                t        t        j                  j	                               D ]  } | dk(  s	t        j                  | =  ddl}|S )u_   sys.modules에서 group_chat을 제거해 매 호출마다 새로운 상태로 임포트한다.
group_chatr   N)r   listr   r   keysrB   )keygcs     r   _fresh_modulerG   }   sF    OCKK$$&' !,C ! Ir   c                   (    e Zd ZdZd Zd Zd Zd Zy)TestLoadEnvKeysu   load_env_keys 함수 테스트.c                    t               }t        j                  t        j                  ddid      5  t        d      5 }|j                          |j                          ddd       ddd       y# 1 sw Y   xY w# 1 sw Y   yxY w)uN   GROUP_CHAT_BOT_TOKEN이 이미 있으면 subprocess를 호출하지 않는다.GROUP_CHAT_BOT_TOKENzalready-setFclearsubprocess.runN)rG   r   dictosenvironload_env_keysassert_not_called)selfrF   mock_runs      r   !test_skips_when_token_already_setz1TestLoadEnvKeys.test_skips_when_token_already_set   sv    _ZZ

%;]$KSXY 	-'( -H  "**,-	- 	-- -	- 	-s"   A=!A1 A=1A:	6A==Bc                 n   t               }|dz  }|j                  d       t               }d|_        t        j
                  j                         D ci c]  \  }}|dk7  s|| }}}t        j                  t        j
                  |d      5  t        j                  |dt        |            5  t        d|	      5  |j                          t        j
                  j                  d      d
k(  sJ 	 ddd       ddd       ddd       yc c}}w # 1 sw Y    xY w# 1 sw Y   $xY w# 1 sw Y   yxY w)uW   GROUP_CHAT_BOT_TOKEN이 없을 때 .env.keys에서 읽어 환경변수에 설정한다.	.env.keysz1export GROUP_CHAT_BOT_TOKEN=test-token-from-file
z8GROUP_CHAT_BOT_TOKEN=test-token-from-file
OTHER_VAR=foo
rK   TrL   ENV_KEYS_FILErN   r
   ztest-token-from-fileN)rG   
write_textr   stdoutrP   rQ   itemsr   rO   objectstrrR   r   rT   tmp_pathrF   env_filefake_resultkvenv_without_tokens           r   test_loads_token_from_env_filez.TestLoadEnvKeys.test_loads_token_from_env_file   s   _k)PQkY.0jj.>.>.@`daAI_D_QT``ZZ

$5TB 	\b/3x=A \++F \$$&::>>*@AE[[[[\\	\ 	\ a\ \\ \	\ 	\sH   D!D!D+0D>5D4D<D+DDD(	$D++D4c                 N   t               }|dz  }|j                  d       t               }d|_        t        j
                  j                         D ci c]  \  }}|dk7  s|| }}}t        j                  t        j
                  |d      5  t        j                  |dt        |            5  t        d|	      5  |j                          dt        j
                  vsJ 	 d
d
d
       d
d
d
       d
d
d
       y
c c}}w # 1 sw Y    xY w# 1 sw Y   $xY w# 1 sw Y   y
xY w)u^   subprocess 출력에 GROUP_CHAT_BOT_TOKEN이 없으면 환경변수가 설정되지 않는다.rX   zexport SOME_OTHER_VAR=value
zSOME_OTHER_VAR=value
rK   TrL   rY   rN   r
   N)rG   rZ   r   r[   rP   rQ   r\   r   rO   r]   r^   rR   r_   s           r   test_no_token_in_env_filez)TestLoadEnvKeys.test_no_token_in_env_file   s   _k);<k5.0jj.>.>.@`daAI_D_QT``ZZ

$5TB 	Db/3x=A D++F D$$&1CCCDD	D 	D aD DD D	D 	DsH   C=!C=!D0D>%D$D,DDDD	DD$c                    t               }|dz  }t        j                  j                         D ci c]  \  }}|dk7  s|| }}}t	        j
                  t        j                  |d      5  t	        j                  |dt        |            5  |j                          ddd       ddd       yc c}}w # 1 sw Y   xY w# 1 sw Y   yxY w)u8   env.keys 파일이 없어도 예외 없이 반환한다.znonexistent.env.keysrK   TrL   rY   N)	rG   rP   rQ   r\   r   rO   r]   r^   rR   )rT   r`   rF   missing_pathrc   rd   re   s          r   $test_missing_env_file_does_not_raisez4TestLoadEnvKeys.test_missing_env_file_does_not_raise   s    _"88.0jj.>.>.@`daAI_D_QT``ZZ

$5TB 	#b/3|3DE #  "#	# 	# a# #	# 	#s.   B0B0-!CB6C6B?	;CCN)__name__
__module____qualname____doc__rV   rf   rh   rk    r   r   rI   rI      s    )-\ D #r   rI   c                   (    e Zd ZdZd Zd Zd Zd Zy)TestLoadBotTokenuK   load_bot_token 함수 테스트 - GROUP_CHAT_BOT_TOKEN 환경변수 기반.c                     t               }t        j                  t        j                  ddid      5  |j                         }ddd       dk(  sJ y# 1 sw Y   xY w)u?   GROUP_CHAT_BOT_TOKEN 환경변수에서 토큰을 로드한다.rK   z1234567:ENV_TOKENFrL   N)rG   r   rO   rP   rQ   load_bot_token)rT   rF   tokens      r   test_loads_from_env_varz(TestLoadBotToken.test_loads_from_env_var   sW    _ZZ

%;=P$QY^_ 	(%%'E	(++++	( 	(s   AAc                 2   t               }|dz  }|j                  d       t               }d|_        t        j
                  j                         D ci c]  \  }}|dk7  s|| }}}t        j                  t        j
                  |d      5  t        j                  |dt        |            5  t        d|	      5  |j                         }d
d
d
       d
d
d
       d
d
d
       dk(  sJ y
c c}}w # 1 sw Y   'xY w# 1 sw Y   +xY w# 1 sw Y   /xY w)u<   환경변수 없을 때 .env.keys에서 폴백 로드한다.rX   z0export GROUP_CHAT_BOT_TOKEN=9999:FALLBACK_TOKEN
z1GROUP_CHAT_BOT_TOKEN=9999:FALLBACK_TOKEN
OTHER=x
rK   TrL   rY   rN   r
   Nz9999:FALLBACK_TOKEN)rG   rZ   r   r[   rP   rQ   r\   r   rO   r]   r^   rt   )	rT   r`   rF   ra   rb   rc   rd   re   ru   s	            r   !test_loads_from_env_keys_fallbackz2TestLoadBotToken.test_loads_from_env_keys_fallback   s    _k)OPkR.0jj.>.>.@`daAI_D_QT``ZZ

$5TB 	0b/3x=A 0++F 0--/E00	0
 ---- a0 00 0	0 	0sH   C/!C/!D0D>C5DD5C>:DD
	DDc           	         t               }|dz  }|j                  d       t               }d|_        t        j
                  j                         D ci c]  \  }}|dk7  s|| }}}t        j                  t        j
                  |d      5  t        j                  |dt        |            5  t        d|	      5  t        j                  t              5  |j                          d
d
d
       d
d
d
       d
d
d
       d
d
d
       y
c c}}w # 1 sw Y   (xY w# 1 sw Y   ,xY w# 1 sw Y   0xY w# 1 sw Y   y
xY w)uX   환경변수도 없고 .env.keys에서도 못 찾으면 RuntimeError를 발생시킨다.rX   zexport OTHER_VAR=something
zOTHER_VAR=something
rK   TrL   rY   rN   r
   N)rG   rZ   r   r[   rP   rQ   r\   r   rO   r]   r^   pytestraisesRuntimeErrorrt   r_   s           r   test_raises_when_no_token_foundz0TestLoadBotToken.test_raises_when_no_token_found   s   _k):;k4.0jj.>.>.@`daAI_D_QT``ZZ

$5TB 	,b/3x=A ,++F ,|4 ,))+,,,	, 	, a, ,, ,, ,	, 	,s`   D
!D
!D40D(>DD	)D1D(9D4DDD%!D((D1	-D44D=c                    t               }|dz  }|j                  d       t               }d|_        t	        j
                  t        j                  ddid      5  t	        j                  |dt        |            5  t	        d	|
      5 }|j                         }|j                          ddd       ddd       ddd       dk(  sJ y# 1 sw Y   !xY w# 1 sw Y   %xY w# 1 sw Y   )xY w)uI   환경변수와 .env.keys 모두 있을 때 환경변수가 우선한다.rX   z+export GROUP_CHAT_BOT_TOKEN=FALLBACK_TOKEN
z$GROUP_CHAT_BOT_TOKEN=FALLBACK_TOKEN
rK   ENV_PRIORITY_TOKENFrL   rY   rN   r
   N)rG   rZ   r   r[   r   rO   rP   rQ   r]   r^   rt   rS   )rT   r`   rF   ra   rb   rU   ru   s          r   test_env_var_takes_priorityz,TestLoadBotToken.test_env_var_takes_priority   s    _k)JKkDZZ

%;=Q$RZ_` 	1b/3x=A 1++F 1(--/E..011	1 ,,,,1 11 1	1 	1s<   !C";C	!C
*C2C"
CCC	C""C+N)rl   rm   rn   ro   rv   rx   r}   r   rp   r   r   rr   rr      s    U,.", -r   rr   c                   @    e Zd ZdZd Zd Zd Zd Zd Zd Z	d Z
d	 Zy
)TestLoadPersonasuH   load_personas 함수 테스트 - 조직도 우선, personas.json 폴백.c                     t               }t        j                         st        j                  d       |j                         }t        |t              sJ t        |      dkD  sJ y)u\   조직도 파일에서 페르소나를 로드한다 (실제 파일 기반 통합 테스트).3   organization-structure.json 파일이 없습니다.r   N)	rG   r/   r0   rz   skipload_personas
isinstancerO   r6   rT   rF   results      r   test_loads_from_org_structurez.TestLoadPersonas.test_loads_from_org_structure  sP    _!((*KKMN!!#&$'''6{Qr   c                     t               }t        j                         st        j                  d       |j                         }d|vsJ y)uI   anu가 조직도 로드 결과에서 제외된다 (실제 파일 기반).r   r)   N)rG   r/   r0   rz   r   r   r   s      r   test_excludes_anu_from_orgz+TestLoadPersonas.test_excludes_anu_from_org  s>    _!((*KKMN!!#F"""r   c           
      2   t               }dddddddddd	g d
gidg idi}t        dt        t        j                  |                  5  t        dd      5  |j                         }ddd       ddd       dvsJ y# 1 sw Y   xY w# 1 sw Y   xY w)uG   planned 상태인 항목은 조직도 로드 결과에서 제외된다.r    r"   zstrategy-teamu	   전략팀plannedzeusu	   제우스u   팀장)r(   r,   r#   role)r%   	team_namer#   r'   r*   r.   )r!   r-   zbuiltins.open)	read_datazpathlib.Path.existsTr
   N)rG   r   r   r2   dumpsload_personas_from_org)rT   rF   fake_orgr   s       r   test_excludes_planned_statusz-TestLoadPersonas.test_excludes_planned_status   s    _ '6)4&/&,(3*3(0	% (*  #B#
* ?I

88L$MN 	5,4@ 52245	5 V###5 5	5 	5s$   BB*BB
	BBc                    t               }ddddddddi}|d	z  }|j                  t        j                  |d
      d       t	        j
                  |dd      5  t	        j
                  |dt        |            5  |j                         }ddd       ddd       dv sJ |d   d   dk(  sJ y# 1 sw Y   %xY w# 1 sw Y   )xY w)u=   조직도 로드 실패 시 personas.json으로 폴백한다.
custom_botu   커스텀봇   테스트팀u	   테스터QAu	   꼼꼼함 r,   r:   r   	expertisepersonalitypersona_desczpersonas.jsonFensure_asciir   r   ORG_STRUCTURE_FILE/nonexistent/path/org.jsonPERSONAS_FILENr,   )rG   rZ   r2   r   r   r]   r^   r   )rT   r`   rF   custompersonas_filer   s         r    test_falls_back_to_personas_jsonz1TestLoadPersonas.test_falls_back_to_personas_json>  s    _&&#!* "	
 !?2  F!GRY Z\\"24PQ 	,b/3}3EF ,))+,	, v%%%l#F+~===	, ,	, 	,s$   !B<;B0B<0B9	5B<<Cc                 $   t               }|dz  }t        j                  |dd      5  t        j                  |dt        |            5  |j	                         }ddd       ddd       |j
                  u sJ y# 1 sw Y   "xY w# 1 sw Y   &xY w)u:   모든 소스 실패 시 DEFAULT_PERSONAS를 반환한다.zno_personas.jsonr   r   r   N)rG   r   r]   r^   r   DEFAULT_PERSONAS)rT   r`   rF   missing_personasr   s        r   test_falls_back_to_defaultsz,TestLoadPersonas.test_falls_back_to_defaultsU  s    _#&88\\"24PQ 	,b/37G3HI ,))+,	, ,,,,,, ,	, 	,s#   !BA:B:B	?BBc                     t               }t        j                         st        j                  d       |j                         }t               }|dkD  sJ d       t        |      |k(  sJ y)u^   조직도에서 기대 인원 수를 로드한다 (anu 제외, planned 제외, 동적 계산).r   r   :   organization-structure.json에서 인원 수 계산 실패NrG   r/   r0   rz   r   r   r@   r6   rT   rF   r   expected_counts       r   test_org_loads_expected_countz.TestLoadPersonas.test_org_loads_expected_count`  s^    _!((*KKMN**,57!_#__!6{n,,,r   c                     t               }h d}|j                  t        |j                  j	                                     sJ y)u?   DEFAULT_PERSONAS에 필수 페르소나가 포함되어 있다.>   irislokiodinthorathenahermesvulcanN)rG   issubsetr4   r   rD   rT   rF   expected_keyss      r   +test_default_personas_contain_expected_keysz<TestLoadPersonas.test_default_personas_contain_expected_keysj  s6    _V%%c"*=*=*B*B*D&EFFFr   c                     t               }|j                  j                         D ]/  \  }}d|v s
J | d       d|v s
J | d       d|vr'J | d        y)u]   DEFAULT_PERSONAS 각 항목이 team, persona_desc 필드를 가지고 emoji 필드가 없다.r:   u   에 team 필드 없음r   u   에 persona_desc 필드 없음emojiu%   에 emoji 필드가 있으면 안 됨N)rG   r   r\   )rT   rF   rE   personas       r   %test_default_personas_have_new_fieldsz6TestLoadPersonas.test_default_personas_have_new_fieldsp  s~    _//557 	YLCW$D-C&DD$!W,T5S.TT,')XcU2W+XX)	Yr   N)rl   rm   rn   ro   r   r   r   r   r   r   r   r   rp   r   r   r   r     s/    R#$<>.	--GY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
)TestLoadPersonasFromOrgu/   load_personas_from_org 함수 상세 테스트.c                    t               }t        j                         st        j                  d       |j                         }d|v sJ |d   d   dk(  sJ d|v sJ |d   d   dk(  sJ d|v sJ |d   d   dk(  sJ d|v sJ |d   d   dk(  sJ d|v sJ |d   d   d	k(  sJ d
|v sJ |d
   d   d	k(  sJ d|v sJ |d   d   dk(  sJ d|v sJ |d   d   dk(  sJ y)uQ   개발1팀, 개발2팀, 개발3팀 멤버를 파싱한다 (실제 파일 기반).r   r   r:   
   개발1팀r   r   r   r   
   개발2팀r   rau
   개발8팀anubisNrG   r/   r0   rz   r   r   r   s      r   test_parses_dev_teamsz-TestLoadPersonasFromOrg.test_parses_dev_teams  s`   _!((*KKMN**, 6!!!h'<7776!!!h'<777f~f%5556!!!h'<777 f~f%555f~f%555 v~~d|F#|3336!!!h'<777r   c                     t               }t        j                         st        j                  d       |j                         }d|v sJ |d   d   dk(  sJ |d   d   dk(  sJ y)u@   보안팀 리더(로키)를 파싱한다 (실제 파일 기반).r   r   r:   	   보안팀r   u   보안팀장 (Primary)Nr   r   s      r   test_parses_security_teamz1TestLoadPersonasFromOrg.test_parses_security_team  sm    _!((*KKMN**,f~f%444f~f%)AAAAr   c                     t               }t        j                         st        j                  d       |j                         }d|v sJ |d   d   dk(  sJ d|v sJ |d   d   dk(  sJ d|v sJ |d   d   dk(  sJ y	)
u`   횡단조직(QC 센터, DevOps 센터, 디자인 센터)을 파싱한다 (실제 파일 기반).r   maatr:   	   QC 센터janus   DevOps 센터venusu   Gemini 센터Nr   r   s      r   test_parses_centersz+TestLoadPersonasFromOrg.test_parses_centers  s    _!((*KKMN**,f~f%444&   gv&/999&   gv&/999r   c                    t               }t        j                         st        j                  d       |j                         }h d}|j                         D ]/  \  }}|t        |j                               z
  }|s%J | d|         y)u\   각 페르소나에 name, team, role, expertise, personality, persona_desc 필드가 있다.r   >   r,   r   r:   r   r   r   u   에 누락된 필드: N)	rG   r/   r0   rz   r   r   r\   r4   rD   )rT   rF   r   required_fieldsrE   r   missings          r    test_persona_has_required_fieldsz8TestLoadPersonasFromOrg.test_persona_has_required_fields  s~    _!((*KKMN**,^"LLN 	HLC%GLLN(;;GG3%'=gY GG;	Hr   c                     t               }t        j                         st        j                  d       |j                         }|d   d   dk(  sJ |d   d   dk(  sJ |d   d   dk(  sJ y	)
uK   '헤르메스 (Hermes)' 형태의 이름을 '헤르메스'로 파싱한다.r   r   r,      헤르메스r      로키r   	   마아트Nr   r   s      r   &test_name_parsing_strips_parentheticalz>TestLoadPersonasFromOrg.test_name_parsing_strips_parenthetical  sx    _!((*KKMN**, h'>999f~f%111f~f%444r   c                     t               }t        j                         st        j                  d       |j                         }d|vsJ y)u!   anu는 결과에서 제외된다.r   r)   Nr   r   s      r   test_excludes_anuz)TestLoadPersonasFromOrg.test_excludes_anu  s>    _!((*KKMN**,F"""r   c                     t               }t        j                         st        j                  d       |j                         }t               }|dkD  sJ d       t        |      |k(  sJ y)uK   planned 상태인 팀은 파싱하지 않는다 (인원 수 동적 검증).r   r   r   Nr   r   s       r   test_excludes_planned_teamsz3TestLoadPersonasFromOrg.test_excludes_planned_teams  s`    _!((*KKMN**, 67!_#__!6{n,,,r   c                    t               }t        j                  |dt        |dz              5  t	        j
                  t              5  |j                          ddd       ddd       y# 1 sw Y   xY w# 1 sw Y   yxY w)u8   조직도 파일이 없으면 예외를 발생시킨다.r   znonexistent.jsonN)rG   r   r]   r^   rz   r{   	Exceptionr   )rT   r`   rF   s      r   test_raises_on_missing_org_filez7TestLoadPersonasFromOrg.test_raises_on_missing_org_file  sl    _\\"2CCU8U4VW 	,y) ,))+,	, 	,, ,	, 	,s#   A6A*A6*A3	/A66A?N)rl   rm   rn   ro   r   r   r   r   r   r   r   r   rp   r   r   r   r   ~  s/    98:	B: 
H5#-,r   r   c                   :    e Zd ZdZd Zd Zd Zd Zd Zd Z	d Z
y	)
TestFormatPersonaTagu$   format_persona_tag 함수 테스트.c                 Z    t               }dddddi}|j                  d|      }|dk(  sJ y)ue   team과 role이 모두 있으면 '⚡ 헤르메스(개발1팀/개발1팀장)' 형태를 반환한다.r   r   r      개발1팀장r,   r:   r   u*   ⚡ 헤르메스(개발1팀/개발1팀장)NrG   format_persona_tagrT   rF   personas_datar   s       r   test_format_with_team_and_rolez3TestFormatPersonaTag.test_format_with_team_and_role  sD    _&$'
 &&x?EEEEr   c                 Z    t               }dddddi}|j                  d|      }|dk(  sJ y)uS   team이 없고 role만 있으면 '이모지 이름(역할)' 형태를 반환한다.r   r   r   r   r   u   ⚡ 헤르메스(개발1팀장)Nr   r   s       r   test_format_with_role_onlyz/TestFormatPersonaTag.test_format_with_role_only  sD    _&'
 &&x?::::r   c                 ~    t               }dddddi}|j                  d|      }|j                  d      sJ d|v sJ y)u<   EMOJI_MAP에 없는 키는 '💬' 이모지를 사용한다.unknown_botu   알수없는봇r   u   봇r   u   💬N)rG   r   
startswithr   s       r   .test_format_unknown_persona_uses_default_emojizCTestFormatPersonaTag.test_format_unknown_persona_uses_default_emoji  sW    _)&
 &&}mD  ((( F***r   c                     t               }i }|j                  d|      }t        |t              sJ t	        |      dkD  sJ y)uU   personas_data에 없는 키를 처리할 때 키 자체를 이름으로 사용한다.nonexistentr   N)rG   r   r   r^   r6   r   s       r   test_format_missing_persona_keyz4TestFormatPersonaTag.test_format_missing_persona_key   s@    _&&}mD&#&&&6{Qr   c                 Z    t               }dddddi}|j                  d|      }|dk(  sJ y)u(   오딘의 태그 포맷을 검증한다.r      오딘r      개발2팀장r   u(   👁️ 오딘(개발2팀/개발2팀장)Nr   r   s       r   test_format_odin_tagz)TestFormatPersonaTag.test_format_odin_tag)  sD    _ $'
 &&v}=CCCCr   c                 Z    t               }dddddi}|j                  d|      }|dk(  sJ y)u(   로키의 태그 포맷을 검증한다.r   r   r   u   보안팀 리더r   u'   🎭 로키(보안팀/보안팀 리더)Nr   r   s       r   test_format_loki_tagz)TestFormatPersonaTag.test_format_loki_tag6  sD    _ #*
 &&v}=BBBBr   c                     t               }t        j                         st        j                  d       |j                         }|j                  d|      }d|v sJ d|v sJ d|v sJ y)u5   실제 조직도 데이터로 포맷을 검증한다.r   r   r   r   r   N)rG   r/   r0   rz   r   r   r   r   s       r   test_format_with_real_org_dataz3TestFormatPersonaTag.test_format_with_real_org_dataC  sl    _!((*KKMN113&&x?'''v%%%&(((r   N)rl   rm   rn   ro   r   r   r   r   r   r   r   rp   r   r   r   r     s+    .F;+DC
)r   r   c                   .    e Zd ZdZd Zd Zd Zd Zd Zy)TestEmojiMapu   EMOJI_MAP 상수 테스트.c                     t               }h d}|j                  t        |j                  j	                                     sJ y)u;   주요 페르소나가 EMOJI_MAP에 있는지 확인한다.   r   r   isisr   r   r   r   argosfreyahorusr   mimirsobekr   r   r   r   r   heimdallN)rG   r   r4   	EMOJI_MAPrD   r   s      r   test_known_personas_have_emojiz+TestEmojiMap.test_known_personas_have_emojiX  s8    _
* %%c",,*;*;*=&>???r   c                 @    t               }|j                  d   dk(  sJ y)u)   헤르메스의 이모지가 '⚡'이다.r   u   ⚡NrG   r  rT   rF   s     r   test_hermes_emojizTestEmojiMap.test_hermes_emojir  s     _||H%...r   c                 @    t               }|j                  d   dk(  sJ y)u$   로키의 이모지가 '🎭'이다.r   u   🎭Nr  r  s     r   test_loki_emojizTestEmojiMap.test_loki_emojiw  s     _||F#v---r   c                 P    t               }t        |j                  t              sJ y)u'   EMOJI_MAP이 딕셔너리 타입이다.N)rG   r   r  rO   r  s     r   test_emoji_map_is_dictz#TestEmojiMap.test_emoji_map_is_dict|  s    _",,---r   c                     t               }|j                  j                         D ]7  \  }}t        |t              s
J | d       t        |      dkD  r/J | d        y)u+   EMOJI_MAP의 모든 값이 문자열이다.u$   의 이모지가 문자열이 아님r   u   의 이모지가 빈 문자열N)rG   r  r\   r   r^   r6   )rT   rF   rE   r   s       r   !test_emoji_map_values_are_stringsz.TestEmojiMap.test_emoji_map_values_are_strings  se    _,,,,. 	JJCeS)WcU2V+WW)u:>IcU*H#II>	Jr   N)	rl   rm   rn   ro   r  r  r  r  r  rp   r   r   r  r  U  s     %@4/
.
.
Jr   r  c                   "    e Zd ZdZd Zd Zd Zy)TestReadTriggeru   read_trigger 함수 테스트.c                     t               }|dz  }t        j                  |dt        |            5  |j	                         }ddd       J y# 1 sw Y   xY w)u3   트리거 파일이 없으면 None을 반환한다.group_chat_trigger.jsonTRIGGER_FILEN)rG   r   r]   r^   read_triggerrT   r`   rF   r   r   s        r   "test_returns_none_when_file_absentz2TestReadTrigger.test_returns_none_when_file_absent  sU    _66\\"nc'l; 	'__&F	' ~~	' 	'   AAc                 8   t               }ddd}|dz  }|j                  t        j                  |d      d       t	        j
                  |d	t        |            5  |j                         }d
d
d
       |k(  sJ |j                         rJ y
# 1 sw Y   #xY w)u(   트리거 파일을 읽고 삭제한다.start   배포 논의)actiontopicr  Fr   r   r   r  N)	rG   rZ   r2   r   r   r]   r^   r  r0   )rT   r`   rF   trigger_datatrigger_filer   s         r   test_reads_and_deletes_filez+TestReadTrigger.test_reads_and_deletes_file  s    _")OD";;

<e LW^_\\"nc,.?@ 	'__&F	' %%%&&((((	' 	's   BBc                     t               }|dz  }|j                  dd       t        j                  |dt	        |            5  |j                         }ddd       J y# 1 sw Y   xY w)uB   트리거 파일의 JSON이 깨져 있으면 None을 반환한다.r  z{broken json}r   r   r  N)rG   rZ   r   r]   r^   r  )rT   r`   rF   r&  r   s        r   !test_returns_none_on_invalid_jsonz1TestReadTrigger.test_returns_none_on_invalid_json  si    _";;'B\\"nc,.?@ 	'__&F	' ~~	' 	's   A!!A*N)rl   rm   rn   ro   r  r'  r)  rp   r   r   r  r    s    ()	r   r  c                   .    e Zd ZdZd Zd Zd Zd Zd Zy)TestDumpAndLoadSessionu-   dump_session / load_session 함수 테스트.c           
      b   t               }|dz  }ddddgdg ddd	d
dd}t        j                  |dt        |            5  |j	                  |       ddd       |j                         sJ t        j                  |j                  d            }|d   dk(  sJ |d   du sJ y# 1 sw Y   TxY w)u@   dump_session은 세션 데이터를 JSON 파일로 저장한다.group_chat_session.jsonT   테스트 주제r   r   123   @TA         r   r   r$   r$  personaschat_idhistorylast_activity
auto_turnsspeak_countsSESSION_FILENr   r   r$  r$   )	rG   r   r]   r^   dump_sessionr0   r2   loads	read_text)rT   r`   rF   session_filesession_dataloadeds         r   test_dump_creates_filez-TestDumpAndLoadSession.test_dump_creates_file  s    _";;'!8,)'(A6	
 \\"nc,.?@ 	*OOL)	* ""$$$L22G2DEg"4444h4'''	* 	*s    B%%B.c           	      4   t               }|dz  }dddgdg ddddid}|j                  t        j                  |d	
      d       t	        j
                  |dt        |            5  |j                         }ddd       J |d   dk(  sJ y# 1 sw Y   xY w)uD   active=True인 세션 파일이 있으면 데이터를 반환한다.r-  Tu   복구 테스트r   456r0  r   r5  Fr   r   r   r<  Nr$  rG   rZ   r2   r   r   r]   r^   load_sessionrT   r`   rF   r@  rA  r   s         r   (test_load_session_returns_active_sessionz?TestDumpAndLoadSession.test_load_session_returns_active_session  s    _";;')#QK	
 	

<e LW^_\\"nc,.?@ 	'__&F	' !!!g"4444		' 	's   &BBc                    t               }|dz  }ddd}|j                  t        j                  |d      d       t	        j
                  |dt        |            5  |j                         }d	d	d	       J y	# 1 sw Y   xY w)
u6   active=False인 세션 파일은 None을 반환한다.r-  Fu   종료된 세션)r$   r$  r   r   r   r<  NrF  rH  s         r   ,test_load_session_returns_none_when_inactivezCTestDumpAndLoadSession.test_load_session_returns_none_when_inactive  s    _";;"'2DE

<e LW^_\\"nc,.?@ 	'__&F	' ~~	' 	's   A;;Bc                     t               }|dz  }t        j                  |dt        |            5  |j	                         }ddd       J y# 1 sw Y   xY w)u0   세션 파일이 없으면 None을 반환한다.no_session.jsonr<  N)rG   r   r]   r^   rG  r  s        r   0test_load_session_returns_none_when_file_missingzGTestDumpAndLoadSession.test_load_session_returns_none_when_file_missing  sU    _..\\"nc'l; 	'__&F	' ~~	' 	'r  c           
      R   t               }|dz  }ddddgddddd	gd
ddddd}t        j                  |dt        |            5  |j	                  |       |j                         }ddd       J |d   dk(  sJ |d   ddgk(  sJ t        |d         dk(  sJ y# 1 sw Y   7xY w)u/   dump_session → load_session 왕복 테스트.r-  Tu   왕복 테스트r   r   789r      안녕)speakerspeaker_namecontentg  tTAr2  r3  )r   r   r5  r<  Nr$  r6  r8  )rG   r   r]   r^   r=  rG  r6   rH  s         r   test_dump_and_load_roundtripz3TestDumpAndLoadSession.test_dump_and_load_roundtrip   s    _";;'($*HQYZ[)%&2	
 \\"nc,.?@ 	'OOL)__&F	' !!!g"4444j!ff%55556)$%***	' 	's   "BB&N)	rl   rm   rn   ro   rC  rI  rK  rN  rU  rp   r   r   r+  r+    s    7(.5,
+r   r+  c                      t               S )u+   group_chat 모듈 (픽스처에서 공유).)rG   rp   r   r   	gc_modulerW    s     ?r   c                 @    | j                  d| j                        }|S )u   격리된 GroupChatSession 인스턴스.

    DEFAULT_PERSONAS 구조 (team, persona_desc 필드 포함, emoji 필드 없음)를 사용한다.
    
fake-tokenru   r   )GroupChatSessionr   )rW  sesss     r   session_objr]  $  s,     %%00 & D Kr   c                   @    e Zd ZdZd Zd Zd Zd Zd Zd Z	d Z
d	 Zy
)TestGroupChatSessionStartu2   GroupChatSession.start 입장 시퀀스 테스트.c                 4   ddddgdd}t        j                  |d      5 }t        d      5  t        d	d
      5  |j                  |       ddd       ddd       ddd       j                  dk(  sJ y# 1 sw Y   +xY w# 1 sw Y   /xY w# 1 sw Y   3xY w)ui   start()는 입장 안내 1회 + 페르소나별 1회 + 시작 안내 1회 = N+2 회 send를 호출한다.r!  u   배포 전략r   r   999r#  r$  r6  r7  sendgroup_chat.dump_sessiongroup_chat.call_claude   테스트 인사입니다.r
   N   )r   r]   r!  
call_count)rT   r]  trigger	mock_sends       r   -test_start_calls_send_correct_number_of_timeszGTestGroupChatSessionStart.test_start_calls_send_correct_number_of_times9  s     $!8,	
 \\+v. 	/)01 /3B^_ /%%g.//	/ ##q(((	/ // /	/ 	/s9   BBA6BB6A?;BB	BBc                 0   dddgdd}t        j                  |d      5  t        d      5  t        dd	
      5  |j                  |       ddd       ddd       ddd       |j                  du sJ y# 1 sw Y   *xY w# 1 sw Y   .xY w# 1 sw Y   2xY w)u%   start() 후 active가 True가 된다.r!  r.  r   100rb  rc  rd  re  rf  r
   NT)r   r]   r!  r$   rT   r]  ri  s      r   test_start_sets_active_truez5TestGroupChatSessionStart.test_start_sets_active_trueI  s     '	
 \\+v. 	/01 /3B^_ /%%g.//	/
 !!T)))/ // /	/ 	/s9   BB A4B B4A=9B  B		BBc                 8   dg dd}t        j                  |d      5  t        d      5  t        dd      5  |j                  |       d	d	d	       d	d	d	       d	d	d	       |j                  d
d
d
dk(  sJ y	# 1 sw Y   /xY w# 1 sw Y   3xY w# 1 sw Y   7xY w)u4   start() 후 speak_counts가 0으로 초기화된다.u   발화 카운트 초기화r   r   r   r$  r6  rc  rd  re  rf  r
   Nr   )r   r]   r!  r;  rn  s      r   #test_start_initializes_speak_countsz=TestGroupChatSessionStart.test_start_initializes_speak_countsX  s     24
 \\+v. 	/01 /3B^_ /%%g.//	/
 ''a1a+PPPP/ // /	/ 	/s9   BBA8
BB8B=BB		BBc                    ddgdd}t        j                  |d      5  t        d      5  t        dd	      5  |j                  |       d
d
d
       d
d
d
       d
d
d
       t        |j                        dk(  sJ |j                  d   d   dk(  sJ |j                  d   d   dk(  sJ y
# 1 sw Y   bxY w# 1 sw Y   fxY w# 1 sw Y   jxY w)u7   user_message가 있으면 히스토리에 추가된다.u   프로젝트 킥오프r   u   안녕하세요 여러분!)r$  r6  user_messagerc  rd  re  rf  r
   Nr3  r   rT  rR  user)r   r]   r!  r6   r8  rn  s      r   ,test_start_with_user_message_adds_to_historyzFTestGroupChatSessionStart.test_start_with_user_message_adds_to_historye  s     .!
8

 \\+v. 	/01 /3B^_ /%%g.//	/
 ;&&'1,,,""1%i04PPPP""1%i0F:::/ // /	/ 	/s9   CB7B+
B7C+B40B77C 	<CCc                 .   ddgd}g t        j                  |dfd      5  t        d      5  t        dd	
      5  |j                  |       ddd       ddd       ddd       dd   v sJ y# 1 sw Y   #xY w# 1 sw Y   'xY w# 1 sw Y   +xY w)uH   start()의 첫 번째 send 메시지는 '잠깐요' 안내 메시지다.u   긴급 회의r   rr  rc  c                 &    j                  |       S Nappendtsent_messagess    r   <lambda>zLTestGroupChatSessionStart.test_start_first_message_content.<locals>.<lambda>}      ]EYEYZ[E\ r   side_effectrd  re  rf  r
   Nu	   잠깐요r   r   r]   r!  )rT   r]  ri  r  s      @r    test_start_first_message_contentz:TestGroupChatSessionStart.test_start_first_message_contentu  s     %
 \\+v;\] 	/01 /3B^_ /%%g.//	/
 mA..../ // /	/ 	/s9   BA?A3A?B3A<8A??B	BBc                 D   g d}d|d}g t        j                  |dfd      5  t        d      5  t        dd	
      5  |j                  |       ddd       ddd       ddd       d   }d|v sJ d|v sJ y# 1 sw Y   +xY w# 1 sw Y   /xY w# 1 sw Y   3xY w)uI   start()의 마지막 send 메시지는 참여 인원 수를 포함한다.rq  u
   팀 회의rr  rc  c                 &    j                  |       S rz  r{  r}  s    r   r  zYTestGroupChatSessionStart.test_start_last_message_contains_member_count.<locals>.<lambda>  r  r   r  rd  re  rf  r
   Nr   3u   명r  )rT   r]  r6  ri  last_msgr  s        @r   -test_start_last_message_contains_member_countzGTestGroupChatSessionStart.test_start_last_message_contains_member_count  s    /(h?\\+v;\] 	/01 /3B^_ /%%g.//	/
 !$h   / // /	/ 	/s:   BB
A>B
B>BB

B	BBc                 D   ddgd}g t        j                  |dfd      5  t        d      5  t        dt        d	            5  |j                  |       d
d
d
       d
d
d
       d
d
d
       d   }d|v sJ y
# 1 sw Y   %xY w# 1 sw Y   )xY w# 1 sw Y   -xY w)u_   CLI 오류 시 폴백 인사('잘 부탁드립니다.')가 포함된 메시지가 전송된다.u   폴백 테스트r   rr  rc  c                 &    j                  |       S rz  r{  r}  s    r   r  zWTestGroupChatSessionStart.test_start_api_error_uses_fallback_greeting.<locals>.<lambda>  r  r   r  rd  re  
   CLI 오류Nr3  u   잘 부탁드립니다.)r   r]   r|   r!  rT   r]  ri  persona_msgr  s       @r   +test_start_api_error_uses_fallback_greetingzETestGroupChatSessionStart.test_start_api_error_uses_fallback_greeting  s     (
 \\+v;\] 	/01 /3lA[\ /%%g.//	/ $A&(K777/ // /	/ 	/s:   BB
A>B
"B>BB

B	BBc                 >   ddgd}g t        j                  |dfd      5  t        d      5  t        dd	
      5  |j                  |       ddd       ddd       ddd       d   }d|v sJ d|v sJ y# 1 sw Y   +xY w# 1 sw Y   /xY w# 1 sw Y   3xY w)uM   페르소나 입장 메시지가 '이름(팀/역할)' 포맷을 포함한다.u   포맷 확인r   rr  rc  c                 &    j                  |       S rz  r{  r}  s    r   r  zZTestGroupChatSessionStart.test_start_persona_message_contains_new_format.<locals>.<lambda>  r  r   r  rd  re  rf  r
   Nr3  r   r   r  r  s       @r   .test_start_persona_message_contains_new_formatzHTestGroupChatSessionStart.test_start_persona_message_contains_new_format  s     %!

 \\+v;\] 	/01 /3B^_ /%%g.//	/ $A&,,,{***/ // /	/ 	/s9   BBA;BB;B BB	BBN)rl   rm   rn   ro   rk  ro  rs  rw  r  r  r  r  rp   r   r   r_  r_  6  s.    <) *Q; /!8"+r   r_  c                   <    e Zd ZdZd
dZd Zd Zd Zd Zd Z	d	 Z
y)TestGroupChatSessionEndu0   GroupChatSession.end 퇴장 시퀀스 테스트.Nc                 ~    |ddg}d|_         d|_        ||_        |D ci c]  }|d c}|_        g |_        yc c}w )u)   테스트용 세션 상태 준비 헬퍼.Nr   r   Tu   종료 테스트r3  )r$   r$  r6  r;  r8  )rT   r]  r6  ps       r   _preparez TestGroupChatSessionEnd._prepare  sN     (+H!.'2:#;QAqD#;   $<s   
:c           	         | j                  |       |dz  }t        j                  |d      5  t        dt        |            5  t        d      5  t        dd      5  |j	                  d       d	d	d	       d	d	d	       d	d	d	       d	d	d	       |j
                  d
u sJ y	# 1 sw Y   2xY w# 1 sw Y   6xY w# 1 sw Y   :xY w# 1 sw Y   >xY w)u$   end() 후 active가 False가 된다.r-  rc  group_chat.SESSION_FILEgroup_chat.save_session_logre     수고하셨습니다.r
   	user_exitNF)r  r   r]   r^   endr$   rT   r]  r`   r@  s       r   test_end_sets_active_falsez2TestGroupChatSessionEnd.test_end_sets_active_false  s    k"";;\\+v. 	50#l2CD 589 57F^_ 5#4555	5 !!U***5 55 55 5	5 	5sS   CB8B,B 	/B,7B8?C B)%B,,B51B88C	=CCc           	         | j                  |       |dz  }|j                  t        j                  ddi      d       t	        j
                  |d      5  t	        dt        |            5  t	        d      5  t	        d	d
      5  |j                  d       ddd       ddd       ddd       ddd       |j                         rJ y# 1 sw Y   4xY w# 1 sw Y   8xY w# 1 sw Y   <xY w# 1 sw Y   @xY w)u(   end() 후 세션 파일이 삭제된다.r-  r$   Tr   r   rc  r  r  re  r  r
   r  N)	r  rZ   r2   r   r   r]   r^   r  r0   r  s       r   test_end_removes_session_filez5TestGroupChatSessionEnd.test_end_removes_session_file  s    k"";;

Hd+; <wO\\+v. 	50#l2CD 589 57F^_ 5#4555	5  &&((((5 55 55 5	5 	5sT   C.+C"7CC
	CC"'C.
CCCC""C+	'C..C7c           	         g d}| j                  ||       |dz  }g t        j                  |dfd      5  t        dt        |            5  t        d      5  t        dd	
      5  |j	                  d       ddd       ddd       ddd       ddd       t              dk(  sJ y# 1 sw Y   2xY w# 1 sw Y   6xY w# 1 sw Y   :xY w# 1 sw Y   >xY w)u`   end()는 페르소나별 작별 인사 1회 + 종료 요약 1회 = N+1 회 send를 호출한다.rq  rM  rc  c                 &    j                  |       S rz  r{  r~  
sent_callss    r   r  zZTestGroupChatSessionEnd.test_end_sends_farewell_per_persona_plus_summary.<locals>.<lambda>      ZEVEVWXEY r   r  r  r  re  r  r
   r  Nrg  )r  r   r]   r^   r  r6   )rT   r]  r`   r6  r@  r  s        @r   0test_end_sends_farewell_per_persona_plus_summaryzHTestGroupChatSessionEnd.test_end_sends_farewell_per_persona_plus_summary  s    /k8,"33
\\+v;YZ 	50#l2CD 589 57F^_ 5#4555	5 :!###	5 55 55 5	5 	5sS   CCB9*B-	<B9CC-B62B99C>CC	
CCc           	         | j                  |dg       d|_        g |dz  }t        j                  |dfd      5  t        dt	        |            5  t        d      5  t        d	d
      5  |j                  d       ddd       ddd       ddd       ddd       dd   v sJ y# 1 sw Y   +xY w# 1 sw Y   /xY w# 1 sw Y   3xY w# 1 sw Y   7xY w)u:   end()의 마지막 메시지에는 주제가 포함된다.r   u   UI 개선 논의rM  rc  c                 &    j                  |       S rz  r{  r  s    r   r  zOTestGroupChatSessionEnd.test_end_final_message_contains_topic.<locals>.<lambda>  r  r   r  r  r  re  r  r
   r  Nr   )r  r$  r   r]   r^   r  )rT   r]  r`   r@  r  s       @r   %test_end_final_message_contains_topicz=TestGroupChatSessionEnd.test_end_final_message_contains_topic  s    kF8,.
"33\\+v;YZ 	50#l2CD 589 57F^_ 5#4555	5 "Z^3335 55 55 5	5 	5sS   CC B6.B*	 B6CC*B3/B66B?;CC	CCc           	         | j                  |dg       g |dz  }t        j                  |dfd      5  t        dt        |            5  t        d      5  t        dt	        d	            5  |j                  d
       ddd       ddd       ddd       ddd       |j                  du sJ d   }d|v sJ y# 1 sw Y   =xY w# 1 sw Y   AxY w# 1 sw Y   ExY w# 1 sw Y   IxY w)u_   CLI 오류 시 폴백 작별 인사('수고하셨습니다, 제이회장님.')가 전송된다.r   rM  rc  c                 &    j                  |       S rz  r{  r  s    r   r  zUTestGroupChatSessionEnd.test_end_api_failure_uses_fallback_farewell.<locals>.<lambda>	  r  r   r  r  r  re  r  r  NFr   u   수고하셨습니다)r  r   r]   r^   r|   r  r$   rT   r]  r`   r@  farewell_msgr  s        @r   +test_end_api_failure_uses_fallback_farewellzCTestGroupChatSessionEnd.test_end_api_failure_uses_fallback_farewell  s    kF8,
"33\\+v;YZ 	50#l2CD 589 57\R^E_` 5#4555	5 !!U***!!}&,6665 55 55 5	5 	5sS   C"CC
0B>	C

CC">CC

CCC	C""C+c           	         | j                  |dg       g |dz  }t        j                  |dfd      5  t        dt        |            5  t        d      5  t        dd	
      5  |j	                  d       ddd       ddd       ddd       ddd       d   }d|v sJ d|v sJ y# 1 sw Y   3xY w# 1 sw Y   7xY w# 1 sw Y   ;xY w# 1 sw Y   ?xY w)uP   end()의 작별 인사 메시지가 '이름(팀/역할)' 포맷을 포함한다.r   rM  rc  c                 &    j                  |       S rz  r{  r  s    r   r  zWTestGroupChatSessionEnd.test_end_farewell_message_contains_new_format.<locals>.<lambda>  r  r   r  r  r  re  r  r
   r  Nr   r   r   )r  r   r]   r^   r  r  s        @r   -test_end_farewell_message_contains_new_formatzETestGroupChatSessionEnd.test_end_farewell_message_contains_new_format  s    kH:.
"33\\+v;YZ 	50#l2CD 589 57F^_ 5#4555	5 "!}---|+++5 55 55 5	5 	5sS   CCB7'B+	9B7C	C+B40B77C <CC	CCrz  )rl   rm   rn   ro   r  r  r  r  r  r  r  rp   r   r   r  r    s(    :!+)$ 47 ,r   r  c                   .    e Zd ZdZd Zd Zd Zd Zd Zy)TestAddUserInputu*   GroupChatSession.add_user_input 테스트.c                 V    d|_         |j                  d       |j                   dk(  sJ y)u?   add_user_input() 호출 시 auto_turns가 0으로 리셋된다.   u   새 메시지r   Nr:  add_user_inputrT   r]  s     r   test_resets_auto_turnsz'TestAddUserInput.test_resets_auto_turns-  s,    !"""?3%%***r   c                     g |_         |j                  d       t        |j                         dk(  sJ |j                   d   d   dk(  sJ |j                   d   d   dk(  sJ |j                   d   d   dk(  sJ y	)
uF   add_user_input() 호출 시 히스토리에 메시지가 추가된다.u   테스트 입력r3  r   rT  rR  rv  rS  u   제이회장님Nr8  r  r6   r  s     r   test_appends_to_historyz(TestAddUserInput.test_appends_to_history3  s     ""#56;&&'1,,,""1%i04FFFF""1%i0F:::""1%n59JJJJr   c                     d|_         |j                  d       |j                   dkD  sJ |j                   t        j                         k  sJ y)u:   add_user_input() 호출 시 last_activity가 갱신된다.g        u   활동 시간 갱신N)r9  r  timer  s     r   test_updates_last_activityz+TestAddUserInput.test_updates_last_activity<  sG    $'!""#9:((3...((DIIK777r   c                     g |_         |j                  d       |j                  d       |j                  d       t        |j                         dk(  sJ |j                   d   d   dk(  sJ y)u5   여러 번 호출하면 히스토리가 누적된다.u
   첫 번째u
   두 번째u
   세 번째r1  r2  rT  Nr  r  s     r   *test_multiple_inputs_accumulate_in_historyz;TestAddUserInput.test_multiple_inputs_accumulate_in_historyC  sk     ""<0""<0""<0;&&'1,,,""1%i0L@@@r   c                 d    dD ]+  }||_         |j                  d       |j                   dk(  r+J  y)u4   auto_turns가 어떤 값이든 0으로 리셋된다.)r   r3     d   u   리셋 확인r   Nr  )rT   r]  initials      r   3test_auto_turns_resets_regardless_of_previous_valuezDTestAddUserInput.test_auto_turns_resets_regardless_of_previous_valueL  s;    % 	/G%,K"&&7))Q...	/r   N)	rl   rm   rn   ro   r  r  r  r  r  rp   r   r   r  r  *  s     4+K8A/r   r  c                   4    e Zd ZdZd Zd Zd Zd Zd Zd Z	y)	TestSelectNextSpeakeru%   select_next_speaker 함수 테스트.c                     t               }dddd}g d}t        dt        d            5  |j                  |g d	|
      }ddd       d	k7  sJ y# 1 sw Y   xY w)u=   CLI 오류 시 폴백에서 직전 발화자를 제외한다.r1  r3  r2  rq  )r   	   아테나   토르re  r  r  r   persona_namesr8  last_speakerr;  NrG   r   r|   select_next_speakerrT   rF   r;  r  r   s        r   &test_excludes_last_speaker_in_fallbackz<TestSelectNextSpeaker.test_excludes_last_speaker_in_fallback\  sp    _"#q!<?+l9ST 	+++%)	 , F	 !!!	 	   AAc                     t               }dddd}g d}t        dt        d            5  |j                  |g d	|
      }ddd       dk(  sJ y# 1 sw Y   xY w)uN   CLI 오류 폴백에서 발화 횟수가 가장 적은 화자를 선택한다.r  r   r1  )r   r   r   )r   r   r   re  r  r  r   r  Nr   r  r  s        r    test_fallback_picks_least_spokenz6TestSelectNextSpeaker.test_fallback_picks_least_spokenm  sr    _ #$Q:<+l9ST 	+++%)	 , F	 	 	r  c           
          t               }t        dd      5  |j                  g dg ddddd	      }d
d
d
       dk(  sJ y
# 1 sw Y   xY w)uA   CLI가 유효한 JSON을 반환하면 그 결과를 사용한다.re  u<   {"next_speaker": "odin", "reason": "자연스러운 순서"}r
   )r   r   r   r   r2  r3  )r   r   r   r  Nr   rG   r   r  r   s      r   !test_uses_api_response_when_validz7TestSelectNextSpeaker.test_uses_api_response_when_valid  sb    _+:xy 	++B%()1a@	 , F	 	 	   AAc           	          t               }t        dd      5  |j                  ddgg dddd	
      }ddd       dk(  sJ y# 1 sw Y   xY w)uN   CLI 응답의 persona_key가 speak_counts에 없으면 폴백을 사용한다.re  u0   {"next_speaker": "zeus", "reason": "없는 키"}r
   r   r  r   r1  r   r4  r  Nr   r  r   s      r   ,test_falls_back_when_api_returns_invalid_keyzBTestSelectNextSpeaker.test_falls_back_when_api_returns_invalid_key  se    _+:lm 	++-{;%()Q7	 , F	 !!!	 	s   AAc                     t               }t        dt        d            5  |j                  dgg dddi      }ddd       dk(  sJ y# 1 sw Y   xY w)	uT   후보가 한 명뿐이고 직전 발화자와 같으면 그 화자를 선택한다.re  r  r  r   r   r2  r  Nr  r   s      r   9test_fallback_allows_last_speaker_when_only_one_candidatezOTestSelectNextSpeaker.test_fallback_allows_last_speaker_when_only_one_candidate  sf    _+l9ST 	++-.%&]	 , F	 !!!	 	s   A

Ac           
          t               }t        dd      5  |j                  g dg ddddd	
      }ddd       dk(  sJ y# 1 sw Y   xY w)uQ   CLI 응답이 JSON 앞뒤에 텍스트를 포함해도 올바르게 파싱한다.re  ue   다음 발화자를 선택했습니다. {"next_speaker": "thor", "reason": "순서"} 감사합니다.r
   )r   r  r  r   r3  r   r2  )r   r   r   r  Nr   r  r   s      r   +test_api_returns_json_with_surrounding_textzATestSelectNextSpeaker.test_api_returns_json_with_surrounding_text  si    _$ A
 		 ++E%()1B	 , F			 		 		r  N)
rl   rm   rn   ro   r  r  r  r  r  r  rp   r   r   r  r  Y  s#    /"" $
 "" r   r  c                   4    e Zd ZdZd Zd Zd Zd Zd Zd Z	y)	TestMaxAutoTurnsu0   MAX_AUTO_TURNS >= 6 시 대기 동작 테스트.c                 :    t               }|j                  dk(  sJ y)u!   MAX_AUTO_TURNS 상수가 6이다.r  N)rG   MAX_AUTO_TURNSr  s     r   #test_max_auto_turns_constant_is_sixz4TestMaxAutoTurns.test_max_auto_turns_constant_is_six  s    _  A%%%r   c                    d|_         d|_        d|_        dg|_        ddi|_        d|_        t        j                         |_        ddifd}t        d|	      5  t        d
      5 }t        d      5  t        j                  |d      5  t        d      5  t        dd      5  |j                          ddd       ddd       ddd       ddd       ddd       ddd       j                          y# 1 sw Y   BxY w# 1 sw Y   FxY w# 1 sw Y   JxY w# 1 sw Y   NxY w# 1 sw Y   RxY w# 1 sw Y   VxY w)uR   auto_turns >= MAX_AUTO_TURNS 이면 select_next_speaker를 호출하지 않는다.Tr  u   자동 턴 테스트r   nr   c                  :     dxx   dz  cc<    d   dk(  ry dddS )Nr  r3  r  	test_exitr#  reasonrp   rh  s   r   fake_read_triggerz_TestMaxAutoTurns.test_run_loop_does_not_select_speaker_when_at_limit.<locals>.fake_read_trigger  s+    sOq O#!##{;;r   group_chat.read_triggerr  group_chat.select_next_speakerr  rc  
time.sleepr  z/tmp/no_session_auto_turns.jsonN)r$   r:  r$  r6  r;  r  r  r9  r   r]   run_looprS   rT   r]  r  mock_selectrh  s       @r   3test_run_loop_does_not_select_speaker_when_at_limitzDTestMaxAutoTurns.test_run_loop_does_not_select_speaker_when_at_limit  s1   !!"2 (z$,a= #+ $(IIK!1X
	< ,:KL 	778 7K89 7k6: 7"<0 7!&'@Bc!d 7 + 4 4 677777	7 	%%'	7 77 77 77 77 7	7 	7s   D4*D(6DD	D&C87D?D	DD(D48D=DD	D	DDD%!D((D1	-D44D=c                 |   d|_         d|_        dg|_        ddi|_        d|_        g |_        t        dd      5  t        j                  |d      5  t        d	      5  |j                  d       d
d
d
       d
d
d
       d
d
d
       |j                  dk(  sJ y
# 1 sw Y   +xY w# 1 sw Y   /xY w# 1 sw Y   3xY w)u2   speak() 호출마다 auto_turns가 1 증가한다.Tu   증가 테스트r   r   $group_chat.generate_persona_response   테스트 응답r
   rc  rd  Nr3  )	r$   r$  r6  r;  r:  r8  r   r]   speakr  s     r   #test_auto_turns_increments_on_speakz4TestMaxAutoTurns.test_auto_turns_increments_on_speak  s    !. (z$,a= !" 9HZ[ 	0k62 045 0%%h/00	0
 %%***0 00 0	0 	0s;   B2B&B0B&8B2B#B&&B/	+B22B;c                    d|_         d|_        dg|_        ddi|_        d|_        g |_        t        dd      5  t        j                  |d      5  t        d	      5  t        d
      D ]  }|j                  d        	 ddd       ddd       ddd       |j                  d
k(  sJ y# 1 sw Y   +xY w# 1 sw Y   /xY w# 1 sw Y   3xY w)uA   speak()를 여러 번 호출할수록 auto_turns가 누적된다.Tu   누적 테스트r   r   r  r  r
   rc  rd  rg  N)
r$   r$  r6  r;  r:  r8  r   r]   ranger  )rT   r]  _s      r   'test_auto_turns_increments_cumulativelyz8TestMaxAutoTurns.test_auto_turns_increments_cumulatively  s    !. (z$,a= !" 9HZ[ 	4k62 445 4"1X 4#))(3444	4 %%***	4 44 4	4 	4s;   CB7"B+B7	C+B40B77C 	<CCc                 V    d|_         |j                  d       |j                   dk(  sJ y)u>   add_user_input() 후에는 auto_turns가 0으로 리셋된다.r  u    유저가 새로 입력했어요r   Nr  r  s     r   -test_add_user_input_resets_auto_turns_to_zeroz>TestMaxAutoTurns.test_add_user_input_resets_auto_turns_to_zero  s-    !"""#EF%%***r   c                    d|_         d|_        d|_        dg|_        ddi|_        d|_        t        j                         |_        ddifd}t        d|	      5  t        d
d      5 }t        d      5  t        j                  |d      5  t        dd      5  t        d      5  t        d      5  t        dd      5  |j                          ddd       ddd       ddd       ddd       ddd       ddd       ddd       ddd       j                          y# 1 sw Y   RxY w# 1 sw Y   VxY w# 1 sw Y   ZxY w# 1 sw Y   ^xY w# 1 sw Y   bxY w# 1 sw Y   fxY w# 1 sw Y   jxY w# 1 sw Y   nxY w)uZ   유저 입력으로 auto_turns가 리셋되면 루프가 다시 발화자를 선택한다.Tr  u   재개 테스트r   r  r   c                  T     dxx   dz  cc<    d   dk(  rdddS  d   dk(  rddd	S y )
Nr  r3  
user_inputu   계속해주세요r#  messager2  r  doner  rp   r  s   r   r  zdTestMaxAutoTurns.test_run_loop_resumes_after_user_input_resets_auto_turns.<locals>.fake_read_trigger  sC    sOq O#!#".;OPP#!#"'6::r   r  r  r  r
   r  rc  r  u   응답rd  r  r  z/tmp/no_session_resume.jsonN)r$   r:  r$  r6  r;  r  r  r9  r   r]   r  assert_calledr  s       @r   8test_run_loop_resumes_after_user_input_resets_auto_turnszITestMaxAutoTurns.test_run_loop_resumes_after_user_input_resets_auto_turns  ss   !!". (z$,a= #+ $(IIK!1X
	 ,:KL 	?7hO ?S^89 ?k6: ?"#IX`a ?!&'@!A ?%*<%8 !?)./HJg)h %?(3(<(<(>%?!??????	? 	!!#	%? %?!? !?? ?? ?? ?? ?? ?	? 	?s   E8,E,8E E	E)D<5D0D$D0D<#E+E	3E ;E,E8$D-)D00D95D<<EEEE	EE  E)%E,,E5	1E88FN)
rl   rm   rn   ro   r  r  r  r  r   r  rp   r   r   r  r    s#    :&
(>+ +"+!$r   r  c                   F    e Zd ZdZd Zd Zd Zd Zd Zd Z	d Z
d	 Zd
 Zy)TestTelegramPolleru#   TelegramPoller 클래스 테스트.c                     t               }|j                  dd      }|j                  dk(  sJ |j                  dk(  sJ |j                  dk(  sJ |j
                  dk(  sJ y)uU   __init__이 token, chat_id, base_url, last_update_id를 올바르게 초기화한다.
test-token12345ru   r7  z&https://api.telegram.org/bottest-tokenr   N)rG   TelegramPollerru   r7  base_urllast_update_id)rT   rF   pollers      r   test_init_sets_attributesz,TestTelegramPoller.test_init_sets_attributes7  si    _""w"G|||+++~~((("JJJJ$$)))r   c                     t               }|j                  dd      }t               }ddddidd	d
gd|j                  _        t        d|      5  |j                         }ddd       dgk(  sJ y# 1 sw Y   xY w)u6   정상 응답 시 메시지 리스트를 반환한다.tokra  r  Te   r(     u   안녕하세요chattext	update_idr  r	   r   group_chat.requests.getr
   NrG   r  r   r2   r   r   get_updatesrT   rF   r  	fake_respmessagess        r   ,test_get_updates_returns_messages_on_successz?TestTelegramPoller.test_get_updates_returns_messages_on_success@  s    _""">K	 "%!%s 1 '
	# ,9E 	,))+H	, -....	, 	,s   A44A=c                     t               }|j                  dd      }t               }ddddidd	d
gd|j                  _        t        d|      5  |j                         }ddd       g k(  sJ y# 1 sw Y   xY w)uS   chat_id가 불일치하는 메시지는 제외하고 빈 리스트를 반환한다.r  ra  r  Tf   r(   ix  u   다른 채팅방r  r  r  r  r
   Nr  r!  s        r   #test_get_updates_filters_by_chat_idz6TestTelegramPoller.test_get_updates_filters_by_chat_idX  s    _""">K	 "%!%s 2 '
	# ,9E 	,))+H	, 2~~	, 	,s   A33A<c                     t               }|j                  dd      }t               }ddi|j                  _        t        d|      5  |j                         }ddd       g k(  sJ y# 1 sw Y   xY w)	u>   API 응답의 ok가 False이면 빈 리스트를 반환한다.r  ra  r  r	   Fr  r
   Nr  r!  s        r   +test_get_updates_returns_empty_on_api_errorz>TestTelegramPoller.test_get_updates_returns_empty_on_api_errorp  sq    _""">K	'+Um	#,9E 	,))+H	, 2~~	, 	,s   A))A2c                     t               }|j                  dd      }t        dt        d            5  |j	                         }ddd       g k(  sJ y# 1 sw Y   xY w)uH   requests.get이 예외를 발생시키면 빈 리스트를 반환한다.r  ra  r  r  zNetwork errorr  N)rG   r  r   r   r   )rT   rF   r  r#  s       r   +test_get_updates_returns_empty_on_exceptionz>TestTelegramPoller.test_get_updates_returns_empty_on_exception}  s_    _""">,)O:TU 	,))+H	, 2~~	, 	,s   AAc                 F   t               }|j                  dd      }|j                  dk(  sJ t               }ddddid	d
ddddidd
dgd|j                  _        t        d|      5  |j                          ddd       |j                  dk(  sJ y# 1 sw Y   xY w)uL   get_updates() 호출 후 last_update_id가 최신 update_id로 갱신된다.r  ra  r  r   T   r(   r  u   첫 메시지r  r     u   두 번째 메시지r  r  r
   N)rG   r  r  r   r2   r   r   r   )rT   rF   r  r"  s       r   'test_get_updates_updates_last_update_idz:TestTelegramPoller.test_get_updates_updates_last_update_id  s    _""">$$)))K	 "%)-s_M
 "%)-s=ST	'
	# ,9E 	! 	! $$+++	! 	!s   ,BB c                    t               }|j                  dd      }t               }dg d|j                  _        t        d|      5  |j                         }ddd       g k(  sJ |j                  d	k(  sJ y# 1 sw Y   "xY w)
ub   result가 빈 리스트이면 빈 리스트를 반환하고 last_update_id는 변하지 않는다.r  ra  r  Tr  r  r
   Nr   rG   r  r   r2   r   r   r   r  r!  s        r   ,test_get_updates_returns_empty_on_no_resultsz?TestTelegramPoller.test_get_updates_returns_empty_on_no_results  s    _""">K	-1R&@	#,9E 	,))+H	, 2~~$$)))		, 	,s   	A;;Bc                    t               }|j                  dd      }t               }dddddiid	gd
|j                  _        t        d|      5  |j                         }ddd       g k(  sJ |j                  dk(  sJ y# 1 sw Y   "xY w)uG   text 필드가 없는 메시지(사진, 스티커 등)는 스킵한다.r  ra  r  Ti-  r  r(   r  r  r  r  r
   Nr1  r!  s        r   ,test_get_updates_skips_messages_without_textz?TestTelegramPoller.test_get_updates_skips_messages_without_text  s    _""">K	 "%s '
	# ,9E 	,))+H	, 2~~$$+++	, 	,s   BBc                    t               }|j                  dd      }d|_        t               }dg d|j                  _        t        d|      5 }|j                          d	d	d	       j                  }t        |d
         dkD  r|d   j                  d      xs |d
   d   n|d   d   }|d   dk(  sJ y	# 1 sw Y   WxY w)uA   get_updates()가 last_update_id + 1을 offset으로 전달한다.r  ra  r  r  Tr  r  r
   Nr   r3  paramsoffsetr  )rG   r  r  r   r2   r   r   r   	call_argsr6   r   )rT   rF   r  r"  mock_getcall_kwargsr6  s          r   &test_get_updates_passes_correct_offsetz9TestTelegramPoller.test_get_updates_passes_correct_offset  s    _"""> #K	-1R&@	#,9E 	! 	! ((AD[QR^ATWXAXKNx(=KN1,=^ijk^lmu^v 	 h3&&&	! 	!s   B77C N)rl   rm   rn   ro   r  r$  r'  r)  r+  r/  r2  r4  r;  rp   r   r   r
  r
  4  s2    -*/00,4*,4'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d
 Zd Zd Zd Zd Zy)TestDetectIntentu   detect_intent 함수 테스트.c                 R    t               }|j                  dd      }|d   dk(  sJ y)uG   '팀 모여' 키워드 → start_chat 인텐트 반환 (세션 없음).
   팀 모여Fsession_activeintent
start_chatNrG   detect_intentr   s      r   test_start_keyword_team_gatherz/TestDetectIntent.test_start_keyword_team_gather  s1    _!!,u!Eh<///r   c                 R    t               }|j                  dd      }|d   dk(  sJ y)uI   '회의하자' 키워드 → start_chat 인텐트 반환 (세션 없음).   회의하자Fr@  rB  rC  NrD  r   s      r   test_start_keyword_have_meetingz0TestDetectIntent.test_start_keyword_have_meeting  s1    _!!.!Gh<///r   c                     t               }|j                  dd      }d|v sJ d|v sJ t        |d   t              sJ y)u;   start_chat 인텐트에 topic과 personas 필드가 있다.r?  Fr@  r$  r6  N)rG   rE  r   rC   r   s      r   -test_start_chat_result_has_topic_and_personasz>TestDetectIntent.test_start_chat_result_has_topic_and_personas  sO    _!!,u!E&   V###&,d333r   c                     t               }d}t        d|      5 }|j                  dd      }ddd       j                          d   d	k(  sJ y# 1 sw Y   $xY w)
uL   일반 메시지 '안녕' → Claude 호출 후 none 반환 (세션 없음).{"intent": "none"}re  r
   rQ  Fr@  NrB  nonerG   r   rE  assert_called_oncerT   rF   fake_responsemock_clauder   s        r   0test_normal_message_calls_claude_when_no_sessionzATestDetectIntent.test_normal_message_calls_claude_when_no_session  sh    _,+-H 	FK%%hu%EF	F&&(h6)))	F 	F   AAc                 R    t               }|j                  dd      }|d   dk(  sJ y)uA   '잘래' 키워드 → end_chat 인텐트 반환 (세션 활성).   잘래Tr@  rB  end_chatNrD  r   s      r   test_end_keyword_sleepz'TestDetectIntent.test_end_keyword_sleep  1    _!!(4!@h:---r   c                 R    t               }|j                  dd      }|d   dk(  sJ y)uA   '빠이' 키워드 → end_chat 인텐트 반환 (세션 활성).   빠이Tr@  rB  rX  NrD  r   s      r   test_end_keyword_byez%TestDetectIntent.test_end_keyword_bye  rZ  r   c                     t               }d}t        d|      5 }|j                  dd      }ddd       j                          d   d	k(  sJ y# 1 sw Y   $xY w)
uF   일반 질문 → Claude 호출 → user_input 반환 (세션 활성).u<   {"intent": "user_input", "message": "오늘 일정 어때?"}re  r
   u   오늘 일정 어때?Tr@  NrB  r  rO  rQ  s        r   5test_normal_question_calls_claude_when_session_activezFTestDetectIntent.test_normal_question_calls_claude_when_session_active  si    _V+-H 	TK%%&=d%SF	T&&(h<///	T 	TrU  c                     t               }t        dt        d            5  |j                  dd      }ddd       d   d	k(  sJ |d
   dk(  sJ y# 1 sw Y   xY w)uC   Claude 호출 실패 시 session_active=True → user_input 폴백.re     Claude 실패r  u   어떻게 생각해?Tr@  NrB  r  r  rG   r   r   rE  r   s      r   (test_claude_fallback_when_session_activez9TestDetectIntent.test_claude_fallback_when_session_active  so    _+?9ST 	S%%&<T%RF	Sh<///i $::::	S 	Ss   AAc                     t               }t        dt        d            5  |j                  dd      }ddd       d   d	k(  sJ y# 1 sw Y   xY w)
u>   Claude 호출 실패 시 session_active=False → none 폴백.re  ra  r  rQ  Fr@  NrB  rN  rb  r   s      r   *test_claude_fallback_when_session_inactivez;TestDetectIntent.test_claude_fallback_when_session_inactive"  sZ    _+?9ST 	F%%hu%EF	Fh6)))	F 	Fs   AAc                     t               }t        dd      5  |j                  dd      }ddd       d   d	k(  sJ y# 1 sw Y   xY w)
uX   Claude가 유효하지 않은 JSON 반환 시 session_active=True → user_input 폴백.re     유효하지 않은 응답r
      어떤 질문Tr@  NrB  r  rG   r   rE  r   s      r   0test_claude_returns_invalid_json_fallback_activezATestDetectIntent.test_claude_returns_invalid_json_fallback_active)  sV    _+:VW 	L%%od%KF	Lh<///	L 	L	   ?Ac                     t               }t        dd      5  |j                  dd      }ddd       d   d	k(  sJ y# 1 sw Y   xY w)
uS   Claude가 유효하지 않은 JSON 반환 시 session_active=False → none 폴백.re  rg  r
   rh  Fr@  NrB  rN  ri  r   s      r   2test_claude_returns_invalid_json_fallback_inactivezCTestDetectIntent.test_claude_returns_invalid_json_fallback_inactive0  sV    _+:VW 	M%%oe%LF	Mh6)))	M 	Mrk  c                     t               }t        dd      5  |j                  dd      }ddd       d   d	k7  sJ y# 1 sw Y   xY w)
uO   세션 활성 중에는 시작 키워드가 end_chat을 유발하지 않는다.re  u1   {"intent": "user_input", "message": "팀 모여"}r
   r?  Tr@  NrB  rX  ri  r   s      r   3test_start_keywords_not_matched_when_session_activezDTestDetectIntent.test_start_keywords_not_matched_when_session_active7  sX    _+:mn 	I%%l4%HF	I h:---	I 	Irk  c                     t               }t        dd      5  |j                  dd      }ddd       d   d	k7  sJ y# 1 sw Y   xY w)
uY   세션 비활성 중에는 종료 키워드가 즉각 end_chat을 반환하지 않는다.re  rM  r
   rW  Fr@  NrB  rX  ri  r   s      r   3test_end_keywords_not_matched_when_session_inactivezDTestDetectIntent.test_end_keywords_not_matched_when_session_inactive?  sX    _+:NO 	F%%hu%EF	F h:---	F 	Frk  N)rl   rm   rn   ro   rF  rI  rK  rT  rY  r]  r_  rc  re  rj  rm  ro  rq  rp   r   r   r=  r=    sF    )004*..0;*0*..r   r=  c                   .    e Zd ZdZd Zd Zd Zd Zd Zy)TestIsActiveu1   GroupChatSession.is_active() 메서드 테스트.c                 ,    |j                         du sJ y)u9   초기 상태에서 is_active()는 False를 반환한다.FN)	is_activer  s     r   'test_is_active_returns_false_by_defaultz4TestIsActive.test_is_active_returns_false_by_defaultQ  s    $$&%///r   c                 :    d|_         |j                         du sJ y)uB   active=False로 설정하면 is_active()가 False를 반환한다.FNr$   ru  r  s     r   .test_is_active_returns_false_when_active_falsez;TestIsActive.test_is_active_returns_false_when_active_falseU  s!    "$$&%///r   c                 4   ddgd}t        j                  |d      5  t        d      5  t        dd      5  |j                  |       d	d	d	       d	d	d	       d	d	d	       |j                         d
u sJ y	# 1 sw Y   .xY w# 1 sw Y   2xY w# 1 sw Y   6xY w)u7   start() 호출 후 is_active()가 True를 반환한다.	   테스트r   rr  rc  rd  re     인사r
   NT)r   r]   r!  ru  rn  s      r   'test_is_active_returns_true_after_startz4TestIsActive.test_is_active_returns_true_after_startZ  s    'hZ@\\+v. 	/01 /3(K /%%g.//	/ $$&$.../ // /	/ 	/s9   BBA6	BB6A?;BB	BBc           	         ddgd}|dz  }t        j                  |d      5  t        d      5  t        dd	      5  |j                  |       d
d
d
       d
d
d
       d
d
d
       |j                         du sJ t        j                  |d      5  t        dt	        |            5  t        d      5  t        dd	      5  |j                  d       d
d
d
       d
d
d
       d
d
d
       d
d
d
       |j                         du sJ y
# 1 sw Y   xY w# 1 sw Y   xY w# 1 sw Y   xY w# 1 sw Y   ZxY w# 1 sw Y   ^xY w# 1 sw Y   bxY w# 1 sw Y   fxY w)uK   start() 후 end() 호출하면 is_active()가 다시 False를 반환한다.r{  r   rr  session.jsonrc  rd  re  r|  r
   NTr  r     작별r  F)r   r]   r!  ru  r^   r  )rT   r]  r`   ri  r@  s        r   &test_is_active_returns_false_after_endz3TestIsActive.test_is_active_returns_false_after_endc  sK   'hZ@.0\\+v. 	/01 /3(K /%%g.//	/
 $$&$...\\+v. 	50#l2CD 589 57hO 5#4555	5 $$&%//// // /	/ 	/5 55 55 5	5 	5s   DDDDDE&E2D7 D+	D7E"EDDD	DD(+D40D77E <EE	EEc                 p    d|_         |j                         du sJ d|_         |j                         du sJ y)uF   is_active()가 self.active 속성과 일치하는 값을 반환한다.TFNrx  r  s     r   (test_is_active_reflects_active_attributez5TestIsActive.test_is_active_reflects_active_attributew  s@    !$$&$..."$$&%///r   N)	rl   rm   rn   ro   rv  ry  r}  r  r  rp   r   r   rs  rs  N  s    ;00
/0(0r   rs  c                   :    e Zd ZdZd Zd Zd Zd Zd Zd Z	d Z
y	)
TestRunOneTurnu4   GroupChatSession.run_one_turn() 메서드 테스트.c                     d|_         d|_        ddg|_        ddd|_        d|_        t        j
                         |_        d|_        g |_        y)	u6   테스트용 활성 세션 상태를 만드는 헬퍼.Tu   런원턴 테스트r   r   r   r4  r   N)	r$   r$  r6  r;  r  r  r9  r:  r8  r  s     r   _make_active_sessionz#TestRunOneTurn._make_active_session  sR    !1 ((3./1#= #% $(IIK!!" r   c                    d|_         t        d      5 }t        j                  |d      5 }|j                          ddd       ddd       j	                          j	                          y# 1 sw Y   2xY w# 1 sw Y   6xY w)u5   active=False이면 아무 동작도 하지 않는다.Fr  r  N)r$   r   r]   run_one_turnrS   rT   r]  r  
mock_speaks       r   test_does_nothing_when_inactivez.TestRunOneTurn.test_does_nothing_when_inactive  su    "34 	+k73 +z((*+	+ 	%%'$$&	+ +	+ 	+s!   A8A,A8,A5	1A88Bc           	         d|_         d|_        dg|_        ddi|_        d|_        t        j
                         dz
  |_        d|_        g |_        |dz  }t        j                  |d      5  t        d	t        |            5  t        d
      5  t        dd      5  |j                          ddd       ddd       ddd       ddd       |j                   du sJ y# 1 sw Y   2xY w# 1 sw Y   6xY w# 1 sw Y   :xY w# 1 sw Y   >xY w)uB   last_activity가 SESSION_TIMEOUT 초과 시 end()를 호출한다.Tu   타임아웃 테스트r   r   r   i  r  rc  r  r  re  r  r
   NF)r$   r$  r6  r;  r  r  r9  r:  r8  r   r]   r^   r  r  s       r   test_calls_end_on_timeoutz(TestRunOneTurn.test_calls_end_on_timeout  s   !4 (z$,a= #% $(IIK#$5!!" .0\\+v. 	30#l2CD 389 37hO 3#002333	3 !!U***3 33 33 3	3 	3sT   ,DC6C*C	-C*5C6=DC'#C**C3/C66C?	;DDc                 *   | j                  |       d|_        t        d      5 }t        j                  |d      5 }|j	                          ddd       ddd       j                          j                          y# 1 sw Y   2xY w# 1 sw Y   6xY w)uE   auto_turns >= MAX_AUTO_TURNS이면 speak()를 호출하지 않는다.r  r  r  N)r  r:  r   r]   r  rS   r  s       r   /test_does_not_speak_when_max_auto_turns_reachedz>TestRunOneTurn.test_does_not_speak_when_max_auto_turns_reached  s    !!+.!"34 	+k73 +z((*+	+ 	%%'$$&	+ +	+ 	+s"   B	A=B	=B	B		Bc                    | j                  |       t        dd      5  t        j                  |dd      5 }|j                          ddd       ddd       j	                  d       y# 1 sw Y   #xY w# 1 sw Y   'xY w)u>   정상 상태에서 run_one_turn()은 speak()를 호출한다.r  r   r
   r  TN)r  r   r]   r  assert_called_once_withrT   r]  r  s      r   test_calls_speak_on_normal_turnz.TestRunOneTurn.test_calls_speak_on_normal_turn  ss    !!+.3(K 	+k7F +*((*+	+ 	**84+ +	+ 	+s"   A7A+	A7+A4	0A77B c                    | j                  |       t        dt        d            5  t        j                  |dd      5 }|j	                          ddd       ddd       j                  d       y# 1 sw Y   #xY w# 1 sw Y   'xY w)	u]   select_next_speaker 오류 시 personas[0]를 폴백으로 사용해 speak()를 호출한다.r  u   선택 오류r  r  Tr
   Nr   )r  r   r   r]   r  r  r  s      r   %test_fallback_speaker_on_select_errorz4TestRunOneTurn.test_fallback_speaker_on_select_error  sx    !!+.3?A[\ 	+k7F +*((*+	+
 	**84	+ +	+ 	+s#   B A4B 4A=	9B  B	c                 N   | j                  |       |dz  }t        dd      5  t        j                  |dt        d            5  t        j                  |d      5  t        d	t	        |            5  t        d
      5  t        dd      5  |j                          ddd       ddd       ddd       ddd       ddd       ddd       |j                  du sJ y# 1 sw Y   BxY w# 1 sw Y   FxY w# 1 sw Y   JxY w# 1 sw Y   NxY w# 1 sw Y   RxY w# 1 sw Y   VxY w)u5   speak() 중 예외 발생 시 세션을 종료한다.r  r  r   r
   r  u   발화 오류r  rc  r  r  re  r  NF)r  r   r]   r   r^   r  r$   r  s       r   $test_ends_session_on_speak_exceptionz3TestRunOneTurn.test_ends_session_on_speak_exception  s   !!+..03(K 	;k7	/@Z[ ;\\+v6 ;8#l:KL ;"#@A ;!&'?h!W ; + 8 8 :;;;;;	; !!U***; ;; ;; ;; ;; ;	; 	;s   "DDD3C7	?C+CC+&C7	.D6D>DC($C++C40C7	7D <DDDD	DD$N)rl   rm   rn   ro   r  r  r  r  r  r  r  rp   r   r   r  r    s(    >	!	'+*
'5	5+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
)TestCheckTriggerFileu$   check_trigger_file 함수 테스트.c                 0   t               }t               }t        dd      5  |j                  |d       ddd       |j                  j                          |j                  j                          |j                  j                          y# 1 sw Y   XxY w)u@   트리거 파일이 없으면 아무 동작도 하지 않는다.r  Nr
   rY  )rG   r   r   check_trigger_filer!  rS   r  r  )rT   rF   sessions      r   !test_does_nothing_when_no_triggerz6TestCheckTriggerFile.test_does_nothing_when_no_trigger  st    _+,4@ 	9!!'<8	9 	'')002%%'	9 	9s   BBc                     t               }t               }d|j                  _        dddgd}t	        d|      5  |j                  |d       d	d	d	       |j                  j                  |       y	# 1 sw Y   %xY w)
u2   action=start 트리거 → session.start() 호출.Fr!  r"  r   r#  r$  r6  r  r
   rY  N)rG   r   ru  r   r   r  r!  r  rT   rF   r  ri  s       r   %test_action_start_calls_session_startz:TestCheckTriggerFile.test_action_start_calls_session_start  sp    _+).&$XJW,7C 	9!!'<8	9 	--g6	9 	9s   A11A:c                     t               }t               }d|j                  _        ddd}t	        d|      5  |j                  |d       ddd       |j                  j                  d       y# 1 sw Y   %xY w)	uF   action=user_input + 활성 세션 → session.add_user_input() 호출.Tr  u   좋은 의견이에요r  r  r
   rY  N)rG   r   ru  r   r   r  r  r  r  s       r   *test_action_user_input_with_active_sessionz?TestCheckTriggerFile.test_action_user_input_with_active_session  sp    _+)-&)6NO,7C 	9!!'<8	9 	667OP	9 	9   A//A8c                     t               }t               }d|j                  _        ddd}t	        d|      5  |j                  |d       ddd       |j                  j                  d       y# 1 sw Y   %xY w)	u4   action=end + 활성 세션 → session.end() 호출.Tr  r  r  r  r
   rY  N)rG   r   ru  r   r   r  r  r  r  s       r   #test_action_end_with_active_sessionz8TestCheckTriggerFile.test_action_end_with_active_session  sl    _+)-&"k:,7C 	9!!'<8	9 	++K8	9 	9r  c                     t               }t               }d|j                  _        ddd}t	        d|      5  |j                  |d       ddd       |j                  j                          y# 1 sw Y   $xY w)	uD   action=user_input + 비활성 세션 → add_user_input() 미호출.Fr  u   무시될 메시지r  r  r
   rY  N)rG   r   ru  r   r   r  r  rS   r  s       r   7test_action_user_input_with_inactive_session_is_ignoredzLTestCheckTriggerFile.test_action_user_input_with_inactive_session_is_ignored  sm    _+).&)6KL,7C 	9!!'<8	9 	002	9 	9   A..A7c                     t               }t               }d|j                  _        ddd}t	        d|      5  |j                  |d       ddd       |j                  j                          y# 1 sw Y   $xY w)	u2   action=end + 비활성 세션 → end() 미호출.Fr  r  r  r  r
   rY  N)rG   r   ru  r   r   r  r  rS   r  s       r   0test_action_end_with_inactive_session_is_ignoredzETestCheckTriggerFile.test_action_end_with_inactive_session_is_ignored+  sj    _+).&"k:,7C 	9!!'<8	9 	%%'	9 	9r  c                 0   t               }t               }d|j                  _        dddgd}t	        d|      5  |j                  |d       d	d	d	       |j                  j                  d
       |j                  j                  |       y	# 1 sw Y   @xY w)uO   action=start + 이미 활성 세션 → 기존 세션 end() 후 start() 호출.Tr!  u
   새 회의r   r  r  r
   rY  Nrestart)	rG   r   ru  r   r   r  r  r  r!  r  s       r   -test_action_start_ends_existing_session_firstzBTestCheckTriggerFile.test_action_start_ends_existing_session_first8  s    _+)-&$|(T,7C 	9!!'<8	9 	++I6--g6		9 	9s   BBc                 V   t               }t               }d|j                  _        t	        d      |j
                  _        dddd}t        d|      5  t        d	      5 }|j                  |d
       ddd       ddd       j                          y# 1 sw Y   "xY w# 1 sw Y   &xY w)u;   session.start() 오류 시 Telegram 알림을 전송한다.Fu   시작 오류r!  u   오류 테스트ra  )r#  r$  r7  r  r
   group_chat.send_telegramrY  N)
rG   r   ru  r   r   r!  r  r   r  rP  )rT   rF   r  ri  mock_send_tgs        r   3test_action_start_error_sends_telegram_notificationzHTestCheckTriggerFile.test_action_start_error_sends_telegram_notificationF  s    _+).&$-o$>!$/AeT,7C 	=12 =l%%g|<=	= 	'')= =	= 	=s$   BB2BB	BB(N)rl   rm   rn   ro   r  r  r  r  r  r  r  r  rp   r   r   r  r    s.    .
(7Q93(7*r   r  c                   R    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d Zy)TestNewConstantsu)   새로 추가된 상수 검증 테스트.c                 4    t               }t        |d      sJ y)u&   START_KEYWORDS 상수가 존재한다.START_KEYWORDSNrG   r   r  s     r   test_start_keywords_existsz+TestNewConstants.test_start_keywords_exists^  s    _r+,,,r   c                 P    t               }t        |j                  t              sJ y)u)   START_KEYWORDS가 리스트 타입이다.N)rG   r   r  rC   r  s     r   test_start_keywords_is_listz,TestNewConstants.test_start_keywords_is_listc  s    _"++T222r   c                 X    t               }d|j                  v sJ d|j                  v sJ y)u?   START_KEYWORDS에 '팀 모여', '회의하자'가 포함된다.r?  rH  N)rG   r  r  s     r   +test_start_keywords_contains_expected_itemsz<TestNewConstants.test_start_keywords_contains_expected_itemsh  s1    _r00000!2!2222r   c                 4    t               }t        |d      sJ y)u$   END_KEYWORDS 상수가 존재한다.END_KEYWORDSNr  r  s     r   test_end_keywords_existsz)TestNewConstants.test_end_keywords_existsn  s    _r>***r   c                 P    t               }t        |j                  t              sJ y)u'   END_KEYWORDS가 리스트 타입이다.N)rG   r   r  rC   r  s     r   test_end_keywords_is_listz*TestNewConstants.test_end_keywords_is_lists  s    _"//4000r   c                 X    t               }d|j                  v sJ d|j                  v sJ y)u3   END_KEYWORDS에 '잘래', '빠이'가 포함된다.rW  r\  N)rG   r  r  s     r   )test_end_keywords_contains_expected_itemsz:TestNewConstants.test_end_keywords_contains_expected_itemsx  s-    _2??***2??***r   c                 4    t               }t        |d      sJ y)u'   ALL_PERSONA_IDS 상수가 존재한다.ALL_PERSONA_IDSNr  r  s     r   test_all_persona_ids_existsz,TestNewConstants.test_all_persona_ids_exists~  s    _r,---r   c                 P    t               }t        |j                  t              sJ y)u*   ALL_PERSONA_IDS가 리스트 타입이다.N)rG   r   r  rC   r  s     r   test_all_persona_ids_is_listz-TestNewConstants.test_all_persona_ids_is_list  s    _",,d333r   c                 L    t               }t        |j                        dk(  sJ y)u-   ALL_PERSONA_IDS에 정확히 19명이 있다.   N)rG   r6   r  r  s     r   #test_all_persona_ids_has_19_membersz4TestNewConstants.test_all_persona_ids_has_19_members  s"    _2%%&",,,r   c                 T    t               }h d}|t        |j                        k(  sJ y)u7   ALL_PERSONA_IDS에 주요 페르소나가 포함된다.r  N)rG   r4   r  )rT   rF   expecteds      r   .test_all_persona_ids_contains_expected_membersz?TestNewConstants.test_all_persona_ids_contains_expected_members  s+    _
* 3r112222r   c                     t               }t        |j                        t        t        |j                              k(  sJ y)u$   ALL_PERSONA_IDS에 중복이 없다.N)rG   r6   r  r4   r  s     r   "test_all_persona_ids_no_duplicatesz3TestNewConstants.test_all_persona_ids_no_duplicates  s2    _2%%&#c"2D2D.E*FFFFr   N)rl   rm   rn   ro   r  r  r  r  r  r  r  r  r  r  r  rp   r   r   r  r  [  s=    3-
3
3+
1
+.
4
-
34Gr   r  c                   X    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d Zd Zy)TestDetectIntentControluG   세션 활성 중 제어 명령 감지 테스트 (session_active=True).c                     t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ |d   d	k(  sJ |d
   dk(  sJ y# 1 sw Y   (xY w)u9   '10명만 얘기해' → control/limit_personas/count=10.re  u   10명만 얘기해Tr@  NrB  controltypelimit_personascount
   ri  r   s      r      test_limit_personas_10명만u4   TestDetectIntentControl.test_limit_personas_10명만  sy    _+, 	Q%%&:4%PF	Qh9,,,f~!1111g"$$$		Q 	Q   AAc                     t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ |d   d	k(  sJ |d
   dk(  sJ y# 1 sw Y   (xY w)u3   '5명까지만' → control/limit_personas/count=5.re  u   5명까지만Tr@  NrB  r  r  r  r  r  ri  r   s      r   !   test_limit_personas_5명까지만u9   TestDetectIntentControl.test_limit_personas_5명까지만  sx    _+, 	L%%od%KF	Lh9,,,f~!1111g!###		L 	Lr  c                     t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ |d   d	k(  sJ |d
   dk(  sJ y# 1 sw Y   (xY w)u:   '로키 빠져' → control/remove_persona/persona='loki'.re  u   로키 빠져Tr@  NrB  r  r  remove_personar   r   ri  r   s      r      test_remove_persona_로키u2   TestDetectIntentControl.test_remove_persona_로키  sy    _+, 	L%%od%KF	Lh9,,,f~!1111i F***		L 	Lr  c                     t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ |d   d	k(  sJ |d
   dk(  sJ y# 1 sw Y   (xY w)u?   '아테나 나가' → control/remove_persona/persona='athena'.re  u   아테나 나가Tr@  NrB  r  r  r  r   r   ri  r   s      r      test_remove_persona_나가u2   TestDetectIntentControl.test_remove_persona_나가  sz    _+, 	O%%&8%NF	Oh9,,,f~!1111i H,,,		O 	Or  c                     t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ |d   d	k(  sJ |d
   dk(  sJ y# 1 sw Y   (xY w)u;   '비너스 불러' → control/add_persona/persona='venus'.re  u   비너스 불러Tr@  NrB  r  r  add_personar   r   ri  r   s      r      test_add_persona_불러u/   TestDetectIntentControl.test_add_persona_불러  sy    _+, 	O%%&8%NF	Oh9,,,f~...i G+++		O 	Or  c                     t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ |d   d	k(  sJ |d
   dk(  sJ y# 1 sw Y   (xY w)u=   '로키 합류시켜' → control/add_persona/persona='loki'.re  u   로키 합류시켜Tr@  NrB  r  r  r  r   r   ri  r   s      r      test_add_persona_합류u/   TestDetectIntentControl.test_add_persona_합류  sy    _+, 	R%%&;D%QF	Rh9,,,f~...i F***		R 	Rr  c                     t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ |d   d	k(  sJ t        |d
         h dk(  sJ y# 1 sw Y   3xY w)uU   '백엔드만 남아' → control/filter_by_role/personas=['vulcan','thor','anubis'].re  u   백엔드만 남아Tr@  NrB  r  r  filter_by_roler6     r   r   r   rG   r   rE  r4   r   s      r      test_filter_by_role_백엔드u5   TestDetectIntentControl.test_filter_by_role_백엔드  s    _+, 	R%%&;D%QF	Rh9,,,f~!11116*%&*FFFF		R 	R   AA%c                     t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ |d   d	k(  sJ t        |d
         h dk(  sJ y# 1 sw Y   3xY w)u%   '1팀만' → control/filter_by_team.re  u   1팀만Tr@  NrB  r  r  filter_by_teamr6     r   r  r   r   r   r  r   s      r      test_filter_by_team_1팀u0   TestDetectIntentControl.test_filter_by_team_1팀  s    _+, 	F%%i%EF	Fh9,,,f~!11116*%&*YYYY		F 	Fr  c                     t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ |d   d	k(  sJ |d
   dk(  sJ y# 1 sw Y   (xY w)u@   '3턴까지만 자동으로' → control/set_auto_turns/count=3.re  u   3턴까지만 자동으로Tr@  NrB  r  r  set_auto_turnsr  r1  ri  r   s      r      test_set_auto_turns_3턴u0   TestDetectIntentControl.test_set_auto_turns_3턴  sz    _+, 	Y%%&BSW%XF	Yh9,,,f~!1111g!###		Y 	Yr  c                     t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ |d   d	k(  sJ |d
   dk(  sJ y# 1 sw Y   (xY w)u7   '계속 얘기해' → control/set_auto_turns/count=99.re  u   계속 얘기해Tr@  NrB  r  r  r  r  c   ri  r   s      r      test_set_auto_turns_계속u2   TestDetectIntentControl.test_set_auto_turns_계속  sy    _+, 	O%%&8%NF	Oh9,,,f~!1111g"$$$		O 	Or  c                     t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ y# 1 sw Y   xY w)u*   '끝' → end_chat (기존 기능 유지).re  u   끝Tr@  NrB  rX  ri  r   s      r   test_end_chat_still_worksz1TestDetectIntentControl.test_end_chat_still_works  sS    _+, 	B%%eD%AF	Bh:---	B 	Bs	   =Ac                     t               }d}t        d|      5  |j                  dd      }ddd       d   d	k(  sJ y# 1 sw Y   xY w)
uW   일반 메시지는 제어 명령이 아닌 경우 Claude 호출 후 user_input 반환.uH   {"intent": "user_input", "message": "오늘 어떻게 진행할까요?"}re  r
   u!   오늘 어떻게 진행할까요?Tr@  NrB  r  ri  )rT   rF   rR  r   s       r   test_normal_user_inputz.TestDetectIntentControl.test_normal_user_input  s\    _b+-H 	`%%&IZ^%_F	`h<///	` 	`s   AA
N)rl   rm   rn   ro   r  r  r  r  r  r  r  r  r  r  r  r  rp   r   r   r  r    sC    Q%$+-,+GZ$%.0r   r  c                   4    e Zd ZdZd Zd Zd Zd Zd Zd Z	y)	TestDetectIntentStartPersonasuD   시작 시 자연어 인원 지정 테스트 (session_active=False).c                    t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ t        |d         t        |j                        k(  sJ t        |d         d	k(  sJ y# 1 sw Y   MxY w)
u<   '전원 집합' → personas 19명 (ALL_PERSONA_IDS 전체).re  u   전원 집합Fr@  NrB  rC  r6  r  )rG   r   rE  r4   r  r6   r   s      r      test_전원_집합u0   TestDetectIntentStartPersonas.test_전원_집합'  s    _+, 	M%%oe%LF	Mh<///6*%&#b.@.@*AAAA6*%&",,,		M 	Ms   A66A?c                     t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ t        |d         h d	k(  sJ y# 1 sw Y   )xY w)
uX   '백엔드만 소집' → personas=['vulcan','thor','anubis'] (소집 키워드 포함).re  u   백엔드만 소집Fr@  NrB  rC  r6  r  r  r   s      r      test_백엔드만_모여u6   TestDetectIntentStartPersonas.test_백엔드만_모여0  sm    _+, 	S%%&;E%RF	Sh<///6*%&*FFFF	S 	S   AAc                     t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ t        |d         h d	k(  sJ y# 1 sw Y   )xY w)
uK   '1팀 모여봐' → personas=개발1팀 5명 (모여봐 키워드 포함).re  u   1팀 모여봐Fr@  NrB  rC  r6  r  r  r   s      r      test_1팀_모여u.   TestDetectIntentStartPersonas.test_1팀_모여8  sm    _+, 	N%%&6u%MF	Nh<///6*%&*YYYY	N 	Nr  c                     t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ t        |d         h d	k(  sJ y# 1 sw Y   )xY w)
uN   '팀장 소집' → personas=['hermes','odin','ra'] (소집 키워드 포함).re  u   팀장 소집Fr@  NrB  rC  r6  >   r   r   r   r  r   s      r      test_팀장들_모여u3   TestDetectIntentStartPersonas.test_팀장들_모여@  sl    _+, 	M%%oe%LF	Mh<///6*%&*BBBB	M 	Mr  c                     t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ t        |d         d	k(  sJ y# 1 sw Y   'xY w)
ua   '전원 집합 5명만' → personas 5명으로 제한 (전원 집합 키워드 + 인원 제한).re  u   전원 집합 5명만Fr@  NrB  rC  r6  r  )rG   r   rE  r6   r   s      r      test_5명만_모여u1   TestDetectIntentStartPersonas.test_5명만_모여H  sl    _+, 	U%%&=e%TF	Uh<///6*%&!+++	U 	Us   AAc                     t               }t        d      5  |j                  dd      }ddd       d   dk(  sJ |d   g d	k(  sJ y# 1 sw Y    xY w)
u8   '팀 모여' → 기본 3명 ['hermes','athena','thor'].re  r?  Fr@  NrB  rC  r6  rq  ri  r   s      r      test_기본_소집u0   TestDetectIntentStartPersonas.test_기본_소집P  sg    _+, 	J%%l5%IF	Jh<///j!%AAAA	J 	Js   A		AN)
rl   rm   rn   ro   r  r  r  r  r  r
  rp   r   r   r  r  $  s'    N-GZC,Br   r  r   r   r   r   r   r   r   u   불칸u   백엔드 개발자r   u	   이리스u   프론트엔드 개발자r   r  u   UX/UI 설계자r  u   아르고스u   QA 엔지니어r   r   r   r   r   r  r  u   프레이야r  u	   미미르u   UX 설계자r
  u	   헤임달r   u   라u
   개발3팀u   개발3팀장r   u   아누비스r  u	   이시스r	  u	   소베크r  u	   호루스r   r   u	   레드팀u   레드팀 리더r   r   r   u   QC 센터장u	   야누스r   u   DevOps 센터장u	   비너스u   디자인 센터u   디자인 센터장)r   r   c                   T    e Zd ZdZddZd Zd Zd Zd Zd Z	d	 Z
d
 Zd Zd Zd Zy)TestHandleControlu#   handle_control 메서드 테스트.Nc                 @   t               }|j                  dt              }d|_        d|_        |t        |      |_        n"t        t        j                               |_        |t        |      |_	        |S |j                  D ci c]  }|d c}|_	        |S c c}w )uB   테스트용 GroupChatSession 인스턴스를 생성하는 헬퍼.r  rZ  Tra  r   )
rG   r[  _ALL_PERSONAS_DATAr$   r7  rC   r6  rD   rO   r;  )rT   r6  r;  rF   r  r  s         r   _make_sessionzTestHandleControl._make_session  s    _%%LHZ%[#H~G#$6$;$;$=>G##'#5G   3:2B2B#CQAqD#CG  $Ds   
Bc                 x   | j                         }t        |j                        dk(  sJ t        d      5 }|j	                  ddd       ddd       t        |j                        dk(  sJ t        |j
                        dk(  sJ j                          |j                  d   d   }d	|v sJ y# 1 sw Y   fxY w)
uH   19명에서 5명으로 축소하고 send_system_message를 호출한다.r  r  r  r  r  r  Nr   r2  u   ⚙️)r  r6   r6  r   handle_controlr;  rP  r8  rT   r  r  	sent_texts       r   test_limit_personasz%TestHandleControl.test_limit_personas	  s    $$&7##$***-. 	K,"",<q#IJ	K 7##$)))7''(A---'') **1-a0	9$$$	K 	Ks   B00B9c                     | j                  g d      }t        d      5 }|j                  ddd       ddd       t        |j                        dk(  sJ j                          y# 1 sw Y   4xY w)	u>   count=0이면 무시되고 personas가 변경되지 않는다.rq  )r6  r  r  r   r  Nr1  )r  r   r  r6   r6  rS   rT   r  r  s      r    test_limit_personas_zero_ignoredz2TestHandleControl.test_limit_personas_zero_ignored	  su    $$.J$K-. 	K,"",<q#IJ	K 7##$)))&&(		K 	Ks   A((A1c                    | j                  g ddddd      }t        d      5  |j                  ddd	       d
d
d
       d|j                  vsJ d|j                  vsJ t        |j                        dk(  sJ y
# 1 sw Y   DxY w)uD   특정 인원을 퇴장시키고 speak_counts에서도 제거한다.rq  r2  r3  r1  r6  r;  r  r  r   r  r   Nr  r   r  r6  r;  r6   rT   r  s     r   test_remove_personaz%TestHandleControl.test_remove_persona'	  s    $$1$%A> % 

 -. 	T"",<#RS	T w/////w333337##$)))	T 	Ts   A==Bc                    | j                  dgddi      }t        d      5 }|j                  ddd       ddd       d|j                  v sJ t	        |j                        dk(  sJ j                          y# 1 sw Y   DxY w)	uL   1명만 남았을 때 제거 시도 → 무시하고 그대로 유지한다.r   r   r  r  r  r  Nr3  )r  r   r  r6  r6   rS   r  s      r   test_remove_persona_last_onez.TestHandleControl.test_remove_persona_last_one5	  s    $$Z"A % 

 -. 	T,"",<#RS	T 7+++++7##$)))&&(	T 	Ts   A::Bc                    | j                  ddgddd      }t        d      5  |j                  dd	d
       ddd       d	|j                  v sJ |j                  d	   dk(  sJ t        |j                        dk(  sJ y# 1 sw Y   HxY w)uC   새 인원을 합류시키고 speak_counts에 0으로 추가한다.r   r   r3  r2  r4  r  r  r  r   r  Nr   r1  r  r  s     r   test_add_personaz"TestHandleControl.test_add_personaC	  s    $$)$%3 % 

 -. 	O""Mf#MN	O )))))##F+q0007##$)))	O 	Os   B  B	c                 0   | j                  ddgddd      }t        d      5 }|j                  ddd	       d
d
d
       |j                  j	                  d      dk(  sJ t        |j                        dk(  sJ j                          y
# 1 sw Y   TxY w)uP   이미 있는 인원을 추가하면 무시하고 중복 추가하지 않는다.r   r   r3  r2  r4  r  r  r  r  N)r  r   r  r6  r  r6   rS   r  s      r   test_add_persona_duplicatez,TestHandleControl.test_add_persona_duplicateQ	  s    $$)$%3 % 

 -. 	Q,""Mh#OP	Q %%h/14447##$)))&&(	Q 	Qs   BBc                    | j                  g ddddd      }t        d      5  |j                  dg dd	d
       ddd       t        |j                        h dk(  sJ d|j
                  vsJ d|j
                  vsJ d|j
                  vsJ |j
                  d   dk(  sJ |j
                  d   dk(  sJ |j
                  d   dk(  sJ y# 1 sw Y   xY w)u4   역할 필터를 적용해 personas를 교체한다.r   r   r   r3  r   r2  r  r  r  )r   r   r   u	   백엔드)r  r6  r   Nr  r   r   r   r   r   r   )r  r   r  r4   r6  r;  r  s     r   test_filter_by_rolez%TestHandleControl.test_filter_by_role_	  s   $$1$%A> % 

 -. 	"", <'	 7##$(DDDDw33333w33333W11111##H-222##F+q000##H-222#	 	s   CCc                 >   | j                  g ddddd      }t        d      5  |j                  dg dd	d
       ddd       t        |j                        h dk(  sJ d|j
                  vsJ |j
                  j                  dd      dk(  sJ y# 1 sw Y   WxY w)u1   팀 필터를 적용해 personas를 교체한다.r&  r3  r   r2  r  r  r  )r   r   r  r  r
  u   2팀)r  r6  r:   N>   r   r   r  r  r
  r   r   r   )r  r   r  r4   r6  r;  r   r  s     r   test_filter_by_teamz%TestHandleControl.test_filter_by_teamy	  s    $$1$%A> % 

 -. 	"", N"	 7##$(VVVVw33333##''3q888	 	s   BBc                 .   | j                         }d|_        t        d      5 }|j                  ddd       ddd       |j                  dk(  sJ |j                  dk(  sJ j                          |j                  d   d   }d	|v sJ y# 1 sw Y   TxY w)
u:   max_auto_turns를 설정하고 auto_turns를 리셋한다.r  r  r  r1  r  Nr   r2  r  r  r:  r   r  max_auto_turnsrP  r8  r  s       r   test_set_auto_turnsz%TestHandleControl.test_set_auto_turns	  s    $$&-. 	K,"",<q#IJ	K %%***!!Q&&&'') **1-a0	i	K 	K   BBc                 .   | j                         }d|_        t        d      5 }|j                  ddd       ddd       |j                  dk(  sJ |j                  dk(  sJ j                          |j                  d   d   }d	|v sJ y# 1 sw Y   TxY w)
u7   count=99 → '계속 진행' 메시지를 전송한다.rg  r  r  r  r  Nr   r2  u   계속r+  r  s       r   test_set_auto_turns_unlimitedz/TestHandleControl.test_set_auto_turns_unlimited	  s    $$&-. 	L,"",<r#JK	L %%+++!!Q&&&'') **1-a0	9$$$	L 	Lr.  )NN)rl   rm   rn   ro   r  r  r  r  r   r"  r$  r'  r)  r-  r0  rp   r   r   r  r    s<    - %)*)*)349( %r   r  c                       e Zd ZdZd Zy)TestSendSystemMessageu(   send_system_message 메서드 테스트.c                     t               }|j                  dddddddddi      }d	|_        t        d
      5 }|j	                  d       ddd       j                  dd	d       y# 1 sw Y   xY w)uP   send_system_message는 '⚙️ 텍스트' 형식으로 Telegram에 전송한다.r  r   r   r   r   r   r   rZ  r  r  u   인원 조정 완료Nu   ⚙️ 인원 조정 완료)rG   r[  r7  r   send_system_messager  )rT   rF   r  r  s       r   test_sends_with_gear_emojiz0TestSendSystemMessage.test_sends_with_gear_emoji	  s    _%%*(+!##%$&	 & 
 "-. 	@,''(>?	@ 	,,\7Dab	@ 	@s   A''A0N)rl   rm   rn   ro   r5  rp   r   r   r2  r2  	  s    2cr   r2  )3ro   r2   rP   r   r  r   pathlibr   unittest.mockr   r   r   rz   r   rQ   r   r   r/   r^   pathinsertintr@   rG   rI   rr   r   r   r   r  r  r+  fixturerW  r]  r_  r  r  r  r  r
  r=  rs  r  r  r  r  r  r  r  r2  rp   r   r   <module>r<     s  *  	 
    5 5 C"  bjjnn%57LMN#h.1NN ~chh&HHOOAs>*+5c 5p 3# 3#v;- ;-FkY kYfo, o,nX) X)@1J 1Jr$ $X\+ \+H  
 	 	"~+ ~+Lf, f,\'/ '/^\  \ Ho$ o$ne' e'Ze. e.Z.0 .0l\+ \+Hk* k*fOG OGnj0 j0d2B 2BvYY %Y" +#Y2 !3YB !CYR SYb %cYr +sYB CYR !SYb 	cYr %sYB +CYR SYb !cYr "sYB CYT " "%cY xl% l%hc cr   