
    iR                    B   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mZ ddlmZmZ ddlZddlmZ ddlmZ dPdZdPd	Z	 	 	 	 	 dQ	 	 	 	 	 	 	 	 	 	 	 dRd
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$ G d d       Z% G d! d"      Z& G d# d$      Z'e(d%k(  rddl)Z)g d& e       jT                  fd' e       jV                  fd( e       jX                  fd) e       jZ                  fd* e       j\                  fd+ e       j^                  fd, e       j`                  fd- e       jb                  fd. e       jd                  fd/ e       jf                  fd0 e       jh                  fd1 e       jj                  fd2 e       jl                  fd3 e        jn                  fd4 e        jp                  fd5 e        jr                  fd6 e!       jt                  fd7 e!       jv                  fd8 e"       jx                  fd9 e"       jz                  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ZLdZMdZNeLD ]  \  ZOZP	  eP         eQdHeO        eMdIz  ZM  eQdLeM dMeMeNz    dN       eNdk(  r	 eQdO       y e	j                  dI       yy# eR$ r.ZS eQdJeO dKeS         e)j                          eNdIz  ZNY dZS[SdZS[Sww xY w)SuC  
Tests for Story 5.05 (Track B): SessionStore — Session Lifecycle Manager

Black Box tests (BB): verify the public contract from the caller's perspective —
    correct UUIDs returned, active-session filtering, close semantics, None on miss.
White Box tests (WB): verify internals — connection pool getconn/putconn pattern,
    UUID4 generation, parameterised SQL (no f-strings), no SQLite import,
    cleanup_orphaned_sessions WHERE clause shape.

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

Story: 5.05
File under test: core/storage/session_store.py
    )annotationsNz/mnt/e/genesis-system)datetime)	MagicMockpatch)SessionStorec                X   t               }| |j                  _        ||ng |j                  _        d|_        t               }t        |      |j
                  j                  _        t        d      |j
                  j                  _        t               }||j                  _        |||fS )us  
    Returns (mock_pool, mock_conn, mock_cursor) wired so that:
      - pool.getconn()  → conn
      - pool.putconn()  is tracked
      - conn.cursor()   supports the context-manager protocol
      - cursor.fetchone() → fetchone_return
      - cursor.fetchall() → fetchall_return (default [])
      - cursor.rowcount   → 0  (overridden per-test where needed)
    r   return_valueF)	r   fetchoner
   fetchallrowcountcursor	__enter____exit__getconn)fetchone_returnfetchall_returnmock_cursor	mock_conn	mock_pools        6/mnt/e/genesis-system/tests/track_b/test_story_5_05.py_make_pool_and_connr   *   s     +K(7K%*6B % KI.7[.QI!!+-6E-JI!!*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)
z3Return a SessionStore wired to a fully-mocked pool.$psycopg2.pool.ThreadedConnectionPoolr	   	localhost8  upgenesishostportuserpassworddbnameN)r   r   r   
_mock_pool
_mock_conn_mock_cursor)r   r   r   r   r   stores         r   _make_storer+   E   sp    (;Hh(O%Iy+	5I	N 
 $cY@

 !E E$EL
 
s   AAc           	     t    | xs t        t        j                               |xs t        dddddd      |||dS )Ni        
   r   )id
started_atended_atagent_idmetadata)struuiduuid4r   )
session_idr3   r1   r2   r4   s        r   _session_rowr9   X   sA     -C

- CHT1b"a$C r   c                  "    e Zd ZdZd Zd Zd Zy)TestBB1OpenSessionReturnsUUIDz/BB1: open_session returns a valid UUID4 string.c                z   t               }|j                  d      }t        |t              }|sddt	        j
                         v st        j                  t              rt        j                  t              nddt	        j
                         v st        j                  |      rt        j                  |      nddt	        j
                         v st        j                  t              rt        j                  t              ndt        j                  |      dz  }t        t        j                  |            d }y )Nforge-agent5assert %(py4)s
{%(py4)s = %(py0)s(%(py1)s, %(py2)s)
}
isinstanceresultr5   py0py1py2py4)r+   open_sessionr?   r5   @py_builtinslocals
@pytest_ar_should_repr_global_name	_safereprAssertionError_format_explanation)selfr*   r@   @py_assert3@py_format5s        r   test_returns_stringz1TestBB1OpenSessionReturnsUUID.test_returns_stringp   s    ##M2&#&&&&&&&&z&&&z&&&&&&&&&&&&&&&&&#&&&#&&&&&&&&&&r   c                   t               }|j                  d      }t        j                  |      }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=   ==)z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py5)sr5   parsedr@   rB   rC   py3py5assert %(py7)spy7)r+   rF   r6   UUIDr5   rI   _call_reprcomparerG   rH   rJ   rK   rL   rM   )rN   r*   r@   rU   @py_assert2@py_assert4@py_format6@py_format8s           r   !test_returned_value_is_valid_uuidz?TestBB1OpenSessionReturnsUUID.test_returned_value_is_valid_uuidu   s    ##M26"6{${f$$$${f$$$$$$s$$$s$$$$$$6$$$6$$${$$$$$$f$$$f$$$$$$$r   c                   t               }|j                  d      }|j                  d      }||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 )
Nzagent-azagent-b)!=)z%(py0)s != %(py2)sid1id2rB   rD   assert %(py4)srE   )
r+   rF   rI   r\   rG   rH   rJ   rK   rL   rM   )rN   r*   rd   re   @py_assert1@py_format3rP   s          r   %test_two_calls_return_different_uuidszCTestBB1OpenSessionReturnsUUID.test_two_calls_return_different_uuids{   s      +  +czscssccr   N)__name__
__module____qualname____doc__rQ   ra   rj    r   r   r;   r;   m   s    9'
%r   r;   c                      e Zd ZdZd Zd Zy)*TestBB2GetActiveSessionsIncludesNewSessionuE   BB2: open_session → get_active_sessions() includes the new session.c                   t        t        j                               }t        |d       }t	        |g      }|j                         }t        |      }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  t              rt        j                  t              nddt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }d	d
|iz  }	t        t        j                  |	            d x}x}}|d   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}
}|d   d   }
d }|
|u }|slt        j                  d|fd|
|f      t        j                  |
      t        j                  |      dz  }dd|iz  }t        t        j                  |            d x}
x}}y )N)r8   r2   r      rS   )z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py6)slensessionsrB   rC   rW   py6zassert %(py8)spy8r   r0   z%(py1)s == %(py3)sr8   rC   rW   assert %(py5)srX   r2   is)z%(py1)s is %(py4)srC   rE   assert %(py6)srx   )r5   r6   r7   r9   r+   get_active_sessionsru   rI   r\   rG   rH   rJ   rK   rL   rM   )rN   r8   rowr*   rv   r]   @py_assert5r^   @py_format7@py_format9@py_assert0@py_format4r_   rO   rP   s                  r   *test_active_sessions_contains_open_sessionzUTestBB2GetActiveSessionsIncludesNewSession.test_active_sessions_contains_open_session   s   &
j4@cU+,,.8}!!}!!!!}!!!!!!s!!!s!!!!!!8!!!8!!!}!!!!!!!!!!{4 . J.... J... ......J...J.......{:&.$.&$....&$...&...$.......r   c                   t        g       }|j                         }g }||k(  }|st        j                  d|fd||f      dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }dd|iz  }t        t        j                  |            d x}}y )Nrs   rS   z%(py0)s == %(py3)srv   rB   rW   r|   rX   
r+   r   rI   r\   rG   rH   rJ   rK   rL   rM   )rN   r*   rv   r]   rh   r   r_   s          r   *test_returns_empty_when_no_active_sessionszUTestBB2GetActiveSessionsIncludesNewSession.test_returns_empty_when_no_active_sessions   sr    R(,,.x2~x2xx2r   N)rk   rl   rm   rn   r   r   ro   r   r   rq   rq      s    O/r   rq   c                      e Zd ZdZd Zd Zy)$TestBB3CloseSessionRemovesFromActiveuJ   BB3: close_session → session no longer appears in get_active_sessions().c                   t        g       }|j                         }g }||k(  }|st        j                  d|fd||f      dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }dd|iz  }t        t        j                  |            d x}}y )Nrs   rS   r   activer   r|   rX   r   )rN   store_after_closer   r]   rh   r   r_   s          r   &test_closed_session_not_in_active_listzKTestBB3CloseSessionRemovesFromActive.test_closed_session_not_in_active_list   ss    '4"668v|vvvr   c                p    t               }|j                  t        t        j                                      y N)r+   close_sessionr5   r6   r7   rN   r*   s     r   !test_close_session_does_not_raisezFTestBB3CloseSessionRemovesFromActive.test_close_session_does_not_raise   s"    C

-.r   N)rk   rl   rm   rn   r   r   ro   r   r   r   r      s    T/r   r   c                  "    e Zd ZdZd Zd Zd Zy)%TestBB4GetSessionUnknownIdReturnsNoneu.   BB4: get_session(unknown_id) → returns None.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-000000000000r}   z%(py0)s is %(py3)sr@   r   r|   rX   )
r+   get_sessionrI   r\   rG   rH   rJ   rK   rL   rM   )rN   r*   r@   r]   rh   r   r_   s          r   test_unknown_id_returns_nonezBTestBB4GetSessionUnknownIdReturnsNone.test_unknown_id_returns_none   su    T*""#IJv~vvv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  }dd|iz  }t        t        j                  |            d x}}y # t        $ r"}t        j                  d|        Y d }~y d }~ww xY w)	Nr   r}   r   r@   r   r|   rX   z!get_session raised unexpectedly: )r+   r   r5   r6   r7   rI   r\   rG   rH   rJ   rK   rL   rM   	Exceptionpytestfail)rN   r*   r@   r]   rh   r   r_   excs           r   &test_does_not_raise_on_missing_sessionzLTestBB4GetSessionUnknownIdReturnsNone.test_does_not_raise_on_missing_session   s    T*	C&&s4::<'89F!!6T>!!!6T!!!!!!6!!!6!!!T!!!!!!! 	CKK;C5ABB	Cs   CC   	D)DDc                l   t        t        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  }dd|iz  }t        t        j                  |            d x}}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 }	|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 )N)r8   r   )is not)z%(py0)s is not %(py3)sr@   r   r|   rX   r>   r?   dictrA   r0   rS   rz   r8   r{   )r5   r6   r7   r9   r+   r   rI   r\   rG   rH   rJ   rK   rL   rM   r?   r   )rN   r8   r   r*   r@   r]   rh   r   r_   rO   rP   r   s               r   test_known_id_returns_dictz@TestBB4GetSessionUnknownIdReturnsNone.test_known_id_returns_dict   st   &
j1S)"":.!!vT!!!!vT!!!!!!v!!!v!!!T!!!!!!!&$''''''''z'''z''''''&'''&''''''$'''$''''''''''d|)|z))))|z)))|))))))z)))z)))))))r   N)rk   rl   rm   rn   r   r   r   ro   r   r   r   r      s    8
C*r   r   c                  "    e Zd ZdZd Zd Zd Zy)*TestBB5CleanupOrphanedSessionsReturnsCountzDBB5: cleanup_orphaned_sessions() returns the number of rows updated.c                x   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 )Nr>   r?   countintrA   )r+   cleanup_orphaned_sessionsr?   r   rG   rH   rI   rJ   rK   rL   rM   )rN   r*   r   rO   rP   s        r   test_returns_integerz?TestBB5CleanupOrphanedSessionsReturnsCount.test_returns_integer   s    //1%%%%%%%%%z%%%z%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%r   c                   t               }d|j                  _        |j                         }d}||k(  }|st	        j
                  d|fd||f      dt        j                         v st	        j                  |      rt	        j                  |      ndt	        j                  |      dz  }dd|iz  }t        t	        j                  |            d x}}y )N   rS   r   r   r   r|   rX   r+   r)   r   r   rI   r\   rG   rH   rJ   rK   rL   rM   rN   r*   r   r]   rh   r   r_   s          r   &test_returns_nonzero_when_rows_updatedzQTestBB5CleanupOrphanedSessionsReturnsCount.test_returns_nonzero_when_rows_updated   }    &'#//1uzuuur   c                   t               }d|j                  _        |j                         }d}||k(  }|st	        j
                  d|fd||f      dt        j                         v st	        j                  |      rt	        j                  |      ndt	        j                  |      dz  }dd|iz  }t        t	        j                  |            d x}}y )Nr   rS   r   r   r   r|   rX   r   r   s          r   &test_returns_zero_when_no_rows_updatedzQTestBB5CleanupOrphanedSessionsReturnsCount.test_returns_zero_when_no_rows_updated   r   r   N)rk   rl   rm   rn   r   r   r   ro   r   r   r   r      s    N&r   r   c                  "    e Zd ZdZd Zd Zd Zy)TestWB1CleanupOrphanedSQLShapezpWB1: cleanup_orphaned_sessions SQL uses WHERE ended_at IS NULL AND
    started_at < NOW() - INTERVAL '24 hours'.c                   t        j                  d      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}}y )
N3/mnt/e/genesis-system/core/storage/session_store.pyzended_at IS NULLinz%(py1)s in %(py3)ssourcer{   z9cleanup_orphaned_sessions must filter on ended_at IS NULL
>assert %(py5)srX   pathlibPath	read_textrI   r\   rK   rG   rH   rJ   _format_assertmsgrL   rM   rN   r   r   r]   r   r_   s         r   "test_sql_contains_ended_at_is_nullzATestWB1CleanupOrphanedSQLShape.test_sql_contains_ended_at_is_null   s    A

)+ 	 " 	
!V+ 	
 	
!V 	
 	
 		 " 	
 	
	6	
 	
  &, 	
 	
 		 &, 	
 	
  H	
 	
 	
 	
 	
r   c                >   t        j                  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                  |      t	        j                  |      dz  }t	        j                  d      dz   d	|iz  }t        t	        j                  |            d x}x}x}}y )
Nr   z24 hoursr   )zD%(py1)s in %(py7)s
{%(py7)s = %(py5)s
{%(py5)s = %(py3)s.lower
}()
}r   )rC   rW   rX   rZ   z5cleanup_orphaned_sessions must use a 24-hour intervalz
>assert %(py9)spy9)r   r   r   lowerrI   r\   rK   rG   rH   rJ   r   rL   rM   )rN   r   r   r^   @py_assert6r]   r`   @py_format10s           r   #test_sql_contains_24_hours_intervalzBTestWB1CleanupOrphanedSQLShape.test_sql_contains_24_hours_interval   s    A

)+ 	  	
V\\ 	
\^ 	
z^+ 	
 	
z^ 	
 	
 		  	
 	
	6	
 	
  $ 	
 	
 		 $ 	
 	
 		 * 	
 	
 		 , 	
 	
  D	
 	
 	
 	
 	
 	
r   c                   t        j                  d      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}}y )
Nr   r1   r   r   r   r{   zHcleanup_orphaned_sessions must compare started_at to detect old sessionsr   rX   r   r   s         r   'test_sql_contains_started_at_comparisonzFTestWB1CleanupOrphanedSQLShape.test_sql_contains_started_at_comparison   s    A

)+ 	  	
|v% 	
 	
|v 	
 	
 		  	
 	
	6	
 	
   & 	
 	
 		  & 	
 	
  W	
 	
 	
 	
 	
r   N)rk   rl   rm   rn   r   r   r   ro   r   r   r   r      s    1


r   r   c                      e Zd ZdZd Zd Zy) TestWB2OpenSessionGeneratesUUID4z@WB2: open_session generates UUID4 (not sequential int or uuid1).c                H   t               }t        d      5 }t        j                  d      }||_        |j                  d      }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)Nz%core.storage.session_store.uuid.uuid4z$aaaaaaaa-bbbb-4bbb-8bbb-cccccccccccczagent-xrS   )z0%(py0)s == %(py5)s
{%(py5)s = %(py2)s(%(py3)s)
}returnedr5   fixed)rB   rD   rW   rX   rY   rZ   )r+   r   r6   r[   r
   rF   r5   rI   r\   rG   rH   rJ   rK   rL   rM   assert_called)	rN   r*   
mock_uuid4r   r   r^   rh   r_   r`   s	            r   test_uses_uuid4z0TestWB2OpenSessionGeneratesUUID4.test_uses_uuid4   s    :; 	5zIIDEE&+J#)))4H	5 u:%x:%%%%x:%%%%%%x%%%x%%%%%%3%%%3%%%%%%u%%%u%%%:%%%%%%%  "	5 	5s   .FF!c                6   t        j                  d      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 )Nr   zuuid.uuid4()r   r   r   r{   z"open_session must use uuid.uuid4()r   rX   zuuid.uuid1()not inz%(py1)s not in %(py3)su    Must not use uuid1 — use uuid4r   r   s         r    test_source_uses_uuid4_not_uuid1zATestWB2OpenSessionGeneratesUUID4.test_source_uses_uuid4_not_uuid1  s    A

)+ 	 M~'MMM~MMM~MMMMMMMMMMMMM)MMMMMMMO~V+OOO~VOOO~OOOOOOVOOOVOOOO-OOOOOOOr   N)rk   rl   rm   rn   r   r   ro   r   r   r   r      s    J#Pr   r   c                  4    e Zd ZdZd Zd Zd Zd Zd Zd Z	y)	#TestWB3ConnectionPoolGetconnPutconnz=WB3: every public method uses getconn/putconn in try/finally.c                    t               }|j                  d       |j                  j                  j	                          |j                  j
                  j                  |j                         y )Nagent)r+   rF   r'   r   assert_called_onceputconnassert_called_once_withr(   r   s     r   !test_open_session_getconn_putconnzETestWB3ConnectionPoolGetconnPutconn.test_open_session_getconn_putconn  sO    7#  335  889I9IJr   c                   t               }|j                  t        t        j                                      |j
                  j                  j                          |j
                  j                  j                  |j                         y r   )r+   r   r5   r6   r7   r'   r   r   r   r   r(   r   s     r   "test_close_session_getconn_putconnzFTestWB3ConnectionPoolGetconnPutconn.test_close_session_getconn_putconn  sZ    C

-.  335  889I9IJr   c                    t        g       }|j                          |j                  j                  j	                          |j                  j
                  j                  |j                         y )Nrs   )r+   r   r'   r   r   r   r   r(   r   s     r   (test_get_active_sessions_getconn_putconnzLTestWB3ConnectionPoolGetconnPutconn.test_get_active_sessions_getconn_putconn  sP    R(!!#  335  889I9IJr   c                   t        d       }|j                  t        t        j                                      |j
                  j                  j                          |j
                  j                  j                  |j                         y )Nr   )r+   r   r5   r6   r7   r'   r   r   r   r   r(   r   s     r    test_get_session_getconn_putconnzDTestWB3ConnectionPoolGetconnPutconn.test_get_session_getconn_putconn  s]    T*#djjl+,  335  889I9IJr   c                    t               }|j                          |j                  j                  j	                          |j                  j
                  j                  |j                         y r   )r+   r   r'   r   r   r   r   r(   r   s     r   test_cleanup_getconn_putconnz@TestWB3ConnectionPoolGetconnPutconn.test_cleanup_getconn_putconn$  sM    '')  335  889I9IJr   c                <   t               }t        d      |j                  j                  _        t        j                  t              5  |j                  d       ddd       |j                  j                  j                  |j                         y# 1 sw Y   9xY w)z:putconn must run in finally even if cursor.execute raises.zDB errorr   N)r+   RuntimeErrorr)   executeside_effectr   raisesrF   r'   r   r   r(   r   s     r   ,test_putconn_called_even_when_execute_raiseszPTestWB3ConnectionPoolGetconnPutconn.test_putconn_called_even_when_execute_raises*  sq    1=j1I"".]]<( 	(w'	(  889I9IJ	( 	(s   BBN)
rk   rl   rm   rn   r   r   r   r   r   r   ro   r   r   r   r   	  s)    GKKKKKKr   r   c                      e Zd ZdZd Zd Zy)TestWB4NoSQLiteImportz.WB4: session_store.py must not import sqlite3.c                   t        j                  d      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}}y )
Nr   zimport sqlite3r   r   r   r{   uG   session_store.py must NOT import sqlite3 — Genesis Rule 7 (no SQLite)r   rX   r   r   s         r   test_no_sqlite3_in_sourcez/TestWB4NoSQLiteImport.test_no_sqlite3_in_source6  s    A

)+ 	   	
v- 	
 	
v 	
 	
 		   	
 	
	6	
 	
  (. 	
 	
 		 (. 	
 	
  V	
 	
 	
 	
 	
r   c                    dd l mc m} d}t        ||      }| }|st	        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                  |      dz  }t        t	        j                  |            d x}x}}y )Nr   sqlite3z9sqlite3 must not appear in session_store module namespacez;
>assert not %(py5)s
{%(py5)s = %(py0)s(%(py1)s, %(py3)s)
}hasattrmodrV   )core.storage.session_storestoragesession_storer   rI   r   rG   rH   rJ   rK   rL   rM   )rN   r   r]   r^   r   r   s         r   $test_sqlite3_not_in_module_namespacez:TestWB4NoSQLiteImport.test_sqlite3_not_in_module_namespace>  s    00 ) 	
73	* 	
** 	
* 	
  H	
 	
	6	
 	
   	
 	
 		  	
 	
	6	
 	
   	
 	
 		  	
 	
 		 !* 	
 	
 		 + 	
 	
 	
 	
 	
 	
r   N)rk   rl   rm   rn   r   r   ro   r   r   r   r   3  s    8

r   r   c                      e Zd ZdZd Zd Zy)TestPackageExportsz;SessionStore 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 )Nr}   )z%(py0)s is %(py2)sSessionStoreFromPackager   rf   rg   rE   )
r  r   rI   r\   rG   rH   rJ   rK   rL   rM   )rN   rh   ri   rP   s       r   *test_session_store_importable_from_packagez=TestPackageExports.test_session_store_importable_from_packageM  sm    &,6666&,666666&666&666666,666,6666666r   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   r  r{   r|   rX   )
core.storager  rI   r\   rK   rG   rH   rJ   rL   rM   )rN   r  r   r]   r   r_   s         r   test_all_includes_session_storez2TestPackageExports.test_all_includes_session_storeP  s^    ((~((((~(((~((((((((((((((((r   N)rk   rl   rm   rn   r  r  ro   r   r   r  r  J  s    E7)r   r  c                      e Zd ZdZd Zd Zy)TestConnectionPoolInitzGSessionStore must create ThreadedConnectionPool(minconn=2, maxconn=10).c                   t        d      5 }t               |_        dddddd}t        |       d d d        j                  d   }|d   }d	}||k(  }|st        j                  d
|fd||f      t        j                  |      t        j                  |      dz  }t        j                  d|d          dz   d|iz  }t        t        j                  |            d x}x}}|d   }d}||k(  }|st        j                  d
|fd||f      t        j                  |      t        j                  |      dz  }t        j                  d|d          dz   d|iz  }t        t        j                  |            d x}x}}y # 1 sw Y   RxY w)Nr   hr   r   r   dbr!   r   r-   rS   z%(py1)s == %(py4)sr   zminconn must be 2, got z
>assert %(py6)srx   rt   r/   zmaxconn must be 10, got )r   r   r
   r   	call_argsrI   r\   rK   r   rL   rM   )	rN   mock_clsparams
positionalr   rO   r]   rP   r   s	            r   &test_pool_created_with_correct_min_maxz=TestConnectionPoolInit.test_pool_created_with_correct_min_max]  s$   9: 	!h$-KH!!4!sdDF 		! ''*
!}LL}!LLL}LLL}LLLLLL%<Z]O#LLLLLLLL!}NN}"NNN}NNN}NNNNNN&>z!}o$NNNNNNNN	! 	!s   #E??F	c                   t        d      5 }t               |_        dddddd}t        |       d d d        j                  d   }|d	   }d}||k(  }|slt        j                  d
|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            d x}x}}|d   }d}||k(  }|slt        j                  d
|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            d x}x}}y # 1 sw Y   xY w)Nr   myhosti9  adminsecretgenesis_testr!   rt   r"   rS   r  r   r   rx   r&   )
r   r   r
   r   r  rI   r\   rK   rL   rM   )	rN   r  r  kwargsr   rO   r]   rP   r   s	            r   $test_pool_receives_connection_paramsz;TestConnectionPoolInit.test_pool_receives_connection_paramsh  s    9: 	!h$-KH!&%8 .0F  	! ##A&f~))~))))~)))~))))))))))h1>1>1111>111111>1111111	! 	!s   #EEN)rk   rl   rm   rn   r  r  ro   r   r   r
  r
  Z  s    Q	O
2r   r
  c                      e Zd ZdZd Zd Zy)	TestClosez"close() must call pool.closeall().c                    t               }|j                          |j                  j                  j	                          y r   )r+   closer'   closeallr   r   s     r   test_close_calls_closeallz#TestClose.test_close_calls_closeall}  s*    !!446r   c                p   t               }|j                          |j                          |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 )Nr-   rS   )zp%(py6)s
{%(py6)s = %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s._mock_pool
}.closeall
}.call_count
} == %(py9)sr*   )rB   rD   rE   rx   r   zassert %(py11)spy11)r+   r  r'   r  
call_countrI   r\   rG   rH   rJ   rK   rL   rM   )	rN   r*   rh   rO   r   @py_assert8@py_assert7r   @py_format12s	            r   test_close_twice_does_not_raisez)TestClose.test_close_twice_does_not_raise  s    8((8(338q83q88883q888888u888u888888(8883888q88888888r   N)rk   rl   rm   rn   r   r'  ro   r   r   r  r  z  s    ,7
9r   r  c                      e Zd ZdZd Zd Zy)TestParameterisedQuerieszDAll SQL in session_store.py must use %s placeholders, not f-strings.c                   t        j                  d      j                         }t        j                  d|      }| }|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 )Nr   z4f["\'].*?(SELECT|INSERT|UPDATE|DELETE|WHERE).*?["\']z%Found f-string SQL (injection risk): z
>assert not %(py0)srB   fstring_sql)r   r   r   refindallrI   r   rG   rH   rJ   rK   rL   rM   )rN   r   r+  rh   @py_format2s        r   test_no_fstring_sql_in_sourcez6TestParameterisedQueries.test_no_fstring_sql_in_source  s    A

)+ 	 jjCV
  	
 	
  4K=A	
 	
	6	
 	
   	
 	
 		  	
 	
 	
 	
 	
r   c                   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                  |      dz  }d	d
|iz  }t        t        j                  |            dx}x}x}}|j                  j                  D ]/  }	|	d   }
t        |
      }d}||k\  }|st        j                  d|fd||f      dt        j                         v st        j                  t              rt        j                  t              nddt        j                         v st        j                  |
      rt        j                  |
      ndt        j                  |      t        j                  |      dz  }t        j                  d      dz   d|iz  }t        t        j                  |            dx}x}}2 y)uJ   open_session must call cur.execute(sql, params) — not bare execute(sql).r=   keyvalrt   )>=)zO%(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.execute
}.call_count
} >= %(py7)scur)rB   rD   rE   rZ   zassert %(py9)sr   Nr   r-   )z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} >= %(py6)sru   argsrw   uC   cur.execute() must be called with (sql, params) — not just (sql,)z
>assert %(py8)sry   )r+   rF   r)   r   r#  rI   r\   rG   rH   rJ   rK   rL   rM   call_args_listru   r   )rN   r*   r4  rh   rO   r   r   r`   r   r  r5  r]   r^   r   r   s                  r   2test_open_session_execute_called_with_params_tuplezKTestParameterisedQueries.test_open_session_execute_called_with_params_tuple  s   =5%.9  {{*{%%**%****%******s***s***{***%**********33 	IQ<Dt9  9>   9  v     I   v     I   I   I !"    V     	r   N)rk   rl   rm   rn   r/  r7  ro   r   r   r)  r)    s    N


r   r)  __main__z!BB1a: open_session returns stringz"BB1b: returned value is valid UUIDu#   BB1c: two calls → different UUIDsz+BB2a: active sessions contains open sessionz#BB2b: empty when no active sessionsz'BB3a: closed session not in active listz"BB3b: close_session does not raisezBB4a: unknown id returns Nonez%BB4b: no exception on missing sessionzBB4c: known id returns dictzBB5a: cleanup returns integerz#BB5b: cleanup returns nonzero countzBB5c: cleanup returns zerozWB1a: SQL has ended_at IS NULLzWB1b: SQL has 24 hours intervalz#WB1c: SQL has started_at comparisonzWB2a: uses uuid4z!WB2b: source uses uuid4 not uuid1z"WB3a: open_session getconn/putconnz#WB3b: close_session getconn/putconnz)WB3c: get_active_sessions getconn/putconnz!WB3d: get_session getconn/putconnzWB3e: cleanup getconn/putconnz*WB3f: putconn called even on execute errorzWB4a: no sqlite3 in sourcez%WB4b: sqlite3 not in module namespacez)PKG: SessionStore importable from packagez"PKG: __all__ includes SessionStorezPOOL: minconn=2, maxconn=10zPOOL: params forwardedzCLOSE: closeall calledz!CLOSE: close twice does not raisezSQL: no f-string SQLz+SQL: open_session execute uses params tuplez	  [PASS] rt   z	  [FAIL] z: 
/z tests passedz6ALL TESTS PASSED -- Story 5.05 (Track B): SessionStore)NN)Nz
test-agentNNN)r8   r5   r3   r5   r1   r   r2   r   r4   r   returnr   )Vrn   
__future__r   builtinsrG   _pytest.assertion.rewrite	assertionrewriterI   syspathinsertr   r,  r6   r   unittest.mockr   r   r   r   r   r  r  r   r+   r9   r;   rq   r   r   r   r   r   r   r   r  r
  r  r)  rk   	tracebackrQ   ra   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  r  r   r'  r/  r7  testspassedfailednamefnprintr   r   	print_excexitro   r   r   <module>rN     s   #   
 * +  	   *  4 @-6(    	
  
* * $/ /* *4 6
 
:P P('K 'KT
 
.) ) 2 2@9 9( @ zK	,	&	(	<	<	>K 
.	&	(	J	J	L	K 
/	&	(	N	N	PK 
7	3	5	`	`	bK 
/	3	5	`	`	bK 
3	-	/	V	V	XK 
.	-	/	Q	Q	SK  
)	.	0	M	M	O!K$ 
1	.	0	W	W	Y%K( 
'	.	0	K	K	M)K, 
)	3	5	J	J	L-K0 
/	3	5	\	\	^1K4 
&	3	5	\	\	^5K: 
*	'	)	L	L	N;K> 
+	'	)	M	M	O?KB 
/	'	)	Q	Q	SCKF 
	)	+	;	;	=GKJ 
-	)	+	L	L	NKKN 
.	,	.	P	P	ROKR 
/	,	.	Q	Q	SSKV 
5	,	.	W	W	YWKZ 
-	,	.	O	O	Q[K^ 
)	,	.	K	K	M_Kb 
6	,	.	[	[	]cKf 
&		 	:	:	<gKj 
1		 	E	E	GkKp 
5			H	H	JqKt 
.			=	=	?uKz 
'		!	H	H	J{K~ 
"		!	F	F	HKD 
"		.	.	0EKH 
-		4	4	6IKN 
 	!	#	A	A	COKR 
7	!	#	V	V	XSKEZ FF b	DIdV$%aKF	 
Bvha(
67{FGA n  	IdV2cU+,I!aKF	s   O++P0$PP