
    ]jlV              
      .   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ddlmZ ddlmZ ddlZ ed      Z ed      ZdZd	Zd
Zej/                         r. e	j0                  ej3                               j5                         ndZddZdddZd Zd Zg dddefdddefdddefdddefdddefddd efd!d"d#efd$d%d&efd'd(d)efd*d+d,efd-d.d/efd0d1d2ez   d3z   ez   d4z   efd5d6d7efd8d9d:ez   d;z   ez   d<z   efd=d>d?ez   d@z   efdAdBdCefdDdEdFez   dGz   efdHdIdJefdKdLdMefdNdOdPefdQdRdSefdTdUdVefdWdXdYefdZd[d\efd]d^d_efd`dadbefdcdddeefdfdgdhefdidjdkefdldmdnefdodpdqez   drz   ez   d4z   ez   d@z   efdsdtduefdvdwdxefdydzd{efd|d}d~efdddefdddefdddefdddefdddefdddez   dz   efdddez   d@z   efdddefdddefdddefdddefdddefdddefdddefdddefdddefdddefdddefdddez   dz   efdddez   dz   ez   d4z   efdddez   dz   efdddefdddefdddefZ ejB                  jE                  de e D  cg c]
  } d| d     c} ɫ      dʄ        Z#dZ$dZ%d̈́ Z&d΄ Z'dτ Z(dZ)dZ*g dҢZ+ejX                  dӄ        Z-ddԄZ.ejB                  jE                  de+      dք        Z/dׄ Z0d؄ Z1g d٢Z2ddڄZ3ejB                  jE                  de2      dۄ        Z4d܄ Z5yc c} w )ui  task-3017 회귀 테스트 — ``~/.claude/hooks/gh-token-guard.sh`` (PreToolUse, matcher=Bash).

이 훅은 "봇 토큰이 주입되지 않은 GitHub 쓰기 명령"을 차단한다.
토큰 없이 ``gh`` 를 부르면 ``~/.config/gh/hosts.yml`` 의 회장님 개인 PAT 로 폴백해
PR 작성자·``merged_by`` 가 회장님으로 찍히고 감사 기록이 오염된다.

여기 담긴 케이스는 task-3017 검증 당시 ``/tmp/t3017/{probe,probe2,verify,historical,
r1cases,redline}.py`` 로 돌렸던 판정 집합을 **저장소로 이식**한 것이다.
/tmp 에 의존하지 않으며(자기완결), 훅을 읽기만 하고 수정하지 않는다.

★ 훅 파일이 없거나 못 읽으면 skip 이 아니라 **fail** 이다. 가드 유실 자체가 회귀다.
★ 이 테스트는 명령을 실행하지 않는다. 훅에 stdin JSON 을 먹여 판정만 본다.
    )annotationsN)Path)SimpleNamespacez)/home/jay/.claude/hooks/gh-token-guard.shz/home/jay/.claude/settings.jsonDENYALLOW
c                     t        t        j                        }| j                         D ](  \  }}||j	                  |d       t        |      ||<   * |S )uT   현재 환경을 복사해 일부 변수만 덮어쓰거나(값) 제거한다(None).N)dictosenvironitemspopstr)	overridesenvkeyvalues       </home/jay/workspace/scripts/__tests__/test_gh_token_guard.py_envr   '   sQ    
rzz
Coo' "
U=GGC5zCH	"
 J    c           	     	   t         j                  } |       }|st        j                  dt          d      dz   dt	        j
                         v st        j                  t               rt        j                  t               ndt        j                  |      t        j                  |      dz  }t        t        j                  |            dx}}t        j                  }t        j                  } |t         |      }|sXt        j                  dt                dz   d	t	        j
                         v st        j                  t              rt        j                  t              nd	t        j                  |      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  }t        t        j                  |            dx}x}}t        j                  dd| idd      }t        j                   dt#        t               g|dd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  }t        j                  d|	j$                   d|	j(                  dd       dz   d|iz  }t        t        j                  |            dx}x}}
|	j*                  xs dj-                         }|st/        t0        ddd      S 	 t        j2                  |      }j;                  d!      xs i }|j;                  d"      d#k(  rt<        nt0        }t/        ||j;                  d$d      xs d|j;                  d%d      xs d|      S # t        j4                  $ r t7        j8                  d |dd        Y w xY w)&uX   훅에 PreToolUse stdin JSON 을 넣고 (verdict, reason, system_message) 를 얻는다.   가드 유실(회귀): u    이 없다D
>assert %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.is_file
}()
}HOOKpy0py2py4Nu%   가드를 읽을 수 없다(회귀): m
>assert %(py8)s
{%(py8)s = %(py2)s
{%(py2)s = %(py0)s.access
}(%(py3)s, %(py6)s
{%(py6)s = %(py4)s.R_OK
})
}r   r   r   py3r   py6py8Bashcommand)	tool_name
tool_inputF)ensure_asciibashTx   )inputcapture_outputtexttimeoutr   r   ==)z2%(py2)s
{%(py2)s = %(py0)s.returncode
} == %(py5)sprocr   r   py5u5   훅은 어떤 경우에도 rc=0 이어야 한다. rc=z stderr=  
>assert %(py7)spy7 )verdictreasonsystem_messagerawu"   훅 출력이 JSON 이 아니다: hookSpecificOutputpermissionDecisiondenypermissionDecisionReasonsystemMessage)r   is_file
@pytest_ar_format_assertmsg@py_builtinslocals_should_repr_global_name	_safereprAssertionError_format_explanationr   accessR_OKjsondumps
subprocessrunr   
returncode_call_reprcomparestderrstdoutstripr   r   loadsJSONDecodeErrorpytestfailgetr   )r%   r   @py_assert1@py_assert3@py_format5@py_assert5@py_assert7@py_format9payloadr1   @py_assert4@py_format6@py_format8outdatahook_outr8   s                    r   judgerg   2   s4   <<F<>F>FF4TF+FFFFFFF4FFF4FFF<FFF>FFFFFF99S277S9T7#S#SS'LTF%SSSSSSS2SSS2SSS9SSSSSSTSSSTSSSSSS2SSS2SSS7SSS#SSSSSSjjY,@APUG >>	TD ?? a ?a  ?a                       @?P Q++ds#&	(    
 ;;"
#
#
%CuRPRSSHzz# xx,-3Hll#78FBdG||6;Arxx4:	   H8TcFGHs   Q .RRc            	     b	   t         j                  }  |        }|st        j                  dt                dz   dt	        j
                         v st        j                  t               rt        j                  t               ndt        j                  |       t        j                  |      dz  }t        t        j                  |            dx} }t        j                  } t        j                  } | t         |      }|sXt        j                  dt                dz   dt	        j
                         v st        j                  t              rt        j                  t              ndt        j                  |       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  }t        t        j                  |            dx} x}}t         j                  d
d      }|j                  } d} | |      }|st        j                  d      dz   dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |       t        j                  |      t        j                  |      dz  }t        t        j                  |            dx} x}}|j                  } |       }	t!        |	      }
d}|
|kD  }|s4t        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  }t        j                  d      dz   d|iz  }t        t        j                  |            dx}x}	x}
x}}y)up   가드 파일이 삭제/이동/권한상실되면 모든 우회가 그대로 통과하므로 fail 로 잡는다.r   r   r   r   Nu   가드 읽기 불가(회귀): r   r   r    utf-8replace)encodingerrorsz#!u'   가드가 셸 스크립트가 아니다zN
>assert %(py6)s
{%(py6)s = %(py2)s
{%(py2)s = %(py0)s.startswith
}(%(py4)s)
}body)r   r   r   r"   d   )>)zg%(py7)s
{%(py7)s = %(py0)s(%(py5)s
{%(py5)s = %(py3)s
{%(py3)s = %(py1)s.splitlines
}()
})
} > %(py10)slen)r   py1r!   r3   r6   py10u?   가드 본문이 비정상적으로 짧다(덮어쓰기 의심)z
>assert %(py12)spy12)r   rA   rB   rC   rD   rE   rF   rG   rH   rI   r   rJ   rK   	read_text
startswith
splitlinesrp   rQ   )rZ   r[   r\   r]   r^   r_   rm   @py_format7@py_assert2ra   @py_assert6@py_assert9@py_assert8@py_format11@py_format13s                  r   %test_hook_file_exists_and_is_readabler~   [   s~   <<;<>;>;;4TF;;;;;;;4;;;4;;;<;;;>;;;;;;99L277L9T7#L#LL'EdV%LLLLLLL2LLL2LLL9LLLLLLTLLLTLLLLLL2LLL2LLL7LLL#LLLLLL>>79>=D??K4K?4 K KK"KKKKKKK4KKK4KKK?KKK4KKK KKKKKKj j3 !jCj!C'jjj!Cjjjjjj3jjj3jjjjjjtjjjtjjjjjj jjj!jjjCjjj)jjjjjjjjr   c            	        t         j                  }  |        }|st        j                  dt                dz   dt	        j
                         v st        j                  t               rt        j                  t               ndt        j                  |       t        j                  |      dz  }t        t        j                  |            dx} }t        j                  t         j                  d            }|j                  di       j                  d	      xs g D cg c]  }|j                  d
      dk(  r| }}|D cg c].  }|j                  d      xs g D ]  }|j                  dd       0 }}}d |D        } t        |       }|st        j                  d|       dz   dt	        j
                         v st        j                  t              rt        j                  t              ndt        j                  |       t        j                  |      dz  }t        t        j                  |            dx} }yc c}w c c}}w )uj   settings.json PreToolUse(matcher=Bash) 등록이 빠지면 훅 파일이 멀쩡해도 가드는 죽는다.u   settings.json 유실(회귀): r   SETTINGSr   Nri   rk   hooks
PreToolUsematcherr$   r%   r7   c              3  $   K   | ]  }d |v  
 yw)zgh-token-guard.shN ).0cmds     r   	<genexpr>z;test_hook_is_registered_in_settings_json.<locals>.<genexpr>r   s      '*s"s   uB   PreToolUse(matcher=Bash) 에 gh-token-guard.sh 미등록(회귀): z.
>assert %(py4)s
{%(py4)s = %(py0)s(%(py2)s)
}any)r   rA   rB   rC   rD   rE   rF   rG   rH   rI   rL   rU   rt   rY   r   )rZ   r[   r\   re   entrybash_entrieshookcommandss           r   (test_hook_is_registered_in_settings_jsonr   d   s    JJJJ!?zJJJJJJJ8JJJ8JJJJJJJJJJJJ::h(('(:;D hhw+//=C99Y6) 	L  "YYw'-2  	BH 
.6 W3   W  WEVEV	KH:VW WPVPVW W>V>V  W WMVY  W WMVY W WMVY W W WCVCVW W
s    I3I   u   gh pr merge (토큰 없음)zgh pr merge 251 --squash   zgh api -X PUT contentsz3gh api -X PUT repos/o/r/contents/a.txt -f message=m   zgh api -X PUT pulls merge<gh api -X PUT repos/o/r/pulls/8/merge -f merge_method=squash   u   토큰 주입 + pr mergez5GH_TOKEN="$BOT_GITHUB_TOKEN" gh pr merge 251 --squash   z
gh pr viewzgh pr view 251 --json state   z
gh api GETzgh api repos/o/r/pulls/8   zgh pr checkszgh pr checks 251   zgh api GET contentsz gh api /repos/o/r/contents/a.txt	   u   롱플래그 --method PUTz9gh api --method PUT repos/o/r/contents/a.txt -f message=m
   u   등호형 --method=PUTz,gh api --method=PUT repos/o/r/contents/a.txt   u   붙여쓴 -XPUTz%gh api -XPUT repos/o/r/contents/a.txt   u   heredoc 동반z6gh api -X PUT repos/o/r/contents/a.txt --input - <<EOFz{"message":"m"}EOF   zherestring <<<zgh pr merge 251 --squash <<< ""   u   백슬래시 개행 -X PUTzgh api \z
  -X PUT \z  repos/o/r/contents/a.txt   u%   여러 줄인데 토큰은 1행에만zGH_TOKEN="$B" gh api repos/o/rz&gh api -X PUT repos/o/r/contents/a.txt   u%   GH_TOKEN= 이 주석 안에만 있음u:   gh api -X PUT repos/o/r/contents/a.txt  # GH_TOKEN= 안씀   u   gh 와 pr 사이 개행zgh pr \z  merge 251 --squash   u   파이프 뒤z/echo x | gh api -X PUT repos/o/r/contents/a.txt   u	   서브셸z((gh api -X PUT repos/o/r/contents/a.txt)   zgh release uploadz%gh release upload v1.0 ./dist/app.zip   u   gh api --input 파일 + -X PUTz=gh api -X PUT repos/o/r/contents/a.txt --input /tmp/body.json   u   -X put 소문자z&gh api -X put repos/o/r/contents/a.txt   u   -X  PUT 이중 공백z'gh api -X  PUT repos/o/r/contents/a.txt   u   --method=POST git refs 생성z;gh api --method=POST repos/o/r/git/refs -f ref=refs/heads/x   u   환경변수 별칭 경유z GH=gh; $GH pr merge 251 --squash   zgh secret setzgh secret set FOO --body bar   zgh workflow runzgh workflow run deploy.yml   zgh repo deletezgh repo delete o/r --yes   zgh pr readyzgh pr ready 251   u   탭 구분자 gh pr mergezgh	pr	merge 251 --squash   u1   무해한 heredoc 이 앞에 있고 뒤에 쓰기zcat <<EOF > /tmp/xhi    u-   앞선 echo 가 GH_TOKEN= 문자열을 포함z9echo "GH_TOKEN=" ; gh api -X PUT repos/o/r/contents/a.txt!   u   bash -c 문자열 안의 쓰기z"bash -c 'gh pr merge 251 --squash'"   zgh api -X DELETEz+gh api -X DELETE repos/o/r/git/refs/heads/x#   zgh issue commentzgh issue comment 5 --body hi$   u   graphql mutation (기본 POST)zggh api graphql -f query='mutation { mergePullRequest(input:{pullRequestId:"x"}) { clientMutationId } }'%   zgh pr merge --admin + 2>&1z%gh pr merge 251 --squash --admin 2>&1&   u   무관한 선행 env 대입z.FOO=bar gh api -X PUT repos/o/r/contents/a.txt'   u!   주석에 훅 이름 자기언급zFgh api -X PUT repos/o/r/contents/a.txt  # per gh-token-guard.sh policy(   u   -X 와 PUT 사이 탭z&gh api -X	PUT repos/o/r/contents/a.txt)   u"   쓰기 뒤에 무관한 herestringzgrep -q x <<< "y"*   u.   GH_TOKEN= 을 grep 하는 줄이 앞에 있음z,grep GH_TOKEN= /home/jay/workspace/.env.keys+   u   gh pr create (토큰 없음)gh pr create --title t --body b,   u   GITHUB_TOKEN 주입(정당)zGGITHUB_TOKEN="$BOT_GITHUB_TOKEN" gh api -X PUT repos/o/r/contents/a.txte   u   graphql 읽기 쿼리z4gh api graphql -f query='query { viewer { login } }'f   zgh pr list --jsonzgh pr list --json numberg   u   gh api 읽기 + --jqz$gh api repos/o/r/pulls/8 --jq .stateh   u   토큰 주입 + --method PUTzVGH_TOKEN="$BOT_GITHUB_TOKEN" gh api --method PUT repos/o/r/contents/a.txt -f message=mi   u   ALLOW_OWNER_PAT 탈출구z*ALLOW_OWNER_PAT=1 gh pr merge 251 --squashj   u   GH_TOKEN 이 빈 값z"GH_TOKEN= gh pr merge 251 --squashk   u   gh run list 읽기zgh run list --limit 5l   u   명시 GET + 필드z2gh api --method GET repos/o/r/issues -f state=openm   u   gh release view 읽기zgh release view v1.0n   u"   같은 셸에서 export 후 쓰기z#export GH_TOKEN="$BOT_GITHUB_TOKEN"o   u)   heredoc 본문에 실린 무토큰 쓰기z
bash <<EOFp   u5   미종료 heredoc 이 뒤 문장을 삼키면 안 됨zecho "x <<EOF"q   u    graphql mutation + 토큰 주입z6GH_TOKEN="$B" gh api graphql -f query='mutation { x }'r   u   gh workflow view 읽기zgh workflow view deploy.ymls   u   gh 아닌 명령 무간섭zgit status && echo doneznum,label,command,expectedc)idsc                |   t        |      }|j                  }||k(  }|st        j                  d|fd||f      dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      dt	        j
                         v st        j                  |      rt        j                  |      nddz  }t        j                  d|  d| d|d	| d
|j                   d|j                  dd        dz   d|iz  }t        t        j                  |            dx}}y)ui   토큰 없는 gh 쓰기는 차단, 읽기/토큰주입/탈출구는 통과 — 59케이스 판정 회귀.r/   z/%(py2)s
{%(py2)s = %(py0)s.verdict
} == %(py4)sgotexpectedr   # 
cmd=   
기대=    실제=   
사유=N  
>assert %(py6)sr"   )rg   r8   rB   rQ   rD   rE   rF   rG   rC   r9   rH   rI   )	numlabelr%   r   r   rZ   r[   r\   rw   s	            r   <test_write_without_bot_token_is_denied_and_reads_are_allowedr      s    .C;; ;("   ;(                    #    #    C5%wk8*HS[[M R**Tc"#	%    r   aA  cd /home/jay/projects/InsuRo && cat > /tmp/anu3006/merge.json <<'EOF'
{"merge_method":"squash","sha":"5b2f2765d74f9e02e202570122de7cdc7a46ac97","commit_title":"[task-3006] \ud655\uc7a5 \ud30c\uc77c\ub7ff (#251)"}
EOF
gh api -X PUT repos/Jeon-Jonghyuk/InsuRo/pulls/251/merge --input /tmp/anu3006/merge.json 2>&1 | head -10zsource ~/.env.keys 2>/dev/null; export GH_TOKEN="${BOT_GITHUB_TOKEN:-$GH_TOKEN}"
gh api -X PUT repos/Jeon-Jonghyuk/InsuWiki/pulls/8/merge \
  -f merge_method=squash \
  -f commit_title="[task-3011] runner (#8)" \
  --jq '{merged,sha,message}' 2>&1c                 :   t        t              } | j                  }|t        k(  }|st	        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  }t	        j                  dt               dz   d|iz  }t        t	        j                  |            d	x}}d
}| j                  }||v }|st	        j
                  d|fd||f      t	        j                  |      dt        j                         v st	        j                  |       rt	        j                  |       ndt	        j                  |      dz  }t	        j                  d| j                  d	d        dz   d|iz  }	t        t	        j                  |	            d	x}x}}y	)uf   실사고 A(InsuRo #251): heredoc 이 앞에 있어도 뒤의 무토큰 머지는 차단돼야 한다.r/   r   r   r   r   u%   실사고 A 가 다시 통과한다:
r   r"   Nu	   미주입inz.%(py1)s in %(py5)s
{%(py5)s = %(py3)s.reason
}rq   r!   r3   u2   차단 사유가 '토큰 미주입'이 아니다: r4   r5   r6   )rg   INCIDENT_INSURO_251r8   r   rB   rQ   rD   rE   rF   rG   rC   rH   rI   r9   )
r   rZ   r[   r\   rw   @py_assert0ra   rx   rb   rc   s
             r   2test_incident_insuro_251_heredoc_bypass_is_blockedr     s/   
#
$C;;^;$^^^;$^^^^^^3^^^3^^^;^^^^^^$^^^$^^^^"HI\H] ^^^^^^^m#**m;*$mmm;*mmm;mmmmmm#mmm#mmm*mmm(Z[^[e[efjgj[kZl&mmmmmmmmr   c                   | dz  }|j                          d}||z  }|j                  } |       }| }|sddt        j                         v st	        j
                  |      rt	        j                  |      ndt	        j                  |      t	        j                  |      t	        j                  |      dz  }t        t	        j                  |            dx}x}x}x}}t        t        t        |            }|j                  }|t        k(  }|st	        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  }	t	        j                  dt               dz   d|	iz  }
t        t	        j                  |
            dx}}d}|j                   }||v }|st	        j                  d|fd||f      t	        j                  |      dt        j                         v st	        j
                  |      rt	        j                  |      ndt	        j                  |      dz  }t	        j                  d|j                   dd        dz   d|iz  }t        t	        j                  |            dx}x}}y)u   실사고 B(InsuWiki #8): source 대상 파일이 없어 토큰이 빈 값이 되는 머지는 차단돼야 한다.

    사고 조건(``~/.env.keys`` 부재)을 재현하려고 HOME 을 빈 tmp 디렉터리로 고정한다.
    home	.env.keysQassert not %(py7)s
{%(py7)s = %(py5)s
{%(py5)s = (%(py0)s / %(py2)s).exists
}()
}	fake_homer   r   r3   r6   NHOMEr   r/   r   r   r   r   u%   실사고 B 가 다시 통과한다:
r   r"   u   공급 파일 부재r   r   r   u@   차단 사유가 'R1 토큰 공급 경로 부재'가 아니다: r4   r5   r6   )mkdirexistsrD   rE   rB   rF   rG   rH   rI   rg   INCIDENT_INSUWIKI_8r   r8   r   rQ   rC   r9   )tmp_pathr   rZ   r[   ra   ry   r{   r_   r   r\   rw   r   rx   rb   rc   s                  r   6test_incident_insuwiki_8_empty_token_bypass_is_blockedr     s   
 6!IOO'1	K'1'//1/11111111111	111	111K111/11111111111
#9)=
>C;;^;$^^^;$^^^^^^3^^^3^^^;^^^^^^$^^^$^^^^"HI\H] ^^^^^^^! SZZ !Z/  !Z    "      &)    &)    &0    K3::VZWZK[J\]     r   c                   | dz  }|j                  dd       d| d}t        |t        |             }|j                  }|t        k(  }|st        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  }t        j                  d|j                  dd        dz   d|iz  }t        t        j                  |            dx}}y)ug   대조군: 실존 파일 source + 토큰 주입 머지는 통과해야 한다(과차단 회귀 방지).env.keys$BOT_GITHUB_TOKEN=ghs_dummy_for_test
ri   r   set -a; source zu; set +a
GH_TOKEN="$BOT_GITHUB_TOKEN" gh api -X PUT repos/Jeon-Jonghyuk/InsuRo/pulls/256/merge -f merge_method=squashr   r   r/   r   r   r   r   u   정상 머지가 차단됐다: Nr   r   r"   )
write_textrg   r   r8   r   rB   rQ   rD   rE   rF   rG   rC   r9   rH   rI   )r   keysr%   r   rZ   r[   r\   rw   s           r   7test_incident_control_group_real_token_merge_is_allowedr     s    j DOO;gON
$  L 	L 
 Tx0
1C;;U;%UUU;%UUUUUU3UUU3UUU;UUUUUU%UUU%UUUU#B3::dsCSBT!UUUUUUUr   zlGH_TOKEN="$BOT_GITHUB_TOKEN" gh api -X PUT repos/Jeon-Jonghyuk/InsuWiki/pulls/8/merge -f merge_method=squashz7gh api repos/Jeon-Jonghyuk/InsuWiki/pulls/8 --jq .state)R1-aR1-bR1-cR1-dR1-eR1-fR1-gR1-hR1-iR1-jR1-kR1-lR2-aR2-bR2-cR2-dc                    | dz  }|j                  dd       | dz  dz  }| dz  }|j                          g }|j                  } |       }|}|r|j                  } |       }	|	 }
|
}|s@dd	t	        j
                         v st        j                  |      rt        j                  |      nd	t        j                  |      t        j                  |      d
z  }|j                  |       |rddt	        j
                         v st        j                  |      rt        j                  |      ndt        j                        t        j                  	      dz  }|j                  |       t        j                  |d      i z  }dd|iz  }t        t        j                  |            dx}x}x}x}x}x}	}
d}||z  }|j                  } |       }| }|sddt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      t        j                  |      dz  }t        t        j                  |            dx}x}x}x}}t        |||      S )ud   R1 판정에 필요한 '실존 파일' / '부재 파일' / '빈 HOME' 을 tmp_path 안에 만든다.r   r   ri   r   nowherezdefinitely-not-here.keysr   z;%(py6)s
{%(py6)s = %(py4)s
{%(py4)s = %(py2)s.is_file
}()
}real)r   r   r"   zBnot %(py12)s
{%(py12)s = %(py10)s
{%(py10)s = %(py8)s.exists
}()
}gone)r#   rr   rs   r   zassert %(py16)spy16Nr   r   r   )r  r  r   )r   r   rA   r   rD   rE   rB   rF   rG   append_format_booloprH   rI   r   )r   r  r  r   rZ   r[   r]   r   rz   @py_assert11@py_assert13rw   @py_format14@py_format15@py_format17ra   ry   r{   r_   s                      r   keyfilesr  7  s    j DOO;gONi"<<DfDJJL/4<</<>/>/>$++/+-/-/////////4///4///<///>////>/////$///$///+///-/////////////",{","**,*,,,,,,,,,,,,,,,,,{,,,*,,,,,,,,,,,4d;;r   c           	     `   i ddd| j                    dt         t         t        fddd| j                   dt         t         t
        fd	d
d| j                   dt         t         t        fdddt         t         t        fddd| j                   t         t         t
        fdddt         t         t
        fdddt         t         t
        fdddt         t         t        fddd| j                   dt         dt         t        fddd| j                    d| j                   t         t         t
        fd d!d| j                   dt         d"t        fd#d$d| j                    dt         d%t
        fd&d'd(t        fd)d*d+t        fd,d-d.t        fd/d0d1t         t        fS )2uO   R1(깨진 토큰 공급 경로)·R2(근거 없는 변수 전개) 케이스 표.r   u)   실존 파일 source + 토큰주입 writer   z; set +ar   u)   부재 파일 source + 토큰주입 writezsource z 2>/dev/nullr   u    부재 파일 source + 읽기 ghr   uG   미해결 변수 경로 source + write(확정 불가 → 오탐 방지)zsource "$KEYS_FILE"r  u%   `.` 축약형 + 부재 파일 + writez. r  u    ~ 전개 + 부재 파일 + writezsource ~/.env.keys 2>/dev/nullr  u$   $HOME 전개 + 부재 파일 + writez$source "$HOME/.env.keys" 2>/dev/nullr  u*   상대경로 source(확정 불가) + writezsource .env.keysr  u2   깨진 source 라도 ALLOW_OWNER_PAT 는 탈출구zALLOW_OWNER_PAT=1 r  u;   실존+부재 혼합 source (하나라도 부재면 차단)z	; source r  u/   깨진 source 인데 gh 호출 자체가 없음zecho hir  uI   실존 파일 source + 토큰 미주입 write(기존 규칙으로 차단)r   r	  uF   source 없음 + 변수 전개 토큰 + write(경고만, 차단 아님)z_GH_TOKEN="$BOT_GITHUB_TOKEN" gh api --method PUT repos/o/r/pulls/8/merge -f merge_method=squashr
  u=   같은 명령에서 변수를 직접 정의 → 경고 없음zvBOT_GITHUB_TOKEN=ghs_x GH_TOKEN="$BOT_GITHUB_TOKEN" gh api --method PUT repos/o/r/pulls/8/merge -f merge_method=squashr  u"   리터럴 토큰 → 경고 없음zWGH_TOKEN=ghs_literal gh api --method PUT repos/o/r/pulls/8/merge -f merge_method=squashr  u   읽기 호출은 무간섭GH_TOKEN="$BOT_GITHUB_TOKEN" )r  NLWRITEr   r  r   READ)ks    r   _r1_r2_casesr   D  sx   $<"166((2$ug>G$ 	<166(,rd5':DB$
 	3166(,rd4&95B$ 	Z&rd5'2E;$ 	8affXbT%)41$ 	31"eW=tE$ 	77tE7CTK$ 	=#B4w/8$" 	E166(,rd2DUGLeU#$& 	N166()AFF8B4w?G'$* 	B166(,rd':EC+$. 	\"166((2$OPQUW/$4 	YBCHJ5$: 	PBCHJ;$@ 	5*+02A$F 	-1Ntf/UW\]G$ $r   case_idc                   t        |      |    \  }}}t        |t        |j                  d            }|j                  }||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      dt        j                         v st        j                  |      rt        j                  |      nddz  }t        j                  |  d	| d
|d| d|j                   d|j                  dd        dz   d|iz  }	t        t        j                  |	            dx}}y)uy   토큰 변수가 대입돼 있어도 공급 파일(source 대상)이 없으면 런타임 빈 값 → 차단해야 한다.Nr   BOT_GITHUB_TOKENr   r/   r   r   r   r   r   r   r   r   r   r   r   r"   )r   rg   r   r   r8   rB   rQ   rD   rE   rF   rG   rC   r9   rH   rI   )
r!  r  r   r%   r   r   rZ   r[   r\   rw   s
             r   'test_broken_token_supply_path_is_deniedr%  m  s@     ,H5g>E7H
Tx}}tL
MC;; ;("   ;(                    #    #    )1UG6'IhZx} U**Tc"#	%    r   c                p   t        |       d   \  }}}t        |t        | j                  dd            }|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  }t        j                  d
      dz   d|iz  }t        t        j                  |            dx}}d}|j                  }	||	v }
|
st        j                  d|
fd||	f      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |	      dz  }t        j                  d|j                        dz   d|iz  }t        t        j                  |            dx}x}
}	y)ug   R2: source 도 없고 훅 환경에도 변수가 없으면 '차단'이 아니라 '경고'여야 한다.r	  N)r   r$  GH_TOKENr   r/   r   r   r   r   u*   R2 는 경고여야 하는데 차단됐다r   r"   u   경고r   )z6%(py1)s in %(py5)s
{%(py5)s = %(py3)s.system_message
}r   u-   R2 경고 메시지가 사라졌다(회귀): r5   r6   )r   rg   r   r   r8   r   rB   rQ   rD   rE   rF   rG   rC   rH   rI   r:   )r  _r%   r   rZ   r[   r\   rw   r   ra   rx   rb   rc   s                r   5test_unbacked_variable_token_warns_but_does_not_blockr)  x  s    *62MAw
Tx}}tVZ[
\C;;M;%MMM;%MMMMMM3MMM3MMM;MMMMMM%MMM%MMMM!MMMMMMM s)) 8))  8)                  *    88J8J7MN     r   c                D   t        |       d   \  }}}t        |t        | j                  d            }|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}}|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  }	t        j                  d|j                        dz   d|	iz  }
t        t        j                  |
            dx}x}}y)ui   리터럴 토큰은 공급 경로가 자명하므로 경고조차 없어야 한다(경고 남발 방지).r  Nr#  r   r/   r   r   r   r   zassert %(py6)sr"   r7   )z6%(py2)s
{%(py2)s = %(py0)s.system_message
} == %(py5)sr2   u   불필요한 경고: r5   r6   )r   rg   r   r   r8   r   rB   rQ   rD   rE   rF   rG   rH   rI   r:   rC   )r  r(  r%   r   rZ   r[   r\   rw   ra   rb   rc   s              r   &test_literal_token_produces_no_warningr+    s6    *62MAw
Tx}}tL
MC;;;%;%33;%%SS#SSSSSSSSS3SSS3SSSSSSSSS'<S=O=O<R%SSSSSSSSr   	N1N2N3N4N5N6N7N8X1c                    d|  d}d}d||z   dz   t         fd|dz   t         fddt         fd	d
t         fd||z   dz   t         fd||z   dz   t         fddt         fddt         fddt        fd	S )uV   ANU 가 실제로 쓰는 정상 경로 — 여기가 막히면 운영이 정지한다.r   z	; set +a
r  u"   PR 머지 (정본 source + 토큰)zOgh api -X PUT repos/Jeon-Jonghyuk/InsuRo/pulls/256/merge -f merge_method=squashu#   PR 머지 (source 없이 토큰만)u   PR 상태 조회z7gh pr view 256 --repo Jeon-Jonghyuk/InsuRo --json stateu   CI 체크 조회z9gh api repos/Jeon-Jonghyuk/InsuRo/commits/main/check-runsu   PR 생성 (토큰 주입)r   u'   신규 파일 contents PUT (sha 없음)zegh api -X PUT repos/Jeon-Jonghyuk/InsuRo/contents/new.txt -f message=m -f content=eA== -f branch=mainu   일반 셸 명령zgit status --short && ls -lau   조회 작업 (--jq)z9gh api repos/Jeon-Jonghyuk/InsuRo/pulls --jq '.[].number'u$   무토큰 머지는 반드시 차단r,  )r   r   )	keys_pathsrctoks      r   _redline_casesr:    s    I;j
1C
)C3Sy ( ().0 5 ( ().0 "H%Q!JES*C#I8Y,Y[`a8Sy = =>CE #$BEJ%JES5()-/% r   c                   |dz  }|j                  dd       t        |      |    \  }}}t        |t        |            }|j                  }||k(  }|st        j                  d|fd||f      d	t        j                         v st        j                  |      rt        j                  |      nd	t        j                  |      d
t        j                         v st        j                  |      rt        j                  |      nd
dz  }	t        j                  |  d| d|d| d|j                   d|j                  dd        dz   d|	iz  }
t        t        j                  |
            dx}}y)uh   레드라인: 가드가 ANU 의 정상 운영 경로(머지/조회/PR생성)를 막지 않아야 한다.r   r   ri   r   r   r   r/   r   r   r   r   r   r   r   r   r   Nr   r   r"   )r   r:  rg   r   r8   rB   rQ   rD   rE   rF   rG   rC   r9   rH   rI   )r!  r   r   r   r%   r   r   rZ   r[   r\   rw   s              r   %test_anu_normal_paths_are_not_blockedr<    sU    j DOO;gON-d3G<E7H
Tx0
1C;; ;("   ;(                    #    #    )1UG6'IhZx} U**Tc"#	%    r   c                    d} t         | u}|st        j                  d|fdt         | f      dt        j                         v st        j
                  t               rt        j                  t               ndt        j                  |       dz  }t        j                  dt               dz   d|iz  }t        t        j                  |            dx}} t        j                  }t        j                  } |       } ||      }|j                  } |       }|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                  |      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t        j                         v st        j
                  t               rt        j                  t               ndd	z  }
t        j                  d      dz   d|
iz  }t        t        j                  |            dx}x}x}x}x}x}}	y)uv   이 스위트는 훅을 '읽기만' 해야 한다 — 실행 중 본문이 바뀌면 테스트 자체가 오염이다.N)is not)z%(py0)s is not %(py3)s_DIGEST_AT_IMPORT)r   r!   u.   수집 시점에 가드가 없었다(회귀): z
>assert %(py5)sr3   r/   )z%(py13)s
{%(py13)s = %(py11)s
{%(py11)s = %(py9)s
{%(py9)s = %(py2)s
{%(py2)s = %(py0)s.md5
}(%(py7)s
{%(py7)s = %(py5)s
{%(py5)s = %(py3)s.read_bytes
}()
})
}.hexdigest
}()
} == %(py15)shashlibr   )	r   r   r!   r3   r6   py9py11py13py15u2   테스트 실행 중 가드 본문이 변경됐다z
>assert %(py17)spy17)r?  rB   rQ   rD   rE   rF   rG   rC   r   rH   rI   r@  md5
read_bytes	hexdigest)rx   rZ   @py_format4rb   ra   ry   r{   @py_assert10@py_assert12@py_assert14@py_format16@py_format18s               r   )test_hook_file_is_untouched_by_this_suiterO    s$   $(aD(aaaDaaaaaaaaaaaaDaaa,Z[_Z`*aaaaaaa;; t ( ;() )33 35 59JJ   59J                            '    )    *    4    6      :K    :K    	=      r   )returnr
   )N)r%   r   r   zdict | NonerP  r   )r7  r   rP  r
   )6__doc__
__future__r   builtinsrD   _pytest.assertion.rewrite	assertionrewriterB   r@  rL   r   rN   pathlibr   typesr   rW   r   r   r   r   r  rA   rF  rG  rH  r?  r   rg   r~   r   
BASE_CASESmarkparametrizer   r   r   r   r   r   r  r  	R1_R2_IDSfixturer  r   r%  r)  r+  REDLINE_IDSr:  r<  rO  )r   s   0r   <module>r_     s	   #     	   ! 7812	 CG,,.KGKK 12<<>VZ %RkW*d%'A4Hd  :DBd
 #CTKd "<eEd 3U;d 0%8d *E2d A5Id" #@$H#d& 	!#QSWX'd( 	CTJ)d* 	=B %&'+-+d0 	<dC1d2 	%2%*-II4Q3d6 	0%*/01577d< 	0A4I=d@ 	"JO6L$LdSAdB KTRCdD @$GEdF 	EtLGdH 	)DdLIdL 	EtLMdN 	 "KTROdP 	(BDJQdT 	%'I4PUdV 8$?WdX 	8$?YdZ 	5t<[d\ )40]d^ 	$&BDI_d` 	<B%*U2R7/0157adf 	8@$Hgdj 	*,PRVWkdl 	JDQmdn 	;TBodp 	) $&qdv 	%'NPTUwdx 	&5t=yd@ 	,MtUAdD 	 "KTREdF 	-"$'::DBGdJ 	93b8/0157KdP 	')JDQQdR 	&NPUWSdZ 	
!;UD[d^ 	
95A_d` 	
 "H%Padb 	
(cdh 	
%15:idl 	
 "FMmdn 	
 7?odp 	
!UW\]qdr 	
"$:EBsdt 	
.*R/2LLeUudx 	
5B33b85@$Hyd| 	
A77?}d@ 	
,?HAdD 	
#%BEJEdF 	
&(A5IGd
N  '(1QqTF(  

6 ) n 
VH  A	 	< 	<&R I. /T E8 K0	 1	k 	)s   4L