
    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ZddlZej                  j                  dd       dZdZi e
j$                  ddiZddd	Zddd
Z G d d      Zy)uu  Tests for v3.6 Harness — production-load hook subprocess simulation.

chair_authorization_id=CHAIR-AUTH-TASK-2703-V36-HARNESS-MVP-260528

Verifies that the hook script works when called as a subprocess (as Claude Code does):
  - stdin JSON → exit code + stdout JSON (for DENY/HOLD)
  - DENY exits 2 with {"decision":"block",...} stdout
  - ALLOW exits 0 with no stdout
    )annotationsNz/home/jay/workspacez3/home/jay/.claude/hooks/pre_tool_use_v36_harness.pyz$/tmp/v36_harness_decision_test.jsonlANU_V36_HARNESS_TEST_MODE1c                    |xs t         }t        j                  t        j                  t
        gt        j                  |       dd|d      }|j                  |j                  |j                  fS )zIRun hook subprocess with JSON stdin. Returns (exit_code, stdout, stderr).T
   inputcapture_outputtextenvtimeout)	_BASE_ENV
subprocessrunsys
executable
_HOOK_PATHjsondumps
returncodestdoutstderr)eventr   results      ?/home/jay/workspace/tests/harness/test_v36_harness_hook_load.py	_run_hookr      s[    

C^^	$jjF fmmV]]::    c                Z   g }t         j                  j                  t              s|S t	        t        d      5 }|D ]:  }|j                         }|s	 |j                  t        j                  |             < 	 d d d        ||  d  S # t        j                  $ r Y aw xY w# 1 sw Y   (xY w)Nzutf-8)encoding)
ospathexists_TEST_JSONLopenstripappendr   loadsJSONDecodeError)nrecordsflines       r   _read_jsonl_tailr-   *   s    G77>>+&	kG	,  	D::<DNN4::d#34		 A23< ++  s/   B!$B7B!BB!BB!!B*c                  r    e 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d Zd Zd Zy)TestHookLoadc                ~    t         j                  j                  t              rt        j                  t               yy)z"Clear test JSONL before each test.N)r    r!   r"   r#   unlink)selfs    r   setup_methodzTestHookLoad.setup_method:   s#    77>>+&IIk" 'r   c                   t         j                  }|j                  } |t              }|st	        j
                  dt               dz   dt        j                         v st	        j                  t               rt	        j                  t               ndt	        j                  |      t	        j                  |      dt        j                         v st	        j                  t              rt	        j                  t              ndt	        j                  |      dz  }t        t	        j                  |            dx}x}}y)z&Hook file must exist at expected path.zHook not found at zd
>assert %(py7)s
{%(py7)s = %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.path
}.exists
}(%(py5)s)
}r    r   )py0py2py4py5py7N)r    r!   r"   r   
@pytest_ar_format_assertmsg@py_builtinslocals_should_repr_global_name	_safereprAssertionError_format_explanation)r2   @py_assert1@py_assert3@py_assert6@py_format8s        r   test_hook_file_existsz"TestHookLoad.test_hook_file_exists?   s    wwLw~~L~j)L)LL-?
|+LLLLLLLrLLLrLLLwLLL~LLLLLLjLLLjLLL)LLLLLLr   c                   dddid}t        |      \  }}}d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j
                  |      rt        j                  |      ndt        j                  |      d	z  }t        j                  d
| d|      dz   d|iz  }t        t        j                  |            dx}}t        j                  |      }	|	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   }|
|v }|slt        j                  d|fd|
|f      t        j                  |
      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}
x}}d}
|	d   }|
|v }|slt        j                  d|fd|
|f      t        j                  |
      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}
x}}y)z-git push --force must exit 2 with block JSON.Bashcommandgit push origin main --force	tool_name
tool_input   ==z%(py0)s == %(py3)scoder5   py3zExpected exit 2, got 	; stderr=
>assert %(py5)sr8   Ndecisionblockz%(py1)s == %(py4)spy1r7   assert %(py6)spy6V36_HARNESSreasoninz%(py1)s in %(py4)szpattern.forbidden_tool_or_shell)r   r:   _call_reprcomparer<   r=   r>   r?   r;   r@   rA   r   r'   )r2   r   rR   r   r   @py_assert2rB   @py_format4@py_format6resp@py_assert0rC   @py_format5@py_format7s                 r   test_git_push_force_deniedz'TestHookLoad.test_git_push_force_deniedC   s   $Y@^4_`(/ffKtqyKKKtqKKKKKKtKKKtKKKqKKK1$y
KKKKKKKzz&!J*7*7****7******7*******.X.}....}...}..........0BDNB0NBBBB0NBBB0BBBNBBBBBBBr   c                   dddid}t        |      \  }}}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        j                  |      }	|	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}}y)z)gh pr create must exit 2 with block JSON.rH   rI   zgh pr create --title 'Bad PR'rK   rN   rO   rQ   rR   rS   assert %(py5)sr8   NrW   rX   rY   rZ   r\   r]   r   r:   rc   r<   r=   r>   r?   r@   rA   r   r'   r2   r   rR   r   _stderrrd   rB   re   rf   rg   rh   rC   ri   rj   s                 r   test_gh_pr_create_deniedz%TestHookLoad.test_gh_pr_create_deniedM   s    $Y@_4`a )% 0fgtqytqttqzz&!J*7*7****7******7*******r   c                   dddid}t        |      \  }}}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        j                  |      }	|	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}}y)z(gh pr merge must exit 2 with block JSON.rH   rI   zgh pr merge 158 --squashrK   rN   rO   rQ   rR   rS   rm   r8   NrW   rX   rY   rZ   r\   r]   rn   ro   s                 r   test_gh_pr_merge_deniedz$TestHookLoad.test_gh_pr_merge_deniedU   s    $Y@Z4[\ )% 0fgtqytqttqzz&!J*7*7****7******7*******r   c                   dddid}t        |      \  }}}d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j
                  |      rt        j                  |      ndt        j                  |      d	z  }t        j                  d
| d|      dz   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                  |	      t        j                  |
      dz  }t        j                  d|      dz   d|iz  }t        t        j                  |            dx}x}	x}}
y)z$ls must exit 0 with empty/no stdout.rH   rI   ls -larK   r   rO   rQ   rR   rS   zExpected exit 0, got rU   rV   r8   N )zD%(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.strip
}()
} == %(py7)sr   )r5   r6   r7   r9   zExpected empty stdout, got z
>assert %(py9)spy9)r   r:   rc   r<   r=   r>   r?   r;   r@   rA   r%   )r2   r   rR   r   r   rd   rB   re   rf   rC   rD   @py_assert5rE   @py_format10s                 r   test_ls_allowedzTestHookLoad.test_ls_allowed]   s"   $Y4IJ(/ffKtqyKKKtqKKKKKKtKKKtKKKqKKK1$y
KKKKKKK||M|~MM~#MMM~MMMMMMvMMMvMMM|MMM~MMMMMM'B6*%MMMMMMMMr   c                |   dddid}t        |      \  }}}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}}y)zgit status must exit 0.rH   rI   z
git statusrK   r   rO   rQ   rR   rS   rm   r8   N	r   r:   rc   r<   r=   r>   r?   r@   rA   	r2   r   rR   _stdoutrp   rd   rB   re   rf   s	            r   test_git_status_allowedz$TestHookLoad.test_git_status_allowede   sz    $Y4MN!*5!1gwtqytqttqr   c                   dddid}t        |      \  }}}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        j                  |      }	d}
|	d   }|
|v }|slt        j                  d|fd|
|f      t        j                  |
      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}
x}}y)z"gh run watch must exit 2 (rule 2).rH   rI   zgh run watch 99999rK   rN   rO   rQ   rR   rS   rm   r8   Nz!pattern.anu_direct_ci_gemini_waitr_   r`   rb   rZ   r\   r]   rn   ro   s                 r   test_gh_run_watch_deniedz%TestHookLoad.test_gh_run_watch_deniedk   s    $Y@T4UV )% 0fgtqytqttqzz&!2Dd8nD2nDDDD2nDDD2DDDnDDDDDDDr   c                |   dddid}t        |      \  }}}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}}y)z#tail -f .done must exit 2 (rule 1).rH   rI   z8tail -f /home/jay/workspace/memory/events/task-2703.donerK   rN   rO   rQ   rR   rS   rm   r8   Nr|   r}   s	            r   test_tail_f_deniedzTestHookLoad.test_tail_f_denieds   s      $&`a
 "+5!1gwtqytqttqr   c                   dddid}t        |       t        d      }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 cg c]  }|j                  d      dv s| }	}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c c}w )z1After DENY, JSONL must contain at least 1 record.rH   rI   rJ   rK         >=z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} >= %(py6)slenr*   r5   r[   rT   r]   assert %(py8)spy8NrW   DENYHOLD_FOR_CHAIRdeny_records)r   r-   r   r:   rc   r<   r=   r>   r?   r@   rA   get)
r2   r   r*   rd   rx   @py_assert4rj   @py_format9rr   s
             r   "test_jsonl_deny_record_accumulatedz/TestHookLoad.test_jsonl_deny_record_accumulated|   sj   $Y@^4_`%"1%7| q |q    |q      s   s      7   7   |   q       #*^aaeeJ.?C].]^^< %A% A%%%% A%%%%%%s%%%s%%%%%%<%%%<%%% %%%A%%%%%%% _s   2I"I"c                   dddid}t        |       t        d      }|D cg c]  }|j                  d      dv s| }}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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      dz   d|iz  }t        t	        j                  |            d} yc c}w )z2DENY JSONL record must have all 5 required fields.rH   rI   zgh pr merge 200 --squashrK   r   rW   r   r   r   r   r   denyr   r   r   N)tsrW   matched_rulecommand_or_tooltask_id	timestampr`   )z%(py0)s in %(py2)sfieldrec)r5   r6   zMissing required field z in DENY recordz
>assert %(py4)sr7   )r   r-   r   r   r:   rc   r<   r=   r>   r?   r@   rA   r;   )r2   r   r*   r   r   rd   rx   r   rj   r   r   r   rB   @py_format3ri   s                  r   $test_deny_record_has_required_fieldsz1TestHookLoad.test_deny_record_has_required_fields   so   $Y@Z4[\%"1%"VaaeeJ&7;U&UVV4yAyA~yAss44yA2hb 	TEC<SSS5CSSSSSS5SSS5SSSSSSCSSSCSSSS#:5)?!SSSSSSS	T Ws
   IIc                ^   dddid}t        |       t        d      }|D cg c]  }|j                  d      dv s| }}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   }
|
j                  }d} ||      }d}||k(  }|st	        j
                  d|fd||f      t	        j                  |
      t	        j                  |      t	        j                  |      t	        j                  |      t	        j                  |      dz  }dd|iz  }t        t	        j                  |            dx}
x}x}x}x}}yc c}w )z0DENY record must contain chair_authorization_id.rH   rI   zgit push origin mainrK   r   rW   r   r   r   r   r   r   r   r   r   Nr   chair_authorization_idz+CHAIR-AUTH-TASK-2703-V36-HARNESS-MVP-260528rO   )zJ%(py7)s
{%(py7)s = %(py3)s
{%(py3)s = %(py1)s.get
}(%(py5)s)
} == %(py10)s)r[   rT   r8   r9   py10zassert %(py12)spy12)r   r-   r   r   r:   rc   r<   r=   r>   r?   r@   rA   )r2   r   r*   r   r   rd   rx   r   rj   r   rh   rD   @py_assert9@py_assert8@py_format11@py_format13s                   r   +test_deny_record_has_chair_authorization_idz8TestHookLoad.test_deny_record_has_chair_authorization_id   sm   $Y@V4WX%"1%"VaaeeJ&7;U&UVV4yAyA~yAss44yABxfx||f4f|45f9ff59fffff59ffffxfff|fff4fff5fff9fffffffff Ws
   H*H*c                   i t         ddi}dddid}t        ||      \  }}}d}||k(  }|st        j                  d	|fd
||f      dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }t        j                  d|       dz   d|iz  }	t        t        j                  |	            dx}}y)zCANU_DISABLE_V36_HARNESS=1 must allow everything (emergency bypass).ANU_DISABLE_V36_HARNESSr   rH   rI   rJ   rK   r   r   rO   rQ   rR   rS   z&Emergency bypass must allow; got exit=rV   r8   N)r   r   r:   rc   r<   r=   r>   r?   r;   r@   rA   )
r2   r   r   rR   r~   rp   rd   rB   re   rf   s
             r   *test_emergency_bypass_allows_forbidden_cmdz7TestHookLoad.test_emergency_bypass_allows_forbidden_cmd   s    ;;5s;$Y@^4_`!*5c!:gwItqyIIItqIIIIIItIIItIIIqIIIB4&IIIIIIIr   c                R   i t         ddi}dddid}t        ||      \  }}}d}||k(  }|st        j                  d	|fd
||f      dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }t        j                  d|       dz   d|iz  }	t        t        j                  |	            dx}}t        d      }
|
D cg c]  }|j                  d      dk(  s| }}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c c}w )zAANU_V36_HARNESS_DRY_RUN=1 must exit 0 but log DRY_RUN_WOULD_DENY.ANU_V36_HARNESS_DRY_RUNr   rH   rI   rJ   rK   r   r   rO   rQ   rR   rS   zDry-run must allow; got exit=rV   r8   Nr   rW   DRY_RUN_WOULD_DENYr   r   r   r   
would_denyr   r   r   )r   r   r:   rc   r<   r=   r>   r?   r;   r@   rA   r-   r   r   )r2   r   r   rR   r~   rp   rd   rB   re   rf   r*   r   r   rx   r   rj   r   s                    r   'test_dry_run_allows_but_logs_would_denyz4TestHookLoad.test_dry_run_allows_but_logs_would_deny   s_   ;;5s;$Y@^4_`!*5c!:gw@tqy@@@tq@@@@@@t@@@t@@@q@@@9$@@@@@@@"1%!(VAAEE*,=AU,UaV
V:#!#!####!######s###s######:###:######!####### Ws   3H$H$c                   t        j                  t        j                  t        gdddt
        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  }d
d|iz  }t        t        j                  |            dx}x}}y)u/   Empty/invalid stdin must not crash — exits 0.rv   Tr   r   r   rO   z2%(py2)s
{%(py2)s = %(py0)s.returncode
} == %(py5)sr   r5   r6   r8   assert %(py7)sr9   Nr   r   r   r   r   r   r   r:   rc   r<   r=   r>   r?   r@   rA   r2   r   rB   r   rC   rf   rE   s          r   test_empty_stdin_allowsz$TestHookLoad.test_empty_stdin_allows   s    ^^Z(
   %A% A%%%% A%%%%%%v%%%v%%% %%%A%%%%%%%r   c                   t        j                  t        j                  t        gdddt
        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  }d
d|iz  }t        t        j                  |            dx}x}}y)u<   Malformed JSON stdin must not crash — exits 0 (fail-safe).znot valid json {{Tr   r   r   rO   r   r   r   r   r9   Nr   r   s          r   test_invalid_json_stdin_allowsz+TestHookLoad.test_invalid_json_stdin_allows   s    ^^Z(%
   %A% A%%%% A%%%%%%v%%%v%%% %%%A%%%%%%%r   c                   dddid}t        j                         }t        |       t        j                         |z
  dz  }d}||k  }|st        j                  d|fd||f      d	t        j                         v st        j                  |      rt        j                  |      nd	t        j                  |      d
z  }t        j                  d|dd      dz   d|iz  }t        t        j                  |            dx}}y)z*Hook execution must complete within 200ms.rH   rI   ru   rK   i  i  )<)z%(py0)s < %(py3)s
elapsed_msrS   z
Hook took z.1fu1   ms (target ≤200ms, subprocess overhead allowed)rV   r8   N)timeperf_counterr   r:   rc   r<   r=   r>   r?   r;   r@   rA   )r2   r   t0r   rd   rB   re   rf   s           r   test_hook_speed_under_200msz(TestHookLoad.test_hook_speed_under_200ms   s    $Y4IJ %'')B.$6
ozCooozCoooooozooozoooCooo:j-==n!ooooooor   N)__name__
__module____qualname__r3   rF   rk   rq   rs   rz   r   r   r   r   r   r   r   r   r   r   r    r   r   r/   r/   9   s_    #
MC++NE&	TgJ$
&
&pr   r/   )N)r   dictr   zdict | Nonereturnztuple[int, str, str])r   )r)   intr   z
list[dict])__doc__
__future__r   builtinsr<   _pytest.assertion.rewrite	assertionrewriter:   r   r    r   r   r   r!   insertr   r#   environr   r   r-   r/   r   r   r   <module>r      so    #    	  
  ( )B
4 =rzz<6<	;Op Opr   