
    ' j';              	         d Z ddlmZ ddlZddlZddlmZmZmZ ddlm	Z	 ddl
mZ ddlZ e	e      j                         j                  j                  j                  Z ee      ej$                  v r!ej$                  j'                   ee             ej$                  j)                  d ee             ddlmZmZmZmZmZ ddlmZmZmZ  ed	d
ddddej>                        Z dZ!dddd	 	 	 	 	 	 	 d"dZ"d#dZ# G d d      Z$ G d d      Z% G d d      Z& G d d      Z' G d d      Z( G d d      Z)e*d k(  r% ejV                   ejX                  ed!g             yy)$u(  tests/regression/test_schedule_id_freshness_2535.py — 회귀 6건.

회장 명시 task-2535 P0 (신호등 sync fix C / 2026-05-10):
  cron schedule_id 신선도 검증 부재 → 오래된 schedule_id 로 잘못된 활성 판정 발생.
  Fix: utils/schedule_id_freshness.py (validator) + lifecycle_reconciliation_manager
  STALE_SCHEDULE_ID stuck case.

회귀 6건 (회장 §명시):
  1. fresh (5분 전 응답)        → classify_freshness = FRESH
  2. stale (90분 전 응답)       → classify_freshness = STALE
  3. missing (history 부재)     → classify_freshness = MISSING
  4. lifecycle stuck STALE_SCHEDULE_ID 분류
  5. chat=6937032012 격리 (다른 chat record 는 freshness 판정에 포함 X)
  6. token raw 0 (validator 출력 / stuck detail 에 ghs_/ghp_/github_pat_ prefix 부재)
    )annotationsN)datetime	timedeltatimezone)Path)Any) SCHEDULE_FRESHNESS_THRESHOLD_MINCHAIRMAN_CHAT_IDclassify_freshnessis_schedule_id_freshschedule_id_age_seconds)LifecycleEvidenceStuckReasondetect_stuck_casesi     
      )tzinfo)ghs_ghp_github_pat_okp)statusresponsepromptc          	         |j                         |||||ddd}| j                  dd      5 }|j                  t        j                  |d      d	z          d d d        y # 1 sw Y   y xY w)
Ni  z/tmp/workspace)tschat_idschedule_idr   r   r   duration_msworkspace_pathautf-8encodingFensure_ascii
)	isoformatopenwritejsondumps)	pathr   r   r    r   r   r   recordfs	            G/home/jay/workspace/tests/regression/test_schedule_id_freshness_2535.py_write_recordr3   5   sn     lln"*	F 
3	) ?Q	

66=>? ? ?s   *A  A)c                     t        di 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dd dddd}|j                  |        t        di |S )Ntask_idz	task-9999	pr_numberpr_statemerge_commitmerged_into_mainF	ci_statussmoke_statustimer_statustimer_end_timehas_donehas_done_ackedhas_merge_donehas_qc_resulthas_followuphas_escalate_markerescalate_marker_age_minutestelegram_reply_truncatedbot_session_statusworktree_existsbranch_pushed_to_remote )dictupdater   )	overridesbases     r2   _make_evidencerN   E   s       	
           "  %)!" "'#$  %& '( !&)D, 	KK	$t$$    c                  (    e Zd ZdZddZddZddZy)TestFreshRecordu>   회귀 #1: 마지막 chairman record 가 5분 전이면 FRESH.c                    d}|| dz  }t        |t        t        d      z
  t        |d       t	        |t        |      }|dk(  sJ t        |t        |	      \  }}|d
u sJ |dk(  sJ y )NABCDEF12.logr   minutesr   r   r   r    r   nowhistory_dirFRESHrZ   Tr3   NOWr   r
   r   r   selftmp_pathsidlogstateis_freshtokens          r2   test_classify_freshz#TestFreshRecord.test_classify_freshg   s    C5%ccIa$88BR"%	6 #3CXF.sCXN%4rO   c                    d}|| dz  }t        |t        t        d      z
  t        |       t	        |t        |      }|J d|cxk  rdk  sJ  J y )	N	F12RESH00rT   r   rU   r   r   r    rX   i"  i6  )r3   r^   r   r
   r   )r`   ra   rb   rc   ages        r2   test_age_seconds_for_freshz*TestFreshRecord.test_age_seconds_for_fresht   sf    C5%ccIa$88BR"%	' &csIc S     rO   c                    d}|| dz  }t        |t        t        dd      z
  t        |       t	        |t        |      dk(  sJ y)	u<   Threshold = 60min → 59m59s 전 record 는 여전히 FRESH.BNDRY001rT   ;   )rV   secondsrj   rX   r[   Nr3   r^   r   r
   r   r`   ra   rb   rc   s       r2   "test_threshold_boundary_just_underz2TestFreshRecord.test_threshold_boundary_just_under~   sL    C5%ccIb"$EE.C	A!#3HEPPPrO   Nra   r   )__name__
__module____qualname____doc__rg   rl   rs   rI   rO   r2   rQ   rQ   d   s    H !QrO   rQ   c                  &    e Zd ZdZddZddZd Zy)TestStaleRecordu?   회귀 #2: 마지막 chairman record 가 90분 전이면 STALE.c                    d}|| dz  }t        |t        t        d      z
  t        |       t	        |t        |      }|dk(  sJ t        |t        |      \  }}|d	u sJ |dk(  sJ y )
NSTALE001rT   Z   rU   rj   rX   STALEr\   Fr]   r_   s          r2   test_classify_stalez#TestStaleRecord.test_classify_stale   s    C5%ccIb$99CS"%	' #3CXF.sCXN%5   rO   c                    d}|| dz  }t        |t        t        d      z
  t        |       t	        |t        |      dk(  sJ y)	u?   정확히 60분 → STALE (≥ threshold 는 stale 로 박제).BNDRY060rT   <   rU   rj   rX   r~   Nrq   rr   s       r2   %test_threshold_boundary_exactly_60minz5TestStaleRecord.test_threshold_boundary_exactly_60min   sJ    C5%ccIb$99CS"%	'!#3HEPPPrO   c                    t         dk(  sJ y)u8   SCHEDULE_FRESHNESS_THRESHOLD_MIN 은 회장 §명시 60.r   N)r	   r`   s    r2   test_constant_valuez#TestStaleRecord.test_constant_value   s    /2555rO   Nrt   )ru   rv   rw   rx   r   r   r   rI   rO   r2   rz   rz      s    I Q6rO   rz   c                  (    e Zd ZdZddZddZddZy)TestMissingRecordu8   회귀 #3: schedule_history/{ID}.log 부재 → MISSING.c                    d}t        |t        |      }|dk(  sJ t        |t        |      \  }}|du sJ |dk(  sJ t        |t        |      J y )NNONEXISTrX   MISSINGr\   F)r   r^   r   r   )r`   ra   rb   rd   re   rf   s         r2   %test_classify_missing_when_log_absentz7TestMissingRecord.test_classify_missing_when_log_absent   sf    "3CXF	!!!.sCXN%5   	!!!&sJRRRrO   c                2    t        dt        |      dk(  sJ y )N rX   r   )r   r^   )r`   ra   s     r2   &test_empty_schedule_id_returns_missingz8TestMissingRecord.test_empty_schedule_id_returns_missing   s    !"#8D	QQQrO   c                    d}|| dz  }t        |t        t        d      z
  d|       t        |t        |      dk(  sJ y	)
u9   다른 chat 만 있는 log 는 MISSING (chairman 격리).OTHERCHTrT   r   rU    rj   rX   r   Nr3   r^   r   r   rr   s       r2   ,test_log_with_no_chairman_records_is_missingz>TestMissingRecord.test_log_with_no_chairman_records_is_missing   sI    C5%ccIb$997"%	'!#3HERRRrO   Nrt   )ru   rv   rw   rx   r   r   r   rI   rO   r2   r   r      s    B	SRSrO   r   c                  .    e Zd ZdZd Zd Zd Zd Zd Zy) TestLifecycleStuckClassificationu   회귀 #4: lifecycle_reconciliation_manager.detect_stuck_cases 가
    STALE schedule_id + timer running → STALE_SCHEDULE_ID 박제.c                    t        ddddd      }t        |      }|D ch c]  }|j                   }}t        j                  |v sJ y c c}w )Nz	task-9501runningrS   r~        @r5   r<   r    schedule_id_freshnessr   rN   r   reasonr   STALE_SCHEDULE_IDr`   evcasescreasonss        r2   /test_stuck_case_emitted_for_stale_running_timerzPTestLifecycleStuckClassification.test_stuck_case_emitted_for_stale_running_timer   sW    """)$*
 #2&%*+188++,,777 ,   A
c                    t        ddddd      }t        |      }|D ch c]  }|j                   }}t        j                  |vsJ y c c}w )Nz	task-9502r   FRESH001r[   g     r@r   r   r   s        r2   test_no_stuck_when_freshz9TestLifecycleStuckClassification.test_no_stuck_when_fresh   sW    """)$)
 #2&%*+188++,,G;;; ,r   c                    t        ddddd      }t        |      }|D ch c]  }|j                   }}t        j                  |vsJ y c c}w )Nz	task-9503	completedr|   r~   r   r   r   r   s        r2   $test_no_stuck_when_timer_not_runningzETestLifecycleStuckClassification.test_no_stuck_when_timer_not_running   sW    $"")$*
 #2&%*+188++,,G;;; ,r   c           	         t        ddddddd      }t        |      }|D ch c]  }|j                   }}t        j                  |vsJ y	c c}w )
uU   PR MERGED 인 task 는 다른 stuck reason 이 처리 — STALE_SCHEDULE_ID 박제 X.z	task-9504MERGEDTr   STALE002r~   r   )r5   r7   r9   r<   r    r   r   Nr   r   s        r2   $test_no_stuck_when_pr_already_mergedzETestLifecycleStuckClassification.test_no_stuck_when_pr_already_merged   s]    !""")$*
 #2&%*+188++,,G;;; ,s   Ac                    t        ddddd      }t        |      }|D ch c]  }|j                   }}t        j                  |vsJ yc c}w )u]   schedule_id_freshness=MISSING 은 보류 (stuck 박제 X — 매핑 부재만으로 stuck X).z	task-9505r   Nr   r   r   r   s        r2   *test_missing_freshness_does_not_emit_stuckzKTestLifecycleStuckClassification.test_missing_freshness_does_not_emit_stuck  sW    ""+$(
 #2&%*+188++,,G;;; ,r   N)	ru   rv   rw   rx   r   r   r   r   r   rI   rO   r2   r   r      s!    G
8
<
<<<rO   r   c                  .    e Zd ZdZd ZddZddZddZy)TestChairmanChatIsolationuR   회귀 #5: chat=6937032012 외 record 는 freshness 판정에 포함되지 않음.c                    t         dk(  sJ y )Nl   L5: )r
   r   s    r2   test_chairman_chat_id_constantz8TestChairmanChatIsolation.test_chairman_chat_id_constant  s    :---rO   c                    d}|| dz  }t        |t        t        d      z
  d|       t        |t        t        d      z
  t        |       t	        |t        |      }|d	k(  sJ d
       y)u  동일 schedule_id 에 다른 chat 의 fresh record 가 있어도 chairman record 가
        STALE 이면 STALE 로 박제 (다른 chat record 는 격리되어 freshness 판정 미참여).

        시나리오:
          - schedule_id 같은 log 파일에
          - chairman chat (6937032012) 의 90분 전 record
          - 다른 chat (9999999) 의 5분 전 record (포함되면 안 됨)
          → 기대: STALE (chairman 만 보면 90분 전 → STALE)
        ISOL0001rT   r   rU   r   rj   r}   rX   r~   uC   다른 chat record 가 포함되면 안 됨 — chairman 만 사용Nrq   )r`   ra   rb   rc   rd   s        r2   test_other_chat_records_ignoredz9TestChairmanChatIsolation.test_other_chat_records_ignored  sv     C5%ccIa$88'"%	' 	ccIb$99CS"%	' #3CXFf!ffrO   c                    d}|| dz  }dD ]"  }t        |t        t        |      z
  d|       $ t        |t        |      dk(  sJ y )	NISOL0002rT   )r   r         (   rU   r   rj   rX   r   r   )r`   ra   rb   rc   offsets        r2   *test_only_other_chat_records_yield_missingzDTestChairmanChatIsolation.test_only_other_chat_records_yield_missing/  sZ    C5%) 	+F##	&(A"A7&)+	+
 "#3HERRRrO   c                    d}|| dz  }t         t        d      z
  j                         d|dddd}|j                  t	        j
                  |d	
      dz   d       t        |t         |      dk(  sJ y)uW   chat_id 가 문자열 '6937032012' 로 직렬화돼도 정상 인식 (cokacdir 호환).STR_CHIDrT   r   rU   
6937032012r   r   )r   r   r    r   r   r   Fr'   r)   r$   r%   rX   r[   N)r^   r   r*   
write_textr-   r.   r   )r`   ra   rb   rc   r0   s        r2   +test_chairman_record_string_chat_id_handledzETestChairmanChatIsolation.test_chairman_record_string_chat_id_handled:  s|    C5%2..99;#
 	tzz&u=DwW!#3HEPPPrO   Nrt   )ru   rv   rw   rx   r   r   r   r   rI   rO   r2   r   r     s    \.g,	SQrO   r   c                  $    e Zd ZdZddZd Zd Zy)TestTokenRawZerouE   회귀 #6: validator 출력 / stuck detail 에 token raw prefix 0건.c                    d}|| dz  }t        |t        t        d      z
  t        |d       t	        |t        |      }|dk(  sJ t
        D ]  }||vrJ  y	)
ua   log 의 response 본문에 token 이 있어도 freshness 판정 결과에는 노출되지 않음.TOKEN001rT   r   rU   z-this should not leak: ghs_FAKE_TOKEN_ABCDEFGHrW   rX   r[   N)r3   r^   r   r
   r   TOKEN_PREFIXES)r`   ra   rb   rc   rd   prefixs         r2   3test_classify_freshness_does_not_leak_response_bodyzDTestTokenRawZero.test_classify_freshness_does_not_leak_response_bodyQ  sr    C5%ccIb$99CS"%N	P #3CXF$ 	'F&&&	'rO   c                   t        ddddd      }t        |      }|D cg c]"  }|j                  t        j                  k(  s!|$ }}t        |      dk(  sJ |d   j                  }t        D ]  }||vrJ d	| d
        y c c}w )Nz	task-2535r   rS   r~   r   r      r   ztoken prefix z leaked in detail)rN   r   r   r   r   lendetailr   )r`   r   r   r   stale_casesr   r   s          r2   *test_stuck_detail_contains_no_token_prefixz;TestTokenRawZero.test_stuck_detail_contains_no_token_prefix`  s    """)$*
 #2&"'UQ188{7T7T+TqUU;1$$$Q&&$ 	SF'R=@Q)RR'	S Vs   "BBc                    t         dz  dz  }|j                  d      }t        D ]  }||vrJ | d|j                           y)u]   모듈 소스 자체에 token raw prefix 가 없는지 회귀 (회장 §금지 token 박제).utilszschedule_id_freshness.pyr$   r%   z
 found in N)_WORKTREE_ROOT	read_textr   name)r`   src_pathtextr   s       r2   #test_module_source_no_token_stringsz4TestTokenRawZero.test_module_source_no_token_stringso  sX    !G+.HH!!7!3$ 	LF%K&HMM?'KK%	LrO   Nrt   )ru   rv   rw   rx   r   r   r   rI   rO   r2   r   r   N  s    O'SLrO   r   __main__z-v)r/   r   r   r   r   intr    strr   r   r   r   r   r   returnNone)rL   r   r   r   )-rx   
__future__r   r-   sysr   r   r   pathlibr   typingr   pytest__file__resolveparentr   r   r/   removeinsertutils.schedule_id_freshnessr	   r
   r   r   r   &utils.lifecycle_reconciliation_managerr   r   r   utcr^   r   r3   rN   rQ   rz   r   r   r   r   ru   exitmainrI   rO   r2   <module>r      se   #  
 2 2   h'')0077>>~#(("HHOOC'( 3~& '   tQB1X\\:0 !%d#??03?DG?RV? %> Q  QN6 6BS S<B< B<R4Q 4Qv&L &LR zCHH[V[[(D)*+ rO   