
    Zi                    z   d Z ddlmZ ddlZddlmc mZ ddl	Z	ddl
Z
ddlmZ ddlmZmZmZmZ ddlmZmZmZ ddlmZmZ ddlZe
j4                  j7                  d e ee      j<                  j<                  j<                               ddlm Z m!Z! dd	l"m#Z#m$Z$m%Z%m&Z&m'Z'm(Z(m)Z) dd
l*m+Z+m,Z,m-Z-m.Z. ddl/m0Z0m1Z1m2Z2m3Z3 dZ4dZ5 G d d      Z6	 	 d%	 	 	 	 	 d&dZ7 G d d      Z8 G d d      Z9 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$      ZAy)'a  RLM Neo-Cortex -- Module 5: Feedback Collection Pipeline Tests.

Covers Stories 5.01-5.09.  All backends are mocked (AsyncMock for PG,
AsyncMock for Redis) -- no live Elestio connections required.

Story coverage:
    5.01  FeedbackCollector constructor
    5.02  record_feedback() -- positive / negative / neutral
    5.03  cache_interaction() / get_interaction() -- Redis TTL, cross-tenant
    5.04  get_pair_count() / get_recent_pairs()
    5.05  check_dpo_readiness()
    5.06  _generate_fallback_response()
    5.07  handle_callback_query() / build_feedback_keyboard()  (Telegram)
    5.08  POST /api/v1/feedback/webhook  (FastAPI)
    5.09  Integration lifecycle

VERIFICATION_STAMP
Story: 5.09
Verified By: parallel-builder
Verified At: 2026-02-26
Tests: see results
Coverage: >=85%
    )annotationsN)Path)AnyDictListOptional)	AsyncMock	MagicMockpatch)UUIDuuid4)FeedbackSignalPreferencePair)FeedbackCollector_DEFAULT_ANNOTATOR_DPO_MINIMUM_PAIRS_INTERACTION_KEY_PREFIX_INTERACTION_TTL_SECONDS_TABLE_NAME_generate_fallback_response)_CALLBACK_PREFIX_SIGNAL_MAPbuild_feedback_keyboardhandle_callback_query)FeedbackWebhookRequest_SIGNAL_ENUM_MAP_VALID_SIGNALScreate_feedback_routerz-postgresql://user:pass@localhost:5432/genesiszredis://localhost:6379/0c                  (    e Zd ZdZddZddZddZy)	_CollectorWithMockszGThin wrapper that exposes the underlying mock_conn for white-box tests.c                     || _         || _        y N	collector	mock_conn)selfr$   r%   s      0/mnt/e/genesis-system/tests/rlm/test_feedback.py__init__z_CollectorWithMocks.__init__I   s    ""    c                .    t        | j                  |      S r"   )getattrr$   )r&   names     r'   __getattr__z_CollectorWithMocks.__getattr__N   s    t~~t,,r)   c                j    |dv rt         j                  | ||       y t        | j                  ||       y )Nr#   )object__setattr__setattrr$   )r&   r,   values      r'   r0   z_CollectorWithMocks.__setattr__Q   s-    --tT51DNND%0r)   N)r$   r   r%   r	   returnNone)r,   strr3   r   )r,   r5   r2   r   r3   r4   )__name__
__module____qualname____doc__r(   r-   r0    r)   r'   r    r    F   s    Q#
-1r)   r    c                   
 t        t        t              }| xs g }t               }t        dt	        |      i      |_        t        |D cg c]  }|j                  dd      |j                  dd      |j                  dd      |j                  dt              |j                  d	d
      t        j                  |j                  di             |j                  dd      d c}      |_
        t        d      |_        t               }t        t        |            |_        ||_        ||_        i 
|r
j#                  |       t               }d
fd}dd
fd}	t        |      |_        t        |	      |_        ||_        |S c c}w )zBuild a FeedbackCollector with mocked PG pool and Redis client.

    Also attaches ``_mock_conn`` to the returned collector so white-box tests
    can assert directly on it without walking the MagicMock chain.
    pg_dsn	redis_urlcntreturn_value
input_text chosen_outputrejected_outputannotator_id
confidence      ?metadata
created_at2026-02-26T00:00:00ZrB   rD   rE   rF   rG   rI   rJ   Nc                .   K   j                  |       S wr"   get)keystores    r'   
_redis_getz#_make_collector.<locals>._redis_get   s     yy~   c                   K   || <   y wr"   r:   )rP   r2   exrQ   s      r'   
_redis_setz#_make_collector.<locals>._redis_set   s     c
s   
side_effect)rP   r5   r3   Optional[str]r   rP   r5   r2   r5   rU   intr3   r4   )r   	_FAKE_DSN_FAKE_REDISr	   lenfetchrowrO   r   jsondumpsfetchexecuter
   _AsyncContextManageracquire_pg_pool
_mock_connupdateset_redis)pg_rows
redis_datar$   rowsr%   r	mock_pool
mock_redisrR   rV   rQ   s             @r'   _make_collectorrr   X   sa    "kJI =bDI"D	0BCI .  %%b1UU?B7 uu%6;EE.2DE%%c2

155R#89%%.DE	
. IO "t4II!))4I #I$I EZ J :6JN:6JN!IQ.s   BFc                  (    e Zd ZdZddZddZddZy)	re   z5Minimal async context manager wrapping a mock object.c                    || _         y r"   _obj)r&   objs     r'   r(   z_AsyncContextManager.__init__   s	    	r)   c                "   K   | j                   S wr"   ru   )r&   s    r'   
__aenter__z_AsyncContextManager.__aenter__   s     yys   c                   K   y wr"   r:   )r&   _s     r'   	__aexit__z_AsyncContextManager.__aexit__   s	     s   N)rw   r   r3   r4   )r3   r   )r{   r   r3   r4   )r6   r7   r8   r9   r(   ry   r|   r:   r)   r'   re   re      s    ?r)   re   c                  z    e Zd ZdZd	dZd
dZ	 	 	 	 d
dZd	dZej                  j                  d	d       Zd	dZy) TestFeedbackCollectorConstructorzBB + WB tests for Story 5.01.c                n   t        t              }d}||u}|st        j                  d|fd||f      dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }dd|iz  }t        t        j                  |            dx}}y)	z&BB: construct OK with explicit pg_dsn.r=   Nis notz%(py0)s is not %(py3)sr$   py0py3assert %(py5)spy5)
r   r]   
@pytest_ar_call_reprcompare@py_builtinslocals_should_repr_global_name	_safereprAssertionError_format_explanation)r&   r$   @py_assert2@py_assert1@py_format4@py_format6s         r'    test_construct_with_explicit_dsnzATestFeedbackCollectorConstructor.test_construct_with_explicit_dsn   se    %Y7	 $$y$$$$y$$$$$$y$$$y$$$$$$$$$$r)   c                   |j                  dt               t               }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)	z2BB: construct OK when DATABASE_URL env var is set.DATABASE_URLNr   r   r$   r   r   r   )setenvr]   r   r   r   r   r   r   r   r   r   )r&   monkeypatchr$   r   r   r   r   s          r'    test_construct_with_env_fallbackzATestFeedbackCollectorConstructor.test_construct_with_env_fallback   ss    >95%'	 $$y$$$$y$$$$$$y$$$y$$$$$$$$$$r)   c                    |j                  dd       t        j                  t        d      5  t	                ddd       y# 1 sw Y   yxY w)z!BB: no DSN + no env = ValueError.r   F)raisingzPostgreSQL DSN is required)matchN)delenvpytestraises
ValueErrorr   )r&   r   s     r'   %test_no_dsn_no_env_raises_value_errorzFTestFeedbackCollectorConstructor.test_no_dsn_no_env_raises_value_error   sA     	>59]]:-IJ 	 	  	  	 s   AAc                   t         j                  }d}||k(  }|st        j                  d|fd||f      dt	        j
                         v st        j                  t               rt        j                  t               ndt        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}d}t        |k(  }|st        j                  d|fd	t        |f      d
t	        j
                         v st        j                  t              rt        j                  t              nd
t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}}y)z3WB: table name constant equals pl_preference_pairs.pl_preference_pairs==)z2%(py2)s
{%(py2)s = %(py0)s.TABLE_NAME
} == %(py5)sr   r   py2r   assert %(py7)spy7Nz%(py0)s == %(py3)sr   r   r   r   )r   
TABLE_NAMEr   r   r   r   r   r   r   r   r   )r&   r   @py_assert4@py_assert3r   @py_format8r   r   s           r'   &test_table_name_is_pl_preference_pairszGTestFeedbackCollectorConstructor.test_table_name_is_pl_preference_pairs   s     ++D/DD+/DDDDD+/DDDDDDD DDD DDD+DDD/DDDDDDDD33{33333{3333333{333{33333333333r)   c                N  K   t        g       }|j                          d{   }d}||k(  }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}y7 w)	z*BB: get_pair_count on empty collector = 0.rl   Nr   r   z%(py1)s == %(py4)spy1py4assert %(py6)spy6rr   get_pair_countr   r   r   r   r   r&   r$   @py_assert0r   r   @py_format5@py_format7s          r'   )test_get_pair_count_on_empty_returns_zerozJTestFeedbackCollectorConstructor.test_get_pair_count_on_empty_returns_zero   n      $B/	--//414/14444/1444/44414444444/    B%B#BB%c           
        t        t              }d}d}t        |||      }t        |      }|sGd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t	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }t        t        j                  |            dx}x}x}}d	}d}t        |||      }t        |      }|sGd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t	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }t        t        j                  |            dx}x}x}}y)
zCWB: FeedbackCollector implements FeedbackCollectorProtocol methods.r   record_feedbackNz^assert %(py10)s
{%(py10)s = %(py0)s(%(py8)s
{%(py8)s = %(py1)s(%(py2)s, %(py4)s, %(py6)s)
})
}callabler+   r$   )r   r   r   r   r   py8py10r   )r   r]   r+   r   r   r   r   r   r   r   r   )r&   r$   r   @py_assert5@py_assert7@py_assert9@py_format11s          r'   test_implements_protocolz9TestFeedbackCollectorConstructor.test_implements_protocol   s   %Y7	+<DdD	+<dCDxCDDDDDDDDxDDDxDDDDDDDDDDDDDDD	DDD	DDD+<DDDdDDDCDDDDDDDDDD+;CTC	+;TBCxBCCCCCCCCxCCCxCCCCCCCCCCCCCCC	CCC	CCC+;CCCTCCCBCCCCCCCCCCr)   Nr3   r4   )r   zpytest.MonkeyPatchr3   r4   )r6   r7   r8   r9   r   r   r   r   r   markasyncior   r   r:   r)   r'   r~   r~      sM    '%
% - 	 4
 [[5 5
Dr)   r~   c                     e Zd ZdZej
                  j                  d
d       Zej
                  j                  d
d       Zej
                  j                  d
d       Z	ej
                  j                  d
d       Z
ej
                  j                  d
d       Zej
                  j                  d
d       Zej
                  j                  d
d       Zy	)TestRecordFeedbackzBB + WB tests for Story 5.02.c                @  K   t               }d}d}t               }t         d| d| }t        t	        j
                  d|d            |j                  _        |j                  ||t        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}}|j&                  }||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                   |      ndt        j                   |      dt        j                         v st        j                  |      rt        j                   |      nddz  }dd|iz  }t#        t        j$                  |            dx}}y7 w)u-   BB: POSITIVE → AI response = chosen_output.zinteraction-001z&Your appointment is confirmed for 9am.:zBook me an appointmentrB   output_textr@   	tenant_idinteraction_idsignalNr   r   pairr   r   r   r   )z5%(py2)s
{%(py2)s = %(py0)s.chosen_output
} == %(py4)sai_responser   r   r   r   r   )r   rr   r   r	   ra   rb   rk   rO   r   r   POSITIVEr   r   r   r   r   r   r   r   rD   )r&   tidiidr   r$   rP   r   r   r   r   r   r   r   r   s                 r'   !test_positive_signal_ai_is_chosenz4TestRecordFeedback.test_positive_signal_ai_is_chosen   sn     g>#%	()3%q6(6*%  
	 ..!** / 
 
  t4t4tt4!!0![0000![000000t000t000!000000[000[0000000
s   A=H?H FHc                $  K   t               }d}d}t               }t        t        j                  d|d            |j
                  _        |j                  ||t        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}}|j$                  }||k(  }
|
st        j                  d|
fd||f      d
t        j                         v st        j                  |      rt        j                  |      nd
t        j                  |      dt        j                         v st        j                  |      rt        j                  |      nddz  }dd|iz  }t!        t        j"                  |            dx}}
y7 w)u/   BB: NEGATIVE → AI response = rejected_output.zinteraction-002z"I'm sorry, I can't help with that.zI have a complaintr   r@   r   Nr   r   r   r   r   r   r   z7%(py2)s
{%(py2)s = %(py0)s.rejected_output
} == %(py4)sr   r   r   r   )r   rr   r	   ra   rb   rk   rO   r   r   NEGATIVEr   r   r   r   r   r   r   r   rE   )r&   r   r   r   r$   r   r   r   r   r   r   r   r   s                r'   #test_negative_signal_ai_is_rejectedz6TestRecordFeedback.test_negative_signal_ai_is_rejected   sW     g:#%	(2*%  
	 ..!** / 
 
  t4t4tt4##2#{2222#{222222t222t222#222222{222{2222222
s   A/H1H2FHc                  K   t               }|j                  t               dt        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7 w)
u1   BB: NEUTRAL → None returned, no pair generated.zinteraction-003r   Nisz%(py0)s is %(py3)sresultr   r   r   )rr   r   r   r   NEUTRALr   r   r   r   r   r   r   r   r&   r$   r   r   r   r   r   s          r'    test_neutral_signal_returns_nonez3TestRecordFeedback.test_neutral_signal_returns_none  s      $%	 00g,!)) 1 
 

 v~vvv
s   8C'C%B+C'c                   K   t               }|j                  t               dt        j                         d{    |j
                  j                  j                          y7 )w)u1   WB: NEUTRAL signal → conn.execute never called.zinteraction-004r   N)rr   r   r   r   r   rh   rd   assert_not_called)r&   r$   s     r'   test_neutral_signal_no_db_writez2TestRecordFeedback.test_neutral_signal_no_db_write  s]      $%	''g,!)) ( 
 	
 	
 	$$668	
s   8A&A$*A&c                8  K   t               }t        t        j                  ddd            |j                  _        |j                  t               dt        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}}|j$                  }|t&        k(  }|st        j                  d|fd|t&        f      d
t        j                         v st        j                  |      rt        j                  |      nd
t        j                  |      dt        j                         v st        j                  t&              rt        j                  t&              nddz  }dd|iz  }	t!        t        j"                  |	            dx}}y7 w)z,WB: pair.annotator_id = 'telegram_feedback'.HellozHi therer   r@   zinteraction-005r   Nr   r   r   r   r   r   r   z4%(py2)s
{%(py2)s = %(py0)s.annotator_id
} == %(py4)sr   r   r   r   )rr   r	   ra   rb   rk   rO   r   r   r   r   r   r   r   r   r   r   r   r   rF   r   )
r&   r$   r   r   r   r   r   r   r   r   s
             r'   test_pair_has_correct_annotatorz2TestRecordFeedback.test_pair_has_correct_annotator  sL     $%	(%)%  
	 ..g,!** / 
 

  t4t4tt4  6 $66666 $6666666t666t666 666666$6666$66666666
s   A)H+H,F,Hc                  K   t               }t        t        j                  ddd            |j                  _        |j                  t               dt        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}}|j$                  }d}||k(  }|st        j                  d|fd||f      d
t        j                         v st        j                  |      rt        j                  |      nd
t        j                  |      t        j                  |      dz  }dd|iz  }	t!        t        j"                  |	            dx}x}}y7 sw)zWB: pair.confidence = 1.0.xyr   r@   i006r   Nr   r   r   r   r   r   rH   r   )z2%(py2)s
{%(py2)s = %(py0)s.confidence
} == %(py5)sr   r   r   )rr   r	   ra   rb   rk   rO   r   r   r   r   r   r   r   r   r   r   r   r   rG   )
r&   r$   r   r   r   r   r   r   r   r   s
             r'   test_pair_confidence_is_1z,TestRecordFeedback.test_pair_confidence_is_10  s.     $%	(3s$KL 
	 ..g!!** / 
 

  t4t4tt4%#%#%%%%#%%%%%%t%%%t%%%%%%#%%%%%%%
s   A)G!+G,E3G!c           	       K   d}t               }t        t        j                  ddd            |j                  _        |j                  t               |t        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}}|j$                  }|j
                  }d}	 ||	      }
|
|k(  }|st        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t        j                         v st        j                  |      rt        j                  |      nddz  }dd|iz  }t!        t        j"                  |            dx}x}x}	x}
}y7 w)z+WB: interaction_id stored in pair.metadata.zinteraction-007qar   r@   r   Nr   r   r   r   r   r   r   r   )zh%(py8)s
{%(py8)s = %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.metadata
}.get
}(%(py6)s)
} == %(py10)sr   )r   r   r   r   r   r   assert %(py12)spy12)rr   r	   ra   rb   rk   rO   r   r   r   r   r   r   r   r   r   r   r   r   rI   )r&   r   r$   r   r   r   r   r   r   r   r   r   r   @py_format13s                 r'   test_interaction_id_in_metadataz2TestRecordFeedback.test_interaction_id_in_metadata?  s      #%	(3s$KL 
	 ..g!** / 
 

  t4t4tt4}}9}  9!19 !1292c99992c999999t999t999}999 999!19992999999c999c99999999
s   A+I%-I".G5I%Nr   )r6   r7   r8   r9   r   r   r   r   r   r   r   r   r   r   r:   r)   r'   r   r      s    '[[1 12 [[3 3. [[  [[	9 	9 [[7 7" [[& & [[: :r)   r   c                  2   e Zd ZdZej
                  j                  dd       Zej
                  j                  dd       Zej
                  j                  dd       Z	ej
                  j                  dd       Z
ej
                  j                  dd       Zy)	TestInteractionCachezBB + WB tests for Story 5.03.c                  K   t               }t               }d}|j                  ||dd       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}}|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7 7 w)u5   BB: cache interaction → get_interaction returns it.z
i-cache-01zBook me a slotzConfirmed for 10amr   r   rB   r   Nr   r   r   r   r   r   rB   r   r   r   r   r   r   rr   r   cache_interactionget_interactionr   r   r   r   r   r   r   r   )r&   r$   r   r   r   r   r   r   r   r   r   r   r   s                r'   test_cache_and_retrieve_okz/TestInteractionCache.test_cache_and_retrieve_okW  sl     $%	g))',	 * 
 	
 	
 !00c::!!vT!!!!vT!!!!!!v!!!v!!!T!!!!!!!l#7'77#'77777#'7777#777'77777777m$<(<<$(<<<<<$(<<<<$<<<(<<<<<<<<	
 ;s"   /G<G6G<G9F+G<9G<c                  K   t               }|j                  t               d       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7 w)	u)   BB: non-existent interaction_id → None.does-not-existNr   r   r   r   r   r   )rr   r  r   r   r   r   r   r   r   r   r   r   s          r'   test_nonexistent_returns_nonez2TestInteractionCache.test_nonexistent_returns_nonej  s      $%	 00:JKKv~vvv Ls   (CCB+Cc                  K   t               }t               }t               }d}|j                  ||dd       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7 7 w)z=BB: different tenant cannot see another tenant's interaction.z
shared-iidSecretResponser  Nr   r   r   r   r   r   r  )
r&   r$   tid_atid_br   r   r   r   r   r   s
             r'   test_cross_tenant_returns_nonez3TestInteractionCache.test_cross_tenant_returns_noneq  s      $%	))"	 * 
 	
 	
 !00<<v~vvv	
 =s"   9DD DDB+DDc                X  K   t               }g ddfd}t        |      |j                  _        t	               }d}|j                  ||dd       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   }
|
t        k(  }|st        j                  d
|fd|
t        f      t        j                  |
      dt        j                         v st        j                  t              rt        j                  t              nddz  }dd|iz  }t        t        j                  |            dx}
}y7 ҭw)z.WB: Redis SET called with ex=86400 (24hr TTL).r   rU   c                8   K   j                  | ||d       y w)N)rP   r2   rU   append)rP   r2   rU   	set_callss      r'   _capture_setzPTestInteractionCache.test_redis_set_called_with_correct_ex.<locals>._capture_set  s     S5CDs   rW   zttl-testinoutN   r   z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py6)sr_   r  r   r   r   r   assert %(py8)sr   z%(py1)s == %(py3)sr   r   r   r   r   rZ   r[   )rr   r	   rk   rj   r   r  r_   r   r   r   r   r   r   r   r   r   )r&   r$   r  r   r   r   r   r   r   @py_format9r   r   r   r  s                @r'   %test_redis_set_called_with_correct_exz:TestInteractionCache.test_redis_set_called_with_correct_ex  sR     $%	!		E  )\B	g))#sD%@@@9~""~""""~""""""s"""s""""""9"""9"""~""""""""""|D!=!%=====!%====!======%====%======== 	As   AH*H'GH*c                  K   t               }g ddfd}t        |      |j                  _        t	               }d}|j                  ||dd       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}}t        |      }d   }||v }|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   }||v }
|
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}
}d   }|j                   } |t"              }|sdt        j                  |      t        j                  |      dt        j                         v st        j                  t"              rt        j                  t"              ndt        j                  |      dz  }t        t        j                  |            dx}x}}y7 w)z4WB: Redis key includes tenant_id and interaction_id.r   r5   c                0   K   j                  |        y wr"   r  )rP   r2   rU   	keys_useds      r'   r  zETestInteractionCache.test_key_includes_both_ids.<locals>._capture_set  s     S!s   rW   zkey-test-001r  r  Nr  r   r  r_   r"  r  r  r   r  )z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} in %(py6)sr   z%(py0)s in %(py3)sr   r   r   r   zLassert %(py6)s
{%(py6)s = %(py3)s
{%(py3)s = %(py1)s.startswith
}(%(py4)s)
}r   )r   r   r   r   rZ   r[   )rr   r	   rk   rj   r   r  r_   r   r   r   r   r   r   r   r   r5   
startswithr   )r&   r$   r  r   r   r   r   r   r   r  r   r   r   r   r"  s                 @r'   test_key_includes_both_idsz/TestInteractionCache.test_key_includes_both_ids  sL     $%	!		"  )\B	g))#sD%@@@9~""~""""~""""""s"""s""""""9"""9"""~""""""""""3x'9Q<'x<''''x<''''''s'''s''''''3'''3'''x'''<'''''''l"sl""""sl""""""s"""s"""l"""""""|?|&&?&'>?????|???&??????'>???'>?????????? 	As   AO6O3NO6Nr   )r6   r7   r8   r9   r   r   r   r  r	  r  r  r&  r:   r)   r'   r   r   T  s    '[[= =$ [[  [[ $ [[> >" [[@ @r)   r   c                  2   e Zd ZdZej
                  j                  dd       Zej
                  j                  dd       Zej
                  j                  dd       Z	ej
                  j                  dd       Z
ej
                  j                  dd       Zy)	TestPairQuerieszBB + WB tests for Story 5.04.c                N  K   t        g       }|j                          d{   }d}||k(  }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}y7 w)	u   BB: empty table → count = 0.r   Nr   r   r   r   r   r   r   r   s          r'   test_empty_returns_zeroz'TestPairQueries.test_empty_returns_zero  r   r   c                  K   t        d      D cg c]  }d| ddd }}t        |      }|j                          d{   }d}||k(  }|slt        j                  d|fd	||f      t        j
                  |      t        j
                  |      d
z  }dd|iz  }t        t        j                  |            dx}x}}yc c}w 7 w)u   BB: 5 rows → count = 5.   r   r   brB   rD   rE   r   Nr   r   r   r   r   )rangerr   r   r   r   r   r   r   )	r&   irn   r$   r   r   r   r   r   s	            r'   !test_after_5_feedbacks_count_is_5z1TestPairQueries.test_after_5_feedbacks_count_is_5  s      `eef_ghZ[1#wQTUhh#D1	--//414/14444/1444/44414444444 i/s   C	C!C	CBC	c                0  K   t        d      D cg c]  }d| ddd }}t        |      }t        dddt        dd	d
ddddt        dd	d
dg      |j                  _        |j                  d       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}}yc c}w 7 w)z%BB: recent(2) returns exactly 2 rows.r,  r   r   r-  r.  r   q0rH   z{}rK   rL   q1r@      limitNr   r  r_   pairsr  r  r   )r/  rr   r	   r   rh   rc   get_recent_pairsr_   r   r   r   r   r   r   r   r   )
r&   r0  rn   r$   r8  r   r   r   r   r  s
             r'    test_recent_pairs_respects_limitz0TestPairQueries.test_recent_pairs_respects_limit  s4     `eef_ghZ[1#wQTUhh#D1	%.##/s-CE  ##/s-CE	=
 &	"  00q0995zQzQzQss55zQ i :s   FFAF8F9DFc                  K   t        ddig      }|j                  }|j                          d{    |j                  j	                          |j                  j
                  d   d   }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  }dd|iz  }	t        t        j                  |	            dx}x}x}}y7  w)z/WB: COUNT(*) query executed for get_pair_count.r   r  r   Nr   zCOUNT(*)r#  )zD%(py1)s in %(py7)s
{%(py7)s = %(py5)s
{%(py5)s = %(py3)s.upper
}()
}	call_args)r   r   r   r   zassert %(py9)spy9)rr   rh   r   r`   assert_called_oncer<  upperr   r   r   r   r   r   r   r   )
r&   r$   r%   r<  r   r   @py_assert6r   r   @py_format10s
             r'   $test_get_pair_count_uses_count_queryz4TestPairQueries.test_get_pair_count_uses_count_query  s      $c1XJ7	((	&&((( 	--/&&003A6	.Y__._..z.....z....z......Y...Y..._........... 	)s   /EED Ec                  K   t        g       }|j                  }t        g       |_        |j	                  d       d{    |j                  j
                  d   d   }d}||v }|st        j                  d|fd	||f      t        j                  |      d
t        j                         v st        j                  |      rt        j                  |      nd
dz  }dd|iz  }t        t        j                  |            dx}}y7 ƭw)z.WB: ORDER BY created_at DESC present in query.r   r@   
   r6  Nr   zORDER BY created_at DESCr#  z%(py1)s in %(py3)scall_sqlr  r   r   )rr   rh   r	   rc   r9  r<  r   r   r   r   r   r   r   r   )r&   r$   r%   rF  r   r   r   r   s           r'   .test_get_recent_pairs_order_by_created_at_descz>TestPairQueries.test_get_recent_pairs_order_by_created_at_desc  s      $B/	((	#4	((r(222??,,Q/2)5)X5555)X555)555555X555X5555555 	3s   ?D
DCD
Nr   )r6   r7   r8   r9   r   r   r   r*  r1  r:  rB  rG  r:   r)   r'   r(  r(    s    '[[5 5
 [[5 5 [[   [[
/ 
/ [[	6 	6r)   r(  c                  L   e Zd ZdZ	 d		 	 	 	 	 	 	 d
dZej                  j                  dd       Zej                  j                  dd       Z	ej                  j                  dd       Z
ej                  j                  dd       Zej                  j                  dd       Zy)TestDPOReadinesszBB + WB tests for Story 5.05.c                f   t        t        t              }t               }dfd}dfd}t        |      |_        t        |      |_        t               }t        t        |            |_        ||_	        t               }	t        d      |	_
        t        d      |	_        |	|_        |S )	z7Build collector whose mock returns `count` total pairs.r<   c                   K   diS w)Nr?   r:   )sql_argscounts     r'   	_fetchrowz>TestDPOReadiness._make_collector_with_pairs.<locals>._fetchrow  s     5>!s   c                  K   d| v rd| vrg }t              D ]*  }|j                  dt        j                  ddi      i       , t              D ]*  }|j                  dt        j                  ddi      i       , z
  z
  }t        t	        |d            D ]*  }|j                  dt        j                  ddi      i       , |S t
        dgS w)	NrI   rF   r   r   r   r   r   rF   r?   )r/  r  ra   rb   maxr   )rL  rM  rn   r{   neutral_countrN  negposs        r'   _fetchz;TestDPOReadiness._make_collector_with_pairs.<locals>._fetch  s     S ^3%>s RAKKTZZ:8N-O PQRs RAKKTZZ:8N-O PQR %c 1s=!45 QAKKTZZ98M-N OPQ *<EJKKs   CCrW   r@   N)rL  r5   rM  r   r3   zDict[str, Any])rL  r5   rM  r   r3   z
List[Dict])r   r]   r^   r	   r`   rc   r
   re   rf   rg   rO   rj   rk   )
r&   rN  rU  rT  r$   r%   rO  rV  rp   rq   s
    ```      r'   _make_collector_with_pairsz+TestDPOReadiness._make_collector_with_pairs  s     &Y+N	 K		"	L  '9=	#7	K	%-i8
	 '	 [
"5
"5
%	r)   c                h  K   | j                  ddd      }|j                          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}}|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7 w)u   BB: 50 pairs → ready=False.2      rU  rT  NreadyFr   z%(py1)s is %(py4)sr   r   r   
pair_countr   r   rW  check_dpo_readinessr   r   r   r   r   r&   r$   r   r   r   r   r   r   s           r'   test_50_pairs_not_readyz(TestDPOReadiness.test_50_pairs_not_ready  s      33BBB3G	 4466g'%'%''''%''''''%'''''''l#)r)#r))))#r)))#)))r))))))) 7   (D2D/DD2c                h  K   | j                  ddd      }|j                          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}}|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7 w)u   BB: 100 pairs → ready=True.d   <   (   r[  Nr\  Tr   r]  r   r   r   r^  r   r   r_  ra  s           r'   test_100_pairs_readyz%TestDPOReadiness.test_100_pairs_ready&  s      33CRR3H	 4466g&$&$&&&&$&&&&&&$&&&&&&&l#*s*#s****#s***#***s******* 7rc  c                  K   | j                  d      }|j                          d{   }|d   }|t        k(  }|st        j                  d|fd|t        f      t        j
                  |      dt        j                         v st        j                  t              rt        j
                  t              nddz  }dd	|iz  }t        t        j                  |            d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7 ?w)z"BB: minimum_required always = 100.r   Nminimum_requiredr   r  r   r  r   r   re  r   r   r   r   )rW  r`  r   r   r   r   r   r   r   r   r   )
r&   r$   r   r   r   r   r   r   r   r   s
             r'   test_minimum_required_is_100z-TestDPOReadiness.test_minimum_required_is_100.  s      33A6	 4466()?)-?????)-????)??????-????-????????()0S0)S0000)S000)000S0000000 7s   %E)E&D?E)c                x  K   d}t         |k(  }|st        j                  d|fdt         |f      dt        j                         v st        j
                  t               rt        j                  t               ndt        j                  |      dz  }dd|iz  }t        t        j                  |            dx}}yw)	z*WB: threshold constant = 100 in contracts.re  r   r   r   r   r   r   N)	r   r   r   r   r   r   r   r   r   r&   r   r   r   r   s        r'   test_threshold_hardcoded_at_100z0TestDPOReadiness.test_threshold_hardcoded_at_1006  se      &)(!S((((!S((((((!(((!(((S(((((((s   B8B:c                  K   | j                  ddd      }|j                          d{   }d}||v }|st        j                  d|fd||f      t        j                  |      d	t        j                         v st        j                  |      rt        j                  |      nd	d
z  }dd|iz  }t        t        j                  |            dx}}|d   }t        |t              }|sddt        j                         v st        j                  t              rt        j                  t              ndt        j                  |      dt        j                         v st        j                  t              rt        j                  t              ndt        j                  |      dz  }t        t        j                  |            dx}}y7 w)z*WB: annotator_dist groups by annotator_id.rY        r[  Nannotator_distr#  rE  r   r  r   r   z5assert %(py5)s
{%(py5)s = %(py0)s(%(py2)s, %(py3)s)
}
isinstancedict)r   r   r   r   )rW  r`  r   r   r   r   r   r   r   r   rs  rt  )	r&   r$   r   r   r   r   r   r   r   s	            r'   %test_annotator_distribution_in_resultz6TestDPOReadiness.test_annotator_distribution_in_result;  s     33BBB3G	 4466)6))))6)))))))))6)))6))))))) !129z2D99999999z999z9992999999D999D9999999999 7s   (GGF$GN)r   r   )rN  r\   rU  r\   rT  r\   r3   r   r   )r6   r7   r8   r9   rW  r   r   r   rb  rh  rk  rn  ru  r:   r)   r'   rI  rI    s    ' 45++"+-0+	+Z [[* * [[+ + [[1 1 [[) ) [[: :r)   rI  c                  8    e Zd ZdZddZddZddZddZddZy)	TestGenerateFallbackResponsezBB + WB tests for Story 5.06.c                ~   t        d      fddD        }t        |      }|sddt        j                         v st	        j
                  t              rt	        j                  t              ndt	        j                  |      t	        j                  |      dz  }t        t	        j                  |            dx}}y)u8   BB: booking-related input → response mentions booking.zI want to book an appointmentc              3  B   K   | ]  }|j                         v   y wr"   lower.0kwresps     r'   	<genexpr>zSTestGenerateFallbackResponse.test_booking_input_mentions_booking.<locals>.<genexpr>N  s     Z"2%Z   )bookappointmentdetailhelp,assert %(py4)s
{%(py4)s = %(py0)s(%(py2)s)
}anyr   N)	r   r  r   r   r   r   r   r   r   )r&   r   r   r   r  s       @r'   #test_booking_input_mentions_bookingz@TestGenerateFallbackResponse.test_booking_input_mentions_bookingK  sn    *+JKZ0YZZsZZZZZZZZZsZZZsZZZZZZZZZZZZZZr)   c                  	 t        d      	t        	      }d}||kD  }|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}}	fddD        }t        |      }|sddt	        j
                         v st        j                  t              rt        j                  t              ndt        j                  |      t        j                  |      dz  }t        t        j                  |            d
x}}y
)u/   BB: complaint input → neutral acknowledgment.z#I have a complaint about my servicer   >z/%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} > %(py6)sr_   r  r  r  r   Nc              3  B   K   | ]  }|j                         v   y wr"   rz  r|  s     r'   r  zPTestGenerateFallbackResponse.test_complaint_input_neutral_ack.<locals>.<genexpr>T  s     ]"2%]r  )feedbacknotedshare
appreciater  r  r   )r   r_   r   r   r   r   r   r   r   r   r  )
r&   r   r   r   r   r  r   r   r   r  s
            @r'    test_complaint_input_neutral_ackz=TestGenerateFallbackResponse.test_complaint_input_neutral_ackP  s    *+PQ4y1y1}y1ss44y1]0\]]s]]]]]]]]]s]]]s]]]]]]]]]]]]]]r)   c           
     
   d}t        |      }t        |      }d}||kD  }|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                  t               rt        j                  t               ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }dd	|iz  }t        t        j                  |            d
x}x}x}x}}d}t        |      }t        |      }d}||kD  }|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                  t               rt        j                  t               ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }dd	|iz  }t        t        j                  |            d
x}x}x}x}}d}t        |      }t        |      }d}||kD  }|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                  t               rt        j                  t               ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }dd	|iz  }t        t        j                  |            d
x}x}x}x}}d}d}||z  }t        |      }t        |      }d}	||	kD  }
|
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                  t               rt        j                  t               ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      t        j                  |	      dz  }dd|iz  }t        t        j                  |            d
x}x}x}x}x}x}
}	y
)z0BB: response is never empty regardless of input.rC   r   r  )zN%(py7)s
{%(py7)s = %(py0)s(%(py5)s
{%(py5)s = %(py1)s(%(py3)s)
})
} > %(py10)sr_   r   r   r   r   r   r   r   r   r   Nz   ?r   i  )z\%(py10)s
{%(py10)s = %(py0)s(%(py8)s
{%(py8)s = %(py1)s((%(py3)s * %(py5)s))
})
} > %(py13)sr   r   r   r   r   r   py13assert %(py15)spy15
r   r_   r   r   r   r   r   r   r   r   r&   r   r   r@  r   @py_assert8r   r   r   @py_assert12@py_assert11@py_format14@py_format16s                r'   test_never_emptyz-TestGenerateFallbackResponse.test_never_emptyV  s   /17.r27s237a73a77773a777777s777s777777.777.777r77727773777a7777777/4:.u5:s56::6::::6::::::s:::s::::::.:::.:::u:::5:::6::::::::::/28.s38s348q84q88884q888888s888s888888.888.888s88838884888q8888888/2?T?sTz?.z:?s:;?a?;a????;a??????s???s??????.???.???s???T???:???;???a????????r)   c           
        d}t        |      }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                  t               rt        j                  t               ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }dd	|iz  }t        t        j                  |            d
x}x}x}x}}d}d}||z  }t        |      }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                  t               rt        j                  t               ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      t        j                  |	      dz  }dd|iz  }t        t        j                  |            d
x}x}x}x}x}x}
}	y
)z%WB: response always < 500 characters.short  <)zN%(py7)s
{%(py7)s = %(py0)s(%(py5)s
{%(py5)s = %(py1)s(%(py3)s)
})
} < %(py10)sr_   r   r  r   r   Nr   i'  )z\%(py10)s
{%(py10)s = %(py0)s(%(py8)s
{%(py8)s = %(py1)s((%(py3)s * %(py5)s))
})
} < %(py13)sr  r  r  r  r  s                r'   test_response_under_500_charsz:TestGenerateFallbackResponse.test_response_under_500_chars]  s   /6>.w7>s78>3>83>>>>83>>>>>>s>>>s>>>>>>.>>>.>>>w>>>7>>>8>>>3>>>>>>>/2CVCsV|C.|<Cs<=CC=CCCC=CCCCCCsCCCsCCCCCC.CCC.CCCsCCCVCCC<CCC=CCCCCCCCCCCr)   c                   t        d      }t        d      }t        d      }t        d      }||||fD ]#  }t        |      }d}||kD  }|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}}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}}& y)z3WB: different keywords trigger different templates.zbook a slotzthis is a problemzwhat time does it open?zhello therer   r  r  r_   r  r  r  r   Nr  r  )z/%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} < %(py6)sr  )r&   booking_respcomplaint_respquestion_respdefault_respr  r   r   r   r   r  s              r'   "test_template_selection_by_keywordz?TestGenerateFallbackResponse.test_template_selection_by_keywordb  s\   2=A45HI34MN2=A ">=,O 	#Dt9 q 9q=   9q      3   3      t   t   9   q       t9"s"9s?"""9s""""""3"""3""""""t"""t"""9"""s"""""""	#r)   Nr   )	r6   r7   r8   r9   r  r  r  r  r  r:   r)   r'   rw  rw  H  s"    '[
^@D

#r)   rw  c                     e Zd ZdZej
                  j                  dd       Zej
                  j                  dd       Zej
                  j                  dd       Z	ej
                  j                  dd       Z
ej
                  j                  	 	 dd       ZddZddZej
                  j                  dd	       Zy
)TestTelegramHandlerzBB + WB tests for Story 5.07.c                  K   t               }t        t        j                  ddd            |j                  _        t               }t        ddd||       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}||u }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            d	x}x}}y	7 w)u4   BB: valid positive callback → pair_generated=True.r   Hir   r@   z!feedback:interaction-abc:positive90  c   callback_datachat_id
message_idr$   r   Nstatusrecordedr   r   r   r   r   pair_generatedTr   r]  rr   r	   ra   rb   rk   rO   r   r   r   r   r   r   r   	r&   r$   r   r   r   r   r   r   r   s	            r'   "test_valid_positive_generates_pairz6TestTelegramHandler.test_valid_positive_generates_pairv  s     $%	(74$PQ 
	 g,=
 
 h-:-:----:------:-------&'/4/'4////'4///'///4///////
   AE#E DE#c                  K   t               }t        t        j                  ddd            |j                  _        t               }t        ddd||       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}||u }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            d	x}x}}y	7 w)u4   BB: valid negative callback → pair_generated=True.Requestr  r   r@   z!feedback:interaction-def:negativer  re  r  Nr  r  r   r   r   r   r   r  Tr   r]  r  r  s	            r'   "test_valid_negative_generates_pairz6TestTelegramHandler.test_valid_negative_generates_pair  s     $%	(9Z$XY 
	 g,=
 
 h-:-:----:------:-------&'/4/'4////'4///'///4///////
r  c                  K   t               }t        ddd|       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}||v }|st        j                  d|fd||f      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      nddz  }dd|iz  }	t        t        j                  |	            dx}}|d   }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7 w)u9   BB: malformed callback_data → error dict, no exception.bad_datar  e   r  r  r  r$   Nr  errorr   r   r   r   r   r#  rE  r   r  r   r   r  Fr   r]  )
rr   r   r   r   r   r   r   r   r   r   )
r&   r$   r   r   r   r   r   r   r   r   s
             r'   %test_malformed_callback_returns_errorz9TestTelegramHandler.test_malformed_callback_returns_error  s0     $%	,$	
 
 h*7*7****7******7******* w&    w&   w      &   &       &'050'50000'5000'00050000000
s   GG	F*Gc                R  K   t               }t        ddd|       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7 w)u    BB: wrong prefix → error dict.zaction:interaction-xyz:positiver  f   r  Nr  r  r   r   r   r   r   )rr   r   r   r   r   r   r   ra  s           r'   test_wrong_prefix_returns_errorz3TestTelegramHandler.test_wrong_prefix_returns_error  s      $%	,;	
 
 h*7*7****7******7*******
s   B'B%BB'c                  K   t               }t        t        j                  ddd            |j                  _        g |j                  	 d	 	 	 	 	 	 	 	 	 dfd}||_        t               }t        dd	d
||       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
   }	t$        j&                  }|	|k(  }|st        j                  d|fd|	|f      t        j                  |	      dt        j                         v st        j                  t$              rt        j                  t$              ndt        j                  |      dz  }
dd|
iz  }t!        t        j"                  |            dx}	x}}y7 w)zCWB: handle_callback_query calls record_feedback with parsed signal.r  r  r   r@   Nr   c                X   K   j                  |        | |||       d {   S 7 wr"   r  )r   r   r   contextoriginalrecorded_signalss       r'   _capturez_TestTelegramHandler.test_collector_record_feedback_called_with_correct_signal.<locals>._capture  s.      ##F+!)^VWMMMMs    *(*zfeedback:i-wbtest:negativeo   r   r  r  r   r  r_   r  r  r  r   )z0%(py1)s == %(py5)s
{%(py5)s = %(py3)s.NEGATIVE
})r   r   r   r   r   r"   )
r   r   r   r5   r   r   r  zOptional[Dict]r3   zOptional[PreferencePair])rr   r	   ra   rb   rk   rO   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  s               @@r'   9test_collector_record_feedback_called_with_correct_signalzMTestTelegramHandler.test_collector_record_feedback_called_with_correct_signal  s    
 $%	(4$NO 
	 24,, '+		N	N	N #	N $		N
 &	N %-	!g#6
 	
 	
 #$))$))))$))))))s)))s))))))#)))#)))$))))))))))"=n&=&=="&====="&===="======n===n===&========	
s   B I4I1G-I4c                ^   t        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  }dd|iz  }t        t        j                  |            d	x}}|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   }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 ]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}}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}}P y	)z#BB: keyboard has correct structure.zinteraction-kb-01inline_keyboardr#  rE  kbr  r   r   Nr  r   r  r_   buttonsr  r  r   r   r5  rowtextbtnr  )
r   r   r   r   r   r   r   r   r   r_   )r&   r  r   r   r   r   r  r   r   r   r  r  r  s                r'   &test_build_feedback_keyboard_structurez:TestTelegramHandler.test_build_feedback_keyboard_structure  sT   $%89 & B&&&& B&&& &&&&&&B&&&B&&&&&&&&'7| q |q    |q      s   s      7   7   |   q       aj3x1x1}x1ss33x1 	*C 6S=   6S   6      S   S       ")?c))))?c)))?))))))c)))c)))))))	*r)   c                J   d}t        |      }|d   d   }|D ]  }|d   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   }|t        k(  }|st        j                  d|fd|t        f      t        j                  |      dt        j                         v st        j                  t              rt        j                  t              nddz  }dd|iz  }t        t        j                  |            dx}}|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}||v }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }	t        t        j                  |	            dx}x}} y)z:WB: callback_data follows 'feedback:{id}:{signal}' format.ztest-interaction-999r  r   r  r      r   r  r_   partsr  r  r   Nr  r   r  r   r   r  r   r5  )positivenegativer#  )z%(py1)s in %(py4)sr   r   r   )r   splitr_   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   s                   r'   ,test_build_feedback_keyboard_callback_formatz@TestTelegramHandler.test_build_feedback_keyboard_callback_format  s   $$S)"#A& 	8C(..s3Eu:"":?""":""""""3"""3""""""u"""u""":""""""""""8/8/////8////8//////////////////8"8s?"""8s"""8""""""s"""s"""""""877787777787777877777777777	8r)   c                h  K   t               }t               }t        ddd||       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}||u }|slt        j                  d|fd||f      t        j
                  |      t        j
                  |      d
z  }dd|iz  }t        t        j                  |            dx}x}}y7 w)u,   BB: neutral signal → pair_generated=False.z feedback:interaction-neu:neutrali  r   r  Nr  r  r   r   r   r   r   r  Fr   r]  )rr   r   r   r   r   r   r   r   r  s	            r'   3test_neutral_telegram_callback_pair_generated_falsezGTestTelegramHandler.test_neutral_telegram_callback_pair_generated_false  s      $%	g,<
 
 h-:-:----:------:-------&'050'50000'5000'00050000000
rc  Nr   )r6   r7   r8   r9   r   r   r   r  r  r  r  r  r  r  r  r:   r)   r'   r  r  s  s    '[[0 0" [[0 0" [[1 1 [[	+ 	+ [[>	> >B
*
8 [[1 1r)   r  c                  Z    e Zd ZdZ	 d	 	 	 ddZddZddZddZddZddZ	dd	Z
dd
Zy)TestFeedbackWebhookzBB + WB tests for Story 5.08.Nc                    ddl m} ddlm}  |       }|
t	               }t        |      }|j                  |        ||      S )z3Build a TestClient for the feedback webhook router.r   FastAPI
TestClient)fastapir  fastapi.testclientr  rr   r   include_router)r&   r$   r  r  approuters         r'   _make_test_clientz%TestFeedbackWebhook._make_test_client  sA    
 	$1i')I'	26"#r)   c                   t               }d}t               }i t         d| d| }t        j                  ddd      |<   dfd}t        |      |j                  _        | j                  |      }|j                  dt        |      |d	d
      }|j                  }d}	||	k(  }
|
st        j                  d|
fd||	f      dt        j                         v st        j                   |      rt        j"                  |      ndt        j"                  |      t        j"                  |	      dz  }dd|iz  }t%        t        j&                  |            dx}x}
}	|j                         }|d   }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)u#   BB: valid positive payload → 200.zwh-001r   hihellor   c                .   K   j                  |       S wr"   rN   )krQ   s    r'   rR   zGTestFeedbackWebhook.test_valid_positive_returns_200.<locals>._redis_get  s     99Q<rS   rW   /api/v1/feedback/webhookr  r   ra      r   z3%(py2)s
{%(py2)s = %(py0)s.status_code
} == %(py5)sr  r   r   r   Nr  r  r   r   r   r   )r  r5   r3   rY   )r   rr   r   ra   rb   r	   rk   rO   r  postr5   status_coder   r   r   r   r   r   r   r   )r&   r   r   r$   rP   rR   clientr  r   r   r   r   r   datar   r   r   r   rQ   s                     @r'   test_valid_positive_returns_200z3TestFeedbackWebhook.test_valid_positive_returns_200  si   g#%	 "()3%q6ZZtG LMc
	   )Z@	''	2{{& X"%$  
 &3&3&&&&3&&&&&&t&&&t&&&&&&3&&&&&&&yy{H~++~++++~+++~++++++++++r)   c                   t               }| j                  |      }|j                  dt        t	                     ddd      }|j
                  }d}||k(  }|st        j                  d|fd||f      d	t        j                         v st        j                  |      rt        j                  |      nd	t        j                  |      t        j                  |      d
z  }dd|iz  }t        t        j                  |            dx}x}}y)u#   BB: invalid signal 'maybe' → 422.r  z
wh-invalidmayber   r    r   r  r  r   r   r   N)rr   r  r  r5   r   r  r   r   r   r   r   r   r   r   	r&   r$   r  r  r   r   r   r   r   s	            r'   test_invalid_signal_returns_422z3TestFeedbackWebhook.test_invalid_signal_returns_4220  s    #%	''	2{{& \".!  
 &3&3&&&&3&&&&&&t&&&t&&&&&&3&&&&&&&r)   c                P   t               }t        d      |j                  _        | j	                  |      }|j                  dt        t                     ddd      }|j                  }d}||k(  }|st        j                  d	|fd
||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                   |            dx}x}}y)u#   BB: unknown interaction_id → 404.Nr@   r  r  r  r   r  i  r   r  r  r   r   r   )rr   r	   rk   rO   r  r  r5   r   r  r   r   r   r   r   r   r   r   r  s	            r'   $test_unknown_interaction_returns_404z8TestFeedbackWebhook.test_unknown_interaction_returns_404>  s    #%	(d;	''	2{{& \"2$  
 &3&3&&&&3&&&&&&t&&&t&&&&&&3&&&&&&&r)   c                    ddl }t        j                  |j                  t        f      5  t        t               dd       ddd       y# 1 sw Y   yxY w)z*WB: Pydantic model rejects invalid signal.r   NtestINVALID_SIGNALr   )pydanticr   r   ValidationErrorr   r   r   )r&   r	  s     r'   #test_pydantic_validates_signal_enumz7TestFeedbackWebhook.test_pydantic_validates_signal_enumN  sD    ]]H44jAB 	"'%'	 	 	s   A

Ac                t   h d}t         |k(  }|st        j                  d|fdt         |f      dt        j                         v st        j
                  t               rt        j                  t               ndt        j                  |      dz  }dd|iz  }t        t        j                  |            dx}}y)	z0WB: valid signals are positive/negative/neutral.>   neutralr  r  r   r   r   r   r   r   N)	r   r   r   r   r   r   r   r   r   rm  s        r'   test_valid_signals_setz*TestFeedbackWebhook.test_valid_signals_setY  s^    !DD~!DDDDD~!DDDDDDD~DDD~DDD!DDDDDDDDr)   c                   t         D ]  }|t        v }|st        j                  d|fd|t        f      dt	        j
                         v st        j                  |      rt        j                  |      nddt	        j
                         v st        j                  t              rt        j                  t              nddz  }dd|iz  }t        t        j                  |            d} y)	z1WB: all valid signals map to FeedbackSignal enum.r#  )z%(py0)s in %(py2)ssigr   )r   r   zassert %(py4)sr   N)
r   r   r   r   r   r   r   r   r   r   )r&   r  r   @py_format3r   s        r'   test_signal_enum_map_coveragez1TestFeedbackWebhook.test_signal_enum_map_coverage]  s}    ! 	+C*****3*******3***3******************	+r)   c                4   t               }| j                  |      }|j                  dt        t	                     ddd      }|j
                  }d}||k(  }|st        j                  d|fd||f      d	t        j                         v st        j                  |      rt        j                  |      nd	t        j                  |      t        j                  |      d
z  }dd|iz  }t        t        j                  |            dx}x}}|j                         d   }	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)u1   BB: neutral signal → 200, pair_generated=False.r  z
neutral-whr  r   r  r  r   r  r  r   r   r   Nr  Fr   r]  r   r   r   )rr   r  r  r5   r   r  r   r   r   r   r   r   r   r   ra   )r&   r$   r  r  r   r   r   r   r   r   r   r   r   s                r'   (test_neutral_webhook_returns_200_no_pairz<TestFeedbackWebhook.test_neutral_webhook_returns_200_no_pairb  s   #%	''	2{{& \".#  
 &3&3&&&&3&&&&&&t&&&t&&&&&&3&&&&&&&yy{+,55,5555,555,5555555555r)   r"   )r$   zOptional[FeedbackCollector]r3   r   r   )r6   r7   r8   r9   r  r  r  r  r  r  r  r  r:   r)   r'   r  r    sE    ' 26. 
,8'' 	E+
6r)   r  c                  t   e Zd ZdZej
                  j                  d
d       Zej
                  j                  d
d       Zej
                  j                  d
d       Z	ej
                  j                  d
d       Z
ej
                  j                  d
d       Zd
dZej
                  j                  d
d       Zy	)TestFeedbackIntegrationz0Full lifecycle integration tests for Story 5.09.c                  K   t               }t               }d}|j                  ||dd       d{    |j                  ||t        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}}|j                  }d}	||	k(  }
|
st        j                  d|
fd||	f      d	t        j                         v st        j                  |      rt        j                  |      nd	t        j                  |      t        j                  |	      dz  }dd|iz  }t        t        j                  |            dx}x}
}	|j                  }|t         k(  }
|
st        j                  d|
fd|t         f      d	t        j                         v st        j                  |      rt        j                  |      nd	t        j                  |      dt        j                         v st        j                  t               rt        j                  t               nddz  }dd|iz  }t        t        j                  |            dx}}
y7 7 ww)u>   BB: cache interaction → positive feedback → pair verified.z	integ-001zCan I book a table?zSure, what time works for you?r  Nr   r   r   r   r   r   r   r   )z5%(py2)s
{%(py2)s = %(py0)s.chosen_output
} == %(py5)sr   r   r   r   r   r   r   r   )rr   r   r  r   r   r   r   r   r   r   r   r   r   r   rD   rF   r   )r&   r$   r   r   r   r   r   r   r   r   r   r   r   r   s                 r'   test_full_lifecycle_positivez4TestFeedbackIntegration.test_full_lifecycle_positivez  s     $%	g)),8	 * 
 	
 	
 ..!** / 
 
  t4t4tt4!!E%EE!%EEEEE!%EEEEEEEtEEEtEEE!EEE%EEEEEEEE  6 $66666 $6666666t666t666 666666$6666$66666666	

s"   /KK)KKI4KKc                  K   t               }t               }d}d}|j                  ||d|       d{    |j                  ||t        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}}|j                  }||k(  }
|
st        j                  d|
fd||f      d	t        j                         v st        j                  |      rt        j                  |      nd	t        j                  |      dt        j                         v st        j                  |      rt        j                  |      nddz  }dd|iz  }t        t        j                  |            dx}}
y7 7 w)z<BB: negative feedback creates inverted pair (AI = rejected).z	integ-002zI cannot help with that.zI need help urgentlyr  Nr   r   r   r   r   r   r   r   r   ai_respr   r   r   )rr   r   r  r   r   r   r   r   r   r   r   r   r   r   rE   )r&   r$   r   r   r  r   r   r   r   r   r   r   r   s                r'   $test_negative_feedback_inverted_pairz<TestFeedbackIntegration.test_negative_feedback_inverted_pair  s\     $%	g,))-	 * 
 	
 	
 ..!** / 
 
  t4t4tt4##.#w....#w......t...t...#......w...w.......	

s"   1G?G9)G?G<FG?<G?c                  K   t               }|j                  }|j                  t               dt        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}}|j                  j                          y7 ĭw)
u:   BB: neutral → no pair, no DB write (zero execute calls).z	integ-003r   Nr   r   r   r   r   r   )rr   rh   r   r   r   r   r   r   r   r   r   r   r   r   rd   r   )r&   r$   r%   r   r   r   r   r   s           r'    test_neutral_no_pair_no_db_writez8TestFeedbackIntegration.test_neutral_no_pair_no_db_write  s      $%	((	 00g&!)) 1 
 
 v~vvv++-
s   ADDCDc                h  K   t        t        t              }t               }t        ddi      |_        t        t        d      D cg c]  }dt        j                  ddi      i c}t        d	      D cg c]  }dt        j                  dd
i      i c}z   t        ddgg      |_	        t               }t        t        |            |_        ||_        t               }t        d      |_        t        d      |_        ||_        |j#                          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}	}|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c c}w c c}w 7 w)u4   BB: DPO readiness check at 100 pairs → ready=True.r<   r?   re  r@   rf  rI   r   r   rg  r   rQ  rW   Nr\  Tr   r]  r   r   r   r^  r   r   )r   r]   r^   r	   r`   r/  ra   rb   r   rc   r
   re   rf   rg   rO   rj   rk   r`  r   r   r   r   r   )r&   r$   r%   r{   rp   rq   r   r   r   r   r   r   s               r'   test_dpo_readiness_at_100z1TestFeedbackIntegration.test_dpo_readiness_at_100  s     &Y+N	K	&UCLA	#GLRyQ!j$**h
%;<=QINrSA
DJJ*'=>?ST0=>1
 	 K	%3G	3RS	&	[
"5
"5
%	 4466g&$&$&&&&$&&&&&&$&&&&&&&l#*s*#s****#s***#***s*******! RS 7s,   AH2 H%'H26 H*
B
H2 H/!DH2c                  K   t               }t        t        j                  ddd            |j                  _        t               }t        ddd||       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}||u }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            d	x}x}}y	7 w)z,BB: Telegram callback parsing (integration).r  okr   r@   zfeedback:integ-tg-001:positivei	  r   r  Nr  r  r   r   r   r   r   r  Tr   r]  r  r  s	            r'   test_telegram_callback_parsingz6TestFeedbackIntegration.test_telegram_callback_parsing  s     $%	(6$$OP 
	 g,:
 
 h-:-:----:------:-------&'/4/'4////'4///'///4///////
r  c                D   t               }ddlm} ddlm}  |       }t        |      }|j                  |        ||      }|j                  ddddd	      }|j                  }d
}	||	k(  }
|
st        j                  d|
fd||	f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |	      dz  }dd|iz  }t        t        j                   |            dx}x}
}	y)z)BB: webhook rejects bad tenant_id format.r   r  r  r  z
not-a-uuidr   r  r   r  r  r   r  r  r   r   r   N)rr   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   s                r'    test_webhook_endpoint_validationz8TestFeedbackIntegration.test_webhook_endpoint_validation  s    #%	#1i'	26"C{{&)"%$  
 &3&3&&&&3&&&&&&t&&&t&&&&&&3&&&&&&&r)   c                  K   t               }t        t        j                  ddd            |j                  _        |j                  }g dfd}t        |      |_        |j                  t               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   }t*        |v }	|	st        j                  d|	fdt*        |f      dt        j                          v st        j"                  t*              rt        j$                  t*              ndt        j$                  |      dz  }
dd|
iz  }t'        t        j(                  |            d
x}	}y
7 ѭw)z5WB: record_feedback inserts into pl_preference_pairs.r  r  r   r@   rL  c                6   K   j                  | |d       y w)N)rL  argsr  )rL  r'  execute_callss     r'   _capture_executezZTestFeedbackIntegration.test_writes_to_pl_preference_pairs_table.<locals>._capture_execute  s       d!;<s   rW   zwb-insert-001r   Nr  r   r  r_   r(  r  r  r   r   r#  r$  r   r   r   r   )rL  r5   r'  r   r3   r4   )rr   r	   ra   rb   rk   rO   rh   rd   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(  s               @r'   (test_writes_to_pl_preference_pairs_tablez@TestFeedbackIntegration.test_writes_to_pl_preference_pairs_table  s     $%	(4$NO 
	 ((	#%	= &2BC	''g*!** ( 
 	
 	
 =!&Q&!Q&&&&!Q&&&&&&s&&&s&&&&&&=&&&=&&&!&&&Q&&&&&&&+A.u55{55555{5555555{555{55555555555	
s   BI%I"GI%Nr   )r6   r7   r8   r9   r   r   r   r  r  r  r  r"  r$  r*  r:   r)   r'   r  r  w  s    :[[7 7. [[/ /. [[. . [[+ +2 [[0 0$'. [[6 6r)   r  )NN)rl   zOptional[List[Dict]]rm   zOptional[Dict[str, str]]r3   r   )Br9   
__future__r   builtinsr   _pytest.assertion.rewrite	assertionrewriter   ra   syspathlibr   typingr   r   r   r   unittest.mockr	   r
   r   uuidr   r   r   pathinsertr5   __file__parentcore.rlm.contractsr   r   core.rlm.feedbackr   r   r   r   r   r   r   core.rlm.feedback_telegramr   r   r   r   core.rlm.feedback_webhookr   r   r   r   r]   r^   r    rr   re   r~   r   r   r(  rI  rw  r  r  r  r:   r)   r'   <module>r=     s=  . #    
  , , 5 5   3tH~,,33::; < =     <	(1 1& %)+/:!:(: :z
 
"%D %DX{: {:DT@ T@v86 86~S: S:t$# $#VH1 H1^n6 n6j\6 \6r)   