
     jZ                    J   d Z ddlm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  ee      j                         j                   d   Z ee      ej&                  vr"ej&                  j)                  d ee             ddlmZmZmZmZmZ  ee      j6                  j6                  d	z  Zdd
Zddddded	 	 	 ddZddZddZ ddZ!d dZ"d!dZ#d"d#dZ$d$dZ%d$dZ&d$dZ'd$dZ(d$dZ) ed      d$d       Z* ed      d$d       Z+ ed      d$d       Z,d$dZ-y)%u4  anu_v2.tests.test_pr_open_gemini_trigger_prevention_2544 — 회귀 9건 (task-2544).

회귀 케이스 (회장 §명시):
  1. preflight_base_head_fresh             — behind=0, diverged=False → fresh=True
  2. preflight_ref_fetchability_ok         — fetch exit=0, merge-base exit=0 → fetchable=True
  3. preflight_git_exit_128_detection      — fetch exit=128 → fetchable=False, git_exit_code=128
  4. pr_open_head_ref_oid_mismatch         — headRefOid != pushed_sha → match=False, INTERNAL_HEAD_MISMATCH
  5. gemini_review_gate_check_missing      — check-runs에 gemini-review-gate 없음 + git_exit_128=True
  6. first_evidence_grace_window_expiry    — poller 항상 [], clock으로 180초 만료 → call_count ≤ 6
  7. pr86_fixture_external_trigger         — fixture 기반 run() → EXTERNAL_TRIGGER_REQUIRED
  8. normal_pr_open_classification_ok      — 모든 정상 → PR_OPEN_GEMINI_TRIGGER_OK
  9. post_merge_audit_warn_to_pass_spec    — spec 정합 + isolation 검증

본 회귀는 anu_v2/* 모듈만 import 한다 (one-way isolation).
    )annotationsN)Path)Any)Mockpatch   )(CLASSIFICATION_EXTERNAL_TRIGGER_REQUIRED%CLASSIFICATION_INTERNAL_HEAD_MISMATCHCLASSIFICATION_OKDEFAULT_CHAT_IDPROpenGeminiTriggerPreventionfixturesc                    | j                  d      s|  d} t        j                  t        | z  j	                  d            S )u   Local helper for fixture JSON loading. Test 파일 내부 한정.

    회장 §명시 (2026-05-10): anu_v2/fixtures/__init__.py 외부에 두지 않음.
    Critical #3 사고 재발 방지를 위한 local 정의.z.jsonzutf-8)encoding)endswithjsonloads_FIXTURE_DIR	read_text)names    O/home/jay/workspace/anu_v2/tests/test_pr_open_gemini_trigger_prevention_2544.py_load_fixturer   *   s=    
 ==!u~::|d*55w5GHH    	gh_runner
git_runnerevidence_pollerclockaudit_writerchat_idc                $    t        | |||||      S )u?   테스트용 PROpenGeminiTriggerPrevention 인스턴스 생성.r   )r   r   s         r   _make_moduler"   6   s#     )'! r   c                     ddidfd} | S )u=   fresh 상태 git_runner stub: behind=0, diverged=0, fetch=OK.nr   c                n    dxx   dz  cc<   dj                  |       }d|v rdddS d|v rdd	dS dddS )
Nr$       fetchr    	exit_codestdoutrev-list0join)_argscmd
call_counts     r   r   z#_git_stub_fresh.<locals>.git_runnerN   sN    31hhuoc>!"b11!"c22"--r   r1   	list[str]returndict )r   r3   s    @r   _git_stub_freshr9   J   s    qJ. r   c                     dd} | S )z>fetchable OK git_runner stub: fetch exit=0, merge-base exit=0.c                d    dj                  |       }d|v rdddS d|v rdddS d|v rdd	dS dddS )
Nr'   r(   r   r)   r*   
merge-baseabc123r-   r.   r/   r1   r2   s     r   r   z*_git_stub_fetchable_ok.<locals>.git_runner\   sR    hhuoc>!"b113!"h77!"c22"--r   r4   r8   r   s    r   _git_stub_fetchable_okr@   Z   s    . r   c                     dd} | S )z6exit 128 git_runner stub: fetch exit=128 (broken ref).c                B    dj                  |       }d|v rddddS dddS )	Nr'   r(      r)   zfatal: unable to fetch)r+   r,   stderrr   r*   r/   r>   s     r   r   z&_git_stub_exit_128.<locals>.git_runnerk   s/    hhuoc>!$>VWW"--r   r4   r8   r?   s    r   _git_stub_exit_128rE   i   s    . r   c                     d fd}|S )z#headRefOid mismatch gh_runner stub.c                "    d | v rdiS dg iS )Npulls/
headRefOid
check_runsr8   )endpointr1   
actual_sha	pr_numbers     r   r   z,_gh_stub_pr_head_mismatch.<locals>.gh_runnerv   s(    I;8+ *--b!!r   rK   strr1   r5   r6   r7   r8   )rM   _expected_sharL   r   s   ` ` r   _gh_stub_pr_head_mismatchrQ   t   s    " r   c                     d fd}|S )u  gemini-review-gate 없는 check-runs + git_exit_128 시뮬레이션용 gh_runner.

    check-runs에 gemini-review-gate가 없고, exit 128 여부는 gate_present 내부 로직에서 결정.
    여기서는 failure + exit 128 텍스트가 들어있는 다른 check로 대신 커버.
    실제로는 check_present=False → git_exit_128은 별도로 preflight에서 옴.
    이 stub은 check-runs를 빈 배열로 반환해 check_present=False로 만든다.
    c                *    d| v rdddddidgiS diS )	N
check-runsrJ   zci-buildfailuretextzexit 128 fatal: r   
conclusionoutputrI   r8   )rK   r1   head_shas     r   r   z5_gh_stub_gate_missing_with_exit128.<locals>.gh_runner   s@    8#  *&/#)+=">  h''r   rN   r8   )rZ   r   s   ` r   "_gh_stub_gate_missing_with_exit128r[      s    ( r   c                     ddidfd}|S )uQ   clock stub: 매 호출마다 interval씩 증가. grace_seconds 초과 시 만료.t        c                 ,    d   } dxx   z  cc<   | S )Nr]   r8   )valintervalstates    r   r   z"_clock_advance_stub.<locals>.clock   s    Cjc
h

r   r6   floatr8   )_grace_secondsra   r   rb   s    ` @r   _clock_advance_stubrf      s    #JE
 Lr   c                     t        t                     } | j                  dd      }|d   du s
J d|        |d   dk(  sJ |d	   d
u sJ |d   g k(  sJ y)u9   git_runner stub: behind=0, diverged=False → fresh=True.r?   mainzfeature/testfreshTzExpected fresh=True, got behind_countr   head_divergedFreasonsN)r"   r9   preflight_base_head_freshnessmodresults     r   test_preflight_base_head_freshrq      sy    
/"3
4C..v~FF'?d"H&?x$HH".!Q&&&/"e+++)"""r   c                     t        t                     } | j                  d      }|d   du s
J d|        |d   du sJ |d   J y)	u`   git_runner stub: fetch exit=0, merge-base exit=0 → fetchable=True, merge_base_resolvable=True.r?   abc123deadbeef	fetchableTzExpected fetchable=True, got merge_base_resolvablegit_exit_codeN)r"   r@   preflight_ref_fetchabilityrn   s     r   "test_preflight_ref_fetchability_okrx      sf    
"8":
;C++,<=F+$&P*Gx(PP&)*d222/"***r   c                     t        t                     } | j                  d      }|d   du s
J d|        |d   dk(  sJ |d   du sJ y	)
uT   git_runner stub: fetch exit=128 (broken ref) → fetchable=False, git_exit_code=128.r?   deadbeef0000rt   FzExpected fetchable=False, got rv   rC   ru   N)r"   rE   rw   rn   s     r   %test_preflight_git_exit_128_detectionr{      sg    
"4"6
7C++N;F+%'R+I&)RR'/"c))))*e333r   c                 J   d} d}d}t        || |      }t        |      }|j                  ||       }|d   du sJ |d   |k(  sJ |d   | k(  sJ d	d
dd	d	dd}d	ddg d}ddddd}|j                  ||||      }	|	d   t        k(  sJ |	d   d	u sJ |	d   du sJ |	d   dk(  sJ y)uc   gh_runner stub: pulls/{n} returns headRefOid != pushed_sha → match=False, INTERNAL_HEAD_MISMATCH.aaaabbbbcccc1111ddddeeee22223333c   r   matchFgithub_head_sha
pushed_shaTr   Nri   rj   rk   rt   ru   rv   successcheck_presentgit_exit_128rX   internal_cause_candidate
   )evidence_arrivedelapsed_seconds	review_idreview_commit_idclassificationauto_retry_allowed
human_onlynext_actionrepush_head_and_retry)rQ   r"   verify_pr_head_sha_matchclassify_trigger_missr
   )
r   
github_sharM   r   ro   
head_match	preflightgate_presentevidencer   s
             r   "test_pr_open_head_ref_oid_mismatchr      s   #J#JI))ZLI

+C--iDJg%''''(J666l#z111 E"TTXZI%)5PY024L$)btimnH..y*lT\]N*+/TTTT./4777,'5000-(,CCCCr   c                 8   dt              } t        |       }|j                  d      }|d   du s
J d|        d|d	   v sJ t        |d	         d
k\  sJ dfd}t        |      }|j                  d      }|d   du sJ |d   du sJ d|d	   v sJ y)u   gh_runner stub: check-runs에 gemini-review-gate 없음 → check_present=False.
    git_exit_128=True 시나리오: preflight에서 exit 128 발생으로 gate의 내부 원인 포함.
    sha_for_gate_testr   <   )max_wait_secondsr   Fz"Expected check_present=False, got check_missingr   r&   c                *    d| v rdddddidgiS diS )	NrT   rJ   gemini-review-gaterU   rV   z"fatal: exit 128 unable to read refrW   rI   r8   )rK   argsrZ   s     r   gh_with_gate_exit128zVtest_gemini_review_gate_check_missing_60s_internal_cause.<locals>.gh_with_gate_exit128   s>    8# 4&/#)+O"P  h''r   Tr   NrK   rO   r   r5   r6   r7   )r[   r"   'verify_gemini_review_gate_check_presentlen)r   ro   gate_resultr   mod2gate_result2rZ   s         @r   8test_gemini_review_gate_check_missing_60s_internal_causer      s     #H28<I

+C==hY[=\K'50d4VWbVc2dd0k*DEEEE
 {5671<<<( "67D??[]?^L(D000'4///\*DEEEEr   z3anu_v2.pr_open_gemini_trigger_prevention.time.sleepc                   d}d}t        j                  ||z        }ddidfd}t        ||      }t        ||      }|j	                  dd|	      }|d
   du sJ d   |k  sJ dd    d| d       ddddddd}dddd}	dddg d}
|j                  ||	|
|      }|d   t        k(  sJ |d   du sJ |d   du sJ y)u&  evidence_poller stub: 항상 [] (gemini 응답 0). clock stub으로 grace_seconds=180 만료.
    poller call_count <= ceil(180/30) = 6 (25분 polling/self-register 절대 부재 검증).
    classify → EXTERNAL_TRIGGER_REQUIRED (head_match=True, fetchable=True, evidence_arrived=False).
          r$   r   c                "    dxx   dz  cc<   g S )Nr$   r&   r8   )rM   rZ   r3   s     r   poller_stubzEtest_first_evidence_grace_window_expiry_classify.<locals>.poller_stub  s    31	r   )r   r   *   sha_grace_test)rM   rZ   grace_secondsr   Fu   poller 호출 횟수 u   가 hard limit u6   를 초과. 25분 polling 재발 패턴 금지 위반.TNr   )r   r   r   r   r   r   r   )rM   intrZ   rO   r6   
list[dict])mathceilrf   r"   poll_first_gemini_evidencer   r	   )_mock_sleepGRACEINTERVAL	MAX_CALLSr   r   ro   r   r   r   r   r   r3   s               @r   0test_first_evidence_grace_window_expiry_classifyr     sB    EH		%(*+IqJ
  x0E
{%
@C--EU=B . DH &'5000c?i' 

30	{ K5 	5' E"TTXZI4DTdeJ%)5PT024L ..y*lT\]N*+/WWWW./5888,'4///r   c                X  
 t        d      }|d   |d   |d   }|d   |d   d%fd}d&fd}d'd	}d
dddd(fd}g 
d)
fd}t        |||||      }|j                  |      }	|	d   |d   k(  sJ d|d    d|	d           |	d   |d   k(  sJ d|d    d|	d           |	d   |d   k(  sJ d|d    d|	d           |d   j                  d      d   |	d   v s&J d|d   j                  d      d    d |	d    d!       t	        
      d"k(  sJ d#       y$)*un   _load_fixture("pr_open_gemini_miss_pr86") 기반 모든 stub 셋업 → run() 결과 fixture 기대값 일치.pr_open_gemini_miss_pr86rM   base_branchhead_branchhead_sha_at_openr   c                    dj                  |       }d|v rd|v r	|vrdddS d|v r	|v rdddS d|v rd |v rdd	dS d|v rdd
dS d|v rdddS dd
dS )Nr'   r(   originr   r)   r*   r-   zorigin/1r.   r<   abcr/   )r   r2   r   rZ   s     r   git_runner_pr86zDtest_pr86_fixture_external_trigger_required.<locals>.git_runner_pr86K  s    hhtnc>h#o(#2E!"b11c>h#o
 "#b117;-!8C!?!"c22!"c223!"e44#..r   c                <    d | v rdiS d| v rdddddid	giS i S )
NrH   rI   rT   rJ   r   rU   rV   r)   rW   r8   )rK   r   rM   r   s     r   gh_runner_pr86zCtest_pr86_fixture_external_trigger_required.<locals>.gh_runner_pr86`  sR    I;8+ *--8#  4&/#)2,  	r   c                    g S Nr8   
pr_number_	head_sha_s     r   poller_pr86z@test_pr86_fixture_external_trigger_required.<locals>.poller_pr86q  s    	r   r   r^   r   )r]   callsc                 |    d   } dxx   dz  cc<   d   dk\  rt        dz         d<   | S dxx   dz  cc<   | S )Nr]   r   r&      r   )rd   )vclock_stategraces    r   
clock_pr86z?test_pr86_fixture_external_trigger_required.<locals>.clock_pr86w  s[    G!w1$$UQY/K  "r   c                :    j                  t        |              y r   )appendr7   )recaudit_recordss    r   r   zAtest_pr86_fixture_external_trigger_required.<locals>.audit_writer  s    T#Y'r   )r   r   r   r   r   rM   r   r   rZ   r   r   expected_classification	Expected , got r   expected_auto_retry_allowedzExpected auto_retry_allowed=r   expected_human_onlyzExpected human_only=expected_next_action_r   z!Expected next_action to contain 'z', got ''r&   u2   audit_writer가 정확히 1회 호출되어야 함Nr   r5   r6   r7   r   r   r   r   rO   r6   r   rc   )r   r7   r6   None)r   r"   runsplitr   )r   fxr   r   r   r   r   r   ro   rp   r   r   r   r   rZ   rM   r   s             @@@@@@@r   +test_pr86_fixture_external_trigger_requiredr   ?  s    
1	2B_I-(K-(K)*H&J/*" Ea(K !#M(  "#!C WW  F "#r*C'DD 
B012&@P9Q8RSD &'2.K+LL 
&r*G'H&IPVWkPlOmnL ,2&;#<< 
r"789|@T?UV< $%++C03vm7LL 
+B/E,F,L,LS,QRS,T+U V}%&a	)L
 }"X$XX"r   c                  	 dd	dd}d	fd}dd}ddidfd}t        ||||	      }|j                  	d
dd      }|d   t        k(  sJ dt         d|d           |d   du sJ |d   du sJ |d   dk(  sJ y)u   모든 stub 정상: head_match=True, fetchable=True, behind=0, evidence_arrived=True
    → classification == PR_OPEN_GEMINI_TRIGGER_OK, auto_retry_allowed=False, human_only=False.
    normalshaok1234d   c                d    dj                  |       }d|v rdddS d|v rdddS d|v rdd	dS dddS )
Nr'   r(   r   r)   r*   r-   r.   r<   base123r/   )r   r2   s     r   git_runner_okz<test_normal_pr_open_classification_ok.<locals>.git_runner_ok  sR    hhtnc>!"b11!"c223!"i88"--r   c                <    d | v rdiS d| v rdddddid	giS i S )
NrH   rI   rT   rJ   r   r   rV   r)   rW   r8   )rK   r   rZ   rM   s     r   gh_runner_okz;test_normal_pr_open_classification_ok.<locals>.gh_runner_ok  sP    I;8+ (++8# 4&/#)2,  	r   c                    dddi|dgS )Ni'  loginzgemini-code-assist[bot])iduser	commit_idr8   r   s     r   	poller_okz8test_normal_pr_open_classification_ok.<locals>.poller_ok  s"      ";<&
 	
r   r]   r^   c                 ,    d   } dxx   dz  cc<   | S )Nr]   r   r8   )r   r   s    r   clock_okz7test_normal_pr_open_classification_ok.<locals>.clock_ok  s"    CBr   )r   r   r   r   rh   z
feature/okr   r   r   r   r   r   Fr   r   proceed_to_merge_gatesNr   r   r   rc   )r"   r   r   )
r   r   r   r   r   ro   rp   r   rZ   rM   s
          @@@r   %test_normal_pr_open_classification_okr     s    
 !HI. 
 *K
  !	C WW   F "#'88 
%&fV4D-E,FG8 &'5000,5(((- $<<<<r   c                 @   t        d      } | d   dk(  sJ d| d           | d   dk(  sJ d| d           | j                  dd	      }d
|v s
J d|       t        j                  t              }d|vsJ d       d|vsJ d       g d}|D ]  }||vrJ d| d        y)uM  _load_fixture("post_merge_audit_warn_to_pass_pr86") + 본 모듈 외부 인터페이스 spec(§4) 위반 0건 확인.

    검증 항목:
    a. fixture["initial_audit_verdict"] == "POST_MERGE_AUDIT_WARN"
    b. fixture["final_audit_verdict_after_backfill"] == "POST_MERGE_AUDIT_PASS"
    c. fixture["lesson_pinned"]에 "marker 부재" 포함
    d. 본 모듈 source에 ".smoke-evidence" 키워드 부재 (smoke 검사 직접 수행 안 함)
    e. 본 모듈 source에 ".reconcile-evidence" 키워드 부재
    f. isolation 검증: utils/dispatch/scripts/dashboard 키워드 부재
    "post_merge_audit_warn_to_pass_pr86initial_audit_verdictPOST_MERGE_AUDIT_WARNz$Expected POST_MERGE_AUDIT_WARN, got "final_audit_verdict_after_backfillPOST_MERGE_AUDIT_PASSz$Expected POST_MERGE_AUDIT_PASS, got lesson_pinnedr)   u   marker 부재u,   lesson_pinned에 'marker 부재' 미포함: z.smoke-evidenceu   PROpenGeminiTriggerPrevention 소스에 '.smoke-evidence' 키워드가 있어서는 안 됨. smoke 검사는 후속 task 책임.z.reconcile-evidenceu   PROpenGeminiTriggerPrevention 소스에 '.reconcile-evidence' 키워드가 있어서는 안 됨. reconcile 검사는 후속 task 책임.)z
from utilszimport utilszfrom dispatchzimport dispatchzfrom scriptszimport scriptszfrom dashboardzimport dashboardu:   PROpenGeminiTriggerPrevention 소스에 금지된 import 'u)   '가 발견됨. one-way isolation 위반.N)r   getinspect	getsourcer   )r   lessonsrcforbidden_keywordskws        r   2test_post_merge_audit_warn_to_pass_spec_compliancer    s&    
;	<B %&*AA 
.r2I/J.KLA
 237NN 
.r2V/W.XYN
 VVOR(Ff$ 
6vjA$
 

9
:C C' 	.' !+ 	2+b  
} 	
H M( (	
}
r   )r   rO   r6   r7   )r    rO   r6   r   )r6   r   )rM   r   rP   rO   rL   rO   r6   r   )rZ   rO   r6   r   )r   )re   r   ra   r   r6   r   )r6   r   ).__doc__
__future__r   r  r   r   syspathlibr   typingr   unittest.mockr   r   __file__resolveparentsWORKSPACE_ROOTrO   pathinsert(anu_v2.pr_open_gemini_trigger_preventionr	   r
   r   r   r   parentr   r   r"   r9   r@   rE   rQ   r[   rf   rq   rx   r{   r   r   r   r   r   r  r8   r   r   <module>r     sZ    #    
   % h'')11!4~chh&HHOOAs>*+  H~$$++j8I 
"  #( 2	#+4D<$FR <='0 >'0X <=cY >cYP <=E= >E=T3
r   