
    wj                       d Z ddlmZ ddlZddlmc mZ ddl	Z	ddl
Z
ddlZddlmZ ddl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mZ dZdZd	d
ddZd Z d Z!d Z"d Z#d Z$d Z%d Z&d Z'd Z(d Z)d Z*d Z+d Z,d Z-d Z.y)u  anu_v2.tests.test_owner_trigger_dedupe_2554 — audit append-only + atomic dedupe 회귀 (task-2554).

회장 §명시 14장 §8 1:1:
  - same pr + same head + action=POST_GEMINI_REVIEW_TRIGGER_COMMENT + result=POSTED → 차단
  - audit JSONL append-only (``open("a")`` 강제)
  - atomic fcntl.flock
  - head 변경 시 stale reset

system spec §10 tests #6, #12, #13 (3건) + 본 파일 추가 dedupe 회귀.
    )annotationsN)Path   )AUDIT_REL_PATHAUDIT_SCHEMAAuditRedactionErrorDedupeViolationOwnerTriggerAudittoken_hash_prefix(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbPOSTEDresultc               $    d| |d|dd|  ddddd	
S )
Nz	task-2554"POST_GEMINI_REVIEW_TRIGGER_COMMENTz/gemini reviewz/repos/o/r/issues/z	/commentszdecision.jsonTdeadbeef)
task_idprheadactionr   comment_bodyendpointdecision_pathtoken_presentr    )r   r   r   s      B/home/jay/workspace/anu_v2/tests/test_owner_trigger_dedupe_2554.py_rowr   '   s3    6((I6('     c                   t        |       }|j                  }| t        z  }||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t        j                         v st        j                  t              rt        j                  t              nddz  }dd|iz  }t        t        j                  |            d x}x}}y )	N==)z8%(py2)s
{%(py2)s = %(py0)s.path
} == (%(py4)s / %(py5)s)audittmp_pathr   )py0py2py4py5assert %(py8)spy8)r
   pathr   
@pytest_ar_call_reprcompare@py_builtinslocals_should_repr_global_name	_safereprAssertionError_format_explanation)r$   r#   @py_assert1@py_assert6@py_assert3@py_format7@py_format9s          r   $test_audit_path_under_workspace_rootr9   8   s    h'E::2N22:22222:222222252225222:222222222222222N222N2222222r   c                R   t        |       }|j                  t        dt                     |j                  j                  d      j                         D cg c]  }|st        j                  |       }}t        |      }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  t              rt        j                  t              nddt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      d	z  }d
d|iz  }t!        t        j"                  |            d x}x}}|d   }	|	d   }
|
t$        k(  }|st        j                  d|fd|
t$        f      t        j                  |
      dt        j                         v st        j                  t$              rt        j                  t$              nddz  }dd|iz  }t!        t        j"                  |            d x}
}|	d   }
d}|
|k(  }|slt        j                  d|fd|
|f      t        j                  |
      t        j                  |      dz  }dd|iz  }t!        t        j"                  |            d x}
x}}|	d   }
|
t        k(  }|st        j                  d|fd|
t        f      t        j                  |
      dt        j                         v st        j                  t              rt        j                  t              nddz  }dd|iz  }t!        t        j"                  |            d x}
}|	d   }
d}|
|k(  }|slt        j                  d|fd|
|f      t        j                  |
      t        j                  |      dz  }dd|iz  }t!        t        j"                  |            d x}
x}}|	d   }
d}|
|k(  }|slt        j                  d|fd|
|f      t        j                  |
      t        j                  |      dz  }dd|iz  }t!        t        j"                  |            d x}
x}}|	d   }
d}|
|u }|slt        j                  d |fd!|
|f      t        j                  |
      t        j                  |      dz  }dd|iz  }t!        t        j"                  |            d x}
x}}d"}
|
|	v }|st        j                  d#|fd$|
|	f      t        j                  |
      d%t        j                         v st        j                  |	      rt        j                  |	      nd%dz  }dd|iz  }t!        t        j"                  |            d x}
}y c c}w )&Ng   utf-8encoding   r!   z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py6)slenlinesr%   py1py3py6r)   r*   r   schema)z%(py1)s == %(py3)sr   rD   rE   assert %(py5)sr(   r   z%(py1)s == %(py4)srD   r'   assert %(py6)srF   r   _HEAD_Ar   r   r   r   token_value_loggedF)is)z%(py1)s is %(py4)stsinz%(py1)s in %(py3)srec)r
   appendr   rM   r+   	read_text
splitlinesjsonloadsrA   r,   r-   r.   r/   r0   r1   r2   r3   r   )r$   r#   linerB   @py_assert2@py_assert5@py_assert4r7   r8   rT   @py_assert0@py_format4@py_format6r6   @py_format5s                  r   2test_append_writes_jsonl_line_with_required_fieldsrb   =   sF   h'E	LLc7#$*/***>*>*>*P*[*[*]f$aeTZZfEfu::?:33uu:
(Cx=(=L((((=L(((=((((((L(((L(((((((t9999v;!;'!!!!;'!!!;!!!!!!'!!!'!!!!!!!x=@@@=@@@@@=@@@@=@@@@@@@@@@@x=$H$=H$$$$=H$$$=$$$H$$$$$$$#$--$----$---$----------43;43433 gs   V$V$c                b   t        |       }|j                  t        dt                     |j                  t        dt                     |j
                  j                  d      }|j                  }d} ||      }d}||k(  }|st        j                  d|fd||f      d	t        j                         v st        j                  |      rt        j                  |      nd	t        j                  |      t        j                  |      t        j                  |      t        j                  |      d
z  }dd|iz  }	t        t        j                  |	            d x}x}x}x}}|j!                         D 
cg c]  }
|
st#        j$                  |
       }}
|D ch c]  }|d   	 }}ddh}||k(  }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            d x}x}}y c c}
w c c}w )Nr;   h   r<   r=   
r   r!   zK%(py6)s
{%(py6)s = %(py2)s
{%(py2)s = %(py0)s.count
}(%(py4)s)
} == %(py9)srawr%   r&   r'   rF   py9assert %(py11)spy11r   rJ   rK   rL   rF   )r
   rU   r   rM   _HEAD_Br+   rV   countr,   r-   r.   r/   r0   r1   r2   r3   rW   rX   rY   )r$   r#   rg   r4   r6   r\   @py_assert8@py_assert7@py_format10@py_format12lrowsrr^   r[   ra   r7   s                    r   test_append_only_no_truncateru   L   si   h'E	LLc7#$	LLc7#$
**



0C99T9T?a?a?a339T?a#&>>#39aqDJJqM9D9!"AdG"0"sCj0"j0000"j000"000j0000000 :"s   6H'>H'H,c                 j   t         dz  dz  j                  d      } d}|| v }|st        j                  d|fd|| f      t        j                  |      dt        j                         v st        j                  |       rt        j                  |       ndd	z  }d
d|iz  }t        t        j                  |            dx}}g d}|D ]  }|| v}|st        j                  d|fd|| f      dt        j                         v st        j                  |      rt        j                  |      nddt        j                         v st        j                  |       rt        j                  |       nddz  }t        j                  d|       dz   d|iz  }	t        t        j                  |	            d} y)uW   source 정적 검사: ``open(.., "a")`` 만 사용. ``"w"`` / ``"r+"`` / ``"a+"`` 0건.anu_v2zowner_trigger_audit.pyr<   r=   zopen(self._path, "a"rQ   rS   	audit_srcrH   rI   r(   N)zopen(self._path, "w"zopen(self._path, "r+"zopen(self._path, "a+"zopen(self._path, "w+")not in)z%(py0)s not in %(py2)sf)r%   r&   z&forbidden mode found in audit source: z
>assert %(py4)sr'   )WORKSPACE_ROOTrV   r,   r-   r1   r.   r/   r0   r2   r3   _format_assertmsg)
rx   r^   r[   r_   r`   forbidden_modesrz   r4   @py_format3ra   s
             r   $test_open_mode_is_append_only_staticr   W   s   (*-EEPPZaPbI!.!Y....!Y...!......Y...Y.......yO P	!OOOq	OOOOOOqOOOqOOOOOO	OOO	OOOO%KA3#OOOOOOOPr   c                    t        |       }|j                  t        dt                     t	        j
                  t              5  |j                  dt               d d d        y # 1 sw Y   y xY wNr;   r   r   )r
   rU   r   rM   pytestraisesr	   check_deduper$   r#   s     r   *test_check_dedupe_blocks_same_pr_same_headr   c   sS    h'E	LLc7#$		' 1c01 1 1s   A%%A.c                    t        |       }|j                  t        dt                     |j	                  dt
               y r   )r
   rU   r   rM   r   rl   r   s     r   6test_check_dedupe_allows_after_head_change_stale_resetr   j   s2    h'E	LLc7#$	#G,r   c                    t        |       }|j                  t        dt                     |j	                  dt               y )Nr;   rd   r   r
   rU   r   rM   r   r   s     r   %test_check_dedupe_allows_different_prr   q   s2    h'E	LLc7#$	#G,r   c                    t        |       }|j                  t        dt        d             |j	                  dt               y )Nr;   DEDUPEDr   r   r   r   s     r   0test_check_dedupe_does_not_block_failed_attemptsr   w   s4    h'E	LLc7956	#G,r   c                    t        |       }|j                  t        dt                     t	        j
                  t              5  |j                  t        dt                     ddd       y# 1 sw Y   yxY w)uk   append() 내부 두 번째 dedupe check (lock 보호) — race 시 같은 (pr, head) 두 번 기록 못함.r;   N)r
   rU   r   rM   r   r   r	   r   s     r   .test_atomic_re_check_inside_append_blocks_racer      sU    h'E 
LLc7#$		' )T#w'() ) )s    A--A6c                   t        |       g t        j                         fd}t        d      D cg c]  }t        j                  |       }}|D ]  }|j                           |D ]  }|j                           j                  }d} ||      }d}||k(  }	|	st        j                  d|	fd||f      dt        j                         v st        j                        rt        j                        ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      d	z  }
d
d|
iz  }t        t        j                  |            dx}x}x}x}	}j                  }d} ||      }d}||k(  }	|	st        j                  d|	fd||f      dt        j                         v st        j                        rt        j                        ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      d	z  }
d
d|
iz  }t        t        j                  |            dx}x}x}x}	}yc c}w )u\   동시 두 thread 가 같은 (pr, head) POSTED append 시도 → 정확히 하나만 성공.c                    	  j                  t        dt                     5  j                  d       d d d        y # 1 sw Y   y xY w# t        $ r, 5  j                  d       d d d        Y y # 1 sw Y   Y y xY ww xY w)Nr;   OKr   )rU   r   rM   r	   )r#   lockresultss   r   workerz9test_concurrent_appends_only_one_succeeds.<locals>.worker   sq    	*LLc7+, %t$% % % 	* *y)* * *	*sC   !A ?A AA A B A2(B 2A<	7B <B    )targetr   r?   r!   rf   r   rh   rj   rk   Nr      )r
   	threadingLockrangeThreadstartjoinrm   r,   r-   r.   r/   r0   r1   r2   r3   )r$   r   _threadstr4   r6   r\   rn   ro   rp   rq   r#   r   r   s               @@@r   )test_concurrent_appends_only_one_succeedsr      s   h'EG>>D* 9>aA1yv.AGA 		 	 ==##=#!#!####!######7###7###=#########!#######==((=#(q(#q((((#q((((((7(((7(((=((((((#(((q(((((((( Bs   Jc                    t        |       }t        dt              }d|d<   t        j                  t
              5  |j                  |       d d d        y # 1 sw Y   y xY w)Nr;   ghp_realtoken_should_not_leakr   r
   r   rM   r   r   r   rU   r$   r#   bads      r   %test_audit_rejects_raw_token_sentinelr      sQ    h'E
sG
C>C	*	+ S     AAc                    t        |       }t        dt              }d|d<   t        j                  t
              5  |j                  |       d d d        y # 1 sw Y   y xY w)Nr;   anythingleaked_secretr   r   s      r   test_audit_rejects_extra_keysr      sP    h'E
sG
C%C	*	+ S  r   c                    t        |       }t        dt              }d|d<   t        j                  t
              5  |j                  |       d d d        y # 1 sw Y   y xY w)Nr;   TrN   r   r   s      r   *test_audit_rejects_token_value_logged_truer      sQ    h'E
sG
C $C	*	+ S  r   c                    d} t        |       }dd l}|j                  | j                               j	                         d d }||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }dd	|iz  }t        t        j                  |            d x}}t        |      }d}||k(  }|st        j                  d|fd
||f      dt        j                         v st        j                  t              rt        j                  t              nddt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }	dd|	iz  }
t        t        j                  |
            d x}x}}y )Nz
any-secretr   r   r!   )z%(py0)s == %(py3)sp)r%   rE   rI   r(   r@   rA   rC   r)   r*   )r   hashlibsha256encode	hexdigestr,   r-   r.   r/   r0   r1   r2   r3   rA   )r   r   r   r[   r4   r_   r`   r\   r]   r7   r8   s              r   .test_token_hash_prefix_is_sha256_first_8_charsr      s   A!Aqxxz*446r::1:::::1:::::::1:::1:::::::::::q6Q6Q;6Q33qq6Qr   c                 v    t        j                  t              5  t        d       d d d        y # 1 sw Y   y xY w)N )r   r   
ValueErrorr   r   r   r   )test_token_hash_prefix_empty_token_raisesr      s+    	z	" "  s   /8)r   intr   strr   r   returndict)/__doc__
__future__r   builtinsr.   _pytest.assertion.rewrite	assertionrewriter,   rX   sysr   pathlibr   r   __file__resolveparentsr{   r   r+   insertanu_v2.owner_trigger_auditr   r   r   r	   r
   r   rM   rl   r   r9   rb   ru   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   <module>r      s   	 #    
   h'')11!4~chh&HHOOAs>*+  
 /7 "3
1P1---))8r   