From 76581a210e1c3577570cee5db9c6ff0ea48d175b Mon Sep 17 00:00:00 2001 From: dog Date: Thu, 9 Jul 2026 18:07:06 +0800 Subject: [PATCH] test: quality guard for endpoint artifacts; re-baseline mouse goldens Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 13 ++++++++ tests/unit/data/golden_mouse.npz | Bin 18962 -> 19212 bytes tests/unit/test_mouse.py | 49 +++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8956f6..a481857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this project will be documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows [Semantic Versioning](https://semver.org/). +## [Unreleased] + +### Changed + +- Mouse post-processing pipeline reworked to remove endpoint artifacts: + hard forward clip → backtrack-tolerant soft monotonic with tanh + overshoot compression (`soften_forward`); tail-drag endpoint snapping → + whole-curve smoothstep residual correction (`warp_endpoints`); abrupt + start damping → continuous smoothstep damping (`damp_start`); + gaussian smoothing now applied to both axes. +- `tests/unit/data/golden_mouse.npz` re-baselined against the new + pipeline (intentional behavior change; scroll goldens unchanged). + ## [0.2.0] - 2026-05-12 ### Changed (breaking) diff --git a/tests/unit/data/golden_mouse.npz b/tests/unit/data/golden_mouse.npz index 079f08c7fd85bf6df1f1af5203243039da9e4604..6fca28eedcf9e97d77af52ba97cb29a1f228dad0 100644 GIT binary patch literal 19212 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 diff --git a/tests/unit/test_mouse.py b/tests/unit/test_mouse.py index ff8a5af..b2398bf 100644 --- a/tests/unit/test_mouse.py +++ b/tests/unit/test_mouse.py @@ -85,3 +85,52 @@ def test_mouse_model_click_events_have_matching_coords() -> None: 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" + )