"""Tests documenting the coords consumption gap: write-only navigate coords. Test 1 (POSITIVE): _resolve_runtime_vars mechanism works — template strings like {{navigate_login_coords.x_pct}} resolve correctly when variables dict contains the stored coords. Test 2 (NEGATIVE): _edge_to_normalized_actions bakes coords as literal floats, never producing template strings — so runtime variable resolution is never triggered for navigate coords, proving the write-only gap. These tests are evidence, not regression guards. Test 2 documents a known structural gap; when the gap is fixed, Test 2 should be updated to assert templates ARE produced. """ import os import re from types import SimpleNamespace os.environ.setdefault("RPA_AUTH_DISABLED", "true") from agent_v0.server_v1.replay_engine import ( _edge_to_normalized_actions, _resolve_runtime_vars, _resolve_runtime_vars_in_str, ) # ── Fake fixtures (minimal, per test_visual_anchor_semantics.py pattern) ── class _FakeAction: def __init__(self, type_, target=None, parameters=None): self.type = type_ self.target = target self.parameters = parameters or {} class _FakeEdge: def __init__(self, action): self.edge_id = "edge_coords_gap" self.from_node = "node_src" self.to_node = "node_dst" self.action = action # ── Test 1: resolve mechanism is viable ────────────────────────────────── class TestResolveRuntimeVarsViable: """Prove _resolve_runtime_vars infrastructure works with template strings.""" VARIABLES = { "navigate_login_coords": { "x_pct": 0.15, "y_pct": 0.07, "method": "ocr_anchor", } } def test_resolve_in_str_dot_path(self): """{{navigate_login_coords.x_pct}} → "0.15" (string, not float).""" result = _resolve_runtime_vars_in_str( "{{navigate_login_coords.x_pct}}", self.VARIABLES ) assert result == "0.15" def test_resolve_in_str_y_pct(self): """{{navigate_login_coords.y_pct}} → "0.07".""" result = _resolve_runtime_vars_in_str( "{{navigate_login_coords.y_pct}}", self.VARIABLES ) assert result == "0.07" def test_resolve_dict_with_templates(self): """_resolve_runtime_vars substitutes templates inside dict values.""" action = { "type": "click", "x_pct": "{{navigate_login_coords.x_pct}}", "y_pct": "{{navigate_login_coords.y_pct}}", } resolved = _resolve_runtime_vars(action, self.VARIABLES) assert resolved["x_pct"] == "0.15" assert resolved["y_pct"] == "0.07" assert resolved["type"] == "click" # no-template strings unchanged def test_resolve_nested_dict(self): """_resolve_runtime_vars handles nested dicts with templates.""" action = { "parameters": { "coords": "{{navigate_login_coords.x_pct}}", }, } resolved = _resolve_runtime_vars(action, self.VARIABLES) assert resolved["parameters"]["coords"] == "0.15" def test_resolve_missing_var_leaves_template_intact(self): """Missing variable: template string stays unchanged.""" result = _resolve_runtime_vars_in_str( "{{navigate_password_coords.x_pct}}", self.VARIABLES ) assert "{{navigate_password_coords.x_pct}}" in result def test_resolve_float_passthrough(self): """_resolve_runtime_vars returns non-str values unchanged — floats pass through.""" action = {"x_pct": 0.15, "y_pct": 0.07} resolved = _resolve_runtime_vars(action, self.VARIABLES) # Floats are NOT substituted — they're not strings containing {{...}} assert resolved["x_pct"] == 0.15 # literal float, unchanged assert resolved["y_pct"] == 0.07 # ── Test 2: compiler gap — literals not templates ──────────────────────── class TestCompilerGapLiteralFloats: """Document that _edge_to_normalized_actions produces literal floats, never template strings — so navigate coords are write-only. This is the STRUCTURAL GAP: the compiler bakes coords as floats, _resolve_runtime_vars only operates on strings, so stored navigate variables are never consumed downstream. """ def test_mouse_click_produces_literal_floats(self): """mouse_click edge: x_pct/y_pct are literal floats, not templates.""" target = SimpleNamespace( by_position=(0.15, 0.07), by_role=None, by_text=None, context_hints={}, ) edge = _FakeEdge( _FakeAction("mouse_click", target=target, parameters={"button": "left"}) ) actions = _edge_to_normalized_actions(edge, params={}) assert len(actions) == 1 action = actions[0] # GAP: coords are literal floats, not template strings assert isinstance(action["x_pct"], float) assert isinstance(action["y_pct"], float) assert action["x_pct"] == 0.15 assert action["y_pct"] == 0.07 # Proof: no template string is ever produced by the compiler assert not isinstance(action["x_pct"], str) assert not isinstance(action["y_pct"], str) def test_literal_floats_not_resolved(self): """Literal floats pass through _resolve_runtime_vars unchanged — proving navigate coords stored in variables are NEVER consumed.""" target = SimpleNamespace( by_position=(0.15, 0.07), by_role=None, by_text=None, context_hints={}, ) edge = _FakeEdge( _FakeAction("mouse_click", target=target, parameters={"button": "left"}) ) actions = _edge_to_normalized_actions(edge, params={}) action = actions[0] # Simulate variables from a prior navigate_login step different_coords = { "navigate_login_coords": {"x_pct": 0.20, "y_pct": 0.10} } resolved = _resolve_runtime_vars(action, different_coords) # Coords REMAIN the original literal floats — no substitution assert resolved["x_pct"] == 0.15 # NOT 0.20 (no substitution) assert resolved["y_pct"] == 0.07 # NOT 0.10 (no substitution) def test_text_input_produces_literal_floats(self): """text_input edge: same literal float pattern for click target.""" target = SimpleNamespace( by_position=(0.30, 0.50), by_role=None, by_text=None, context_hints={}, ) edge = _FakeEdge( _FakeAction("text_input", target=target, parameters={"text": "admin"}) ) actions = _edge_to_normalized_actions(edge, params={}) assert len(actions) == 1 action = actions[0] assert isinstance(action["x_pct"], float) assert isinstance(action["y_pct"], float) assert action["x_pct"] == 0.30 assert action["y_pct"] == 0.50 def test_navigate_action_type_unknown(self): """navigate action type is NOT handled by _edge_to_normalized_actions — falls into the else branch logging "Type d'action inconnu".""" edge = _FakeEdge(_FakeAction("navigate", parameters={"target": "login"})) actions = _edge_to_normalized_actions(edge, params={}) # navigate produces empty actions — not compiled at all assert actions == []