
    )["j;                       d Z ddlmZ ddlZddlZddlZddlZ ej                  e	      j                         j                  d   Z ee      Zeej                  v r*ej                  j!                  e       eej                  v r*ej                  j#                  de        eej&                        D ]U  Zedk(  sej+                  d      sej&                  e   Z eedd      xs dZej+                  e      rIej&                  e= W dd	lmZmZmZmZmZmZ dd
l m!Z!m"Z"m#Z#m$Z$m%Z%m&Z& edz  dz  Z'ejP                  jS                  de'      Z*e*e*jV                  J ejP                  jY                  e*      Z-e-ej&                  d<   e*jV                  j]                  e-       d-dZ/d Z0d Z1d Z2d Z3d Z4d Z5d Z6d Z7d Z8d Z9d Z:d Z;d Z<d Z=d Z>d Z?d Z@d  ZAd! ZBd" ZCd# ZDd$ ZEd% ZFd& ZGd' ZHd( ZId) ZJd* ZKd+ ZLd, ZMy).u  tests/regression/test_ci_watcher_lifecycle_2729p0a.py — task-2729+1 P0-A 회귀.

회귀 보호 시나리오:
  (a) watcher stale + PR pending → CI_WATCHER_SESSION_LIFETIME_GAP 분류
  (b) terminal 도달 → callback 필수, 미발사 시 WATCHER_TERMINAL_CALLBACK_NOT_WIRED
  (c) quiet-window settled 판정 (review-settle 골격)
  (d) fallback = dead-man safety-net only (final-report trigger 아님)
  (e) 기존 6-state/terminal 회귀 무손상 (heartbeat 필드 추가가 6-state dict 오염 없음)

대상 모듈(progress_watcher_gate.py, ci_watch_handoff_runner.py,
pr_watcher_terminal_state_classifier.py, normal_fallback_callback_helper.py)은
수정하지 않고 실제 동작에 맞춰 테스트만 작성한다.
외부 네트워크 / 실제 cokacdir / gh 호출 없음.
    )annotationsN   dispatchz	dispatch.__file__ )DISPATCH_INCOMPLETE#WATCHER_TERMINAL_CALLBACK_NOT_WIREDWATCHER_TRACKED_STATESevaluate_progress_watcher_gate watcher_terminal_callback_statusall_states_tracked)
PRSnapshotTERMINAL_STATESCI_WATCHER_SESSION_LIFETIME_GAPLIFETIME_GAP_DIAGNOSTIC_STATES%WATCHER_HEARTBEAT_STALE_THRESHOLD_SECclassify_lifetime_gapscriptszci_watch_handoff_runner.pyci_watch_handoff_runnerc                     d fd	}|S )Nc                Z    t        j                  dk(  rdnddk(  rd      S d      S )Nr   okr   boom)
returncodestdoutstderr)typesSimpleNamespace)cmdcapture_outputtexttimeoutcheckkwargsr   s         5tests/regression/test_ci_watcher_lifecycle_2729p0a.py_runnerz"_make_fake_runner.<locals>._runnerV   s<    $$!%?4#q2
 	
 /5
 	
    )TTNF )r   r&   s   ` r%   _make_fake_runnerr)   U   s    
 Nr'   c                 B    t        dd      \  } }| t        k(  sJ |sJ y)uA   stale=True + pr 비-terminal → CI_WATCHER_SESSION_LIFETIME_GAP.Tr   watcher_stalepr_terminal_stateN)r   r   	state_strreasons     r%   *test_a_classify_lifetime_gap_stale_pendingr1   d   s*    -DTVWIv7777M6r'   c                 :    t         j                  dd      du sJ y)u?   last_poll_ts=None → stale=True (heartbeat 한 번도 없음).N      Y@)last_poll_tsnow_tsT
runner_modis_watcher_staler(   r'   r%   &test_a_is_watcher_stale_none_last_pollr9   k   s     &&D&G4OOOr'   c                 <    t         j                  ddd      du sJ y)u2   now_ts - last_poll_ts >= threshold → stale=True.        g      @  r4   r5   stale_threshold_secTNr6   r(   r'   r%   $test_a_is_watcher_stale_at_thresholdr?   p   s.    &&T ' 	  r'   c                 <    t         j                  ddd      du sJ y)u2   now_ts - last_poll_ts < threshold → stale=False.r;   g     @r<   r=   FNr6   r(   r'   r%   'test_a_is_watcher_stale_below_thresholdrA   w   s.    &&T ' 	  r'   c                     t         j                         } | j                  J t         j                  | dd      }|d   t        k(  sJ |d   du sJ |d   du sJ |d	   d
u sJ y)uM   WatcherState last_poll_ts=None → classify_watcher_lifetime_gap returns gap.Nr3   r   )stater5   r-   lifetime_gap_statelifetime_gap_detectedTr,   activeF)r7   WatcherStater4   classify_watcher_lifetime_gapr   souts     r%   1test_a_classify_watcher_lifetime_gap_no_heartbeatrL   ~   s    !A>>!!!

2
2 3 C #$(GGGG&'4///4'''x=E!!!r'   c                     t         j                         } t         j                  | d       t         j                  | ddd      }|d   dk(  sJ |d   du sJ y	)
u?   record_poll_heartbeat 직후 → not stale → no lifetime gap.r3   g      ^@r   r<   )rC   r5   r-   r>   rD   rE   FN)r7   rG   record_poll_heartbeatrH   rI   s     r%   4test_a_classify_watcher_lifetime_gap_after_heartbeatrO      sh    !A$$Q.

2
2 3 C #$***&'5000r'   c                     t         j                         } t         j                  | d      }|| u sJ | j                  dk(  sJ | j                  dk(  sJ y)uU   record_poll_heartbeat → state.last_poll_ts 와 state.heartbeat_ts 가 설정된다.g     8@N)r7   rG   rN   r4   heartbeat_ts)rJ   rets     r%   (test_a_record_poll_heartbeat_sets_fieldsrS      sQ    !A

*
*1e
4C!8O8>>U""">>U"""r'   c                      t         t        vsJ y)u   ★ CI_WATCHER_SESSION_LIFETIME_GAP 는 TERMINAL_STATES 에 포함되지 않는다
    (diagnostic, non-terminal-merge 보장).Nr   r   r(   r'   r%   =test_a_ci_watcher_session_lifetime_gap_not_in_terminal_statesrV      s     +/AAAr'   c                      t         t        v sJ y)uT   LIFETIME_GAP_DIAGNOSTIC_STATES 는 CI_WATCHER_SESSION_LIFETIME_GAP 를 포함한다.N)r   r   r(   r'   r%   7test_a_lifetime_gap_diagnostic_states_contains_sentinelrX      s    *.LLLLr'   c                 @    t        dd      \  } }| dk(  sJ |dk(  sJ y)uD   ★ PR 이 terminal(MERGE_READY) 이면 stale 여도 gap 이 아님.TMERGE_READYr+   r   no_lifetime_gapNr   r.   s     r%   /test_a_classify_lifetime_gap_pr_terminal_no_gapr]      s3    -mIv ??&&&&r'   c                 @    t        dd      \  } }| dk(  sJ |dk(  sJ y)u-   watcher_stale=False → 무조건 gap 없음.Fr   r+   r[   Nr\   r.   s     r%   -test_a_classify_lifetime_gap_not_stale_no_gapr_      s/    -EUWXIv??&&&&r'   c                     t         dk(  sJ y)uA   WATCHER_HEARTBEAT_STALE_THRESHOLD_SEC == 3600 (60분 convention).r<   N)r   r(   r'   r%   .test_a_watcher_heartbeat_stale_threshold_valuera      s    0G;;;r'   c            
        t         j                         } | j                  du sJ t         j                  ddt        d   d| t        d            }|d   du sJ |d	   d
k(  sJ |d   du sJ |d   du sJ | j                  du sJ y)uF   returncode=0 → fired=True, callback_mandatory=True, not_wired=False.F2729*   r   test_b_wiredtask_id	pr_numberterminal_stater0   rC   runnerfiredTcallback_statusWATCHER_TERMINAL_CALLBACK_WIREDcallback_mandatory	not_wiredN)r7   rG   normal_callbackfire_terminal_callbackr   r)   rC   rK   s     r%   ,test_b_fire_terminal_callback_success_runnerrs      s    ##%E  E)))

+
+&q) # , C w<4 !%FFFF#$,,,{u$$$  D(((r'   c            
         t         j                         } t         j                  dddd| t        d            }|d   du sJ |d	   t        k(  sJ |d
   du sJ |d   du sJ | j
                  du sJ y)uQ   returncode=1 → fired=False, callback_mandatory=True, not_wired=True, NOT_WIRED.rc   rd   HOLD_FOR_CHAIRtest_b_not_wired   rf   rk   Frl   rn   Tro   N)r7   rG   rq   r)   r	   rp   rr   s     r%   ,test_b_fire_terminal_callback_failure_runnerrx      s    ##%E

+
+'! # , C w<5    !%HHHH#$,,,{t###  E)))r'   c                 0    t        dd      t        k(  sJ y)zNwatcher_terminal_callback_status(terminal_reached=True, callback_fired=False).TF)terminal_reachedcallback_firedN)r   r	   r(   r'   r%   1test_b_watcher_terminal_callback_status_not_wiredr|      s      	)$uU.	/	/r'   c            
         t         j                         } t         j                  dddd| t        d            }|d   dk(  sJ y)	uA   fire_terminal_callback 반환 dict 에 terminal_state 키 포함.rc   rd   LOOP_BOUNDARY	test_b_tsr   rf   ri   N)r7   rG   rq   r)   rr   s     r%   4test_b_fire_terminal_callback_returns_terminal_stater      sR    ##%E

+
+& # , C  O333r'   c                 <    t         j                  ddd      du sJ y)u6   now_sec - last_event_sec >= quiet_window_sec → True.r   x   last_event_secnow_secquiet_window_secTNr7   is_quiet_window_settledr(   r'   r%   test_c_quiet_window_settledr     s2     	**cC 	+ 	
 		r'   c                 <    t         j                  ddd      du sJ y)u6   now_sec - last_event_sec < quiet_window_sec → False.r   w   r   r   FNr   r(   r'   r%   test_c_quiet_window_not_settledr     s2     	**cC 	+ 	
 		r'   c                 .    t         j                  dk(  sJ y)z&REVIEW_SETTLE_QUIET_WINDOW_SEC == 120.r   N)r7   REVIEW_SETTLE_QUIET_WINDOW_SECr(   r'   r%   *test_c_review_settle_quiet_window_constantr     s    44;;;r'   c                     t         j                         } t         j                  |        | j                  du sJ | j                  du sJ y)uZ   mark_fallback_prune → state.fallback_prune==True 이지만 state.normal_callback==False.TFN)r7   rG   mark_fallback_prunefallback_prunerp   rC   s    r%   2test_d_fallback_prune_does_not_set_normal_callbackr     sF    ##%E""5)4'''  E)))r'   c                     t        dd      } | j                  du sJ | j                  du sJ | j                  t        k(  sJ y)ub   fallback만 등록 시 evaluate_progress_watcher_gate → fallback_only=True, DISPATCH_INCOMPLETE.FT)watcher_registeredfallback_registeredN)r   
incompletefallback_onlydispatch_statusr   )grs    r%   $test_d_fallback_only_gate_incompleter   '  sJ    	'5VZ	[B==D   t###!4444r'   c                     ddl m}  | dk(  sJ y)u_   FALLBACK_ROLE_SINGLE_PURPOSE == 'RECOVERY_ONLY_NO_FINAL_REPORT_TRIGGER' doctrine 상수 검증.r   FALLBACK_ROLE_SINGLE_PURPOSE%RECOVERY_ONLY_NO_FINAL_REPORT_TRIGGERN)(dispatch.normal_fallback_callback_helperr   r   s    r%   ,test_d_fallback_role_single_purpose_doctriner   /  s    U'+RRRRr'   c                 T    t         j                         } | j                         du sJ y)u,   새 WatcherState → all_tracked() == False.FN)r7   rG   all_trackedr   s    r%   "test_e_all_tracked_initially_falser   9  s'    ##%E%'''r'   c                    t         j                         } t         j                  |        t         j                  |        t         j	                  |        t         j                  |        t         j                  |        t         j                  |        | j                         du sJ t        | j                               du sJ y)u4   6개 mark_* 모두 호출 후 all_tracked() == True.TN)r7   rG   mark_head_changemark_non_force_pushmark_cimark_finish_donemark_normal_callbackr   r   r   as_tracked_dictr   s    r%   "test_e_all_tracked_after_six_marksr   ?  s    ##%E&""5)u&##E*""5)$&&&e3356$>>>r'   c                     t         j                         } t        | j                         j	                               t
        k(  sJ y)u   ★ tuple(state.as_tracked_dict().keys()) == WATCHER_TRACKED_STATES
    (heartbeat 필드 추가가 6-state dict 를 오염시키지 않음 보장).N)r7   rG   tupler   keysr
   r   s    r%   *test_e_as_tracked_dict_keys_match_constantr   L  s8     ##%E&&(--/04JJJJr'   c                 ,    t        t              dk(  sJ y)u   TERMINAL_STATES 길이 == 5.   N)lenr   r(   r'   r%   "test_e_terminal_states_length_fiver   S  s    1$$$r'   c                      t         t        vsJ y)uB   CI_WATCHER_SESSION_LIFETIME_GAP 는 TERMINAL_STATES 에 미포함.NrU   r(   r'   r%   ,test_e_terminal_states_excludes_lifetime_gapr   X  s    */AAAr'   c                     t        dd      } t        j                         }t        j                  | dd|      \  }}|t        v sJ |dk(  sJ d|v sJ |j
                  d	k(  sJ |j                  dk(  sJ y
)uB   run_once head drift → HOLD_FOR_CHAIR (terminal), state.polls==1.deadbeefdriftedBLOCKED)head_ref_oidmerge_state_statusexpectedhead000
   )expected_headelapsed_watcher_secrC   ru   
head_driftrw   N)r   r7   rG   run_oncer   pollselapsed_sec)snaprC   ri   r0   s       r%   1test_e_run_once_head_drift_returns_hold_for_chairr   ]  s    #4SD##%E'00'	 1 NF _,,,----6!!!;;!"""r'   c                 d    t         j                         } | j                  J | j                  J y)u:   새 WatcherState → last_poll_ts=None, heartbeat_ts=None.N)r7   rG   r4   rQ   r   s    r%   )test_e_watcher_state_new_has_no_heartbeatr   n  s4    ##%E%%%%%%r'   c                 d    t         j                         } | j                         }d|vsJ d|vsJ y)uM   last_poll_ts / heartbeat_ts 는 as_tracked_dict() 에 포함되지 않는다.r4   rQ   N)r7   rG   r   )rC   ds     r%   +test_e_heartbeat_fields_not_in_tracked_dictr   u  s:    ##%EA""""""r'   )r   int)N__doc__
__future__r   importlib.util	importlibpathlibsysr   Pathr   resolveparents_ROOTstr	_ROOT_STRpathremoveinsertlistmodules_name
startswith_modgetattr_fdispatch.progress_watcher_gater   r	   r
   r   r   r   *utils.pr_watcher_terminal_state_classifierr   r   r   r   r   r   _RUNNER_PATHutilspec_from_file_location_specloadermodule_from_specr7   exec_moduler)   r1   r9   r?   rA   rL   rO   rS   rV   rX   r]   r_   ra   rs   rx   r|   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r(   r'   r%   <module>r      s   #   
  	X&&(003J	388HHOOI 388 9  #++ #E
e..{;{{5!T:t,2}}Y'E"#   y #??../H,WU\\5 55^^,,U3
)3% &    $P

"1#BM
''<)&*$4$<*5S(
?K%
B
#"&#r'   