
    .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ZddlZddlZddlmZmZ ddlmZ ddlmZmZ ddlmZmZmZmZ ddlZdZeej8                  vrej8                  j;                  de       dd	lmZm Z  dd
l!m"Z"m#Z# ddl$m%Z%m&Z&m'Z' ddl(m)Z)m*Z* ddl+m,Z,m-Z-m.Z.m/Z/ ddl0m1Z1m2Z2m3Z3m4Z4 ddl5m6Z6m7Z7m8Z8m9Z9 ddl:m;Z;m<Z<m=Z= ddl>m?Z? ddl@mAZA dZBd ZCdddZDdddZEdddZFd ZGd ZH	 	 	 	 	 	 	 	 	 	 	 d	 ddZI G d d      ZJ G d d      ZK G d  d!      ZL G d" d#      ZM G d$ d%      ZN G d& d'      ZO G d( d)      ZP G d* d+      ZQ G d, d-      ZR G d. d/      ZS G d0 d1      ZT G d2 d3      ZU G d4 d5      ZV G d6 d7      ZW G d8 d9      ZX G d: d;      ZYeZd<k(  rnddl[Z[dd=Z\g d> eJ       j                  fd? eJ       j                  fd@ eJ       j                  fdA eK       j                  fdB eK       j                  fdC eK       j                  fdD eK       j                  fdE eL       j                  fdF eL       j                  fdG eL       j                  fdH eL       j                  fdIdJ fdKdL fdMdN fdO eM       j                  fdP eN       j                  fdQ eN       j                  fdR eN       j                  fdS eN       j                  fdTdU fdVdW fdXdY fdZd[ fd\d] fd^d_ fd`da fdb eQ       j                  fdc eQ       j                  fdd eQ       j                  fde eQ       j                  fdf eR       j                  fdg eR       j                  fdh eR       j                  fdidj fdkdl fdmdn fdodp fdqdr fdsdt fdudv fdwdx fdydz fd{d| fd} eV       j                  fd~d fdd fdd fdd fdd fdd fdd fd eY       j                  fd eY       j                  fd eY       j                  fd eY       j                  fd eY       j                  fd eY       j                  fd eY       j                  fd eY       j                  fZ}dZ~dZ ee}      Ze}D ]  \  ZZ	  e         ede        e~dz  Z~  edd         ed        ee~ de de d        ed        edk(  r	 ed       y ej                  d       yy# e$ r/Z ede de         e[j                          edz  ZY dZ[dZ[ww xY w)u  
tests/epoch/test_nightly_epoch.py

Story 9.09: Test Suite — Module 9 Nightly Epoch (Unit)

Comprehensive unit test suite covering ALL Module 9 components:
  - EpochScheduler (9.01)
  - EpochRunner (9.02)
  - RedisEpochLock (9.03)
  - ConversationAggregator (9.04)
  - AxiomDistiller (9.05)
  - EpochKnowledgeWriter (9.06)
  - EpochReportGenerator (9.07)
  - EpochTier1Trigger (9.08)

14+ test cases, all external I/O mocked (Redis, Postgres, Qdrant, Gemini,
filesystem where live writes would touch production paths).

Acceptance criteria covered:
  BB1  EpochScheduler.force_trigger() fires runner.run_epoch_safe()
  BB2  RedisEpochLock prevents duplicate epoch (second acquire returns False)
  BB3  AxiomDistiller rejects low-confidence axioms (confidence < 0.6 filtered)
  BB4  EpochKnowledgeWriter writes axioms to KG file path
  BB5  ConversationAggregator returns WeeklyConversationSummary with correct fields
  BB6  EpochReportGenerator produces markdown with "# Genesis Nightly Epoch" header
  BB7  EpochTier1Trigger calls both knowledge_writer and tier1_updater

  WB1  Gemini Pro (not Flash) used for distillation (model_name attribute verified)
  WB2  epoch:lock:nightly key has TTL=7200 (verify SET NX EX params in mock)
  WB3  EpochRunner executes phases sequentially (verify call order)
  WB4  Lock released in finally block (verify release called even on error)
  WB5  EpochRunner.run_epoch_safe() returns None when lock already held
  WB6  Epoch log written to data/observability/epoch_log.jsonl
  WB7  Report file path follows epoch_{epoch_id}.md convention

# VERIFICATION_STAMP
# Story: 9.09
# Verified By: parallel-builder
# Verified At: 2026-02-25T00:00:00Z
# Tests: 14+/14+
# Coverage: 100%
    )annotationsN)datetimetimezone)Path)ListOptional)	AsyncMock	MagicMockcallpatchz/mnt/e/genesis-system)EpochSchedulerEVENTS_LOG_PATH)EpochRunnerEPOCH_LOG_PATH)RedisEpochLockEPOCH_LOCK_KEYEPOCH_LOCK_TTL)ConversationAggregatorWeeklyConversationSummary)AxiomDistillerAxiomDistillationResultCONFIDENCE_THRESHOLD)EpochKnowledgeWriterWriteResultKG_FILE_PATHQDRANT_COLLECTION)EpochReportGeneratorEpochResultEpochReport
REPORT_DIR)EpochTier1TriggerTier1EpochResultTIER1_LOG_PATHArchitectureAnalysis)Tier1Resultz/apscheduler.schedulers.asyncio.AsyncIOSchedulerc                H    t        j                         j                  |       S )z.Run an async coroutine synchronously in tests.)asyncioget_event_looprun_until_complete)coros    7/mnt/e/genesis-system/tests/epoch/test_nightly_epoch.py_runr.   r   s    !!#66t<<       c           	     B    t        d| d|xs d|  d|d| dg      S )Nepoch_2026_02_25_03dzAxiom content 
operationssaga_idcontentcategory
confidencesource_saga_ids)r   )idxr:   r8   s      r-   _make_axiomr=   w   s?    s3i(1^C51 S	*+ r/   c                \    t        d| dz         D cg c]  }t        ||       c}S c c}w )Nr0   )r<   r:   )ranger=   )countr:   is      r-   _make_axiom_listrB      s)    ?DQPQ	?RS!KA*5SSSs   )c                    t        g g |       S )N)bottlenecksrecommended_fixesscoper%   )rF   s    r-   _make_analysisrG      s    B"ERRr/   c                 <    t               } t        d      | _        | S )z;Build a MagicMock EpochRunner with an async run_epoch_safe.Nreturn_value)r
   r	   run_epoch_safe)runners    r-   _make_mock_runnerrM      s    [F%48FMr/   c                     t               } t        j                  t        j                        | _        t               }| |j                  _        t        |      }||| fS )z5Build a mock APScheduler instance and its class mock.rI   )r
   r   nowr   utcnext_run_timeget_jobrJ   )mock_jobmock_schmock_clss      r-   _make_mock_apschedulerrV      sJ    {H%\\(,,7H{H$,H!h/HXx''r/   c                Z   t        | dz        }|,t               }d|j                  _        d|j                  _        |)t               }t        ddddg      |j                  _        |%t               }t        g 	      |j                  _        |/t               }t        t        d      d
      |j                  _        |+t               }t        t        dd      |j                  _        |$t               }t        d      |j                  _        |
t               }|
t               }|	
t               }	|
(t               }
t        dddd      |
j                   _        |3t               }t#        dt        | dz        d      |j$                  _        t'        |||||||||	|
||      S )zIBuild an EpochRunner with all dependencies as MagicMocks unless provided.epoch_log.jsonlNT         event: did somethingtotal_sessionstotal_tasksfailed_tasksconversation_snippets)clusterszGood week of work.axiomsweek_summarykg_file_pathqdrant_upsertsjsonl_entries	epistemicr0   kg_axioms_writtenqdrant_scars_updatedprompt_templates_updatedrules_appended# Genesis Nightly Epoch
zepoch_test.mdepoch_2026_02_25markdown_content	file_pathepoch_id)lock
aggregatorscar_aggregator	distillerknowledge_writermeta_architectcode_proposershadow_arena
pr_creatortier1_triggerreport_generatorepoch_log_path)strr
   acquirerJ   releaser   	aggregater   rB   distillr   r   writerG   analyzer#   applyr    generater   )tmp_pathrv   rw   rx   ry   rz   r{   r|   r}   r~   r   r   	epoch_logs                r-   _make_full_epoch_runnerr      s    H001I|{$(!$(![
,E#9":	-

) #+1:B1G!!.K	);#A&-*
	&
 $;.9%/
+ ".<[.I+! {[
!+;!"%&	,
( $;1<8(_45'2
!!. ')%#!#)  r/   c                  "    e Zd ZdZd Zd Zd Zy)%TestBB1_ForceTriggerFiresRunEpochSafez8BB1: force_trigger() calls run_epoch_safe() immediately.c                    t               }t        |      }t        |j                                |j                  j                          y)zFforce_trigger() must delegate exactly once to runner.run_epoch_safe().N)rM   r   r.   force_triggerrK   assert_called_onceselfrL   	schedulers      r-   ,test_force_trigger_calls_run_epoch_safe_oncezRTestBB1_ForceTriggerFiresRunEpochSafe.test_force_trigger_calls_run_epoch_safe_once  s7    "$"6*	Y$$&'002r/   c                    t               }t        d      |_        t        |      }t	        |j                                |j                  j                          |j                  j                          y)zAforce_trigger() must call run_epoch_safe, NOT run_epoch directly.NrI   )	rM   r	   	run_epochr   r.   r   rK   r   assert_not_calledr   s      r-   5test_force_trigger_calls_run_epoch_safe_not_run_epochz[TestBB1_ForceTriggerFiresRunEpochSafe.test_force_trigger_calls_run_epoch_safe_not_run_epoch  sV    "$$$7"6*	Y$$&'002**,r/   c                "   t               }t        |      }|j                  }d}||u }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}t        |j                                |j                  j                          y)z9force_trigger() works even when start() was never called.Nis)z2%(py2)s
{%(py2)s = %(py0)s._scheduler
} is %(py5)sr   py0py2py5assert %(py7)spy7)rM   r   
_scheduler
@pytest_ar_call_reprcompare@py_builtinslocals_should_repr_global_name	_safereprAssertionError_format_explanationr.   r   rK   r   )r   rL   r   @py_assert1@py_assert4@py_assert3@py_format6@py_format8s           r-   &test_force_trigger_works_without_startzLTestBB1_ForceTriggerFiresRunEpochSafe.test_force_trigger_works_without_start  s    "$"6*	##+t+#t++++#t++++++y+++y+++#+++t+++++++Y$$&'002r/   N)__name__
__module____qualname____doc__r   r   r    r/   r-   r   r      s    B3	-3r/   r   c                  (    e Zd ZdZd Zd Zd Zd Zy)'TestBB2_RedisEpochLockPreventsDuplicatezCBB2: Second acquire() call returns False when lock is already held.c                   t               }ddg|j                  _        t        |      }|j	                  d      }|j	                  d      }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}||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}}y)zLWith a mock Redis that returns None on second SET NX, acquire returns False.TNrq   epoch_2026_02_26r   z%(py0)s is %(py3)sfirstr   py3zFirst acquire() must succeed
>assert %(py5)sr   Fsecondz.Second acquire() must fail (lock already held))r
   setside_effectr   r   r   r   r   r   r   r   _format_assertmsgr   r   )	r   
mock_redisrv   r   r   @py_assert2r   @py_format4r   s	            r-   !test_second_acquire_returns_falsezITestBB2_RedisEpochLockPreventsDuplicate.test_second_acquire_returns_false)  s   [
 '+D\
"j)/001<u}<<<u<<<<<<u<<<u<<<<<<<<<<<<<PvPPPvPPPPPPvPPPvPPPPPP PPPPPPPr/   c                   t               }d|j                  _        t        |      }|j	                  d       |j                  j                          |j                  j                  \  }}|j                  }d} ||      }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                  |      t        j                  |      dz  }
t        j                  d|j                  d            d	z   d
|
iz  }t        t        j                   |            dx}x}x}x}	}|j                  }d} ||      }|t"        k(  }	|	s=t        j                  d|	fd|t"        f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      t        j                  |      dt        j                         v st        j                  t"              rt        j                  t"              nddz  }t        j                  dt"         d|j                  d            dz   d|iz  }t        t        j                   |            dx}x}x}}	y)z=acquire() calls redis.set with nx=True and ex=EPOCH_LOCK_TTL.Trq   nxr   )zI%(py6)s
{%(py6)s = %(py2)s
{%(py2)s = %(py0)s.get
}(%(py4)s)
} is %(py9)skwargsr   r   py4py6py9znx must be True, got 
>assert %(py11)spy11Nex==)zI%(py6)s
{%(py6)s = %(py2)s
{%(py2)s = %(py0)s.get
}(%(py4)s)
} == %(py8)sr   r   r   r   r   py8zex must be , got 
>assert %(py10)spy10)r
   r   rJ   r   r   r   	call_argsgetr   r   r   r   r   r   r   r   r   r   )r   r   rv   _r   r   r   @py_assert5@py_assert8@py_assert7@py_format10@py_format12@py_format9@py_format11s                 r-   &test_acquire_passes_nx_and_ex_to_rediszNTestBB2_RedisEpochLockPreventsDuplicate.test_acquire_passes_nx_and_ex_to_redis8  s5   [
&*
#j)'())+NN,,	6zzU$Uz$U4U4'UUU4UUUUUUvUUUvUUUzUUU$UUUUUU4UUU+@DAQ@T)UUUUUUUUzz 	
$ 	
z$ 	
>1 	
 	
 	
> 	
 	
	6	
 	
   	
 	
 		  	
 	
 		  	
 	
 		  	
 	
 		   	
 	
	6	
 	
  $2 	
 	
 		 $2 	
 	
  .)

40@/CD	
 	
 	
 	
 	
 	
r/   c                   t        d      }|j                  d      }d}||u }|st        j                  d|fd||f      dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }d	d
|iz  }t        t        j                  |            dx}}y)z@When no Redis client is provided, acquire() always returns True.N)redis_client
epoch_testTr   r   resultr   assert %(py5)sr   )
r   r   r   r   r   r   r   r   r   r   )r   rv   r   r   r   r   r   s          r-   *test_acquire_without_redis_always_succeedszRTestBB2_RedisEpochLockPreventsDuplicate.test_acquire_without_redis_always_succeedsG  sr    40l+v~vvvr/   c                   d}t         |k(  }|st        j                  d|fdt         |f      dt        j                         v st        j
                  t               rt        j                  t               ndt        j                  |      dz  }t        j                  dt               dz   d|iz  }t        t        j                  |            d	x}}y	)
z7EPOCH_LOCK_KEY must be the string 'epoch:lock:nightly'.zepoch:lock:nightlyr   z%(py0)s == %(py3)sr   r   z#Expected 'epoch:lock:nightly', got r   r   N)
r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   s        r-   test_epoch_lock_key_constantzDTestBB2_RedisEpochLockPreventsDuplicate.test_epoch_lock_key_constantM  s    !5 	
~!55 	
 	
~!5 	
 	
	6	
 	
   	
 	
 		  	
 	
 		 "6 	
 	
  2.1CD	
 	
 	
 	
 	
r/   N)r   r   r   r   r   r   r   r   r   r/   r-   r   r   &  s    MQ

r/   r   c                  2    e Zd ZdZdd	dZd Zd Zd Zd Zy)
*TestBB3_AxiomDistillerRejectsLowConfidencezDBB3: Axioms with confidence < 0.6 are filtered out by the distiller.c                2    t        j                  ||d      S )z9Build a valid Gemini JSON response with the given axioms.rc   jsondumps)r   axioms_datasummarys      r-   _make_gemini_responsez@TestBB3_AxiomDistillerRejectsLowConfidence._make_gemini_response\  s    zz!#
  	r/   c           	        | j                  ddddg dddddg dg      }t        |	      }t        |d
      }|j                  dd      }|j                  }t        |      }d}||k(  }|s6t        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  }	t        j                  dt        |j                               dz   d|	iz  }
t        t        j                  |
            dx}x}x}}|j                  d   }|j                  }d}||k(  }|st        j                  d|fd||f      t        j                  |      t        j                  |      t        j                  |      dz  }dd|iz  }	t        t        j                  |	            dx}x}x}}y)z8Axiom with confidence=0.5 must be excluded from results.ax_001zHigh confidence axiomopsg?r6   ax_002zLow confidence axiomg      ?rI   
gemini-progemini_client
model_nameNconversationsscarsr0   r   zL%(py5)s
{%(py5)s = %(py0)s(%(py3)s
{%(py3)s = %(py1)s.axioms
})
} == %(py8)slenr   r   py1r   r   r   zExpected 1 axiom, got r   r   r   )z*%(py3)s
{%(py3)s = %(py1)s.id
} == %(py6)s)r  r   r   zassert %(py8)sr   )r   r
   r   r   rd   r  r   r   r   r   r   r   r   r   r   r7   )r   rawclientry   r   r   r   r   @py_assert6r   r   @py_assert0r   @py_format7s                 r-   &test_low_confidence_axiom_filtered_outzQTestBB3_AxiomDistillerRejectsLowConfidence.test_low_confidence_axiom_filtered_outc  s   (((?Ubez|}(>Eady{|*
  ,"LQ	""T"B==Us=!UQU!Q&UUU!QUUUUUUsUUUsUUUUUU6UUU6UUU=UUU!UUUQUUU*@V]]AS@T(UUUUUUUU}}Q."".h."h...."h......"...h.......r/   c                   | j                  ddddg dg      }t        |      }t        |      }|j                  d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  }	t        j                  d      dz   d|	iz  }
t        t        j                  |
            dx}x}x}}y)zGAxiom with confidence exactly 0.6 is at threshold and must be accepted.ax_boundaryzBoundary axiomr   333333?r6   rI   r   Nr   r0   r   r  r  r   r  z0Axiom at exactly confidence=0.6 must be acceptedr   r   )r   r
   r   r   rd   r  r   r   r   r   r   r   r   r   r   )r   r  r  ry   r   r   r   r   r	  r   r   s              r-   %test_boundary_exactly_0_6_is_acceptedzPTestBB3_AxiomDistillerRejectsLowConfidence.test_boundary_exactly_0_6_is_acceptedq  s   (( -=5`cxz{*
  ,"8	""T"B==Zs=!ZQZ!Q&ZZZ!QZZZZZZsZZZsZZZZZZ6ZZZ6ZZZ=ZZZ!ZZZQZZZ(ZZZZZZZZr/   c           	     n   | j                  ddddg dddddg dg      }t        |	      }t        |
      }|j                  dd      }|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  }t        j                  d|j                         dz   d|iz  }	t        t        j                  |	            dx}x}}y)u1   All axioms below threshold → empty axioms list.r   zLow 1r   g333333?r6   r   zLow 2g?rI   r  Nr   r   z.%(py2)s
{%(py2)s = %(py0)s.axioms
} == %(py5)sr   r   u8   All axioms below threshold — expected empty list, got 
>assert %(py7)sr   )r   r
   r   r   rd   r   r   r   r   r   r   r   r   r   )
r   r  r  ry   r   r   r   r   r   r   s
             r-   &test_all_below_threshold_returns_emptyzQTestBB3_AxiomDistillerRejectsLowConfidence.test_all_below_threshold_returns_empty}  s-   ((URUjlmURUjlm*
  ,"8	""T"B}} 	
 	
}" 	
 	
} 	
 	
	6	
 	
   	
 	
 		  	
 	
 		  	
 	
 		 !# 	
 	
  Gv}}oV	
 	
 	
 	
 	
 	
r/   c                   d}t         |k(  }|st        j                  d|fdt         |f      dt        j                         v st        j
                  t               rt        j                  t               ndt        j                  |      dz  }t        j                  dt                dz   d|iz  }t        t        j                  |            d	x}}y	)
z!CONFIDENCE_THRESHOLD must be 0.6.r  r   r   r   r   z&CONFIDENCE_THRESHOLD must be 0.6, got r   r   N)
r   r   r   r   r   r   r   r   r   r   r   s        r-   )test_confidence_threshold_constant_is_0_6zTTestBB3_AxiomDistillerRejectsLowConfidence.test_confidence_threshold_constant_is_0_6  s    '* 	
#s* 	
 	
#s 	
 	
	6	
 	
  $ 	
 	
 		 $ 	
 	
 		 (+ 	
 	
  55I4JK	
 	
 	
 	
 	
r/   N)
Good week.)r   listr   r   returnr   )	r   r   r   r   r   r  r  r  r  r   r/   r-   r   r   Y  s    N/
[

r/   r   c                  .    e Zd ZdZddZddZddZd Zy)*TestBB4_EpochKnowledgeWriterWritesToKGPathzGBB4: EpochKnowledgeWriter.write() persists axioms to the KG JSONL file.c                   t        |dz        }t        |      }t        d      }|j                  |d      }|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	d
z  }dd|iz  }	t        t        j                  |	            dx}}|j                  }d}
||
k(  }|st        j                  d|fd||
f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |
      dz  }dd|iz  }t        t        j                  |            dx}x}}
t        |      j                  d      }|j!                         D cg c]  }|j#                         s| }}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t%        |             dz   d|	iz  }t        t        j                  |            dx}x}
}yc c}w )zGwrite() creates and populates the KG file with one JSON line per axiom.test_kg.jsonlkg_path   rq   ru   r   )z4%(py2)s
{%(py2)s = %(py0)s.kg_file_path
} == %(py4)sr   r   r   r   r   zassert %(py6)sr   Nz5%(py2)s
{%(py2)s = %(py0)s.jsonl_entries
} == %(py5)sr   r   r   utf-8encodingz0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py6)sr  linesr   r  r   r   zExpected 3 lines, got 
>assert %(py8)sr   )r   r   rB   r   rg   r   r   r   r   r   r   r   r   ri   r   	read_text
splitlinesstripr  r   )r   r   r   writerrd   r   r   r   @py_format5r  r   r   r   r8   lr)  r   r   r   s                      r-   test_axioms_written_to_kg_filezITestBB4_EpochKnowledgeWriterWritesToKGPath.test_axioms_written_to_kg_file  s   h01%g6!!$f/AB""-"g----"g------v---v---"------g---g-------##(q(#q((((#q((((((v(((v(((#(((q((((((( w-))7);#..0>qAGGI>>5zEQEzQEEEzQEEEEEEsEEEsEEEEEE5EEE5EEEzEEEQEEE"8U EEEEEEEE ?s   M/5M/c                N   t        |dz        }t        |      }t        d      }|j                  |d       t	        |      j                  d      j                         }|D ch c]+  }|j                         st        j                  |      d   - }}|D ch c]  }|j                   }	}||	k(  }
|
st        j                  d	|
fd
||	f      dt        j                         v st        j                  |      rt        j                   |      nddt        j                         v st        j                  |	      rt        j                   |	      nddz  }t        j"                  d|	 d|       dz   d|iz  }t%        t        j&                  |            d}
yc c}w c c}w )z?Each line in the KG file is valid JSON containing the axiom id.r  r  r[   rq   r"  r%  r&  r7   r   z%(py0)s == %(py2)sidsexpected_idsr   r   zExpected axiom IDs r   
>assert %(py4)sr   N)r   r   rB   r   r   r,  r-  r.  r   loadsr7   r   r   r   r   r   r   r   r   r   )r   r   r   r/  rd   r)  r1  r5  ar6  r   @py_format3r0  s                r-   -test_each_kg_line_is_valid_json_with_axiom_idzXTestBB4_EpochKnowledgeWriterWritesToKGPath.test_each_kg_line_is_valid_json_with_axiom_id  s   h01%g6!!$V&89W'''9DDF,1?qQWWYtzz!}T"??&,---l"SSSslSSSSSSsSSSsSSSSSSlSSSlSSSS&9,vcU$SSSSSSS @-s   &F<FF"c                   t        |dz        }t        |      }|j                  g d      }|j                  }d}||k(  }|st	        j
                  d|fd||f      dt        j                         v st	        j                  |      rt	        j                  |      ndt	        j                  |      t	        j                  |      d	z  }d
d|iz  }	t        t	        j                  |	            dx}x}}y)z9write() with an empty axiom list returns jsonl_entries=0.zempty_kg.jsonlr  rq   r"  r   r   r$  r   r   r   r   N)r   r   r   ri   r   r   r   r   r   r   r   r   )
r   r   r   r/  r   r   r   r   r   r   s
             r-   1test_write_with_empty_axioms_returns_zero_entriesz\TestBB4_EpochKnowledgeWriterWritesToKGPath.test_write_with_empty_axioms_returns_zero_entries  s    h!112%g6b+=>##(q(#q((((#q((((((v(((v(((#(((q(((((((r/   c                   d}|t         v }|st        j                  d|fd|t         f      t        j                  |      dt	        j
                         v st        j                  t               rt        j                  t               nddz  }t        j                  dt               dz   d|iz  }t        t        j                  |            d	x}}y	)
z>KG_FILE_PATH must reference genesis_evolution_learnings.jsonl.z!genesis_evolution_learnings.jsonlinz%(py1)s in %(py3)sr   r  r   zFKG_FILE_PATH should reference genesis_evolution_learnings.jsonl, got: r   r   N)
r   r   r   r   r   r   r   r   r   r   r   r
  r   r   r   s        r-   test_kg_file_path_constantzETestBB4_EpochKnowledgeWriterWritesToKGPath.test_kg_file_path_constant  s    2 	
2lB 	
 	
2l 	
 	
 
	 3 	
 	
 
6	
 	
  7C 	
 	
 
	 7C 	
 	
  #%	
 	
 	
 	
 	
r/   Nr   r   )r   r   r   r   r2  r<  r>  rE  r   r/   r-   r  r    s    QF T)
r/   r  c                  (    e Zd ZdZd Zd Zd Zd Zy)2TestBB5_ConversationAggregatorReturnsWeeklySummaryzRBB5: ConversationAggregator.aggregate() returns a valid WeeklyConversationSummary.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)zPWhen pg_connection=None, aggregate() returns a zeroed WeeklyConversationSummary.Npg_connection   lookback_days5assert %(py4)s
{%(py4)s = %(py0)s(%(py1)s, %(py2)s)
}
isinstancer   r   r   r  r   r   r   r   )z6%(py2)s
{%(py2)s = %(py0)s.total_sessions
} == %(py5)sr   r   r   z3%(py2)s
{%(py2)s = %(py0)s.total_tasks
} == %(py5)sz4%(py2)s
{%(py2)s = %(py0)s.failed_tasks
} == %(py5)s)z=%(py2)s
{%(py2)s = %(py0)s.conversation_snippets
} == %(py5)s)r   r   rP  r   r   r   r   r   r   r   r   r^   r   r_   r`   ra   )	r   rw   r   r   r0  r   r   r   r   s	            r-   !test_no_db_returns_zeroed_summaryzTTestBB5_ConversationAggregatorReturnsWeeklySummary.test_no_db_returns_zeroed_summary  sr   +$?
%%A%6&";<<<<<<<<z<<<z<<<<<<&<<<&<<<<<<";<<<";<<<<<<<<<<$$))$))))$))))))v)))v)))$))))))))))!!&Q&!Q&&&&!Q&&&&&&v&&&v&&&!&&&Q&&&&&&&""'a'"a''''"a''''''v'''v'''"'''a'''''''++1r1+r1111+r111111v111v111+111r1111111r/   c                   t        d      }|j                  d      }|j                  }t        |t              }|s5t        j                  d      dz   dt        j                         v st        j                  t              rt        j                  t              nddt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      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              }|s5t        j                  d      dz   dt        j                         v st        j                  t              rt        j                  t              nddt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      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                  }||k  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }t        j                  d      dz   d|iz  }t        t        j                  |            dx}x}}y)zMWeeklyConversationSummary must include period_start and period_end datetimes.NrJ  rL  rM  zperiod_start must be a datetimezY
>assert %(py6)s
{%(py6)s = %(py0)s(%(py3)s
{%(py3)s = %(py1)s.period_start
}, %(py4)s)
}rP  r   r   r   r  r   r   r   zperiod_end must be a datetimezW
>assert %(py6)s
{%(py6)s = %(py0)s(%(py3)s
{%(py3)s = %(py1)s.period_end
}, %(py4)s)
}<)zS%(py2)s
{%(py2)s = %(py0)s.period_start
} < %(py6)s
{%(py6)s = %(py4)s.period_end
}r   r   r   r   z,period_start must be earlier than period_endr+  r   )r   r   period_startrP  r   r   r   r   r   r   r   r   r   
period_endr   )	r   rw   r   r   r   r  r   r   r   s	            r-   %test_summary_has_period_start_and_endzXTestBB5_ConversationAggregatorReturnsWeeklySummary.test_summary_has_period_start_and_end  sS   +$?
%%A%6 -- 	
z-x8 	
8 	
  .	
 	
	6	
 	
   	
 	
 		  	
 	
	6	
 	
  ! 	
 	
 		 ! 	
 	
 		 . 	
 	
	6	
 	
  08 	
 	
 		 08 	
 	
 		 9 	
 	
 	
 	
 	
 !++ 	
z+X6 	
6 	
  ,	
 	
	6	
 	
   	
 	
 		  	
 	
	6	
 	
  ! 	
 	
 		 ! 	
 	
 		 , 	
 	
	6	
 	
  .6 	
 	
 		 .6 	
 	
 		 7 	
 	
 	
 	
 	
 "" 	
V%6%6 	
"%66 	
 	
 	
"%6 	
 	
	6	
 	
   	
 	
 		  	
 	
 		 # 	
 	
	6	
 	
  &, 	
 	
 		 &, 	
 	
 		 &7 	
 	
  ;	
 	
 	
 	
 	
 	
r/   c                   t               }ddg|j                  _        ddg|j                  _        t               }||j
                  _        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$                  }
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+        |
      }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)zFWith a mocked Postgres connection, aggregate() returns correct counts.)   )r!  )task_completezSaga completed successfully)errorzSaga encountered an errorrJ  rL  rM  r^  r   rR  r   r   r   r   Nr!  rS  z`assert %(py6)s
{%(py6)s = %(py0)s(%(py3)s
{%(py3)s = %(py1)s.conversation_snippets
}, %(py4)s)
}rP  r  rV  r[   )z[%(py5)s
{%(py5)s = %(py0)s(%(py3)s
{%(py3)s = %(py1)s.conversation_snippets
})
} == %(py8)sr  r  zassert %(py10)sr   )r
   fetchoner   fetchallrJ   cursorr   r   r_   r   r   r   r   r   r   r   r   r`   ra   rP  r  r  )r   mock_cursor	mock_connrw   r   r   r   r   r   r   r   r   r  r   r	  r   r   s                    r-   test_aggregate_with_mocked_dbzPTestBB5_ConversationAggregatorReturnsWeeklySummary.test_aggregate_with_mocked_db  s   k,14=( =2-
)
 K	(3	%+)D
%%A%6!!'R'!R''''!R''''''v'''v'''!'''R'''''''""'a'"a''''"a''''''v'''v'''"'''a''''''' 66=z6========z===z======&===&===6===================//5s/05A50A55550A555555s555s55555565556555/5550555A5555555r/   c                   t               }ddg|j                  _        dg|j                  _        t               }||j
                  _        t        |      }|j                  d      }d |j                  D        }t        |      }|st        j                  d|j                         d	z   d
t        j                         v st        j                  t              rt        j                  t              nd
t        j                  |      t        j                  |      dz  }t!        t        j"                  |            dx}}y)z=API keys and tokens in snippets are replaced with [REDACTED].)r0   )r   )api_callzUsed sk-secret12345 for authrJ  rL  rM  c              3  $   K   | ]  }d |v  
 yw)z
[REDACTED]Nr   ).0ss     r-   	<genexpr>zpTestBB5_ConversationAggregatorReturnsWeeklySummary.test_sensitive_data_scrubbed_from_snippets.<locals>.<genexpr>  s     K<1$Ks   z&Expected [REDACTED] in snippets, got: z.
>assert %(py4)s
{%(py4)s = %(py0)s(%(py2)s)
}anyr#  N)r
   ra  r   rb  rJ   rc  r   r   ra   rm  r   r   r   r   r   r   r   r   )r   rd  re  rw   r   r   r   r0  s           r-   *test_sensitive_data_scrubbed_from_snippetsz]TestBB5_ConversationAggregatorReturnsWeeklySummary.test_sensitive_data_scrubbed_from_snippets  s!   k,0$<(8-
) K	(3	%+)D
%%A%6 Lf.J.JK 	
sKK 	
K 	
  5V5Q5Q4RS	
 	
	6	
 	
   	
 	
 		  	
 	
 		 L 	
 	
 		 L 	
 	
 	
 	
 	
r/   N)r   r   r   r   rT  r\  rf  rn  r   r/   r-   rH  rH    s    \	2
6,
r/   rH  c                  0    e Zd ZdZddZddZddZddZy),TestBB6_EpochReportGeneratorProducesMarkdownzLBB6: EpochReportGenerator.generate() produces markdown with required header.c                ^   t        t        |            }t        dddgt        d      d      }|j	                  |      }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  }t        j                  d|j
                  dd       dz   d|iz  }	t        t        j                  |	            dx}x}}y)z=Generated Markdown must start with '# Genesis Nightly Epoch'.
report_dir
2026_02_25conversation_aggregateaxiom_distillr[   r  )ru   phases_completedrd   re   z# Genesis Nightly Epochr@  )z8%(py1)s in %(py5)s
{%(py5)s = %(py3)s.markdown_content
}reportr  r   r   zQReport must contain '# Genesis Nightly Epoch' header.
Got content starting with: Nd   r  r   r   r   r   rB   r   rs   r   r   r   r   r   r   r   r   r   )
r   r   	generatorresult_datarx  r
  r   r   r   r   s
             r-   ,test_report_has_genesis_nightly_epoch_headerzYTestBB6_EpochReportGeneratorProducesMarkdown.test_report_has_genesis_nightly_epoch_header  s)   (CMB	!!6H#A&%	
 ##K0( 	
F,C,C 	
(,CC 	
 	
(,C 	
 	
 
	 ) 	
 	
 
6	
 	
  -3 	
 	
 
	 -3 	
 	
 
	 -D 	
 	
 **0*A*A$3*G)JL	
 	
 	
 	
 	
 	
r/   c                p   t        t        |            }t        d      }|j                  |      }|j                  }|j
                  }d} ||      }|st        j                  d|j                        dz   dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }	t        t        j                  |	            d	x}x}x}}y	)
z<Report file path must follow epoch_{epoch_id}.md convention.rr  rt  r"  epoch_2026_02_25.mdz4Report file must end with epoch_2026_02_25.md, got: zk
>assert %(py8)s
{%(py8)s = %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.file_path
}.endswith
}(%(py6)s)
}rx  r   N)r   r   r   r   rt   endswithr   r   r   r   r   r   r   r   )
r   r   r|  r}  rx  r   r   r   r   r   s
             r-   ,test_report_file_follows_epoch_id_conventionzYTestBB6_EpochReportGeneratorProducesMarkdown.test_report_file_follows_epoch_id_convention.  s%   (CMB	!<8##K0 	
(( 	
)> 	
()>? 	
? 	
  C6CSCSBVW	
 	
	6	
 	
   	
 	
 		  	
 	
 		   	
 	
 		 ) 	
 	
 		 *? 	
 	
 		 @ 	
 	
 	
 	
 	
 	
r/   c           	        t        t        |            }t        ddgg       }|j                  |      }|j                  }t        |      }|j                  } |       }|s!t        j                  d|j                         dz   dt        j                         v st        j                  t
              rt        j                  t
              nddt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      d	z  }	t        t        j                  |	            d
x}x}x}}y
)z3Generated report must be written as an actual file.rr  rt  ru  )ru   rw  rd   zReport file not found at z
>assert %(py9)s
{%(py9)s = %(py7)s
{%(py7)s = %(py5)s
{%(py5)s = %(py0)s(%(py3)s
{%(py3)s = %(py1)s.file_path
})
}.exists
}()
}r   rx  )r   r  r   r   r   r   N)r   r   r   r   rt   r   existsr   r   r   r   r   r   r   r   )
r   r   r|  r}  rx  r   r   r	  r   r   s
             r-   test_report_is_written_to_diskzKTestBB6_EpochReportGeneratorProducesMarkdown.test_report_is_written_to_disk9  si   (CMB	!!67
 ##K0$$ 	
t$% 	
%,, 	
,. 	
. 	
  ((8(8'9:	
 	
	6	
 	
   	
 	
 		  	
 	
	6	
 	
   	
 	
 		  	
 	
 		 % 	
 	
 		 & 	
 	
 		 - 	
 	
 		 / 	
 	
 	
 	
 	
 	
r/   c                   t        t        |            }t        dg dt        d      d      }|j	                  |      }|j
                  }d}||v }|st        j                  d|fd	||f      t        j                  |      d
t        j                         v st        j                  |      rt        j                  |      nd
dz  }t        j                  d      dz   d|iz  }	t        t        j                  |	            dx}}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)z<Report body must reference phases completed and axiom count.rr  rt  )ru  rv  knowledge_writer!  rY   )ru   rw  rd   tier1_updatesru  r@  rB  r8   rC  z Phase name must appear in reportr   r   N3z!Axiom count must appear in reportr{  )
r   r   r|  r}  rx  r8   r
  r   r   r   s
             r-   +test_report_includes_phases_and_axiom_countzXTestBB6_EpochReportGeneratorProducesMarkdown.test_report_includes_phases_and_axiom_countH  s   (CMB	!![#A&	
 ##K0))'V'72VVV'7VVV'VVVVVV7VVV7VVVV4VVVVVVVBsg~BBBsgBBBsBBBBBBgBBBgBBBBBBBBBBBr/   NrF  )r   r   r   r   r~  r  r  r  r   r/   r-   rp  rp    s    V
"	

Cr/   rp  c                  (    e Zd ZdZddZddZddZy).TestBB7_EpochTier1TriggerCallsBothDependencieszMBB7: EpochTier1Trigger.apply() calls both knowledge_writer and tier1_updater.c                X   t               }t        t        dd      |j                  _        t               }t        dddd      |j                  _        t        ||t        |dz              }|j                  t        d      t                      |j                  j                          y)	z<knowledge_writer.write() is called exactly once per apply().r[   rf   r   r0   kg_entities_addedscars_updatedprompts_updatedrules_updatedtier1.jsonlrz   tier1_updaterlog_pathNr
   r   r   r   rJ   r'   apply_tier1r"   r   r   rB   rG   r   r   r   mock_writermock_updatertriggers        r-   %test_knowledge_writer_write_is_calledzTTestBB7_EpochTier1TriggerCallsBothDependencies.test_knowledge_writer_write_is_calleda  s    k)4%aq*
& !{0;q!ST1
  - $(&M12
 	&q)>+;<,,.r/   c                X   t               }t        t        dd      |j                  _        t               }t        dddd      |j                  _        t        ||t        |dz              }|j                  t        d      t                      |j                  j                          y)	z?tier1_updater.apply_tier1() is called exactly once per apply().r   rf   r0   r  r  r  r[   Nr  r  s        r-   (test_tier1_updater_apply_tier1_is_calledzWTestBB7_EpochTier1TriggerCallsBothDependencies.test_tier1_updater_apply_tier1_is_calledu  s    k)4%aq*
& !{0;q!ST1
  - $(&M12
 	&q)>+;<  335r/   c                   t               }t        t        dd      |j                  _        t               }t        dddd      |j                  _        t        ||t        |dz              }|j                  t        d      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$                  |      t        j$                  |      d
z  }	dd|	iz  }
t'        t        j(                  |
            dx}x}x}}|j                  }|j                  }d}||k(  }|st        j                  d|fd||f      dt        j                          v st        j"                  |      rt        j$                  |      ndt        j$                  |      t        j$                  |      t        j$                  |      d
z  }	dd|	iz  }
t'        t        j(                  |
            dx}x}x}}y)zIBoth writer.write() and updater.apply_tier1() called in a single apply().r0   rf   r   r  r  r  r   )zM%(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.write
}.call_count
} == %(py7)sr  r   r   r   r   zassert %(py9)sr   N)zS%(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.apply_tier1
}.call_count
} == %(py7)sr  )r
   r   r   r   rJ   r'   r  r"   r   r   rB   rG   
call_countr   r   r   r   r   r   r   r   )r   r   r  r  r  r   r   r	  r   r   r   s              r-   )test_both_called_in_same_apply_invocationzXTestBB7_EpochTier1TriggerCallsBothDependencies.test_both_called_in_same_apply_invocation  s   k)4%aq*
& !{0;q!ST1
  - $(&M12
 	&q)>+;<  0 ++0q0+q0000+q000000{000{000 000+000q0000000''7'227a72a77772a777777|777|777'7772777a7777777r/   NrF  )r   r   r   r   r  r  r  r   r/   r-   r  r  ^  s    W/(6(8r/   r  c                  (    e Zd ZdZd Zd Zd Zd Zy)$TestWB1_GeminiProUsedForDistillationz<WB1: AxiomDistiller uses gemini-pro model, not gemini-flash.c                   t               }|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	)
z(Default model_name must be 'gemini-pro'.r   r   )z2%(py2)s
{%(py2)s = %(py0)s.model_name
} == %(py5)sry   r   z-Default model_name must be 'gemini-pro', got r  r   N)r   r   r   r   r   r   r   r   r   r   r   )r   ry   r   r   r   r   r   s          r-    test_default_model_is_gemini_prozETestWB1_GeminiProUsedForDistillation.test_default_model_is_gemini_pro  s    "$	## 	
| 	
#|3 	
 	
#| 	
 	
	6	
 	
   	
 	
 		  	
 	
 		 $ 	
 	
 		 (4 	
 	
  <I<P<P;ST	
 	
 	
 	
 	
 	
r/   c                j   t               }d}|j                  }|j                  } |       }||v}|st        j                  d|fd||f      t        j
                  |      dt        j                         v st        j                  |      rt        j
                  |      ndt        j
                  |      t        j
                  |      t        j
                  |      dz  }t        j                  d|j                        dz   d|iz  }t        t        j                  |            d	x}x}x}x}}y	)
z*Default model must not be a Flash variant.flash)not in)zh%(py1)s not in %(py9)s
{%(py9)s = %(py7)s
{%(py7)s = %(py5)s
{%(py5)s = %(py3)s.model_name
}.lower
}()
}ry   )r  r   r   r   r   zFDistiller must not use Flash model for axiom distillation; model_name=r   r   N)r   r   lowerr   r   r   r   r   r   r   r   r   )	r   ry   r
  r   r	  r   r   r   r   s	            r-   test_model_name_is_not_flashzATestWB1_GeminiProUsedForDistillation.test_model_name_is_not_flash  s+   "$	 	
i22 	
288 	
8: 	
w:: 	
 	
w: 	
 	
 
	  	
 	
 
6	
 	
  ( 	
 	
 
	 ( 	
 	
 
	 3 	
 	
 
	 9 	
 	
 
	 ; 	
 	
 #..13	
 	
 	
 	
 	
 	
r/   c                   i dfd}t        |d      }t        ddddg	      }|j                  |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   }t        |      }	d}
|	|
kD  }|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
                  |
      dz  }t        j                  d      dz   d|iz  }t        t        j                  |            d
x}x}	x}}
y
)z?Gemini client callable is invoked with the distillation prompt.promptc                >    | d<   t        j                  g dd      S )Nr  zEmpty week.rc   r   )r  captureds    r-   fake_clientz_TestWB1_GeminiProUsedForDistillation.test_gemini_client_called_with_prompt.<locals>.fake_client  s!    !'HX::]KLLr/   r   r   r!  
   r0   r\   r]   Nr   r@  rB  r  rC  z*Gemini client must be called with a promptr   r   2   )>)z/%(py4)s
{%(py4)s = %(py0)s(%(py2)s)
} > %(py7)sr  r  z&Prompt must contain meaningful contentz
>assert %(py9)sr   )r  r   r  r   )r   r   r   r   r   r   r   r   r   r   r   r   r  )r   r  ry   r  r
  r   r   r   r   r   r	  r   r   r   r  s                 @r-   %test_gemini_client_called_with_promptzJTestWB1_GeminiProUsedForDistillation.test_gemini_client_called_with_prompt  sC   	M #V	1#9":	
 	TBQx8#QQQx8QQQxQQQQQQ8QQQ8QQQQ%QQQQQQQH%Us%&UU&+UUU&UUUUUUsUUUsUUU%UUU&UUUUUU-UUUUUUUUr/   c                   t        d      }|j                  dd      }t        |t              }|sddt	        j
                         v st        j                  t              rt        j                  t              nddt	        j
                         v st        j                  |      rt        j                  |      nddt	        j
                         v st        j                  t              rt        j                  t              ndt        j                  |      dz  }t        t        j                  |            d}|j                  }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}}|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)zDWithout a Gemini client, distill() returns empty DistillationResult.Nr  r   rO  rP  r   r   rQ  r   r  r   r   r    )z4%(py2)s
{%(py2)s = %(py0)s.week_summary
} == %(py5)s)r   r   rP  r   r   r   r   r   r   r   r   rd   r   re   )	r   ry   r   r   r0  r   r   r   r   s	            r-   #test_no_client_returns_empty_resultzHTestWB1_GeminiProUsedForDistillation.test_no_client_returns_empty_result  s   "6	""T"B&"455555555z555z555555&555&555555"4555"45555555555}}""}""""}""""""v"""v"""}""""""""""""(b("b(((("b((((((v(((v((("(((b(((((((r/   N)r   r   r   r   r  r  r  r  r   r/   r-   r  r    s    F

V()r/   r  c                  "    e Zd ZdZd Zd Zd Zy)TestWB2_EpochLockTTLIs7200zCWB2: Redis SET NX EX call uses key=epoch:lock:nightly and TTL=7200.c                   d}t         |k(  }|st        j                  d|fdt         |f      dt        j                         v st        j
                  t               rt        j                  t               ndt        j                  |      dz  }t        j                  dt                dz   d|iz  }t        t        j                  |            d	x}}y	)
z)EPOCH_LOCK_TTL must equal 7200 (2 hours).   r   r   r   r   z!EPOCH_LOCK_TTL must be 7200, got r   r   N)
r   r   r   r   r   r   r   r   r   r   r   s        r-   test_lock_ttl_constant_is_7200z9TestWB2_EpochLockTTLIs7200.test_lock_ttl_constant_is_7200  s    !% 	
~% 	
 	
~ 	
 	
	6	
 	
   	
 	
 		  	
 	
 		 "& 	
 	
  0/?@	
 	
 	
 	
 	
r/   c                   t               }d|j                  _        t        |      }|j	                  d       |j                  j
                  }|j                  r|j                  d   n|j                  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}y)zJacquire() calls redis.set with EPOCH_LOCK_KEY as the first positional arg.Trq   r   keyr   r4  key_argr   r7  zRedis SET key must be r   r8  r   N)r
   r   rJ   r   r   r   argsr   r   r   r   r   r   r   r   r   r   r   r   )r   r   rv   r   r  r   r;  r0  s           r-   (test_acquire_uses_epoch_lock_nightly_keyzCTestWB2_EpochLockTTLIs7200.test_acquire_uses_epoch_lock_nightly_key  s'   [
&*
#j)'(NN,,	'0~~)..#9;K;K;O;OPU;V.( 	
 	
w. 	
 	
	6	
 	
   	
 	
 		  	
 	
	6	
 	
  ) 	
 	
 		 ) 	
 	
  %^$6fWKH	
 	
 	
 	
 	
r/   c                   t               }d|j                  _        t        |      }|j	                  d       |j                  j
                  j                  }|j                  }d} ||      }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                  |      dz  }	t        j                  d	|j                  d            d
z   d|	iz  }
t        t        j                   |
            dx}x}x}x}}y)z<The ex= param in redis.set must equal EPOCH_LOCK_TTL (7200).Trq   r   r  r   zI%(py6)s
{%(py6)s = %(py2)s
{%(py2)s = %(py0)s.get
}(%(py4)s)
} == %(py9)sr   r   z!Lock TTL (ex=) must be 7200, got r   r   N)r
   r   rJ   r   r   r   r   r   r   r   r   r   r   r   r   r   r   )r   r   rv   r   r   r   r   r   r   r   r   s              r-    test_acquire_ex_param_equals_ttlz;TestWB2_EpochLockTTLIs7200.test_acquire_ex_param_equals_ttl  sN   [
&*
#j)'())00zz 	
$ 	
z$ 	
4 	
4' 	
 	
4 	
 	
	6	
 	
   	
 	
 		  	
 	
 		  	
 	
 		  	
 	
 		   	
 	
 		 $( 	
 	
  0

40@/CD	
 	
 	
 	
 	
 	
r/   N)r   r   r   r   r  r  r  r   r/   r-   r  r    s    M


r/   r  c                  (    e Zd ZdZddZddZddZy)&TestWB3_EpochRunnerPhasesAreSequentialz>WB3: EpochRunner calls phases in the correct sequential order.c                   g fd}fd}t               }||j                  _        t               }||j                  _        t	        |||      }t        |j                  d             j                  }d} ||      }	j                  }
d} |
|      }|	|k  }|sWt        j                  d|fd|	|f      d	t        j                         v st        j                        rt        j                        nd	t        j                  |      t        j                  |      t        j                  |	      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}	x}x}
x}}y)zIPhase 1 (conversation_aggregate) must run before Phase 3 (axiom_distill).c                 B    j                  d       t        ddd      S )Nr   r0   rY   r   )r^   r_   r`   )appendr   r  r   
call_orders     r-   track_aggregatezgTestWB3_EpochRunnerPhasesAreSequential.test_aggregator_called_before_distiller.<locals>.track_aggregate
  s%    k*, aa r/   c                 R    j                  d       t        t        d      d      S Nr   r0   okrc   r  r   rB   r  s     r-   track_distillzeTestWB3_EpochRunnerPhasesAreSequential.test_aggregator_called_before_distiller.<locals>.track_distill  $    i(%-=a-@tTTr/   )rw   ry   rq   r   r   rW  z%(py6)s
{%(py6)s = %(py2)s
{%(py2)s = %(py0)s.index
}(%(py4)s)
} < %(py14)s
{%(py14)s = %(py10)s
{%(py10)s = %(py8)s.index
}(%(py12)s)
}r  r   r   r   r   r   r   py12py14z3Aggregation must precede distillation. Call order: 
>assert %(py16)spy16N)r
   r   r   r   r   r.   r   indexr   r   r   r   r   r   r   r   r   )r   r   r  r  rw   ry   rL   r   r   r   @py_assert9@py_assert11@py_assert13r   @py_format15@py_format17r  s                   @r-   'test_aggregator_called_before_distillerzNTestWB3_EpochRunnerPhasesAreSequential.test_aggregator_called_before_distiller  s   
		U [
+:
(K	(5	%(y
 	V012 	
 	
, 	
z/?/? 	
	 	
/?	/J 	
,/JJ 	
 	
 	
,/J 	
 	
	6	
 	
   	
 	
 		  	
 	
 		   	
 	
 		 !, 	
 	
 		 - 	
 	
	6	
 	
  0: 	
 	
 		 0: 	
 	
 		 0@ 	
 	
 		 AJ 	
 	
 		 0K 	
 	
  B*N	
 	
 	
 	
 	
 	
 	
r/   c                   g fd}fd}t               }||j                  _        t               }||j                  _        t	        |||      }t        |j                  d             j                  }d} ||      }	j                  }
d} |
|      }|	|k  }|sWt        j                  d|fd|	|f      d	t        j                         v st        j                        rt        j                        nd	t        j                  |      t        j                  |      t        j                  |	      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}	x}x}
x}}y)zAPhase 3 (distill) must complete before Phase 4 (knowledge_write).c                 R    j                  d       t        t        d      d      S r  r  r  s     r-   r  zkTestWB3_EpochRunnerPhasesAreSequential.test_distiller_called_before_knowledge_writer.<locals>.track_distill'  r  r/   c                 J    j                  d       t        t        dd      S )Nr   r0   rf   )r  r   r   r  s     r-   track_writeziTestWB3_EpochRunnerPhasesAreSequential.test_distiller_called_before_knowledge_writer.<locals>.track_write+  s!    g&LZ[\\r/   )ry   rz   rq   r   r   rW  r  r  r  z7Distillation must precede knowledge write. Call order: r  r  N)r
   r   r   r   r   r.   r   r  r   r   r   r   r   r   r   r   r   )r   r   r  r  ry   rz   rL   r   r   r   r  r  r  r   r  r  r  s                   @r-   -test_distiller_called_before_knowledge_writerzTTestWB3_EpochRunnerPhasesAreSequential.test_distiller_called_before_knowledge_writer#  s   
	U	] K	(5	%$;-8*(	<L
 	V012 	
	 	
	* 	
Z-=-= 	
g 	
-=g-F 	
*-FF 	
 	
 	
*-F 	
 	
	6	
 	
   	
 	
 		  	
 	
 		   	
 	
 		 !* 	
 	
 		 + 	
 	
	6	
 	
  .8 	
 	
 		 .8 	
 	
 		 .> 	
 	
 		 ?F 	
 	
 		 .G 	
 	
  Fj\R	
 	
 	
 	
 	
 	
 	
r/   c                   g fd}fd}t               }||j                  _        t               }||j                  _        t	        ||      }t        |j                  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 rj                   }d} ||      }j                   }d} ||      }||k  }|sWt        j                  d|fd||f      dt        j                         v st        j                        rt        j                        ndt        j                  |      t        j                  |      t        j                  |      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}x}x}x}}yy)z2EpochReportGenerator.generate() is the final step.c                 D    j                  d       t        dddd      S )Ntier1r0   r   rk   )r  r#   r  s     r-   track_tier1z`TestWB3_EpochRunnerPhasesAreSequential.test_report_generator_is_called_last.<locals>.track_tier1B  s(    g&#"#!)*1 r/   c                 Z    j                  d       t        dt        dz        d      S )Nrx  rp   	report.mdrq   rr   )r  r    r   )r  r   r  r   s     r-   track_reportzaTestWB3_EpochRunnerPhasesAreSequential.test_report_generator_is_called_last.<locals>.track_reportI  s1    h'!<h45+ r/   )r   r   rq   rx  r@  rB  r  rC  z*report_generator.generate() must be calledr   r   Nr  rW  r  r  z9Tier1 update must precede report generation. Call order: r  r  )r
   r   r   r   r   r.   r   r   r   r   r   r   r   r   r   r   r  )r   r   r  r  r   r   rL   r
  r   r   r   r   r   r   r  r  r  r   r  r  r  s    `                  @r-   $test_report_generator_is_called_lastzKTestWB3_EpochRunnerPhasesAreSequential.test_report_generator_is_called_last>  sj   
		 "*5'$;0<!!-('-

 	V012Sx:%SSSx:SSSxSSSSSS:SSS:SSSS'SSSSSSSj ## G #G, z/?/?  /?/I ,/II   ,/I  v     I   I $  I %,  I -  v   0:  I 0:  I 0@  I AI  I 0J    LJ<X       !r/   NrF  )r   r   r   r   r  r  r  r   r/   r-   r  r    s    H
:
6$r/   r  c                  (    e Zd ZdZddZddZddZy)"TestWB4_LockReleasedInFinallyBlockzCWB4: run_epoch_safe() releases the lock even when run_epoch raises.c                    t               }d|j                  _        d|j                  _        t	        ||      }t        |j                                |j                  j                          y)z4Lock is released after a normal epoch run completes.TNrv   )r
   r   rJ   r   r   r.   rK   r   r   r   	mock_lockrL   s       r-   'test_lock_released_after_successful_runzJTestWB4_LockReleasedInFinallyBlock.test_lock_released_after_successful_runm  sT    K	)-	&)-	&(	BV""$%,,.r/   c                   t               }d|j                  _        t               }t        d      |j                  _        t        |||      }t        |j                               }|j                  j                          y)zDLock is released even when run_epoch raises an unexpected exception.TzPostgres explodedrv   rw   N)r
   r   rJ   RuntimeErrorr   r   r   r.   rK   r   r   )r   r   r  mock_aggregatorrL   r   s         r-   (test_lock_released_when_run_epoch_raiseszKTestWB4_LockReleasedInFinallyBlock.test_lock_released_when_run_epoch_raisesx  sn    K	)-	& $+0<=P0Q!!-(9

 f++-. 	,,.r/   c                   t               }d|j                  _        t        ||      }t	        |j                               }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}}y)zJIf the lock cannot be acquired, run_epoch_safe() returns None immediately.Fr  Nr   r   r   r   zArun_epoch_safe() must return None when lock is not acquired, got r   r   r
   r   rJ   r   r.   rK   r   r   r   r   r   r   r   r   r   	r   r   r  rL   r   r   r   r   r   s	            r-   7test_run_epoch_safe_returns_none_when_lock_not_acquiredzZTestWB4_LockReleasedInFinallyBlock.test_run_epoch_safe_returns_none_when_lock_not_acquired  s    K	).	&(	Bf++-. 	
v~ 	
 	
v 	
 	
 
6	
 	
   	
 	
 
	  	
 	
 
	  	
 	
 *	
 	
 	
 	
 	
r/   NrF  )r   r   r   r   r  r  r  r   r/   r-   r  r  j  s    M	//&
r/   r  c                       e Zd ZdZddZddZy)+TestWB5_RunEpochSafeReturnsNoneWhenLockHeldzHWB5: run_epoch_safe() returns None immediately if lock already acquired.c                   t               }d|j                  _        t               }t        |||      }t	        |j                               }d}||u }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }dd	|iz  }	t        t        j                  |	            dx}}|j                  j                          y)
zKLock held by another process: run_epoch_safe() returns None, no epoch runs.Fr  Nr   r   r   r   r   r   )r
   r   rJ   r   r.   rK   r   r   r   r   r   r   r   r   r   r   )
r   r   r  r  rL   r   r   r   r   r   s
             r-    test_returns_none_when_lock_heldzLTestWB5_RunEpochSafeReturnsNoneWhenLockHeld.test_returns_none_when_lock_held  s    K	).	&#+(9
 f++-.v~vvv!!335r/   c                   t               }d|j                  _        t        ||      }t	        |j                               }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}}y)z?When lock IS acquired, run_epoch_safe() returns an EpochResult.Tr  N)is not)z%(py0)s is not %(py3)sr   r   z3run_epoch_safe() must return EpochResult on successr   r   r  r  s	            r-   ,test_returns_epoch_result_when_lock_acquiredzXTestWB5_RunEpochSafeReturnsNoneWhenLockHeld.test_returns_epoch_result_when_lock_acquired  s    K	)-	&(	Bf++-.!XvT!XXXvTXXXXXXvXXXvXXXTXXX#XXXXXXXr/   NrF  )r   r   r   r   r  r  r   r/   r-   r  r    s    R6 Yr/   r  c                  &    e Zd ZdZddZddZd Zy)TestWB6_EpochLogWrittenToJSONLz>WB6: EpochRunner writes an epoch log entry to epoch_log.jsonl.c                   t        |dz        }t        |      }||_        t        |j	                  d             t        |      }|j                  } |       }|st        j                  d|       dz   dt        j                         v st        j                  t
              rt        j                  t
              nddt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      t        j                  |      dz  }t        t        j                  |            dx}x}}t        |      j                         j!                         D cg c]  }|j#                         s| }	}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t%        |	             dz   d|iz  }t        t        j                  |            dx}x}}
yc c}w )z;After run_epoch(), an entry is appended to epoch_log.jsonl.rX   rq   zEpoch log not created at za
>assert %(py7)s
{%(py7)s = %(py5)s
{%(py5)s = %(py3)s
{%(py3)s = %(py0)s(%(py1)s)
}.exists
}()
}r   r   )r   r  r   r   r   Nr0   r   r(  r  r)  r*  zExpected 1 log entry, got r+  r   )r   r   r   r.   r   r   r  r   r   r   r   r   r   r   r   r,  r-  r.  r  r   )r   r   r   rL   r   r   r	  r   r1  r)  r   r  r   s                r-    test_epoch_log_written_after_runz?TestWB6_EpochLogWrittenToJSONL.test_epoch_log_written_after_run  s   #445	(2 )V012IP%%P%'P'PP+DYK)PPPPPPPtPPPtPPPPPPIPPPIPPPPPP%PPP'PPPPPP O557BBDRq	RR5zIQIzQIIIzQIIIIIIsIIIsIIIIII5III5IIIzIIIQIII"<SZL IIIIIIII Ss   K$Kc                   t        |dz        }t        |      }||_        t        |j	                  d             t        j                  t        |      j                         j                               }|j                  }d} ||      }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                   |      dz  }
dd	|
iz  }t#        t        j$                  |            d
x}x}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}}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
)zBEpoch log entry contains epoch_id, phases_completed, axioms_count.rX   rq   ru   r   r  entryr   zassert %(py11)sr   Nrw  r@  rB  rC  r   r   axioms_countduration_seconds)r   r   r   r.   r   r   r9  r   r,  r.  r   r   r   r   r   r   r   r   r   )r   r   r   rL   r
  r   r   r   r   r   r   r   r
  r   r   r   s                   r-   (test_epoch_log_entry_has_expected_fieldszGTestWB6_EpochLogWrittenToJSONL.test_epoch_log_entry_has_expected_fields  s   #445	(2 )V012

4	?446<<>?yy::y$:(::$(:::::$(:::::::u:::u:::y::::::$:::(::::::::!*!U****!U***!******U***U*******&~&&&&~&&&~&&&&&&&&&&&&&&&&!*!U****!U***!******U***U*******r/   c                L   d}|t         v }|st        j                  d|fd|t         f      t        j                  |      dt	        j
                         v st        j                  t               rt        j                  t               nddz  }t        j                  dt               dz   d|iz  }t        t        j                  |            d	x}}d
}|t         v }|st        j                  d|fd|t         f      t        j                  |      dt	        j
                         v st        j                  t               rt        j                  t               nddz  }t        j                  dt               dz   d|iz  }t        t        j                  |            d	x}}y	)zAEPOCH_LOG_PATH must reference data/observability/epoch_log.jsonl.rX   r@  rB  r   rC  z6EPOCH_LOG_PATH should contain 'epoch_log.jsonl', got: r   r   Nobservabilityz9EPOCH_LOG_PATH should be under data/observability/, got: )
r   r   r   r   r   r   r   r   r   r   rD  s        r-   test_epoch_log_path_constantz;TestWB6_EpochLogWrittenToJSONL.test_epoch_log_path_constant  sN     	
 N2 	
 	
 N 	
 	
 		 ! 	
 	
	6	
 	
  %3 	
 	
 		 %3 	
 	
  E^DVW	
 	
 	
 	
 	
  	
.0 	
 	
. 	
 	
 		  	
 	
	6	
 	
  #1 	
 	
 		 #1 	
 	
  HGYZ	
 	
 	
 	
 	
r/   NrF  )r   r   r   r   r  r  r  r   r/   r-   r  r    s    HJ+
r/   r  c                  (    e Zd ZdZddZddZddZy)'TestWB7_ReportFilePathFollowsConventionzGWB7: EpochReportGenerator uses epoch_{epoch_id}.md filename convention.c                8   t        t        |            }t        d      }|j                  |      }t        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                  |      dz  }t        j                  d	|      d
z   d|iz  }	t        t        j                   |	            dx}}y)uH   Report file is named epoch_{epoch_id}.md — not 'report.md' or similar.rr  rt  r"  r  r   r   filenamer   z$Expected 'epoch_2026_02_25.md', got r   r   N)r   r   r   r   ospathbasenamert   r   r   r   r   r   r   r   r   r   )
r   r   r|  r   rx  r  r   r   r   r   s
             r-   'test_report_filename_is_epoch_id_dot_mdzOTestWB7_ReportFilePathFollowsConvention.test_report_filename_is_epoch_id_dot_md  s    (CMB	l3##F+77##F$4$450 	
x00 	
 	
x0 	
 	
	6	
 	
   	
 	
 		  	
 	
 		 1 	
 	
  38,?	
 	
 	
 	
 	
r/   c                
   t        t        |            }|j                  t        d            }|j                  t        d            }|j                  }|j                  }||k7  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      d	z  }t        j                  d
      dz   d|iz  }	t        t        j                  |	            dx}x}}d}
|j                  }|
|v }|st        j                  d|fd|
|f      t        j                  |
      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }dd|iz  }t        t        j                  |            dx}
x}}d}
|j                  }|
|v }|st        j                  d|fd|
|f      t        j                  |
      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }dd|iz  }t        t        j                  |            dx}
x}}y)z6Different epoch IDs must generate distinct file names.rr  
2026_02_24r"  rt  )!=)zP%(py2)s
{%(py2)s = %(py0)s.file_path
} != %(py6)s
{%(py6)s = %(py4)s.file_path
}r1r2rY  z5Different epoch IDs must produce different file pathsr+  r   Nzepoch_2026_02_24.mdr@  )z1%(py1)s in %(py5)s
{%(py5)s = %(py3)s.file_path
}ry  r   r   r  )r   r   r   r   rt   r   r   r   r   r   r   r   r   r   )r   r   r|  r  r  r   r   r   r  r   r
  r   r   r   r   s                  r-   4test_different_epoch_ids_produce_different_filenamesz\TestWB7_ReportFilePathFollowsConvention.test_different_epoch_ids_produce_different_filenames  s   (CMB	\ BC\ BC|| 	
r|| 	
||+ 	
 	
 	
|| 	
 	
	6	
 	
   	
 	
 		  	
 	
 		  	
 	
	6	
 	
   " 	
 	
 		  " 	
 	
 		  , 	
 	
  D	
 	
 	
 	
 	
 %44$4444$444$4444444444444444444$44$444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z  dz        }t        |      }t        j                  }|j                  } ||      }| }|sddt        j                         v st        j                  t              rt        j                  t              ndt        j                  |      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }t        t        j                  |            dx}x}x}}|j                  t        d	
             t        j                  }|j                  } ||      }|st        j                  d|       dz   dt        j                         v st        j                  t              rt        j                  t              ndt        j                  |      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }	t        t        j                  |	            dx}x}}y)zGEpochReportGenerator creates the report directory if it does not exist.deepreportsrr  zeassert not %(py7)s
{%(py7)s = %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.path
}.isdir
}(%(py5)s)
}r  
nested_dir)r   r   r   r   r   Nrt  r"  z-Report directory should have been created at zc
>assert %(py7)s
{%(py7)s = %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.path
}.isdir
}(%(py5)s)
})r   r   r  r  isdirr   r   r   r   r   r   r   r   r   r   )
r   r   r"  r|  r   r   r	  r   r   r   s
             r-   %test_report_dir_is_created_if_missingzMTestWB7_ReportFilePathFollowsConvention.test_report_dir_is_created_if_missing
  s   F*Y67
(J?	 77,7==,=,,,,,,,,,,,2,,,2,,,7,,,=,,,,,,,,,,,,,,,,,,,;=>ww 	
w}} 	
}Z( 	
( 	
  <J<H	
 	
	6	
 	
   	
 	
 		  	
 	
 		  	
 	
 		  	
 	
	6	
 	
  ( 	
 	
 		 ( 	
 	
 		 ) 	
 	
 	
 	
 	
 	
r/   NrF  )r   r   r   r   r  r  r$  r   r/   r-   r  r    s    Q

5
r/   r  c                  0    e Zd ZdZddZddZddZddZy)!TestIntegration_FullEpochPipelinezVIntegration-style tests verifying the complete epoch pipeline end-to-end (all mocked).c                   t        |      }t        |j                  d            }d}t        ||      }|sddt	        j
                         v st        j                  t              rt        j                  t              nddt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }t        t        j                  |            dx}}|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                  }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}}
y)z9run_epoch() returns an EpochResult with populated fields.rq   ru   z5assert %(py5)s
{%(py5)s = %(py0)s(%(py1)s, %(py3)s)
}hasattrr   )r   r  r   r   Nr   )z0%(py2)s
{%(py2)s = %(py0)s.epoch_id
} == %(py5)sr   r   r   z[assert %(py6)s
{%(py6)s = %(py0)s(%(py3)s
{%(py3)s = %(py1)s.phases_completed
}, %(py4)s)
}rP  r  rV  zQassert %(py6)s
{%(py6)s = %(py0)s(%(py3)s
{%(py3)s = %(py1)s.axioms
}, %(py4)s)
})r   r.   r   r(  r   r   r   r   r   r   r   ru   r   rw  rP  r  rd   )r   r   rL   r   r   r   r   r   r   r   r   r  s               r-   (test_full_epoch_run_returns_epoch_resultzJTestIntegration_FullEpochPipeline.test_full_epoch_run_returns_epoch_result!  sH   (2f&&'9:;)*wvz********w***w******v***v***z**********4"44"44444"4444444v444v444444"44444444 118z1488888888z888z888888&888&8881888888488848888888888 --.z-........z...z......&...&...-...................r/   c                   t        |      }t        |j                  d            }h d}t        |j                        }||z
  }| }|st        j                  d| d| d|       dz   ddt        j                         v st        j                  |      rt        j                  |      ndiz  }t        t        j                  |            d	}y	)
z=A healthy epoch run must complete at least the core 4 phases.rq   >   tier1_updaterv  r  ru  z Epoch must complete core phases z. Missing: z. Completed: z
>assert not %(py0)sr   missingN)r   r.   r   r   rw  r   r   r   r   r   r   r   r   )	r   r   rL   r   core_phases	completedr,  r   @py_format2s	            r-   (test_full_epoch_completes_minimum_phaseszJTestIntegration_FullEpochPipeline.test_full_epoch_completes_minimum_phases+  s    (2f&&'9:;
 //0		){ 	
{ 	
  /{m <yi[:	
 	
 
6	
 	
   	
 	
 
	  	
 	
 	
 	
 	
r/   c                    t               }d|j                  _        t        ||      }t	        |j                                |j                  j                          |j                  j                          y)zDrun_epoch_safe() acquires lock before running and releases it after.Tr  N)r
   r   rJ   r   r.   rK   r   r   r  s       r-   .test_run_epoch_safe_acquires_and_releases_lockzPTestIntegration_FullEpochPipeline.test_run_epoch_safe_acquires_and_releases_lock=  sX    K	)-	&(	BV""$%,,.,,.r/   c                    t               }t        dt        |dz        d      |j                  _        t        ||      }t        |j                  d             |j                  j                          y)z?After run_epoch(), report_generator.generate() has been called.rp   r  rq   rr   )r   N)	r
   r    r   r   rJ   r   r.   r   r   )r   r   mock_report_genrL   s       r-   #test_epoch_report_written_after_runzETestIntegration_FullEpochPipeline.test_epoch_report_written_after_runH  sc    #+0;8([01'1
  - )OTV012  335r/   NrF  )r   r   r   r   r)  r0  r2  r5  r   r/   r-   r&  r&    s    `/
$	/6r/   r&  c                  @    e Zd ZdZd Zd Zd Zd Zd Zd Z	d Z
d	 Zy
)TestPackageInitExportsz=Verify that core.epoch __init__ exports all expected symbols.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 )
Nr   )r   r   z%(py0)s is %(py2)sESr   r7  assert %(py4)sr   )

core.epochr   r   r   r   r   r   r   r   r   )r   r:  r   r;  r0  s        r-   test_epoch_scheduler_exportedz4TestPackageInitExports.test_epoch_scheduler_exported_  m    3^####r^######r###r######^###^#######r/   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 )
Nr   )r   r   r9  ERr   r7  r;  r   )
r<  r   r   r   r   r   r   r   r   r   )r   r@  r   r;  r0  s        r-   test_epoch_runner_exportedz1TestPackageInitExports.test_epoch_runner_exportedc  sm    0[    r[      r   r      [   [       r/   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 )
Nr   )r   r   r9  RELr   r7  r;  r   )
r<  r   r   r   r   r   r   r   r   r   )r   rC  r   r;  r0  s        r-   test_redis_epoch_lock_exportedz5TestPackageInitExports.test_redis_epoch_lock_exportedg  sm    4n$$$$sn$$$$$$s$$$s$$$$$$n$$$n$$$$$$$r/   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 )
Nr   )r   r   r9  CAr   r7  r;  r   )
r<  r   r   r   r   r   r   r   r   r   )r   rF  r   r;  r0  s        r-   %test_conversation_aggregator_exportedz<TestPackageInitExports.test_conversation_aggregator_exportedk  sq    ;+++++r+++++++r+++r++++++++++++++++++r/   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 )
Nr   )r   r   r9  ADr   r7  r;  r   )
r<  r   r   r   r   r   r   r   r   r   )r   rI  r   r;  r0  s        r-   test_axiom_distiller_exportedz4TestPackageInitExports.test_axiom_distiller_exportedo  r>  r/   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 )
Nr   )r   r   r9  EKWr   r7  r;  r   )
r<  r   r   r   r   r   r   r   r   r   )r   rL  r   r;  r0  s        r-   $test_epoch_knowledge_writer_exportedz;TestPackageInitExports.test_epoch_knowledge_writer_exporteds  q    :*****s*******s***s******************r/   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 )
Nr   )r   r   r9  ERGr   r7  r;  r   )
r<  r   r   r   r   r   r   r   r   r   )r   rP  r   r;  r0  s        r-   $test_epoch_report_generator_exportedz;TestPackageInitExports.test_epoch_report_generator_exportedw  rN  r/   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 )
Nr   )r"   r   r9  ETTr"   r7  r;  r   )
r<  r"   r   r   r   r   r   r   r   r   )r   rS  r   r;  r0  s        r-   !test_epoch_tier1_trigger_exportedz8TestPackageInitExports.test_epoch_tier1_trigger_exported{  sq    7'''''s'''''''s'''s''''''''''''''''''r/   N)r   r   r   r   r=  rA  rD  rG  rJ  rM  rQ  rT  r   r/   r-   r7  r7  \  s-    G$!%,$++(r/   r7  __main__c                 <    t        t        j                               S N)r   tempfilemkdtempr   r/   r-   _tmprZ    s    H$$&''r/   z'BB1: force_trigger calls run_epoch_safez%BB1: force_trigger calls safe not rawz&BB1: force_trigger works without startz!BB2: second acquire returns Falsez&BB2: acquire passes nx and ex to redisz*BB2: acquire without redis always succeedszBB2: EPOCH_LOCK_KEY constantz&BB3: low confidence axiom filtered outzBB3: boundary 0.6 acceptedz&BB3: all below threshold returns emptyz BB3: CONFIDENCE_THRESHOLD is 0.6zBB4: axioms written to KG filec                 D    t               j                  t                     S rW  )r  r2  rZ  r   r/   r-   <lambda>r\    s$    3]3_3~3~  @D  @F  4G r/   zBB4: each KG line has axiom idc                 D    t               j                  t                     S rW  )r  r<  rZ  r   r/   r-   r\  r\    s*    3]3_  4N  4N  OS  OU  4V r/   z&BB4: empty axioms returns zero entriesc                 D    t               j                  t                     S rW  )r  r>  rZ  r   r/   r-   r\  r\    s*    ;e;g  <Z  <Z  [_  [a  <b r/   zBB4: KG_FILE_PATH constantz!BB5: no db returns zeroed summaryz,BB5: summary has period_start and period_endzBB5: aggregate with mocked dbzBB5: sensitive data scrubbedz,BB6: report has Genesis Nightly Epoch headerc                 D    t               j                  t                     S rW  )rp  r~  rZ  r   r/   r-   r\  r\    s-    AmAo  B]  B]  ^b  ^d  Be r/   z/BB6: report file follows epoch_id.md conventionc                 D    t               j                  t                     S rW  )rp  r  rZ  r   r/   r-   r\  r\    s-    DpDr  E`  E`  ae  ag  Eh r/   zBB6: report is written to diskc                 D    t               j                  t                     S rW  )rp  r  rZ  r   r/   r-   r\  r\    s*    3_3a  4A  4A  BF  BH  4I r/   z+BB6: report includes phases and axiom countc                 D    t               j                  t                     S rW  )rp  r  rZ  r   r/   r-   r\  r\    s-    @l@n  A[  A[  \`  \b  Ac r/   z$BB7: knowledge_writer.write() calledc                 D    t               j                  t                     S rW  )r  r  rZ  r   r/   r-   r\  r\    s*    9g9i  :P  :P  QU  QW  :X r/   z'BB7: tier1_updater.apply_tier1() calledc                 D    t               j                  t                     S rW  )r  r  rZ  r   r/   r-   r\  r\    s*    <j<l  =V  =V  W[  W]  =^ r/   z BB7: both called in same apply()c                 D    t               j                  t                     S rW  )r  r  rZ  r   r/   r-   r\  r\    s*    5c5e  6P  6P  QU  QW  6X r/   z WB1: default model is gemini-prozWB1: model is not flashz%WB1: gemini client called with promptzWB1: no client returns emptyzWB2: EPOCH_LOCK_TTL is 7200z(WB2: acquire uses epoch:lock:nightly keyz WB2: acquire ex param equals TTLz WB3: aggregator before distillerc                 D    t               j                  t                     S rW  )r  r  rZ  r   r/   r-   r\  r\    s*    5[5]  6F  6F  GK  GM  6N r/   z&WB3: distiller before knowledge_writerc                 D    t               j                  t                     S rW  )r  r  rZ  r   r/   r-   r\  r\    s*    ;a;c  <R  <R  SW  SY  <Z r/   zWB3: report generator is lastc                 D    t               j                  t                     S rW  )r  r  rZ  r   r/   r-   r\  r\    s$    2X2Z22  AE  AG  3H r/   z WB4: lock released after successc                 D    t               j                  t                     S rW  )r  r  rZ  r   r/   r-   r\  r\    s*    5W5Y  6B  6B  CG  CI  6J r/   z(WB4: lock released when run_epoch raisesc                 D    t               j                  t                     S rW  )r  r  rZ  r   r/   r-   r\  r\    s*    =_=a  >K  >K  LP  LR  >S r/   z(WB4: returns None when lock not acquiredc                 D    t               j                  t                     S rW  )r  r  rZ  r   r/   r-   r\  r\    s*    =_=a  >Z  >Z  [_  [a  >b r/   z WB5: returns None when lock heldc                 D    t               j                  t                     S rW  )r  r  rZ  r   r/   r-   r\  r\    s*    5`5b  6D  6D  EI  EK  6L r/   z+WB5: returns EpochResult when lock acquiredc                 D    t               j                  t                     S rW  )r  r  rZ  r   r/   r-   r\  r\    s-    @k@m  A[  A[  \`  \b  Ac r/   z WB6: epoch log written after runc                 D    t               j                  t                     S rW  )r  r  rZ  r   r/   r-   r\  r\    s    5S5U5v5vw{w}5~ r/   z"WB6: epoch log has expected fieldsc                 D    t               j                  t                     S rW  )r  r  rZ  r   r/   r-   r\  r\    s*    7U7W  8A  8A  BF  BH  8I r/   zWB6: EPOCH_LOG_PATH constantz#WB7: report filename is epoch_id.mdc                 D    t               j                  t                     S rW  )r  r  rZ  r   r/   r-   r\  r\    s*    8_8a  9J  9J  KO  KQ  9R r/   z.WB7: different epoch IDs = different filenamesc                 D    t               j                  t                     S rW  )r  r  rZ  r   r/   r-   r\  r\    s-    CjCl  Db  Db  cg  ci  Dj r/   z"WB7: report dir created if missingc                 D    t               j                  t                     S rW  )r  r$  rZ  r   r/   r-   r\  r\    s*    7^7`  8G  8G  HL  HN  8O r/   z#INT: full epoch returns EpochResultc                 D    t               j                  t                     S rW  )r&  r)  rZ  r   r/   r-   r\  r\    s*    8Y8[  9E  9E  FJ  FL  9M r/   z$INT: full epoch completes min phasesc                 D    t               j                  t                     S rW  )r&  r0  rZ  r   r/   r-   r\  r\    s*    9Z9\  :F  :F  GK  GM  :N r/   z.INT: run_epoch_safe acquires and releases lockc                 D    t               j                  t                     S rW  )r&  r2  rZ  r   r/   r-   r\  r\    s-    CdCf  DV  DV  W[  W]  D^ r/   z#INT: epoch report written after runc                 D    t               j                  t                     S rW  )r&  r5  rZ  r   r/   r-   r\  r\    s$    8Y8[88  AE  AG  9H r/   zPKG: EpochScheduler exportedzPKG: EpochRunner exportedzPKG: RedisEpochLock exportedz$PKG: ConversationAggregator exportedzPKG: AxiomDistiller exportedz"PKG: EpochKnowledgeWriter exportedz"PKG: EpochReportGenerator exportedzPKG: EpochTier1Trigger exportedz	  [PASS] z	  [FAIL] z: 
z<============================================================u'   Story 9.09 — Nightly Epoch Unit Tests/z tests passed (z failed)zALL TESTS PASSED)r0   333333?r  )r<   intr:   floatr8   r   r  r   )r!  ry  )r@   rz  r:   r{  r  zList[Axiom])rj   )rF   r   r  r&   )NNNNNNNNNNNrF  )r  r   )r   
__future__r   builtinsr   _pytest.assertion.rewrite	assertionrewriter   r)   r   r  sysrX  r   r   pathlibr   typingr   r   unittest.mockr	   r
   r   r   pytestGENESIS_ROOTr  insertcore.epoch.epoch_schedulerr   r   core.epoch.epoch_runnerr   r   core.epoch.redis_epoch_lockr   r   r   "core.epoch.conversation_aggregatorr   r   core.epoch.axiom_distillerr   r   r   r   !core.epoch.epoch_knowledge_writerr   r   r   r   !core.epoch.epoch_report_generatorr   r   r    r!   core.epoch.epoch_tier1_triggerr"   r#   r$   core.evolution.meta_architectr&   'core.evolution.tier1_autonomous_updaterr'   _APSCHEDULER_PATCHr.   r=   rB   rG   rM   rV   r   r   r   r   r  rH  rp  r  r  r  r  r  r  r  r  r&  r7  r   	tracebackrZ  'test_force_trigger_calls_run_epoch_safer   r   r   r   r   r   r  r  r  r  rE  rT  r\  rf  rn  r  r  r  r  r  r  r  r  r=  rA  rD  rG  rJ  rM  rQ  rT  	all_testspassedfailedr  totalnamefnprint	Exceptionexc	print_excexitr   r/   r-   <module>r     s		  )V #     	 
  '  ! ; ;  'sxxHHOOA|$ G ? 
    
 ? ? G =
TS( 
]]J3 3N+
 +
f7
 7
~.
 .
lD
 D
X<C <CH>8 >8L-) -)j"
 "
T_ _N,
 ,
hY YF%
 %
Z(
 (
`66 66|!( !(P z(L	24Y4[  5D  5D  	EL 
12W2Y  3P  3P  	QL 
23X3Z  4B  4B  	C	L 
-.U.W.y.yzL 
23Z3\  4D  4D  	EL 
67^7`  8L  8L  	ML 
()P)R)o)opL 
23]3_  4G  4G  	HL 
&'Q'S'y'yzL 
23]3_  4G  4G  	HL 
,-W-Y  .D  .D  	EL  
*  ,G  	H!L" 
*  ,V  	W#L$ 
2  4b  	c%L& 
&'Q'S'n'no'L* 
-.`.b  /E  /E  	F+L, 
89k9m  :T  :T  	U-L. 
)*\*^*|*|}/L0 
()[)]  *I  *I  	J1L4 
8  :e  	f5L6 
;  =h  	i7L8 
*  ,I  	J9L: 
7  9c  	d;L> 
0  2X  	Y?L@ 
3  5^  	_ALB 
,  .X  	YCLF 
,-Q-S-t-tuGLH 
#$H$J$g$ghILJ 
12V2X2~2~KLL 
()M)O)s)stMLP 
'(B(D(c(cdQLR 
45O5Q5z5z{SLT 
,-G-I-j-jkULX 
,  .N  	OYLZ 
2  4Z  	[[L\ 
)  +H  	I]L` 
,  .J  	KaLb 
4  6S  	TcLd 
4  6b  	ceLh 
,  .L  	MiLj 
7  9c  	dkLn 
,-~oLp 
.  0I  	JqLr 
()G)I)f)fgsLv 
/  1R  	SwLx 
:  <j  	kyLz 
.  0O  	P{L~ 
/  1M  	NL@ 
0  2N  	OALB 
:  <^  	_CLD 
/  1H  	IELH 
()?)A)_)_`ILJ 
%&<&>&Y&YZKLL 
()?)A)`)`aMLN 
01G1I1o1opOLP 
()?)A)_)_`QLR 
./E/G/l/lmSLT 
./E/G/l/lmULV 
+,B,D,f,fgWLI\ FF	NE b	DIdV$%aKF	 
Bvh-	35	VHAeWOF88
<=	VH{ !S z  	IdV2cU+,I!aKF	s   /TU%UU