feat(navigate): jalon partiel D1 — compile navigate + coercion coords sûre
Ferme Gap C : _edge_to_normalized_actions produit désormais une action navigate (handler serveur atteignable). Ajoute _coerce_action_coords : cast x_pct/y_pct en float APRÈS résolution des templates, JAMAIS de fallback (0,0) — template non résolu / valeur invalide → pause_for_human (safety_level=high). Non-régression prouvée sur mouse_click classiques (idempotent sur floats). ⚠️ NE FERME PAS le write-only : Gap A (P1-B) non livré — aucun step click/type ne déclare encore consommer navigate_login_coords. TestCompilerGapLiteralFloats assert l'état ouvert. Boucle complète = chantier suivant (P1-B + test e2e edge→action). 21 tests verts, boot OK. Revue croisée Claude (GO jalon partiel). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1948,6 +1948,21 @@ def _edge_to_normalized_actions(edge, params: Dict[str, Any]) -> List[Dict[str,
|
||||
normalized["parameters"]["temperature"] = action_params.get("temperature")
|
||||
return [normalized]
|
||||
|
||||
elif action_type == "navigate":
|
||||
normalized["type"] = "navigate"
|
||||
normalized["parameters"] = {
|
||||
"action": action_params.get("action", "login"),
|
||||
"login_coords_var": action_params.get("login_coords_var", "navigate_login_coords"),
|
||||
"password_coords_var": action_params.get("password_coords_var", "navigate_password_coords"),
|
||||
"submit_coords_var": action_params.get("submit_coords_var", "navigate_submit_coords"),
|
||||
}
|
||||
login_config_keys = ("login_field", "password_field", "submit_button",
|
||||
"success_elements", "context")
|
||||
for key in login_config_keys:
|
||||
if action_params.get(key) is not None:
|
||||
normalized["parameters"][key] = action_params[key]
|
||||
return [normalized]
|
||||
|
||||
else:
|
||||
logger.warning(f"Type d'action inconnu : {action_type}")
|
||||
return []
|
||||
@@ -2045,6 +2060,38 @@ def _resolve_runtime_vars(value: Any, variables: Dict[str, Any]) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
def _coerce_action_coords(action: dict) -> dict:
|
||||
"""Cast x_pct/y_pct en float après template resolution par _resolve_runtime_vars.
|
||||
|
||||
Politique : si string non convertible ou template encore present → skip + pause_for_human.
|
||||
Idempotent sur les actions qui ont déjà des floats (mouse_click existant).
|
||||
Jamais fallback 0.0/0.0 — un clic sur coords (0,0) = top-left = potentiellement dangereux.
|
||||
|
||||
Appelé APRÈS _resolve_runtime_vars dans la boucle dispatch (api_stream.py).
|
||||
"""
|
||||
for key in ("x_pct", "y_pct"):
|
||||
val = action.get(key)
|
||||
if val is None:
|
||||
continue
|
||||
if isinstance(val, float):
|
||||
continue # déjà float, idempotent
|
||||
if isinstance(val, str):
|
||||
# Template encore présent = non résolu par _resolve_runtime_vars
|
||||
if val.startswith("{{") and val.endswith("}}"):
|
||||
action["_skip_reason"] = f"coords_var non résolu: {key}={val}"
|
||||
action["type"] = "pause_for_human"
|
||||
action["safety_level"] = "high"
|
||||
return action
|
||||
try:
|
||||
action[key] = float(val)
|
||||
except (ValueError, TypeError):
|
||||
action["_skip_reason"] = f"coords invalide: {key}={val}"
|
||||
action["type"] = "pause_for_human"
|
||||
action["safety_level"] = "high"
|
||||
return action
|
||||
return action
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Handlers pour les actions exécutées côté serveur (extract_text, t2a_decision)
|
||||
# =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user