test: quality guard for endpoint artifacts; re-baseline mouse goldens

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
dog
2026-07-09 18:07:06 +08:00
parent 7c33c13a87
commit 76581a210e
3 changed files with 62 additions and 0 deletions

View File

@@ -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 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
[Semantic Versioning](https://semver.org/). [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 ## [0.2.0] - 2026-05-12
### Changed (breaking) ### Changed (breaking)

Binary file not shown.

View File

@@ -85,3 +85,52 @@ def test_mouse_model_click_events_have_matching_coords() -> None:
assert up[2] > down[2] assert up[2] > down[2]
# Within click_dist bounds 20..500 # Within click_dist bounds 20..500
assert 20 <= up[2] - down[2] <= 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"
)