
    Si~                       d 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 ddlm	Z	m
Z
 ddlZej                  j                  dd      Zdefd	Zdefd
Zdedej&                  f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 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 d0 d1      Z( G d2 d3      Z)d4 Z*d5 Z+d6 Z, G d7 d8      Z- G d9 d:      Z. G d; d<      Z/ G d= d>      Z0 G d? d@      Z1 G dA dB      Z2 G dC dD      Z3 G dE dF      Z4 G dG dH      Z5 G dI dJ      Z6 G dK dL      Z7 G dM dN      Z8 G dO dP      Z9 G dQ dR      Z: G dS dT      Z; G dU dV      Z< G dW dX      Z= G dY dZ      Z>y)[u[  
test_dispatch.py

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

테스트 항목:
- generate_task_id(): 유니크한 ID 생성 (tmp_path + monkeypatch로 WORKSPACE 격리)
- build_prompt(): 올바른 팀 정보 포함 여부 확인
- TEAM_BOT 매핑 정확성 검증
- 잘못된 팀 ID 전달 시 에러 처리 (sys.exit 또는 ValueError)
    NdatetimePath)	MagicMockpatchCOKACDIR_CHAT_ID
6937032012returnc                     t        t        j                  j                  dd            } | dz  dz  }|j	                         s
t               S t        |d      5 }t        j                  |      }ddd       t               }j                  di       j                  d	i       j                  d
g       D ]  }|j                  d      dk(  s|j                  dg       D ]^  }|j                  dd      }|j                  d      s'|j                  d      s9|j                  d      dk7  sN|j                  |       `  |S # 1 sw Y   xY w)uD   organization-structure.json에서 dev 팀 ID 목록을 로드한다.WORKSPACE_ROOT/home/jay/workspacememoryorganization-structure.jsonutf-8encodingN	structurecolumnsteamsteam_iddevelopment-office	sub_teamssub_team_id devz-teamstatusplanned)r   osenvirongetexistssetopenjsonload
startswithendswithadd)	workspaceorg_pathforgdev_team_idsteamsubtids           D/home/jay/workspace/.worktrees/task-2117-dev1/tests/test_dispatch.py_load_dev_team_ids_from_orgr3      s!   RZZ^^$46KLMI8#&CCH??u	h	) Qiil5LR(,,Y;??L *88I"66xxR0 *ggmR0>>%(S\\'-BswwxGX\eGe $$S)**  s   EEc                  :   t        t        j                  j                  dd            } | dz  dz  }|j	                         syt        |d      5 }t        j                  |      }ddd       d	}j                  d
i       j                  di       j                  dg       D ]j  }|j                  d      dk(  r|j                  dd      }|dk(  r2|j                  dg       D ]  }|j                  d      dk7  s|dz  } a|dv sf|dz  }l |S # 1 sw Y   xY w)uP   organization-structure.json에서 전체 팀(dev + logical) 수를 계산한다.r   r   r   r   r   r   Nr   r   r   r   r   r   r   r   r   r      )marketing-teamzconsulting-teamzpublishing-teamzdesign-teamzcontent-team)r   r   r    r!   r"   r$   r%   r&   )r*   r+   r,   r-   countr/   r1   r0   s           r2   _count_all_team_info_from_orgr9   1   s   RZZ^^$46KLMI8#&CCH??	h	) QiilER(,,Y;??L 88H*hhy"%&&xxR0 778$	1QJE kkQJE L s   DDtmp_pathc                    t        t        j                  j                  dd            }t	        |      t
        j                  vr)t
        j                  j                  dt	        |             ddl}t        t
        j                  j                               D ]  }|dk(  s	t
        j                  |=  ddl}| |_        |S )u   dispatch 모듈을 tmp_path를 WORKSPACE로 설정하여 로드한다.

    dispatch.py는 모듈 최상단에서 WORKSPACE를 __file__ 기준으로 결정하므로
    sys.modules에서 제거 후 WORKSPACE 패치를 적용한다.
    r   r   r   Ndispatch)r   r   r    r!   strsyspathinsertprompts.team_promptslistmoduleskeysr<   	WORKSPACE)r:   r*   promptsmod_name	_dispatchs        r2   _load_dispatch_with_workspacerI   N   s     RZZ^^$46KLMI
9~SXX%3y>*   ))+, &z!H%& ! #I    c              #   Z  K   ddl }| dz  j                  dd       | dz  dz  j                  dd       t        t        j                  j                  dd            }|j                  j                  d	      }t        |       }d
 |_        | ||_	        |||j                  d	<   yyw)u:   격리된 WORKSPACE를 사용하는 dispatch 모듈 반환r   Nr   Tparentsexist_oktasksr   r   r<   c                      yNT keys    r2   <lambda>zdispatch_mod.<locals>.<lambda>y       rJ   )
r>   mkdirr   r   r    r!   rC   rI   _check_bot_processrE   )r:   _sysreal_workspace_original_dispatchmods        r2   dispatch_modr]   m   s      t<7"))$)F"**..)9;PQRN))*5
'
1C-C
I"CM%#5Z  &s   B)B+c                   (    e Zd ZdZd Zd Zd Zd Zy)TestTeamBotMappinguG   TEAM_BOT 딕셔너리가 올바른 팀→봇 매핑을 갖는지 검증c                 ,    |j                   d   dk(  sJ y )N	dev1-teamdev1TEAM_BOTselfr]   s     r2   test_dev1_team_maps_to_dev1z.TestTeamBotMapping.test_dev1_team_maps_to_dev1       $$[1V;;;rJ   c                 ,    |j                   d   dk(  sJ y )N	dev2-teamdev2rc   re   s     r2   test_dev2_team_maps_to_dev2z.TestTeamBotMapping.test_dev2_team_maps_to_dev2   rh   rJ   c                 ,    |j                   d   dk(  sJ y )N	dev3-teamdev3rc   re   s     r2   test_dev3_team_maps_to_dev3z.TestTeamBotMapping.test_dev3_team_maps_to_dev3   rh   rJ   c                     t               }t        |      dkD  sJ d       t        |j                  j	                               |k(  sJ y )Nr   u7   organization-structure.json에서 dev 팀 로드 실패)r3   lenr#   rd   rD   )rf   r]   expected_dev_teamss      r2   $test_team_bot_contains_all_dev_teamsz7TestTeamBotMapping.test_team_bot_contains_all_dev_teams   sH    8:%&*e,ee*<((--/04FFFFrJ   N)__name__
__module____qualname____doc__rg   rl   rp   rt   rR   rJ   r2   r_   r_      s    Q<<<GrJ   r_   c                   4    e Zd ZdZd Zd Zd Zd Zd Zd Z	y)	TestGenerateTaskIdu@   generate_task_id()가 중복 없는 ID를 생성하는지 검증c                 2    |j                         }|dk(  sJ y)u/   타이머 파일이 없을 때 첫 ID는 task-1task-1Ngenerate_task_id)rf   r]   r:   task_ids       r2   test_first_id_is_task_1z*TestGenerateTaskId.test_first_id_is_task_1   s    //1("""rJ   c                 `    |j                         }|j                         }|dk(  sJ |dk(  sJ y)u   두 번째 호출은 task-2r|   task-2Nr}   )rf   r]   r:   id1id2s        r2   test_second_id_incrementsz,TestGenerateTaskId.test_second_id_increments   s6    ++-++-hhrJ   c                     t        d      D cg c]  }|j                          }}t        t        |            dk(  sJ yc c}w )u'   연속 5회 호출 시 모두 다른 ID   N)ranger~   rr   r#   )rf   r]   r:   _idss        r2   test_ids_are_uniquez&TestGenerateTaskId.test_ids_are_unique   s>    8=aA1|,,.AA3s8}!!! Bs   Ac                     |j                         }|dz  dz  }|j                         sJ t        j                  |j	                               }||d   v sJ y)uC   생성된 ID가 task-timers.json의 tasks 딕셔너리에 기록됨r   task-timers.jsonrO   N)r~   r"   r%   loads	read_textrf   r]   r:   r   
timer_filedatas         r2   test_id_written_to_timer_filez0TestGenerateTaskId.test_id_written_to_timer_file   s[    //1(+==
  """zz*..01$w-'''rJ   c                     |j                         }|dz  dz  }t        j                  |j                               }|d   |   d   dk(  sJ y)u$   예약된 ID의 status가 'reserved'r   r   rO   r   reservedN)r~   r%   r   r   r   s         r2   "test_reserved_status_in_timer_filez5TestGenerateTaskId.test_reserved_status_in_timer_file   sS    //1(+==
zz*..01G}W%h/:===rJ   c                     |dz  dz  }dddiddiddidi}|j                  t        j                  |      d	       |j                         }|d
k(  sJ y)uA   기존 task-timers.json에 task-3.1이 있으면 다음은 task-4r   r   rO   r   	completedrunningtask-1.1task-2.1task-3.1r   r   task-4N
write_textr%   dumpsr~   rf   r]   r:   r   existingr   s         r2   -test_existing_timer_file_increments_correctlyz@TestGenerateTaskId.test_existing_timer_file_increments_correctly   ss    (+==
%{3%{3%y1
 	djj2WE//1("""rJ   N)
ru   rv   rw   rx   r   r   r   r   r   r   rR   rJ   r2   rz   rz      s#    J#
"
(>#rJ   rz   c                   l    e Zd ZdZ ej
                  d      d        Zd Zd Zd Z	d Z
d	 Zd
 Zd Zy)TestBuildPromptuG   build_prompt()가 각 팀의 올바른 정보를 포함하는지 검증T)autousec                     ddl m j                  }	 dfd	}|j                  |d|       |j                  t        j
                  j                  d|      d|       y)uM   team_prompts.build_prompt 내부의 하드코딩 경로를 tmp_path로 교체r   Nc                     dz  dz  | dz  }|j                   j                  dd       |j                  |d       j                  j	                  |       }|st        d|        |d d	 }	d
| d|  d|	 d}
d| } d| d}|d   dk(  r$j                  || |t        |      ||
|||	      }nj                  || ||||
|||	      }|dk(  r|j                  |      z  }|dk(  rd|z   }|S |dk(  rd|z   }|S )Nr   rO   .mdTrL   r   r   u   알 수 없는 팀 ID:    zpython3 ... start z --team z	 --desc ""zpython3 ... end z/memory/reports/typedirect
project_idcodingcriticalum   **[CRITICAL] 이 작업은 중요도 critical입니다. 품질 우선으로 신중하게 작업하세요.**

securityue   **[SECURITY] 이 작업은 보안 중요 작업입니다. 보안 최우선으로 작업하세요.**

)
parentrW   r   	TEAM_INFOr!   
ValueError_build_direct_promptr=   _build_glm_prompt_build_verification_section)r   r   	task_desclevelr   chain_id	task_type	task_filer/   
short_desctimer_start	timer_endreport_pathpromptr:   tps                 r2   _patched_buildz8TestBuildPrompt._patch_task_file.<locals>._patched_build   s    !8+g57)3GI""4$"?  W = <<##G,D #:7)!DEE"3BJ.wixy	R\Q]]^_K*7)4I%J&6wisCKF|x'00	N) 1 
 --) . 
 H$"88??
" F  IO  O  M *$ C  FL  LMrJ   _build_team_promptr<   )normalNNr   )rA   team_promptsbuild_promptsetattrr>   rC   r!   )rf   r]   r:   monkeypatchoriginal_buildr   r   s     `   @r2   _patch_task_filez TestBuildPrompt._patch_task_file   s[     	* dl6	p 	L*>O 	KKOOJ5 	
rJ   c                 :    |j                  dddd      }d|v sJ y )Nra      테스트 작업r   r   r      헤르메스r   rf   r]   r   s      r2   #test_dev1_team_contains_leader_namez3TestBuildPrompt.test_dev1_team_contains_leader_name   s+    **;8JJ^f*g'''rJ   c                 ^    |j                  dddd      }d|v sJ d|v sJ d|v sJ d	|v sJ y )
Nra   r   r   r   r   u   불칸u	   이리스u	   아테나u   아르고스r   r   s      r2   test_dev1_team_contains_membersz/TestBuildPrompt.test_dev1_team_contains_members$  sU    **;8JJ^f*g6!!!f$$$f$$$'''rJ   c                 :    |j                  dddd      }d|v sJ y )Nrj   r   r   r   r      오딘r   r   s      r2   #test_dev2_team_contains_leader_namez3TestBuildPrompt.test_dev2_team_contains_leader_name+  s+    **;8JJ^f*g6!!!rJ   c                 ^    |j                  dddd      }d|v sJ d|v sJ d|v sJ d	|v sJ y )
Nrj   r   r   r   r   u   토르u   프레이야u	   미미르u	   헤임달r   r   s      r2   test_dev2_team_contains_membersz/TestBuildPrompt.test_dev2_team_contains_members/  sU    **;8JJ^f*g6!!!'''f$$$f$$$rJ   c                 :    |j                  dddd      }d|v sJ y )Nrn   r   r   r   r   u	   다그다r   r   s      r2   #test_dev3_team_contains_leader_namez3TestBuildPrompt.test_dev3_team_contains_leader_name6  s+    **;8JJ^f*gf$$$rJ   c                 :    |j                  dddd      }d|v sJ y)uO   dev3-team은 다그다 팀장의 direct 타입으로 팀원 목록이 포함됨rn   r   r   r   r   Nr   r   s      r2   )test_dev3_team_contains_team_id_in_promptz9TestBuildPrompt.test_dev3_team_contains_team_id_in_prompt:  s+    **;8JJ^f*gf$$$rJ   c                 :    |j                  dddd      }d|v sJ y )Nra   r   z	task-99.1r   r   r   r   s      r2   test_prompt_contains_task_idz,TestBuildPrompt.test_prompt_contains_task_id?  s+    **;8JK_g*hf$$$rJ   N)ru   rv   rw   rx   pytestfixturer   r   r   r   r   r   r   r   rR   rJ   r2   r   r      sH    QV^^D!E
 "E
N(("%%%
%rJ   r   c                   (    e Zd ZdZd Zd Zd Zd Zy)TestGetDispatchTimeuR   get_dispatch_time()이 올바른 형식의 미래 시간을 반환하는지 검증c                 J    |j                  d      }t        |t              sJ y )N
   )get_dispatch_time
isinstancer=   rf   r]   results      r2   test_returns_stringz'TestGetDispatchTime.test_returns_stringL  s"    //3&#&&&rJ   c                 R    |j                  d      }t        j                  |d       y )Nr   %Y-%m-%d %H:%M:%S)r   r   strptimer   s      r2   test_returns_parseable_datetimez3TestGetDispatchTime.test_returns_parseable_datetimeP  s#    //3&"56rJ   c                     ddl m } |j                         }|j                  d      }|j                  |d      }||kD  sJ y )Nr   r   <   r   r   nowr   r   rf   r]   dtbeforer   parseds         r2   test_returns_future_timez,TestGetDispatchTime.test_returns_future_timeT  s=    +//3V%89rJ   c                     ddl m } |j                         }|j                         }|j                  |d      }||kD  sJ y)u3   기본 delay_seconds=10일 때 미래 시간 반환r   r   r   Nr   r   s         r2   test_default_delayz&TestGetDispatchTime.test_default_delay\  s;    +//1V%89rJ   N)ru   rv   rw   rx   r   r   r   r   rR   rJ   r2   r   r   I  s    \'7rJ   r   c                   (    e Zd ZdZd Zd Zd Zd Zy)TestOrganizationConstantsuB   조직 구조 상수가 올바르게 정의되어 있는지 검증c                 d    t               }|dkD  sJ d       t        |j                        |k(  sJ y )Nr   u7   organization-structure.json에서 팀 수 계산 실패)r9   rr   r   )rf   r]   expected_team_counts      r2   test_team_info_has_all_teamsz6TestOrganizationConstants.test_team_info_has_all_teamsn  s9    ;="Q&a(aa&<))*.AAAArJ   c                 l    |j                   j                         D ]  \  }}d|v sJ d|v sJ d|v rJ  y )Nleaderrolemembers)r   items)rf   r]   r   infos       r2   %test_each_team_info_has_required_keysz?TestOrganizationConstants.test_each_team_info_has_required_keyss  sL    )3399; 	%MGTt###T>!>$$$	%rJ   c                 \    h d}t        |j                  j                               |k(  sJ y )N>   qcdesigndevopsredteam)r#   CROSS_FUNCTIONALrD   )rf   r]   expecteds      r2   (test_cross_functional_has_expected_roleszBTestOrganizationConstants.test_cross_functional_has_expected_rolesy  s)    8<005578HDDDrJ   c                 `    |j                   j                         D ]  \  }}d|v sJ d|v rJ  y )Nnamer  )r  r  )rf   r]   rT   r  s       r2   ,test_cross_functional_each_has_name_and_rolezFTestOrganizationConstants.test_cross_functional_each_has_name_and_role}  s<    %66<<> 	"ICT>!>T>!>	"rJ   N)ru   rv   rw   rx   r   r  r  r  rR   rJ   r2   r   r   k  s    LB
%E"rJ   r   c                   "    e Zd ZdZd Zd Zd Zy)TestInvalidTeamIduT   알 수 없는 팀 ID를 전달했을 때 적절한 에러가 발생하는지 검증c                     t        j                  t              5 }|j                  dddd       ddd       j                  j
                  dk(  sJ y# 1 sw Y   %xY w)uH   dispatch.build_prompt()는 잘못된 팀 ID에 대해 sys.exit(1) 호출nonexistent-team   작업r   r   r   Nr6   r   raises
SystemExitr   valuecoderf   r]   exc_infos      r2   !test_invalid_team_causes_sys_exitz3TestInvalidTeamId.test_invalid_team_causes_sys_exit  sZ    ]]:& 	`(%%&8(JV^%_	`~~""a'''	` 	`s   AAc                 $    d|j                   vsJ y)uF   TEAM_INFO에 정의되지 않은 팀 키는 포함되지 않아야 함r  N)r   re   s     r2   +test_team_info_does_not_contain_invalid_keyz=TestInvalidTeamId.test_team_info_does_not_contain_invalid_key  s    !)?)????rJ   c                     t        j                  t              5 }|j                  ddd       ddd       j                  j
                  dk(  sJ y# 1 sw Y   %xY w)u*   빈 문자열 팀 ID도 sys.exit(1) 호출r   r  r   Nr6   r  r  s      r2   "test_empty_team_id_causes_sys_exitz4TestInvalidTeamId.test_empty_team_id_causes_sys_exit  sS    ]]:& 	@(%%b(J?	@~~""a'''	@ 	@s   AAN)ru   rv   rw   rx   r  r!  r#  rR   rJ   r2   r  r    s    ^(@(rJ   r  c                   .    e Zd ZdZd Zd Zd Zd Zd Zy)TestGenerateTaskIdErrorHandlingu4   generate_task_id() 에러 핸들링 분기 테스트c                     |dz  dz  }|j                  dd       t        j                  t        d      5  |j	                          ddd       y# 1 sw Y   yxY w)	uq   task-timers.json이 깨진 JSON일 때 RuntimeError 발생 (파일 손상 = 하드코딩 기본값 사용 금지)r   r   INVALID JSON{{{r   r   u   손상matchN)r   r   r  RuntimeErrorr~   rf   r]   r:   r   s       r2   (test_corrupted_json_raises_runtime_errorzHTestGenerateTaskIdErrorHandling.test_corrupted_json_raises_runtime_error  sU    (+==
/'B]]<x8 	,))+	, 	, 	,s   AAc                     |dz  dz  }dddiddidi}|j                  t        j                  |      d       |j                         }|d	k(  sJ y
)uG   숫자로 변환 불가능한 task ID가 있으면 skip (lines 110-111)r   r   rO   r   r   )ztask-abc.defr   r   r   task-3Nr   r   s         r2    test_unparseable_task_id_skippedz@TestGenerateTaskIdErrorHandling.test_unparseable_task_id_skipped  se    (+==
x.ET\^iSjkldjj2WE//1("""rJ   c                     |dz  dz  }|j                  dd       ddl}|j                  dgfd}t        d	|
      5  |j	                         }ddd       j                  d      sJ y# 1 sw Y   xY w)u?   task-timers.json 재읽기 실패 시 초기화 (lines 121-123)r   r   z+{"tasks": {"task-1.1": {"status": "done"}}}r   r   r   Nc                      | rt        | d         nd}t        |       dkD  rt        | d         }nt        |j                  dd            }d|v r(d|v r$d|vr dxx   dz  cc<   d   dk(  rt        d	       | i |S )
Nr   r   r6   moderr   w   u   모킹된 읽기 실패r=   rr   r!   OSError)argskwargsf_pathr2  
call_countoriginal_opens       r2   mock_open_fnzTTestGenerateTaskIdErrorHandling.test_timer_file_reread_failure.<locals>.mock_open_fn  s    %)Sa\rF4y1}47|6::fc23!V+t41"a=A%!";<< $1&11rJ   builtins.openside_effecttask-)r   builtinsr$   r   r~   r'   )	rf   r]   r:   r   rB  r=  r   r;  r<  s	          @@r2   test_timer_file_reread_failurez>TestGenerateTaskIdErrorHandling.test_timer_file_reread_failure  s    (+==
KV]^ S
	2 ?= 	6"335G	6 !!'***	6 	6s   A11A:c                     |dz  dz  }|j                  dd       |j                         }|dk(  sJ t        j                  |j	                               }d|v sJ y)	u@   timer_data에 'tasks' 키가 없을 때 자동 추가 (line 127)r   r   {}r   r   r|   rO   N)r   r~   r%   r   r   )rf   r]   r:   r   r   r   s         r2   !test_timer_data_without_tasks_keyzATestGenerateTaskIdErrorHandling.test_timer_data_without_tasks_key  sd    (+==
dW5//1("""zz*..01$rJ   c                     ddl }|j                  fd}t        d|      5  |j                         }ddd       j	                  d      sJ y# 1 sw Y   xY w)uA   task-timers.json 쓰기 실패해도 ID는 반환 (lines 136-137)r   Nc                      | rt        | d         nd}t        |       dkD  rt        | d         }nt        |j                  dd            }d|v rd|v rt        d       | i |S )	Nr   r   r6   r2  r3  r   r4  u   쓰기 실패 모킹r6  )r8  r9  r:  r2  r<  s       r2   mock_open_writez\TestGenerateTaskIdErrorHandling.test_write_failure_still_returns_id.<locals>.mock_open_write  sk    %)Sa\rF4y1}47|6::fc23!V+t455 $1&11rJ   r>  r?  rA  )rB  r$   r   r~   r'   )rf   r]   r:   rB  rI  r   r<  s         @r2   #test_write_failure_still_returns_idzCTestGenerateTaskIdErrorHandling.test_write_failure_still_returns_id  sW     	2 ?@ 	6"335G	6!!'***	6 	6s   AAN)	ru   rv   rw   rx   r,  r/  rC  rF  rJ  rR   rJ   r2   r%  r%    s    >,#+8+rJ   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)TestDispatchFunctionu+   dispatch() 함수 테스트 (lines 198-278)Nc                 v    t               }||_        ||nt        j                  ddi      |_        ||_        |S )u   subprocess mock 헬퍼r   okr   
returncoder%   r   stdoutstderrrf   rP  rQ  rR  mock_results        r2   _make_mock_subprocessz*TestDispatchFunction._make_mock_subprocess  s=    k!+'-'9Vtzz8UYJZ?[#rJ   c                    | j                  dt        j                  ddd            }t        j                  |d      5 }t        j                  |dddd	      5  ||j
                  _        |j                  d
d      }ddd       ddd       d   dk(  sJ |d   d
k(  sJ d|v sJ y# 1 sw Y   ,xY w# 1 sw Y   0xY w)u.   dispatch() 성공 시 status=dispatched 반환r   rN  z	sched-123)r   id
subprocessBOT_KEYSkey1anu-keyrb   anura   r   Nr   
dispatchedr/   r   rU  r%   r   r   objectrunreturn_valuer<   rf   r]   r:   rT  mock_subr   s         r2   test_dispatch_successz*TestDispatchFunction.test_dispatch_success  s    00DJJ$Va?b4cd LL|4	L8@LLzF93UV	L )4HLL%!**;8JKF	L 	L h<///f~,,,F"""	L 	L 	L 	Ls#   B5$B)>B5)B2	.B55B>c                 J   | j                  ddd      }t        j                  |d      5 }t        j                  |dddd      5  ||j                  _        |j                  d	d
      }ddd       ddd       d   dk(  sJ d|d   v sJ y# 1 sw Y   %xY w# 1 sw Y   )xY w)u)   dispatch() 실패 시 status=error 반환r6   r   zcommand not foundrX  rY  rZ  r[  r\  ra   r   Nr   errormessagerU  r   r`  ra  rb  r<   rc  s         r2   test_dispatch_failurez*TestDispatchFunction.test_dispatch_failure  s    00B8KL LL|4	L8@LLzF93UV	L )4HLL%!**;8JKF	L 	L h7***"fY&7777	L 	L 	L 	Ls#   B$B)BB	BB"c                 @   t        j                  |d      5 }t        j                  |dddd      5  t        ddd	      |j                  _        |j                  d
dd      }ddd       ddd       d   dk(  sJ d|d   v sJ y# 1 sw Y   %xY w# 1 sw Y   )xY w)u3   존재하지 않는 project_id일 때 에러 반환rX  rY  rZ  r[  r\  r   rE  r   rP  rQ  rR  ra   r  znonexistent-projr   Nr   rg     존재하지 않rh  r   r`  r   ra  rb  r<   rf   r]   r:   rd  r   s        r2   !test_dispatch_nonexistent_projectz6TestDispatchFunction.test_dispatch_nonexistent_project!  s     LL|4	a8@LLzF93UV	a )2QtTV(WHLL%!**;M_*`F	a 	a h7***!VI%6666	a 	a 	a 	as"   B2B$BB	BBc                 6   t        j                  |d      5 }t        j                  |di       5  t        ddd      |j                  _        |j                  dd      }d	d	d	       d	d	d	       d
   dk(  sJ d|d   v sJ y	# 1 sw Y   %xY w# 1 sw Y   )xY w)u'   BOT_KEYS에 봇 키가 없으면 에러rX  rY  r   rE  r   rl  ra   r  Nr   rg  u   할당된 봇이 없습니다rh  rn  ro  s        r2   test_dispatch_no_bot_keyz-TestDispatchFunction.test_dispatch_no_bot_key,  s     LL|4	B8@LLz26	B )2QtTV(WHLL%!**;AF	B 	B h7***.&2CCCC	B 	B 	B 	Bs"   B0BBB	BBc                 <   t        j                  |d      5 }t        j                  |dddd      5  t        ddd	      |j                  _        |j                  d
d      }ddd       ddd       d   dk(  sJ d|d   v sJ y# 1 sw Y   %xY w# 1 sw Y   )xY w)uB   BOT_KEYS[bot_id]가 None이면 _cleanup_task 후 error dict 반환rX  rY  Nr[  r\  r   rE  r   rl  ra   r  r   rg  '   봇 키가 설정되지 않았습니다rh  rn  ro  s        r2    test_dispatch_bot_key_none_exitsz5TestDispatchFunction.test_dispatch_bot_key_none_exits7  s     LL|4	B8@LLzD3ST	B )2QtTV(WHLL%!**;AF	B 	B h7***8F9<MMMM	B 	B 	B 	Bs"   B0B"BB	BBc                 H   | j                  dd      }t        j                  |d      5 }t        j                  |dddd      5  ||j                  _        |j                  dd	      }d
d
d
       d
d
d
       d   dk(  sJ d|d   v sJ y
# 1 sw Y   %xY w# 1 sw Y   )xY w)uD   subprocess 성공했지만 stdout이 유효한 JSON이 아닌 경우r   znot valid json outputrX  rY  rZ  r[  r\  ra   	   테스트Nr   r^  rawcron_responseri  rc  s         r2   )test_dispatch_json_decode_error_in_stdoutz>TestDispatchFunction.test_dispatch_json_decode_error_in_stdoutB  s    004KL LL|4	E8@LLzF93UV	E )4HLL%!**;DF	E 	E h<///////	E 	E 	E 	Es#   B$B(BB	BB!c                    | j                  dt        j                  ddi            }t        j                  |d      5 }t        j                  |dddd      5  ||j
                  _        |j                  d	d
      }ddd       ddd       d   }|dz  dz  | dz  }|j                         sJ y# 1 sw Y   7xY w# 1 sw Y   ;xY w)uA   dispatch()가 memory/tasks/<task_id>.md 파일을 생성하는지r   r   rN  rX  rY  rZ  r[  r\  ra   u   파일 생성 테스트Nr   r   rO   r   )	rU  r%   r   r   r`  ra  rb  r<   r"   )rf   r]   r:   rT  rd  r   r   r   s           r2   test_dispatch_creates_task_filez4TestDispatchFunction.test_dispatch_creates_task_fileP  s    00DJJ$?O4PQ LL|4	S8@LLzF93UV	S )4HLL%!**;8QRF	S 	S #x''1wisOC	!!!	S 	S 	S 	Ss#   B?$B3=B?3B<	8B??Cc                    |dz  dz  j                  dd       | j                  dt        j                  ddi            }t	        j
                  |d      5 }t	        j
                  |d	d
dd      5  ||j                  _        |j                  d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)u.   존재하는 project_id일 때 정상 dispatchprojectsmyprojTrL   r   r   rN  rX  rY  rZ  r[  r\  ra      프로젝트 작업r   Nr^  )	rW   rU  r%   r   r   r`  ra  rb  r<   rc  s         r2   test_dispatch_existing_projectz3TestDispatchFunction.test_dispatch_existing_project_  s    	J		)000M00DJJ$?O4PQ LL|4	d8@LLzF93UV	d )4HLL%!**;8MZb*cF	d 	d h<///	d 	d 	d 	ds$   B?2&B3B?3B<	8B??Cc                 r   | j                  dt        j                  ddi            }t        j                  |d      5 }t        j                  |dddd      5  ||j
                  _        |j                  d	d
      }ddd       ddd       d   dk(  sJ d|d   v sJ y# 1 sw Y   %xY w# 1 sw Y   )xY w)u9   dispatch() 결과에 팀장 이름이 포함되어야 함r   r   rN  rX  rY  key2r[  rk   r]  rj   r  Nr^  r   leadr_  rc  s         r2   test_dispatch_returns_lead_namez4TestDispatchFunction.test_dispatch_returns_lead_namem  s    00DJJ$?O4PQ LL|4	B8@LLzF93UV	B )4HLL%!**;AF	B 	B h<///6&>)))	B 	B 	B 	Bs#   B-$B!=B-!B*	&B--B6c                 d   | j                  dt        j                  ddi            }t        j                  |d      5 }t        j                  |dddd      5  ||j
                  _        |j                  d	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)u1   dispatch()에 level 인자가 결과에 반영됨r   r   rN  rX  rY  rZ  r[  r\  ra      중요 작업r   r   Nr   r_  rc  s         r2   test_dispatch_level_propagatedz3TestDispatchFunction.test_dispatch_level_propagated{  s    00DJJ$?O4PQ LL|4	[8@LLzF93UV	[ )4HLL%!**;z*ZF	[ 	[ g*,,,	[ 	[ 	[ 	[s#   B&&B?B&B#	B&&B/r   Nr   )ru   rv   rw   rx   rU  re  rj  rp  rr  ru  rz  r|  r  r  r  rR   rJ   r2   rL  rL    s>    5#8	7	D	N0"0*-rJ   rL  c                   4    e Zd ZdZd Zd Zd Zd Zd Zd Z	y)	TestMainCLIu+   main() CLI 함수 테스트 (lines 282-299)c                    t               }d|_        t        j                  ddi      |_        |j                  t        dg d       t        j                  |d      5 }t        j                  |ddd	d
      5  ||j                  _
        |j                          ddd       ddd       |j                         }t        j                  |j                        }d|v sJ y# 1 sw Y   GxY w# 1 sw Y   KxY w)u0   main()이 dispatch() 결과를 JSON으로 출력r   r   rN  argv)dispatch.py--teamra   --tasku   CLI 테스트rX  rY  rZ  r[  r\  Nr   rP  r%   r   rQ  r   r>   r   r`  ra  rb  main
readouterrr   outrf   r]   r   capsysrT  rd  capturedoutputs           r2   $test_main_dispatches_and_prints_jsonz0TestMainCLI.test_main_dispatches_and_prints_json  s    k!"!ZZ4(89C)jk LL|4	 8@LLzF93UV	  )4HLL%	  	  $$&HLL)6!!!	  	  	  	 $   C,8"C C, C)	%C,,C5c                    t               }d|_        t        j                  ddi      |_        |j                  t        dg d       t        j                  |d      5 }t        j                  |ddd	d
      5  ||j                  _
        |j                          ddd       ddd       |j                         }t        j                  |j                        }|j                  d      dk(  sd|v sJ yy# 1 sw Y   \xY w# 1 sw Y   `xY w)u!   main()에 --level critical 전달r   r   rN  r  )r  r  rj   r  r  z--levelr   rX  rY  r  r[  r  Nr   r   )r   rP  r%   r   rQ  r   r>   r   r`  ra  rb  r  r  r   r  r!   r  s           r2   test_main_with_level_criticalz)TestMainCLI.test_main_with_level_critical  s    k!"!ZZ4(89q	

 LL|4	 8@LLzF93UV	  )4HLL%	  	  $$&HLL)zz'"j0H4FFF4F0	  	  	  	 s$   D8"C5D5C>	:DD
c                 "   |dz  dz  j                  dd       t               }d|_        t        j                  ddi      |_        |j                  t        dg d	       t        j                  |d
      5 }t        j                  |dddd      5  ||j                  _        |j                          ddd       ddd       |j                         }t        j                  |j                        }d|v sJ y# 1 sw Y   GxY w# 1 sw Y   KxY w)u%   main()에 --session, --project 전달r~  testprojTrL   r   r   rN  r  )	r  r  ra   r  r  z	--sessionzsess-abcz	--projectr  rX  rY  rZ  r[  r\  N)rW   r   rP  r%   r   rQ  r   r>   r   r`  ra  rb  r  r  r   r  )	rf   r]   r   r  r:   rT  rd  r  r  s	            r2   "test_main_with_session_and_projectz.TestMainCLI.test_main_with_session_and_project  s     
J		+224$2Ok!"!ZZ4(89
	
" LL|4	 8@LLzF93UV	  )4HLL%	  	  $$&HLL)6!!!	  	  	  	 s$   6D"C93D9D	>DDc                    t               }d|_        t        j                  ddi      |_        |j                  t        dg d       t        j                  |d      5 }t        j                  |ddd	d
      5  ||j                  _
        |j                          ddd       ddd       |j                         }t        j                  |j                        }d|v sJ y# 1 sw Y   GxY w# 1 sw Y   KxY w)u   main()에 dev3-team 전달r   r   rN  r  )r  r  rn   r  u
   GLM 작업rX  rY  key3r[  ro   r]  Nr  r  s           r2   test_main_dev3_teamzTestMainCLI.test_main_dev3_team  s    k!"!ZZ4(89C)gh LL|4	 8@LLzF93UV	  )4HLL%	  	  $$&HLL)6!!!	  	  	  	 r  c           
      T   |dz  dz  }|j                  t        j                  di i      d       t               }d|_        t        j                  ddi      |_        |j                  t        d	g d
       t        j                  |d      5 }t        j                  |dddddd      5  ||j                  _        |j                          ddd       ddd       |j                         }t        j                  |j                        }	d|	v sJ y# 1 sw Y   GxY w# 1 sw Y   KxY w)u!   main()에 --team marketing 전달r   r   rO   r   r   r   r   rN  r  )r  r  	marketingr     마케팅 작업rX  rY  rZ  r  r  r[  rb   rk   ro   r]  Nr   r%   r   r   rP  rQ  r   r>   r   r`  ra  rb  r  r  r   r  
rf   r]   r   r  r:   r   rT  rd  r  r  s
             r2   test_main_marketing_teamz$TestMainCLI.test_main_marketing_team  s   (+==
djj'27'Jk!"!ZZ4(89C)mn LL|4		 8@LL	R		  )4HLL%		  		  $$&HLL)6!!!		  		  		  		 $   D*"DDD	DD'c           
      T   |dz  dz  }|j                  t        j                  di i      d       t               }d|_        t        j                  ddi      |_        |j                  t        d	g d
       t        j                  |d      5 }t        j                  |dddddd      5  ||j                  _        |j                          ddd       ddd       |j                         }t        j                  |j                        }	d|	v sJ y# 1 sw Y   GxY w# 1 sw Y   KxY w)u"   main()에 --team consulting 전달r   r   rO   r   r   r   r   rN  r  )r  r  
consultingr  u   컨설팅 작업rX  rY  rZ  r  r  r[  r  Nr  r  s
             r2   test_main_consulting_teamz%TestMainCLI.test_main_consulting_team
  s   (+==
djj'27'Jk!"!ZZ4(89C)no LL|4		 8@LL	R		  )4HLL%		  		  $$&HLL)6!!!		  		  		  		 r  N)
ru   rv   rw   rx   r  r  r  r  r  r  rR   rJ   r2   r  r    s%    5"&G*""H"&"4"rJ   r  c                   .    e Zd ZdZd Zd Zd Zd Zd Zy)TestSubprocessTimeoutu)   subprocess.run() timeout 처리 테스트c                 Z   ddl fd}t        j                  |d      5 }t        j                  |dddd      5  ||j                  _        j
                  |_        |j                  d	d
      }ddd       ddd       d   dk(  sJ d|d   v sJ y# 1 sw Y   %xY w# 1 sw Y   )xY w)uC   cokacdir 호출에서 TimeoutExpired 발생 시 status=error 반환r   Nc                      | r| d   n|j                  dg       }t        |t              r|r|d   dk(  rj                  |d      t	               }d|_        d|_        d|_        |S Nr   r8  cokacdirr   r   r!   r   rB   TimeoutExpiredr   rP  rQ  rR  r8  r9  cmdrT  real_subprocesss       r2   mock_run_side_effectzRTestSubprocessTimeout.test_dispatch_cokacdir_timeout.<locals>.mock_run_side_effect1  sj    !$q'vzz&"'=C#t$Q:1E%44S"==#+K%&K"!#K!#KrJ   rX  rY  rZ  r[  r\  ra   u   타임아웃 테스트 작업r   rg  u   타임아웃rh  rX  r   r`  ra  r@  r  r<   )rf   r]   r:   r  rd  r   r  s         @r2   test_dispatch_cokacdir_timeoutz4TestSubprocessTimeout.test_dispatch_cokacdir_timeout-  s    ,
	 LL|4	Y8@LLzF93UV	Y (<HLL$&5&D&DH#!**;8WXF	Y 	Y h7***	!2222	Y 	Y 	Y 	Ys"   B!5B1B!B	B!!B*c                    ddl g fd}fd}t        j                  |d      5 }t        j                  |dddd	      5  t        j                  |d
|      5  ||j                  _        j
                  |_        |j                  dd       ddd       ddd       ddd       t              dk(  sJ y# 1 sw Y   *xY w# 1 sw Y   .xY w# 1 sw Y   2xY w)u6   cokacdir TimeoutExpired 시 _cleanup_task가 호출됨r   Nc                 (    j                  |        y Nappendr   cleanup_calleds    r2   mock_cleanupzXTestSubprocessTimeout.test_dispatch_cokacdir_timeout_calls_cleanup.<locals>.mock_cleanupN      !!'*rJ   c                      | r| d   n|j                  dg       }t        |t              r|r|d   dk(  rj                  |d      t	               }d|_        d|_        d|_        |S r  r  r  s       r2   r  z`TestSubprocessTimeout.test_dispatch_cokacdir_timeout_calls_cleanup.<locals>.mock_run_side_effectQ  sj    !$q'vzz&"'=C#t$Q:1E%44S"==#+K%&K"!#K!#KrJ   rX  rY  rZ  r[  r\  _cleanup_taskr?  ra   u   cleanup 확인 테스트r6   )rX  r   r`  ra  r@  r  r<   rr   )rf   r]   r:   r  r  rd  r  r  s         @@r2   ,test_dispatch_cokacdir_timeout_calls_cleanupzBTestSubprocessTimeout.test_dispatch_cokacdir_timeout_calls_cleanupH  s    ,	+	 LL|4	K8@LLzF93UV	K LLLQ	K
 (<HLL$&5&D&DH#!!+/IJ	K 	K 	K >"a'''	K 	K 	K 	K 	K 	Ks;   CC5B;CC;C CC	CCc                    ddl }t        t        j                  j	                  dd            }|dz  dz  }|j
                  j                  d|      }||j                  J |j
                  j                  |      }|j                  j                  |       |dz  d	z  }|j                  d
d
       t        j                  |dt        |            5  t        j                  |dd
dddd      5  t        j                  |dddi      5  t        j                  |dd      5  t        j                  t        dddg      5  t        j                  t        j                  ddd      5  |j!                          ddd       ddd       ddd       ddd       ddd       ddd       |dz  j#                         rJ y# 1 sw Y   GxY w# 1 sw Y   KxY w# 1 sw Y   OxY w# 1 sw Y   SxY w# 1 sw Y   WxY w# 1 sw Y   [xY w)uP   notify-completion.py의 Telegram 전송 실패 시 sys.exit 없이 정상 반환r   Nr   r   scriptsznotify-completion.pynotify_completionr   eventsTrL   check_chain_statusFz
chain-test)in_chainis_lastr   next_task_idrb  dispatch_next_phaseactionnonesend_telegram_notificationr  
task-999.1ztest-keyz
test-token)COKACDIR_KEY_ANUANU_BOT_TOKENztask-999.1.done.clear)importlib.utilr   r   r    r!   utilspec_from_file_locationloadermodule_from_specexec_modulerW   r   r`  r=   r>   dictr  r"   )rf   r:   	importlibr*   notify_scriptspecr\   
events_dirs           r2   +test_notify_completion_send_failure_no_exitzATestSubprocessTimeout.test_notify_completion_send_failure_no_exitf  s   (8:OPQ	!I-0FF~~556I=YDKK$;;;nn--d3$(83
5 LL.H>	LL$*.5llpq	 LL%&/	 LL:N	 LLf'=|&LM	 JJrzz
Ua#bc	" HHJ#	 	 	 	 	 	( !88@@BBBB)	 	 	 	 	 	 	 	 	 	 	 	s   G>1G2G&%G	(G+G<GG	G&G2G>GGGG	G#G&&G/+G22G;	7G>>Hc                   	 ddl }g 		fd}t        j                  |d      5 }t        j                  |dddd      5  ||j                  _        |j
                  |_        |j                  d	d
       ddd       ddd       	D cg c]*  }t        |d   t              s|d   s|d   d   dk(  s)|, }}|D ]#  }|d   j                  d      dk(  rJ d|         y# 1 sw Y   oxY w# 1 sw Y   sxY wc c}w )u9   로컬 python3 timer 호출은 timeout=30으로 호출됨r   Nc                      j                  | r| d   ng |d       t               }d|_        t        j                  ddi      |_        d|_        |S Nr   )r8  r9  r   rN  r   r  r   rP  r%   r   rQ  rR  r8  r9  rT  	run_callss      r2   mock_run_capturezVTestSubprocessTimeout.test_dispatch_timer_cmd_has_timeout_30.<locals>.mock_run_capture  Q    d1g2PQ#+K%&K"!%Xt,<!=K!#KrJ   rX  rY  rZ  r[  r\  ra   u   타이머 timeout 확인r8  python3r9  timeout   u(   python3 호출에 timeout=30 미설정: )
rX  r   r`  ra  r@  r  r<   r   rB   r!   )
rf   r]   r:   r  r  rd  cpython3_callscallr  s
            @r2   &test_dispatch_timer_cmd_has_timeout_30z<TestSubprocessTimeout.test_dispatch_timer_cmd_has_timeout_30  s   ,		 LL|4	K8@LLzF93UV	K (8HLL$&5&D&DH#!!+/IJ	K 	K !
Jqy$$?AfIRSTZR[\]R^bkRkA
 
 " 	jD>%%i0B6i:bcgbh8ii6	j	K 	K 	K 	K
s:   C-5C!3C-C9 C9&C92C9!C*	&C--C6c                 X  	 ddl }g 		fd}t        j                  |d      5 }t        j                  |dddd      5  ||j                  _        |j
                  |_        |j                  d	d
       ddd       ddd       	D cg c]C  }t        |d   t              r.|d   r)|d   d   dk(  rt        |d         dk\  r|d   d   dk7  r|E }}t        |      dk\  sJ d       |D ]#  }|d   j                  d      dk(  rJ d|         y# 1 sw Y   xY w# 1 sw Y   xY wc c}w )u4   외부 cokacdir 호출은 timeout=60으로 호출됨r   Nc                      j                  | r| d   ng |d       t               }d|_        t        j                  ddi      |_        d|_        |S r  r  r  s      r2   r  zYTestSubprocessTimeout.test_dispatch_cokacdir_cmd_has_timeout_60.<locals>.mock_run_capture  r  rJ   rX  rY  rZ  r[  r\  ra   u   cokacdir timeout 확인r8  r     r5  .r6   u    메인 cokacdir 호출이 없음r9  r  r   u)   cokacdir 호출에 timeout=60 미설정: )rX  r   r`  ra  r@  r  r<   r   rB   rr   r!   )
rf   r]   r:   r  r  rd  r  main_cokacdir_callsr  r  s
            @r2   )test_dispatch_cokacdir_cmd_has_timeout_60z?TestSubprocessTimeout.test_dispatch_cokacdir_cmd_has_timeout_60  sV   ,		 LL|4	J8@LLzF93UV	J (8HLL$&5&D&DH#!!+/HI	J 	J !
!F)T*qyQvYq\Z=WAfI!#&	!(; 
 

 &'1,P.PP,' 	kD>%%i0B6j:cdhci8jj6	k!	J 	J 	J 	J
s)   D5D3DAD'D	DD$N)	ru   rv   rw   rx   r  r  r  r  r  rR   rJ   r2   r  r  *  s"    336(<$CLj:krJ   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
)TestFindAvailableBotu8   _find_available_bot() 가용 봇 자동 선택 테스트c                     |dz  dz  }|j                  t        j                  di i      d       |j                         }|dk(  sJ y)uA   모든 봇이 비어있으면 우선순위 최고인 bot-b 반환r   r   rO   r   r   bot-bNr   r%   r   _find_available_botrf   r]   r:   r   r   s        r2    test_all_bots_free_returns_bot_bz5TestFindAvailableBot.test_all_bots_free_returns_bot_b  sN    (+==
djj'27'J113   rJ   c                     |dz  dz  }dddddii}|j                  t        j                  |      d	       |j                         }|d
k(  sJ y)u<   dev1-team이 running이면 bot-b 사용 중 → bot-c 반환r   r   rO   r   ra   r   r   r   r   r   bot-cNr  rf   r]   r:   r   r   r   s         r2   test_dev1_running_returns_bot_cz4TestFindAvailableBot.test_dev1_running_returns_bot_c  s^    (+==
*+&STUdjj.A113   rJ   c                     |dz  dz  }ddddddddi}|j                  t        j                  |      d	
       |j                         }|dk(  sJ y)u*   dev1, dev2 모두 running → bot-d 반환r   r   rO   ra   r   r  rj   r   r   r   r   bot-dNr  r  s         r2   $test_dev1_dev2_running_returns_bot_dz9TestFindAvailableBot.test_dev1_dev2_running_returns_bot_d  sk    (+==
(3yI(3yI
 	djj.A113   rJ   c                 *   |dz  dz  }ddddddddddd	ddd
ddddddddddddi}|j                  t        j                  |      d       t        j                  t
        d      5  |j                          ddd       y# 1 sw Y   yxY w)u2   모든 봇이 사용 중이면 RuntimeError 발생r   r   rO   ra   r   r  rj   rn   	dev4-team	dev5-team	dev6-team	dev7-team	dev8-teamr   r   r   task-4.1task-5.1task-6.1task-7.1task-8.1r   r      모든 봇이 작업 중r(  N)r   r%   r   r   r  r*  r   rf   r]   r:   r   r   s        r2   test_all_bots_busy_raises_errorz4TestFindAvailableBot.test_all_bots_busy_raises_error  s    (+==
(3yI(3yI(3yI(3yI(3yI(3yI(3yI(3yI	
 	djj.A]]</IJ 	/,,.	/ 	/ 	/s   /B		Bc                     |dz  dz  }ddddddddddd	i}|j                  t        j                  |      d
       |j                         }|dk(  sJ y)u;   completed 상태의 태스크는 봇을 점유하지 않음r   r   rO   ra   r   r  rj   rn   r   r   r   r  Nr  r  s         r2   test_completed_tasks_dont_blockz4TestFindAvailableBot.test_completed_tasks_dont_block  ss    (+==
(3{K(3{K(3{K
 	djj.A113   rJ   c                     |dz  dz  }ddddddii}|j                  t        j                  |      d	
       |j                         }|dk(  sJ y)uD   마케팅 태스크가 bot-b에서 running이면 bot-b 사용 불가r   r   rO   r   r  r   r  r   r   botr   r   r  Nr  r  s         r2   $test_marketing_on_bot_b_blocks_bot_bz9TestFindAvailableBot.test_marketing_on_bot_b_blocks_bot_b  sh    (+==
yQXY

 	djj.A113   rJ   c                     |dz  dz  }|j                         r|j                          |j                         }|dk(  sJ y)u5   timer 파일이 없으면 모든 봇 가용 → bot-br   r   r  N)r"   unlinkr   r  s        r2    test_no_timer_file_returns_bot_bz5TestFindAvailableBot.test_no_timer_file_returns_bot_b  sF    (+==
113   rJ   c                 h    |dz  dz  }|j                  dd       |j                         }|dk(  sJ y)u@   timer 파일이 깨진 JSON이면 모든 봇 가용으로 처리r   r   r'  r   r   r  N)r   r   r  s        r2   !test_corrupted_json_returns_bot_bz6TestFindAvailableBot.test_corrupted_json_returns_bot_b$  sB    (+==
/'B113   rJ   N)ru   rv   rw   rx   r  r  r  r  r  r   r#  r%  rR   rJ   r2   r  r    s-    B!!!/&!
!!!rJ   r  c                   (    e Zd ZdZd Zd Zd Zd Zy)TestPatchTimerMetadatau8   _patch_timer_metadata() 메타데이터 패치 테스트c                     |dz  dz  }dddddii}|j                  t        j                  |      d	       |j                  ddd
       t        j                  |j                               }|d   d   d   dk(  sJ |d   d   d   d
k(  sJ y)u3   기존 태스크에 role/bot 메타데이터 추가r   r   rO   r   r  r   r  r   r   r  r  r  r  r  Nr   r%   r   _patch_timer_metadatar   r   r  s         r2   test_patches_existing_taskz1TestPatchTimerMetadata.test_patches_existing_task4  s    (+==
*+&STUdjj.A**:KW*UJ0023gz*62kAAAgz*51W<<<rJ   c                     |dz  dz  }di i}|j                  t        j                  |      d       |j                  ddd	       t        j                  |j                               }d|d   vsJ y
)u9   존재하지 않는 태스크에 패치 시 에러 없음r   r   rO   r   r   r  r  r  r)  Nr*  r  s         r2   test_nonexistent_task_no_errorz5TestPatchTimerMetadata.test_nonexistent_task_no_error>  su    (+==
}djj.A**<kw*WJ00236'?222rJ   c                 |    |dz  dz  }|j                         r|j                          |j                  ddd       y)u'   timer 파일이 없어도 에러 없음r   r   r   rb   r  r)  N)r"   r"  r+  r+  s       r2   test_no_timer_file_no_errorz2TestPatchTimerMetadata.test_no_timer_file_no_errorG  s?    (+==
**:F*PrJ   c                 B   |dz  dz  }ddddddii}|j                  t        j                  |      d	
       |j                  ddd       t        j                  |j                               }|d   d   d   dk(  sJ |d   d   d   dk(  sJ |d   d   d   dk(  sJ y)u2   기존 필드는 유지하면서 새 필드 추가r   r   rO   r   ra   r   testr   r   descriptionr   r   rb   r  r)  r   r4  r  Nr*  r  s         r2   test_preserves_existing_fieldsz5TestPatchTimerMetadata.test_preserves_existing_fieldsO  s    (+==
*+ci&jkldjj.A**:F*PJ0023gz*95DDDgz*=9VCCCgz*62f<<<rJ   N)ru   rv   rw   rx   r,  r.  r0  r5  rR   rJ   r2   r'  r'  1  s    B=3Q	=rJ   r'  c                   6    e Zd ZdZd	dZd Zd Zd Zd Zd Z	y)
TestMarketingConsultingDispatchu*   마케팅/컨설팅 팀 dispatch 테스트Nc                 v    t               }||_        ||nt        j                  ddi      |_        ||_        |S Nr   rN  rO  rS  s        r2   rU  z5TestMarketingConsultingDispatch._make_mock_subprocessc  =    k!+'-'9Vtzz8UYJZ?[#rJ   c           
         |dz  dz  }|j                  t        j                  di i      d       | j                  dt        j                  ddi            }t	        j
                  |d	      5 }t	        j
                  |d
ddddd      5  ||j                  _        |j                  dd      }ddd       ddd       d   dk(  sJ |d   dk(  sJ y# 1 sw Y   &xY w# 1 sw Y   *xY w)u   marketing 팀 dispatch 성공r   r   rO   r   r   r   r   rN  rX  rY  rZ  r  r  r[  r  r  u   마케팅 콘텐츠 작성Nr^  r/   	r   r%   r   rU  r   r`  ra  rb  r<   rf   r]   r:   r   rT  rd  r   s          r2   test_marketing_dispatch_successz?TestMarketingConsultingDispatch.test_marketing_dispatch_successj  s    (+==
djj'27'J00DJJ$?O4PQ LL|4		V8@LL	R		V )4HLL%!**;8TUF		V 		V h<///f~,,,		V 		V 		V 		V$   .C $C/C C	C  C)c           
         |dz  dz  }|j                  t        j                  di i      d       | j                  dt        j                  ddi            }t	        j
                  |d	      5 }t	        j
                  |d
ddddd      5  ||j                  _        |j                  dd      }ddd       ddd       d   dk(  sJ |d   dk(  sJ y# 1 sw Y   &xY w# 1 sw Y   *xY w)u   consulting 팀 dispatch 성공r   r   rO   r   r   r   r   rN  rX  rY  rZ  r  r  r[  r  r  u   보험 약관 비교Nr^  r/   r<  r=  s          r2    test_consulting_dispatch_successz@TestMarketingConsultingDispatch.test_consulting_dispatch_success~  s    (+==
djj'27'J00DJJ$?O4PQ LL|4		Q8@LL	R		Q )4HLL%!**<9OPF		Q 		Q h<///f~---		Q 		Q 		Q 		Qr?  c                    |dz  dz  }ddddddddddd	ddd
ddddddddddddi}|j                  t        j                  |      d       t        j                  |d      5 }t        j                  |ddddddddddd	      5  t        ddd       |j                  _        |j                  d!d"      }d#d#d#       d#d#d#       d$   d%k(  sJ d&|d'   v sJ y## 1 sw Y   %xY w# 1 sw Y   )xY w)(u<   모든 봇이 사용 중일 때 marketing dispatch → errorr   r   rO   ra   r   r  rj   rn   r  r  r  r  r  r  r   r   rX  rY  rZ  r  r  key4key5key6key7key8r[  )	rb   rk   ro   dev4dev5dev6dev7dev8r]  r   rE  r   rl  r  r  Nr   rg  r  rh  )	r   r%   r   r   r`  r   ra  rb  r<   rf   r]   r:   r   r   rd  r   s          r2   *test_marketing_all_bots_busy_returns_errorzJTestMarketingConsultingDispatch.test_marketing_all_bots_busy_returns_error  sI   (+==
(3yI(3yI(3yI(3yI(3yI(3yI(3yI(3yI	
 	djj.A LL|4	L8@LL""""""""$
	L$ )2QtTV(WHLL%!**;8JKF'	L 	L* h7***)VI->>>>-	L 	L 	L 	Ls$   *"C,0C <C, C)	%C,,C5c           	         |dz  dz  }|j                  t        j                  di i      d       t        j                  |dddd	d
d      5  |j                  dd      }ddd       d   dk(  sJ d|d   v sJ y# 1 sw Y   xY w)uA   선택된 봇의 키가 None이면 error 반환 (sys.exit 아님)r   r   rO   r   r   rY  Nr  r  r[  r  r  u   봇 키 테스트r   rg  rt  rh  )r   r%   r   r   r`  r<   r  s        r2   )test_marketing_bot_key_none_returns_errorzITestMarketingConsultingDispatch.test_marketing_bot_key_none_returns_error  s    (+==
djj'27'J\\66)L
 	M
 "**;8KLF	M h7***8F9<MMMM	M 	Ms   A<<Bc           
         |dz  dz  }|j                  t        j                  di i      d       | j                  dt        j                  ddi            }t	        j
                  |d	      5 }t	        j
                  |d
ddddd      5  ||j                  _        |j                  dd      }ddd       ddd       t        j                  |j                               }d   }d|v r|n| d}	|	|d   v r8|d   |	   j                  d      dk(  sJ |d   |	   j                  d      dv sJ yy# 1 sw Y   xY w# 1 sw Y   xY w)u6   마케팅 dispatch 시 role/bot 메타데이터 기록r   r   rO   r   r   r   r   rN  rX  rY  rZ  r  r  r[  r  r  u   메타데이터 테스트Nr   r  z.1r  r  )r  r  r
  )r   r%   r   rU  r   r`  ra  rb  r<   r   r   r!   )
rf   r]   r:   r   rT  rd  r   r   r   	timer_keys
             r2    test_marketing_metadata_recordedz@TestMarketingConsultingDispatch.test_marketing_metadata_recorded  sb   (+==
djj'27'J00DJJ$?O4PQ LL|4		U8@LL	R		U )4HLL%!**;8STF		U 		U zz*..01#"g~GgYb>	W%=+//7;FFF=+//6:UUUU &!		U 		U 		U 		Us$   .D>$D2/D>2D;	7D>>Er  )
ru   rv   rw   rx   rU  r>  rA  rN  rP  rS  rR   rJ   r2   r7  r7  `  s&    4-(.('?RNVrJ   r7  c                   *    e Zd ZdZddZd Zd Zd Zy)TestTaskIdReplacementu   task_desc 첫 줄의 플레이스홀더 task-id가 실제 task_id로 교체되어 저장되어야 함.

    현재 dispatch.py는 task_desc를 그대로 저장하므로 이 테스트는 실패(RED).
    Nc                 v    t               }||_        ||nt        j                  ddi      |_        ||_        |S r9  rO  rS  s        r2   rU  z+TestTaskIdReplacement._make_mock_subprocess  r:  rJ   c                 &   d}| j                  dt        j                  ddi            }t        j                  |d      5 }t        j                  |dddd	      5  ||j
                  _        |j                  d
|      }ddd       ddd       d   dk(  sJ |d   }|dz  dz  | dz  }|j                         sJ |j                  d      }	|	j                         d   }
|
d| dk(  sJ d| d|
 d       y# 1 sw Y   }xY w# 1 sw Y   xY w)u<  task_desc 첫 줄에 '# task-999.1: 제목' 패턴이 있으면
        저장된 파일의 첫 줄이 실제 task_id로 교체되어야 함.

        예: task_desc = "# task-999.1: 테스트 작업

내용"
        생성된 task_id가 task-1.1이면 저장 파일 첫 줄 = "# task-1.1: 테스트 작업"
        uE   # task-999.1: 테스트 작업

상세 내용이 여기 있습니다.r   r   rN  rX  rY  r  r[  r  rj   Nr^  r   r   rO   r   r   r   # u   : 테스트 작업+   첫 줄이 교체되지 않음. 기대: '# u   : 테스트 작업', 실제: ''rU  r%   r   r   r`  ra  rb  r<   r"   r   
splitlinesrf   r]   r:   r   rT  rd  r   actual_task_idr   saved_content
first_lines              r2   *test_task_desc_first_line_task_id_replacedz@TestTaskIdReplacement.test_task_desc_first_line_task_id_replaced  sR    ^	00DJJ$?O4PQ LL|4	C8@LLzF93UV	C )4HLL%!**;	BF	C 	C h<///	*x''1~6Fc4JJ	!!!!++W+="--/2
 B~..@AA	u88HHfgqfrrst	uA#	C 	C 	C 	Cs$    D$C;?D;D	 DDc                    d}| j                  dt        j                  ddi            }t        j                  |d      5 }t        j                  |dddd	      5  ||j
                  _        |j                  d
|      }ddd       ddd       d   dk(  sJ |d   }|dz  dz  | dz  }|j                         sJ |j                  d      }	|	j                         d   }
|
dk(  sJ d|
 d       y# 1 sw Y   vxY w# 1 sw Y   zxY w)uY   task_desc 첫 줄에 task-id 패턴이 없으면 내용이 그대로 저장되어야 함.u/   # 일반 제목

내용이 여기 있습니다.r   r   rN  rX  rY  r  r[  r  rj   Nr^  r   r   rO   r   r   r   u   # 일반 제목uK   첫 줄이 의도치 않게 변경됨. 기대: '# 일반 제목', 실제: 'rZ  r[  r]  s              r2   (test_task_desc_without_task_id_unchangedz>TestTaskIdReplacement.test_task_desc_without_task_id_unchanged  sL   G	00DJJ$?O4PQ LL|4	C8@LLzF93UV	C )4HLL%!**;	BF	C 	C h<///	*x''1~6Fc4JJ	!!!!++W+="--/2
 ..  	L2}  I  ~J  JK  1L  	L.!	C 	C 	C 	Cs$    D $C4?D 4C=	9D  D	c                    d}d}| j                  dt        j                  ddi            }|dz  dz  }d|d	d
dii}|j                  t        j                  |      d       t	        j
                  |d      5 }t	        j
                  |dddd      5  ||j                  _        |j                  d||      }	ddd       ddd       	d   dk(  sJ |	d   |k(  sJ |dz  dz  | dz  }
|
j                         sJ |
j                  d      }|j                         d   }|d| dk(  sJ d| d| d       y# 1 sw Y   xY w# 1 sw Y   xY w)uC  --task-id로 task_id를 직접 지정한 경우에도
        task_desc 첫 줄의 플레이스홀더가 지정된 task_id로 교체되어야 함.

        예: task_id="task-42.1", task_desc="# task-999.1: 명시 지정 테스트

내용"
        → 저장 파일 첫 줄 = "# task-42.1: 명시 지정 테스트"
        u5   # task-999.1: 명시 지정 테스트

상세 내용.z	task-42.1r   r   rN  r   r   rO   r   z2026-01-01T00:00:00)r   reserved_atr   r   rX  rY  r  r[  r  rj   )r   Nr^  r   r   rX  u   : 명시 지정 테스트rY  u%   : 명시 지정 테스트', 실제: 'rZ  )rU  r%   r   r   r   r`  ra  rb  r<   r"   r   r\  )rf   r]   r:   r   explicit_task_idrT  r   
timer_datard  r   r   r_  r`  s                r2   )test_task_desc_with_explicit_task_id_flagz?TestTaskIdReplacement.test_task_desc_with_explicit_task_id_flag,  s    N	&00DJJ$?O4PQ (+==
 0ZXm2nop
djj4wG LL|4	]8@LLzF93UV	] )4HLL%!**;	K[*\F	] 	] h<///i $4444x''17G6H4LL	!!!!++W+="--/2
 B/00IJJ	~89I8JJopzo{{|}	~J#	] 	] 	] 	]s$   9E&D;:E;E	 EEr  )ru   rv   rw   rx   rU  ra  rc  rh  rR   rJ   r2   rU  rU    s    
u<L."~rJ   rU  c                   *    e Zd ZdZddZd Zd Zd Zy)TestDispatchFailureCleanupup  dispatch() 실패 시 task-timers.json에 잔여 task_id가 없어야 함.

    현재 dispatch.py에서 project_id 디렉토리 없음(line 412)은 _cleanup_task를 호출하지 않고
    바로 return하며, dev팀 봇 키 None(line 475)은 sys.exit(1)을 호출하므로
    두 경우 모두 task-timers.json에 orphan 항목이 남는 버그가 있음(RED).
    Nc                 r    |dz  dz  }d|xs i i}|j                  t        j                  |      d       |S )u+   테스트용 빈 task-timers.json 초기화r   r   rO   r   r   r   r%   r   )rf   r:   extra_tasksr   r   s        r2   _setup_timer_filez,TestDispatchFailureCleanup._setup_timer_file^  sC    (+==
*+djj.ArJ   c                     |dz  dz  }|j                         si S t        j                  |j                  d            j	                  di       S )u4   현재 task-timers.json의 tasks 딕셔너리 반환r   r   r   r   rO   )r"   r%   r   r   r!   )rf   r:   r   s      r2   _get_timer_tasksz+TestDispatchFailureCleanup._get_timer_taskse  sM    (+==
  "Izz*...@AEEgrRRrJ   c                 R   | j                  |       t               }d|_        d|_        d|_        t        j                  |d      5 }t        j                  |dddd      5  ||j                  _        |j                  dd	d
      }ddd       ddd       d   dk(  sJ d|d   v sJ | j                  |      }|j                         D cg c]  \  }}|j                  d      dv s| }	}}t        |	      dk(  s
J d|	        y# 1 sw Y   xY w# 1 sw Y   xY wc c}}w )u	  project_id를 지정했는데 해당 디렉토리가 없으면
        error를 반환하고 task-timers.json에 해당 task_id가 남아있지 않아야 함.

        현재 dispatch.py line 412에서 _cleanup_task 없이 return → orphan 발생(버그).
        r   r   rX  rY  rZ  r[  r\  ra   u   프로젝트 없는 작업znonexistent-project-xyzr   Nr   rg  rm  rh  r   r   >   실패 후 task-timers.json에 orphan 항목이 남아있음: )rn  r   rP  rQ  rR  r   r`  ra  rb  r<   rp  r  r!   rr   
rf   r]   r:   rT  rd  r   rO   r1   entry
orphan_idss
             r2   &test_cleanup_on_project_dir_not_existszATestDispatchFailureCleanup.test_cleanup_on_project_dir_not_existsl  s?    	x(  k!" LL|4		8@LLzF93UV		 )4HLL%!**,4 + F		 		 h7***!VI%6666 %%h/,1KKMljc5UYYx=PTk=kcl
l:!#r'efpeq%rr#'		 		 		 		$ ms0   D"&DDD#+D#D	DD c                 j   | j                  |       t               }d|_        d|_        d|_        t        j                  |d      5 }t        j                  |dddd      5  ||j                  _        |j                  dd	      }ddd       ddd       t        t              sJ d
       |d   dk(  sJ | j                  |      }|j                         D cg c]  \  }}|j                  d      dv s| }	}}t        |	      dk(  s
J d|	        y# 1 sw Y   xY w# 1 sw Y   xY wc c}}w )uB  dev팀 봇 키가 None일 때 error를 반환하고
        task-timers.json에 해당 task_id가 남아있지 않아야 함.

        현재 dispatch.py line 475에서 sys.exit(1) 호출 → _cleanup_task 미호출(버그).
        이 테스트는 sys.exit 대신 error dict를 반환하는 동작을 기대함.
        r   r   rX  rY  Nr[  r  rj   u   봇 키 없는 작업u?   봇 키 None 시 sys.exit 대신 error dict를 반환해야 함r   rg  rr  rs  )rn  r   rP  rQ  rR  r   r`  ra  rb  r<   r   r  rp  r  r!   rr   rt  s
             r2   test_cleanup_on_bot_key_missingz:TestDispatchFailureCleanup.test_cleanup_on_bot_key_missing  s?    	x(k!" LL|4	Q8@LLzD3ST	Q )4HLL% "**;8OPF	Q 	Q &$'j)jj'h7*** %%h/,1KKMljc5UYYx=PTk=kcl
l:!#r'efpeq%rr#!	Q 	Q 	Q 	Q ms0   D#"$DD#D/7D/D 	D##D,r  )ru   rv   rw   rx   rn  rp  rw  ry  rR   rJ   r2   rj  rj  V  s    S!sFsrJ   rj  c                   @    e Zd ZdZddedefdZd Zd Zd Z	d Z
d	 Zy
)TestDispatchPhasedChaininguU   dispatch() + phases 파라미터로 chain_manager.py create를 호출하는 테스트rP  rQ  c                 D    t               }||_        ||_        d|_        |S )u+   subprocess.run mock 결과를 생성한다.r   )r   rP  rQ  rR  )rf   rP  rQ  mocks       r2   _make_dispatch_mockz.TestDispatchPhasedChaining._make_dispatch_mock  s#    {$rJ   c                 .  
 ddl }g 

fd}t        j                  |d      5 }t        j                  |dddd      5  ||j                  _        |j
                  |_        |j                  d	d
dd      }ddd       ddd       d   dk(  sJ 
D cg c]  }t        |d          }}
D cg c]&  }dt        |d         v sdt        |d         v s%|( }	}t        |	      dk\  s
J d|        y# 1 sw Y   xY w# 1 sw Y   xY wc c}w c c}w )uQ   --phases 5로 dispatch 시 chain_manager.py create가 subprocess로 호출된다.r   Nc                      j                  | r| d   ng |d       t               }d|_        t        j                  ddi      |_        d|_        |S r  r  r  s      r2   mock_runzTTestDispatchPhasedChaining.test_dispatch_with_phases_creates_chain.<locals>.mock_run  r  rJ   rX  rY  rZ  r[  r\  ra   !   # task-566.1: 체이닝 테스트
task-566.1r   r   phasesr   r^  r8  chain_manager.pycreater6   u9   chain_manager.py create 호출이 없음. 실제 호출: )	rX  r   r`  ra  r@  r  r<   r=   rr   )rf   r]   r:   r  r  rd  r   r  all_cmdschain_create_callsr  s             @r2   'test_dispatch_with_phases_creates_chainzBTestDispatchPhasedChaining.test_dispatch_with_phases_creates_chain  s>   ,		 LL|4	8@LLzF93UV	 (0HLL$&5&D&DH#!**3$	 + F	 	 h</// -66qC&	N66 
$6#ai.$HXY\]^_e]fYgMgA
 
 %&!+s/hiqhr-ss++	 	 	 	" 7
s:   D8C56DD/DDD5C>	:DD
c                   
 ddl }g 

fd}t        j                  |d      5 }t        j                  |dddd      5  ||j                  _        |j
                  |_        |j                  d	d
dd       ddd       ddd       t        
      dk\  sJ d       
d   }|j                  d      dz   }||   }t        j                  |      }	t        |	      dk(  sJ |	d   d   dk(  sJ |	d   d   dk(  sJ |	d   d   dk(  sJ |	d   d   dk(  sJ |	d   d   dk(  sJ |	d   d   dk(  sJ y# 1 sw Y   xY w# 1 sw Y   xY w)uW   phases=3이면 task-566.1, task-566.2, task-566.3 형식의 tasks JSON이 생성된다.r   Nc                      | r| d   ng }dt        |      v rdt        |      v rj                  |       t               }d|_        t	        j
                  ddi      |_        d|_        |S Nr   r  r  r   rN  r   r=   r  r   rP  r%   r   rQ  rR  r8  r9  r  rT  captured_chain_argss       r2   r  z_TestDispatchPhasedChaining.test_dispatch_phases_generates_correct_chain_tasks.<locals>.mock_run  h    !$q'rC!SX-(c#h2F#**3/#+K%&K"!%Xt,<!=K!#KrJ   rX  rY  rZ  r[  r\  ra   r  r  r  r  r6   (   chain_manager.py create 호출이 없음z--tasksr   z
task-566.2r5  z
task-566.3order)rX  r   r`  ra  r@  r  r<   rr   indexr%   r   )rf   r]   r:   r  r  rd  r  	tasks_idx
tasks_jsonrO   r  s             @r2   2test_dispatch_phases_generates_correct_chain_taskszMTestDispatchPhasedChaining.test_dispatch_phases_generates_correct_chain_tasks  s   ,$&	 LL|4	8@LLzF93UV	 (0HLL$&5&D&DH#!!3$	 " 	 	 &'1,X.XX,!!$IIi(1,	^


:&5zQQx	"l222Qx	"l222Qx	"l222Qx A%%%Qx A%%%Qx A%%%5	 	 	 	s"   D98D-6D9-D6	2D99Ec                   	 ddl }g 		fd}t        j                  |d      5 }t        j                  |dddd      5  ||j                  _        |j
                  |_        |j                  d	d
dd       ddd       ddd       t        	      dk\  sJ d       	d   }|j                  d      dz   }||   }|dk(  s
J d|        y# 1 sw Y   TxY w# 1 sw Y   XxY w)uP   task_id에서 base 번호를 올바르게 추출하여 chain_id를 생성한다.r   Nc                      | r| d   ng }dt        |      v rdt        |      v rj                  |       t               }d|_        t	        j
                  ddi      |_        d|_        |S r  r  r  s       r2   r  z\TestDispatchPhasedChaining.test_dispatch_phases_extracts_base_from_task_id.<locals>.mock_run  r  rJ   rX  rY  rZ  r[  r\  ra   r  r  r5  r  r6   r  z
--chain-id
scoped-566u!   chain_id가 예상값과 다름: )	rX  r   r`  ra  r@  r  r<   rr   r  )
rf   r]   r:   r  r  rd  r  chain_id_idxr   r  s
            @r2   /test_dispatch_phases_extracts_base_from_task_idzJTestDispatchPhasedChaining.test_dispatch_phases_extracts_base_from_task_id  s    ,$&	 LL|4	8@LLzF93UV	 (0HLL$&5&D&DH#!!3$	 " 	 	 &'1,X.XX,!!$yy.2|$<'W+LXJ)WW''	 	 	 	s"   C8C	6C	C	CCc                 T   ddl }dgfd}t        j                  |d      5 }t        j                  |dddd      5  ||j                  _        |j
                  |_        |j                  d	d
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)uP   chain_manager.py create 실패 시에도 dispatch 자체는 성공해야 한다.r   Nc                      | r| d   ng }dxx   dz  cc<   t               }dt        |      v r#dt        |      v rd|_        d|_        d|_        n#d|_        t        j                  ddi      |_        d|_        |S )	Nr   r6   r  r  r   zchain create errorr   rN  )r   r=   rP  rQ  rR  r%   r   )r8  r9  r  rT  r;  s       r2   r  z`TestDispatchPhasedChaining.test_dispatch_chain_failure_does_not_abort_dispatch.<locals>.mock_run=  s    !$q'rCqMQM#+K!SX-(c#h2F)*&%'"%9")*&%)ZZ40@%A"!#KrJ   rX  rY  rZ  r[  r\  ra   r  r  r   r  r   r^  r  )rf   r]   r:   r  r  rd  r   r;  s          @r2   3test_dispatch_chain_failure_does_not_abort_dispatchzNTestDispatchPhasedChaining.test_dispatch_chain_failure_does_not_abort_dispatch7  s    ,S
	  LL|4	8@LLzF93UV	 (0HLL$&5&D&DH#!**3$	 + F	 	 h<///	 	 	 	s"   B8B7BB	BB'c                 r   ddl }d }t        j                  |d      5 }t        j                  |dddd      5  ||j                  _        |j
                  |_        |j                  d	d
dd      }ddd       ddd       d   dk(  sJ d|v sJ d       |d   dk(  sJ y# 1 sw Y   1xY w# 1 sw Y   5xY w)u>   phases 지정 시 dispatch 결과에 chain_id가 포함된다.r   Nc                  n    t               }d|_        t        j                  ddi      |_        d|_        |S )Nr   r   rN  r   rO  )r8  r9  rT  s      r2   r  zcTestDispatchPhasedChaining.test_dispatch_result_includes_chain_id_when_phases_set.<locals>.mock_run`  s5    #+K%&K"!%Xt,<!=K!#KrJ   rX  rY  rZ  r[  r\  ra   r  r  r  r  r   r^  r   u%   dispatch 결과에 chain_id가 없음r  r  )rf   r]   r:   r  r  rd  r   s          r2   6test_dispatch_result_includes_chain_id_when_phases_setzQTestDispatchPhasedChaining.test_dispatch_result_includes_chain_id_when_phases_set\  s    ,	 LL|4	8@LLzF93UV	 (0HLL$&5&D&DH#!**3$	 + F	 	 h<///V#L%LL#j!\111	 	 	 	s"   B-8B!1B-!B*	&B--B6N)r   r   )ru   rv   rw   rx   intr=   r~  r  r  r  r  r  rR   rJ   r2   r{  r{    s7    _c s #tJ*&X#XJ#0J2rJ   r{  c                   ^    e Zd ZdZddZddededefdZd Zd	 Z	d
 Z
d Zd Zd Zd Zd Zy)TestDispatchParallelBlocku  동일 팀에 running 태스크가 있을 때 dispatch 거부(force=False) / 허용(force=True) 테스트.

    P1 요구사항 (task-792.1):
    - force=False(기본값): 동일 팀 running 태스크가 있으면 status="error" 반환 + task_id 포함
    - force=True: 기존처럼 경고만 로깅하고 dispatch 진행
    - running 태스크 없으면: force 무관하게 정상 통과
    - cleanup 보장: 거부 시에도 _cleanup_task() 호출 필수
    Nc                 v    t               }||_        ||nt        j                  ddi      |_        ||_        |S r9  rO  rS  s        r2   rU  z/TestDispatchParallelBlock._make_mock_subprocess  r:  rJ   r:   r   running_task_idc                 v    |dz  dz  }d||dddii}|j                  t        j                  |      d       |S )	uD   task-timers.json에 지정 팀의 running 태스크를 등록한다.r   r   rO   r   u   기존 진행 중 작업r3  r   r   rl  )rf   r:   r   r  r   r   s         r2   _setup_running_taskz-TestDispatchParallelBlock._setup_running_task  sV    (+==
&'#="
 	djj.ArJ   c                 j   | j                  |d      }t        j                  |d      5 }t        j                  |dddd      5  | j                  d      |j                  _        |j                  dd	d
      }ddd       ddd       d   dk(  s
J d|        y# 1 sw Y   $xY w# 1 sw Y   (xY w)u\   동일 팀에 running 태스크가 있고 force=False이면 status='error' 반환해야 함.ra   r   rX  rY  rZ  r[  r\  r   
   새 작업FforceNr   rg  u-   force=False 시 error 반환 기대, 실제: r  r   r`  rU  ra  rb  r<   rf   r]   r:   
running_idrd  r   s         r2   5test_running_task_same_team_force_false_returns_errorzOTestDispatchParallelBlock.test_running_task_same_team_force_false_returns_error  s    --h-L
 LL|4	S8@LLzF93UV	S )-(B(B1(EHLL%!**;E*RF	S 	S h7*d.[\b[c,dd*	S 	S 	S 	Ss#   B)5B:B)B&	"B))B2c                    | j                  |dd      }t        j                  |d      5 }t        j                  |dddd      5  | j                  d	      |j                  _        |j                  dd
d      }ddd       ddd       d   dk(  sJ d|d   v sJ d|d           y# 1 sw Y   0xY w# 1 sw Y   4xY w)uG   에러 메시지에 현재 running 태스크 ID가 포함되어야 함.ra   
task-100.1r   r  rX  rY  rZ  r[  r\  r   r  Fr  Nr   rg  rh  uN   에러 메시지에 running task ID 'task-100.1' 미포함. 실제 메시지: r  r  s         r2   Btest_running_task_same_team_error_message_contains_running_task_idz\TestDispatchParallelBlock.test_running_task_same_team_error_message_contains_running_task_id  s    --h]i-j
 LL|4	S8@LLzF93UV	S )-(B(B1(EHLL%!**;E*RF	S 	S h7***F9--	p[\bcl\m[no	p-	S 	S 	S 	Ss#   B65B*;B6*B3	/B66B?c           	         | j                  |d       t        j                  |d      5 }t        j                  |dddd      5  | j                  dt	        j
                  d	d
i            |j                  _        |j                  ddd      }ddd       ddd       d	   dk(  s
J d|        y# 1 sw Y   $xY w# 1 sw Y   (xY w)u[   동일 팀 running 태스크가 있어도 force=True이면 dispatch가 허용되어야 함.ra   r  rX  rY  rZ  r[  r\  r   r   rN     강제 작업Tr  Nr^  u*   force=True 시 dispatched 기대, 실제: 	r  r   r`  rU  r%   r   ra  rb  r<   ro  s        r2   6test_running_task_same_team_force_true_allows_dispatchzPTestDispatchParallelBlock.test_running_task_same_team_force_true_allows_dispatch  s      ; ? LL|4	U8@LLzF93UV	U )-(B(B1djjRZ\`QaFb(cHLL%!**;t*TF	U 	U h</f3]^d]e1ff/	U 	U 	U 	Us$   B?AB3B?3B<	8B??Cc           	         |dz  dz  }|j                  t        j                  di i      d       t        j                  |d      5 }t        j                  |ddd	d
      5  | j                  dt        j                  ddi            |j                  _        |j                  d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)uO   running 태스크가 없으면 force=False라도 정상 dispatch 되어야 함.r   r   rO   r   r   rX  rY  rZ  r[  r\  r   r   rN  ra   u   일반 작업Fr  Nr^  	r   r%   r   r   r`  rU  ra  rb  r<   rf   r]   r:   r   rd  r   s         r2   (test_no_running_task_dispatches_normallyzBTestDispatchParallelBlock.test_no_running_task_dispatches_normally  s     (+==
djj'27'J LL|4	V8@LLzF93UV	V )-(B(B1djjRZ\`QaFb(cHLL%!**;u*UF	V 	V h<///	V 	V 	V 	Vs%   C"AC-CC	CCc           	         | j                  |dd       t        j                  |d      5 }t        j                  |dddd      5  | j                  d	t	        j
                  d
di            |j                  _        |j                  ddd      }ddd       ddd       d
   dk(  s
J d|        y# 1 sw Y   $xY w# 1 sw Y   (xY w)uV   다른 팀에 running 태스크가 있어도 현재 팀은 차단하지 않아야 함.rj   
task-200.1r  rX  rY  rZ  r[  r\  r   r   rN  ra   u   다른 팀 있을 때 작업Fr  Nr^  u>   다른 팀 running 태스크가 차단하면 안 됨. 실제: r  ro  s        r2   /test_running_task_different_team_does_not_blockzITestDispatchParallelBlock.test_running_task_different_team_does_not_block  s     	  ;P\ ] LL|4	e8@LLzF93UV	e )-(B(B1djjRZ\`QaFb(cHLL%!**;8V^c*dF	e 	e h</z3qrxqy1zz/	e 	e 	e 	es$   C AB4C 4B=	9C  C	c                    | j                  |d       g fd}t        j                  |d      5 }t        j                  |dddd      5  t        j                  |d	|
      5  | j                  d      |j                  _        |j                  ddd      }ddd       ddd       ddd       d   dk(  sJ t              dk(  s
J d        y# 1 sw Y   <xY w# 1 sw Y   @xY w# 1 sw Y   DxY w)uK   force=False로 거부 시 _cleanup_task()가 반드시 호출되어야 함.ra   r  c                 (    j                  |        y r  r  r  s    r2   r  zMTestDispatchParallelBlock.test_block_calls_cleanup_task.<locals>.mock_cleanup  r  rJ   rX  rY  rZ  r[  r\  r  r?  r   r  Fr  Nr   rg  r6   u=   _cleanup_task가 정확히 1회 호출되어야 함, 실제: )r  r   r`  rU  ra  rb  r<   rr   )rf   r]   r:   r  rd  r   r  s         @r2   test_block_calls_cleanup_taskz7TestDispatchParallelBlock.test_block_calls_cleanup_task  s     ; ?	+ LL|4	S8@LLzF93UV	S LLLQ	S
 )-(B(B1(EHLL%!**;E*RF	S 	S 	S h7***>"a'y+hiwhx)yy'	S 	S 	S 	S 	S 	Ss;   C.C"&5CC"#C.CC""C+	'C..C7c           	         | j                  |d       t        j                  |d      5 }t        j                  |dddd      5  | j                  dt	        j
                  d	d
i            |j                  _        |j                  ddd      }ddd       ddd       d	   dk7  s
J d|        |d	   dk(  sJ y# 1 sw Y   .xY w# 1 sw Y   2xY w)uM   force=True일 때 경고만 로깅하고 error를 반환하지 않아야 함.rj   r  rX  rY  r  r[  r  r   r   rN  u   force 허용 작업Tr  Nrg  u4   force=True 시 에러 반환하면 안 됨. 실제: r^  r  ro  s        r2   +test_force_true_only_logs_warning_not_errorzETestDispatchParallelBlock.test_force_true_only_logs_warning_not_error  s      ; ? LL|4	[8@LLzF93UV	[ )-(B(B1djjRZ\`QaFb(cHLL%!**;8MUY*ZF	[ 	[ h7*k.bcibj,kk*h<///	[ 	[ 	[ 	[s$   C	AB=C	=C	C		Cc                 `   | j                  |d       t        j                  |d      5 }t        j                  |dddd      5  | j                  d      |j                  _        |j                  dd	      }d
d
d
       d
d
d
       d   dk(  sJ d       y
# 1 sw Y   !xY w# 1 sw Y   %xY w)uZ   force 파라미터 기본값은 False이어야 함 (명시하지 않으면 거부 동작).ra   r  rX  rY  rZ  r[  r\  r   u   기본값 테스트Nr   rg  uS   force 기본값이 False여야 하며, running 태스크 있으면 거부해야 함r  ro  s        r2   test_force_default_is_falsez5TestDispatchParallelBlock.test_force_default_is_false	  s      ; ? LL|4	O8@LLzF93UV	O )-(B(B1(EHLL%!**;8MNF	O 	O h7*  	B  -B  	B*	O 	O 	O 	Os#   B$3B8B$B!	B$$B-r  )ra   r  )ru   rv   rw   rx   rU  r   r=   r  r  r  r  r  r  r  r  r  rR   rJ   r2   r  r  ~  sU    D 3 _b ep g0{z(0BrJ   r  c                       e Zd ZdZd Zd Zy)TestDispatchForceCLIu    --force CLI 플래그 테스트.c           
        
 |dz  dz  }|j                  t        j                  ddddddii      d	
       |j                  t        dg d       g 
|j
                  }
fd}|j                  |d|       ddl}ddlm} |j                         }	 ||	      5  |j                          ddd       t        
      dk(  sJ d       
d   du sJ d
d           y# 1 sw Y   3xY w)uO   --force 플래그가 있으면 dispatch()에 force=True가 전달되어야 함.r   r   rO   r  ra   r   u   기존 작업r3  r   r   r  )r  r  ra   r     force CLI 테스트z--forcec            	      \    j                  |j                  dd             ddddddd	i d
S )Nr  Fr^  r   ra   r   r   r  rN  r   r   r/   r  r   r4  rh  ry  r  r!   r8  r9  captured_forces     r2   mock_dispatchzWTestDispatchForceCLI.test_main_with_force_flag_passes_force_true.<locals>.mock_dispatch6  s@    !!&**We"<= '%#&!4!#	 	rJ   r<   r   Nredirect_stdoutr6   u0   dispatch()가 정확히 1번 호출되어야 함Tu8   --force 플래그 시 force=True 전달 기대, 실제: )r   r%   r   r   r>   r<   io
contextlibr  StringIOr  rr   )rf   r]   r   r:   r   original_dispatchr  r  r  r,   r  s             @r2   +test_main_with_force_flag_passes_force_truez@TestDispatchForceCLI.test_main_with_force_flag_passes_force_true  s    (+==
JJ$'2&/+:'
  	 	
 	k	
 (11	 	L*mD.KKMQ 	 	  >"a'[)[['a D(x,destuevdw*xx(		  	 s   CC%c                   	 |dz  dz  }|j                  t        j                  di i      d       |j                  t        dg d       g 		fd}|j                  |d	|       d
dl}d
dlm} |j                         } ||      5  |j                          ddd       t        	      dk(  sJ 	d
   du sJ d	d
           y# 1 sw Y   .xY w)uP   --force 플래그가 없으면 dispatch()에 force=False가 전달되어야 함.r   r   rO   r   r   r  )r  r  ra   r     일반 CLI 테스트c            	      \    j                  |j                  dd             ddddddd	i d
S )Nr  Fr^  r   ra   r   r   r  rN  r  r  r  s     r2   r  z[TestDispatchForceCLI.test_main_without_force_flag_passes_force_false.<locals>.mock_dispatchY  s>    !!&**We"<=&%#&!5!#	 	rJ   r<   r   Nr  r6   Fu6   --force 없을 때 force=False 전달 기대, 실제: )r   r%   r   r   r>   r  r  r  r  r  rr   )
rf   r]   r   r:   r   r  r  r  r,   r  s
            @r2   /test_main_without_force_flag_passes_force_falsezDTestDispatchForceCLI.test_main_without_force_flag_passes_force_falseP  s    (+==
djj'27'JC)qr	 	L*mD.KKMQ 	 	  >"a'''a E)w-cdrstducv+ww)		  	 s   CCN)ru   rv   rw   rx   r  r  rR   rJ   r2   r  r    s    *3yj xrJ   r  c                   (    e Zd ZdZd Zd Zd Zd Zy)TestWarnResearchImplMixu>   _warn_research_impl_mix 함수의 WARNING 로그 출력 검증c                     d}t        j                  |j                  d      5 }|j                  |d       |j	                          |j
                  d   d   }d|v sJ 	 ddd       y# 1 sw Y   yxY w)uD   리서치+구현 키워드가 모두 포함된 경우 WARNING 출력G   API 문서를 조사하고 Publisher 파이프라인을 구현하세요warningr   r   z[research-impl-mix]N)r   r`  logger_warn_research_impl_mixassert_called_once	call_args)rf   r]   
mixed_desc	mock_warncall_msgs        r2   &test_mixed_research_impl_emits_warningz>TestWarnResearchImplMix.test_mixed_research_impl_emits_warning{  sq    ^
\\,--y9 	5Y00XF((* **1-a0H(H444		5 	5 	5s   ;A((A1c                     d}t        j                  |j                  d      5 }|j                  |d       |j	                          ddd       y# 1 sw Y   yxY w)u3   구현 키워드만 있을 때는 WARNING 미출력uC   Publisher 파이프라인을 구현하고 테스트 작성하세요r  r   Nr   r`  r  r  assert_not_called)rf   r]   	impl_descr  s       r2   test_impl_only_no_warningz1TestWarnResearchImplMix.test_impl_only_no_warning  sN    Y	\\,--y9 	*Y00HE'')	* 	* 	*   #AAc                     d}t        j                  |j                  d      5 }|j                  |d       |j	                          ddd       y# 1 sw Y   yxY w)u6   리서치 키워드만 있을 때는 WARNING 미출력u;   API 문서를 조사하고 인증 방식을 파악하세요r  r   Nr  )rf   r]   research_descr  s       r2   test_research_only_no_warningz5TestWarnResearchImplMix.test_research_only_no_warning  sN    U\\,--y9 	*Y00I'')	* 	* 	*r  c                     d}t        j                  |j                  d      5 }|j                  |d       |j	                          ddd       y# 1 sw Y   yxY w)uN   리서치+구현 혼합이지만 task_type이 research이면 WARNING 미출력r  r  researchNr  )rf   r]   r  r  s       r2   'test_mixed_but_research_type_no_warningz?TestWarnResearchImplMix.test_mixed_but_research_type_no_warning  sN    ^
\\,--y9 	*Y00ZH'')	* 	* 	*r  N)ru   rv   rw   rx   r  r  r  r  rR   rJ   r2   r  r  x  s    H5***rJ   r  c                 .   ddl }ddl}ddlm}  |t        j
                  j                  dd            }t        |      |j                  vr%|j                  j                  dt        |             ddl
}d}| j                  |j                        5  |j                  |       ddd       | j                  D cg c](  }|j                  |j                  k(  s|j                   * }}t#        d |D              s
J d|        y# 1 sw Y   cxY wc c}w )	u?   3000자 이상 지시서에 대해 WARNING 로그 발생 확인r   Nr   r   r     AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAc              3   $   K   | ]  }d |v  
 ywz[large-task-desc]NrR   .0msgs     r2   	<genexpr>z,test_warn_large_task_desc.<locals>.<genexpr>  s      '*s"   uC   WARNING 로그에 '[large-task-desc]' 미포함. 기록된 로그: loggingr>   pathlibr   r   r    r!   r=   r?   r@   r<   at_levelWARNING_warn_large_task_descrecordslevelnorh  any)	caplogr  r>   r   r*   r]   
large_descr3  warning_messagess	            r2   test_warn_large_task_descr    s    RZZ^^$46KLMI
9~SXX%3y>*#J		) 7**:67 ,2>>ZaQYY'//=Y		ZZ .>  `	LM]L^_` 	7 7 [   D:DDDc                 .   ddl }ddl}ddlm}  |t        j
                  j                  dd            }t        |      |j                  vr%|j                  j                  dt        |             ddl
}d}| j                  |j                        5  |j                  |       ddd       | j                  D cg c](  }|j                  |j                  k(  s|j                   * }}t#        d |D              r
J d|        y# 1 sw Y   cxY wc c}w )	u?   3000자 미만 지시서에 대해 WARNING 로그 없음 확인r   Nr   r   r     AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAc              3   $   K   | ]  }d |v  
 ywr  rR   r  s     r2   r  z7test_warn_large_task_desc_under_3000.<locals>.<genexpr>  s      '*s"r  u>   3000자 미만인데 WARNING 로그 발생. 기록된 로그: r   )	r	  r  r>   r   r*   r]   
small_descr3  r  s	            r2   $test_warn_large_task_desc_under_3000r    s    RZZ^^$46KLMI
9~SXX%3y>*#J		) 7**:67 ,2>>ZaQYY'//=Y		ZZ .>  [	GHXGYZ[  	7 7 [r  c                 0   ddl }ddl}ddlm}  |t        j
                  j                  dd            }t        |      |j                  vr%|j                  j                  dt        |             ddl
}d}| j                  |j                        5  |j                  |d       ddd       | j                  D cg c](  }|j                  |j                  k(  s|j                   * }}t#        d |D              s
J d	|        y# 1 sw Y   cxY wc c}w )
uQ   리서치+구현 혼합 경고에 세션 경량화 참조 메시지 포함 확인r   Nr   r   r   r  r   c              3   $   K   | ]  }d |v  
 yw)z/compactNrR   r  s     r2   r  z:test_warn_research_impl_mix_session_msg.<locals>.<genexpr>  s      !
cr  u:   WARNING 로그에 '/compact' 미포함. 기록된 로그: )r  r>   r  r   r   r    r!   r=   r?   r@   r<   r  r  r  r  r  rh  r  )	r	  r  r>   r   r*   r]   r  r3  r  s	            r2   'test_warn_research_impl_mix_session_msgr    s    RZZ^^$46KLMI
9~SXX%3y>*#ZJ		) C,,ZBC ,2>>ZaQYY'//=Y		ZZ %5  W	CDTCUVW 	C C [s   D;DDDc                   4    e Zd ZdZd Zd Zd Zd Zd Zd Z	y)	TestCounterBasedTaskIduN   카운터 파일 기반 generate_task_id() 테스트 (v2 채번 방어 로직)c                     |dz  dz  }|j                  d       |dz  dz  }|j                  t        j                  di i             |j                         }|dk(  sJ y)u5   카운터 파일이 있으면 그 값으로 ID 생성r   .task-counter100r   rO   task-100Nr   rf   r]   r:   counter_filer   r   s         r2   $test_counter_file_determines_next_idz;TestCounterBasedTaskId.test_counter_file_determines_next_id  sf    (*_<&(+==
djj'278//1*$$$rJ   c                     |dz  dz  }|j                  d       |dz  dz  }|j                  t        j                  di i             |j                          |j	                         j                         dk(  sJ y)u+   ID 생성 후 카운터 파일이 +1 증가r   r  50r   rO   51N)r   r%   r   r~   r   striprf   r]   r:   r  r   s        r2   (test_counter_increments_after_generationz?TestCounterBasedTaskId.test_counter_increments_after_generation  sv    (*_<%(+==
djj'278%%'%%'--/4777rJ   c                     |dz  dz  }|j                  d       |dz  dz  }|j                  t        j                  di i             t        d      D cg c]  }|j	                          }}|g dk(  sJ yc c}w )	u9   연속 호출 시 카운터 기반으로 연속 ID 생성r   r  10r   rO   r  )ztask-10ztask-11ztask-12N)r   r%   r   r   r~   )rf   r]   r:   r  r   r   r   s          r2   !test_consecutive_ids_from_counterz8TestCounterBasedTaskId.test_consecutive_ids_from_counter  s    (*_<%(+==
djj'2788=aA1|,,.AA7777 Bs   A8c                     |dz  dz  }|j                  d       |dz  dz  }|j                  t        j                  ddddiii             |j                         }|d	k(  sJ y
)u>   카운터 파일이 손상되면 task-timers.json에서 복구r   r  not_a_numberr   rO   r  r   r   ztask-6Nr   r  s         r2   +test_corrupted_counter_falls_back_to_timerszBTestCounterBasedTaskId.test_corrupted_counter_falls_back_to_timers  sq    (*_</(+==
djj'J;@W3X)YZ[//1("""rJ   c                     |dz  dz  }|j                  t        j                  ddddiii             |j                         }|dk(  sJ y)	u8   카운터 파일 없으면 task-timers.json에서 계산r   r   rO   r   r   r   r   Nr   rf   r]   r:   r   r   s        r2   )test_missing_counter_falls_back_to_timersz@TestCounterBasedTaskId.test_missing_counter_falls_back_to_timers  sT    (+==
djj'J;@W3X)YZ[//1("""rJ   c                     |dz  dz  }|j                  d       |dz  dz  }|j                  t        j                  ddddiii             |j                         }|d	k(  sJ y
)ua   카운터(99999)가 timers 최대(4) 대비 1000 이상 큰 경우 → timers 기준으로 보정r   r  99999r   rO   r.  r   r   r   Nr   r  s         r2   /test_counter_outlier_1000_over_timers_correctedzFTestCounterBasedTaskId.test_counter_outlier_1000_over_timers_corrected  sq    (*_<((+==
djj'Hx>U3V)WXY//1("""rJ   N)
ru   rv   rw   rx   r  r$  r'  r*  r-  r0  rR   rJ   r2   r  r    s#    X%88###rJ   r  c                   .    e Zd ZdZd Zd Zd Zd Zd Zy)TestOutlierFilteringuc   이상치 ID 필터링 테스트 — 비정상 큰 ID가 채번을 오염시키지 않는지 검증c                     |dz  dz  }|j                  t        j                  dddiddiddiddiddidi             |j                         }|dk(  sJ y)	uQ   정상 ID + 비정상적으로 큰 ID가 있을 때 정상 ID 기반으로 채번r   r   rO   r   r   )r   r   r   ztask-9991.1ztask-9992.1r   Nr   r,  s        r2   test_large_gap_ids_filteredz0TestOutlierFiltering.test_large_gap_ids_filtered&  s    (+==
JJ%-{$;%-{$;%-{$;(0+'>(0+'>
	
 //1("""rJ   c           	          |dz  dz  }|j                  t        j                  dddiddiddidi             |j                         }|dk(  sJ y	)
u&   이상치 없으면 기존처럼 max+1r   r   rO   r   r   r   r   r   Nr   r,  s        r2    test_no_outliers_normal_behaviorz5TestOutlierFiltering.test_no_outliers_normal_behavior:  so    (+==
JJ%-{$;%-{$;%-y$9
	
 //1("""rJ   c                     |dz  dz  }|j                  d       |dz  dz  }|j                  t        j                  dddiddidi             |j                         }|d	k(  sJ y
)uH   카운터 파일이 있으면 이상치와 무관하게 카운터 사용r   r  r  r   rO   r   r   )r   ztask-9999.1r  Nr   r  s         r2   +test_counter_takes_precedence_over_outliersz@TestOutlierFiltering.test_counter_takes_precedence_over_outliersK  s    (*_<&(+==
JJ%-{$;(0+'>		
 //1*$$$rJ   c                     |dz  dz  }|j                  t        j                  dddiddidi             |j                         }|dk(  sJ y)	u9   갭 999는 정상, 갭 1000 이상은 이상치로 판정r   r   rO   r   r   )r   ztask-1000.1z	task-1001Nr   r,  s        r2   test_gap_threshold_is_1000z/TestOutlierFiltering.test_gap_threshold_is_1000]  sg    (+==
JJ%-{$;(0+'>		
 //1+%%%rJ   c                     |dz  dz  }|j                  t        j                  dddiddidi             |j                         }|dk(  sJ y)	u%   갭이 정확히 1000이면 이상치r   r   rO   r   r   )r   ztask-1001.1r   Nr   r,  s        r2    test_gap_exactly_1000_is_outlierz5TestOutlierFiltering.test_gap_exactly_1000_is_outliern  sg    (+==
JJ%-{$;(0+'>		
 //1("""rJ   N)	ru   rv   rw   rx   r4  r6  r8  r:  r<  rR   rJ   r2   r2  r2  #  s    m#(#"%$&"#rJ   r2  c                   "    e Zd ZdZd Zd Zd Zy)TestCounterFileEdgeCasesu+   카운터 파일 엣지 케이스 테스트c                 |    |dz  dz  }|j                         rJ |j                          |j                         sJ y)u,   첫 실행 시 카운터 파일이 생성됨r   r  N)r"   r~   rf   r]   r:   r  s       r2   &test_counter_file_created_on_first_runz?TestCounterFileEdgeCases.test_counter_file_created_on_first_run  sA    (*_<&&(((%%'""$$$rJ   c                     |dz  dz  }|j                  d       |dz  dz  }|j                  t        j                  ddddiii             |j                         }|d	k(  sJ y
)u    빈 카운터 파일은 fallbackr   r  r   r   rO   r  r   r   ztask-8Nr   r  s         r2   "test_empty_counter_file_falls_backz;TestCounterFileEdgeCases.test_empty_counter_file_falls_back  sq    (*_<#(+==
djj'J;@W3X)YZ[//1("""rJ   c                     |dz  dz  }|j                  d       |dz  dz  }|j                  t        j                  di i             y)uH   음수 카운터도 그대로 사용 (방어는 하되 동작은 유지)r   r  z-5r   rO   Nrl  r#  s        r2    test_negative_counter_falls_backz9TestCounterFileEdgeCases.test_negative_counter_falls_back  sL      (*_<%(+==
djj'278rJ   N)ru   rv   rw   rx   rA  rC  rE  rR   rJ   r2   r>  r>    s    5%#9rJ   r>  c                   "    e Zd ZdZd Zd Zd Zy)TestSyncCounterPhaseAwareuS   _sync_counter_if_needed()가 Phase 접미사를 올바르게 무시하는지 검증c                     |dz  dz  }|j                  d       |j                  d       |j                         j                         dk(  sJ y)uC   task-1845_2.2 → 기본 번호 1845만 추출하여 카운터 syncr   r  r  ztask-1845_2.21846Nr   _sync_counter_if_neededr   r"  r@  s       r2   test_phase_suffix_ignoredz3TestSyncCounterPhaseAware.test_phase_suffix_ignored  sM    (*_<&,,_=%%'--/6999rJ   c                     |dz  dz  }|j                  d       |j                  d       |j                         j                         dk(  sJ y)u*   task-500_a → 기본 번호 500만 추출r   r  r  z
task-500_a501NrJ  r@  s       r2   test_parallel_suffix_ignoredz6TestSyncCounterPhaseAware.test_parallel_suffix_ignored  M    (*_<&,,\:%%'--/5888rJ   c                     |dz  dz  }|j                  d       |j                  d       |j                         j                         dk(  sJ y)u*   task-300+1 → 기본 번호 300만 추출r   r  r  z
task-300+1301NrJ  r@  s       r2   test_retry_suffix_ignoredz3TestSyncCounterPhaseAware.test_retry_suffix_ignored  rP  rJ   N)ru   rv   rw   rx   rL  rO  rS  rR   rJ   r2   rG  rG    s    ]:99rJ   rG  c                       e Zd ZdZd Zd Zy)TestTaskIdFormatValidationu$   --task-id 포맷 v2 검증 테스트c                 v   t               }d|_        t        j                  ddi      |_        d|_        |j                  t        dg d       t        j                  |d      5 }t        j                  |dd	d
d      5  t        j                  |j                  d      5 }||j                  _        |j                          ddd       ddd       ddd       j                  D cg c]  }dt        |      v s| }}t!        |      dk(  sJ y# 1 sw Y   QxY w# 1 sw Y   UxY w# 1 sw Y   YxY wc c}w )u!   task-100 → 경고 없이 통과r   r   rN  r   r  )r  r  ra   r  rw  	--task-idr  rX  rY  rZ  r[  r\  r  Ntask-id-formatr   rP  r%   r   rQ  rR  r   r>   r   r`  r  ra  rb  r  call_args_listr=   rr   	rf   r]   r   r  rT  rd  r  r  format_warningss	            r2   test_valid_simple_formatz3TestTaskIdFormatValidation.test_valid_simple_format  s    k!"!ZZ4(89C)  	ALL|4	 8@LLzF93UV	  LL,,i8	  =F(3HLL%	  	  	  '0&>&>]BRVYZ[V\B\1]]?#q(((	  	  	  	  	  	  ^H   $D*?!D "DD
D*(D6;D6DDD'	#D**D3c                 v   t               }d|_        t        j                  ddi      |_        d|_        |j                  t        dg d       t        j                  |d      5 }t        j                  |dd	d
d      5  t        j                  |j                  d      5 }||j                  _        |j                          ddd       ddd       ddd       j                  D cg c]  }dt        |      v s| }}t!        |      dk(  sJ y# 1 sw Y   QxY w# 1 sw Y   UxY w# 1 sw Y   YxY wc c}w )u0   task-잘못된형식 → 경고 메시지 출력r   r   rN  r   r  )r  r  ra   r  rw  rW  u   task-잘못된형식rX  rY  rZ  r[  r\  r  NrX  r6   rY  r[  s	            r2   !test_invalid_format_emits_warningz<TestTaskIdFormatValidation.test_invalid_format_emits_warning  s#   k!"!ZZ4(89C  *L  	MLL|4	 8@LLzF93UV	  LL,,i8	  =F(3HLL%	  	  	  '0&>&>]BRVYZ[V\B\1]]?#q(((	  	  	  	  	  	  ^r^  N)ru   rv   rw   rx   r]  r`  rR   rJ   r2   rU  rU    s    .)&)rJ   rU  c                   :    e Zd ZdZd Zd Zd Zd Zd Zd Z	d Z
y	)
TestGetBusyBotsInfou8   _get_busy_bots_info() 봇 점유 정보 반환 테스트c                     |dz  dz  }|j                  t        j                  di i      d       |j                         }|i k(  sJ y)u2   빈 task-timers.json이면 빈 딕셔너리 반환r   r   rO   r   r   Nr   r%   r   _get_busy_bots_infor  s        r2   test_empty_timers_returns_emptyz3TestGetBusyBotsInfo.test_empty_timers_returns_empty  sL    (+==
djj'27'J113||rJ   c                     |dz  dz  }|j                         r|j                          |j                         }|i k(  sJ y)u1   timer 파일이 없으면 빈 딕셔너리 반환r   r   N)r"   r"  re  r  s        r2    test_no_timer_file_returns_emptyz4TestGetBusyBotsInfo.test_no_timer_file_returns_empty  sD    (+==
113||rJ   c                     |dz  dz  }dddddii}|j                  t        j                  |      d	       |j                         }d
|v sJ |d
   d   dk(  sJ |d
   d   dk(  sJ y)u"   dev1-team running → bot-b 점유r   r   rO   r   ra   r   r  r   r   r  r   r   Nrd  r  s         r2   !test_dev_team_running_maps_to_botz5TestGetBusyBotsInfo.test_dev_team_running_maps_to_bot	  s    (+==
*+&STUdjj.A113&   gy)Z777gy)[888rJ   c                     |dz  dz  }ddddddii}|j                  t        j                  |      d	
       |j                         }d|v sJ |d   d   dk(  sJ |d   d   dk(  sJ y)u+   composite 태스크의 bot 필드가 반영r   r   rO   r  	compositer   bot-gr  r   r   r   r   Nrd  r  s         r2   test_composite_bot_field_mappedz3TestGetBusyBotsInfo.test_composite_bot_field_mapped	  s    (+==
,K9]d(efgdjj.A113&   gy)\999gy)[888rJ   c                     |dz  dz  }dddddii}|j                  t        j                  |      d	       |j                         }|i k(  sJ y
)u1   completed 상태는 점유로 간주하지 않음r   r   rO   r   ra   r   r  r   r   Nrd  r  s         r2   test_completed_tasks_excludedz1TestGetBusyBotsInfo.test_completed_tasks_excluded	  s\    (+==
*+&UVWdjj.A113||rJ   c                 h    |dz  dz  }|j                  dd       |j                         }|i k(  sJ y)u)   깨진 JSON이면 빈 딕셔너리 반환r   r   r'  r   r   N)r   re  r  s        r2   !test_corrupted_json_returns_emptyz5TestGetBusyBotsInfo.test_corrupted_json_returns_empty!	  s@    (+==
/'B113||rJ   c                     |dz  dz  }dddddddd	d
dddi}|j                  t        j                  |      d       |j                         }d|v sJ d|v sJ d|vsJ y)u4   여러 running 태스크가 있을 때 모두 반영r   r   rO   ra   r   r  rl  rm  r  rn   r   r   r   r   r  r
  Nrd  r  s         r2   test_multiple_running_tasksz/TestGetBusyBotsInfo.test_multiple_running_tasks(	  s    (+==
(3yI(3yQXY(3{K
 	djj.A113&   &   f$$$rJ   N)ru   rv   rw   rx   rf  rh  rj  rn  rp  rr  rt  rR   rJ   r2   rb  rb    s(    B99%rJ   rb  c                   H    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y)TestDevTeamBotConflictu^   dev팀 dispatch 시 composite/dynamic 작업에 봇이 점유되어 있으면 차단 테스트Nc                 v    t               }||_        ||nt        j                  ddi      |_        ||_        |S r9  rO  rS  s        r2   rU  z,TestDevTeamBotConflict._make_mock_subprocessA	  r:  rJ   c                    |dz  dz  }ddddddii}|j                  t        j                  |      d	
       t        j                  |d      5 }t        j                  |dddd      5  | j                  d      |j                  _        |j                  ddd      }ddd       ddd       d   dk(  sJ d|d   v sJ d|d   v sJ d|d   v sJ y# 1 sw Y   7xY w# 1 sw Y   ;xY w)u>   composite가 bot-g 점유 중 → dev6-team dispatch 시 errorr   r   rO   task-1220.1rl  r   rm  r  r   r   rX  rY  rE  r[  rJ  r]  r   r  r   Fr  Nr   rg  rh  r  rM  s          r2   ,test_composite_on_bot_g_blocks_dev6_dispatchzCTestDevTeamBotConflict.test_composite_on_bot_g_blocks_dev6_dispatchH	  s&   (+==
;)T[\

 	djj.A LL|4	Y8@LLzF93UV	Y )-(B(B1(EHLL%!**;8JRW*XF	Y 	Y h7***&++++fY////y 1111	Y 	Y 	Y 	Y$   C!*5CC!C	C!!C*c                    |dz  dz  }ddddddii}|j                  t        j                  |      d	
       t        j                  |d      5 }t        j                  |dddd      5  | j                  d      |j                  _        |j                  ddd      }ddd       ddd       d   dk(  sJ d|d   v sJ d|d   v sJ y# 1 sw Y   .xY w# 1 sw Y   2xY w)u>   composite가 bot-b 점유 중 → dev1-team dispatch 시 errorr   r   rO   ry  rl  r   r  r  r   r   rX  rY  rZ  r[  r\  r   ra   r   Fr  Nr   rg  rh  r  rM  s          r2   ,test_composite_on_bot_b_blocks_dev1_dispatchzCTestDevTeamBotConflict.test_composite_on_bot_b_blocks_dev1_dispatch^	  s   (+==
;)T[\

 	djj.A LL|4	Y8@LLzF93UV	Y )-(B(B1(EHLL%!**;8JRW*XF	Y 	Y h7***&++++y 1111	Y 	Y 	Y 	Y$   C*5CCC	CC!c           	         |dz  dz  }|j                  t        j                  di i      d       t        j                  |d      5 }t        j                  |ddd	d
      5  | j                  dt        j                  ddi            |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)u?   composite 없을 때 dev6-team 정상 위임 (회귀 테스트)r   r   rO   r   r   rX  rY  rE  r[  rz  r   r   rN  r     정상 작업Nr^  r  r  s         r2   &test_no_composite_allows_dev6_dispatchz=TestDevTeamBotConflict.test_no_composite_allows_dev6_dispatchs	  s    (+==
djj'27'J LL|4	I8@LLzF93UV	I )-(B(B1djjRZ\`QaFb(cHLL%!**;HF	I 	I h<///	I 	I 	I 	Is%   C"A	C+CC	CCc           	         |dz  dz  }ddddddii}|j                  t        j                  |      d	
       t        j                  |d      5 }t        j                  |dddd      5  | j                  dt        j                  ddi            |j                  _        |j                  d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)uN   composite가 bot-g 점유 중이라도 force=True면 dev6-team dispatch 허용r   r   rO   ry  rl  r   rm  r  r   r   rX  rY  rE  r[  rz  r   r   rN  r  r  Tr  Nr^  r  rM  s          r2   .test_composite_on_bot_g_force_true_allows_dev6zETestDevTeamBotConflict.test_composite_on_bot_g_force_true_allows_dev6	  s    (+==
;)T[\

 	djj.A LL|4	U8@LLzF93UV	U )-(B(B1djjRZ\`QaFb(cHLL%!**;t*TF	U 	U h<///	U 	U 	U 	Us%   C*AC5CC	CC%c           	         |dz  dz  }ddddddii}|j                  t        j                  |      d	
       t        j                  |d      5 }t        j                  |dddd      5  | j                  dt        j                  ddi            |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)uI   composite가 completed면 봇 점유 아님 → dev6-team 정상 dispatchr   r   rO   ry  rl  r   rm  r  r   r   rX  rY  rE  r[  rz  r   r   rN  r  r  Nr^  r  rM  s          r2   -test_composite_completed_allows_dev6_dispatchzDTestDevTeamBotConflict.test_composite_completed_allows_dev6_dispatch	  s    (+==
;+V]^

 	djj.A LL|4	I8@LLzF93UV	I )-(B(B1djjRZ\`QaFb(cHLL%!**;HF	I 	I h<///	I 	I 	I 	I%   C*A	C3CC	CC#c                    |dz  dz  }ddddddii}|j                  t        j                  |      d	
       t        j                  |d      5 }t        j                  |dddd      5  | j                  d      |j                  _        |j                  ddd      }ddd       ddd       d   dk(  sJ d|d   v sJ d|d   v sJ y# 1 sw Y   .xY w# 1 sw Y   2xY w)u>   marketing이 bot-g 점유 중 → dev6-team dispatch 시 errorr   r   rO   
task-500.1r  r   rm  r  r   r   rX  rY  rE  r[  rz  r   r  r   Fr  Nr   rg  rh  r  rM  s          r2   ,test_marketing_on_bot_g_blocks_dev6_dispatchzCTestDevTeamBotConflict.test_marketing_on_bot_g_blocks_dev6_dispatch	  s   (+==
+SZ[

 	djj.A LL|4	Y8@LLzF93UV	Y )-(B(B1(EHLL%!**;8JRW*XF	Y 	Y h7***&++++fY////	Y 	Y 	Y 	Yr  c                 .   |dz  dz  }ddddddii}|j                  t        j                  |      d	
       g fd}t        j                  |d      5 }t        j                  |dddd      5  t        j                  |d|      5  | j                  d      |j                  _        |j                  ddd      }ddd       ddd       ddd       d   dk(  sJ t              dk(  sJ y# 1 sw Y   4xY w# 1 sw Y   8xY w# 1 sw Y   <xY w)u3   봇 충돌로 거부 시 _cleanup_task가 호출됨r   r   rO   ry  rl  r   rm  r  r   r   c                 (    j                  |        y r  r  r  s    r2   r  zMTestDevTeamBotConflict.test_conflict_calls_cleanup_task.<locals>.mock_cleanup	  r  rJ   rX  rY  rE  r[  rz  r  r?  r   r  r   Fr  Nr   rg  r6   )
r   r%   r   r   r`  rU  ra  rb  r<   rr   )	rf   r]   r:   r   r   r  rd  r   r  s	           @r2    test_conflict_calls_cleanup_taskz7TestDevTeamBotConflict.test_conflict_calls_cleanup_task	  s6   (+==
;)T[\

 	djj.A	+ LL|4	Y8@LLzF93UV	Y LLLQ	Y
 )-(B(B1(EHLL%!**;8JRW*XF	Y 	Y 	Y h7***>"a'''	Y 	Y 	Y 	Y 	Y 	Ys<   D2C?5C3 C?D3C<8C??D	DDc                    |dz  dz  }dddddii}|j                  t        j                  |      d	       t        j                  |d
      5 }t        j                  |dddd      5  | j                  d      |j                  _        |j                  ddd      }ddd       ddd       d   dk(  sJ d|d   v s
d|d   v sJ yy# 1 sw Y   -xY w# 1 sw Y   1xY w)uZ   같은 팀(dev6-team)의 running 태스크는 봇 충돌이 아닌 팀 충돌로 처리됨r   r   rO   r  r  r   r  r   r   rX  rY  rE  r[  rz  r   r   Fr  Nr   rg  u
   같은 팀rh  r  rM  s          r2   2test_same_team_running_not_treated_as_bot_conflictzITestDevTeamBotConflict.test_same_team_running_not_treated_as_bot_conflict	  s   (+==
+K

 	djj.A LL|4	Y8@LLzF93UV	Y )-(B(B1(EHLL%!**;8JRW*XF	Y 	Y h7***vi00K6)CT4TTT4T0	Y 	Y 	Y 	Ys$   C)5C
C
C	CCr  )ru   rv   rw   rx   rU  r{  r~  r  r  r  r  r  r  rR   rJ   r2   rv  rv  >	  s3    h2,2*00&0&0*(4UrJ   rv  c                   (    e Zd ZdZd Zd Zd Zd Zy)TestGetBusyBotsInfoExcludeuG   _get_busy_bots_info(exclude_task_id=...) 자기 자신 제외 테스트c                     |dz  dz  }dddddii}|j                  t        j                  |      d	       |j                  d
      }|i k(  sJ y)uN   exclude_task_id로 자기 자신을 제외하면 결과에 포함되지 않음r   r   rO   r  ra   r   r  r   r   exclude_task_idNrd  r  s         r2   test_exclude_removes_own_entryz9TestGetBusyBotsInfoExclude.test_exclude_removes_own_entry	  sh    (+==
+K

 	djj.A11,1O||rJ   c                     |dz  dz  }dddddddd	d
i}|j                  t        j                  |      d       |j                  d      }d|v sJ |d   d   dk(  sJ y)uF   exclude_task_id로 자기 자신만 제외, 다른 태스크는 유지r   r   rO   ra   r   r  rl  r  r  )r  r  r   r   r  r  r   r  Nrd  r  s         r2    test_exclude_keeps_other_entriesz;TestGetBusyBotsInfoExclude.test_exclude_keeps_other_entries
  s    (+==
*5K*5SZ[
 	djj.A11,1O&   gy)\999rJ   c                     |dz  dz  }dddddii}|j                  t        j                  |      d	       |j                         }d
|v sJ y)uQ   exclude_task_id=None이면 모든 running 태스크 반환 (기존 동작 호환)r   r   rO   r  ra   r   r  r   r   r  Nrd  r  s         r2   test_exclude_none_returns_allz8TestGetBusyBotsInfoExclude.test_exclude_none_returns_all
  se    (+==
+K

 	djj.A113&   rJ   c                     |dz  dz  }dddddddd	d
i}|j                  t        j                  |      d       |j                  d      }d|v sJ |d   d   dk(  sJ |d   d   dk(  sJ y)u\   핵심 버그 시나리오: dev팀 자신의 entry가 composite entry를 덮어쓰지 않음r   r   rO   rl  r   r  r  rj   r  task-1241.1task-1242.1r   r   r  r  r   r  r   Nrd  r  s         r2   2test_exclude_prevents_overwrite_of_composite_entryzMTestGetBusyBotsInfoExclude.test_exclude_prevents_overwrite_of_composite_entry
  s    (+==
 +6)T[\+6)L
 	djj.A11-1P&   gy)]:::gy)[888rJ   N)ru   rv   rw   rx   r  r  r  r  rR   rJ   r2   r  r  	  s    Q
:
!9rJ   r  c                   *    e Zd ZdZddZd Zd Zd Zy)TestBotConflictWithTimerEntryuY   dev팀 dispatch 시 이미 timer가 시작된 상태에서도 composite 봇 충돌 감지Nc                 v    t               }||_        ||nt        j                  ddi      |_        ||_        |S r9  rO  rS  s        r2   rU  z3TestBotConflictWithTimerEntry._make_mock_subprocess5
  r:  rJ   c                    |dz  dz  }dddddddd	d
di}|j                  t        j                  |      d       t        j                  |d      5 }t        j                  |dddd      5  | j                  d      |j                  _        |j                  dddd      }ddd       ddd       d   dk(  sJ d|d   v sJ d|d   v sJ y# 1 sw Y   .xY w# 1 sw Y   2xY w)uo   핵심 회귀 테스트: composite가 bot-c 점유 + dev2-team timer entry가 이미 존재해도 충돌 감지r   r   rO   rl  r   r  r  rj   rw  r3  r  r   r   rX  rY  r  r[  r  r   r   r  Fr   r  Nr   rg  rh  r  r  rM  s          r2   8test_composite_conflict_detected_despite_own_timer_entryzVTestBotConflictWithTimerEntry.test_composite_conflict_detected_despite_own_timer_entry<
  s!   (+==
 +6)T[\+6)\gh
 	djj.A LL|4	p8@LLzF93UV	p )-(B(B1(EHLL%!**;8JTain*oF	p 	p h7***&++++y 1111	p 	p 	p 	ps$   C/6C%CC	CC'c                    |dz  dz  }dddddddd	d
i}|j                  t        j                  |      d       t        j                  |d      5 }t        j                  |dddd      5  | j                  d      |j                  _        |j                  dddd      }ddd       ddd       d   dk(  sJ d|d   v sJ y# 1 sw Y   %xY w# 1 sw Y   )xY w)u9   marketing이 bot-c 점유 + dev2-team dispatch 시 차단r   r   rO   r  r   r  r  rj   r  )r  
task-501.1r   r   rX  rY  r  r[  r  r   r   r  Fr  Nr   rg  rh  r  rM  s          r2   'test_dynamic_bot_on_dev_team_bot_blockszETestBotConflictWithTimerEntry.test_dynamic_bot_on_dev_team_bot_blocksS
  s	   (+==
*5SZ[*5K
 	djj.A LL|4	o8@LLzF93UV	o )-(B(B1(EHLL%!**;8JT`hm*nF	o 	o h7***&++++	o 	o 	o 	os$   C.6C$CC	CCc           	         |dz  dz  }dddddddd	d
i}|j                  t        j                  |      d       t        j                  |d      5 }t        j                  |dddd      5  | j                  dt        j                  ddi            |j                  _        |j                  dd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)uK   force=True면 timer entry 존재해도 composite 충돌 무시하고 진행r   r   rO   rl  r   r  r  rj   r  r  r   r   rX  rY  r  r[  r  r   r   rN  r  r  Tr  Nr^  r  rM  s          r2   -test_force_bypasses_conflict_with_timer_entryzKTestBotConflictWithTimerEntry.test_force_bypasses_conflict_with_timer_entryh
  s   (+==
+6)T[\+6)L
 	djj.A LL|4	l8@LLzF93UV	l )-(B(B1djjRZ\`QaFb(cHLL%!**;Q^fj*kF	l 	l h<///	l 	l 	l 	ls%   C!.AC:C!C	C!!C*r  )ru   rv   rw   rx   rU  r  r  r  rR   rJ   r2   r  r  2
  s    c2.,*0rJ   r  c                   (    e Zd ZdZd Zd Zd Zd Zy)TestGetAvailableBotsWithTeamsuN   _get_available_bots_with_teams() — 가용 봇 목록 반환 헬퍼 테스트c                     |j                  i       }t        |      dk(  sJ |D cg c]  }|d   	 }}dD ]  }||v rJ  yc c}w )u3   busy_bots가 빈 dict일 때 8개 봇 전부 반환   bot_idr  r  r
  bot-ebot-frm  bot-hbot-iN_get_available_bots_with_teamsrr   )rf   r]   r   ru  bot_idsr  s         r2   &test_all_bots_available_when_none_busyzDTestGetAvailableBotsWithTeams.test_all_bots_available_when_none_busy
  s[    <<R@6{a067u5?77[ 	"C'>!>	" 8s   Ac                     ddddddd}|j                  |      }t        |      dk(  sJ |D cg c]  }|d   	 }}d|vsJ d	|vsJ d
D ]  }||v rJ  yc c}w )u4   bot-b, bot-g가 busy일 때 나머지 6개만 반환rl  r   r   r   r   )r  rm     r  r  rm  )r  r
  r  r  r  r  Nr  )rf   r]   	busy_botsr   ru  r  r  s          r2   test_excludes_busy_botsz5TestGetAvailableBotsWithTeams.test_excludes_busy_bots
  s     "-D!,D
	 <<YG6{a067u5?77g%%%g%%%I 	"C'>!>	" 8s   Ac           
      z    d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                  |      }|g k(  sJ y)u.   모든 봇이 busy일 때 빈 리스트 반환rl  r   r  r   r   r  r  r  r  r  r  Nr  )rf   r]   r  r   s       r2    test_all_bots_busy_returns_emptyz>TestGetAvailableBotsWithTeams.test_all_bots_busy_returns_empty
  si     "-D!,D!,D!,D!,D!,D!,D!,D	
	 <<YG||rJ   c           	      n    ddddddddd	}|j                  i       }|D ]  }|d
   }|d   ||   k(  rJ  y)u=   각 봇의 default_team이 TEAM_TO_BOT_ID 역매핑과 일치ra   rj   rn   r  r  r  r  r  r  r  default_teamNr  )rf   r]   expected_mappingr   ru  r  s         r2   )test_returns_correct_default_team_mappingzGTestGetAvailableBotsWithTeams.test_returns_correct_default_team_mapping
  si     !       	
 <<R@ 	EE8_F(,<V,DDDD	ErJ   N)ru   rv   rw   rx   r  r  r  r  rR   rJ   r2   r  r  
  s    X""ErJ   r  c                   0    e Zd ZdZddZd Zd Zd Zd Zy)	TestBotConflictAvailableBotsuJ   봇 충돌 에러 응답에 가용 봇 추천이 포함되는지 테스트Nc                 v    t               }||_        ||nt        j                  ddi      |_        ||_        |S r9  rO  rS  s        r2   rU  z2TestBotConflictAvailableBots._make_mock_subprocess
  r:  rJ   c                    |dz  dz  }ddddddii}|j                  t        j                  |      d	
       t        j                  |d      5 }t        j                  |dddd      5  | j                  d      |j                  _        |j                  ddd      }ddd       ddd       d   dk(  sJ d|v sJ y# 1 sw Y   "xY w# 1 sw Y   &xY w)uZ   composite가 bot-g 점유 중 → dev6-team dispatch error에 available_bots 필드 존재r   r   rO   ry  rl  r   rm  r  r   r   rX  rY  rE  r[  rz  r   r  r   Fr  Nr   rg  available_botsr  rM  s          r2   1test_conflict_error_includes_available_bots_fieldzNTestBotConflictAvailableBots.test_conflict_error_includes_available_bots_field
  s    (+==
;)T[\

 	djj.A LL|4	Y8@LLzF93UV	Y )-(B(B1(EHLL%!**;8JRW*XF	Y 	Y h7***6)))	Y 	Y 	Y 	Ys$   C*5C C C		CCc                    |dz  dz  }ddddddii}|j                  t        j                  |      d	
       t        j                  |d      5 }t        j                  |dddd      5  | j                  d      |j                  _        |j                  ddd      }ddd       ddd       d   dk(  sJ d|d   v sJ y# 1 sw Y   %xY w# 1 sw Y   )xY w)uJ   에러 메시지에 '가용 대안:' 문자열과 가용 봇 정보 포함r   r   rO   ry  rl  r   rm  r  r   r   rX  rY  rE  r[  rz  r   r  r   Fr  Nr   rg  u   가용 대안:rh  r  rM  s          r2   1test_conflict_error_message_includes_alternativeszNTestBotConflictAvailableBots.test_conflict_error_message_includes_alternatives
  s    (+==
;)T[\

 	djj.A LL|4	Y8@LLzF93UV	Y )-(B(B1(EHLL%!**;8JRW*XF	Y 	Y h7***6)#4444	Y 	Y 	Y 	Ys$   C*5CCC	CCc                    |dz  dz  }ddddddii}|j                  t        j                  |      d	
       t        j                  |d      5 }t        j                  |dddd      5  | j                  d      |j                  _        |j                  ddd      }ddd       ddd       d   dk(  sJ |j                  dg       D cg c]  }|d   	 }}d|vsJ y# 1 sw Y   DxY w# 1 sw Y   HxY wc c}w )uA   available_bots에 점유 중인 봇(bot-g)이 포함되지 않음r   r   rO   ry  rl  r   rm  r  r   r   rX  rY  rE  r[  rz  r   r  r   Fr  Nr   rg  r  r  )
r   r%   r   r   r`  rU  ra  rb  r<   r!   )	rf   r]   r:   r   r   rd  r   ru  available_bot_idss	            r2   0test_conflict_error_available_bots_excludes_busyzMTestBotConflictAvailableBots.test_conflict_error_available_bots_excludes_busy
  s&   (+==
;)T[\

 	djj.A LL|4	Y8@LLzF93UV	Y )-(B(B1(EHLL%!**;8JRW*XF	Y 	Y h7***:@**EUWY:Z[U8_[[////	Y 	Y 	Y 	Y \s*   C.*5C"C.C:"C+	'C..C7c                    |dz  dz  }dddddddddddd	dddd
ddddddddddddddddddi}|j                  t        j                  |      d       t        j                  |d      5 }t        j                  |dddd      5  | j                  d      |j                  _        |j                  ddd      }ddd       ddd       d   dk(  sJ d|d    v sJ y# 1 sw Y   %xY w# 1 sw Y   )xY w)!u>   모든 봇이 busy일 때 '모든 봇이 작업 중' 메시지r   r   rO   rl  r   r  r  r  r
  r  r  rm  r  r  r  r   r   rX  rY  rE  r[  rz  r   r  r   Fr  Nr   rg  r  rh  r  rM  s          r2   #test_conflict_all_bots_busy_messagez@TestBotConflictAvailableBots.test_conflict_all_bots_busy_message  sK   (+==
(3yQXY(3yQXY(3yQXY(3yQXY(3yQXY(3yQXY(3yQXY(3yQXY	
 	djj.A LL|4	Y8@LLzF93UV	Y )-(B(B1(EHLL%!**;8JRW*XF	Y 	Y h7***)VI->>>>	Y 	Y 	Y 	Ys$   2C25C&C2&C/	+C22C;r  )	ru   rv   rw   rx   rU  r  r  r  r  rR   rJ   r2   r  r  
  s    T*(5(0*?rJ   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
)TestImageQcGateBlocku]   이미지/광고 작업 시 --workflow 미적용 시 차단 + --skip-qc-gate 우회 테스트c                     |j                  t        dg d       t        j                  t              5 }|j                          ddd       j                  j                  dk(  sJ y# 1 sw Y   %xY w)uE   이미지 키워드 포함 + --workflow 없음 → sys.exit(1) 호출r  )r  r  ra   r     배너 이미지 생성Nr6   r   r>   r   r  r  r  r  r  rf   r]   r   r  s       r2   )test_image_keyword_without_workflow_exitsz>TestImageQcGateBlock.test_image_keyword_without_workflow_exits,  s\    C)tu]]:& 	 (	 ~~""a'''	  	    A((A1c                    t               }d|_        t        j                  ddi      |_        d|_        |j                  t        dg d       t        j                  |d      5 }t        j                  |dd	d
d      5  ||j                  _        |j                          ddd       ddd       |j                         }t        j                  |j                        }d|v sJ y# 1 sw Y   GxY w# 1 sw Y   KxY w)uO   이미지 키워드 + --skip-qc-gate → 차단 없이 통과 (dispatch 진행)r   r   rN  r   r  )r  r  ra   r  r  z--skip-qc-gaterX  rY  rZ  r[  r\  Nr   rP  r%   r   rQ  rR  r   r>   r   r`  ra  rb  r  r  r   r  r  s           r2   +test_image_keyword_with_skip_qc_gate_passesz@TestImageQcGateBlock.test_image_keyword_with_skip_qc_gate_passes3  s    k!"!ZZ4(89i	
 LL|4	 8@LLzF93UV	  )4HLL%	  	  $$&HLL)6!!!	  	  	  	 $   $C3?"C'!C3'C0	,C33C<c                 B   t               }d|_        t        j                  ddi      |_        d|_        |j                  t        dg d       t        j                  |d      5 }t        j                  |dd	d
d      5  t        dd      5  ||j                  _        |j                          ddd       ddd       ddd       |j                         }t        j                  |j                        }d|v sJ y# 1 sw Y   OxY w# 1 sw Y   SxY w# 1 sw Y   WxY w)uG   이미지 키워드 + --workflow image-qc-gate → 차단 없이 통과r   r   rN  r   r  )r  r  ra   r  r  z
--workflowzimage-qc-gaterX  rY  rZ  r[  r\  z5prompts.image_workflow.build_workflow_overview_promptzworkflow promptr  Nr  r  s           r2   'test_image_keyword_with_workflow_passesz<TestImageQcGateBlock.test_image_keyword_with_workflow_passesK  s   k!"!ZZ4(89	
 LL|4	 8@LLzF93UV	  IXij	 
 )4HLL%	  	  	  $$&HLL)6!!!	  	  	  	  	  	 s<   $D?D	"C=/D	7D=DD		D	DDc                    t               }d|_        t        j                  ddi      |_        d|_        |j                  t        dg d       t        j                  |d      5 }t        j                  |dd	d
d      5  ||j                  _        |j                          ddd       ddd       |j                         }t        j                  |j                        }d|v sJ y# 1 sw Y   GxY w# 1 sw Y   KxY w)uP   이미지 키워드 없음 + --workflow 없음 → 정상 통과 (영향 없음)r   r   rN  r   r  )r  r  ra   r  u   API 엔드포인트 구현rX  rY  rZ  r[  r\  Nr  r  s           r2   -test_no_image_keyword_without_workflow_passeszBTestImageQcGateBlock.test_no_image_keyword_without_workflow_passesl  s    k!"!ZZ4(89Z	
 LL|4	 8@LLzF93UV	  )4HLL%	  	  $$&HLL)6!!!	  	  	  	 r  c                     |j                  t        dg d       t        j                  t              5 }|j                          ddd       j                  j                  dk(  sJ y# 1 sw Y   %xY w)u#   영문 'banner' 키워드도 차단r  )r  r  ra   r  zCreate banner designNr6   r  r  s       r2   "test_english_banner_keyword_blocksz7TestImageQcGateBlock.test_english_banner_keyword_blocks  s\    C)qr]]:& 	 (	 ~~""a'''	  	 r  c                     |j                  t        dg d       t        j                  t              5 }|j                          ddd       j                  j                  dk(  sJ y# 1 sw Y   %xY w)u"   영문 'image' 키워드도 차단r  )r  r  ra   r  zGenerate product imageNr6   r  r  s       r2   !test_english_image_keyword_blocksz6TestImageQcGateBlock.test_english_image_keyword_blocks  s\    C)st]]:& 	 (	 ~~""a'''	  	 r  c                     |j                  t        dg d       t        j                  t              5 }|j                          ddd       j                  j                  dk(  sJ y# 1 sw Y   %xY w)u&   한국어 '광고' 키워드도 차단r  )r  r  ra   r  u   메타 광고 소재 제작Nr6   r  r  s       r2   test_korean_ad_keyword_blocksz2TestImageQcGateBlock.test_korean_ad_keyword_blocks  s\    C)xy]]:& 	 (	 ~~""a'''	  	 r  c                     |j                  t        dg d       t        j                  t              5 }|j                          ddd       j                  j                  dk(  sJ y# 1 sw Y   %xY w)u)   한국어 '디자인' 키워드도 차단r  )r  r  ra   r  u   UI 디자인 작업Nr6   r  r  s       r2   !test_korean_design_keyword_blocksz6TestImageQcGateBlock.test_korean_design_keyword_blocks  s\    C)pq]]:& 	 (	 ~~""a'''	  	 r  N)ru   rv   rw   rx   r  r  r  r  r  r  r  r  rR   rJ   r2   r  r  )  s.    g("0"B"0((((rJ   r  c                   6    e Zd ZdZd	dZd Zd Zd Zd Zd Z	y)
TestLogicalTeamBotConflictug   논리적팀(design/marketing/content)이 봇 점유 시 해당 봇의 dev팀 dispatch 차단 테스트Nc                 v    t               }||_        ||nt        j                  ddi      |_        ||_        |S r9  rO  rS  s        r2   rU  z0TestLogicalTeamBotConflict._make_mock_subprocess  r:  rJ   c                    |dz  dz  }ddddddii}|j                  t        j                  |      d	
       t        j                  |d      5 }t        j                  |dddd      5  | j                  d      |j                  _        |j                  ddd      }ddd       ddd       d   dk(  sJ d|d   v sJ d|d   v sJ d|d   v sJ y# 1 sw Y   7xY w# 1 sw Y   ;xY w)u;   design이 bot-e 점유 중 → dev4-team dispatch 시 errorr   r   rO   task-1400.1r
  r   r  r  r   r   rX  rY  rC  r[  rH  r]  r   r  r   Fr  Nr   rg  rh  r  rM  s          r2   )test_design_on_bot_e_blocks_dev4_dispatchzDTestLogicalTeamBotConflict.test_design_on_bot_e_blocks_dev4_dispatch  s&   (+==
8yQXY

 	djj.A LL|4	Y8@LLzF93UV	Y )-(B(B1(EHLL%!**;8JRW*XF	Y 	Y h7***&++++6),,,,y 1111	Y 	Y 	Y 	Yr|  c                    |dz  dz  }ddddddii}|j                  t        j                  |      d	
       t        j                  |d      5 }t        j                  |dddd      5  | j                  d      |j                  _        |j                  ddd      }ddd       ddd       d   dk(  sJ d|d   v sJ d|d   v sJ d|d   v sJ y# 1 sw Y   7xY w# 1 sw Y   ;xY w)u<   content가 bot-b 점유 중 → dev1-team dispatch 시 errorr   r   rO   ztask-1380.1contentr   r  r  r   r   rX  rY  rZ  r[  r\  r   ra   r   Fr  Nr   rg  rh  r  rM  s          r2   *test_content_on_bot_b_blocks_dev1_dispatchzETestLogicalTeamBotConflict.test_content_on_bot_b_blocks_dev1_dispatch  s&   (+==
9	RYZ

 	djj.A LL|4	Y8@LLzF93UV	Y )-(B(B1(EHLL%!**;8JRW*XF	Y 	Y h7***&++++F9----y 1111	Y 	Y 	Y 	Yr|  c           	         |dz  dz  }ddddddii}|j                  t        j                  |      d	
       t        j                  |d      5 }t        j                  |dddd      5  | j                  dt        j                  ddi            |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)uF   design이 completed면 봇 점유 아님 → dev4-team 정상 dispatchr   r   rO   r  r
  r   r  r  r   r   rX  rY  rC  r[  r  r   r   rN  r  r  Nr^  r  rM  s          r2   *test_design_completed_allows_dev4_dispatchzETestLogicalTeamBotConflict.test_design_completed_allows_dev4_dispatch  s    (+==
8{SZ[

 	djj.A LL|4	I8@LLzF93UV	I )-(B(B1djjRZ\`QaFb(cHLL%!**;HF	I 	I h<///	I 	I 	I 	Ir  c           	         |dz  dz  }dddddii}|j                  t        j                  |      d	       t        j                  |d
      5 }t        j                  |dddd      5  | j                  dt        j                  ddi            |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)ub   논리적팀이 running이지만 bot 필드 없음 → dev팀 정상 dispatch (false block 없음)r   r   rO   r  r
  r   r  r   r   rX  rY  rZ  r[  r\  r   r   rN  ra   r  Nr^  r  rM  s          r2   2test_logical_team_without_bot_field_no_false_blockzMTestLogicalTeamBotConflict.test_logical_team_without_bot_field_no_false_block  s    (+==
8yI

 	djj.A LL|4	I8@LLzF93UV	I )-(B(B1djjRZ\`QaFb(cHLL%!**;HF	I 	I h<///	I 	I 	I 	Is%   C)A	C2CC	CC"c           	         |dz  dz  }dddddddd	dd
i}|j                  t        j                  |      d       t        j                  |d      5 }t        j                  |dddd      5  | j                  dt        j                  ddi            |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)ua   여러 논리적팀이 각각 다른 봇 점유 → 해당 dev팀만 차단, 다른 팀은 정상r   r   rO   r
  r   r  r  r  r  r	  r   r   rX  rY  r  r[  r  r   r   rN  rn   r  Nr^  r  rM  s          r2   *test_multiple_logical_teams_different_botszETestLogicalTeamBotConflict.test_multiple_logical_teams_different_bots  s    (+==
(0IgV(3yQXY
 	djj.A LL|4	I8@LLzF93UV	I )-(B(B1djjRZ\`QaFb(cHLL%!**;HF	I 	I h<///	I 	I 	I 	Is%   C/A	C8CC	CC(r  )
ru   rv   rw   rx   rU  r  r  r  r  r  rR   rJ   r2   r  r    s#    q2,2,0&0&0rJ   r  c                   (    e Zd ZdZd Zd Zd Zd Zy)TestSelectAndReserveBotuA   _select_and_reserve_bot() 원자적 봇 선택 + 예약 테스트c                 $   |dz  dz  }dddddii}|j                  t        j                  |      d	       |j                  d      }|d
k(  sJ t        j                  |j                  d	            }|d   d   j                  d      d
k(  sJ y)uO   빈 timers → bot-b(첫 번째 봇) 선택 + timer에 bot 필드 기록 확인r   r   rO   task-test.1ra   r   r  r   r   r  r  N)r   r%   r   _select_and_reserve_botr   r   r!   )rf   r]   r:   r   r   selectedwrittens          r2    test_selects_first_available_botz8TestSelectAndReserveBot.test_selects_first_available_bot  s    (+==
;)L

 	djj.A77F7""" **Z1171CDw.2259WDDDrJ   c                     |dz  dz  }ddddddddddd	i}|j                  t        j                  |      d
       |j                  d      }|dk(  sJ y)u1   dev1(bot-b), dev2(bot-c) running → bot-d 선택r   r   rO   ra   r   r  rj   rn   )ztask-other1.1ztask-other2.1r  r   r   r  r
  N)r   r%   r   r  )rf   r]   r:   r   r   r  s         r2   test_skips_busy_botsz,TestSelectAndReserveBot.test_skips_busy_bots1  su    (+==
-8I!N-8I!N+6)L
 	djj.A77F7"""rJ   c                    |dz  dz  }ddddddii}|j                  t        j                  |      d	
       |j                  d      }t        j                  |j                  d	
            }|d   d   }d|v sJ |d   |k(  sJ y)uj   봇 선택 후 task-timers.json을 다시 읽어서 해당 task의 bot 필드가 설정되었는지 확인r   r   rO   ztask-reserve.1ra   r   u   예약 테스트r3  r   r   r  N)r   r%   r   r  r   r   )rf   r]   r:   r   r   r  r  
task_entrys           r2   test_reserves_bot_in_timer_filez7TestSelectAndReserveBot.test_reserves_bot_in_timer_fileA  s    (+==
 kY_q"r

 	djj.A778HI **Z1171CDW%&67

"""% H,,,rJ   c                 0   |dz  dz  }ddddddddddd	ddd
ddddddddddddddd	i}|j                  t        j                  |      d       t        j                  t
              5  |j                  d       ddd       y# 1 sw Y   yxY w)u*   모든 봇이 busy → RuntimeError 발생r   r   rO   ra   r   r  rj   rn   r  r  r  r  r  rl  )	r   r   r   r  r  r  r  r  r  r   r   r  N)r   r%   r   r   r  r*  r  r  s        r2   test_raises_when_all_busyz1TestSelectAndReserveBot.test_raises_when_all_busyS  s    (+==
(3yI(3yI(3yI(3yI(3yI(3yI(3yI(3yI+6)L

 	djj.A]]<( 	@00?	@ 	@ 	@s   1BBN)ru   rv   rw   rx   r   r  r  r  rR   rJ   r2   r  r    s    KE$# -$@rJ   r  c                   X    e Zd ZdZddeddfdZ	 ddededdfdZd	 Zd
 Zd Z	d Z
d Zy)TestValidateModelConsistencyu.   _validate_model_consistency() 단위 테스트	org_modelr   r   c                     |dz  }dddddd|dd	gd
dddddgiii}|j                  t        j                  |      d       |S )Nzorg-structure.jsonr   r   r   r   rn   dagda)rW  model)r   r  )r   r   r7   	aphroditeclaude-opus-4-6)r   r  r   r   rl  )rf   r:   r
  org_filer   s        r2   _make_org_fixturez.TestValidateModelConsistency._make_org_fixtureq  s    22'; 4?3:Y,O!"* (8+6AR$S
* 	DJJt,w?rJ   	bot_modelkey_hashc                 x    |dz  }|dt         |iddi}|j                  t        j                  |      d       |S )Nbot_settings.jsondev3_Dagda_botfake-token-12345display_namemodelstokenr   r   )_TEST_CHAT_IDr   r%   r   )rf   r:   r  r  settings_filer   s         r2   _make_bot_settings_fixturez7TestValidateModelConsistency._make_bot_settings_fixture  sP     !#66 0()4+
 	  D!1G DrJ   c                 R   | j                  d      }| j                  d      |j                  d|       |j                  dddi       |j                  ddd	i       |j                  d
t               |j                  dfd       ddl} G fdd       G d d      |j
                  j                  }|j                  |j
                  dt        fd             ddl}ddl	}d|j                  v r|j                  d= t        t        j                  j                  dd            }t        |      |j                  vr%|j                  j!                  dt        |             ddl}	|j                  |	d|       |j                  |	dddi       |j                  |	ddd	i       |j                  |	dt               |	j%                  d      }
|
d   du sJ |
d   dk(  sJ |
d   dk(  sJ |
d    dk(  sJ |j                  |j
                  dt        |             y)!u/   org와 bot 모델이 같을 때 consistent=Truer  r
  r  zdispatch.ORG_FILEzdispatch.TEAM_BOTrn   ro   zdispatch.BOT_KEYS0b94683120a691cfzdispatch.CHAT_IDzdispatch.Pathc                  \    | dk(  rj                   S  t        d      j                  | i |S )NrR   r  )r   
__import__r   )r8  r9  r  s     r2   rU   zETestValidateModelConsistency.test_consistent_models.<locals>.<lambda>  s2    DBJM$8$8 LfJW`LaLfLfhlLwpvLw rJ   r   Nc                       e Zd Z fdZy)FTestValidateModelConsistency.test_consistent_models.<locals>._FakeHomec                            S r  rR   rf   other_FakeCokacdirr:   s     r2   __truediv__zRTestValidateModelConsistency.test_consistent_models.<locals>._FakeHome.__truediv__      $X..rJ   Nru   rv   rw   r+  r*  r:   s   r2   	_FakeHomer&        /rJ   r/  c                       e Zd Zd Zd Zy)JTestValidateModelConsistency.test_consistent_models.<locals>._FakeCokacdirc                     || _         y r  _baserf   bases     r2   __init__zSTestValidateModelConsistency.test_consistent_models.<locals>._FakeCokacdir.__init__  	    !
rJ   c                      | j                   |z  S r  r4  rf   r)  s     r2   r+  zVTestValidateModelConsistency.test_consistent_models.<locals>._FakeCokacdir.__truediv__      zzE))rJ   Nru   rv   rw   r8  r+  rR   rJ   r2   r*  r2        "*rJ   r*  homec                               S r  rR   r/  s   r2   rU   zETestValidateModelConsistency.test_consistent_models.<locals>.<lambda>  	    y{ rJ   r<   r   r   ORG_FILErd   rY  CHAT_ID
consistentTr
  r  r   )r  r  r   r  r  r   r?  staticmethodr  r>   rC   r   r    r!   r=   r?   r@   r<   _validate_model_consistency)rf   r:   r   r  r  original_homer  r>   r*   r]   r   r*  r/  r  s    `         @@@r2   test_consistent_modelsz3TestValidateModelConsistency.test_consistent_models  s   ))(>O)P77L]7^/:/+v1FG/&:L1MN.>w	
 		/ 	/	* 	*  ))GLL&,?R2ST$J'(8:OPQ	y>)HHOOAs9~.'L*h?L*{F6KLL*v?Q6RSL)]C99+Fl#t+++k"&7777k"&7777i K///GLL&,}2MNrJ   c                    ddl }ddl}| j                  d      }| j                  d      } G fdd       G d	 d
      |j                  j
                  }|j                  |j                  dt        fd             ddl}	t	        t        j                  j                  dd            }
t        |
      |	j                  vr%|	j                  j                  dt        |
             d|	j                  v r|	j                  d= ddl}|j                  |d|       |j                  |dddi       |j                  |dddi       |j                  |dt"               |j%                  |j&                        5  |j)                  d      }ddd       d   du sJ |d   dk(  sJ |d   dk(  sJ |j*                  D cg c](  }|j,                  |j&                  k(  s|j.                  * }}t1        d |D              s
J d|        |j                  |j                  dt        |             y# 1 sw Y   xY wc c}w )uA   org와 bot 모델이 다를 때 consistent=False + WARNING 로그r   Nr  r   zclaude-sonnet-4-5r!  c                       e Zd Z fdZy)HTestValidateModelConsistency.test_inconsistent_models.<locals>._FakeHomec                            S r  rR   r(  s     r2   r+  zTTestValidateModelConsistency.test_inconsistent_models.<locals>._FakeHome.__truediv__  r,  rJ   Nr-  r.  s   r2   r/  rL    r0  rJ   r/  c                       e Zd Zd Zd Zy)LTestValidateModelConsistency.test_inconsistent_models.<locals>._FakeCokacdirc                     || _         y r  r4  r6  s     r2   r8  zUTestValidateModelConsistency.test_inconsistent_models.<locals>._FakeCokacdir.__init__  r9  rJ   c                      | j                   |z  S r  r4  r;  s     r2   r+  zXTestValidateModelConsistency.test_inconsistent_models.<locals>._FakeCokacdir.__truediv__  r<  rJ   Nr=  rR   rJ   r2   r*  rO    r>  rJ   r*  r?  c                               S r  rR   rA  s   r2   rU   zGTestValidateModelConsistency.test_inconsistent_models.<locals>.<lambda>  rB  rJ   r   r   r<   rC  rd   rn   ro   rY  r"  rD  rE  Fr
  r  c              3   $   K   | ]  }d |v  
 yw)u   모델 불일치NrR   r  s     r2   r  zHTestValidateModelConsistency.test_inconsistent_models.<locals>.<genexpr>  s      
*-#%
r  uB   WARNING 로그에 '모델 불일치' 미포함. 기록된 로그: )r  r  r  r  r   r?  r   rF  r>   r   r    r!   r=   r?   r@   rC   r<   r  r  r  rG  r  r  rh  r  )rf   r:   r   r	  r  r  r  r  rH  r>   r*   r]   r   r3  r  r*  r/  s    `             @@r2   test_inconsistent_modelsz5TestValidateModelConsistency.test_inconsistent_models  s   ))(>O)P77L_7`	/ 	/	* 	*  ))GLL&,?R2ST(8:OPQ	y>)HHOOAs9~.$J''L*h?L*{F6KLL*v?Q6RSL)]C__W__- 	K!==kJF	K l#u,,,k"&7777k"&9999/5~~^!gooA]AII^^ 
1A
 
 	cOP`Oab	c 
 	GLL&,}2MN	K 	K _s   ?H6I#I6H?c                 h   ddl }ddl}dz  }| j                        } G fdd       G d d      |j                  j                  }|j                  |j                  dt        fd	             t        t        j                  j                  d
d            }t        |      |j                  vr%|j                  j                  dt        |             d|j                  v r|j                  d= ddl}	|j                  |	d|       |j                  |	dddi       |j                  |	dddi       |j                  |	dt               |	j!                  d      }
t#        |
t$              sJ d|
v sJ |
d   dk(  sJ |
d   dk(  sJ |j                  |j                  dt        |             y)u6   org-structure.json 없을 때 graceful (에러 안남)r   Nznonexistent-org.jsonc                       e Zd Z fdZy)ETestValidateModelConsistency.test_org_file_missing.<locals>._FakeHomec                            S r  rR   r(  s     r2   r+  zQTestValidateModelConsistency.test_org_file_missing.<locals>._FakeHome.__truediv__  r,  rJ   Nr-  r.  s   r2   r/  rW    r0  rJ   r/  c                       e Zd Zd Zd Zy)ITestValidateModelConsistency.test_org_file_missing.<locals>._FakeCokacdirc                     || _         y r  r4  r6  s     r2   r8  zRTestValidateModelConsistency.test_org_file_missing.<locals>._FakeCokacdir.__init__  r9  rJ   c                      | j                   |z  S r  r4  r;  s     r2   r+  zUTestValidateModelConsistency.test_org_file_missing.<locals>._FakeCokacdir.__truediv__  r<  rJ   Nr=  rR   rJ   r2   r*  rZ    r>  rJ   r*  r?  c                               S r  rR   rA  s   r2   rU   zDTestValidateModelConsistency.test_org_file_missing.<locals>.<lambda>  rB  rJ   r   r   r<   rC  rd   rn   ro   rY  r"  rD  rE  r   r
  r   )r  r>   r  r   r?  r   rF  r   r    r!   r=   r?   r@   rC   r<   r  rG  r   r  )rf   r:   r   r  r>   missing_orgr  rH  r*   r]   r   r*  r/  s    `         @@r2   test_org_file_missingz2TestValidateModelConsistency.test_org_file_missing  s   !7777A	/ 	/	* 	*  ))GLL&,?R2ST(8:OPQ	y>)HHOOAs9~.$J''L*kBL*{F6KLL*v?Q6RSL)]C 99+F&$'''v%%%i K///k"b(((GLL&,}2MNrJ   c                   
 ddl }ddl}| j                  |      }|dz  j                           G 
fdd       G d d      
|j                  j
                  }|j                  |j                  dt        fd	             t	        t        j                  j                  d
d            }t        |      |j                  vr%|j                  j                  dt        |             d|j                  v r|j                  d= ddl}|j                  |d|       |j                  |dddi       |j                  |dddi       |j                  |dt                |j#                  d      }	t%        |	t&              sJ d|	v sJ |	d   dk(  sJ |	d   dk(  sJ |j                  |j                  dt        |             y)u5   bot_settings.json 없을 때 graceful (에러 안남)r   N
empty_homec                       e Zd Z fdZy)ITestValidateModelConsistency.test_bot_settings_missing.<locals>._FakeHomec                            S r  rR   )rf   r)  r*  	empty_dirs     r2   r+  zUTestValidateModelConsistency.test_bot_settings_missing.<locals>._FakeHome.__truediv__>  s    $Y//rJ   Nr-  )r*  re  s   r2   r/  rc  =  s    0rJ   r/  c                       e Zd Zd Zd Zy)MTestValidateModelConsistency.test_bot_settings_missing.<locals>._FakeCokacdirc                     || _         y r  r4  r6  s     r2   r8  zVTestValidateModelConsistency.test_bot_settings_missing.<locals>._FakeCokacdir.__init__B  r9  rJ   c                      | j                   |z  S r  r4  r;  s     r2   r+  zYTestValidateModelConsistency.test_bot_settings_missing.<locals>._FakeCokacdir.__truediv__E  r<  rJ   Nr=  rR   rJ   r2   r*  rg  A  r>  rJ   r*  r?  c                               S r  rR   rA  s   r2   rU   zHTestValidateModelConsistency.test_bot_settings_missing.<locals>.<lambda>I  rB  rJ   r   r   r<   rC  rd   rn   ro   rY  r"  rD  rE  r   r  r   )r  r>   r  rW   r   r?  r   rF  r   r    r!   r=   r?   r@   rC   r<   r  rG  r   r  )rf   r:   r   r  r>   r  rH  r*   r]   r   r*  r/  re  s             @@@r2   test_bot_settings_missingz6TestValidateModelConsistency.test_bot_settings_missing3  s   ))(3|+		0 	0	* 	*  ))GLL&,?R2ST(8:OPQ	y>)HHOOAs9~.$J''L*h?L*{F6KLL*v?Q6RSL)]C 99+F&$'''v%%%i K///k"b(((GLL&,}2MNrJ   c                    ddl }ddl}| j                        }| j                        } G fdd       G d d      |j                  j
                  }|j                  |j                  dt        fd             t	        t        j                  j                  d	d
            }t        |      |j                  vr%|j                  j                  dt        |             d|j                  v r|j                  d= ddl}	|j                  |	d|       |j                  |	dddi       |j                  |	dddi       |j                  |	dt                |	j#                  d      }
t%        |
t&              sJ |
d   dk(  sJ |
d   du sJ |
d   dk(  sJ |
d   dk(  sJ |j                  |j                  dt        |             y)u7   존재하지 않는 team_id 전달 시 기본값 반환r   Nc                       e Zd Z fdZy)DTestValidateModelConsistency.test_unknown_team_id.<locals>._FakeHomec                            S r  rR   r(  s     r2   r+  zPTestValidateModelConsistency.test_unknown_team_id.<locals>._FakeHome.__truediv__k  r,  rJ   Nr-  r.  s   r2   r/  rn  j  r0  rJ   r/  c                       e Zd Zd Zd Zy)HTestValidateModelConsistency.test_unknown_team_id.<locals>._FakeCokacdirc                     || _         y r  r4  r6  s     r2   r8  zQTestValidateModelConsistency.test_unknown_team_id.<locals>._FakeCokacdir.__init__o  r9  rJ   c                      | j                   |z  S r  r4  r;  s     r2   r+  zTTestValidateModelConsistency.test_unknown_team_id.<locals>._FakeCokacdir.__truediv__r  r<  rJ   Nr=  rR   rJ   r2   r*  rq  n  r>  rJ   r*  r?  c                               S r  rR   rA  s   r2   rU   zCTestValidateModelConsistency.test_unknown_team_id.<locals>.<lambda>v  rB  rJ   r   r   r<   rC  rd   rn   ro   rY  r"  rD  r  r   rE  Tr
  r   r  )r  r>   r  r  r   r?  r   rF  r   r    r!   r=   r?   r@   rC   r<   r  rG  r   r  )rf   r:   r   r  r>   r  r  rH  r*   r]   r   r*  r/  s    `         @@r2   test_unknown_team_idz1TestValidateModelConsistency.test_unknown_team_idb  s   ))(377A	/ 	/	* 	*  ))GLL&,?R2ST(8:OPQ	y>)HHOOAs9~.$J''L*h?L*{F6KLL*v?Q6RSL)]C 99:LM&$'''i $6666l#t+++k"b(((k"b(((GLL&,}2MNrJ   N)r  )r  r"  )ru   rv   rw   rx   r=   r  r  rI  rT  r_  rk  ru  rR   rJ   r2   r	  r	  n  s_    8S QW 6 M_#&FI	7Or1Of,O\-O^-OrJ   r	  c                   D    e Zd ZdZddddeddfdZd Zd Zd	 Zd
 Z	d Z
y)TestSyncBotSettingsu%   _sync_bot_settings() 단위 테스트home_dirr   r  r   c                     |dz  }|j                  dd       |dz  }|dt        diddi}|j                  t        j                  |      d	
       |S )Nz	.cokacdirTrL   r  r  r  r  r  r   r   )rW   r  r   r%   r   )rf   rx  r  r  r  r   s         r2   _make_bot_settingsz&TestSyncBotSettings._make_bot_settings  si    k)td3 #66 0(*;<+
 	  D!1G DrJ   c                 &   ddl }ddl}t        t        j                  j                  dd            }t        |      |j                  vr%|j                  j                  dt        |             d|j                  v r|j                  d= ddl
}|dz  j                  dd       |j                  |d	|       |j                  j                  }|d
z  j                  dd       |j                  |j                  dt        fd             ||fS )uL   공통 monkeypatch: WORKSPACE와 Path.home()을 tmp_path 기반으로 교체r   Nr   r   r<   r   TrL   rE   	fake_homer?  c                       S r  rR   )r|  s   r2   rU   z0TestSyncBotSettings._patch_env.<locals>.<lambda>  s    y rJ   )r  r>   r   r   r    r!   r=   r?   r@   rC   r<   rW   r   r?  rF  )	rf   r:   r   r  r>   r*   r]   rH  r|  s	           @r2   
_patch_envzTestSyncBotSettings._patch_env  s    (8:OPQ	y>)HHOOAs9~.$J'' 
H	##D4#@L+x@  )){*	t4GLL&,?P2QRY55rJ   c                 
   | j                  ||      \  }}}ddl}| j                  |       |j                          |dz  dz  }|j	                         s
J d|        |j                  |j                  dt        |             y)u*   동기화 파일이 올바르게 생성됨r   Nr   bot_settings_sync.jsonu)   동기화 파일이 생성되지 않음: r?  )r~  r  rz  _sync_bot_settingsr"   r   r   rF  rf   r:   r   r]   r|  rH  r  	sync_paths           r2   test_sync_creates_filez*TestSyncBotSettings.test_sync_creates_file  s    15;1W.i	*'')x'*BB	!Z%Nyk#ZZ!GLL&,}2MNrJ   c                    | j                  ||      \  }}}ddl}| j                  |       |j                          |dz  dz  }t	        j
                  |j                  d            }|j                         D ]2  \  }	}
|
j                  d      dk(  rJ d	|
j                  d               |j                  |j                  d
t        |             y)u(   token 값이 ***REDACTED***로 교체됨r   Nr   r  r   r   r  z***REDACTED***u!   token이 마스킹되지 않음: r?  )r~  r  rz  r  r%   r   r   r  r!   r   r   rF  )rf   r:   r   r]   r|  rH  r  r  syncedr  cfgs              r2   test_token_maskedz%TestSyncBotSettings.test_token_masked  s    15;1W.i	*'')x'*BB	I///AB#\\^ 	pMHc777#'77o;\]`]d]del]m\n9oo7	p 	GLL&,}2MNrJ   c                    | j                  ||      \  }}}ddl}| j                  |d       |j                          |dz  dz  }t	        j
                  |j                  d            }|j                  di       }	|	j                  d	      d
k(  sJ |	j                  d      t        dik(  sJ |j                  |j                  dt        |             y)u'   token 외 필드는 그대로 유지됨r   Nr"  )r  r   r  r   r   r  r  r  r  r?  )r~  r  rz  r  r%   r   r   r!   r  r   r   rF  )
rf   r:   r   r]   r|  rH  r  r  r  r  s
             r2   test_other_fields_preservedz/TestSyncBotSettings.test_other_fields_preserved  s    15;1W.i	4FG'')x'*BB	I///ABjj+R0ww~&*::::wwx ]4E$FFFFGLL&,}2MNrJ   c                     | j                  ||      \  }}}ddl}|j                          |dz  dz  }|j                         rJ d       |j	                  |j
                  dt        |             y)u<   원본 bot_settings.json 없을 때 graceful (에러 안남)r   Nr   r  u.   원본 없는데 동기화 파일이 생성됨r?  )r~  r  r  r"   r   r   rF  r  s           r2   rk  z-TestSyncBotSettings.test_bot_settings_missing  sm    15;1W.i 	'')x'*BB	##%W'WW%GLL&,}2MNrJ   N)r"  )ru   rv   rw   rx   r=   rz  r~  r  r  r  rk  rR   rJ   r2   rw  rw    s=    /6 S Z` 64OO"O$OrJ   rw  c                   4    e Zd ZdZd Zd Zd Zd Zd Zd Z	y)	TestPrdDecompositionu*   PRD 자동 분해 (--prd) 기능 테스트c                    d}ddl m}  ||      }t        |      dk(  sJ |d   d   dk(  sJ |d   d   dk(  sJ |d   d	   d
k(  sJ |d   d   dk(  sJ |d   d   ddgk(  sJ |d   d   J d|d   d   v sJ |d   d   dk(  sJ y)u   기본 Phase 추출u   
## 3. 구현 로드맵

### Phase 1 (2일) — 기초 구현 [F1, F2]
- 항목 1
- 항목 2
- **DoD**: 기초 구현 완료

### Phase 2 (1일) — 통합 [F3]
- 통합 테스트
r   _parse_prd_regexr5  
phase_typePhasephase_numberr6   titleu   기초 구현 [F1, F2]durationu   2일featuresF1F2dodNu   기초 구현 완료r<   r  rr   rf   prd_contentr  r  s       r2   test_parse_prd_regex_basicz/TestPrdDecomposition.test_parse_prd_regex_basic  s    
 	.!+.6{aay&'111ay(A---ay!%====ay$...ay$t444ay+++%5)9999ay(A---rJ   c                     d}ddl m}  ||      }t        |      dk(  sJ |d   d   dk(  sJ |d   d   dk(  sJ |d   d   d	k(  sJ y
)u   Sprint 0 포함 추출ua   
### Sprint 0 (0.5일) — 준비
- 준비 항목

### Phase 1 (2일) — 구현
- 구현 항목
r   r  r5  r  Sprintr  r6   r  Nr  r  s       r2    test_parse_prd_regex_with_sprintz5TestPrdDecomposition.test_parse_prd_regex_with_sprint  so     	.!+.6{aay&(222ay(A---ay&'111rJ   c                 .    ddl m}  |d      }|g k(  sJ y)u   Phase 없는 문서r   r  u   # 빈 문서
내용만 있음N)r<   r  )rf   r  r  s      r2   test_parse_prd_regex_emptyz/TestPrdDecomposition.test_parse_prd_regex_empty0  s    -!"BC||rJ   c                 H    ddl m}  |dd      }|d   dk(  sJ d|d   v sJ y	)
u%   존재하지 않는 PRD 파일 에러r   
handle_prdz/nonexistent/path.mdra   r   rg  u   찾을 수 없습니다rh  N)r<   r  )rf   r  r   s      r2    test_handle_prd_nonexistent_filez5TestPrdDecomposition.test_handle_prd_nonexistent_file7  s8    '2K@h7***(F9,====rJ   c                    ddl }ddlm} ddlm} d}|dz  }|j                  |d       |d	z  d
z  }|j                  dd        |d|      5   |t        |      d      }ddd       d   dk(  sJ |d   dk(  sJ |d   dk(  sJ t        |d         dk(  sJ t        |d         dk(  sJ |dz  }	|	j                         sJ |	j                  d      }
d|
v sJ d|
v sJ d|
v sJ y# 1 sw Y   xY w)u   task 파일 생성 확인r   Nr   r  u   
## 3. 구현 로드맵

### Phase 1 (2일) — 기초 구현 [F1, F2]
- 항목 1
- **DoD**: 구현 완료

### Phase 2 (1일) — 통합
- 통합 항목
zprd-test-project.mdr   r   r   rO   TrL   dispatch.WORKSPACEra   r   rN  methodregextotal_phasesr5  createdskippedzdispatch-test-project-phase1.mdztask_id:zF1, F2)r   unittest.mockr   r<   r  r   rW   r=   rr   r"   r   )rf   r:   r   r   r  r  prd_file	tasks_dirr   phase1_filer  s              r2   test_handle_prd_creates_filesz2TestPrdDecomposition.test_handle_prd_creates_files?  sJ   ''	 33K': x''1	t4'2 	<H{;F	< h4'''h7***n%***6)$%***6)$%***  "CC!!###'''9W$$$g%%%7"""	< 	<s   C**C3c                    ddl m} ddlm} d}|dz  }|j	                  |d       |dz  d	z  }|j                  d
d
       |dz  }|j	                  dd        |d|      5   |t        |      d      }ddd       d   dk(  sJ t        |d         dk(  sJ t        |d         dk(  sJ |j                         dk(  sJ y# 1 sw Y   OxY w)u   기존 파일 스킵 확인r   r  r  u+   
### Phase 1 (1일) — 테스트
- 항목
zprd-skip-test.mdr   r   r   rO   TrL   zdispatch-skip-test-phase1.mdr   r  ra   Nr   rN  r  r  r6   )	r  r   r<   r  r   rW   r=   rr   r   )	rf   r:   r   r  r  r  r  r   r   s	            r2   test_handle_prd_skips_existingz3TestPrdDecomposition.test_handle_prd_skips_existingi  s    '' 00K':x''1	t4==J9'2 	<H{;F	< h4'''6)$%***6)$%***!!#z111	< 	<s   #CCN)
ru   rv   rw   rx   r  r  r  r  r  r  rR   rJ   r2   r  r    s$    4.42">(#T2rJ   r  c                       e Zd ZdZd Zd Zy)TestWakeUpDelayuN   task-2046: wake-up 실패 시 dispatch delay가 5초로 설정되는지 검증c                    ddl }|j                  }|j                  }	 d |_        d |_        d}|j                  d      s|j                  dd      }|sd}|dk(  s
J d	|        	 ||_        ||_        y# ||_        ||_        w xY w)
uC   봇 프로세스 미감지 + wake-up 실패 시 _dispatch_delay = 5r   Nc                      yNFrR   rS   s    r2   rU   zFTestWakeUpDelay.test_wake_up_failure_sets_delay_to_5.<locals>.<lambda>  rV   rJ   c                      yr  rR   )chat_idrT   s     r2   rU   zFTestWakeUpDelay.test_wake_up_failure_sets_delay_to_5.<locals>.<lambda>  rV   rJ   r   test_keychatr   zExpected 5, got )r<   rX   _wake_up_bot)rf   r\   original_checkoriginal_wake_dispatch_delay_wake_results         r2   $test_wake_up_failure_sets_delay_to_5z4TestWakeUpDelay.test_wake_up_failure_sets_delay_to_5  s     //((	-%6C"9C !O))*5"//
C#&'O"a'M+;O;L)MM'%3C",C &4C",Cs   AA6 6Bc                     ddl }|j                  }	 d |_        d}|j                  d      sd}|dk(  s
J d|        	 ||_        y# ||_        w xY w)u9   봇 프로세스 감지 시 기본 딜레이 10초 유지r   Nc                      yrQ   rR   rS   s    r2   rU   zNTestWakeUpDelay.test_bot_already_running_keeps_default_delay.<locals>.<lambda>  rV   rJ   r   r  r   zExpected 10, got )r<   rX   )rf   r\   r  r  s       r2   ,test_bot_already_running_keeps_default_delayz<TestWakeUpDelay.test_bot_already_running_keeps_default_delay  sb    //		4%5C" O))*5"#"b(O,=o=N*OO(%3C"^C"s   ,A 	AN)ru   rv   rw   rx   r  r  rR   rJ   r2   r  r    s    X-.4rJ   r  )?rx   r%   r   r>   typesr   r  r   r  r   r   r   r    r!   r  r#   r3   r  r9   
ModuleTyperI   r   r]   r_   rz   r   r   r   r  r%  rL  r  r  r  r'  r7  rU  rj  r{  r  r  r  r  r  r  r  r2  r>  rG  rU  rb  rv  r  r  r  r  r  r  r  r	  rw  r  r  rR   rJ   r2   <module>r     sq  
  	 
    * 

1<@S &s :D U5E5E > 6 62G G./# /#nl% l%h D" ":( (2J+ J+pM- M-jT" T"x^k ^kLY! Y!B'= '=^BV BVTd~ d~XXs Xs@@2 @2PWB WBtXx Xx@* *N`,[,W65# 5#zY# Y#B9 9F9 9<&) &)\C% C%VlU lUh89 89@H0 H0`7E 7E~`? `?Pu( u(zn0 n0lJ@ J@daO aOR	hO hOVA2 A2H(4 (4rJ   