diff --git a/test/test_hud_clicks.py b/test/test_hud_clicks.py index 93c85c2..db6159c 100644 --- a/test/test_hud_clicks.py +++ b/test/test_hud_clicks.py @@ -108,3 +108,41 @@ def test_move_clicks_with_randomization_disabled(): f"move() still randomizes after the guard, which can re-enter " f"the HUD: {line.strip()}" ) + + +def test_every_movement_click_in_ichar_is_guarded(): + """Scope the invariant to every MOVEMENT method, not one of them. + + The first version of this guard covered move(), and its tests asserted only + on move(). walk() carried the identical unguarded click and was missed — it + is reached from bot.py's walk-back-to-town path and from poison_necro, so it + was a live second route to the same loot-filter toggle. + + Only movement clicks may be relocated. Clicks that must land on a specific + thing are deliberately excluded, because moving them breaks what they do: + pick_up_item must hit the item itself + _remap_skill_hotkey deliberately clicks the UI + cast_in_arc aims a cast direction, not a destination + """ + import inspect + from char.i_char import IChar + + MOVEMENT = ("move", "walk") + + offenders = [] + for name in MOVEMENT: + fn = getattr(IChar, name, None) + if fn is None: + continue + src = inspect.getsource(fn) + body = chr(10).join(l for l in src.splitlines() if not l.strip().startswith("#")) + parts = body.split('"""') + body = parts[0] + "".join(parts[2:]) if len(parts) > 2 else parts[0] + for line in body.splitlines(): + if "mouse.move(" in line and "randomize=0" not in line: + offenders.append(f"{name}: {line.strip()}") + + assert not offenders, ( + "movement clicks that can land on the HUD (must go through " + "_hud_safe_target then move with randomize=0): " + "; ".join(offenders) + )