diff --git a/tests/test_balabit_adapter.py b/tests/test_balabit_adapter.py index 436e22c..c8ff90f 100644 --- a/tests/test_balabit_adapter.py +++ b/tests/test_balabit_adapter.py @@ -9,7 +9,7 @@ import pytest def test_module_exports(): """The adapter module must export the public functions used by CLI.""" - from ai_mouse.data_adapters import balabit + from tools.data_adapters import balabit assert hasattr(balabit, "parse_session_csv") assert hasattr(balabit, "segment_by_clicks") assert hasattr(balabit, "filter_segments") @@ -20,7 +20,7 @@ def test_module_exports(): def test_mouse_event_dataclass(): """MouseEvent has expected fields.""" - from ai_mouse.data_adapters.balabit import MouseEvent + from tools.data_adapters.balabit import MouseEvent e = MouseEvent(t_ms=100, button="NoButton", state="Move", x=300, y=400) assert e.t_ms == 100 assert e.state == "Move" @@ -29,7 +29,7 @@ def test_mouse_event_dataclass(): def test_segment_dataclass(): """Segment has expected fields.""" - from ai_mouse.data_adapters.balabit import MouseEvent, Segment + from tools.data_adapters.balabit import MouseEvent, Segment events = [MouseEvent(t_ms=0, button="NoButton", state="Move", x=10, y=20)] s = Segment(events=events, click_x=100, click_y=200, click_t_ms=500, session_id="user1_s1") assert s.events == events @@ -45,7 +45,7 @@ def _write_csv(path: Path, rows: list[str]) -> None: class TestParseSessionCsv: def test_parses_basic_rows(self, tmp_path): - from ai_mouse.data_adapters.balabit import parse_session_csv + from tools.data_adapters.balabit import parse_session_csv csv = tmp_path / "session_1" _write_csv(csv, [ "1500000000.000,0.000,NoButton,Move,100,200", @@ -63,7 +63,7 @@ class TestParseSessionCsv: def test_handles_float_timestamps(self, tmp_path): """Client timestamps are floats in seconds; we convert to int ms.""" - from ai_mouse.data_adapters.balabit import parse_session_csv + from tools.data_adapters.balabit import parse_session_csv csv = tmp_path / "session_2" _write_csv(csv, [ "0,1.234,NoButton,Move,50,60", @@ -75,7 +75,7 @@ class TestParseSessionCsv: def test_skips_malformed_rows(self, tmp_path): """Rows with bad data are logged and skipped, not raised.""" - from ai_mouse.data_adapters.balabit import parse_session_csv + from tools.data_adapters.balabit import parse_session_csv csv = tmp_path / "session_3" _write_csv(csv, [ "0,0.000,NoButton,Move,100,200", @@ -89,7 +89,7 @@ class TestParseSessionCsv: assert events[1].x == 150 def test_returns_empty_list_for_empty_file(self, tmp_path): - from ai_mouse.data_adapters.balabit import parse_session_csv + from tools.data_adapters.balabit import parse_session_csv csv = tmp_path / "session_4" csv.write_text("record timestamp,client timestamp,button,state,x,y\n", encoding="utf-8") events = parse_session_csv(csv) @@ -98,11 +98,11 @@ class TestParseSessionCsv: class TestSegmentByClicks: def _make_event(self, t_ms: int, state: str, x: int, y: int, button: str = "NoButton"): - from ai_mouse.data_adapters.balabit import MouseEvent + from tools.data_adapters.balabit import MouseEvent return MouseEvent(t_ms=t_ms, button=button, state=state, x=x, y=y) def test_one_click_one_segment(self): - from ai_mouse.data_adapters.balabit import segment_by_clicks + from tools.data_adapters.balabit import segment_by_clicks events = [ self._make_event(0, "Move", 10, 20), self._make_event(100, "Move", 50, 60), @@ -120,7 +120,7 @@ class TestSegmentByClicks: def test_window_excludes_old_events(self): """Move events earlier than (click_t - window_ms) are dropped.""" - from ai_mouse.data_adapters.balabit import segment_by_clicks + from tools.data_adapters.balabit import segment_by_clicks events = [ self._make_event(0, "Move", 10, 20), # too old self._make_event(100, "Move", 20, 30), # too old @@ -133,7 +133,7 @@ class TestSegmentByClicks: assert segments[0].events[0].t_ms == 900 def test_multiple_clicks_multiple_segments(self): - from ai_mouse.data_adapters.balabit import segment_by_clicks + from tools.data_adapters.balabit import segment_by_clicks events = [ self._make_event(0, "Move", 10, 20), self._make_event(100, "Pressed", 50, 50, button="Left"), @@ -152,7 +152,7 @@ class TestSegmentByClicks: def test_skips_pressed_with_non_left_button(self): """Right-clicks and wheel-clicks don't anchor segments (only Left).""" - from ai_mouse.data_adapters.balabit import segment_by_clicks + from tools.data_adapters.balabit import segment_by_clicks events = [ self._make_event(0, "Move", 10, 20), self._make_event(100, "Pressed", 50, 50, button="Right"), # ignored @@ -164,7 +164,7 @@ class TestSegmentByClicks: assert segments[0].click_x == 70 def test_no_clicks_returns_empty(self): - from ai_mouse.data_adapters.balabit import segment_by_clicks + from tools.data_adapters.balabit import segment_by_clicks events = [ self._make_event(0, "Move", 10, 20), self._make_event(100, "Move", 20, 30), @@ -174,7 +174,7 @@ class TestSegmentByClicks: def test_excludes_drag_events(self): """Drag events are not Move; segment should only include Move.""" - from ai_mouse.data_adapters.balabit import segment_by_clicks + from tools.data_adapters.balabit import segment_by_clicks events = [ self._make_event(0, "Move", 10, 20), self._make_event(100, "Drag", 30, 40), # not Move @@ -191,7 +191,7 @@ class TestSegmentByClicks: class TestFilterSegments: def _seg(self, events_data: list[tuple[int, int, int]], click=(500, 500), session="s"): """events_data: list of (t_ms, x, y) tuples.""" - from ai_mouse.data_adapters.balabit import MouseEvent, Segment + from tools.data_adapters.balabit import MouseEvent, Segment events = [MouseEvent(t_ms=t, button="NoButton", state="Move", x=x, y=y) for (t, x, y) in events_data] click_t = events[-1].t_ms + 50 if events else 100 @@ -199,14 +199,14 @@ class TestFilterSegments: click_t_ms=click_t, session_id=session) def test_drops_segment_with_too_few_events(self): - from ai_mouse.data_adapters.balabit import filter_segments + from tools.data_adapters.balabit import filter_segments seg = self._seg([(0, 100, 100), (50, 105, 105)]) # 2 events, min=5 result = filter_segments([seg], min_events=5, min_dist=10, max_span_ms=5000, max_gap_ms=200) assert result == [] def test_drops_segment_with_short_distance(self): - from ai_mouse.data_adapters.balabit import filter_segments + from tools.data_adapters.balabit import filter_segments # Start (100,100), end click=(105,100) → dist=5, min_dist=50 events = [(i*10, 100+i, 100) for i in range(10)] seg = self._seg(events, click=(105, 100)) @@ -215,7 +215,7 @@ class TestFilterSegments: assert result == [] def test_drops_segment_with_too_long_span(self): - from ai_mouse.data_adapters.balabit import filter_segments + from tools.data_adapters.balabit import filter_segments # Span 6000ms, max_span=5000 events = [(0, 100, 100), (2000, 200, 200), (4000, 300, 300), (6000, 400, 400), (6010, 410, 400), (6020, 420, 400)] @@ -225,7 +225,7 @@ class TestFilterSegments: assert result == [] def test_drops_segment_with_gap(self): - from ai_mouse.data_adapters.balabit import filter_segments + from tools.data_adapters.balabit import filter_segments # Gap of 500ms between events 2 and 3, max_gap=200 events = [(0, 100, 100), (50, 110, 110), (100, 120, 120), (600, 200, 200), (650, 210, 210), (700, 220, 220)] @@ -235,7 +235,7 @@ class TestFilterSegments: assert result == [] def test_drops_segment_with_out_of_range_coords(self): - from ai_mouse.data_adapters.balabit import filter_segments + from tools.data_adapters.balabit import filter_segments # x=10000 out of range events = [(i*10, 10000, 100) for i in range(6)] seg = self._seg(events, click=(10100, 100)) @@ -245,7 +245,7 @@ class TestFilterSegments: def test_drops_segment_with_short_arc_length(self): """Total arc < 50 even though endpoints are far → high-frequency jitter only.""" - from ai_mouse.data_adapters.balabit import filter_segments + from tools.data_adapters.balabit import filter_segments # Tiny back-and-forth with click 100px away — unrealistic, drop it events = [(i*10, 100 + (i % 2), 100) for i in range(10)] seg = self._seg(events, click=(200, 100)) @@ -255,7 +255,7 @@ class TestFilterSegments: assert result == [] def test_keeps_valid_segment(self): - from ai_mouse.data_adapters.balabit import filter_segments + from tools.data_adapters.balabit import filter_segments # Smooth 100→500 px straight line, 10 events, span 500ms events = [(i*50, 100 + i*40, 100) for i in range(10)] seg = self._seg(events, click=(500, 100)) @@ -267,7 +267,7 @@ class TestFilterSegments: class TestProcessSession: def test_writes_jsonl_in_expected_format(self, tmp_path): from tools.config import BalabitAdapterConfig - from ai_mouse.data_adapters.balabit import process_session + from tools.data_adapters.balabit import process_session # Construct a Balabit-format CSV with one valid segment csv_path = tmp_path / "user_session_42" @@ -304,7 +304,7 @@ class TestProcessSession: def test_returns_zero_for_session_with_no_valid_segments(self, tmp_path): from tools.config import BalabitAdapterConfig - from ai_mouse.data_adapters.balabit import process_session + from tools.data_adapters.balabit import process_session csv_path = tmp_path / "empty_session" _write_csv(csv_path, ["0,0.000,NoButton,Move,100,100"]) # no clicks @@ -319,7 +319,7 @@ class TestProcessSession: def test_appends_to_existing_jsonl(self, tmp_path): from tools.config import BalabitAdapterConfig - from ai_mouse.data_adapters.balabit import process_session + from tools.data_adapters.balabit import process_session out = tmp_path / "out.jsonl" out.write_text('{"meta":{"start":[0,0],"end":[1,1]},"events":[]}\n', encoding="utf-8") diff --git a/tests/test_eval_metrics.py b/tests/test_eval_metrics.py index 2083447..55bd9c3 100644 --- a/tests/test_eval_metrics.py +++ b/tests/test_eval_metrics.py @@ -8,7 +8,7 @@ import pytest class TestKinematics: def test_compute_speed_constant_velocity(self): """Constant-velocity trajectory has constant speed.""" - from ai_mouse.eval.metrics import compute_speed + from tools.eval.metrics import compute_speed # 10 points, moving 10 px in 100 ms each step → speed = 0.1 px/ms xs = np.arange(0, 100, 10, dtype=float) ys = np.zeros(10, dtype=float) @@ -20,7 +20,7 @@ class TestKinematics: def test_compute_speed_handles_zero_dt(self): """Adjacent points with same timestamp must not produce NaN/inf.""" - from ai_mouse.eval.metrics import compute_speed + from tools.eval.metrics import compute_speed xs = np.array([0.0, 10.0, 20.0]) ys = np.array([0.0, 0.0, 0.0]) ts = np.array([0.0, 0.0, 100.0]) # zero dt between [0] and [1] @@ -29,7 +29,7 @@ class TestKinematics: def test_compute_acceleration(self): """Linearly increasing speed → constant acceleration.""" - from ai_mouse.eval.metrics import compute_acceleration + from tools.eval.metrics import compute_acceleration # speeds: 0.1, 0.2, 0.3, 0.4 over dt = 100 ms each → a = 0.001 px/ms² speeds = np.array([0.1, 0.2, 0.3, 0.4]) ts = np.array([100.0, 200.0, 300.0, 400.0]) @@ -37,7 +37,7 @@ class TestKinematics: np.testing.assert_allclose(a, 0.001, rtol=1e-4) def test_compute_jerk(self): - from ai_mouse.eval.metrics import compute_jerk + from tools.eval.metrics import compute_jerk # accelerations: 0.001, 0.002, 0.003 over dt = 100 ms → j = 0.00001 accels = np.array([0.001, 0.002, 0.003]) ts = np.array([200.0, 300.0, 400.0]) @@ -47,7 +47,7 @@ class TestKinematics: class TestStatsSummary: def test_compute_stats_returns_expected_keys(self): - from ai_mouse.eval.metrics import compute_stats + from tools.eval.metrics import compute_stats x = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) s = compute_stats(x) assert "mean" in s @@ -59,7 +59,7 @@ class TestStatsSummary: assert "p95" in s def test_cv_for_constant_is_zero(self): - from ai_mouse.eval.metrics import compute_stats + from tools.eval.metrics import compute_stats x = np.full(10, 3.0) s = compute_stats(x) assert s["cv"] == 0.0 @@ -68,7 +68,7 @@ class TestStatsSummary: class TestFftSpectrum: def test_finds_dominant_frequency(self): """A pure 8 Hz signal should have its peak near 8 Hz.""" - from ai_mouse.eval.metrics import fft_spectrum + from tools.eval.metrics import fft_spectrum # Sample at 100 Hz for 1 second sample_rate_hz = 100.0 ts_ms = np.arange(0, 1000, 1000 / sample_rate_hz) @@ -78,7 +78,7 @@ class TestFftSpectrum: assert abs(peak_freq - 8.0) < 1.0 # within 1 Hz def test_returns_only_positive_frequencies(self): - from ai_mouse.eval.metrics import fft_spectrum + from tools.eval.metrics import fft_spectrum signal = np.random.randn(64) freqs, mags = fft_spectrum(signal, 50.0) assert (freqs >= 0).all() @@ -88,7 +88,7 @@ class TestFftSpectrum: class TestKlDivergence: def test_identical_distributions_zero_kl(self): """KL(p, p) ≈ 0.""" - from ai_mouse.eval.metrics import kl_divergence_histograms + from tools.eval.metrics import kl_divergence_histograms rng = np.random.default_rng(42) x = rng.normal(0, 1, 5000) y = rng.normal(0, 1, 5000) @@ -97,7 +97,7 @@ class TestKlDivergence: def test_different_distributions_positive_kl(self): """Different means → positive KL.""" - from ai_mouse.eval.metrics import kl_divergence_histograms + from tools.eval.metrics import kl_divergence_histograms rng = np.random.default_rng(42) x = rng.normal(0, 1, 5000) y = rng.normal(3, 1, 5000) @@ -106,7 +106,7 @@ class TestKlDivergence: def test_handles_disjoint_supports(self): """No NaN even when histograms have non-overlapping bins.""" - from ai_mouse.eval.metrics import kl_divergence_histograms + from tools.eval.metrics import kl_divergence_histograms x = np.array([1.0, 1.1, 1.2, 1.3, 1.4]) y = np.array([10.0, 10.1, 10.2, 10.3, 10.4]) kl = kl_divergence_histograms(x, y, bins=10) @@ -116,7 +116,7 @@ class TestKlDivergence: class TestReportGeneration: def test_generates_report_md(self, tmp_path): """Smoke test: build_report writes an MD file with all expected sections.""" - from ai_mouse.eval.report import build_report + from tools.eval.report import build_report # Synthetic generated traces (3 traces, 50 points each) rng = np.random.default_rng(0) diff --git a/ai_mouse/data_adapters/__init__.py b/tools/data_adapters/__init__.py similarity index 100% rename from ai_mouse/data_adapters/__init__.py rename to tools/data_adapters/__init__.py diff --git a/ai_mouse/data_adapters/__main__.py b/tools/data_adapters/__main__.py similarity index 86% rename from ai_mouse/data_adapters/__main__.py rename to tools/data_adapters/__main__.py index bb368bd..db63e3e 100644 --- a/ai_mouse/data_adapters/__main__.py +++ b/tools/data_adapters/__main__.py @@ -7,7 +7,7 @@ from __future__ import annotations import sys -from ai_mouse.data_adapters.balabit import main +from tools.data_adapters.balabit import main if __name__ == "__main__": sys.exit(main()) diff --git a/ai_mouse/data_adapters/balabit.py b/tools/data_adapters/balabit.py similarity index 100% rename from ai_mouse/data_adapters/balabit.py rename to tools/data_adapters/balabit.py diff --git a/ai_mouse/eval/__init__.py b/tools/eval/__init__.py similarity index 100% rename from ai_mouse/eval/__init__.py rename to tools/eval/__init__.py diff --git a/ai_mouse/eval/__main__.py b/tools/eval/__main__.py similarity index 98% rename from ai_mouse/eval/__main__.py rename to tools/eval/__main__.py index 27ff9cd..c09f99d 100644 --- a/ai_mouse/eval/__main__.py +++ b/tools/eval/__main__.py @@ -110,7 +110,7 @@ def main(argv: list[str] | None = None) -> int: logger.error("Empty trace sets — aborting") return 1 - from ai_mouse.eval.report import build_report + from tools.eval.report import build_report args.output.parent.mkdir(parents=True, exist_ok=True) build_report( generated_traces=gen_traces, diff --git a/ai_mouse/eval/metrics.py b/tools/eval/metrics.py similarity index 100% rename from ai_mouse/eval/metrics.py rename to tools/eval/metrics.py diff --git a/ai_mouse/eval/report.py b/tools/eval/report.py similarity index 99% rename from ai_mouse/eval/report.py rename to tools/eval/report.py index 4bbafa7..738d8ec 100644 --- a/ai_mouse/eval/report.py +++ b/tools/eval/report.py @@ -15,7 +15,7 @@ import matplotlib matplotlib.use("Agg") # headless import matplotlib.pyplot as plt # noqa: E402 -from ai_mouse.eval.metrics import ( +from tools.eval.metrics import ( compute_acceleration, compute_jerk, compute_speed,