
    iPb                       d Z ddlmZ ddlZddlmc mZ ddl	Z	e	j                  j                  dd       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mZmZ ddlZddlmZmZ ddlmZmZ dd	lmZ dRd
ZdRdZ dRdSdZ! G d d      Z" G d d      Z# G d d      Z$ G d d      Z% G d d      Z& G d d      Z' G d d      Z( G d d      Z) G d d      Z* G d d       Z+e,d!k(  rWddl-Z-g d" e"       j\                  fd# e"       j^                  fd$ e"       j`                  fd% e"       jb                  fd& e"       jd                  fd' e#       jf                  fd( e#       jh                  fd) e#       jj                  fd* e#       jl                  fd+ e#       jn                  fd, e$       jp                  fd- e$       jr                  fd. e$       jt                  fd/ e$       jv                  fd0 e$       jx                  fd1 e$       jz                  fd2 e%       j|                  fd3 e%       j~                  fd4 e%       j                  fd5 e%       j                  fd6 e%       j                  fd7 e&       j                  fd8 e&       j                  fd9 e&       j                  fd: e&       j                  fd; e'       j                  fd< e'       j                  fd= e(       j                  fd> e(       j                  fd? e(       j                  fd@ e(       j                  fdA e(       j                  fdB e(       j                  fdC e)       j                  fdD e)       j                  fdE e*       j                  fdF e*       j                  fdG e+       j                  fdH e+       j                  fdI e+       j                  fZVdZWdZXeVD ]  \  ZYZZ	  eZ         e[dJeY        eWdKz  ZW  e[dNeW dOeWeXz    dP       eXdk(  r	 e[dQ       y e	j                  dK       yy# e\$ r.Z] e[dLeY dMe]         e-j                          eXdKz  ZXY dZ][]dZ][]ww xY w)Tuz  
Tests for Story 5.04 (Track B): SwarmSagaWriter — Saga Lifecycle Recorder

Black Box tests (BB): verify the public contract from the caller's perspective —
    open_saga creates RUNNING saga, record_proposed_delta appends correctly,
    close_saga writes terminal status, invalid status raises ValueError.

White Box tests (WB): verify internals — open_saga returns UUID4, each delta
    entry has submitted_at ISO timestamp, record_proposed_delta issues a
    SQL-level append (not read-modify-write).

ALL tests use mocks — NO real Postgres connection is required.

Story: 5.04
File under test: core/storage/saga_writer.py
    )annotationsNz/mnt/e/genesis-system)datetimetimezone)	MagicMockcallpatch)SwarmSagaWriter_VALID_CLOSE_STATUSES)
ColdLedger	SwarmSaga)r	   c                J   t               }t               }t        |      |j                  j                  _        t        d      |j                  j                  _        | |j
                  _        ||ng |j                  _        t               }||j                  _        |||fS )zIReturn (mock_pool, mock_conn, mock_cursor) wired for full mock operation.return_valueF)r   cursorr   	__enter____exit__fetchonefetchallgetconn)fetchone_returnfetchall_return	mock_connmock_cursor	mock_pools        6/mnt/e/genesis-system/tests/track_b/test_story_5_04.py_make_pool_and_connr   .   s    I+K.7[.QI!!+-6E-JI!!*(7K%;J;V\^K%I%.I"i,,    c           	         t        | |      \  }}}t        d|      5  t        dddddd      }d	d	d	       |_        ||_        ||_        |S # 1 sw Y    xY w)
z1Return a ColdLedger wired to a fully-mocked pool.z$psycopg2.pool.ThreadedConnectionPoolr   	localhosti8  upgenesis)hostportuserpassworddbnameN)r   r   r   
_mock_pool
_mock_conn_mock_cursor)r   r   r   r   r   ledgers         r   _make_ledgerr,   ?   so    (;Hh(O%Iy+	5I	N 
 $	3


 "F!F%FM
 
s   AAc                :    t        | |      }t        |      }||fS )z?Return (SwarmSagaWriter, ColdLedger) pair with a mocked ledger.)r   r   )r,   r	   )r   r   r+   writers       r   _make_writerr/   M   s"    8h?FV$F6>r   c                  .    e Zd ZdZd Zd Zd Zd Zd Zy)#TestBB1OpenSagaCreatesRunningRecordzJBB1: open_saga writes a saga with status='RUNNING' and proposed_deltas=[].c                    t               \  }}t        j                  |dd      5 }|j                  dddi       ddd       j	                          y# 1 sw Y   xY w)z3open_saga must call ledger.write_saga exactly once.
write_sagazfake-idr   session-abcstep   N)r/   r   object	open_sagaassert_called_once)selfr.   r+   
mock_writes       r   test_write_saga_calledz:TestBB1OpenSagaCreatesRunningRecord.test_write_saga_called\   sT    %\\&,YG 	9:]VQK8	9%%'	9 	9s   AAc                  
 t               \  }}i 

fd}t        j                  |d|      5  |j                  dddi       ddd       
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# 1 sw Y   xY w)z>The SwarmSaga passed to write_saga must have status='RUNNING'.c                &    | d<   | j                   S Nsagasaga_idr@   captureds    r   capturezYTestBB1OpenSagaCreatesRunningRecord.test_written_saga_has_running_status.<locals>.captureh       #HV<<r   r3   side_effectr4   r5   r6   Nr@   RUNNING==)z.%(py3)s
{%(py3)s = %(py1)s.status
} == %(py6)spy1py3py6assert %(py8)spy8)
r/   r   r7   r8   status
@pytest_ar_call_reprcompare	_safereprAssertionError_format_explanationr:   r.   r+   rE   @py_assert0@py_assert2@py_assert5@py_assert4@py_format7@py_format9rD   s             @r   $test_written_saga_has_running_statuszHTestBB1OpenSagaCreatesRunningRecord.test_written_saga_has_running_statusc   s    %	  \\&,GD 	9]VQK8	9 3&&3)3&)3333&)333333&333)3333333	9 	9   C..C7c                  
 t               \  }}i 

fd}t        j                  |d|      5  |j                  dddi       ddd       
d   }|j                  }g }||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# 1 sw Y   xY w)z@The SwarmSaga passed to write_saga must have proposed_deltas=[].c                &    | d<   | j                   S r?   rA   rC   s    r   rE   z`TestBB1OpenSagaCreatesRunningRecord.test_written_saga_has_empty_proposed_deltas.<locals>.capturev   rF   r   r3   rG   r4   r5   r6   Nr@   rJ   )z7%(py3)s
{%(py3)s = %(py1)s.proposed_deltas
} == %(py6)srL   rP   rQ   )
r/   r   r7   r8   proposed_deltasrS   rT   rU   rV   rW   rX   s             @r   +test_written_saga_has_empty_proposed_deltaszOTestBB1OpenSagaCreatesRunningRecord.test_written_saga_has_empty_proposed_deltasq   s    %	  \\&,GD 	9]VQK8	9 5//525/25555/2555555/55525555555	9 	9r`   c                  
 t               \  }}i 

fd}t        j                  |d|      5  |j                  dddi       ddd       
d   }|j                  }d}||u }|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# 1 sw Y   xY w)z4A freshly opened saga must have resolved_state=None.c                &    | d<   | j                   S r?   rA   rC   s    r   rE   z^TestBB1OpenSagaCreatesRunningRecord.test_written_saga_has_none_resolved_state.<locals>.capture   rF   r   r3   rG   r4   r5   r6   Nr@   is)z6%(py3)s
{%(py3)s = %(py1)s.resolved_state
} is %(py6)srL   rP   rQ   )
r/   r   r7   r8   resolved_staterS   rT   rU   rV   rW   rX   s             @r   )test_written_saga_has_none_resolved_statezMTestBB1OpenSagaCreatesRunningRecord.test_written_saga_has_none_resolved_state   s    %	  \\&,GD 	9]VQK8	9 6..6$6.$6666.$666666.666$6666666	9 	9s   C--C6c                V  
 t               \  }}ddgddggd}i 

fd}t        j                  |d|      5  |j                  d|       ddd       
d	   }|j                  }||k(  }|st        j                  d
|fd||f      t        j                  |      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}x}}y# 1 sw Y   xY w)z<The orchestrator_dag argument must be persisted on the saga.ab)nodesedgesc                &    | d<   | j                   S r?   rA   rC   s    r   rE   z[TestBB1OpenSagaCreatesRunningRecord.test_orchestrator_dag_stored_correctly.<locals>.capture   rF   r   r3   rG   r4   Nr@   rJ   )z8%(py3)s
{%(py3)s = %(py1)s.orchestrator_dag
} == %(py5)sdagrM   rN   py5assert %(py7)spy7)r/   r   r7   r8   orchestrator_dagrS   rT   rU   @py_builtinslocals_should_repr_global_namerV   rW   )r:   r.   r+   rq   rE   rY   rZ   r\   @py_format6@py_format8rD   s             @r   &test_orchestrator_dag_stored_correctlyzJTestBB1OpenSagaCreatesRunningRecord.test_orchestrator_dag_stored_correctly   s    %c
sCj\:	  \\&,GD 	1]C0	1 70070C77770C7777770777777C777C7777777	1 	1s   DD(N)	__name__
__module____qualname____doc__r<   r_   rd   rj   r|    r   r   r1   r1   Y   s    T(4678r   r1   c                  .    e Zd ZdZd Zd Zd Zd Zd Zy)TestBB2RecordProposedDeltazHBB2: record_proposed_delta appends delta with agent_id and submitted_at.c                0   t               \  }}t        t        j                               }ddi}|j	                  |d|       |j
                  j                  j                  }|d   \  }}t        j                  |d         }t        |      }	d}
|	|
k(  }|st        j                  d|fd|	|
f      dt        j                         v st        j                  t              rt        j                   t              ndd	t        j                         v st        j                  |      rt        j                   |      nd	t        j                   |	      t        j                   |
      d
z  }dd|iz  }t#        t        j$                  |            dx}	x}}
|d   }|d   }d}||k(  }	|	slt        j                  d|	fd||f      t        j                   |      t        j                   |      dz  }dd|iz  }t#        t        j$                  |            dx}x}	}y)zEThe SQL executed must embed the correct agent_id in the JSON element.keyvaluezagent-forger   r6   rJ   )z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py6)slenelement_list)py0rM   rN   rO   rP   rQ   Nagent_idz%(py1)s == %(py4)srM   py4assert %(py6)srO   )r/   struuiduuid4record_proposed_deltar*   execute	call_argsjsonloadsr   rS   rT   rw   rx   ry   rU   rV   rW   )r:   r.   r+   rB   deltar   sqlparamsr   rZ   r[   r\   r]   r^   elementrY   @py_assert3@py_format5s                     r   )test_delta_appended_with_correct_agent_idzDTestBB2RecordProposedDelta.test_delta_appended_with_correct_agent_id   s\   %djjl# $$WmUC ''//99	lVzz&),< %A% A%%%% A%%%%%%s%%%s%%%%%%<%%%<%%% %%%A%%%%%%%q/z"3m3"m3333"m333"333m3333333r   c                \   t               \  }}ddd}|j                  t        t        j                               d|       |j
                  j                  j                  }|d   \  }}t        j                  |d         }|d   d   }||k(  }	|	st        j                  d|	fd||f      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      ndd	z  }
d
d|
iz  }t!        t        j"                  |            dx}}	y)z7The delta dict must be stored as-is inside the element.add_nodeX)mutationnodeagent-1r   r   rJ   z%(py1)s == %(py3)srM   rN   assert %(py5)srs   N)r/   r   r   r   r   r*   r   r   r   r   rS   rT   rU   rw   rx   ry   rV   rW   )r:   r.   r+   r   r   r   r   r   rY   rZ   @py_format4rz   s               r   test_delta_dict_stored_verbatimz:TestBB2RecordProposedDelta.test_delta_dict_stored_verbatim   s    %'5$$S%6	5I''//99	lVzz&),Aw'0'50000'5000'000000500050000000r   c                   t               \  }}|j                  t        t        j                               dddi       |j
                  j                  j                  }|d   \  }}t        j                  |d         }d}|d   }||v }	|	slt        j                  d|	fd||f      t        j                  |      t        j                  |      dz  }
d	d
|
iz  }t        t        j                  |            dx}x}	}y)z3Each delta element must carry a submitted_at field.zagent-xvr6   r   submitted_atin)z%(py1)s in %(py4)sr   r   rO   N)r/   r   r   r   r   r*   r   r   r   r   rS   rT   rU   rV   rW   )r:   r.   r+   r   r   r   r   rY   r   rZ   r   r]   s               r   $test_submitted_at_present_in_elementz?TestBB2RecordProposedDelta.test_submitted_at_present_in_element   s    %$$S%6	C8L''//99	lVzz&),0a0~0000~000~0000000000r   c                    t               \  }}t        t        j                               }|j	                  |di        |j
                  j                  j                  }|d   \  }}|d   }||k(  }|st        j                  d|fd||f      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      nddz  }	dd	|	iz  }
t        t        j                  |
            d
x}}y
)z@The saga_id must be the second SQL parameter (for WHERE clause).zagent-yr   r6   rJ   r   rB   r   r   rs   N)r/   r   r   r   r   r*   r   r   rS   rT   rU   rw   rx   ry   rV   rW   )r:   r.   r+   rB   r   r   r   rY   rZ   r   rz   s              r   #test_saga_id_passed_as_second_paramz>TestBB2RecordProposedDelta.test_saga_id_passed_as_second_param   s    %djjl#$$Wi<''//99	lVay#yG####yG###y######G###G#######r   c                    t               \  }}|j                  t        t        j                               dddi       |j
                  j                  j                          y)z4conn.commit() must be called after the delta append.zagent-zrl   r6   N)r/   r   r   r   r   r)   commitassert_calledr:   r.   r+   s      r   test_commit_called_after_appendz:TestBB2RecordProposedDelta.test_commit_called_after_append   sF    %$$S%6	C8L  ..0r   N)	r}   r~   r   r   r   r   r   r   r   r   r   r   r   r      s    R4"
1	1	$1r   r   c                  4    e Zd ZdZd Zd Zd Zd Zd Zd Z	y)	TestBB3CloseSagaz>BB3: close_saga writes the resolved_state and terminal status.c                ~    t               \  }}|j                  t        t        j                               ddid       y)z2close_saga with status='COMPLETED' must not raise.finalT	COMPLETEDNr/   
close_sagar   r   r   r   s      r   test_completed_status_acceptedz/TestBB3CloseSaga.test_completed_status_accepted   s.    %#djjl+gt_kJr   c                z    t               \  }}|j                  t        t        j                               i d       y)z5close_saga with status='PARTIAL_FAIL' must not raise.PARTIAL_FAILNr   r   s      r   !test_partial_fail_status_acceptedz2TestBB3CloseSaga.test_partial_fail_status_accepted   s*    %#djjl+R@r   c                z    t               \  }}|j                  t        t        j                               i d       y)z/close_saga with status='FAILED' must not raise.FAILEDNr   r   s      r   test_failed_status_acceptedz,TestBB3CloseSaga.test_failed_status_accepted   s*    %#djjl+R:r   c                   t               \  }}ddd}|j                  t        t        j                               |d       |j
                  j                  j                  }|d   \  }}t        j                  |d         }||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  }	dd|	iz  }
t!        t        j"                  |
            d}y)zBresolved_state must be serialised to JSON and passed as SQL param.   ok)agents_donesummaryr   r   rJ   )z%(py0)s == %(py2)sparsedresolvedr   py2assert %(py4)sr   N)r/   r   r   r   r   r*   r   r   r   r   rS   rT   rw   rx   ry   rU   rV   rW   )r:   r.   r+   r   r   r   r   r   @py_assert1@py_format3r   s              r   (test_resolved_state_passed_as_json_paramz9TestBB3CloseSaga.test_resolved_state_passed_as_json_param   s    %#$6#djjl+X{C''//99	lVF1I&!!!!v!!!!!!v!!!v!!!!!!!!!!!!!!!!r   c                   t               \  }}|j                  t        t        j                               i d       |j
                  j                  j                  }|d   \  }}|d   }d}||k(  }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }	dd|	iz  }
t        t        j                  |
            d	x}x}}y	)
z3The status string must be the second SQL parameter.r   r   r6   rJ   r   r   r   rO   N)r/   r   r   r   r   r*   r   r   rS   rT   rU   rV   rW   )r:   r.   r+   r   r   r   rY   r   rZ   r   r]   s              r   &test_status_passed_as_second_sql_paramz7TestBB3CloseSaga.test_status_passed_as_second_sql_param   s    %#djjl+R=''//99	lVay'K'yK''''yK'''y'''K'''''''r   c                    t               \  }}|j                  t        t        j                               i d       |j
                  j                  j                          y)z4conn.commit() must be called after the close update.r   N)r/   r   r   r   r   r)   r   r   r   s      r   test_commit_called_after_closez/TestBB3CloseSaga.test_commit_called_after_close  sB    %#djjl+R=  ..0r   N)
r}   r~   r   r   r   r   r   r   r   r   r   r   r   r   r      s%    HKA
;
"(1r   r   c                  .    e Zd ZdZd Zd Zd Zd Zd Zy)$TestBB4InvalidStatusRaisesValueErrorz=BB4: close_saga with an invalid status must raise ValueError.c                    t               \  }}t        j                  t              5  |j	                  t        t        j                               i d       d d d        y # 1 sw Y   y xY w)NINVALIDr/   pytestraises
ValueErrorr   r   r   r   r   s      r   "test_bad_status_raises_value_errorzGTestBB4InvalidStatusRaisesValueError.test_bad_status_raises_value_error  sO    %]]:& 	@c$**,/Y?	@ 	@ 	@   /AA(c                    t               \  }}t        j                  t              5  |j	                  t        t        j                               i d       ddd       y# 1 sw Y   yxY w)z>'RUNNING' is a valid open status but NOT a valid close status.rI   Nr   r   s      r   'test_running_status_not_valid_for_closezLTestBB4InvalidStatusRaisesValueError.test_running_status_not_valid_for_close  sO    %]]:& 	@c$**,/Y?	@ 	@ 	@r   c                    t               \  }}t        j                  t              5  |j	                  t        t        j                               i d       d d d        y # 1 sw Y   y xY w)N r   r   s      r   test_empty_string_status_raiseszDTestBB4InvalidStatusRaisesValueError.test_empty_string_status_raises  sK    %]]:& 	9c$**,/R8	9 	9 	9r   c                    t               \  }}t        j                  t              5  |j	                  t        t        j                               i d       ddd       y# 1 sw Y   yxY w)u4   Status is case-sensitive — 'completed' must raise.	completedNr   r   s      r   test_lowercase_completed_raiseszDTestBB4InvalidStatusRaisesValueError.test_lowercase_completed_raises  sO    %]]:& 	Bc$**,/[A	B 	B 	Br   c                    t               \  }}t        j                  t        d      5  |j	                  t        t        j                               i d       ddd       y# 1 sw Y   yxY w)z5ValueError message should mention the valid statuses.zCOMPLETED|PARTIAL_FAIL|FAILED)matchNOPENr   r   s      r   *test_error_message_contains_valid_statuseszOTestBB4InvalidStatusRaisesValueError.test_error_message_contains_valid_statuses$  sN    %]]:-LM 	=c$**,/V<	= 	= 	=s   /A!!A*N)	r}   r~   r   r   r   r   r   r   r   r   r   r   r   r     s!    G@
@9
B=r   r   c                  (    e Zd ZdZd Zd Zd Zd Zy)TestWB1OpenSagaReturnsUUIDz<WB1: open_saga returns a UUID4 string (not None, not empty).c                   t               \  }}|j                  di       }t        |t              }|sddt	        j
                         v st        j                  t              rt        j                  t              nddt	        j
                         v st        j                  |      rt        j                  |      nddt	        j
                         v st        j                  t              rt        j                  t              ndt        j                  |      dz  }t        t        j                  |            d }y )N	session-15assert %(py4)s
{%(py4)s = %(py0)s(%(py1)s, %(py2)s)
}
isinstanceresultr   r   rM   r   r   )r/   r8   r   r   rw   rx   rS   ry   rU   rV   rW   )r:   r.   r+   r   r   r   s         r   test_returns_stringz.TestWB1OpenSagaReturnsUUID.test_returns_string3  s    %!!+r2&#&&&&&&&&z&&&z&&&&&&&&&&&&&&&&&#&&&#&&&&&&&&&&r   c                   t               \  }}|j                  di       }t        j                  |d      }t	        |      }||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                  |      dt        j                         v st        j                  |      rt        j                  |      ndd	z  }d
d|iz  }t        t        j                  |            d x}}y )Nr      )versionrJ   )z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py5)sr   r   r   )r   rM   rN   rs   rt   ru   )r/   r8   r   UUIDr   rS   rT   rw   rx   ry   rU   rV   rW   )	r:   r.   r+   r   r   rZ   r\   rz   r{   s	            r   test_returns_valid_uuid4z3TestWB1OpenSagaReturnsUUID.test_returns_valid_uuid48  s    %!!+r261-6{${f$$$${f$$$$$$s$$$s$$$$$$6$$$6$$${$$$$$$f$$$f$$$$$$$r   c                   t               \  }}|j                  di       }|j                  di       }||k7  }|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  }dd|iz  }t        t        j                  |            d	}y	)
z3Each open_saga call must generate a unique saga_id.r   )!=)z%(py0)s != %(py2)sid1id2r   r   r   N)
r/   r8   rS   rT   rw   rx   ry   rU   rV   rW   )r:   r.   r+   r   r   r   r   r   s           r   #test_two_opens_return_different_idsz>TestWB1OpenSagaReturnsUUID.test_two_opens_return_different_ids>  s    %{B/{B/czscssccr   c                P   t               \  }}t        d      5 }t        j                  d      }||_        |j                  di       }ddd       t              }|k(  }|s#t        j                  d|fd||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	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                          y# 1 sw Y   UxY w)u:   open_saga must use uuid.uuid4() — not uuid1/uuid3/uuid5.z#core.storage.saga_writer.uuid.uuid4z$abcdef12-abcd-4bcd-8bcd-abcdef123456r   NrJ   )z0%(py0)s == %(py5)s
{%(py5)s = %(py2)s(%(py3)s)
}r   r   fixed)r   r   rN   rs   rt   ru   )r/   r   r   r   r   r8   r   rS   rT   rw   rx   ry   rU   rV   rW   r9   )
r:   r.   r+   
mock_uuid4r   r   r\   r   rz   r{   s
             r   test_uuid4_is_usedz-TestWB1OpenSagaReturnsUUID.test_uuid4_is_usedE  s    %89 	7ZIIDEE&+J#%%k26F	7 U#v####v######v###v###############U###U##########%%'	7 	7s   /FF%N)r}   r~   r   r   r   r   r   r  r   r   r   r   r   0  s    F'
%(r   r   c                      e Zd ZdZd Zd Zy) TestWB2SubmittedAtIsISOTimestampzJWB2: each delta element must have a valid ISO 8601 submitted_at timestamp.c                x   t               \  }}|j                  t        t        j                               dddi       |j
                  j                  j                  }|d   \  }}t        j                  |d         d   }|d   }t        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d	t        j                         v st        j                   t              rt        j"                  t              nd	t        j"                  |	      d
z  }
t%        t        j&                  |
            d }	y )Nzagent-tsxr6   r   r   r   r   r   r   r   )r/   r   r   r   r   r*   r   r   r   r   r   fromisoformatr   rw   rx   rS   ry   rU   rV   rW   )r:   r.   r+   r   r   r   r   r   r   r   r   s              r   test_submitted_at_is_iso_stringz@TestWB2SubmittedAtIsISOTimestamp.test_submitted_at_is_iso_stringS  s   %$$S%6
S!HM''//99	lV**VAY'*~. ''5&(++++++++z+++z++++++&+++&++++++(+++(++++++++++r   c                f   t               \  }}|j                  t        t        j                               di        |j
                  j                  j                  }|d   \  }}t        j                  |d         d   }|d   }g }d}	|	|v }
|
}|
s|j                  }d} ||      }|}|syt        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  }|j#                  |       |
sddt        j                         v st        j                   |      rt        j                  |      ndt        j                        t        j                        t        j                        dz  }|j#                  |       t        j$                  |d      i z  }t        j&                  d|      dz   d|iz  }t)        t        j*                  |            dx}x}x}	x}
x}x}}y)z*submitted_at must be timezone-aware (UTC).zagent-tzr   r   +Zr   )z%(py3)s in %(py5)s)rN   rs   z%(py7)sru   zH%(py15)s
{%(py15)s = %(py11)s
{%(py11)s = %(py9)s.endswith
}(%(py13)s)
})py9py11py13py15r6   z0submitted_at must contain timezone offset, got: z
>assert %(py18)spy18N)r/   r   r   r   r   r*   r   r   r   r   endswithrS   rT   rU   rw   rx   ry   append_format_boolop_format_assertmsgrV   rW   )r:   r.   r+   r   r   r   r   r   r   rZ   r\   rY   @py_assert10@py_assert12@py_assert14rz   r{   @py_format16@py_format17@py_format19s                       r   (test_submitted_at_includes_timezone_infozITestWB2SubmittedAtIsISOTimestamp.test_submitted_at_includes_timezone_info`  s   %$$S%6
BG''//99	lV**VAY'*~.	
s 	
sl" 	
l&;&; 	
C 	
&;C&@ 	
&@ 	
 	
 	
sl 	
 	
 		  	
 	
	6	
 	
  # 	
 	
 		 # 	
 	
 	
	6	
		
 	
	6	
 	
  '3 	
 	
 		 '3 	
 	
 		 '< 	
 	
 		 =@ 	
 	
 		 'A 	
 	
	6	
		
 	
 	
  ?|>NO	
 	
 	
 	
 	
 	
 	
r   N)r}   r~   r   r   r  r  r   r   r   r  r  P  s    T,
r   r  c                  4    e Zd ZdZd Zd Zd Zd Zd Zd Z	y)	'TestWB3SQLLevelAppendNorReadModifyWritezQWB3: record_proposed_delta must use SQL JSONB append (||), not read-modify-write.c                   t               \  }}|j                  t        t        j                               dddi       |j
                  }|j                  }|j                  }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }dd	|iz  }	t        t        j                  |	            d
x}x}x}x}}y
)uO   record_proposed_delta must issue exactly one SQL statement — no SELECT first.r   r   r6   rJ   )zq%(py6)s
{%(py6)s = %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s._mock_cursor
}.execute
}.call_count
} == %(py9)sr+   )r   r   r   rO   r  zassert %(py11)sr  N)r/   r   r   r   r   r*   r   
call_countrS   rT   rw   rx   ry   rU   rV   rW   )
r:   r.   r+   r   r   r[   @py_assert8@py_assert7@py_format10@py_format12s
             r   %test_only_one_execute_call_for_appendzMTestWB3SQLLevelAppendNorReadModifyWrite.test_only_one_execute_call_for_appends  s    %$$S%6	C8L"":"**:*55::5::::5::::::v:::v:::":::*:::5:::::::::::r   c                F   t               \  }}|j                  t        t        j                               di        |j
                  j                  j                  }|d   d   }d}||v }|st        j                  d|fd||f      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      nddz  }t        j                  d|z         d	z   d
|iz  }t        t        j                   |            dx}}y)z9The SQL must use the || operator for JSONB concatenation.z	agent-sqlr   z||r   z%(py1)s in %(py3)sr   r   z=record_proposed_delta SQL must use || for JSONB append, got: 
>assert %(py5)srs   N)r/   r   r   r   r   r*   r   r   rS   rT   rU   rw   rx   ry   r  rV   rW   )	r:   r.   r+   r   r   rY   rZ   r   rz   s	            r   'test_sql_contains_jsonb_concat_operatorzOTestWB3SQLLevelAppendNorReadModifyWrite.test_sql_contains_jsonb_concat_operatorz  s    %$$S%6RH''//99	l1o 	
ts{ 	
 	
ts 	
 	
 		  	
 	
	6	
 	
   	
 	
 		  	
 	
  LcQ	
 	
 	
 	
 	
r   c                   t               \  }}|j                  t        t        j                               di        |j
                  j                  j                  }|d   d   j                         }|j                  }d} ||      }|st        j                  d      dz   dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      t        j                  |      dz  }t!        t        j"                  |            dx}x}}d	}	|	|v}
|
st        j$                  d
|
fd|	|f      t        j                  |	      dt        j                         v st        j                  |      rt        j                  |      nddz  }t        j                  d      dz   d|iz  }t!        t        j"                  |            dx}	}
y)u>   The SQL must be UPDATE — no SELECT in record_proposed_delta.zagent-ur   UPDATEz8record_proposed_delta must use UPDATE, not SELECT+UPDATEzN
>assert %(py6)s
{%(py6)s = %(py2)s
{%(py2)s = %(py0)s.startswith
}(%(py4)s)
}r   )r   r   r   rO   NSELECTnot in)z%(py1)s not in %(py3)sr   uN   No SELECT allowed in record_proposed_delta — that would be read-modify-writer&  rs   )r/   r   r   r   r   r*   r   r   upper
startswithrS   r  rw   rx   ry   rU   rV   rW   rT   )r:   r.   r+   r   r   r   r   r[   r]   rY   rZ   r   rz   s                r   )test_sql_is_update_not_select_then_updatezQTestWB3SQLLevelAppendNorReadModifyWrite.test_sql_is_update_not_select_then_update  s   %$$S%6	2F''//99	l1o##%~~ 	
h 	
~h' 	
' 	
  G	
 	
	6	
 	
   	
 	
 		  	
 	
 		  	
 	
 		 ' 	
 	
 		 ( 	
 	
 	
 	
 	
  	
xs" 	
 	
xs 	
 	
 		  	
 	
	6	
 	
   # 	
 	
 		  # 	
 	
  ]	
 	
 	
 	
 	
r   c                    t               \  }}t        j                  |d      5 }|j                  t	        t        j                               di        ddd       j                          y# 1 sw Y   xY w)zKrecord_proposed_delta must NOT call ledger.get_saga (no read-before-write).get_sagazagent-vN)r/   r   r7   r   r   r   r   assert_not_called)r:   r.   r+   mock_gets       r   %test_no_get_saga_called_during_appendzMTestWB3SQLLevelAppendNorReadModifyWrite.test_no_get_saga_called_during_append  s_    %\\&*- 	K((TZZ\):IrJ	K""$	K 	Ks   /A,,A5c                (   t               \  }}t        t        j                               }|j	                  |dddi       |j
                  j                  j                          |j
                  j                  j                  |j                         y)uJ   record_proposed_delta must use ledger pool directly — getconn + putconn.z
agent-poolmr6   N)r/   r   r   r   r   r(   r   r9   putconnassert_called_once_withr)   )r:   r.   r+   rB   s       r   )test_connection_pool_getconn_putconn_usedzQTestWB3SQLLevelAppendNorReadModifyWrite.test_connection_pool_getconn_putconn_used  sk    %djjl#$$WlS!HE!!446!!99&:K:KLr   c                |   t               \  }}t        d      |j                  j                  _        t        j                  t              5  |j                  t        t        j                               di        ddd       |j                  j                  j                  |j                         y# 1 sw Y   9xY w)z9putconn must be called in finally even if execute raises.zDB downz	agent-errN)r/   RuntimeErrorr*   r   rH   r   r   r   r   r   r   r(   r7  r8  r)   r   s      r   ,test_putconn_called_even_when_execute_raiseszTTestWB3SQLLevelAppendNorReadModifyWrite.test_putconn_called_even_when_execute_raises  s    %2>y2I##/]]<( 	M((TZZ\):KL	M 	!!99&:K:KL	M 	Ms   /B22B;N)
r}   r~   r   r   r#  r'  r/  r4  r9  r<  r   r   r   r  r  p  s%    [;	

%MMr   r  c                      e Zd ZdZd Zd Zy)TestGetSagaDelegationz4get_saga must delegate cleanly to ledger.get_saga().c                   t               \  }}t        t        j                               }t	        |t        t        j                               i g d dt        ddd            }t        j                  |d|      5 }|j                  |      }d d d        j                  |       |u }|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  }dd|iz  }	t!        t        j"                  |	            d }y # 1 sw Y   xY w)NrI   i        )rB   
session_idrv   rc   ri   rR   
created_atr1  r   rg   z%(py0)s is %(py2)sr   expectedr   r   r   )r/   r   r   r   r   r   r   r7   r1  r8  rS   rT   rw   rx   ry   rU   rV   rW   )
r:   r.   r+   rB   rE  r3  r   r   r   r   s
             r   test_delegates_to_ledgerz.TestGetSagaDelegation.test_delegates_to_ledger  s    %djjl#DJJL(9	a,	
 \\&*8D 	.__W-F	.((1!!!!v!!!!!!v!!!v!!!!!!!!!!!!!!!!	. 	.s   ;E55E>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 )	N)r   z$00000000-0000-0000-0000-000000000000rg   )z%(py0)s is %(py3)sr   )r   rN   r   rs   )
r/   r1  rS   rT   rw   rx   ry   rU   rV   rW   )r:   r.   r+   r   rZ   r   r   rz   s           r   "test_returns_none_for_unknown_sagaz8TestGetSagaDelegation.test_returns_none_for_unknown_saga  sw    %t4!GHv~vvvr   N)r}   r~   r   r   rF  rH  r   r   r   r>  r>    s    >"r   r>  c                      e Zd ZdZd Zd Zy)TestPackageExportz>SwarmSagaWriter must be importable directly from core.storage.c                   t         t        u }|st        j                  d|fdt         t        f      dt	        j
                         v st        j                  t               rt        j                  t               nddt	        j
                         v st        j                  t              rt        j                  t              nddz  }dd|iz  }t        t        j                  |            d }y )Nrg   rD  SwarmSagaWriterFromPackager	   r   r   r   )
rL  r	   rS   rT   rw   rx   ry   rU   rV   rW   )r:   r   r   r   s       r   test_importable_from_packagez.TestPackageExport.test_importable_from_package  sm    )_<<<<)_<<<<<<)<<<)<<<<<<_<<<_<<<<<<<r   c                Z   ddl m} d}||v }|st        j                  d|fd||f      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      nddz  }dd	|iz  }t        t        j                  |            d x}}y )
Nr   )__all__r	   r   r%  rO  r   r   rs   )
core.storagerO  rS   rT   rU   rw   rx   ry   rV   rW   )r:   rO  rY   rZ   r   rz   s         r   test_in_dunder_allz$TestPackageExport.test_in_dunder_all  sa    ( + G++++ G+++ ++++++G+++G+++++++r   N)r}   r~   r   r   rM  rQ  r   r   r   rJ  rJ    s    H=,r   rJ  c                  b    e Zd ZdZ ej
                  d      j                         Zd Zd Z	d Z
y)TestSourceCodeQualityz<saga_writer.py must follow Genesis hardwired code standards.z1/mnt/e/genesis-system/core/storage/saga_writer.pyc                   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      dz   d|iz  }t        t        j                  |            d x}x}}y )	Nzimport sqlite3r+  z3%(py1)s not in %(py5)s
{%(py5)s = %(py3)s._SOURCE
}r:   rr   u9   saga_writer.py must NOT import sqlite3 — Genesis Rule 7
>assert %(py7)sru   
_SOURCErS   rT   rU   rw   rx   ry   r  rV   rW   r:   rY   r\   rZ   rz   r{   s         r   test_no_sqlite3_importz,TestSourceCodeQuality.test_no_sqlite3_import  s     	
t|| 	
|3 	
 	
| 	
 	
 		   	
 	
	6	
 	
  (, 	
 	
 		 (, 	
 	
 		 (4 	
 	
  H	
 	
 	
 	
 	
 	
r   c                N   t        j                  d| j                        }| }|s~t        j                  d|       dz   ddt        j                         v st        j                  |      rt        j                  |      ndiz  }t        t        j                  |            d }y )Nz4f["\'].*?(SELECT|INSERT|UPDATE|DELETE|WHERE).*?["\']z%Found f-string SQL (injection risk): z
>assert not %(py0)sr   fstring_sql)refindallrX  rS   r  rw   rx   ry   rU   rV   rW   )r:   r\  r   @py_format2s       r   test_no_fstring_sqlz)TestSourceCodeQuality.test_no_fstring_sql  s    jjCLL
  	
 	
  4K=A	
 	
	6	
 	
   	
 	
 		  	
 	
 	
 	
 	
r   c                L   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      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}}y )Nzuuid.uuid4()r   )z/%(py1)s in %(py5)s
{%(py5)s = %(py3)s._SOURCE
}r:   rr   z6open_saga must use uuid.uuid4() for saga_id generationrV  ru   zuuid.uuid1()r+  rU  rt   rW  rY  s         r   test_uuid4_used_not_uuid1z/TestSourceCodeQuality.test_uuid4_used_not_uuid1  s,    	
 	
~- 	
 	
~ 	
 	
 		  	
 	
	6	
 	
  "& 	
 	
 		 "& 	
 	
 		 ". 	
 	
  E	
 	
 	
 	
 	
 1T\\1~\1111~\111~111111T111T111\1111111r   N)r}   r~   r   r   pathlibPath	read_textrX  rZ  r`  rb  r   r   r   rS  rS    s.    FgllNOYY[G


2r   rS  __main__z BB1a: open_saga calls write_sagazBB1b: status=RUNNINGzBB1c: proposed_deltas=[]zBB1d: resolved_state=NonezBB1e: orchestrator_dag storedzBB2a: delta has agent_idz BB2b: delta dict stored verbatimzBB2c: submitted_at presentzBB2d: saga_id as second paramz BB2e: commit called after appendzBB3a: COMPLETED acceptedzBB3b: PARTIAL_FAIL acceptedzBB3c: FAILED acceptedz"BB3d: resolved_state as JSON paramzBB3e: status as second paramzBB3f: commit called after closeu   BB4a: INVALID → ValueErroru   BB4b: RUNNING → ValueErroru!   BB4c: empty string → ValueErroru   BB4d: lowercase → ValueErrorz)BB4e: error message mentions valid valueszWB1a: returns stringzWB1b: valid UUID4zWB1c: unique per callzWB1d: uuid4 used internallyz WB2a: submitted_at is ISO stringzWB2b: submitted_at has timezonezWB3a: one execute call onlyzWB3b: SQL uses || operatorzWB3c: UPDATE not SELECT+UPDATEzWB3d: no get_saga during appendzWB3e: pool getconn+putconn usedz"WB3f: putconn called even on errorzDEL1: delegates to ledgerzDEL2: None for unknown sagazPKG1: importable from packagezPKG2: in __all__zSRC1: no sqlite3 importzSRC2: no f-string SQLzSRC3: uuid4 not uuid1z	  [PASS] r6   z	  [FAIL] z: 
/z tests passedu:   ALL TESTS PASSED — Story 5.04 (Track B): SwarmSagaWriter)NN)returnz"tuple[SwarmSagaWriter, ColdLedger])`r   
__future__r   builtinsrw   _pytest.assertion.rewrite	assertionrewriterS   syspathinsertr   rc  r]  r   r   r   unittest.mockr   r   r   r   core.storage.saga_writerr	   r
   core.storage.cold_ledgerr   r   rP  rL  r   r,   r/   r1   r   r   r   r   r  r  r>  rJ  rS  r}   	tracebackr<   r_   rd   rj   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'  r/  r4  r9  r<  rF  rH  rM  rQ  rZ  r`  rb  testspassedfailednamefnprint	Exceptionexc	print_excexitr   r   r   <module>r     s  " #   
 * +   	  ' 0 0  L : F-"A8 A8H:1 :1z.1 .1b= =J( (@
 
@;M ;MF 8, , 2 2< z3	+-P-R-i-ij3 
 !D!F!k!kl3 
$%H%J%v%vw	3
 
%&I&K&u&uv3 
)*M*O*v*vw3 
$%?%A%k%kl3 
,-G-I-i-ij3 
&'A'C'h'hi3 
)*D*F*j*jk3 
,-G-I-i-ij3 
$%5%7%V%VW3 
'(8(:(\(\]3  
!"2"4"P"PQ!3" 
./?/A/j/jk#3$ 
()9);)b)bc%3& 
+,<,>,],]^'3* 
()M)O)r)rs+3, 
()M)O)w)wx-3. 
-.R.T.t.tu/30 
*+O+Q+q+qr132 
56Z6\  7H  7H  	I336 
 !;!=!Q!QR738 
8:SST93: 
!"<">"b"bc;3< 
'(B(D(W(WX=3@ 
,-M-O-o-opA3B 
+,L,N,w,wxC3F 
'(O(Q(w(wxG3H 
&'N'P'x'xyI3J 
*+R+T+~+~K3L 
+,S,U,{,{|M3N 
+,S,U,,  	AO3P 
./V/X  0F  0F  	GQ3T 
%&;&=&V&VWU3V 
'(=(?(b(bcW3Z 
)*;*=*Z*Z[[3\ 
.0CCD]3` 
#$9$;$R$RSa3b 
!"7"9"M"MNc3d 
!"7"9"S"STe3Ej FF b	DIdV$%aKF	 
Bvha(
67{JKQ ~  	IdV2cU+,I!aKF	s   4QQ4$Q//Q4