137 lines
4.8 KiB
Python
137 lines
4.8 KiB
Python
"""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"
|
|
)
|