Files
rpa_vision_v3/tests/integration/test_pause_for_human.py
Dom 0e6e61f2b1 feat(workflow): action 'pause_for_human' — pause supervisée scriptée dans VWB
Nouvelle action native VWB qui force le replay à basculer en paused_need_help
avec un message custom. Quand Léa atteint cette étape, elle ne tente pas
d'exécuter — elle pose immédiatement le state, ce qui déclenche la bulle
interactive ChatWindow (J3.5) avec boutons Continuer / Annuler.

Asset démo majeur GHT Sud 95 : permet de scénariser le moment "Léa doute"
au bon endroit dans le workflow, sans dépendre d'un échec aléatoire.

Chaîne complète :
- VWB UI (types.ts) : nouvelle entrée ACTIONS catégorie 'logic', icône ⏸,
  paramètre 'message' éditable (textarea).
- Bridge VWB → core (learned_workflow_bridge.py) : passthrough du type +
  préservation du message dans parameters.
- Pipeline replay (replay_engine.py) : type ajouté à _ALLOWED_ACTION_TYPES,
  conversion edge → action normalisée préserve le message.
- Streaming server (api_stream.py /replay/next) : interception avant envoi
  à l'Agent V1 → bascule state en paused_need_help avec pause_message,
  retourne {action: None, replay_paused: True}.
- L'action n'est jamais transmise à l'Agent V1 — pure logique serveur.

10 nouveaux tests pytest. Total branche : 57/57 verts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 16:37:46 +02:00

132 lines
4.7 KiB
Python

"""Tests de l'action pause_for_human (C.5).
Vérifie la chaîne :
- Validation côté replay_engine accepte le nouveau type
- Conversion edge → action normalisée préserve le message
- Bridge VWB → core mappe correctement
- Le bridge VWB construit bien un edge avec action.type='pause_for_human'
"""
from agent_v0.server_v1.replay_engine import (
_ALLOWED_ACTION_TYPES,
_validate_replay_action,
_edge_to_normalized_actions,
)
from visual_workflow_builder.backend.services.learned_workflow_bridge import (
VWB_ACTION_TO_CORE,
convert_vwb_to_core_workflow,
_vwb_params_to_core,
)
# ----------------------------------------------------------------------
# Validation pipeline (replay_engine)
# ----------------------------------------------------------------------
def test_pause_for_human_in_allowed_types():
assert "pause_for_human" in _ALLOWED_ACTION_TYPES
def test_validate_pause_for_human_action_valid():
action = {"type": "pause_for_human", "parameters": {"message": "Valider UHCD ?"}}
assert _validate_replay_action(action) is None
def test_validate_pause_for_human_no_params_still_valid():
"""Le validateur ne doit pas exiger 'message' (fallback côté handler)."""
action = {"type": "pause_for_human"}
assert _validate_replay_action(action) is None
# ----------------------------------------------------------------------
# Conversion edge → action normalisée
# ----------------------------------------------------------------------
class _FakeAction:
def __init__(self, type_, parameters=None):
self.type = type_
self.target = None
self.parameters = parameters or {}
class _FakeEdge:
def __init__(self, action, edge_id="e1", from_node="n1", to_node="n2"):
self.edge_id = edge_id
self.from_node = from_node
self.to_node = to_node
self.action = action
def test_edge_to_action_pause_for_human_preserves_message():
edge = _FakeEdge(_FakeAction(
"pause_for_human",
parameters={"message": "Tu valides UHCD ?"},
))
actions = _edge_to_normalized_actions(edge, params={})
assert len(actions) == 1
a = actions[0]
assert a["type"] == "pause_for_human"
assert a["parameters"]["message"] == "Tu valides UHCD ?"
assert "x_pct" not in a # action logique, pas de coords
assert "y_pct" not in a
def test_edge_to_action_pause_for_human_default_message():
edge = _FakeEdge(_FakeAction("pause_for_human", parameters={}))
actions = _edge_to_normalized_actions(edge, params={})
assert actions[0]["parameters"]["message"] == "Validation requise"
def test_edge_to_action_pause_for_human_carries_edge_metadata():
edge = _FakeEdge(
_FakeAction("pause_for_human", parameters={"message": "x"}),
edge_id="edge_42", from_node="n_src", to_node="n_dst",
)
actions = _edge_to_normalized_actions(edge, params={})
a = actions[0]
assert a["edge_id"] == "edge_42"
assert a["from_node"] == "n_src"
assert a["to_node"] == "n_dst"
assert "action_id" in a
# ----------------------------------------------------------------------
# Bridge VWB → core
# ----------------------------------------------------------------------
def test_vwb_action_to_core_passthrough():
assert VWB_ACTION_TO_CORE["pause_for_human"] == "pause_for_human"
def test_vwb_params_to_core_preserves_message():
core_params = _vwb_params_to_core("pause_for_human", {"message": "Coucou"})
assert core_params == {"message": "Coucou"}
def test_vwb_params_to_core_default_message():
core_params = _vwb_params_to_core("pause_for_human", {})
assert core_params["message"] == "Validation requise"
def test_export_vwb_workflow_with_pause_step():
"""Un workflow VWB contenant une step pause_for_human doit produire un edge
avec action.type='pause_for_human' et message dans parameters."""
workflow_data = {"id": "wf_demo", "name": "Demo Urgences", "description": ""}
steps_data = [
{"id": "s1", "action_type": "click_anchor", "parameters": {"target_text": "25003284"}, "label": "Clic IPP"},
{"id": "s2", "action_type": "pause_for_human", "parameters": {"message": "Valider UHCD ?"}, "label": "Pause"},
{"id": "s3", "action_type": "click_anchor", "parameters": {"target_text": "Enregistrer"}, "label": "Clic Enregistrer"},
]
core = convert_vwb_to_core_workflow(workflow_data, steps_data)
assert core["learning_state"] == "COACHING"
assert len(core["nodes"]) == 3
assert len(core["edges"]) == 2
# L'edge sortant du node de pause doit avoir le bon type + message
pause_edges = [
e for e in core["edges"]
if e["action"]["type"] == "pause_for_human"
]
assert len(pause_edges) == 1
assert pause_edges[0]["action"]["parameters"]["message"] == "Valider UHCD ?"