"""Input actions must not silently deliver to a different app than requested. Live QA (Aug 2026, KDE desktop): with kcalc as the sticky target, ``type(text="777", app="kate")`` reported ok:true and typed 777 into KCALC — app= was silently dropped on every input action. The guard refuses provable mismatches with a one-call fix instruction. Also covers the unknown-action suggestion hint (model emitted "hotkey" for "key"). """ import json from types import SimpleNamespace from tools.computer_use.tool import ( _INPUT_ACTIONS, _dispatch, _input_target_mismatch, ) def _backend(last_app): return SimpleNamespace(_last_app=last_app) # ── mismatch predicate ────────────────────────────────────────────────── def test_clear_mismatch_returns_current_app(): assert _input_target_mismatch(_backend("kcalc"), "kate") == "kcalc" def test_same_app_no_mismatch(): assert _input_target_mismatch(_backend("kate"), "kate") is None def test_substring_variants_no_mismatch(): # list_windows names are localized/variant; never refuse fuzzy matches. assert _input_target_mismatch(_backend("Google-chrome"), "chrome") is None assert _input_target_mismatch(_backend("chrome"), "Google-chrome") is None def test_unknown_current_target_fails_open(): assert _input_target_mismatch(_backend(None), "kate") is None assert _input_target_mismatch(_backend(""), "kate") is None # ── dispatch guard ────────────────────────────────────────────────────── def _dispatch_result(backend, action, args): return json.loads(_dispatch(backend, action, args)) def test_type_with_mismatched_app_refused(): backend = _backend("kcalc") out = _dispatch_result(backend, "type", {"text": "777", "app": "kate"}) assert out["ok"] is False assert out["code"] == "input_target_mismatch" assert "kcalc" in out["error"] and "kate" in out["error"] assert "capture(app='kate')" in out["error"] def test_click_with_mismatched_app_refused(): out = _dispatch_result(_backend("kcalc"), "click", {"element": 3, "app": "kate"}) assert out["code"] == "input_target_mismatch" def _fake_action_result(action="type_text"): from tools.computer_use.backend import ActionResult return ActionResult(ok=True, action=action, message="done") def test_type_with_matching_app_reaches_backend(): backend = _backend("kate") calls = {} def _type_text(text, **kw): calls["text"] = text return _fake_action_result() backend.type_text = _type_text out = _dispatch_result(backend, "type", {"text": "hi", "app": "kate"}) assert calls["text"] == "hi" assert out.get("ok") is True def test_type_without_app_unchanged(): """Legacy flows that never pass app= keep working (fail open).""" backend = _backend("kcalc") backend.type_text = lambda text, **kw: _fake_action_result() out = _dispatch_result(backend, "type", {"text": "9"}) assert out.get("ok") is True def test_input_actions_set_matches_dispatch_branches(): # Guard list must cover exactly the sticky-target input actions. assert _INPUT_ACTIONS == { "click", "double_click", "right_click", "middle_click", "drag", "scroll", "type", "key", "set_value", } # ── unknown-action suggestions ────────────────────────────────────────── def test_unknown_action_hotkey_suggests_key(): out = _dispatch_result(_backend(None), "hotkey", {}) assert "did you mean 'key'" in out["error"] def test_unknown_action_without_suggestion_unchanged(): out = _dispatch_result(_backend(None), "frobnicate", {}) assert out["error"] == "unknown action 'frobnicate'" assert "did you mean" not in out["error"]