
    ji+                     x   d Z ddlZddlmc mZ ddlZddlZej                  j                  dd       ddlZddlmZ ddlmZmZmZ ddlmZmZmZmZmZ ddlmZ ded	efd
Zej8                  d        Zej8                  d        Zej8                  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+e,dk(  rxddl-Z- ed      Z.da/da0d Z1d Z2d Z3d Z4 e1d e2        e1d!e3        e1d"e4        e5d#t`         d$t^         d%       t`        t^        k(  r	 e5d&       y ejl                  d'       yy)(uS  
Tests for Story 4.05 (Track B): RoutingTelemetry — Tier Distribution Analytics

Black Box tests (BB): verify the public contract from the outside.
White Box tests (WB): verify internal implementation choices (Redis INCR,
    no TTL, reset behaviour, zero-division guard).

Story: 4.05
File under test: core/routing/routing_telemetry.py
    Nz/mnt/e/genesis-system)Path)	MagicMockcallpatch)RoutingTelemetryT0_KEYT1_KEYT2_KEYEVENTS_LOG_PATHRoutingDecisiontierreturnc                 6    dddd}t        | ||    d|        S )z1Build a minimal RoutingDecision for a given tier.python_functionzgemini-flashzclaude-opus-4-6)T0T1T2ztest r   model	rationaler   )r   	model_maps     6/mnt/e/genesis-system/tests/track_b/test_story_4_05.py	_decisionr   "   s(    (FWXIIdOtf~VV    c                      t        d      S )uA   RoutingTelemetry with no Redis — uses local in-memory counters.Nredis_clientr    r   r   telemetry_no_redisr!   (   s     ..r   c                      t               } i fd}fd}fd}|| j                  _        || j                  _        || j                  _        | _        | S )z-A MagicMock that mimics a redis.Redis client.c                 >    j                  | d      dz   | <   |    S )Nr      )get)key_stores    r   _incrzmock_redis.<locals>._incr5   s&    jja(1,sc{r   c                 `    j                  |       }|t        |      j                         S d S N)r%   strencode)r&   valr'   s     r   _getzmock_redis.<locals>._get9   s*    jjo$'Os3x ==r   c                  8    | D ]  }j                  |d         y r*   )pop)keyskr'   s     r   _deletezmock_redis.<locals>._delete=   s      	 AJJq$	 r   )r   incrside_effectr%   deleter'   )rr(   r.   r3   r'   s       @r   
mock_redisr8   .   sR     	AF>  AFFAEE"AHHAHHr   c                     t        |       S )z(RoutingTelemetry backed by a mock Redis.r   r   )r8   s    r   telemetry_redisr:   H   s     44r   c           	      n   t        d      D ]  }| j                  t        d              t        d      D ]  }| j                  t        d              t        d      D ]  }| j                  t        d              | 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}||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   }t        j                  }d}	d}
 ||	|
      }||k(  }|st	        j
                  d|fd||f      t	        j                  |      dt        j                         v st	        j                  t              rt	        j                  t              ndt	        j                  |      t	        j                  |	      t	        j                  |
      t	        j                  |      dz  }dd|iz  }t        t	        j                  |            dx}x}x}x}	x}
}y)u-   BB1: 8 T0 + 2 T1 + 2 T2 → t0_t1_pct = 83.3%   r      r   r   t0==z%(py1)s == %(py4)spy1py4assert %(py6)spy6Nt1t2	t0_t1_pct33333T@皙?absz[%(py1)s == %(py11)s
{%(py11)s = %(py5)s
{%(py5)s = %(py3)s.approx
}(%(py7)s, abs=%(py9)s)
}pytestrC   py3py5py7py9py11assert %(py13)spy13)rangerecordr   get_distribution
@pytest_ar_call_reprcompare	_safereprAssertionError_format_explanationrO   approx@py_builtinslocals_should_repr_global_name)r!   _dist@py_assert0@py_assert3@py_assert2@py_format5@py_format7@py_assert4@py_assert6@py_assert8@py_assert10@py_format12@py_format14s                 r   .test_bb1_t0_t1_pct_83_3_for_8_2_2_distributionrq   S   s   1X 3!!)D/231X 3!!)D/231X 3!!)D/23 ..0D::?::::?::::?::<<d<<d << <<<<< <<<<<<<<<<<<<<<<<<<d<<<<<< <<<<<<<<<r   c           	         | j                  t        d             | j                  t        d             | j                  t        d             | j                  t        d             | j                  t        d             | j                  t        d             | 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}||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   }t        j                  }d}d}	 |||	      }
||
k(  }|st        j                  d|fd||
f      t        j
                  |      dt        j                         v st        j                  t              rt        j
                  t              ndt        j
                  |      t        j
                  |      t        j
                  |	      t        j
                  |
      dz  }dd|iz  }t        t        j                  |            dx}x}x}x}x}	}
y)zDBB2: get_distribution() returns the exact counts that were recorded.r   r   r   r>   r=   r?   rA   rB   rE   rF   NrG   r$   rH      rI   g      I@rK   rL   rN   rO   rP   rV   rW   )rY   r   rZ   r[   r\   r]   r^   r_   rO   r`   ra   rb   rc   )r!   re   rf   rg   rh   ri   rj   rk   rl   rm   rn   ro   rp   s                r   2test_bb2_get_distribution_reflects_recorded_countsrt   d   s   io.io.io.io.io.io...0D::?::::?::::?::<<d<<d << <<<<< <<<<<<<<<<<<<<<<<<<d<<<<<< <<<<<<<<<r   c                    |dz  }t        d|      5  | j                  t        d             | j                  t        d             | j                          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                  d
      }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}||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"   }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}||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}}y# 1 sw Y   }xY w)&zBBB3: log_tier_report() appends a valid JSON entry to events.jsonl.events.jsonl.core.routing.routing_telemetry.EVENTS_LOG_PATHr   r   Nzevents.jsonl must be createdzC
>assert %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.exists
}()
}log_file)py0py2rD   
r$   r?   z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py6)slenlinesry   rC   rQ   rF   z%Exactly one JSON line must be written
>assert %(py8)spy8r   eventtier_distribution_reportrA   rB   rE   rF   	timestamp)in)z%(py1)s in %(py3)sentry)rC   rQ   zassert %(py5)srR   r>   rG   rH   rI   )r   rY   r   log_tier_reportexistsr[   _format_assertmsgra   rb   rc   r]   r^   r_   	read_textstripsplitr}   r\   jsonloads)r!   tmp_pathrx   @py_assert1rg   ri   r~   rh   @py_assert5rk   rj   @py_format9r   rf   @py_format4@py_format6s                   r   4test_bb3_log_tier_report_writes_json_to_events_jsonlr   u   s   .(H	8(
 - 	!!)D/2!!)D/2**,- ??<?<<<<<<<<<<8<<<8<<<?<<<<<<<<< &&(..t4Eu:CC:?CCC:CCCCCC3CCC3CCCCCCuCCCuCCC:CCCCCCCCCCCCCCJJuQx E>777>77777>7777>77777777777;%;%;%%;!;!;!;!;!;!;!;!;!;!;!;!;%;%;%%#- -s   AWWc           	         t        d      D ]  }| j                  t        d              t        d      D ]  }| j                  t        d              | 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}||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}t        d|z  dz  d      }	|d   }t        j                  }
d} |
|	|      }||k(  }|st	        j
                  d|fd||f      t	        j                  |      dt        j                         v st	        j                  t              rt	        j                  t              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}
x}}y)zBBB4: Without Redis, local in-memory counters accumulate correctly.   r   rs   r   r>   r?   rA   rB   rE   rF   NrG   r   rH   r<   d   r$   rI   rK   rL   )z[%(py1)s == %(py10)s
{%(py10)s = %(py5)s
{%(py5)s = %(py3)s.approx
}(%(py6)s, abs=%(py8)s)
}rO   expected_pct)rC   rQ   rR   rF   r   py10zassert %(py12)spy12)rX   rY   r   rZ   r[   r\   r]   r^   r_   roundrO   r`   ra   rb   rc   )r!   rd   re   rf   rg   rh   ri   rj   totalr   rk   @py_assert7@py_assert9@py_format11@py_format13s                  r   /test_bb4_no_redis_local_counters_work_correctlyr      s    1X 3!!)D/231X 3!!)D/23 ..0D::?::::?::::?::EUS!,LDDDl DD DDDDD DDDDDDDDDDDDDDDDDDDDDDlDDDlDDDDDD DDDDDDDDDr   c                    |j                  t        d             |j                  t        d             |j                  t        d             | 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                  |      t	        j                  |      dz  }d	d
|iz  }t        t	        j                  |            dx}x}x}}| j                  j                          | j                  j                          y)z=WB1: record() calls redis.incr() (atomic), never redis.set().r   r   r   rs   r?   )zL%(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.incr
}.call_count
} == %(py7)sr8   )ry   rz   rD   rS   zassert %(py9)srT   N)rY   r   r4   
call_countr[   r\   ra   rb   rc   r]   r^   r_   setassert_not_calledsetex)r8   r:   r   rg   rl   r   @py_format8@py_format10s           r    test_wb1_redis_uses_incr_not_setr      s    9T?+9T?+9T?+ ??*?%%**%****%******:***:***?***%**********NN$$&&&(r   c                     t        d      D ]  }|j                  t        d              | j                  j	                          | j
                  j	                          | j                  j	                          y)uG   WB2: Counters have no TTL — verify setex and expire are never called.   r   N)rX   rY   r   r   r   expireexpireat)r8   r:   rd   s      r   test_wb2_no_ttl_on_counter_keysr      s_    1X 0y/0 &&('')))+r   c                    t        d      D ]  }| j                  t        d              t        d      D ]  }| j                  t        d              | j                          | 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}||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}||k(  }|slt        j                  d|fd||f      t        j                  |      t        j                  |      d	z  }d
d|iz  }t        t        j                  |            dx}x}}y)zIWB3: reset() zeroes local counters so get_distribution returns all zeros.
   r   r   r   r>   r   r?   rA   rB   rE   rF   NrG   rH   rI           )
rX   rY   r   resetrZ   r[   r\   r]   r^   r_   )r!   rd   re   rf   rg   rh   ri   rj   s           r   "test_wb3_reset_clears_all_countersr      s   2Y 3!!)D/231X 3!!)D/23 ..0D::?::::?::::?::###################r   c                    t        d      D ]  }|j                  t        d              |j                          | j                  j                  t        t        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}||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}||k(  }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd	|iz  }t        t        j                  |            d
x}x}}y
)z2WB3 (Redis): reset() deletes all three Redis keys.rs   r   r>   r   r?   rA   rB   rE   rF   NrG   rH   rI   r   )rX   rY   r   r   r6   assert_called_once_withr   r	   r
   rZ   r[   r\   r]   r^   r_   )	r8   r:   rd   re   rf   rg   rh   ri   rj   s	            r    test_wb3_reset_clears_redis_keysr      s   1X 0y/0  --fffE ++-D::?::::?::::?::###################r   c                 $   | 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}||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}||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@WB4: Before any records, t0_t1_pct = 0.0 (no ZeroDivisionError).r>   r   r?   rA   rB   rE   rF   NrG   rH   rI   r   )rZ   r[   r\   r]   r^   r_   )r!   re   rf   rg   rh   ri   rj   s          r   7test_wb4_empty_state_returns_zero_pct_no_division_errorr      sI   ..0D::?::::?::::?::###################r   c                 b   t        ddd      }| j                  |       | 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}||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}}y)zLrecord() with an unrecognised tier must not raise and must not alter counts.TXunknownzbad tierr   r>   r   r?   rA   rB   rE   rF   NrG   rH   )r   rY   rZ   r[   r\   r]   r^   r_   )r!   bad_decisionre   rf   rg   rh   ri   rj   s           r   test_unknown_tier_is_ignoredr      s   "ITLl+..0D::?::::?::::?::r   c                 r   |dz  }t        d|      5  | j                          | j                          ddd       |j                         j                         j	                  d      }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}}|D ]  }	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# 1 sw Y   xY w)zAlog_tier_report() appends; calling it twice gives two JSON lines.rv   rw   Nr{   r=   r?   r|   r}   r~   r   z&Second call must append, not overwriter   r   r   r   rA   rB   rE   rF   )r   r   r   r   r   r}   r[   r\   ra   rb   rc   r]   r   r^   r_   r   r   )r!   r   rx   r~   rh   r   rk   rj   r   liner   rf   rg   ri   s                 r   +test_log_tier_report_appends_not_overwritesr      sn   .(H	?	J -**,**,-  &&(..t4Eu:DD:?DDD:DDDDDD3DDD3DDDDDDuDDDuDDD:DDDDDDDDDDDDDD <

4 W~;!;;~!;;;;;~!;;;;~;;;!;;;;;;;;<- -s   !H,,H6c                     ddl m}  | t        u }|st        j                  d|fd| t        f      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
}y
)z6RoutingTelemetry must be importable from core.routing.r   r   )is)z%(py0)s is %(py2)sRTr   )ry   rz   zassert %(py4)srD   N)
core.routingr   r[   r\   ra   rb   rc   r]   r^   r_   )r   r   @py_format3ri   s       r   ,test_routing_telemetry_exported_from_packager     sq    3!!!!!2!!!!!!!2!!!2!!!!!!!!!!!!!!!!!!r   __main__r   c                     t         dz  a 	  |        t        d|         t        dz  ay # t        $ r/}t        d|  d|        t	        j
                          Y d }~y d }~ww xY w)Nr$   z	  [PASS] z	  [FAIL] z: )	tests_runprinttests_passed	Exception	traceback	print_exc)namefnexcs      r   run_testr     sb    Q		"DIdV$%AL 	"IdV2cU+,!!	"s   * 	A"%AA"c            	         t               } t        d      D ]  }| j                  t        d              t        d      D ]  }| j                  t        d              t        d      D ]  }| j                  t        d              | j	                         }|d   }t
        j                  }d}d} |||	      }||k(  }|st        j                  d
|fd||f      t        j                  |      dt        j                         v st        j                  t
              rt        j                  t
              ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }	dd|	iz  }
t        t        j                  |
            d x}x}x}x}x}}y )Nr<   r   r=   r   r   rI   rJ   rK   rL   r?   rN   rO   rP   rV   rW   )r   rX   rY   r   rZ   rO   r`   r[   r\   r]   ra   rb   rc   r^   r_   )trd   drf   rk   rl   rm   rn   rh   ro   rp   s              r   _bb1r   %  s    q4A188IdO44q4A188IdO44q4A188IdO44 ~==t==t!==~!=====~!====~===============t======!=========r   c                     t               } t        d      D ]  }| j                  t        d              | 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 )	Nr   r   r>   r?   rA   rB   rE   rF   )
r   rX   rY   r   rZ   r[   r\   r]   r^   r_   )r   rd   r   rf   rg   rh   ri   rj   s           r   _bb4r   .  s    q4A188IdO44 w!w!|w!w!r   c                  8   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 )NrI   r   r?   rA   rB   rE   rF   )r   rZ   r[   r\   r]   r^   r_   )r   r   rf   rg   rh   ri   rj   s          r   _wb4r   5  sf     ~$$~$$$$~$$$~$$$$$$$$$$r   u   BB1: 8+2+2 → 83.3%zBB4: local counters worku.   WB4: empty state → 0.0, no ZeroDivisionErrorr{   /z tests passedu)   ALL TESTS PASSED — Story 4.05 (Track B)r$   )7__doc__builtinsra   _pytest.assertion.rewrite	assertionrewriter[   r   syspathinsertrO   pathlibr   unittest.mockr   r   r   core.routing.routing_telemetryr   r   r	   r
   r   core.routing.tier_classifierr   r+   r   fixturer!   r8   r:   rq   rt   r   r   r   r   r   r   r   r   r   r   __name__r   t_localr   r   r   r   r   r   r   exitr    r   r   <module>r      sr  	   
 * +   0 0  9WC WO W / /
  2 5 5="=" 0E,),$ $$$<&" zD1GIL	">%
 #T*'.=tD	B|nAi[
67y 9:c r   