
    Hi.                       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mZ ddlmZ ddlmZmZ ddlZdZee
j(                  vre
j(                  j+                  de       ddlmZmZmZmZmZmZmZ 	 	 	 	 	 	 d	 	 	 	 	 	 	 	 	 	 	 	 	 dd	Z	 	 d	 	 	 dd
Zd Z d Z!ddZ"d Z#ddZ$ddZ%ddZ&ddZ'd Z(d Z)ddZ*d Z+y)u  
tests/track_b/test_story_7_05.py

Story B-7.05: MergeTelemetry — Merge Analytics

Black Box Tests (BB1–BB4):
    BB1  Record 10 merges (3 conflicts, 2 Opus) → correct rates
    BB2  get_stats() on fresh instance → zero everything
    BB3  avg_latency_ms calculated correctly from recorded latencies
    BB4  MergeRecord is serialisable via dataclasses.asdict()

White Box Tests (WB1–WB4):
    WB1  Redis INCR used for counters (verify mock call args)
    WB2  Events written to events.jsonl (verify file content)
    WB3  Division by zero handled (total=0 → 0.0 rates)
    WB4  Redis rpush used for latencies (not individual keys)

Package Test:
    PKG  core.merge exports MergeTelemetry and MergeRecord
    )annotationsN)asdict)Path)	MagicMockcallz/mnt/e/genesis-system)MergeTelemetryMergeRecordREDIS_PREFIX
_KEY_TOTAL_KEY_CONFLICTS_KEY_OPUS_CALLS_KEY_LATENCIESc                $    t        | |||||      S )z3Convenience factory for MergeRecord test instances.
session_iddelta_countconflict_count	used_opusmerge_latency_mssuccess)r	   r   s         6/mnt/e/genesis-system/tests/track_b/test_story_7_05.pymake_recordr   ;   s#     %)     c                ~     t               } fd}fd}||j                  _        ||j                  _        |S )z
    Build a MagicMock Redis client whose get() and lrange() return
    pre-set values, simulating a Redis instance with existing data.
    c                    t         t              j                         t        t              j                         t        t              j                         i}|j                  | d      S )N   0)r   strencoder   r   get)keymapping	conflicts
opus_callstotals     r   _getzmake_redis_mock.<locals>._getV   sS    E
))+C	N113S_335

 {{3%%r   c                v    | t         k(  r)r'D cg c]  }t        |      j                          c}S g S c c}w )N)r   r   r   )r    startendv	latenciess       r   _lrangez make_redis_mock.<locals>._lrange^   s2    . Y-67CFMMO77	 8s    6)r   r   side_effectlrange)r$   r"   r#   r*   mockr%   r+   s   ````   r   make_redis_mockr/   N   s5     ;D&
  DHH%DKKKr   c                 B   t        dddt        dd      D  cg c]  } t        | dz         c}       }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}|	|z
  }
t        |
      }d}||k  }|st        j                  d|fd||f      dt        j                         v st        j                  t              rt        j                  t              ndt        j                  |	      t        j                  |      t        j                  |      t        j                  |      dz  }t        j                  d|d          dz   d|iz  }t        t        j                  |            dx}	x}x}
x}x}}|d   }	d}|	|z
  }
t        |
      }d}||k  }|st        j                  d|fd||f      dt        j                         v st        j                  t              rt        j                  t              ndt        j                  |	      t        j                  |      t        j                  |      t        j                  |      dz  }t        j                  d|d          dz   d|iz  }t        t        j                  |            dx}	x}x}
x}x}}yc c} w )z
    BB1: Record 10 merges (3 with conflicts, 2 using Opus) via Redis mock.
         Expect conflict_rate_pct=30.0 and opus_rate_pct=20.0.
    
                  r$   r"   r#   r*   	/dev/nullredis_clientevents_pathtotal_merges==z%(py1)s == %(py4)spy1py4assert %(py6)spy6Nconflict_rate_pct      >@ư><z<%(py7)s
{%(py7)s = %(py0)s((%(py2)s - %(py4)s))
} < %(py10)sabspy0py2rB   py7py10zExpected 30.0 got z
>assert %(py12)spy12opus_rate_pct      4@zExpected 20.0 got )r/   rangefloatr   	get_stats
@pytest_ar_call_reprcompare	_safereprAssertionError_format_explanationrK   @py_builtinslocals_should_repr_global_name_format_assertmsg)i
redis_mock	telemetrystats@py_assert0@py_assert3@py_assert2@py_format5@py_format7@py_assert1@py_assert5@py_assert6@py_assert9@py_assert8@py_format11@py_format13s                   r   &test_bb1_correct_rates_after_10_mergesrp   m   s   
 !).q"6A5Q<6	J JKPI!E &B& B&&&& B&&& &&&B&&&&&&&() D )D0 301 D 1D8  1D              *    -1    2    59    U#6789      _%  %, 3,-  -4  -              &    )-    .    15    U?345      7s   Lc                 <   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   }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)uG   BB2: A brand-new MergeTelemetry with no records → all stats are zero.Nr:   r<   r   r=   r?   r@   rC   rD   rE           rR   avg_latency_msr   rV   rW   rX   rY   rZ   r[   rb   rc   rd   re   rf   rg   rh   s          r   !test_bb2_fresh_instance_all_zerosrw      sd   D1I!E %A% A%%%% A%%% %%%A%%%%%%%$%,,%,,,,%,,,%,,,,,,,,,,!(S(!S((((!S(((!(((S(((((((!")c)"c))))"c)))")))c)))))))r   c                x   | dz  }t        dt        |            }g d}|D ]  }|j                  t        |              |j	                         }t        |      t        |      z  }|d   }||z
  }t        |      }	d}
|	|
k  }|s)t        j                  d|fd	|	|
f      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  }t        j                  d| d|d          dz   d|iz  }t        t        j                   |            dx}x}x}	x}}
y)zEBB3: avg_latency_ms is the arithmetic mean of all recorded latencies.events.jsonlNr9   )      $@rS   rF   r   rt   rG   rH   )z;%(py6)s
{%(py6)s = %(py0)s((%(py2)s - %(py3)s))
} < %(py9)srK   expected_avg)rM   rN   py3rD   py9z	Expected z got z
>assert %(py11)spy11)r   r   recordr   rV   sumlenrK   rW   rX   r\   r]   r^   rY   r_   rZ   r[   )tmp_pathevents_filerb   r*   latrc   r|   ri   @py_assert4rj   rm   @py_assert7@py_format10@py_format12s                 r   )test_bb3_avg_latency_calculated_correctlyr      s   ^+KDc+>NOI"I <c:;< !Ey>C	N2L%& &5 356  6=   6              '      *6    *6    7    :>    L>u-='>&?@     r   c                 t   t        dddddd      } t        |       }|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}||u }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}|d   }d}||z
  }t        |      }	d}
|	|
k  }|st        j                  d|fd|	|
f      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}}
|d   }d}||u }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}t        j                  |       y)z@BB4: MergeRecord is fully serialisable via dataclasses.asdict().zsess-XYZr6   r4   Tg     @E@Fr   r   r=   r?   r@   rC   rD   Nr   r   r   isz%(py1)s is %(py4)sr   g&.>rH   rJ   rK   rL   assert %(py12)srQ   r   )r   r   rW   rX   rY   rZ   r[   rK   r\   r]   r^   jsondumps)r   drd   re   rf   rg   rh   ri   rj   rk   rl   rm   rn   ro   s                 r   "test_bb4_merge_record_serialisabler      sj   F 	vA\?(j(?j((((?j(((?(((j(((((((] q q    q      q       #!#!####!######!#######[>!T!>T!!!!>T!!!>!!!T!!!!!!!#$3t3$t+33+,3t3,t3333,t33333333333333$333t333,333t33333333Y< 5 <5    <5   <   5       JJqMr   c                   t               }d|j                  _        g |j                  _        | dz  }t	        |t        |            }t        ddd      }|j                  |       |j                  j                  D cg c]  }|j                  d    }}t        |v }|st        j                  d	|fd
t        |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dz  }t        j$                  d      dz   d|iz  }	t'        t        j(                  |	            d}t*        |v }|st        j                  d	|fd
t*        |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dz  }t        j$                  d      dz   d|iz  }	t'        t        j(                  |	            d}t,        |v }|st        j                  d	|fd
t,        |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dz  }t        j$                  d      dz   d|iz  }	t'        t        j(                  |	            d}yc c}w )z
    WB1: record() must call redis.incr() with the correct counter keys.
         - Always increments :total
         - Increments :conflicts when conflict_count > 0
         - Increments :opus_calls when used_opus is True
    r   ry   r9   r3   Tg      @)r   r   r   r   inz%(py0)s in %(py2)sr   
incr_callsrM   rN   zincr(:total) must be called
>assert %(py4)srB   Nr   z2incr(:conflicts) must be called when conflicts > 0r   z4incr(:opus_calls) must be called when used_opus=True)r   r   return_valuer-   r   r   r   r   incrcall_args_listargsr   rW   rX   r\   r]   r^   rY   r_   rZ   r[   r   r   )
r   ra   r   rb   r   cr   ri   @py_format3rg   s
             r   %test_wb1_redis_incr_used_for_countersr      s    J"&JNN%'J"^+KJCDTUI TCPFV%/__%C%CD!&&)DJD#BBB:BBBBBB:BBB:BBBBBBBBBBBBB%BBBBBBBZ']]]>Z]]]]]]>]]]>]]]]]]Z]]]Z]]]])]]]]]]]j(```?j``````?```?``````j```j````*``````` Es   ?M4c                   t               }d|j                  _        g |j                  _        | dz  }t	        |t        |            }|j                  t        dd             |j                  j                  D cg c]  }|j                  d    }}t        |v }|st        j                  d|fdt        |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
dz  }dd|iz  }t%        t        j&                  |            d}t(        |v}|st        j                  d|fdt(        |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
dz  }t        j*                  d      dz   d|iz  }t%        t        j&                  |            d}t,        |v}|st        j                  d|fdt,        |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
dz  }t        j*                  d      dz   d|iz  }t%        t        j&                  |            d}yc c}w )zHWB1b: When conflict_count=0, :conflicts counter must NOT be incremented.r   ry   r9   r   F)r   r   r   r   r   r   r   assert %(py4)srB   N)not in)z%(py0)s not in %(py2)sr   z8:conflicts must NOT be incremented when conflict_count=0r   r   z8:opus_calls must NOT be incremented when used_opus=False)r   r   r   r-   r   r   r   r   r   r   r   r   rW   rX   r\   r]   r^   rY   rZ   r[   r   r_   r   )	r   ra   r   rb   r   r   ri   r   rg   s	            r    test_wb1b_redis_no_conflict_incrr      s   J"&JNN%'J"^+KJCDTUI[UCD%/__%C%CD!&&)DJD####:######:###:################+ggg>gggggg>ggg>ggggggggggggg-ggggggg*,hhh?*hhhhhh?hhh?hhhhhh*hhh*hhhh.hhhhhhh Es   <Mc                   | dz  }t        dt        |            }t        dddddd	      }|j                  |       |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                  d      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   }d}||k(  }|slt        j"                  d|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}|d    }d}||u }|slt        j"                  d!|fd"||f      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}|d#   }d}||z
  }	t)        |	      }d$}||k  }|st        j"                  d%|fd&||f      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}}|d+   }d}||u }|slt        j"                  d!|fd"||f      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}y),z
    WB2: record() must append a valid JSON line to events.jsonl.
         The serialised record must contain all MergeRecord fields.
    ry   Nr9   zsess-write-testr2   r4   TglS@r   z(events.jsonl must be created by record()zC
>assert %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.exists
}()
}r   )rM   rN   rB   utf-8encodingr=   z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py6)sr   linesrM   rA   r}   rD   z,Exactly one JSON line must have been written
>assert %(py8)spy8r   r   r?   r@   rC   rD   r   r   r   r   r   r   rG   rH   rJ   rK   rL   r   rQ   r   )r   r   r   r   existsrW   r_   r\   r]   r^   rY   rZ   r[   	read_textstrip
splitlinesr   rX   r   loadsrK   )r   r   rb   r   ri   re   rg   r   rf   rj   r   rh   @py_format9parsedrd   rk   rl   rm   rn   ro   s                       r    test_wb2_events_written_to_jsonlr      s   
 ^+KDc+>NOI$F VKKKK!KKKKKKK;KKK;KKKKKKKKKKKK!!7!399;FFHEu:JJ:?JJJ:JJJJJJ3JJJ3JJJJJJuJJJuJJJ:JJJJJJJJJJJJJJZZa!F,4#44#44444#4444444#44444444- %A% A%%%% A%%% %%%A%%%%%%%"#(q(#q((((#q(((#(((q(((((((+&$&$&&&&$&&&&&&$&&&&&&&()8D8)D083018D81D88881D88888838883888)888D8881888D88888888)$$$$$$$$$$$$$$$$$$$r   c           	        | dz  }t        dt        |            }t        d      D ]*  }|j                  t	        d| t        |                   , |j                  d      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g c]  }
t%        j&                  |
      d    }}
t        d      D cg c]  }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c c}
w c c}w )zLWB2 extension: Multiple record() calls each append one line to events.jsonl.zmulti_events.jsonlNr9   r6   zsess-)r   r   r   r   r=   r   r   r   r   zassert %(py8)sr   r   z%(py0)s == %(py3)ssession_idsrM   r}   zassert %(py5)spy5)r   r   rT   r   r   rU   r   r   r   r   rW   rX   r\   r]   r^   rY   rZ   r[   r   r   )r   r   rb   r`   r   rf   rj   r   rh   r   lnr   ri   @py_format4@py_format6s                  r   !test_wb2_multiple_events_appendedr     s   11KDc+>NOI1X Y%seTUhWXY !!7!399;FFHEu::?:33uu::?@B4::b>,/@K@05a91U1#;999;99999;9999999;999;99999999999 A9s   I7I<c                 <   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   }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)zTWB3: get_stats() on a zero-total telemetry returns 0.0 rates (no ZeroDivisionError).Nrr   r<   r   r=   r?   r@   rC   rD   rE   rs   rR   rt   ru   rv   s          r   test_wb3_division_by_zero_safer     sf   D1I !E %A% A%%%% A%%% %%%A%%%%%%%$%,,%,,,,%,,,%,,,,,,,,,,!(S(!S((((!S(((!(((S(((((((!")c)"c))))"c)))")))c)))))))r   c                 \   t        dddg       } 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   }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)zHWB3 (Redis path): When Redis returns 0 for total, all rates must be 0.0.r   r7   r8   r9   r<   r=   r?   r@   rC   rD   NrE   rs   rR   rt   )r/   r   rV   rW   rX   rY   rZ   r[   )ra   rb   rc   rd   re   rf   rg   rh   s           r   test_wb3_redis_zero_totalr   "  sv    qA!rRJJKPI!E %A% A%%%% A%%% %%%A%%%%%%%$%,,%,,,,%,,,%,,,,,,,,,,!(S(!S((((!S(((!(((S(((((((!")c)"c))))"c)))")))c)))))))r   c                   t               }d|j                  _        g |j                  _        | dz  }t	        |t        |            }|j                  t        d             |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}}|d   j(                  d   }
|d   j(                  d   }|
t*        k(  }|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  }t        j"                  dt*        d|
      dz   d|iz  }t%        t        j&                  |            d}d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                   |      ndt        j                   |      dz  }t        j"                  d|      dz   d|iz  }t%        t        j&                  |            dx}}y) z
    WB4: record() must call redis.rpush() with the latency key (not individual
         per-record keys).  The latency is appended to a single list.
    r   ry   r9   gX@r{   r4   r=   r   r   rpush_callsr   z/Exactly one rpush call per record() is expectedr   r   Nr   )z%(py0)s == %(py2)skey_usedr   r   zExpected rpush to z, got r   rB   z99.9r   value_pushedr   z#Expected latency value '99.9', got z
>assert %(py5)sr   )r   r   r   r-   r   r   r   r   rpushr   r   rW   rX   r\   r]   r^   rY   r_   rZ   r[   r   r   )r   ra   r   rb   r   rf   rj   r   rh   r   r   r   ri   r   rg   r   r   s                    r   'test_wb4_redis_rpush_used_for_latenciesr   /  s   
 J"&JNN%'J"^+KJCDTUI[$78 ""11K{SqSq SSSqSSSSSS3SSS3SSSSSS{SSS{SSSSSSqSSS"SSSSSSSS1~""1%Hq>&&q)L~%  8~                &    &    ^.fXLA     " <6!  <6              "    .l-=>    r   c                    ddl m} 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
}|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
)zCPKG: core.merge __init__.py exports MergeTelemetry and MergeRecord.r   )r   r	   r   )z%(py0)s is %(py2)sMTr   r   r   rB   NMRr	   )
core.merger   r	   rW   rX   r\   r]   r^   rY   rZ   r[   )r   r   ri   r   rg   s        r   test_pkg_core_merge_exportsr   Q  s    B222222r   )zsess-001r3   r   Frz   T)r   r   r   intr   r   r   boolr   rU   r   r   returnr	   )r   r   r   N)
r$   r   r"   r   r#   r   r*   zlist[float] | Noner   r   )r   r   ),__doc__
__future__r   builtinsr\   _pytest.assertion.rewrite	assertionrewriterW   r   sysdataclassesr   pathlibr   unittest.mockr   r   pytestGENESIS_ROOTpathinsertcore.merge.merge_telemetryr   r	   r
   r   r   r   r   r   r/   rp   rw   r   r   r   r   r   r   r   r   r   r    r   r   <module>r      s  * #    
   )  'sxxHHOOA|$    !"  	
   & KL481=F>0	*"6a0i"%@:
*
*Dr   