Files
rpa_vision_v3/tests/integration/test_feedback_bus_client.py
Dom 41eba898c0 feat(agent_v1): FeedbackBusClient — client SocketIO pour bus 'lea:*'
Consomme les events 'lea:*' émis par agent_chat (port 5004) et les dispatche
vers un callback fourni par ChatWindow (J3.3 à venir).

Caractéristiques :
- Connexion en thread daemon (non-bloquant pour la mainloop tkinter)
- Reconnect auto illimité (delay 2s → 30s exponentiel)
- Auth Bearer Token via header HTTP au handshake
- Fail-safe : connect échoué, callback qui raise, disconnect qui raise
  → tout silencieusement loggé, ChatWindow continue normalement

13 tests pytest verts (tests/integration/test_feedback_bus_client.py).
Pas de connexion réseau réelle dans les tests (python-socketio mocké).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 08:43:26 +02:00

126 lines
4.0 KiB
Python

"""Tests FeedbackBusClient (J3.2).
On mock python-socketio pour ne pas ouvrir de vraie connexion réseau.
Le test E2E réel (vraie connexion bus 5004) est différé à J4.3.
"""
from unittest.mock import patch
import pytest
from agent_v0.agent_v1.network.feedback_bus import FeedbackBusClient, LEA_EVENTS
def test_init_creates_socketio_client():
bus = FeedbackBusClient("http://localhost:5004")
assert bus._sio is not None
assert bus.connected is False
def test_init_strips_trailing_slash():
bus = FeedbackBusClient("http://localhost:5004/")
assert bus._url == "http://localhost:5004"
def test_lea_events_registered():
bus = FeedbackBusClient("http://localhost:5004")
handlers = bus._sio.handlers.get('/', {})
for ev in LEA_EVENTS:
assert ev in handlers, f"Handler {ev!r} non enregistré sur le client"
def test_dispatch_calls_callback():
received = []
bus = FeedbackBusClient(
"http://localhost:5004",
on_event=lambda e, p: received.append((e, p)),
)
bus._dispatch('lea:paused', {'workflow': 'demo', 'reason': 'incertain'})
assert received == [('lea:paused', {'workflow': 'demo', 'reason': 'incertain'})]
def test_dispatch_handles_none_payload():
received = []
bus = FeedbackBusClient(
"http://localhost:5004",
on_event=lambda e, p: received.append((e, p)),
)
bus._dispatch('lea:done', None)
assert received == [('lea:done', {})]
def test_dispatch_silenced_on_callback_error():
"""Une exception dans le callback consommateur ne doit jamais remonter."""
def boom(event, payload):
raise RuntimeError("callback fail")
bus = FeedbackBusClient("http://localhost:5004", on_event=boom)
bus._dispatch('lea:paused', {}) # ne doit pas raise
def test_default_callback_is_silent():
"""Sans callback fourni, le dispatch ne casse pas."""
bus = FeedbackBusClient("http://localhost:5004")
bus._dispatch('lea:paused', {'x': 1}) # ne doit pas raise
def test_token_in_authorization_header():
bus = FeedbackBusClient("http://localhost:5004", token="abc123")
captured = {}
def fake_connect(url, headers=None, **kwargs):
captured['headers'] = headers
raise RuntimeError("stop here")
with patch.object(bus._sio, 'connect', side_effect=fake_connect):
bus._run()
assert captured['headers']['Authorization'] == 'Bearer abc123'
def test_no_token_means_no_auth_header():
bus = FeedbackBusClient("http://localhost:5004")
captured = {}
def fake_connect(url, headers=None, **kwargs):
captured['headers'] = headers
raise RuntimeError("stop here")
with patch.object(bus._sio, 'connect', side_effect=fake_connect):
bus._run()
assert 'Authorization' not in captured['headers']
def test_run_silenced_on_connect_error():
"""connect() qui raise ne doit pas faire crasher le thread."""
bus = FeedbackBusClient("http://localhost:5004")
with patch.object(bus._sio, 'connect', side_effect=ConnectionError("boom")):
bus._run() # ne doit pas raise
def test_start_is_idempotent():
"""Un second start() pendant que le thread tourne ne doit pas en créer un autre."""
import threading
bus = FeedbackBusClient("http://localhost:5004")
block = threading.Event()
with patch.object(bus, '_run', side_effect=lambda: block.wait(timeout=2)):
bus.start()
first_thread = bus._thread
bus.start()
second_thread = bus._thread
block.set()
assert first_thread is second_thread, "start() doit être idempotent quand un thread tourne"
def test_stop_when_not_connected_is_silent():
bus = FeedbackBusClient("http://localhost:5004")
bus.stop() # ne doit pas raise même si jamais connecté
def test_stop_silenced_on_disconnect_error():
bus = FeedbackBusClient("http://localhost:5004")
# Forcer connected=True sur l'instance et faire raise disconnect()
with patch.object(bus._sio, 'disconnect', side_effect=RuntimeError("boom")):
bus._sio.connected = True
bus.stop() # ne doit pas raise