
    &i5                       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
m
Z
mZmZ ddlmZ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 d.d/dZd	 Zd
 Zd Zd Zd Zd Z d Z!d Z"d Z#d Z$d Z%d Z&d Z'd Z(e)dk(  rddl*Z*defdefdefdefdefde fde!fde"fd e#fd!e$fd"e%fd#e&fd$e'fd%e(fgZ+dZ, e-e+      Z.e+D ]  \  Z/Z0	  e0         e1d&e/        e,d'z  Z,  e1d*e, d+e. d,       e,e.k(  r	 e1d-       y e	jj                  d'       yy# e2$ r)Z3 e1d(e/ d)e3         e*jh                          Y dZ3[3zdZ3[3ww xY w)0ug  
tests/track_b/test_story_9_04.py

Story 9.04: ConversationAggregator — Weekly Summary

Black Box Tests (BB1–BB4):
    BB1  10 sagas in mock DB → total_tasks=10
    BB2  lookback_days=1 → only uses 1-day interval in query
    BB3  Snippets containing "API_KEY" → sanitized to [REDACTED]
    BB4  No pg_connection → returns zeroed summary

White Box Tests (WB1–WB4):
    WB1  Postgres query uses parameterized interval (not string concat)
    WB2  Snippets limited to MAX_SNIPPETS (20 entries)
    WB3  Sanitization catches all SENSITIVE_PATTERNS
    WB4  period_start and period_end are UTC datetimes
    )annotationsN)datetimetimezone	timedelta)	MagicMockcallpatchz/mnt/e/genesis-system)ConversationAggregatorWeeklyConversationSummarySENSITIVE_PATTERNSMAX_SNIPPETSc                    |g }t               }| f|fg|j                  _        ||j                  _        t               }||j
                  _        ||fS )u   
    Build a mock psycopg2 connection whose cursor returns prescribed values.

    Call order matters — cursor.fetchone() is called twice (total, failed) then
    cursor.fetchall() is called once for snippets.
    )r   fetchoneside_effectfetchallreturn_valuecursor)total_tasksfailed_taskssnippetsr   conns        6/mnt/e/genesis-system/tests/track_b/test_story_9_04.py_make_mock_pgr   3   sT     [F$/>L?"CFOO#+FOO ;D%DKK<    c                    t        ddg       \  } }t        |       }|j                  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}||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}||k(  }|st        j                  d|fd||f      d	t        j                         v st        j                  |      rt        j                  |      nd	t        j                  |      t        j                  |      dz  }dd|iz  }	t        t        j                  |	            dx}x}}y)u,   BB1: 10 sagas in mock DB → total_tasks=10.
      r   r   r   pg_connection   lookback_days5assert %(py4)s
{%(py4)s = %(py0)s(%(py1)s, %(py2)s)
}
isinstancesummaryr   py0py1py2py4N==z3%(py2)s
{%(py2)s = %(py0)s.total_tasks
} == %(py5)sr(   r*   py5assert %(py7)spy7z4%(py2)s
{%(py2)s = %(py0)s.failed_tasks
} == %(py5)s)r   r
   	aggregater%   r   @py_builtinslocals
@pytest_ar_should_repr_global_name	_safereprAssertionError_format_explanationr   _call_reprcomparer   )
r   _aggr&   @py_assert3@py_format5@py_assert1@py_assert4@py_format6@py_format8s
             r   )test_bb1_ten_sagas_returns_total_tasks_10rE   L   s   RHGD!
 t
4Cmm!m,Gg899999999:999:999999g999g999999899989999999999$"$"$$$$"$$$$$$7$$$7$$$$$$"$$$$$$$$1$1$$$$1$$$$$$7$$$7$$$$$$1$$$$$$$r   c                 z   t        dd      \  } }t        |       }|j                  d       |j                  j                  }g }|D ]2  }|\  }}t        |      dk\  s|d   }|s|j                  |d          4 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)uE   BB2: lookback_days=1 → the value 1 is passed as interval parameter.   r   r   r   r      r"   r   inz%(py1)s in %(py3)sinterval_values_usedr)   py3z2Expected interval value 1 in execute params, got: 
>assert %(py5)sr0   N)r   r
   r4   executecall_args_listlenappendr7   r<   r9   r5   r6   r8   _format_assertmsgr:   r;   )r   r   r>   	all_callsrM   cargskwargsparams@py_assert0@py_assert2@py_format4rC   s                r   ,test_bb2_lookback_days_1_uses_1_day_intervalr^   X   s)    QQ?LD&
 t
4CMMM" --I  7ft9>!WF$++F1I67  1$$  1$          %    %    ==Q<RS    r   c                    ddg} t        dd|       \  }}t        |      }|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                  |      t        j                  |      dz  }	dd|	iz  }
t        t        j                  |
            dx}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}}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}}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)u?   BB3: Snippets containing 'API_KEY' → sanitized to [REDACTED].)TASK_COMPLETEzran with API_KEY=sk-abc123)
TASK_STARTnormal event description   r   r   r   r   r,   z[%(py5)s
{%(py5)s = %(py0)s(%(py3)s
{%(py3)s = %(py1)s.conversation_snippets
})
} == %(py8)srS   r&   r(   r)   rO   r0   py8assert %(py10)spy10NzAPI_KEY=sk-abc123not in)z%(py1)s not in %(py4)s)r)   r+   zassert %(py6)spy6
[REDACTED]rJ   )z%(py1)s in %(py4)srb   rI   r   r
   r4   conversation_snippetsrS   r7   r<   r5   r6   r8   r9   r:   r;   )raw_snippetsr   r=   r>   r&   r\   rB   @py_assert7@py_assert6@py_format9@py_format11r[   r?   r@   @py_format7s                  r   *test_bb3_snippet_with_api_key_is_sanitizedru   p   s    	82L LQGD!
 t
4CmmoG,,23,-22-2222-22222232223222222w222w222,222-2222222222Fg&C&CA&FF&FFFFF&FFFFFFF&FFFFFFFF;788;;<;;;;;<;;;;<;;;;;;;;;;;%I)F)Fq)II%)IIIII%)IIII%III)IIIIIIIIr   c                    t        d      } | j                  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
}||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
}||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
}||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                  }g }||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)uC   BB4: No pg_connection → returns zeroed WeeklyConversationSummary.Nr   r!   r"   r$   r%   r&   r   r'   r   r,   )z6%(py2)s
{%(py2)s = %(py0)s.total_sessions
} == %(py5)sr/   r1   r2   r.   r3   )z=%(py2)s
{%(py2)s = %(py0)s.conversation_snippets
} == %(py5)s)r
   r4   r%   r   r5   r6   r7   r8   r9   r:   r;   total_sessionsr<   r   r   rn   )r>   r&   r?   r@   rA   rB   rC   rD   s           r   0test_bb4_no_pg_connection_returns_zeroed_summaryrx      so   
 t
4Cmm!m,Gg899999999:999:999999g999g999999899989999999999!!&Q&!Q&&&&!Q&&&&&&7&&&7&&&!&&&Q&&&&&&&#!#!####!######7###7######!#######$1$1$$$$1$$$$$$7$$$7$$$$$$1$$$$$$$((.B.(B....(B......7...7...(...B.......r   c                    t        dd      \  } }t        |       }|j                  d       |j                  j                  D ]  }|\  }}|d   }d}||v}|st        j                  d	|fd
||f      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      nddz  }	t        j                  d|      dz   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  }	t        j                  d|      dz   d|	iz  }
t        t        j                  |
            dx}} y)uS   WB1: Postgres queries use parameterized intervals — no f-string/format injection.   rI   rH   r      r"   r   14ri   z%(py1)s not in %(py3)ssqlrN   z;lookback_days value '14' was interpolated into SQL string: rP   r0   Nz%srJ   rL   z&Expected parameterized query but got: )r   r
   r4   rQ   rR   r7   r<   r9   r5   r6   r8   rU   r:   r;   )r   r   r>   rW   rX   r=   r~   r[   r\   r]   rC   s              r   4test_wb1_queries_are_parameterized_not_string_concatr      si    QQ?LD&
 t
4CMMM#^^** Ma1g 	
t3 	
 	
t3 	
 	
 		  	
 	
	6	
 	
   	
 	
 		  	
 	
  J#Q	
 	
 	
 	
 	
 Lts{LLLtsLLLtLLLLLLsLLLsLLLLDSGLLLLLLLLMr   c                    t        d      D  cg c]  } d|  d|  f }} t        dd|      \  }}t        |      }|j                         }d}|j                  j
                  D ]"  }|\  }}	|d   }
d|
j                         v s |} n d}||u}|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}}|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t          d|       dz   d|iz  }t        t        j                  |            d}|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                  |      t        j                  |      dz  }dd |iz  }t        t        j                  |            dx}x}x}}yc c} w )!zIWB2: fetchall returns more than 20 rows but only MAX_SNIPPETS are stored.   EVENT_zdescription r   r   r   Neventsis not)z%(py0)s is not %(py3)sevents_query_call)r(   rO   zNo events query was executedrP   r0   rI   rJ   )z%(py0)s in %(py2)sr   rZ   r(   r*   zMAX_SNIPPETS (z$) not found in events query params: 
>assert %(py4)sr+   r,   rd   rS   r&   re   rg   rh   )ranger   r
   r4   rQ   rR   lowerr7   r<   r5   r6   r8   r9   rU   r:   r;   r   rn   rS   )iro   r   r   r>   r&   r   rW   rX   r=   r~   r\   rA   r]   rC   rZ   @py_format3r@   rB   rp   rq   rr   rs   s                          r   )test_wb2_snippets_limited_to_max_snippetsr      s    5:"I/06!aS)*L  !QLD& !t
4CmmoG ^^** a1gsyy{" $ %)HD(HHHDHHHHHHHHHHHHDHHH*HHHHHHHq!F6!  <6                "    "    &J6(S     ,,33,-33-3333-33333333333333333w333w333,333-3333333333;s   M=c                    t         D ]  } d|  d}t        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	|      d
z   d|iz  }t        t        j                  |            dx}}| |v}|st        j                  d|fd| |f      dt        j                         v st        j                  |       rt        j
                  |       nddt        j                         v st        j                  |      rt        j
                  |      nddz  }t        j                  d|  d|      dz   d|iz  }	t        t        j                  |	            d} y)z;WB3: _sanitize catches every pattern in SENSITIVE_PATTERNS.z
some text z
 more textrl   rJ   rL   resultrN   z	Pattern 'z' was not sanitized. Got: rP   r0   Nri   )z%(py0)s not in %(py2)spatternr   z)' still present after sanitization. Got: r   r+   )r   r
   	_sanitizer7   r<   r9   r5   r6   r8   rU   r:   r;   )
r   dirtyr   r[   r\   r]   rC   rA   r   r@   s
             r   0test_wb3_sanitize_catches_all_sensitive_patternsr      s   % 
WIZ0'11%8 	
|v% 	
 	
|v 	
 	
 		  	
 	
	6	
 	
   & 	
 	
 		  & 	
 	
  y :6*E	
 	
 	
 	
 	
 f$ 	
 	
wf 	
 	
	6	
 	
   	
 	
 		  	
 	
	6	
 	
  % 	
 	
 		 % 	
 	
  y I&T	
 	
 	
 	
 	

r   c            
        t        d      } | j                  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}}|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}}|j                  }|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                  |      t        j                  |      dz  }t        j                  d      dz   d|iz  }	t        t        j                  |	            dx}x}x}}|j                  }|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                  |      t        j                  |      dz  }t        j                  d      dz   d|iz  }	t        t        j                  |	            dx}x}x}}|j                  }|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                  |      t        j                  |
      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}x}x}
x}x}}|j                  }|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                  |      t        j                  |
      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}x}x}
x}x}}|j                  }|j                  }||k  }|st        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                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}y)z@WB4: period_start and period_end are UTC-aware datetime objects.Nr   r!   r"   zWassert %(py6)s
{%(py6)s = %(py0)s(%(py3)s
{%(py3)s = %(py1)s.period_start
}, %(py4)s)
}r%   r&   r   )r(   r)   rO   r+   rk   zUassert %(py6)s
{%(py6)s = %(py0)s(%(py3)s
{%(py3)s = %(py1)s.period_end
}, %(py4)s)
}r   )zT%(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.period_start
}.tzinfo
} is not %(py7)s)r(   r*   r+   r2   zperiod_start must be tz-awarez
>assert %(py9)spy9)zR%(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.period_end
}.tzinfo
} is not %(py7)szperiod_end must be tz-awarer   r,   )z%(py10)s
{%(py10)s = %(py8)s
{%(py8)s = %(py6)s
{%(py6)s = %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.period_start
}.utcoffset
}()
}.total_seconds
}()
} == %(py13)s)r(   r*   r+   rk   rf   rh   py13zassert %(py15)spy15)z%(py10)s
{%(py10)s = %(py8)s
{%(py8)s = %(py6)s
{%(py6)s = %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.period_end
}.utcoffset
}()
}.total_seconds
}()
} == %(py13)s<)zS%(py2)s
{%(py2)s = %(py0)s.period_start
} < %(py6)s
{%(py6)s = %(py4)s.period_end
}r(   r*   r+   rk   zassert %(py8)srf   )r
   r4   period_startr%   r   r5   r6   r7   r8   r9   r:   r;   
period_endtzinfor<   rU   	utcoffsettotal_seconds)r>   r&   r\   @py_assert5rt   rA   r?   rq   rD   @py_format10rp   @py_assert9@py_assert12@py_assert11@py_format14@py_format16rr   s                    r   /test_wb4_period_start_and_end_are_utc_datetimesr      s   
 t
4Cmm!m,G**5:*H55555555:555:555555g555g555*555555H555H5555555555((3:((33333333:333:333333g333g333(333333(333(3333333333S&&SdS&d2SSS&dSSSSSS7SSS7SSSSSS&SSSdSSS4SSSSSSSSO$$ODO$D0OOO$DOOOOOO7OOO7OOOOOO$OOODOOO2OOOOOOOO@))@)+@+99@9;@q@;q@@@@;q@@@@@@7@@@7@@@@@@)@@@+@@@9@@@;@@@q@@@@@@@@>''>')>)77>79>Q>9Q>>>>9Q>>>>>>7>>>7>>>>>>'>>>)>>>7>>>9>>>Q>>>>>>>>4'"4"44"44444"444444474447444444444'444'444"44444444r   c            
        t        d      } | j                  d      }|j                  |j                  z
  }d}|j                  } |       }||z
  }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t        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }
t        j                  d| d|j	                          d      dz   d|
iz  }t        t        j                  |            dx}x}x}x}x}	}y)u>   period_end - period_start ≈ lookback_days (within 1 second).Nr   r{   r"   i u r   r   )zv%(py9)s
{%(py9)s = %(py0)s((%(py5)s
{%(py5)s = %(py3)s
{%(py3)s = %(py1)s.total_seconds
}()
} - %(py6)s))
} < %(py12)sabsdeltaexpected_seconds)r(   r)   rO   r0   rk   r   py12z
Expected ~zs window, got sz
>assert %(py14)spy14)r
   r4   r   r   r   r   r7   r<   r5   r6   r8   r9   rU   r:   r;   )r>   r&   r   r   r\   rB   rp   @py_assert8r   @py_assert10@py_format13@py_format15s               r   (test_period_window_matches_lookback_daysr      s   
 t
4Cmm"m-G!5!55E!"" "$ $'77 378 1 81<   81                        #    %      (8    (8    9    <=    %&nU5H5H5J4K1M     r   c                 4   t        ddg       \  } }t        |       }|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  }t	        j                  d	|j                         d
z   d|iz  }t        t	        j                  |            dx}x}}y)zFWhen total_tasks=0 from DB, total_sessions should be max(1, 0//5) = 1.r   r   r   rI   )>=)z6%(py2)s
{%(py2)s = %(py0)s.total_sessions
} >= %(py5)sr&   r/   z"Expected total_sessions >= 1, got z
>assert %(py7)sr2   N)r   r
   r4   rw   r7   r<   r5   r6   r8   r9   rU   r:   r;   )	r   r=   r>   r&   rA   rB   r?   rC   rD   s	            r   )test_zero_tasks_gives_minimum_one_sessionr      s    BGGD!
 t
4CmmoG!! Q !Q&  !Q              "    &'    -W-C-C,DE     r   c                 f   ddl } | j                  } |t              }|sd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}}| j                  t              D ch c]  }|j                   }}h d}|j                  } ||      }|st        j                  d||z
         d	z   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                  |      dz  }t        t        j                  |            dx}}yc c}w )zIWeeklyConversationSummary is a proper dataclass with all required fields.r   NzNassert %(py5)s
{%(py5)s = %(py2)s
{%(py2)s = %(py0)s.is_dataclass
}(%(py3)s)
}dataclassesr   )r(   r*   rO   r0   >   r   r   r   r   rw   rn   zMissing fields: zL
>assert %(py5)s
{%(py5)s = %(py2)s
{%(py2)s = %(py0)s.issubset
}(%(py3)s)
}requiredfield_names)r   is_dataclassr   r5   r6   r7   r8   r9   r:   r;   fieldsnameissubsetrU   )r   rA   rB   rC   fr   r   s          r   -test_weekly_conversation_summary_is_dataclassr     s   ##>#$=>>>>>>>>;>>>;>>>#>>>>>>$=>>>$=>>>>>>>>>>#.#5#56O#PQa166QKQH  [) )   8k123                   )    )    *      Rs   H.c                 D   d} t        j                  |       }|j                  }d} ||      }|sddt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      t        j                  |      d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)zSText before a sensitive pattern is preserved; only from pattern onward is redacted.z5Event started successfully. API_KEY=abc123 was found.zEvent started successfully. zLassert %(py6)s
{%(py6)s = %(py2)s
{%(py2)s = %(py0)s.startswith
}(%(py4)s)
}r   r   Nrl   rJ   rL   rN   zassert %(py5)sr0   zAPI_KEY=abc123ri   r}   )r
   r   
startswithr5   r6   r7   r8   r9   r:   r;   r<   )
textr   rA   r?   r   rt   r[   r\   r]   rC   s
             r   5test_sanitize_text_before_sensitive_pattern_preservedr     s?   BD#--d3F<;<;<<<<<<<<6<<<6<<<<<<;<<<<<<<<<<!<6!!!!<6!!!<!!!!!!6!!!6!!!!!!!)6))))6)))))))))6)))6)))))))r   c                 Z   g d} t        dd|       \  }}t        |      }|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                  |      t        j                  |      d
z  }	dd|	iz  }
t        t        j                  |
            dx}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)zHAggregate returns exactly as many snippets as rows returned by fetchall.))EVT_Azclean description one)EVT_Bzclean description two)EVT_Czclean description threerG   r   r   r   r,   rd   rS   r&   re   rg   rh   NzEVT_A: clean description onerJ   )z=%(py1)s in %(py5)s
{%(py5)s = %(py3)s.conversation_snippets
})r)   rO   r0   r1   r2   rm   )ro   r   r=   r>   r&   r\   rB   rp   rq   rr   rs   r[   rC   rD   s                 r   ,test_aggregate_returns_correct_snippet_countr   #  sO   L
 LQGD!
 t
4CmmoG,,23,-22-2222-22222232223222222w222w222,222-2222222222)JW-J-JJ)-JJJJJ)-JJJJ)JJJJJJWJJJWJJJ-JJJJJJJJr   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
)zYPKG: core.epoch __init__.py exports ConversationAggregator and WeeklyConversationSummary.r   )r
   r   )is)z%(py0)s is %(py2)sCAr
   r   zassert %(py4)sr+   NWCSr   )
core.epochr
   r   r7   r<   r5   r6   r8   r9   r:   r;   )r   r   rA   r   r@   s        r   test_package_init_exportsr   3  s    
 '''''2'''''''2'''2''''''''''''''''''+++++3+++++++3+++3++++++++++++++++++r   __main__u    BB1: 10 sagas → total_tasks=10z(BB2: lookback_days=1 uses 1-day intervalu&   BB3: API_KEY in snippet → [REDACTED]u(   BB4: no pg_connection → zeroed summaryzWB1: queries are parameterizedz%WB2: snippets limited to MAX_SNIPPETSz"WB3: sanitize catches all patternszWB4: period dates are UTC-awarez"EDGE: window matches lookback_daysu"   EDGE: zero tasks → min 1 sessionz,EDGE: WeeklyConversationSummary is dataclassz#EDGE: text before pattern preservedz EDGE: snippet count matches rowszPKG: __init__.py exportsz	  [PASS] rI   z	  [FAIL] z: 
/z tests passedz(ALL TESTS PASSED -- Story 9.04 (Track B))r   r   N)r   intr   r   )6__doc__
__future__r   builtinsr5   _pytest.assertion.rewrite	assertionrewriter7   sysr   r   r   unittest.mockr   r   r	   pytestGENESIS_ROOTpathinsert"core.epoch.conversation_aggregatorr
   r   r   r   r   rE   r^   ru   rx   r   r   r   r   r   r   r   r   r   r   __name__	tracebacktestspassedrS   totalr   fnprint	Exceptionexc	print_excexit r   r   <module>r      s  $ #   
 2 2 0 0  'sxxHHOOA|$ 2	%0J&
/$M$ 4F

5,
	&*K , z 
,-VW	35ab	13]^	35ef	)+_`	02[\	-/_`	*,[\	-/WX	-/XY	79fg	.0ef	+-YZ	#%>?E" FJE "b	"DIdV$%aKF	" 
Bvhawm
,-89G 6  	"IdV2cU+,I!!	"s   !D++E0EE