
    iY                       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ej                  j                  dd       ddlZddlmZmZmZmZ ddlZddlmZmZmZmZ ddlmZ ddlmZ dMdNd	ZdOd
Z G d d      Z  G d d      Z! G d d      Z" G d d      Z# G d d      Z$ G d d      Z% G d d      Z& G d d      Z' G d d      Z( G d d      Z) G d d       Z* G d! d"      Z+ G d# d$      Z,e-d%k(  rPddl.Z.d& Z/g Z0e0jc                   e/d' e        jd                               e0jc                   e/d( e        jf                               e0jc                   e/d) e        jh                               e0jc                   e/d* e!       jj                               e0jc                   e/d+ e!       jl                               e0jc                   e/d, e!       jn                               e0jc                   e/d- e"       jp                               e0jc                   e/d. e"       jr                               e0jc                   e/d/ e$       jt                               e0jc                   e/d0 e$       jv                               e0jc                   e/d1 e$       jx                               e0jc                   e/d2 e%       jz                               e0jc                   e/d3 e%       j|                               e0jc                   e/d4 e%       j~                               e0jc                   e/d5 e&       j                               e0jc                   e/d6 e&       j                               e0jc                   e/d7 e&       j                               e0jc                   e/d8 e'       j                               e0jc                   e/d9 e'       j                               e0jc                   e/d: e(       j                               e0jc                   e/d; e(       j                               e0jc                   e/d< e(       j                               e0jc                   e/d= e)       j                               e0jc                   e/d> e)       j                               e0jc                   e/d? e)       j                               e0jc                   e/d@ e*       j                               e0jc                   e/dA e*       j                               e0jc                   e/dB e*       j                               e0jc                   e/dC e*       j                               e0jc                   e/dD e+       j                               e0jc                   e/dE e+       j                               e0jc                   e/dF e,       j                               e0jc                   e/dG e,       j                                eSe0      ZT eUe0      ZV eWdHeT dIeV dJ       eTeVk  r ej                  dK       y eWdL       yy)Pu  
Tests for Story 5.07 (Track B): ShadowRouter — Side-Effect Gating

Black Box tests (BB): verify the public contract from the caller's perspective —
    SHADOW mode logs without executing, LIVE mode executes the handler,
    per-type overrides override the default, shadow log is written, invalid
    effect types raise ValueError.

White Box tests (WB): verify internals — ShadowResult.executed flag, sha256
    payload hash, default mode when env var absent, malformed SHADOW_OVERRIDES
    JSON, LIVE handler exception does not crash the router.

ALL tests use mocks — NO real file I/O to shadow_log.jsonl (except BB4 which
uses tmp_path via monkeypatching) and NO real external calls.

Story: 5.07
File under test: core/storage/shadow_router.py
    )annotationsNz/mnt/e/genesis-system)	MagicMockpatch	mock_opencall)ShadowRouterShadowResultVALID_EFFECT_TYPESSHADOW_LOG_PATH)r   )r	   c                    | |d}t        j                  t        j                  |d      5  t	               cddd       S # 1 sw Y   yxY w)zIReturn a ShadowRouter with SHADOW_MODE and optional SHADOW_OVERRIDES set.SHADOW_MODESHADOW_OVERRIDESFclearN)r   dictosenvironr   )mode	overridesenvs      6/mnt/e/genesis-system/tests/track_b/test_story_5_07.py_router_in_moder   0   s:    I
>C	BJJ5	1 ~  s   
A  A	c                    t        j                  t        j                  | d      j	                  d            j                         S )NT)	sort_keyszutf-8)hashlibsha256jsondumpsencode	hexdigestpayloads    r   _sha256r$   7   s3    >>

7d+227;ik    c                  "    e Zd ZdZd Zd Zd Zy) TestBB1ShadowModeLogsNoExecutionu@   BB1: SHADOW_MODE="SHADOW" → all effects logged, none executed.c           
        t        d      }t               }t        D ]  }|j                  ||        t	        dt                     5  t	        d      5  t        D ]  }|j                  |ddi      }|j                  }d}||u }|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|d      dz   d|iz  }	t        t        j                   |	            d x}x}} 	 d d d        d d d        y # 1 sw Y   xY w# 1 sw Y   y xY w)NSHADOWbuiltins.openpathlib.Path.mkdirdataxFisz0%(py2)s
{%(py2)s = %(py0)s.executed
} is %(py5)sresultpy0py2py5zeffect_type=z': executed must be False in SHADOW mode
>assert %(py7)spy7)r   r   r
   register_handlerr   r   route_side_effectexecuted
@pytest_ar_call_reprcompare@py_builtinslocals_should_repr_global_name	_saferepr_format_assertmsgAssertionError_format_explanation)
selfrouterhandleretyper1   @py_assert1@py_assert4@py_assert3@py_format6@py_format8s
             r   1test_all_valid_effect_types_return_executed_falsezRTestBB1ShadowModeLogsNoExecution.test_all_valid_effect_types_return_executed_falseE   sj    *+' 	4E##E73	4?IK0 	+, / E#55efc]KF!?? e ?e3 ?e  v   "  I "  I +  I /4   'ui/VW    	 	 	 	s%   E2DE&E2&E/	+E22E;c                ,   t        d      }t               }|j                  d|       t        dt	                     5  t        d      5  |j                  dddi       d d d        d d d        |j                          y # 1 sw Y   "xY w# 1 sw Y   &xY w)Nr)   emailr*   r+   toa@b.com)r   r   r8   r   r   r9   assert_not_called)rD   rE   rF   s      r   %test_shadow_mode_handler_never_calledzFTestBB1ShadowModeLogsNoExecution.test_shadow_mode_handler_never_calledR   s     *+1?IK0 	E+, E((42CDE	E 	!!#E E	E 	Es#   B
A>B
>B	B

Bc                d   t        d      }t        dt                     5  t        d      5  |j                  dddi      }d d d        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  }dd|iz  }t        t        j                  |            d x}x}}y # 1 sw Y   xY w# 1 sw Y   xY w)Nr)   r*   r+   smsnumber+61==z,%(py2)s
{%(py2)s = %(py0)s.mode
} == %(py5)sr1   r2   assert %(py7)sr7   )r   r   r   r9   r   r;   r<   r=   r>   r?   r@   rB   rC   rD   rE   r1   rH   rI   rJ   rK   rL   s           r   'test_shadow_result_mode_field_is_SHADOWzHTestBB1ShadowModeLogsNoExecution.test_shadow_result_mode_field_is_SHADOW[   s     *?IK0 	L+, L11%(E9JKL	L {{&h&{h&&&&{h&&&&&&v&&&v&&&{&&&h&&&&&&&L L	L 	Ls"   D&DD&D#	D&&D/N)__name__
__module____qualname____doc__rM   rS   r]    r%   r   r'   r'   B   s    J$'r%   r'   c                  "    e Zd ZdZd Zd Zd Zy)TestBB2LiveModeHandlerCalledu:   BB2: SHADOW_MODE="LIVE" → handler called, executed=True.c                   t        d      }t               }|j                  d|       |j                  dddi      }|j                  }d}||u }|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 )NLIVErO   rP   rQ   Tr.   r0   r1   r2   r[   r7   )r   r   r8   r9   r:   r;   r<   r=   r>   r?   r@   rB   rC   )	rD   rE   rF   r1   rH   rI   rJ   rK   rL   s	            r   test_executed_true_in_live_modez<TestBB2LiveModeHandlerCalled.test_executed_true_in_live_modef   s     (+1))'D)3DE&$&$&&&&$&&&&&&v&&&v&&&&&&$&&&&&&&r%   c                    t        d      }t               }ddd}|j                  d|       |j                  d|       |j	                  |       y )Nrf   zx@example.comHello)rP   subjectrO   )r   r   r8   r9   assert_called_once_with)rD   rE   rF   r#   s       r    test_handler_called_with_payloadz=TestBB2LiveModeHandlerCalled.test_handler_called_with_payloadm   sK     (+(W=1  '2''0r%   c                `   t        d      }|j                  dddi      }|j                  }d}||u }|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}}|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)zFIf no handler is registered in LIVE mode, still returns executed=True.rf   	crm_writerecord123Tr.   r0   r1   r2   r[   r7   NrX   rZ   )r   r9   r:   r;   r<   r=   r>   r?   r@   rB   rC   r   r\   s           r   5test_live_mode_no_handler_still_returns_executed_truezRTestBB2LiveModeHandlerCalled.test_live_mode_no_handler_still_returns_executed_trueu   s     ())+%7HI&$&$&&&&$&&&&&&v&&&v&&&&&&$&&&&&&&{{$f${f$$$${f$$$$$$v$$$v$$${$$$f$$$$$$$r%   N)r^   r_   r`   ra   rg   rl   rq   rb   r%   r   rd   rd   c   s    D'1%r%   rd   c                      e Zd ZdZd Zd Zy)#TestBB3ShadowOverridesPerEffectTypeuF   BB3: SHADOW_OVERRIDES={"email":"SHADOW"} → email shadowed, sms live.c                   t        j                  ddi      }t        d|      }t               }t               }|j	                  d|       |j	                  d|       t        dt                     5  t        d      5  |j                  ddd	i      }|j                  dd
di      }d d d        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  }
dd|
iz  }t        t        j                   |            d x}x}	}|j"                  }d}||u }	|	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}	}|j%                          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}	}|j"                  }d}||u }	|	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}	}|j'                          y # 1 sw Y   QxY w# 1 sw Y   VxY w)NrO   r)   rf   r   rU   r*   r+   rP   rQ   rV   rW   rX   rZ   email_resultr2   r[   r7   Fr.   r0   
sms_resultT)r   r   r   r   r8   r   r   r9   r   r;   r<   r=   r>   r?   r@   rB   rC   r:   rR   assert_called_once)rD   r   rE   email_handlersms_handlerrv   rw   rH   rI   rJ   rK   rL   s               r   test_email_shadowed_sms_livez@TestBB3ShadowOverridesPerEffectType.test_email_shadowed_sms_live   s   JJ23	 9=!k7{3?IK0 	P+, P%77$	ARS#55eh=NO
P	P   ,H, H,,,, H,,,,,,|,,,|,,, ,,,H,,,,,,,$$--$----$------|---|---$----------'') (&(&((((&((((((z(((z((((((&(((((((""*d*"d****"d******z***z***"***d*******&&(P P	P 	Ps$   1P=)O5&P5O?	:PPc           
        t        j                  t        D ci c]  }|d c}      }t        d|      }t	        dt                     5  t	        d      5  t        D ]  }|j                  |i       }|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
      dz   d|	iz  }
t        t        j                   |
            d x}x}} 	 d d d        d d d        y c c}w # 1 sw Y   xY w# 1 sw Y   y xY w)Nr)   rf   ru   r*   r+   rX   rZ   r1   r2   z should be SHADOW via overrider6   r7   )r   r   r
   r   r   r   r9   r   r;   r<   r=   r>   r?   r@   rA   rB   rC   )rD   etr   rE   rG   r1   rH   rI   rJ   rK   rL   s              r   %test_all_types_shadowed_via_overrideszITestBB3ShadowOverridesPerEffectType.test_all_types_shadowed_via_overrides   sd   JJ7IJHJK	 9=?IK0 	+, / E#55eR@F!;; ( ;(2 ;(  v   "  I "  I '  I +3   !)#AB    	 	  K 	 	s*   
E'
E8C?E,E8,E5	1E88FN)r^   r_   r`   ra   r{   r~   rb   r%   r   rs   rs   ~   s    P).	r%   rs   c                  "    e Zd ZdZd Zd Zd Zy)TestBB4ShadowLogFileUpdatedz4BB4: Shadow log file updated after shadow execution.c                r	   |dz  }t        d      }t        d|      5  |j                  dddi       ddd       |j                  } |       }|st	        j
                  d      d	z   d
t        j                         v st	        j                  |      rt	        j                  |      nd
t	        j                  |      t	        j                  |      dz  }t        t	        j                  |            dx}}|j                         j                         j                         }t        |      }d}	||	k(  }
|
st	        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  }t	        j
                  d      dz   d|iz  }t        t	        j                  |            dx}x}
}	t#        j$                  |d         }|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}||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}}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# 1 sw Y   |xY w)$z;Use tmp_path to write a real log and verify the JSONL line.shadow_log.jsonlr)   *core.storage.shadow_router.SHADOW_LOG_PATHrO   rP   ztest@example.comNzShadow log file must be createdzC
>assert %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.exists
}()
}log_filer3   r4   py4   rX   z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py6)slenlinesr3   py1py3py6zExpected exactly 1 JSONL entryz
>assert %(py8)spy8r   effect_typez%(py1)s == %(py4)sr   r   assert %(py6)sr   r   payload_hashinz%(py1)s in %(py3)sentryr   r   assert %(py5)sr5   	timestamp)r   r   r9   existsr;   rA   r=   r>   r?   r@   rB   rC   	read_textstrip
splitlinesr   r<   r   loads)rD   tmp_pathr   rE   rH   rJ   @py_format5r   @py_assert2@py_assert5rI   @py_format7@py_format9r   @py_assert0@py_format4rK   s                    r   test_shadow_log_writtenz3TestBB4ShadowLogFileUpdated.test_shadow_log_written   s   00 * ?J 	J$$Wt5G.HI	J C C CC"CCCCCCCxCCCxCCCCCC CCCCCC""$**,7795z@Q@zQ@@@zQ@@@@@@s@@@s@@@@@@5@@@5@@@z@@@Q@@@ @@@@@@@@

58$]#.w.#w....#w...#...w.......V}((}((((}(((}((((((((((&~&&&&~&&&~&&&&&&&&&&&&&&&&#{e####{e###{######e###e#######	J 	Js   R,,R6c                   |dz  }t        d      }t        d|      5  |j                  dddi       |j                  ddd	i       d d d        |j                         j	                         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 
ch c]  }
t        j                   |
      d    }}
ddh}||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 # 1 sw Y   xY wc c}
w )Nr   r)   r   rO   rP   rQ   rU   rV   rW      rX   r   r   r   r   zassert %(py8)sr   r   )z%(py0)s == %(py3)stypesr3   r   r   r5   )r   r   r9   r   r   r   r   r;   r<   r=   r>   r?   r@   rB   rC   r   r   )rD   r   r   rE   r   r   r   rI   r   r   lr   rH   r   rK   s                  r   (test_shadow_log_appends_multiple_entrieszDTestBB4ShadowLogFileUpdated.test_shadow_log_appends_multiple_entries   s   00 *?J 	?$$WtY.?@$$UXu,=>	? ""$**,7795zQzQzQss55zQ7<=!A}-== %((u(((((u(((((((u(((u(((((((((((	? 	?
 >s   )II%I"c                   |dz  }t        d      }t        d|      5  |j                  dddi       d d d        |j                  } |       }| }|st	        j
                  d      dz   d	t        j                         v st	        j                  |      rt	        j                  |      nd	t	        j                  |      t	        j                  |      d
z  }t        t	        j                  |            d x}x}}y # 1 sw Y   xY w)Nr   rf   r   rO   rP   rQ   z&LIVE mode must NOT write to shadow logzG
>assert not %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.exists
}()
}r   r   )r   r   r9   r   r;   rA   r=   r>   r?   r@   rB   rC   )rD   r   r   rE   rH   rJ   r   rK   s           r   (test_live_mode_does_not_write_shadow_logzDTestBB4ShadowLogFileUpdated.test_live_mode_does_not_write_shadow_log   s    00 (?J 	A$$WtY.?@	A??N?$N$$N$NN&NNNNNNN8NNN8NNN?NNN$NNNNNN	A 	As   C<<DN)r^   r_   r`   ra   r   r   r   rb   r%   r   r   r      s    >$&	)Or%   r   c                  "    e Zd ZdZd Zd Zd Zy)(TestBB5InvalidEffectTypeRaisesValueErrorz+BB5: Invalid effect_type raises ValueError.c                    t        d      }t        j                  t        d      5  |j	                  dddi       d d d        y # 1 sw Y   y xY w)Nrf   zUnknown effect_type)matchfax_machinerV   555r   pytestraises
ValueErrorr9   rD   rE   s     r   test_unknown_type_raiseszATestBB5InvalidEffectTypeRaisesValueError.test_unknown_type_raises   sI     (]]:-BC 	G$$]Xu4EF	G 	G 	Gs   AAc                    t        d      }t        j                  t              5  |j	                  di        d d d        y # 1 sw Y   y xY w)Nrf    r   r   s     r   test_empty_string_raiseszATestBB5InvalidEffectTypeRaisesValueError.test_empty_string_raises   s=     (]]:& 	-$$R,	- 	- 	-s   AA
c                   t        d      }t        D ]  }|j                  |i       }d }||u}|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 )Nrf   is notz%(py0)s is not %(py3)sr1   r   r   r5   )r   r
   r9   r;   r<   r=   r>   r?   r@   rB   rC   )rD   rE   rG   r1   r   rH   r   rK   s           r   test_valid_types_do_not_raisezFTestBB5InvalidEffectTypeRaisesValueError.test_valid_types_do_not_raise   s     (' 	&E--eR8F!%%6%%%%6%%%%%%6%%%6%%%%%%%%%%	&r%   N)r^   r_   r`   ra   r   r   r   rb   r%   r   r   r      s    5G
-
&r%   r   c                  "    e Zd ZdZd Zd Zd Zy)TestWB1ShadowResultExecutedFlagzCWB1: ShadowResult.executed=False in SHADOW mode, True in LIVE mode.c                   t        d      }t        dt                     5  t        d      5  |j                  dddi      }d d d        d d d        t	        t
              }|sd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	d
t        j                         v st        j                  t
              rt        j                  t
              nd
t        j                  |      dz  }t        t        j                  |            d }|j                  }d}||u }|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 # 1 sw Y   xY w# 1 sw Y   xY w)Nr)   r*   r+   external_apiurlhttp://x5assert %(py4)s
{%(py4)s = %(py0)s(%(py1)s, %(py2)s)
}
isinstancer1   r	   r3   r   r4   r   Fr.   r0   r2   r[   r7   )r   r   r   r9   r   r	   r=   r>   r;   r?   r@   rB   rC   r:   r<   	rD   rE   r1   rJ   r   rH   rI   rK   rL   s	            r   !test_shadow_result_executed_falsezATestWB1ShadowResultExecutedFlag.test_shadow_result_executed_false   sa    *?IK0 	W+, W11.5*BUVW	W &,////////z///z//////&///&//////,///,//////////'%'%''''%''''''v'''v''''''%'''''''W W	W 	Ws"   IH9I9I	>IIc                   t        d      }|j                  dddi      }t        |t              }|sd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dt	        j
                         v st        j                  t              rt        j                  t              ndt        j                  |      d	z  }t        t        j                  |            d }|j                  }d
}||u }|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 )Nrf   r   r   r   r   r   r1   r	   r   Tr.   r0   r2   r[   r7   )r   r9   r   r	   r=   r>   r;   r?   r@   rB   rC   r:   r<   r   s	            r   test_live_result_executed_truez>TestWB1ShadowResultExecutedFlag.test_live_result_executed_true   s    ()).5*:MN&,////////z///z//////&///&//////,///,//////////&$&$&&&&$&&&&&&v&&&v&&&&&&$&&&&&&&r%   c                	   t        d      }t        dt                     5  t        d      5  |j                  dddi      }d d d        d d d        j                  }t        |t              }|sd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                  |      d
t        j                         v st        j                  t              rt        j                  t              nd
t        j                  |      d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  }dd|iz  }	t        t        j                  |	            d x}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  }dd|iz  }	t        t        j                  |	            d x}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  }dd|iz  }	t        t        j                  |	            d x}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  }dd|iz  }	t        t        j                  |	            d x}x}}y # 1 sw Y   qxY w# 1 sw Y   vxY w)Nr)   r*   r+   rU   rV   rW   zTassert %(py6)s
{%(py6)s = %(py0)s(%(py3)s
{%(py3)s = %(py1)s.log_entry
}, %(py4)s)
}r   r1   r   )r3   r   r   r   r   r   r   z1%(py1)s in %(py5)s
{%(py5)s = %(py3)s.log_entry
}r   r   r5   r[   r7   r   r   r   )r   r   r   r9   	log_entryr   r   r=   r>   r;   r?   r@   rB   rC   r<   )
rD   rE   r1   r   r   r   r   rI   rK   rL   s
             r   %test_shadow_result_has_log_entry_dictzETestWB1ShadowResultExecutedFlag.test_shadow_result_has_log_entry_dict   s    *?IK0 	L+, L11%(E9JKL	L !**1z*D11111111z111z111111&111&111*111111D111D11111111110 0 00} 00000} 0000}000000000000 000000001!1!11~!11111~!1111~111111111111!11111111.f...{.....{....{......f...f...........))))v)))))v))))v))))))))))))))))))))L L	L 	Ls"   R=R0R=0R:	5R==SN)r^   r_   r`   ra   r   r   r   rb   r%   r   r   r      s    M('	*r%   r   c                  "    e Zd ZdZd Zd Zd Zy)TestWB2PayloadHashIsSha256z3WB2: payload_hash is sha256 of sorted JSON payload.c                   t        d      }dddd}t        |      }|j                  d|      }|j                  d   }||k(  }|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 )Nrf   r   r   hello)bacrO   r   rX   )z%(py1)s == %(py3)sexpected_hashr   r   r5   )r   r$   r9   r   r;   r<   r@   r=   r>   r?   rB   rC   )	rD   rE   r#   r   r1   r   r   r   rK   s	            r   'test_hash_matches_sha256_of_sorted_jsonzBTestWB2PayloadHashIsSha256.test_hash_matches_sha256_of_sorted_json  s     (0())'7;/@/=@@@@/=@@@/@@@@@@=@@@=@@@@@@@r%   c                   t        d      }|j                  dddi      }|j                  dddi      }|j                  d   }|j                  d   }||k7  }|slt        j                  d|fd||f      t        j
                  |      t        j
                  |      d	z  }d
d|iz  }t        t        j                  |            d x}x}}y )Nrf   rO   rP   rQ   zz@y.comr   )!=)z%(py1)s != %(py4)sr   r   r   r   r9   r   r;   r<   r@   rB   rC   )	rD   rE   result1result2r   rJ   r   r   r   s	            r   0test_different_payloads_produce_different_hasheszKTestWB2PayloadHashIsSha256.test_different_payloads_produce_different_hashes  s     (**7T94EF**7T94EF  0UG4E4En4UU04UUUUU04UUUU0UUU4UUUUUUUUr%   c                   t        d      }ddd}ddd}|j                  d|      }|j                  d|      }|j                  d   }|j                  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/Sort-key ensures key order doesn't affect hash.rf   r   r   )r   r   )r   r   rO   r   rX   r   r   r   r   Nr   )rD   rE   
payload_ab
payload_bar   r   r   rJ   r   r   r   s              r   /test_same_payload_different_key_order_same_hashzJTestWB2PayloadHashIsSha256.test_same_payload_different_key_order_same_hash  s     (1%
1%
**7J?**7J?  0UG4E4En4UU04UUUUU04UUUU0UUU4UUUUUUUUr%   N)r^   r_   r`   ra   r   r   r   rb   r%   r   r   r     s    =AVVr%   r   c                      e Zd ZdZd Zd Zy)TestWB3DefaultModeIsLivez:WB3: mode defaults to "LIVE" when SHADOW_MODE env not set.c                   t         j                  j                         D ci c]  \  }}|dvr|| }}}t        j                  t         j                  |d      5  t               }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  }d	d
|iz  }	t        t        j                  |	            d x}x}}y c c}}w # 1 sw Y   xY w)Nr   Tr   rf   rX   )z4%(py2)s
{%(py2)s = %(py0)s.default_mode
} == %(py5)srE   r2   r[   r7   )r   r   itemsr   r   r   default_moder;   r<   r=   r>   r?   r@   rB   rC   )
rD   kvenv_without_shadowrE   rH   rI   rJ   rK   rL   s
             r   !test_default_mode_when_env_absentz:TestWB3DefaultModeIsLive.test_default_mode_when_env_absent!  s    /1zz/?/?/A Otq!!"*M!M  d O OZZ

$6dC 	$!^F	$"",f,"f,,,,"f,,,,,,v,,,v,,,",,,f,,,,,,,	O	$ 	$s   D8D>>Ec           	        t        j                  t        j                  ddid      5  t	               }d d d        t
        D ]+  }j                  } ||      }d}||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t        j                  |      t        j                  |      d	z  }d
d|iz  }t        t        j                  |            d x}x}x}}. y # 1 sw Y   @xY w)Nr   rf   Fr   rX   )zN%(py5)s
{%(py5)s = %(py2)s
{%(py2)s = %(py0)s.get_mode
}(%(py3)s)
} == %(py8)srE   rG   )r3   r4   r   r5   r   zassert %(py10)spy10)r   r   r   r   r   r
   get_moder;   r<   r=   r>   r?   r@   rB   rC   )	rD   rE   rG   rH   rI   @py_assert7@py_assert6r   @py_format11s	            r   9test_get_mode_returns_live_for_any_type_when_default_livezRTestWB3DefaultModeIsLive.test_get_mode_returns_live_for_any_type_when_default_live(  s    ZZ

]F$;5I 	$!^F	$' 	4E??3?5)3V3)V3333)V33333363336333?33333353335333)333V3333333	4	$ 	$s   E22E<N)r^   r_   r`   ra   r   r   rb   r%   r   r   r     s    D-4r%   r   c                  "    e Zd ZdZd Zd Zd Zy)TestWB4MalformedShadowOverridesu@   WB4: SHADOW_OVERRIDES malformed JSON → defaults to empty dict.c                $   t        j                  t        j                  dddd      5  t	               }d d d        j
                  }i }||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 # 1 sw Y   xY w)Nrf   NOT_JSONr   Fr   rX   z2%(py2)s
{%(py2)s = %(py0)s._overrides
} == %(py5)srE   r2   r[   r7   r   r   r   r   r   
_overridesr;   r<   r=   r>   r?   r@   rB   rC   rD   rE   rH   rI   rJ   rK   rL   s          r   'test_malformed_json_falls_back_to_emptyzGTestWB4MalformedShadowOverrides.test_malformed_json_falls_back_to_empty2  s    ZZ

(.JO#% 	$ "^F	$   &B& B&&&& B&&&&&&v&&&v&&& &&&B&&&&&&&		$ 	$   DDc                $   t        j                  t        j                  dddd      5  t	               }d d d        j
                  }i }||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 # 1 sw Y   xY w)Nrf   z	["email"]r   Fr   rX   r   rE   r2   r[   r7   r   r  s          r   &test_non_dict_json_falls_back_to_emptyzFTestWB4MalformedShadowOverrides.test_non_dict_json_falls_back_to_empty9  s    ZZ

(.KP#% 	$ "^F	$   &B& B&&&& B&&&&&&v&&&v&&& &&&B&&&&&&&		$ 	$r  c                $   t        j                  t        j                  dddd      5  t	               }d d d        j
                  }i }||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 # 1 sw Y   xY w)Nr)   {}r   Fr   rX   r   rE   r2   r[   r7   r   r  s          r   test_empty_json_object_is_validz?TestWB4MalformedShadowOverrides.test_empty_json_object_is_valid@  s    ZZ

(0dK#% 	$ "^F	$   &B& B&&&& B&&&&&&v&&&v&&& &&&B&&&&&&&		$ 	$r  N)r^   r_   r`   ra   r  r  r	  rb   r%   r   r   r   /  s    J'''r%   r   c                  "    e Zd ZdZd Zd Zd Zy)"TestWB5LiveHandlerExceptionNocrashuD   WB5: LIVE handler exception → logged, ShadowResult still returned.c                   t        d      }d }|j                  d|       |j                  dddi      }d }||u}|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}}|j                  }d}||u }	|	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}	}|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 )Nrf   c                    t        d      )NzExternal service downRuntimeErrorr"   s    r   boomzZTestWB5LiveHandlerExceptionNocrash.test_handler_exception_does_not_propagate.<locals>.boomN  s    677r%   telnyx_callphonerW   r   r   r1   r   r   r5   Tr.   r0   r2   r[   r7   rX   rZ   )r   r8   r9   r;   r<   r=   r>   r?   r@   rB   rC   r:   r   )rD   rE   r  r1   r   rH   r   rK   rI   rJ   rL   s              r   )test_handler_exception_does_not_propagatezLTestWB5LiveHandlerExceptionNocrash.test_handler_exception_does_not_propagateK  sm    (	8 	t4))-'59IJ!!vT!!!!vT!!!!!!v!!!v!!!T!!!!!!!&$&$&&&&$&&&&&&v&&&v&&&&&&$&&&&&&&{{$f${f$$$${f$$$$$$v$$$v$$${$$$f$$$$$$$r%   c                   t        d      }d }|j                  d|       |j                  dddi      }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  }dd|iz  }t        t	        j                  |            d x}x}}d}|j                  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 )Nrf   c                    t        d      )NConfig missing)r   r"   s    r   r  z]TestWB5LiveHandlerExceptionNocrash.test_handler_exception_recorded_in_log_entry.<locals>.boom[  s    -..r%   ghl_webhookeventleaderrorr   r   r1   r   r[   r7   r  )z%(py1)s in %(py4)sr   r   r   )r   r8   r9   r   r;   r<   r@   r=   r>   r?   rB   rC   )rD   rE   r  r1   r   rI   r   rK   rL   rJ   r   r   s               r   ,test_handler_exception_recorded_in_log_entryzOTestWB5LiveHandlerExceptionNocrash.test_handler_exception_recorded_in_log_entryX  s    (	/ 	t4))-'69JK*&***w*****w****w******&***&***********<6#3#3G#<<#<<<<<#<<<<<<<#<<<<<<<<r%   c                    t        d      }t               }d }|j                  d|       |j                  d|       |j                  di        |j                  dddi       |j	                          y)zFA failing handler for one type must not affect another type's handler.rf   c                    t        d      )Nr  r  r"   s    r   r  zbTestWB5LiveHandlerExceptionNocrash.test_second_handler_still_works_after_first_fails.<locals>.boomh  s    v&&r%   rO   rU   rV   rW   N)r   r   r8   r9   rx   )rD   rE   good_handlerr  s       r   1test_second_handler_still_works_after_first_failszTTestWB5LiveHandlerExceptionNocrash.test_second_handler_still_works_after_first_failsc  sk     ( {	' 	.|4  "-  5(9:'')r%   N)r^   r_   r`   ra   r  r  r  rb   r%   r   r  r  H  s    N%	=*r%   r  c                  (    e Zd ZdZd Zd Zd Zd Zy)TestPackageExportszLShadowRouter and ShadowResult must be importable directly from core.storage.c                   t         t        u }|st        j                  d|fdt         t        f      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dz  }dd|iz  }t        t        j                  |            d }y )Nr.   z%(py0)s is %(py2)sShadowRouterFromPackager   r3   r4   assert %(py4)sr   )
r$  r   r;   r<   r=   r>   r?   r@   rB   rC   rD   rH   @py_format3r   s       r   *test_shadow_router_importable_from_packagez=TestPackageExports.test_shadow_router_importable_from_package|  m    &,6666&,666666&666&666666,666,6666666r%   c                   t         t        u }|st        j                  d|fdt         t        f      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dz  }dd|iz  }t        t        j                  |            d }y )Nr.   r#  ShadowResultFromPackager	   r%  r&  r   )
r,  r	   r;   r<   r=   r>   r?   r@   rB   rC   r'  s       r   *test_shadow_result_importable_from_packagez=TestPackageExports.test_shadow_result_importable_from_package  r*  r%   c                Z   ddl m} 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 )
Nr   __all__r   r   r   r0  r   r   r5   
core.storager0  r;   r<   r@   r=   r>   r?   rB   rC   rD   r0  r   r   r   rK   s         r   test_all_includes_shadow_routerz2TestPackageExports.test_all_includes_shadow_router  ^    ((~((((~(((~((((((((((((((((r%   c                Z   ddl m} 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 )
Nr   r/  r	   r   r   r0  r   r   r5   r1  r3  s         r   test_all_includes_shadow_resultz2TestPackageExports.test_all_includes_shadow_result  r5  r%   N)r^   r_   r`   ra   r)  r-  r4  r7  rb   r%   r   r!  r!  y  s    V77))r%   r!  c                      e Zd ZdZd Zd Zy)TestNoSQLitez)shadow_router.py must not import sqlite3.c                   t        j                  d      j                         }d}||v}|st        j                  d|fd||f      t        j
                  |      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 x}}y )
Nz3/mnt/e/genesis-system/core/storage/shadow_router.pyzimport sqlite3)not in)z%(py1)s not in %(py3)ssourcer   uG   shadow_router.py must NOT import sqlite3 — Genesis Rule 7 (no SQLite)z
>assert %(py5)sr5   )pathlibPathr   r;   r<   r@   r=   r>   r?   rA   rB   rC   )rD   r<  r   r   r   rK   s         r   test_no_sqlite3_in_sourcez&TestNoSQLite.test_no_sqlite3_in_source  s    A

)+ 	   	
v- 	
 	
v 	
 	
 		   	
 	
	6	
 	
  (. 	
 	
 		 (. 	
 	
  V	
 	
 	
 	
 	
r%   c                    dd l mc m} d}t        ||      }| }|st	        j
                  d      dz   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  }t        t	        j                  |            d x}x}}y )Nr   sqlite3z9sqlite3 must not appear in shadow_router module namespacez;
>assert not %(py5)s
{%(py5)s = %(py0)s(%(py1)s, %(py3)s)
}hasattrmod)r3   r   r   r5   )core.storage.shadow_routerstorageshadow_routerrB  r;   rA   r=   r>   r?   r@   rB   rC   )rD   rC  r   rI   r   r   s         r   $test_sqlite3_not_in_module_namespacez1TestNoSQLite.test_sqlite3_not_in_module_namespace  s    00 ) 	
73	* 	
** 	
* 	
  H	
 	
	6	
 	
   	
 	
 		  	
 	
	6	
 	
   	
 	
 		  	
 	
 		 !* 	
 	
 		 + 	
 	
 	
 	
 	
 	
r%   N)r^   r_   r`   ra   r?  rG  rb   r%   r   r9  r9    s    3

r%   r9  c                  $    e Zd ZdZh dZd Zd Zy)TestValidEffectTypesz)Verify the canonical set of effect types.>   rU   rO   rn   r  r  r   c                   | 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}}y )NrX   )z6%(py2)s
{%(py2)s = %(py0)s.EXPECTED_TYPES
} == %(py4)srD   r
   r   r   r   )
EXPECTED_TYPESr
   r;   r<   r=   r>   r?   r@   rB   rC   )rD   rH   rJ   r   r   s        r   test_all_expected_types_presentz4TestValidEffectTypes.test_all_expected_types_present  s    ""8"&88888"&8888888t888t888"888888&8888&88888888r%   c                \   t        t        t              }|sd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dt        j                         v st        j                  t              rt        j                  t              ndt        j                  |      dz  }t        t        j                  |            d }y )Nr   r   r
   	frozensetr   )
r   r
   rN  r=   r>   r;   r?   r@   rB   rC   )rD   rJ   r   s      r   $test_valid_effect_types_is_frozensetz9TestValidEffectTypes.test_valid_effect_types_is_frozenset  s    ,i88888888z888z888888,888,888888i888i8888888888r%   N)r^   r_   r`   ra   rK  rL  rO  rb   r%   r   rI  rI    s    3N99r%   rI  __main__c                    	  ||i | t        d|         y# t        $ r/}t        d|  d|        t        j                          Y d }~yd }~ww xY w)Nz	  [PASS] Tz	  [FAIL] z: F)print	Exception	traceback	print_exc)namefnargskwargsexcs        r   _runr[    sZ    	IdV$% 	IdV2cU+,!	s    	A%AAz(BB1a: shadow mode returns executed=Falsez&BB1b: shadow mode handler never calledz"BB1c: shadow result mode is SHADOWzBB2a: live mode executed=Truez&BB2b: live handler called with payloadz)BB2c: live no handler still executed=TruezBB3a: email shadowed sms livez&BB3b: all types shadowed via overrideszBB5a: unknown type raiseszBB5b: empty string raiseszBB5c: valid types do not raisez"WB1a: shadow result executed=FalsezWB1b: live result executed=Truez&WB1c: shadow result has log_entry dictz%WB2a: hash matches sha256 sorted JSONu-   WB2b: different payloads → different hashesz#WB2c: key order doesn't affect hashz*WB3a: default mode is LIVE when env absentz)WB3b: get_mode returns LIVE for all typesu#   WB4a: malformed JSON → empty dictu"   WB4b: non-dict JSON → empty dictzWB4c: empty JSON object validz*WB5a: handler exception does not propagatez%WB5b: exception recorded in log_entryz,WB5c: second handler works after first failsz)PKG: ShadowRouter importable from packagez)PKG: ShadowResult importable from packagez"PKG: __all__ includes ShadowRouterz"PKG: __all__ includes ShadowResultzNOSQL: no sqlite3 in sourcez&NOSQL: sqlite3 not in module namespacez&VET: all expected effect types presentz$VET: VALID_EFFECT_TYPES is frozenset
/z tests passedr   z6ALL TESTS PASSED -- Story 5.07 (Track B): ShadowRouter)r  )r   strr   r^  returnr   )r#   r   r_  r^  )Yra   
__future__r   builtinsr=   _pytest.assertion.rewrite	assertionrewriter;   r   r   r   syspathinsertr=  unittest.mockr   r   r   r   r   rD  r   r	   r
   r   r2  r$  r,  r   r$   r'   rd   rs   r   r   r   r   r   r   r  r!  r9  rI  r^   rT  r[  resultsappendrM   rS   r]   rg   rl   rq   r{   r~   r   r   r   r   r   r   r   r   r   r   r   r  r  r	  r  r  r  r)  r-  r4  r7  r?  rG  rL  rO  sumpassedr   totalrR  exitrb   r%   r   <module>ro     s  & #     	 
 * +  ; ;   A @' 'B% %6# #L&O &OR& &4* *:V V44 4"' '2)* )*b) ).
 
.9 9& z G NN4B(*\\^ _NN4@(*PPR SNN4<(*RRT UNN47$&FFH INN4@$&GGI JNN4C$&\\^ _NN47+-JJL MNN4@+-SSU V NN4302KKM NNN4302KKM NNN4802PPR S NN4<')KKM NNN49')HHJ KNN4@')OOQ RNN4?"$LLN ONN4G"$UUW XNN4="$TTV WNN4D "DDF GNN4C "\\^ _NN4=')QQS TNN4<')PPR SNN47')IIK LNN4D*,VVX YNN4?*,YY[ \NN4F*,^^` a NN4CGGI JNN4CGGI JNN4<<<> ?NN4<<<> ?NN45002 3NN4@;;= >NN4@>>@ ANN4>CCE F \FLE	Bvhawm
,-~FG} r%   