
     j9s                       d Z ddlmZ ddlZddl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mZmZmZmZmZmZmZmZmZmZm Z m!Z!m"Z"m#Z#m$Z$m%Z% d$d%dZ&d&dZ'dd	gZ(d
dddZ)ddgg dZ*dg ddZ+ejX                  d'd       Z-	 	 	 	 	 d(	 	 	 	 	 d)dZ.d*dZ/d*dZ0d*dZ1d*dZ2d*dZ3d*dZ4d*dZ5d*dZ6d*dZ7d*dZ8d*dZ9d*dZ:d*d Z;d+d!Z<d+d"Z=d+d#Z>y),u  task-2509 회귀 테스트 — merge_queue_executor 12 케이스 + 보너스 2.

QA 담당: 모리건(Morrigan)
대상: utils/merge_queue_executor.py

케이스 목록:
  TC-01  queue head 아닌 경우 → WAITING_FOR_PREDECESSOR
  TC-02  queue head + 10조건 PASS → AUTO_MERGE_ALLOWED
  TC-03  BEHIND 상태 → merge sync (rebase X)
  TC-04  HEAD SHA 변경 → HEAD_SHA_LOCK_BROKEN
  TC-05  effective diff 오염 → DIFF_CONTAMINATION_REPLACEMENT + replacement_pr_runner
  TC-06  forbidden path → BLOCKED_WITH_REASON + CRITICAL_FORBIDDEN_PATH
  TC-07  CI failure → CI_FAILURE_BLOCK
  TC-08a Gemini auto_triage_candidate → GEMINI_UNRESOLVED_BLOCK
  TC-08b Gemini critical_scope_expansion → BLOCKED_WITH_REASON + CRITICAL_GEMINI_SCOPE_EXPANSION
  TC-09a mergeStateStatus=DIRTY → MERGE_STATE_NOT_CLEAN
  TC-09b mergeStateStatus=BLOCKED → BLOCKED_WITH_REASON + CRITICAL_BLOCK_OVERRIDE
  TC-10  post-merge smoke FAIL → BLOCKED_WITH_REASON + CRITICAL_POST_MERGE_SMOKE
  TC-11  post-merge smoke PASS → AUTO_MERGE_SUCCESS
  TC-12  후행 PR stale 재검증 state machine
  BONUS-1  assert_no_forbidden_git_flags: --admin → RuntimeError
  BONUS-2  assert_no_forbidden_git_flags: rebase → RuntimeError
    )annotationsN)Path)AUTO_MERGE_ALLOWEDAUTO_MERGE_SUCCESSBLOCKED_WITH_REASONCI_FAILURE_BLOCKCRITICAL_BLOCK_OVERRIDECRITICAL_FORBIDDEN_PATHCRITICAL_GEMINI_SCOPE_EXPANSIONCRITICAL_POST_MERGE_SMOKEDIFF_CONTAMINATION_REPLACEMENTGEMINI_UNRESOLVED_BLOCKHEAD_SHA_LOCK_BROKENMERGE_STATE_NOT_CLEANREPLACEMENT_PR_RUNNER_HOOKWAITING_FOR_PREDECESSORExecutorContextQueueDecisionTaskSpecassert_no_forbidden_git_flagsevaluate_prrecheck_following_prsverify_head_lock_then_mergec                4    t        j                  g | ||      S )u   CompletedProcess 생성 헬퍼.)args
returncodestdoutstderr)
subprocessCompletedProcess)r   r   r   s      F/home/jay/workspace/tests/regression/test_merge_queue_executor_2509.pycpr"   A   s    &&B:f]cdd    c                *     g d fd	}|_         |S )u   args 패턴별 CompletedProcess 반환 fake runner.

    key = frozenset of arg tokens to match,
    value = CompletedProcess to return.
    Unmatched → returncode=0, empty stdout/stderr.
    c                     j                  t               ||d       j                         D ]  \  }}t         fd|D              s|c S  t	               S )N)r   cwdtimeoutc              3  &   K   | ]  }|v  
 y w)N ).0pr   s     r!   	<genexpr>z.make_runner.<locals>.runner.<locals>.<genexpr>R   s     .19.s   )appendlistitemsallr"   )r   r&   r'   pattern	completedcallsreturns_by_argss   `    r!   runnerzmake_runner.<locals>.runnerO   sT    d4jIJ"1"7"7"9 	!GY.g..  	! tr#   )N<   )r3   )r4   r5   r3   s   ` @r!   make_runnerr7   F   s     E FLMr#   utils/merge_queue_executor.pyz2tests/regression/test_merge_queue_executor_2509.pyCLEANzsha-clean-001mainmergeStateStatus
headRefOidbaseRefNameSUCCESSstatusdetailsrawok)rA   
unresolvedhookc                 D    t        dt        t              dg ddddd 	      S )N	task-2509z(merge_queue/auto_merge/conflict_recoveryserial_only   TF)	task_idexpected_files	risk_area
dependencyparallel_policymerge_queue_positionstale_recheck_requiredcherry_pick_allowedsmoke_command)r   r.   EXPECTED_FILESr)   r#   r!   serial_task_specrU   k   s/    N+<%#!
 
r#   c                >    | t        i       } t        | ||d||      S )u%   기본 ExecutorContext 생성 헬퍼.T)r5   
pr_workdirrS   no_auditfixture_main_shamain_log_grep)r7   r   )r5   rW   rS   rY   rZ   s        r!   	_make_ctxr[   z   s1     ~R#)# r#   c           
     "   | }dg|_         dd}t        |      }t        d|dt        t              t
        t        t        |      }|j                  t        k(  sJ d|j                          d|j                  v sJ d	|j                          y
)u   dependency=["task-9999.merged"]이고 main_log_grep이 False 반환하면
    evaluate_pr이 WAITING_FOR_PREDECESSOR를 결정해야 한다.ztask-9999.mergedc                     y)NFr)   )_task_ids    r!   _not_mergedz6test_tc01_waiting_for_predecessor.<locals>._not_merged   s    r#   )rZ   c   
sha-pr-001	pr_number	task_specpr_head_shaeffective_filesmerge_stateci_stategemini_statectxz&expected WAITING_FOR_PREDECESSOR, got z	task-9999zreason must mention task-9999: N)r^   strreturnbool)rN   r[   r   r.   rT   CLEAN_MERGE_STATECI_OK	GEMINI_OKdecisionr   reason)rU   specr_   rj   rq   s        r!   !test_tc01_waiting_for_predecessorrt      s     D)*DO +
.C ^,%	H  77 
01B1B0CD7 (//)^-LX__L]+^^)r#   c           
         t               }t        d| dt        t              t        t
        t        |      }|j                  t        k(  s!J d|j                   d|j                          y)uR   모든 조건 정상 → evaluate_pr이 AUTO_MERGE_ALLOWED를 반환해야 한다.9   ra   rb   z!expected AUTO_MERGE_ALLOWED, got 
 / reason=N)
r[   r   r.   rT   rn   ro   rp   rq   r   rr   )rU   rj   rq   s      r!   +test_tc02_all_gates_pass_auto_merge_allowedrx      sj    
+C" ^,%	H  22 
+H,=,=+>jHYZ2r#   c           
        d}t        dt        dd      i      }t        ||      }ddd	d
}t        d| dt	        t
              |t        t        |       |j                  D cg c]  }d|d   v sd|d   v s| }}|sJ d       |d   d   }d|v s
J d|        d|v s
J d|        |j                  D cg c]  }d|d   v s| }}|r
J d|        yc c}w c c}w )u   mergeStateStatus=BEHIND + pr_workdir 설정 시
    runner가 ['git','merge','origin/main','--no-edit']로 호출돼야 하고
    'rebase' 명령은 호출되지 않아야 한다.z/tmp/fake-pr-workdir)gitmergeorigin/mainr   zMerge made by ort.r   r   )r5   rW   BEHINDzsha-pr-behindr:   r;   rv   rb   r{   r   rz   z5git merge origin/main --no-edit must have been calledr|   z&expected 'origin/main' in merge args: z	--no-editz$expected '--no-edit' in merge args: rebasez$rebase must NOT be called, but got: N)	r7   r"   r[   r   r.   rT   ro   rp   r3   )	rU   rW   r5   rj   behind_merge_statecmerge_calls
merge_argsrebase_callss	            r!   ,test_tc03_behind_state_uses_merge_not_rebaser      s;    (J'qAU)V F 6j
9C %% "#^,&	 %llZg6.BuPQRXPYGY1ZKZOOO;Q'JJ&]*PQ[P\(]]&*$Y(LZL&YY$  &||E!x1V9/DAELERCL>RR| [ Fs   $C1C9C:CCc                <   t               }t        t        ddd      }dd}t        |d||d      }|j                  t
        k(  sJ d|j                          d|j                  v sJ d	|j                          d
|j                  v sJ d|j                          y)u   pr_head_sha_start와 fetch_pr_head_at_merge가 다른 값 반환 시
    verify_head_lock_then_merge가 HEAD_SHA_LOCK_BROKEN을 반환해야 한다.rv   rH   zsha-start-111rq   rc   rK   pr_head_sha_startc                     y)Nsha-changed-999r)   
_pr_numbers    r!   _changed_shaz<test_tc04_head_sha_changed_breaks_lock.<locals>._changed_sha       r#   Trq   rc   rj   fetch_pr_head_at_mergedry_runz#expected HEAD_SHA_LOCK_BROKEN, got zreason must contain start sha: r   zreason must contain new sha: Nr   intrl   rk   )r[   r   r   r   rq   r   rr   )rU   rj   rq   r   results        r!   &test_tc04_head_sha_changed_breaks_lockr      s     +C #)	H! )+F ??22 
-foo->?2 fmm+^/Nv}}o-^^+-^1Nv}}o/^^-r#   c           
     $   t               }t        t              dgz   }t        d| d|t        t
        t        |      }|j                  t        k(  sJ d|j                          t        |j                  v sJ dt         d|j                          y)	u   effective_files에 expected에 없는 추가 파일 → DIFF_CONTAMINATION_REPLACEMENT
    + reason에 'replacement_pr_runner' 포함.zutils/rogue_extra_file.pyrv   ra   rb   z-expected DIFF_CONTAMINATION_REPLACEMENT, got zreason must mention 'z': N)r[   r.   rT   r   rn   ro   rp   rq   r   r   rr   )rU   rj   contaminated_filesrq   s       r!   (test_tc05_diff_contamination_replacementr     s     +Cn-1L0MM" *%	H  >> 
78I8I7JK> &8 
 :;3x>OP8r#   c           
     \   t               }t        t              dgz   }t        d| d|t        t
        t        |      }|j                  t        k(  sJ d|j                          |j                  t        k(  sJ d|j                          d|j                  v sJ d|j                          y)	uy   effective_files에 '.github/workflows/ci.yml' 포함 →
    BLOCKED_WITH_REASON + critical_code=CRITICAL_FORBIDDEN_PATH.z.github/workflows/ci.ymlrv   ra   rb   "expected BLOCKED_WITH_REASON, got z4expected critical_code=CRITICAL_FORBIDDEN_PATH, got z/forbidden_paths must contain the invaded file: N)r[   r.   rT   r   rn   ro   rp   rq   r   critical_coder
   forbidden_paths)rU   rj   files_with_forbiddenrq   s       r!   !test_tc06_forbidden_path_invasionr   3  s     +C/3M2NN" ,%	H  33 
,X->->,?@3 !!%<< 
>x?U?U>VW< &)A)AA 
9(:R:R9STAr#   c           
         t               }t        dgg d}t        d| dt        t              t
        |t        |      }|j                  t        k(  sJ d|j                          y)uP   ci_state.status=CI_FAILURE_BLOCK 시 결정이 CI_FAILURE_BLOCK이어야 한다.FAILUREr@   rv   ra   rb   zexpected CI_FAILURE_BLOCK, got N)r[   r   r   r.   rT   rn   rp   rq   )rU   rj   ci_failrq   s       r!   test_tc07_ci_failure_blockr   S  sm    
+C)yk"MG" ^,%	H  00 
)(*;*;)<=0r#   c           
         t               }ddddgdddgdd}t        d| dt        t              t        t
        ||	      }|j                  t        k(  sJ d
|j                          y)uF   gemini_state.status=auto_triage_candidate → GEMINI_UNRESOLVED_BLOCK.auto_triage_candidater8   z	style nitpathbodyauto_gemini_triage)rA   rE   insiderF   rv   ra   rb   z&expected GEMINI_UNRESOLVED_BLOCK, got N)r[   r   r.   rT   rn   ro   rq   r   )rU   rj   gemini_autorq   s       r!   'test_tc08a_gemini_auto_triage_candidater   m  s    
+C * ?UV;[QR$	K " ^,% 	H  77 
01B1B0CD7r#   c           
     ,   t               }ddddgdddgdt        d}t        d| dt        t              t
        t        ||	      }|j                  t        k(  sJ d
|j                          |j                  t        k(  sJ d|j                          y)uy   gemini_state.status=critical_scope_expansion →
    BLOCKED_WITH_REASON + critical_code=CRITICAL_GEMINI_SCOPE_EXPANSION.critical_scope_expansionzsome/external/file.pyzreal bug outside expectedr   critical_escalation_reporter)rA   rE   outsiderF   r   rv   ra   rb   r   z<expected critical_code=CRITICAL_GEMINI_SCOPE_EXPANSION, got N)
r[   r   r   r.   rT   rn   ro   rq   r   r   )rU   rj   gemini_criticalrq   s       r!   *test_tc08b_gemini_critical_scope_expansionr     s     +C - 7A\]^4>YZ[.8O " ^,%$	H  33 
,X->->,?@3 !!%DD 
FxG]G]F^_Dr#   c           
         t               }dddd}t        d| dt        t              |t        t
        |      }|j                  t        t        fv sJ d|j                          y)	uL   mergeStateStatus=DIRTY → MERGE_STATE_NOT_CLEAN 또는 BLOCKED_WITH_REASON.DIRTYra   r:   r;   rv   rb   z;expected MERGE_STATE_NOT_CLEAN or BLOCKED_WITH_REASON, got N)	r[   r   r.   rT   ro   rp   rq   r   r   )rU   rj   dirty_staterq   s       r!   test_tc09a_merge_state_dirtyr     sy    
+C $"K " ^,	H !68K LL 
EhFWFWEXYLr#   c           
        t               }dddd}t        d| dt        t              |t        t
        |      }|j                  t        k(  sJ d|j                          |j                  t        k(  sJ d|j                          y	)
uY   mergeStateStatus=BLOCKED → BLOCKED_WITH_REASON + critical_code=CRITICAL_BLOCK_OVERRIDE.BLOCKEDra   r:   r;   rv   rb   r   z4expected critical_code=CRITICAL_BLOCK_OVERRIDE, got N)
r[   r   r.   rT   ro   rp   rq   r   r   r	   )rU   rj   blocked_staterq   s       r!   test_tc09b_merge_state_blockedr     s    
+C &"M " ^,!	H  33 
,X->->,?@3 !!%<< 
>x?U?U>VW<r#   c           	        g d}|| _         t        dt        dd      t        |      t        dd      i      }t	        |d	|d
d      }t        t        ddd      }dd}t        |d||d      }|j                  t        k(  sJ d|j                          |j                  t        k(  sJ d|j                          |j                  dk(  sJ d|j                          y	)u   squash merge 성공(returncode=0) 후 smoke 실패(returncode=1) →
    BLOCKED_WITH_REASON + critical_code=CRITICAL_POST_MERGE_SMOKE.python3z-mpytestztests/smokeghprr{   57z--squashr   	PR mergedr}      zSMOKE FAILED: 3 tests failed)r   r   NTmain-sha-fixture-001r5   rW   rS   rX   rY   rv   rH   sha-pr-smoke-001r   c                     y)Nr   r)   r   s    r!   	_same_shaz5test_tc10_post_merge_smoke_failure.<locals>._same_sha  s    !r#   Fr   z/expected BLOCKED_WITH_REASON (smoke fail), got z6expected critical_code=CRITICAL_POST_MERGE_SMOKE, got FAILz expected smoke_status=FAIL, got r   )rS   r7   r"   tupler   r   r   r   rq   r   r   r   smoke_statusrU   	smoke_cmdr5   rj   allowed_decisionr   r   s          r!   "test_tc10_post_merge_smoke_failurer     s    ;I%."/q1Ui"2PQ	 F /C %#,	" )!(F ??11 
9&//9JK1 #<< 
@AUAU@VW< &( 
*6+>+>*?@(r#   c           	        g d}|| _         t        dt        dd      t        |      t        dd      i      }t	        |d|dd	
      }t        t        ddd      }dd}t        |d||d      }|j                  t        k(  s!J d|j                   d|j                          |j                  dk(  sJ d|j                          y)u^   squash merge + smoke 모두 PASS → verify_head_lock_then_merge가 AUTO_MERGE_SUCCESS 반환.r   r   r   r   r}   z5 passed, 0 failedNTr   r   rv   rH   sha-pr-pass-001r   c                     y)Nr   r)   r   s    r!   r   z2test_tc11_post_merge_smoke_pass.<locals>._same_sha8  r   r#   Fr   z!expected AUTO_MERGE_SUCCESS, got rw   PASSz expected smoke_status=PASS, got r   )rS   r7   r"   r   r   r   r   r   rq   r   rr   r   r   s          r!   test_tc11_post_merge_smoke_passr     s    :I%."/q1Ui"2FG	 F /C %#+	! )!(F ??00 
+FOO+<Jv}}oV0 &( 
*6+>+>*?@(r#   c                 n  
 ddl 
d 
fd} t         | d       | d       | d       | d      d      }d	d
id	did	did	dig}t        ||      }|D ci c]  }|d	   |
 }}|d
   }|d   du s
J d|        |d   du s
J d|        |d   du s
J d|        |d   }|d   du s
J d|        |d   du s
J d|        |d   }|d   du s
J d|        |d   du s
J d|        |d   }	|	d   du s
J d|	        |	d   du s
J d|	        |	d   du s
J d|	        |	d   du s
J d|	        yc c}w )!uy  recheck_following_prs 호출 시 mergeStateStatus에 따라
    needs_recheck/behind/conflict/blocked 필드가 올바르게 설정돼야 한다.

    케이스:
      pr_number=55 : BEHIND  → needs_recheck=True, behind=True
      pr_number=56 : DIRTY   → conflict=True
      pr_number=77 : BLOCKED → blocked=True
      pr_number=88 : CLEAN   → needs_recheck=False
    r   Nc                H    | ddd}t        dj                  |            S )Nzsha-stubr:   r;   r   r}   )r"   dumps)rA   payloadjsons     r!   _make_gh_responsezHtest_tc12_recheck_following_prs_state_machine.<locals>._make_gh_responseZ  s&    '-ZX^_Qtzz'':;;r#   r~   r   r   r9   ))55)56)77)88rc   7   8   M   X   )queuer5   needs_recheckTz*PR#55 BEHIND: needs_recheck must be True: behindz#PR#55 BEHIND: behind must be True: conflictFz&PR#55 BEHIND: conflict must be False: z$PR#56 DIRTY: conflict must be True: z)PR#56 DIRTY: needs_recheck must be True: blockedz%PR#77 BLOCKED: blocked must be True: z+PR#77 BLOCKED: needs_recheck must be True: z*PR#88 CLEAN: needs_recheck must be False: z#PR#88 CLEAN: behind must be False: z%PR#88 CLEAN: conflict must be False: z$PR#88 CLEAN: blocked must be False: )rA   rk   rl   subprocess.CompletedProcess)r   r7   r   )r   r5   r   statessby_prs55s56s77s88r   s             @r!   -test_tc12_recheck_following_prs_state_machiner   N  s#    < "8,"7+"9-"7+	 F 
b	b	b	b	E #v>F(./1Q{^Q/E/ )C4'[+UVYUZ)[['x=D M$Gu"MM z?e#S'McU%SS# )Cz?d"P&J3%$PP"4'Z+TUXTY)ZZ' )Cy>T!P%J3%#PP!4'\+VWZV[)\\' )C5(\,VWZV[*\\(x=E!N%H#NN!z?e#R'LSE%RR#y>U"P&J3%$PP"/ 0s   D2c                 ~    t        j                  t        d      5  t        g d       ddd       y# 1 sw Y   yxY w)uN   assert_no_forbidden_git_flags(['gh','pr','merge','--admin']) → RuntimeError.FORBIDDEN_GIT_FLAGSmatch)r   r   r{   z--adminNr   raisesRuntimeErrorr   r)   r#   r!   test_bonus1_admin_flag_raisesr     s3    	|+@	A H%&FGH H H   3<c                 ~    t        j                  t        d      5  t        g d       ddd       y# 1 sw Y   yxY w)uH   assert_no_forbidden_git_flags(['git','rebase','main']) → RuntimeError.REBASE_FORBIDDENr   )rz   r   r:   Nr   r)   r#   r!   test_bonus2_rebase_raisesr     s3    	|+=	> A%&?@A A Ar   )r    r   )r   r   r   rk   r   rk   rl   r   )r4   dict)rl   r   )NNNr   N)rW   z
str | NonerY   rk   rl   r   )rU   r   rl   None)rl   r   )?__doc__
__future__r   r   syspathlibr   r   __file__resolveparent	WORKSPACErk   r   removeinsertutils.merge_queue_executorr   r   r   r   r	   r
   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r"   r7   rT   rn   ro   rp   fixturerU   r[   rt   rx   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r)   r#   r!   <module>r     s  0 #  
   N""$++2299	y>SXXHHOOC	N# 3y> "     6e
. $8  !  )R@2t<	   !2 	 ._@.%SV_F:@46H4@1n+b7QzHAr#   