from __future__ import annotations import json import struct import sys from pathlib import Path from types import SimpleNamespace import pytest @pytest.fixture def live_rig(monkeypatch, tmp_path): monkeypatch.syspath_prepend(str(Path(__file__).resolve().parents[1] / "tools")) import native_joycon_hub_check as check from switch2_native_imu import encode_mode0 rig = SimpleNamespace(check=check, clock=0.0, activity="independent") monkeypatch.setattr(check, "time", SimpleNamespace(monotonic=lambda: rig.clock)) def configure(pairs=2, mode="GAMEPAD", target="BOTH", neutral=False): cache = [ f"SWITCH2_PROBE_PAIR_COUNT:STRING={pairs}", "SWITCH2_PROBE_HUB:BOOL=ON", "SWITCH2_PROBE_USB_INIT:BOOL=ON", f"SWITCH2_PROBE_NEUTRAL_INPUT:BOOL={'ON' if neutral else 'OFF'}", f"SWITCH_PICO_SWITCH2_USB_BRIDGE:BOOL={'OFF' if neutral else 'ON'}", f"SWITCH2_BRIDGE_INPUT:STRING={mode}", f"SWITCH2_BRIDGE_IMU_TARGET:STRING={target}", ] for index, (child, model) in enumerate(check.child_models(pairs).items()): identity = bytearray(64) identity[0] = index + 1 struct.pack_into("= 2 @pytest.mark.parametrize( ("input_only", "activity"), [(True, "missing"), (False, "disconnect")] ) def test_unassigned_or_disconnected_second_pair_cannot_qualify( live_rig, input_only, activity ): status, audit = live_rig.run(input_only=input_only, activity=activity) assert status == 2 assert not audit["success"] assert all( not child["live_input_proven"] for child in audit["child_results"].values() ) if activity == "missing": assert audit["streams"]["B_R"]["buttons_nonzero"] == 0 assert audit["streams"]["B_L"]["control_changes"] == 0 else: assert {error["side"] for error in audit["errors"]} >= {"B_R", "B_L"} @pytest.mark.parametrize( ("input_only", "activity"), [(True, "mirrored"), (False, "mirrored"), (False, "static")], ) def test_equal_or_static_pair_evidence_is_not_independent_activity( live_rig, input_only, activity ): status, audit = live_rig.run(input_only=input_only, activity=activity) assert status == 2 assert not audit["physical_source_isolation_proven"] assert {error["side"] for error in audit["errors"]} >= {"A_R", "A_L", "B_R", "B_L"} def test_default_one_pair_keeps_right_left_capture_ids(live_rig): status, audit = live_rig.run(pairs=1, input_only=True) assert status == 0, audit["errors"] assert tuple(audit["child_results"]) == ("R", "L") assert [child["port"] for child in audit["child_results"].values()] == [1, 2] def test_two_pair_input_only_accepts_side_target_but_imu_requires_both(live_rig): build = live_rig.configure(target="LEFT") assert tuple( live_rig.check.model_references(build, pairs=2, require_imu=False) ) == ("A_R", "A_L", "B_R", "B_L") with pytest.raises(ValueError): live_rig.check.model_references(build, pairs=2, require_imu=True) def test_two_pair_live_rejects_donor_only_and_neutral_builds(live_rig): for settings in ({"mode": "JOYCON2"}, {"neutral": True}): build = live_rig.configure(**settings) with pytest.raises(ValueError): live_rig.check.model_references(build, pairs=2, require_imu=False) def test_neutral_and_input_only_are_exclusive_before_capture( live_rig, monkeypatch, tmp_path ): capture = tmp_path / "incompatible.json" monkeypatch.setattr( sys, "argv", [ "native_joycon_hub_check.py", "--pairs", "2", "--neutral", "--input-only", "--output", str(capture), ], ) with pytest.raises(SystemExit) as error: live_rig.check.main() assert error.value.code == 2 assert not capture.exists()