feat(analytics): normalise API + contrat explicite get_next_action (Lot A)

Contrat get_next_action() — suppression du None ambigu :
  {"status": "selected", "edge": ..., ...}
  {"status": "terminal"}
  {"status": "blocked", "reason": "no_valid_edge" | ...}

ExecutionLoop dispatche proprement : blocked -> PAUSED + _pause_requested,
terminal -> succès légitime. Rétrocompat défensive (None legacy -> blocked).

Analytics API normalisée (kwargs-only) :
  on_execution_complete(duration_ms, status, steps_total|completed|failed)
  on_step_complete(duration_ms, ...)
  on_recovery_attempt(duration_ms, ...)

Découverte critique : les anciens appels utilisaient des méthodes et champs
inexistants (ExecutionMetrics.duration, metrics_collector.record_execution).
Le code n'avait jamais tourné au runtime — zéro analytics remontée.
L'exception était avalée par le try/except englobant.

58 tests (18 analytics + 11 contrat + 20 ExecutionLoop + 12 edge_scorer
non-régression). Migration complète, pas de pont legacy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-15 09:06:19 +02:00
parent 42f571d496
commit af4ffa189a
9 changed files with 1573 additions and 233 deletions

View File

@@ -96,14 +96,16 @@ class TestWorkflowPipelineEnhanced:
"confidence": 0.92
}
# Mock de l'action suivante
# Mock de l'action suivante (contrat dict normalisé Lot A)
mock_workflow_pipeline.get_next_action.return_value = {
"status": "selected",
"edge_id": "edge_1",
"action": {"type": "click", "target": "button"},
"target_node": "node_2",
"confidence": 0.95
"confidence": 0.95,
"score": 0.95,
}
# Mock du workflow
mock_workflow = Mock(spec=Workflow)
mock_edge = Mock(spec=WorkflowEdge)
@@ -112,7 +114,7 @@ class TestWorkflowPipelineEnhanced:
mock_edge.to_node = "node_2"
mock_workflow.edges = [mock_edge]
mock_workflow_pipeline.load_workflow.return_value = mock_workflow
# Mock du résultat d'exécution
mock_execution_result = Mock(spec=ExecutionResult)
mock_execution_result.status = ExecutionStatus.SUCCESS
@@ -121,24 +123,24 @@ class TestWorkflowPipelineEnhanced:
mock_execution_result.target_resolved = None
mock_execution_result.error = None
mock_workflow_pipeline.action_executor.execute_edge.return_value = mock_execution_result
# Créer l'instance enhanced
enhanced = WorkflowPipelineEnhanced()
# Lier les méthodes du pipeline mock
enhanced.match_current_state = mock_workflow_pipeline.match_current_state
enhanced.get_next_action = mock_workflow_pipeline.get_next_action
enhanced.load_workflow = mock_workflow_pipeline.load_workflow
enhanced.action_executor = mock_workflow_pipeline.action_executor
enhanced.error_handler = mock_workflow_pipeline.error_handler
# Act
result = enhanced.execute_workflow_step_enhanced(
workflow_id=workflow_id,
current_state=mock_screen_state,
context={"test_context": "value"}
)
# Assert
assert isinstance(result, WorkflowExecutionResult)
assert result.success is True
@@ -242,7 +244,8 @@ class TestWorkflowPipelineEnhanced:
}
# Mock de l'action suivante (pas d'action = workflow terminé)
mock_workflow_pipeline.get_next_action.return_value = None
# Contrat dict normalisé Lot A : status="terminal" pour fin légitime
mock_workflow_pipeline.get_next_action.return_value = {"status": "terminal"}
# Créer l'instance enhanced
enhanced = WorkflowPipelineEnhanced()
@@ -347,14 +350,16 @@ class TestWorkflowPipelineEnhanced:
"confidence": 0.92
}
# Mock de l'action suivante
# Mock de l'action suivante (contrat dict normalisé Lot A)
mock_workflow_pipeline.get_next_action.return_value = {
"status": "selected",
"edge_id": "edge_1",
"action": {"type": "click", "target": "button"},
"target_node": "node_2",
"confidence": 0.95
"confidence": 0.95,
"score": 0.95,
}
# Mock du workflow
mock_workflow = Mock(spec=Workflow)
mock_edge = Mock(spec=WorkflowEdge)
@@ -363,7 +368,7 @@ class TestWorkflowPipelineEnhanced:
mock_edge.to_node = "node_2"
mock_workflow.edges = [mock_edge]
mock_workflow_pipeline.load_workflow.return_value = mock_workflow
# Mock du résultat d'exécution
mock_execution_result = Mock(spec=ExecutionResult)
mock_execution_result.status = ExecutionStatus.SUCCESS
@@ -372,17 +377,17 @@ class TestWorkflowPipelineEnhanced:
mock_execution_result.target_resolved = None
mock_execution_result.error = None
mock_workflow_pipeline.action_executor.execute_edge.return_value = mock_execution_result
# Créer l'instance enhanced
enhanced = WorkflowPipelineEnhanced()
# Lier les méthodes du pipeline mock
enhanced.match_current_state = mock_workflow_pipeline.match_current_state
enhanced.get_next_action = mock_workflow_pipeline.get_next_action
enhanced.load_workflow = mock_workflow_pipeline.load_workflow
enhanced.action_executor = mock_workflow_pipeline.action_executor
enhanced.error_handler = mock_workflow_pipeline.error_handler
# Act
result = enhanced.execute_workflow_step_enhanced(
workflow_id=workflow_id,