"""Tests for MouseModel and ai_mouse.generate().""" from __future__ import annotations import pytest def test_mouse_model_init_default() -> None: from ai_mouse.mouse import MouseModel m = MouseModel() assert m._seq_len > 0 assert m._session is not None m.close() def test_mouse_model_generate_returns_correct_shape() -> None: from ai_mouse.mouse import MouseModel m = MouseModel() pts = m.generate((100, 200), (900, 400)) assert len(pts) == 66 # 64 moves + 2 clicks for x, y, t in pts: assert isinstance(x, int) assert isinstance(y, int) assert isinstance(t, int) def test_mouse_model_click_false_omits_clicks() -> None: from ai_mouse.mouse import MouseModel m = MouseModel() pts = m.generate((100, 200), (900, 400), click=False) assert len(pts) == 64 def test_mouse_model_seed_reproducibility() -> None: from ai_mouse.mouse import MouseModel m = MouseModel() a = m.generate((100, 200), (900, 400), seed=42) b = m.generate((100, 200), (900, 400), seed=42) assert a == b def test_mouse_model_invalid_path_raises_model_load_error() -> None: from ai_mouse.mouse import MouseModel from ai_mouse.errors import ModelLoadError with pytest.raises(ModelLoadError): MouseModel(model_path="/nonexistent/path/here") def test_mouse_model_timestamps_monotonic() -> None: from ai_mouse.mouse import MouseModel m = MouseModel() pts = m.generate((100, 200), (900, 400)) times = [p[2] for p in pts] for i in range(1, len(times)): assert times[i] >= times[i - 1] def test_mouse_model_starts_and_ends_near_endpoints() -> None: from ai_mouse.mouse import MouseModel start = (100, 200) end = (900, 400) m = MouseModel() pts = m.generate(start, end) assert abs(pts[0][0] - start[0]) < 30 assert abs(pts[0][1] - start[1]) < 30 last_move = pts[-3] # last 2 are click events assert abs(last_move[0] - end[0]) < 30 assert abs(last_move[1] - end[1]) < 30 def test_mouse_model_n_points_parameter() -> None: from ai_mouse.mouse import MouseModel m = MouseModel() pts = m.generate((100, 200), (900, 400), n_points=32) # 32 moves + 2 clicks assert len(pts) == 34 def test_mouse_model_click_events_have_matching_coords() -> None: from ai_mouse.mouse import MouseModel m = MouseModel() pts = m.generate((100, 200), (900, 400)) down, up = pts[-2], pts[-1] assert down[0] == up[0] assert down[1] == up[1] assert up[2] > down[2] # Within click_dist bounds 20..500 assert 20 <= up[2] - down[2] <= 500 def test_no_sharp_turns_or_walls_near_endpoint() -> None: """Guard against the two endpoint artifact classes: - jagged hook/zigzag chains (two or more >90° turns between consecutive substantial segments) in the final approach (the old tail-drag created hooks); a single reversal is the natural overshoot-and-correct gesture and is allowed; - vertical walls: many points stacked at the target's forward position (the old clip(0,1) stacked overshoot at forward=1). """ import numpy as np from ai_mouse import generate cases = [((100, 300), (900, 350)), ((100, 100), (700, 600)), ((800, 200), (150, 550))] for (start, end) in cases: for seed in range(6): pts = generate(start, end, seed=seed, click=False) arr = np.array([(x, y) for x, y, _ in pts], dtype=float) tail = arr[-12:] seg = np.diff(tail, axis=0) lens = np.linalg.norm(seg, axis=1) # Only consider substantial segments: integer-pixel staircase # on 1-2 px steps produces spurious 90° angles. keep = lens >= 3.0 headings = np.arctan2(seg[keep][:, 1], seg[keep][:, 0]) if len(headings) >= 2: turns = np.abs(np.diff(np.unwrap(headings))) sharp = int(np.sum(np.degrees(turns) > 90.0)) # A single large-angle reversal is the natural # overshoot-and-correct gesture (soften_forward allows # overshoot; warp_endpoints pins the final point). Two or # more mean a jagged hook/zigzag chain — the artifact class. assert sharp <= 1, ( f"{start}->{end} seed={seed}: {sharp} turns >90° " f"in final approach" ) # Vertical wall: >=4 consecutive tail points within 2 px of # the target x while spanning >10 px of y. near_x = np.abs(tail[:, 0] - end[0]) <= 2.0 run = 0 for i, flag in enumerate(near_x[:-1]): # exclude final point run = run + 1 if flag else 0 assert run < 4 or np.ptp(tail[near_x][:, 1]) <= 10.0, ( f"{start}->{end} seed={seed}: vertical wall at target x" )