
    #j;                       d Z ddlmZ ddlZddlmc mZ ddl	Z
ddlZddlZddlZ 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j2                        D ]U  Zedk(  sej7                  d      sej2                  e   Z eedd      xs dZej7                  e      rIej2                  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
j\                  j_                  de-      Z0g Z1dZ2e0e2uZ3e3Z4e3re0jj                  Z6dZ7e6e7uZ8e8Z4e4sB ejr                  de3fde0e2f      d ejt                         v s ejv                  e0      r ejx                  e0      nd ejx                  e2      dz  Z=dde=iz  Z>e1j                  e>       e3r ejr                  de8fde6e7f      d ejt                         v s ejv                  e0      r ejx                  e0      nd ejx                  e6       ejx                  e7      dz  Z@dde@iz  ZAe1j                  eA        ej                  e1d      i z  ZCddeCiz  ZD eE ej                  eD            dxZ4xZ1xZ3xZ2xZ6xZ8Z7e
j\                  j                  e0      ZHeHej2                  d<   e0jj                  j                  eH       d9dZJd ZKd ZLd ZMd ZNd ZOd  ZPd! ZQd" ZRd# ZSd$ ZTd% ZUd& ZVd' ZWd( ZXd) ZYd* ZZd+ Z[d, Z\d- Z]d. Z^d/ Z_d0 Z`d1 Zad2 Zbd3 Zcd4 Zdd5 Zed6 Zfd7 Zgd8 Zhy):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_runner)is not)z%(py2)s is not %(py5)s_spec)py2py5z%(py7)spy7)z5%(py11)s
{%(py11)s = %(py9)s.loader
} is not %(py14)s)py9py11py14z%(py16)spy16zassert %(py19)spy19c                     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         f/home/jay/workspace/.worktrees/task-2729+10-dev6/tests/regression/test_ci_watcher_lifecycle_2729p0a.py_runnerz"_make_fake_runner.<locals>._runnerV   s<    $$!%?4#q2
 	
 /5
 	
    )TTNF )r$   r0   s   ` r/   _make_fake_runnerr3   U   s    
 Nr1   c                    t        dd      \  } }| t        k(  }|st        j                  d|fd| t        f      dt	        j
                         v st        j                  |       rt        j                  |       nddt	        j
                         v st        j                  t              rt        j                  t              nddz  }d	d
|iz  }t        t        j                  |            d}|sedddt	        j
                         v st        j                  |      rt        j                  |      ndiz  }t        t        j                  |            y)uA   stale=True + pr 비-terminal → CI_WATCHER_SESSION_LIFETIME_GAP.Tr   watcher_stalepr_terminal_state==)z%(py0)s == %(py2)s	state_strr   py0r   assert %(py4)spy4Nzassert %(py0)sr<   reason)
r   r   
@pytest_ar_call_reprcompare@py_builtinslocals_should_repr_global_name	_safereprAssertionError_format_explanation)r:   r?   @py_assert1@py_format3@py_format5@py_format1s         r/   *test_a_classify_lifetime_gap_stale_pendingrL   d   s    -DTVWIv777779777777797779777777777777777777MMMMM6MMM6MMMMM6r1   c            	     J   t         j                  } d}d} | ||      }d}||u }|st        j                  d|fd||f      dt	        j
                         v st        j                  t               rt        j                  t               ndt        j                  |       t        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }d	d
|iz  }t        t        j                  |            dx} x}x}x}x}}y)u?   last_poll_ts=None → stale=True (heartbeat 한 번도 없음).N      Y@)last_poll_tsnow_tsTis)zt%(py8)s
{%(py8)s = %(py2)s
{%(py2)s = %(py0)s.is_watcher_stale
}(last_poll_ts=%(py4)s, now_ts=%(py6)s)
} is %(py11)s
runner_mod)r<   r   r>   py6py8r   zassert %(py13)spy13
rS   is_watcher_staler@   rA   rB   rC   rD   rE   rF   rG   )rH   @py_assert3@py_assert5@py_assert7@py_assert10@py_assert9@py_format12@py_format14s           r/   &test_a_is_watcher_stale_none_last_pollr`   k   s    &&ODOO&DGO4OG4OOOOG4OOOOOO:OOO:OOO&OOODOOOOOOGOOO4OOOOOOOOr1   c            
     ~   t         j                  } d}d}d} | |||      }d}||u }|st        j                  d|fd||f      dt	        j
                         v st        j                  t               rt        j                  t               ndt        j                  |       t        j                  |      t        j                  |      t        j                  |      t        j                  |      t        j                  |      d	z  }d
d|iz  }t        t        j                  |            dx} x}x}x}x}x}}y)u2   now_ts - last_poll_ts >= threshold → stale=True.        g      @  rO   rP   stale_threshold_secTrQ   z%(py10)s
{%(py10)s = %(py2)s
{%(py2)s = %(py0)s.is_watcher_stale
}(last_poll_ts=%(py4)s, now_ts=%(py6)s, stale_threshold_sec=%(py8)s)
} is %(py13)srS   r<   r   r>   rT   rU   py10rV   assert %(py15)spy15NrW   	rH   rY   rZ   r[   r]   @py_assert12@py_assert11r_   @py_format16s	            r/   $test_a_is_watcher_stale_at_thresholdro   p   sD   && !'=A&T 	 	    	 v     I   I '  I  I "( I >B I  I       r1   c            
     ~   t         j                  } d}d}d} | |||      }d}||u }|st        j                  d|fd||f      dt	        j
                         v st        j                  t               rt        j                  t               ndt        j                  |       t        j                  |      t        j                  |      t        j                  |      t        j                  |      t        j                  |      d	z  }d
d|iz  }t        t        j                  |            dx} x}x}x}x}x}}y)u2   now_ts - last_poll_ts < threshold → stale=False.rb   g     @rc   rd   FrQ   rf   rS   rg   ri   rj   NrW   rk   s	            r/   'test_a_is_watcher_stale_below_thresholdrq   w   sD   && !'=A&T 	 	    	      Y   Y '  Y  Y "( Y >B Y  Y       r1   c                 V   t         j                         } | j                  }d}||u }|st        j                  d|fd||f      dt        j                         v st        j                  |       rt        j                  |       ndt        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}t         j                  | dd	
      }|d   }|t        k(  }|st        j                  d|fd|t        f      t        j                  |      dt        j                         v st        j                  t              rt        j                  t              nddz  }	dd|	iz  }t        t        j                  |            dx}}|d   }d}||u }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }
dd|
iz  }t        t        j                  |            dx}x}}|d   }d}||u }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }
dd|
iz  }t        t        j                  |            dx}x}}|d   }d}||u }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }
dd|
iz  }t        t        j                  |            dx}x}}y)uM   WatcherState last_poll_ts=None → classify_watcher_lifetime_gap returns gap.NrQ   z4%(py2)s
{%(py2)s = %(py0)s.last_poll_ts
} is %(py5)ssr<   r   r   assert %(py7)sr   rN   r   )staterP   r7   lifetime_gap_stater8   z%(py1)s == %(py3)sr   py1py3assert %(py5)sr   lifetime_gap_detectedTz%(py1)s is %(py4)sr{   r>   assert %(py6)srT   r6   activeF)rS   WatcherStaterO   r@   rA   rB   rC   rD   rE   rF   rG   classify_watcher_lifetime_gapr   )rt   rH   @py_assert4rY   @py_format6@py_format8out@py_assert0@py_assert2@py_format4rJ   @py_format7s               r/   1test_a_classify_watcher_lifetime_gap_no_heartbeatr   ~   s   !A>>!T!>T!!!!>T!!!!!!1!!!1!!!>!!!T!!!!!!!

2
2 3 C #$G$(GGGGG$(GGGG$GGGGGG(GGGG(GGGGGGGG&'/4/'4////'4///'///4///////'4'4''''4''''''4'''''''x=!E!=E!!!!=E!!!=!!!E!!!!!!!r1   c                    t         j                         } t         j                  | d       t         j                  | ddd      }|d   }d}||k(  }|slt	        j
                  d|fd||f      t	        j                  |      t	        j                  |      d	z  }d
d|iz  }t        t	        j                  |            dx}x}}|d   }d}||u }|slt	        j
                  d|fd||f      t	        j                  |      t	        j                  |      d	z  }d
d|iz  }t        t	        j                  |            dx}x}}y)u?   record_poll_heartbeat 직후 → not stale → no lifetime gap.rN   g      ^@r   rc   )rw   rP   r7   re   rx   r8   z%(py1)s == %(py4)sr   r   rT   Nr~   FrQ   r   )	rS   r   record_poll_heartbeatr   r@   rA   rE   rF   rG   )rt   r   r   rY   r   rJ   r   s          r/   4test_a_classify_watcher_lifetime_gap_after_heartbeatr      s    !A$$Q.

2
2 3 C #$**$****$***$**********&'050'50000'5000'00050000000r1   c                    t         j                         } t         j                  | d      }|| u }|st        j                  d|fd|| f      dt        j                         v st        j                  |      rt        j                  |      nddt        j                         v st        j                  |       rt        j                  |       nddz  }dd|iz  }t        t        j                  |            d	}| j                  }d}||k(  }|st        j                  d
|fd||f      dt        j                         v st        j                  |       rt        j                  |       ndt        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            d	x}x}}| j                  }d}||k(  }|st        j                  d
|fd||f      dt        j                         v st        j                  |       rt        j                  |       ndt        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            d	x}x}}y	)uU   record_poll_heartbeat → state.last_poll_ts 와 state.heartbeat_ts 가 설정된다.g     8@rQ   )z%(py0)s is %(py2)sretrt   r;   r=   r>   Nr8   )z4%(py2)s
{%(py2)s = %(py0)s.last_poll_ts
} == %(py5)sru   rv   r   )z4%(py2)s
{%(py2)s = %(py0)s.heartbeat_ts
} == %(py5)s)rS   r   r   r@   rA   rB   rC   rD   rE   rF   rG   rO   heartbeat_ts)	rt   r   rH   rI   rJ   r   rY   r   r   s	            r/   (test_a_record_poll_heartbeat_sets_fieldsr      sG   !A

*
*1e
4C!8OOO3!OOOOOO3OOO3OOOOOO!OOO!OOOOOOO>>"U">U"""">U""""""1"""1""">"""U""""""">>"U">U"""">U""""""1"""1""">"""U"""""""r1   c                    t         t        v} | st        j                  d| fdt         t        f      dt	        j
                         v st        j                  t               rt        j                  t               nddt	        j
                         v st        j                  t              rt        j                  t              nddz  }dd|iz  }t        t        j                  |            d} y)	u   ★ CI_WATCHER_SESSION_LIFETIME_GAP 는 TERMINAL_STATES 에 포함되지 않는다
    (diagnostic, non-terminal-merge 보장).not inz%(py0)s not in %(py2)sr   r   r;   r=   r>   N
r   r   r@   rA   rB   rC   rD   rE   rF   rG   rH   rI   rJ   s      r/   =test_a_ci_watcher_session_lifetime_gap_not_in_terminal_statesr      so     +/AAAA*/AAAAAA*AAA*AAAAAA/AAA/AAAAAAAr1   c                    t         t        v } | st        j                  d| fdt         t        f      dt	        j
                         v st        j                  t               rt        j                  t               nddt	        j
                         v st        j                  t              rt        j                  t              nddz  }dd|iz  }t        t        j                  |            d} y)	uT   LIFETIME_GAP_DIAGNOSTIC_STATES 는 CI_WATCHER_SESSION_LIFETIME_GAP 를 포함한다.inz%(py0)s in %(py2)sr   r   r;   r=   r>   N)
r   r   r@   rA   rB   rC   rD   rE   rF   rG   r   s      r/   7test_a_lifetime_gap_diagnostic_states_contains_sentinelr      sq    *.LLLLL*.LLLLLLL*LLL*LLLLLL.LLLL.LLLLLLLLr1   c                    t        dd      \  } }d}| |k(  }|st        j                  d|fd| |f      dt        j                         v st        j
                  |       rt        j                  |       ndt        j                  |      dz  }d	d
|iz  }t        t        j                  |            dx}}d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j
                  |      rt        j                  |      ndt        j                  |      dz  }d	d
|iz  }t        t        j                  |            dx}}y)uD   ★ PR 이 terminal(MERGE_READY) 이면 stale 여도 gap 이 아님.TMERGE_READYr5   r   r8   z%(py0)s == %(py3)sr:   r<   r|   r}   r   Nno_lifetime_gapr?   	r   r@   rA   rB   rC   rD   rE   rF   rG   r:   r?   r   rH   r   r   s         r/   /test_a_classify_lifetime_gap_pr_terminal_no_gapr      s    -mIv 9?999&&6&&&&&6&&&&&&&6&&&6&&&&&&&&&&&r1   c                    t        dd      \  } }d}| |k(  }|st        j                  d|fd| |f      dt        j                         v st        j
                  |       rt        j                  |       ndt        j                  |      dz  }dd	|iz  }t        t        j                  |            d
x}}d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j
                  |      rt        j                  |      ndt        j                  |      dz  }dd	|iz  }t        t        j                  |            d
x}}y
)u-   watcher_stale=False → 무조건 gap 없음.Fr   r5   r8   r   r:   r   r}   r   Nr   r?   r   r   s         r/   -test_a_classify_lifetime_gap_not_stale_no_gapr      s    -EUWXIv9?999&&6&&&&&6&&&&&&&6&&&6&&&&&&&&&&&r1   c                    d} d}| |z  }t         |k(  }|st        j                  d|fdt         |f      dt        j                         v st        j
                  t               rt        j                  t               ndt        j                  |       t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x} x}}y)	uA   WATCHER_HEARTBEAT_STALE_THRESHOLD_SEC == 3600 (60분 convention).<   r8   )z%(py0)s == (%(py3)s * %(py5)s)r   )r<   r|   r   assert %(py8)srU   N)	r   r@   rA   rB   rC   rD   rE   rF   rG   )r   r   @py_assert6rH   r   @py_format9s         r/   .test_a_watcher_heartbeat_stale_threshold_valuer      sx    46;;BG;0G;;;;0G;;;;;;0;;;0;;;B;;;;;;;;;;r1   c            
        t         j                         } | j                  }d}||u }|st        j                  d|fd||f      dt        j                         v st        j                  |       rt        j                  |       ndt        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}t         j                  d	d
t        d   d| t        d            }|d   }d}||u }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }	dd|	iz  }
t        t        j                  |
            dx}x}}|d   }d}||k(  }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }	dd|	iz  }
t        t        j                  |
            dx}x}}|d   }d}||u }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }	dd|	iz  }
t        t        j                  |
            dx}x}}|d   }d}||u }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }	dd|	iz  }
t        t        j                  |
            dx}x}}| j                  }d}||u }|st        j                  d|fd||f      dt        j                         v st        j                  |       rt        j                  |       ndt        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}y)uF   returncode=0 → fired=True, callback_mandatory=True, not_wired=False.FrQ   z7%(py2)s
{%(py2)s = %(py0)s.normal_callback
} is %(py5)srw   ru   rv   r   N2729*   r   test_b_wiredtask_id	pr_numberterminal_stater?   rw   runnerfiredTr   r   r   rT   callback_statusWATCHER_TERMINAL_CALLBACK_WIREDr8   r   callback_mandatory	not_wired)rS   r   normal_callbackr@   rA   rB   rC   rD   rE   rF   rG   fire_terminal_callbackr   r3   )rw   rH   r   rY   r   r   r   r   r   rJ   r   s              r/   ,test_b_fire_terminal_callback_success_runnerr      sq   ##%E  )E) E)))) E))))))5)))5))) )))E)))))))

+
+&q) # , C w<4<4<4<4 !F%FF!%FFFFF!%FFFF!FFF%FFFFFFFF#$,,$,,,,$,,,$,,,,,,,,,,{$u$u$$$$u$$$$$$u$$$$$$$  (D( D(((( D((((((5(((5((( (((D(((((((r1   c            
     n   t         j                         } t         j                  dddd| t        d            }|d   }d}||u }|slt	        j
                  d	|fd
||f      t	        j                  |      t	        j                  |      dz  }dd|iz  }t        t	        j                  |            dx}x}}|d   }|t        k(  }|st	        j
                  d|fd|t        f      t	        j                  |      dt        j                         v st	        j                  t              rt	        j                  t              nddz  }dd|iz  }t        t	        j                  |            dx}}|d   }d}||u }|slt	        j
                  d	|fd
||f      t	        j                  |      t	        j                  |      dz  }dd|iz  }t        t	        j                  |            dx}x}}|d   }d}||u }|slt	        j
                  d	|fd
||f      t	        j                  |      t	        j                  |      dz  }dd|iz  }t        t	        j                  |            dx}x}}| j                  }	d}
|	|
u }|st	        j
                  d	|fd|	|
f      dt        j                         v st	        j                  |       rt	        j                  |       ndt	        j                  |	      t	        j                  |
      dz  }dd|iz  }t        t	        j                  |            dx}	x}}
y)uQ   returncode=1 → fired=False, callback_mandatory=True, not_wired=True, NOT_WIRED.r   r   HOLD_FOR_CHAIRtest_b_not_wired   r   r   FrQ   r   r   r   rT   Nr   r8   ry   r	   rz   r}   r   r   Tr   r   rw   ru   rv   r   )rS   r   r   r3   r@   rA   rE   rF   rG   r	   rB   rC   rD   r   )rw   r   r   rY   r   rJ   r   r   r   rH   r   r   s               r/   ,test_b_fire_terminal_callback_failure_runnerr      s	   ##%E

+
+'! # , C w< 5 <5    <5   <   5        !H!%HHHHH!%HHHH!HHHHHH%HHHH%HHHHHHHH#$,,$,,,,$,,,$,,,,,,,,,,{#t#t####t######t#######  )E) E)))) E))))))5)))5))) )))E)))))))r1   c                 z   d} d}t        | |      }|t        k(  }|st        j                  d|fd|t        f      dt	        j
                         v st        j                  t               rt        j                  t               ndt        j                  |       t        j                  |      t        j                  |      dt	        j
                         v st        j                  t              rt        j                  t              nddz  }d	d
|iz  }t        t        j                  |            dx} x}x}}y)zNwatcher_terminal_callback_status(terminal_reached=True, callback_fired=False).TF)terminal_reachedcallback_firedr8   )zY%(py6)s
{%(py6)s = %(py0)s(terminal_reached=%(py2)s, callback_fired=%(py4)s)
} == %(py8)sr   r	   )r<   r   r>   rT   rU   zassert %(py10)srh   N)
r   r	   r@   rA   rB   rC   rD   rE   rF   rG   )rH   rY   rZ   r[   r   @py_format11s         r/   1test_b_watcher_terminal_callback_status_not_wiredr      s    ;?OT($uUU.	/  U.     	)   	)   ;?   PU   	V     /   /      r1   c            
     t   t         j                         } t         j                  dddd| t        d            }|d   }d}||k(  }|slt	        j
                  d|fd	||f      t	        j                  |      t	        j                  |      d
z  }dd|iz  }t        t	        j                  |            dx}x}}y)uA   fire_terminal_callback 반환 dict 에 terminal_state 키 포함.r   r   LOOP_BOUNDARY	test_b_tsr   r   r   r8   r   r   r   rT   N)	rS   r   r   r3   r@   rA   rE   rF   rG   )rw   r   r   rY   r   rJ   r   s          r/   4test_b_fire_terminal_callback_returns_terminal_stater      s    ##%E

+
+& # , C  3O3 O3333 O333 333O3333333r1   c            
     ~   t         j                  } d}d}d} | |||      }d}||u }|st        j                  d|fd||f      dt	        j
                         v st        j                  t               rt        j                  t               ndt        j                  |       t        j                  |      t        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }d	d
|iz  }t        t        j                  |            dx} x}x}x}x}x}}y)u6   now_sec - last_event_sec >= quiet_window_sec → True.r   x   last_event_secnow_secquiet_window_secTrQ   z%(py10)s
{%(py10)s = %(py2)s
{%(py2)s = %(py0)s.is_quiet_window_settled
}(last_event_sec=%(py4)s, now_sec=%(py6)s, quiet_window_sec=%(py8)s)
} is %(py13)srS   rg   ri   rj   N
rS   is_quiet_window_settledr@   rA   rB   rC   rD   rE   rF   rG   rk   s	            r/   test_c_quiet_window_settledr     sG    	**&)<?*cC	
 		
 	 
 	
 	 
  
 	 	 
 	 	 
 	 	+ 
   
  '* 
  =@ 
 		
 
  	   
    r1   c            
     ~   t         j                  } d}d}d} | |||      }d}||u }|st        j                  d|fd||f      dt	        j
                         v st        j                  t               rt        j                  t               ndt        j                  |       t        j                  |      t        j                  |      t        j                  |      t        j                  |      t        j                  |      d	z  }d
d|iz  }t        t        j                  |            dx} x}x}x}x}x}}y)u6   now_sec - last_event_sec < quiet_window_sec → False.r   w   r   r   FrQ   r   rS   rg   ri   rj   Nr   rk   s	            r/   test_c_quiet_window_not_settledr     sG    	**&)<?*cC	
 		
 	 
 	
 	 
  
 	 	 
 	 	 
 	 	+ 
   
  '* 
  =@ 
 		
 
  	   
    r1   c                    t         j                  } d}| |k(  }|st        j                  d|fd| |f      dt	        j
                         v st        j                  t               rt        j                  t               ndt        j                  |       t        j                  |      dz  }dd|iz  }t        t        j                  |            dx} x}}y)	z&REVIEW_SETTLE_QUIET_WINDOW_SEC == 120.r   r8   )zF%(py2)s
{%(py2)s = %(py0)s.REVIEW_SETTLE_QUIET_WINDOW_SEC
} == %(py5)srS   ru   rv   r   N)
rS   REVIEW_SETTLE_QUIET_WINDOW_SECr@   rA   rB   rC   rD   rE   rF   rG   )rH   r   rY   r   r   s        r/   *test_c_review_settle_quiet_window_constantr     st    44;;4;;;;4;;;;;;:;;;:;;;4;;;;;;;;;;r1   c                 r   t         j                         } t         j                  |        | j                  }d}||u }|st	        j
                  d|fd||f      dt        j                         v st	        j                  |       rt	        j                  |       ndt	        j                  |      t	        j                  |      dz  }dd|iz  }t        t	        j                  |            dx}x}}| j                  }d	}||u }|st	        j
                  d|fd
||f      dt        j                         v st	        j                  |       rt	        j                  |       ndt	        j                  |      t	        j                  |      dz  }dd|iz  }t        t	        j                  |            dx}x}}y)uZ   mark_fallback_prune → state.fallback_prune==True 이지만 state.normal_callback==False.TrQ   )z6%(py2)s
{%(py2)s = %(py0)s.fallback_prune
} is %(py5)srw   ru   rv   r   NFr   )rS   r   mark_fallback_prunefallback_pruner@   rA   rB   rC   rD   rE   rF   rG   r   rw   rH   r   rY   r   r   s         r/   2test_d_fallback_prune_does_not_set_normal_callbackr     s   ##%E""5)'4'4''''4''''''5'''5''''''4'''''''  )E) E)))) E))))))5)))5))) )))E)))))))r1   c                 <   t        dd      } | j                  }d}||u }|st        j                  d|fd||f      dt	        j
                         v st        j                  |       rt        j                  |       ndt        j                  |      t        j                  |      dz  }dd	|iz  }t        t        j                  |            d
x}x}}| j                  }d}||u }|st        j                  d|fd||f      dt	        j
                         v st        j                  |       rt        j                  |       ndt        j                  |      t        j                  |      dz  }dd	|iz  }t        t        j                  |            d
x}x}}| j                  }|t        k(  }|st        j                  d|fd|t        f      dt	        j
                         v st        j                  |       rt        j                  |       ndt        j                  |      dt	        j
                         v st        j                  t              rt        j                  t              nddz  }dd|iz  }t        t        j                  |            d
x}}y
)ub   fallback만 등록 시 evaluate_progress_watcher_gate → fallback_only=True, DISPATCH_INCOMPLETE.FT)watcher_registeredfallback_registeredrQ   )z2%(py2)s
{%(py2)s = %(py0)s.incomplete
} is %(py5)sgrru   rv   r   N)z5%(py2)s
{%(py2)s = %(py0)s.fallback_only
} is %(py5)sr8   )z7%(py2)s
{%(py2)s = %(py0)s.dispatch_status
} == %(py4)sr   )r<   r   r>   r   rT   )r   
incompleter@   rA   rB   rC   rD   rE   rF   rG   fallback_onlydispatch_statusr   )r   rH   r   rY   r   r   rJ   r   s           r/   $test_d_fallback_only_gate_incompleter   '  ss   	'5VZ	[B== D =D    =D      2   2   =   D       #t#t####t######2###2######t#######4!44444!444444424442444444444!4444!44444444r1   c                 \   ddl m}  d}| |k(  }|st        j                  d|fd| |f      dt	        j
                         v st        j                  |       rt        j                  |       ndt        j                  |      dz  }dd	|iz  }t        t        j                  |            d
x}}y
)u_   FALLBACK_ROLE_SINGLE_PURPOSE == 'RECOVERY_ONLY_NO_FINAL_REPORT_TRIGGER' doctrine 상수 검증.r   )FALLBACK_ROLE_SINGLE_PURPOSE%RECOVERY_ONLY_NO_FINAL_REPORT_TRIGGERr8   r   r   r   r}   r   N)
(dispatch.normal_fallback_callback_helperr   r@   rA   rB   rC   rD   rE   rF   rG   )r   r   rH   r   r   s        r/   ,test_d_fallback_role_single_purpose_doctriner   /  se    U+RR'+RRRRR'+RRRRRRR'RRR'RRR+RRRRRRRRr1   c                    t         j                         } | j                  } |       }d}||u }|st        j                  d|fd||f      dt        j                         v st        j                  |       rt        j                  |       ndt        j                  |      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}x}}y)	u,   새 WatcherState → all_tracked() == False.FrQ   zJ%(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.all_tracked
}()
} is %(py7)srw   r<   r   r>   r   assert %(py9)sr   N)rS   r   all_trackedr@   rA   rB   rC   rD   rE   rF   rG   )rw   rH   rY   r   rZ   r   @py_format10s          r/   "test_e_all_tracked_initially_falser   9  s    ##%E''%'%''''%''''''5'''5'''''''''%'''''''r1   c            	        t         j                         } t         j                  |        t         j                  |        t         j	                  |        t         j                  |        t         j                  |        t         j                  |        | j                  } |       }d}||u }|st        j                  d|fd||f      dt        j                         v st        j                  |       rt        j                  |       ndt        j                  |      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                   |            dx}x}x}}| j"                  } |       }t%        |      }d}	||	u }
|
st        j                  d|
fd	||	f      d
t        j                         v st        j                  t$              rt        j                  t$              nd
dt        j                         v st        j                  |       rt        j                  |       ndt        j                  |      t        j                  |      t        j                  |      t        j                  |	      dz  }dd|iz  }t        t        j                   |            dx}x}x}x}
}	y)u4   6개 mark_* 모두 호출 후 all_tracked() == True.TrQ   r   rw   r   r   r   N)zm%(py7)s
{%(py7)s = %(py0)s(%(py5)s
{%(py5)s = %(py3)s
{%(py3)s = %(py1)s.as_tracked_dict
}()
})
} is %(py10)sr   )r<   r{   r|   r   r   rh   zassert %(py12)spy12)rS   r   mark_head_changemark_non_force_pushmark_cimark_finish_donemark_normal_callbackr   r   r@   rA   rB   rC   rD   rE   rF   rG   as_tracked_dictr   )rw   rH   rY   r   rZ   r   r   r   r   r]   @py_assert8r   @py_format13s                r/   "test_e_all_tracked_after_six_marksr  ?  s   ##%E&""5)u&##E*""5)&&$&$&&&&$&&&&&&5&&&5&&&&&&&&&$&&&&&&&#33>35>56>$>6$>>>>6$>>>>>>>>>>>>>>>e>>>e>>>3>>>5>>>6>>>$>>>>>>>>r1   c                    t         j                         } | j                  } |       }|j                  } |       }t	        |      }|t
        k(  }|st        j                  d|fd|t
        f      dt        j                         v st        j                  t              rt        j                  t              nddt        j                         v st        j                  |       rt        j                  |       ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      t        j                  |      dt        j                         v st        j                  t
              rt        j                  t
              nddz  }dd|iz  }t        t        j                  |            d	x}x}x}x}x}}y	)
u   ★ tuple(state.as_tracked_dict().keys()) == WATCHER_TRACKED_STATES
    (heartbeat 필드 추가가 6-state dict 를 오염시키지 않음 보장).r8   )z%(py11)s
{%(py11)s = %(py0)s(%(py9)s
{%(py9)s = %(py7)s
{%(py7)s = %(py5)s
{%(py5)s = %(py3)s
{%(py3)s = %(py1)s.as_tracked_dict
}()
}.keys
}()
})
} == %(py13)stuplerw   r
   )r<   r{   r|   r   r   r   r   rV   ri   rj   N)rS   r   r  keysr  r
   r@   rA   rB   rC   rD   rE   rF   rG   )	rw   r   r   r   r  r\   rl   r_   rn   s	            r/   *test_e_as_tracked_dict_keys_match_constantr	  L  s    ##%E&&J&(J(--J-/J5/0J04JJJJJ04JJJJJJJ5JJJ5JJJJJJJJJJJJ&JJJ(JJJ-JJJ/JJJ0JJJJJJ4JJJJ4JJJJJJJJJr1   c                 <   t        t              } d}| |k(  }|st        j                  d|fd| |f      dt	        j
                         v st        j                  t               rt        j                  t               nddt	        j
                         v st        j                  t              rt        j                  t              ndt        j                  |       t        j                  |      dz  }dd|iz  }t        t        j                  |            d	x} x}}y	)
u   TERMINAL_STATES 길이 == 5.   r8   )z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py6)slenr   )r<   r{   r|   rT   r   rU   N)
r  r   r@   rA   rB   rC   rD   rE   rF   rG   )r   rZ   r   r   r   s        r/   "test_e_terminal_states_length_fiver  S  s    $1$1$$$$1$$$$$$3$$$3$$$$$$$$$$$$$$$1$$$$$$$r1   c                    t         t        v} | st        j                  d| fdt         t        f      dt	        j
                         v st        j                  t               rt        j                  t               nddt	        j
                         v st        j                  t              rt        j                  t              nddz  }dd|iz  }t        t        j                  |            d} y)	uB   CI_WATCHER_SESSION_LIFETIME_GAP 는 TERMINAL_STATES 에 미포함.r   r   r   r   r;   r=   r>   Nr   r   s      r/   ,test_e_terminal_states_excludes_lifetime_gapr  X  sm    */AAAA*/AAAAAA*AAA*AAAAAA/AAA/AAAAAAAr1   c                    t        dd      } t        j                         }t        j                  | dd|      \  }}|t        v }|st        j                  d|fd|t        f      d	t        j                         v st        j                  |      rt        j                  |      nd	d
t        j                         v st        j                  t              rt        j                  t              nd
dz  }dd|iz  }t        t        j                  |            d}d}||k(  }|st        j                  d|fd||f      d	t        j                         v st        j                  |      rt        j                  |      nd	t        j                  |      dz  }dd|iz  }	t        t        j                  |	            dx}}d}
|
|v }|st        j                  d|fd|
|f      t        j                  |
      dt        j                         v st        j                  |      rt        j                  |      nddz  }dd|iz  }	t        t        j                  |	            dx}
}|j                  }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }	dd|	iz  }t        t        j                  |            dx}x}}|j                  }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }	dd|	iz  }t        t        j                  |            dx}x}}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_secrw   r   r   r   r   r;   r=   r>   Nr   r8   r   r   r}   r   
head_drift)z%(py1)s in %(py3)sr?   rz   r   )z-%(py2)s
{%(py2)s = %(py0)s.polls
} == %(py5)srw   ru   rv   r   )z3%(py2)s
{%(py2)s = %(py0)s.elapsed_sec
} == %(py5)s)r   rS   r   run_oncer   r@   rA   rB   rC   rD   rE   rF   rG   pollselapsed_sec)snaprw   r   r?   rH   rI   rJ   r   r   r   r   r   rY   r   s                 r/   1test_e_run_once_head_drift_returns_hold_for_chairr  ]  sA   #4SD##%E'00'	 1 NF _,,,,>_,,,,,,>,,,>,,,,,,_,,,_,,,,,,,-->----->------->--->-----------!<6!!!!<6!!!<!!!!!!6!!!6!!!!!!!;;!;!;!55;!""""""""""""5"""5"""""""""""""r1   c                 H   t         j                         } | j                  }d}||u }|st        j                  d|fd||f      dt        j                         v st        j                  |       rt        j                  |       ndt        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}| j                  }d}||u }|st        j                  d|fd||f      dt        j                         v st        j                  |       rt        j                  |       ndt        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}y)	u:   새 WatcherState → last_poll_ts=None, heartbeat_ts=None.NrQ   rs   rw   ru   rv   r   )z4%(py2)s
{%(py2)s = %(py0)s.heartbeat_ts
} is %(py5)s)rS   r   rO   r@   rA   rB   rC   rD   rE   rF   rG   r   r   s         r/   )test_e_watcher_state_new_has_no_heartbeatr   n  s    ##%E%%%%%%%%%%%%5%%%5%%%%%%%%%%%%%%%%%%%%%%%%%5%%%5%%%%%%%%%%%%%r1   c                    t         j                         } | j                         }d}||v}|st        j                  d|fd||f      t        j
                  |      dt        j                         v st        j                  |      rt        j
                  |      nddz  }dd|iz  }t        t        j                  |            dx}}d	}||v}|st        j                  d|fd||f      t        j
                  |      dt        j                         v st        j                  |      rt        j
                  |      nddz  }dd|iz  }t        t        j                  |            dx}}y)
uM   last_poll_ts / heartbeat_ts 는 as_tracked_dict() 에 포함되지 않는다.rO   r   )z%(py1)s not in %(py3)sdrz   r}   r   Nr   )rS   r   r  r@   rA   rE   rB   rC   rD   rF   rG   )rw   r"  r   r   r   r   s         r/   +test_e_heartbeat_fields_not_in_tracked_dictr#  u  s    ##%EA">"""">""">""""""""""""""""">"""">""">""""""""""""""""r1   )r$   int)i__doc__
__future__r   builtinsrB   _pytest.assertion.rewrite	assertionrewriter@   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_locationr   rH   r   rY   r   loaderr\   @py_assert13rl   rA   rC   rD   rE   r   r   append@py_format15@py_format17_format_boolop@py_format18@py_format20rF   rG   module_from_specrS   exec_moduler3   rL   r`   ro   rq   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r	  r  r  r  r   r#  r2   r1   r/   <module>rN     s   #      
  	X&&(003J	388HHOOI 388 9  #++ #E
e..{;{{5!T:t,2}}Y'E"#   y #??../H,W 5D 5uD 5U\\ 5 5\5 5 5 5uD 5 5 5 5 5u 5 5 5u 5 5 5D 5 5 5 5 5 5 5\ 5 5 5 5 5U 5 5 5U 5 5 5\ 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5^^,,U3
)3% &    $P

"1#BM
''<)&*$4$<*5S(
?K%
B
#"&#r1   