tests: hotplug API and virtual-device hotplug scenarios #826

Open
Youw wants to merge 7 commits from hotplug-tests into connection-callback
Youw commented 2026-07-14 14:02:41 -05:00 (Migrated from github.com)

Adds the test suite for the hotplug API (#764 / connection-callback), in two tiers, built on the virtual-device test harness from #815.

What is in here

  • src/tests/test_hotplug_api.c — tier 1, HotplugAPI_<backend>: everything in the hotplug contract that is observable without a device event (argument validation, callback-handle properties, implicit hid_init(), hid_exit() teardown incl. a register→immediate-hid_exit stress loop, and a 2-thread register/deregister churn). Needs no device, no privileges: registered for all four backends and runs in the ordinary per-push CI matrix. Self-skips (77) when a backend reports hotplug as unsupported at runtime (e.g. libusb without LIBUSB_CAP_HAS_HOTPLUG).
  • src/tests/test_hotplug.c — tier 2, Hotplug_<backend>: device-backed scenarios: async delivery, exactly-once ENUMERATE pass, callback-return deregistration (live + mid-pass), pass-before-live ordering, ARRIVED/LEFT payloads and path correlation, LEFT-without-ENUMERATE, VID/PID filtering, registration-order dispatch, the deregistration post-condition, and register/deregister from within a callback.
  • test_virtual_device_unplug() / test_virtual_device_replug() — new provider calls that toggle device presence without destroying the provider context. Implemented for uhid (UHID_DESTROY/UHID_CREATE2), rawgadget (unbind/rebind the gadget from the dummy_hcd UDC) and the Windows vhidmini provider (disable/enable the HID child devnode); the IOHIDUserDevice provider still returns TEST_VDEV_UNAVAILABLE, so only Hotplug_darwin self-skips.

The tier-2 test follows a strict synchronization discipline (see src/tests/README.md): callbacks only deep-copy events into a lock-protected log; every expectation is a deadline-based predicate poll; absence assertions sit behind event barriers, not time windows; no retries.

Where each test runs

Test Where it runs
HotplugAPI_hidraw, HotplugAPI_libusb per-push, builds.yml ubuntu-cmake
HotplugAPI_winapi per-push, builds.yml windows-cmake (MSVC, NMake, ClangCL, MinGW)
HotplugAPI_darwin per-push, builds.yml macos-cmake
Hotplug_hidraw per-push, builds.yml ubuntu-cmake (via modprobe uhid + sudo ctest)
Hotplug_libusb builds + self-skips per push; runs in the label-gated / workflow_dispatch libusb-vhid-test VM job
Hotplug_winapi builds + self-skips per push; runs in the label-gated / workflow_dispatch win-vhid-test job (driver built on windows-2022, installed and tested on the second runner)
Hotplug_darwin builds + self-skips everywhere (unplug/replug not implemented for IOHIDUserDevice), unlike DeviceIO_darwin which runs on an entitled Mac

⚠️ CI status pending the backend PRs

The behavioral expectations encode the async-ENUMERATE contract, i.e. they require #822, #823, #824 and #825. This PR is no longer draft. CI is green on the hotplug-integration branch (all four backends + this PR) and will be green here once #822-#825 merge. Until then, tier 1 remains red on connection-callback as-is (e.g. *callback_handle is not zeroed on a failed registration yet), and the tier-2 async-ENUMERATE expectations still require those backend changes.

Current validation: tier-1 HotplugAPI_* per push on all four backends; tier-2 Hotplug_hidraw per push (uhid), Hotplug_libusb and Hotplug_winapi in the label-gated ci-virtual-device jobs, Hotplug_darwin self-skips.

Relates to: #764

Assisted-by: claude-code:claude-fable-5
Assisted-by: claude-code:claude-opus-4-8
Assisted-by: claude-code:claude-opus-5
Assisted-by: claude-code:claude-fable-5-1
Assisted-by: codex-cli:gpt-6-astra

Adds the test suite for the hotplug API (#764 / connection-callback), in two tiers, built on the virtual-device test harness from #815. ## What is in here - **`src/tests/test_hotplug_api.c`** — tier 1, `HotplugAPI_<backend>`: everything in the hotplug contract that is observable without a device event (argument validation, callback-handle properties, implicit `hid_init()`, `hid_exit()` teardown incl. a register→immediate-`hid_exit` stress loop, and a 2-thread register/deregister churn). Needs no device, no privileges: registered for **all four backends** and runs in the ordinary per-push CI matrix. Self-skips (77) when a backend reports hotplug as unsupported at runtime (e.g. libusb without `LIBUSB_CAP_HAS_HOTPLUG`). - **`src/tests/test_hotplug.c`** — tier 2, `Hotplug_<backend>`: device-backed scenarios: async delivery, exactly-once ENUMERATE pass, callback-return deregistration (live + mid-pass), pass-before-live ordering, ARRIVED/LEFT payloads and path correlation, LEFT-without-ENUMERATE, VID/PID filtering, registration-order dispatch, the deregistration post-condition, and register/deregister from within a callback. - **`test_virtual_device_unplug()` / `test_virtual_device_replug()`** — new provider calls that toggle device presence without destroying the provider context. Implemented for uhid (UHID_DESTROY/UHID_CREATE2), rawgadget (unbind/rebind the gadget from the dummy_hcd UDC) and the Windows vhidmini provider (disable/enable the HID child devnode); the IOHIDUserDevice provider still returns TEST_VDEV_UNAVAILABLE, so only Hotplug_darwin self-skips. The tier-2 test follows a strict synchronization discipline (see `src/tests/README.md`): callbacks only deep-copy events into a lock-protected log; every expectation is a deadline-based predicate poll; absence assertions sit behind event barriers, not time windows; no retries. ## Where each test runs | Test | Where it runs | |------|---------------| | `HotplugAPI_hidraw`, `HotplugAPI_libusb` | per-push, `builds.yml` ubuntu-cmake | | `HotplugAPI_winapi` | per-push, `builds.yml` windows-cmake (MSVC, NMake, ClangCL, MinGW) | | `HotplugAPI_darwin` | per-push, `builds.yml` macos-cmake | | `Hotplug_hidraw` | per-push, `builds.yml` ubuntu-cmake (via `modprobe uhid` + `sudo ctest`) | | `Hotplug_libusb` | builds + self-skips per push; runs in the label-gated / workflow_dispatch libusb-vhid-test VM job | | `Hotplug_winapi` | builds + self-skips per push; runs in the label-gated / workflow_dispatch win-vhid-test job (driver built on windows-2022, installed and tested on the second runner) | | `Hotplug_darwin` | builds + self-skips everywhere (unplug/replug not implemented for IOHIDUserDevice), unlike DeviceIO_darwin which runs on an entitled Mac | ## ⚠️ CI status pending the backend PRs The behavioral expectations encode the **async-ENUMERATE contract**, i.e. they require #822, #823, #824 and #825. This PR is no longer draft. CI is green on the `hotplug-integration` branch (all four backends + this PR) and will be green here once #822-#825 merge. Until then, tier 1 remains red on `connection-callback` as-is (e.g. `*callback_handle` is not zeroed on a failed registration yet), and the tier-2 async-ENUMERATE expectations still require those backend changes. Current validation: tier-1 HotplugAPI_* per push on all four backends; tier-2 Hotplug_hidraw per push (uhid), Hotplug_libusb and Hotplug_winapi in the label-gated ci-virtual-device jobs, Hotplug_darwin self-skips. Relates to: #764 Assisted-by: claude-code:claude-fable-5 Assisted-by: claude-code:claude-opus-4-8 Assisted-by: claude-code:claude-opus-5 Assisted-by: claude-code:claude-fable-5-1 Assisted-by: codex-cli:gpt-6-astra
Youw commented 2026-08-04 11:54:59 -05:00 (Migrated from github.com)

Full implementation review (fresh pass, head 6753c0b)

Reviewed the T6–T15 device-backed scenarios and tier-1 contract tests against the documented contract in hidapi/hidapi.h, all four virtual-device providers (uhid, rawgadget, win/vhidmini, mac stub), the CMake wiring, and the two label-gated workflows. Each candidate finding was adversarially self-checked before being reported. The suite is well designed — barrier-based absence assertions, deadline-polled waits, per-handle event log, and skip semantics are sound, and every T6–T15 scenario genuinely proves what its name claims. Findings: 1 major, 5 minor, 1 nit. No blockers. (Note: this PR's CI is red only because its base still has the pre-async backends; the tests are verified green on the aggregate branch with PRs #822–#825 included.)

[major] src/tests/test_hotplug.c:358-376, 912-921 — T15: acted flag published before the results it gates

cb_parent sets ctx->acted = 1 under g_log_lock (359-363) before it performs the child registration and self-deregistration; ctx->child_rc / ctx->child_handle / ctx->self_dereg_rc are stored only afterwards in a second locked section (372-376). The main thread's hp_wait_flag(&ctx.acted, ...) (912) can observe acted == 1, take the lock, and read the still-zero results while the event thread is between the two critical sections — a window that includes a full hid_hotplug_register_callback(... ENUMERATE ...) (snapshot construction) plus a deregistration, i.e. potentially milliseconds on a loaded CI host. CHECK(h_child > 0) (920) then fails spuriously on a fully compliant backend.

Suggested fix: store child_rc/child_handle/self_dereg_rc first and set a separate done flag last (same locked section); wait on done; keep acted only as the run-once guard.

[minor] src/tests/test_hotplug_api.c:188 — T2 asserts handle monotonicity, contract only promises non-reuse

CHECK(h2 > h1) — hidapi.h promises handles are unique, positive, and not reused while initialized; it never promises a later registration gets a numerically larger handle. A compliant backend with, e.g., randomized handles would fail this contract test. Fix: relax to CHECK(h2 != h1) (keeping h2 > 0), or make monotonicity contractual in hidapi.h.

[minor] src/tests/test_virtual_device_rawgadget.c:651-668 — rg_unplug can join a still-blocked int_in thread with no further signal

The SIGUSR1 spin terminates as soon as ep0_exited is set. The int_in thread is only signalled inside that loop; if it is blocked in the blocking EP_WRITE ioctl (a queued input the host never read — precisely the failed-test case) it may never receive a signal, and the unconditional pthread_join (662) hangs until the CTest timeout kills the process, converting a clean failure into a 300 s timeout. Fix: gate the spin on both threads having exited (add int_in_exited), or keep signalling int_in until a timed join succeeds.

[minor] src/tests/test_virtual_device_rawgadget.c:682-702 — rg_plug has no already-plugged guard

test_virtual_device_replug() calls rg_plug() unconditionally, and rg_plug resets stop/fd/ep0_started/... without checking the device is actually unplugged. ensure_present() in test_hotplug.c decides to replug based on enumeration visibility, not plug state, so a plugged-but-not-enumerating device leads to rg_plug on a live device: the old fd leaks and the two orphaned worker threads keep running with stop cleared and fd retargeted — fd/thread state corruption. Fix: begin rg_plug with if (dev->fd >= 0 || dev->ep0_started) rg_unplug(dev); (documented idempotent), making replug safe from any state.

[minor] src/tests/test_virtual_device_win.c:188-193 — a crashed run that left the HID child disabled turns later local runs into silent skips

create() probes hid_enumerate and returns UNAVAILABLE when empty. destroy() best-effort re-enables the child, but a process killed by a CTest timeout — plausible mid-hotplug-test, exactly when the child is disabled — never runs destroy. The next local run finds nothing and skips (exit 77) even though the driver is installed, masking the tests until the device is re-enabled manually. (CI is unaffected: fresh runner each run.) Fix: in create(), before the probe, best-effort locate_vhid_devnode() + find_hid_child() + CM_Enable_DevNode, then probe with a brief re-poll.

[minor] src/tests/CMakeLists.txt:77,113-114,121, src/tests/README.md, .github/workflows/libusb-vhid-test.yml:67 — stale docs contradict the toggling this PR implements

CMake comments still say only uhid implements presence toggling and that the rawgadget/vhidmini tests "self-skip until implemented"; the README tier-2 table marks Hotplug_libusb/Hotplug_winapi as future; the VM workflow step is still named "Run DeviceIO_libusb inside a VM". All are contradicted by this PR's own rawgadget rg_plug/rg_unplug and Windows child-PDO toggling, which the label-gated jobs actually run. Fix: update the three CMake comments, the README paragraph/table, and the workflow step name.

[nit] test_hotplug_api.c:276, test_virtual_device_uhid.c:47, test_virtual_device_rawgadget.c:180-189 — cross-thread flags are volatile int, not atomics

Correct in practice for these one-way flags and invisible to ASan, but formal data races a future TSan leg would report. Fix: C11 atomic_int or the __atomic_* helpers test_platform.h already uses.


Verified sound (checked, no issues): T6–T14 assertion logic against the contract (exactly-once with LEFT barriers, return-stops-pass with fresh-ENUMERATE barrier, pass-before-live path correlation, zero-window T11 proof, registration-order absence barriers, race-free one-sided T14); event-log machinery (deep-copy under mutex, seq == dispatch order, per-test invariant sweeps, VID/PID/serial filtering); complete skip semantics (exit 77 on no-hotplug, provider-unavailable, no-toggle, never-enumerates, non-elevated Windows; T8b second-device sub-skip); uhid provider lifecycle; Windows devnode location (ROOT-enumerator scan by INF hardware id) and child-PDO toggling; CMake targets/linkage/timeouts; label gating and driver artifact flow in both workflows.

Drafted with Claude Code.

## Full implementation review (fresh pass, head `6753c0b`) Reviewed the T6–T15 device-backed scenarios and tier-1 contract tests against the documented contract in `hidapi/hidapi.h`, all four virtual-device providers (uhid, rawgadget, win/vhidmini, mac stub), the CMake wiring, and the two label-gated workflows. Each candidate finding was adversarially self-checked before being reported. The suite is well designed — barrier-based absence assertions, deadline-polled waits, per-handle event log, and skip semantics are sound, and every T6–T15 scenario genuinely proves what its name claims. **Findings: 1 major, 5 minor, 1 nit. No blockers.** (Note: this PR's CI is red only because its base still has the pre-async backends; the tests are verified green on the aggregate branch with PRs #822–#825 included.) ### [major] `src/tests/test_hotplug.c:358-376, 912-921` — T15: `acted` flag published before the results it gates `cb_parent` sets `ctx->acted = 1` under `g_log_lock` (359-363) *before* it performs the child registration and self-deregistration; `ctx->child_rc` / `ctx->child_handle` / `ctx->self_dereg_rc` are stored only afterwards in a second locked section (372-376). The main thread's `hp_wait_flag(&ctx.acted, ...)` (912) can observe `acted == 1`, take the lock, and read the still-zero results while the event thread is between the two critical sections — a window that includes a full `hid_hotplug_register_callback(... ENUMERATE ...)` (snapshot construction) plus a deregistration, i.e. potentially milliseconds on a loaded CI host. `CHECK(h_child > 0)` (920) then fails spuriously on a fully compliant backend. **Suggested fix:** store `child_rc`/`child_handle`/`self_dereg_rc` first and set a separate `done` flag last (same locked section); wait on `done`; keep `acted` only as the run-once guard. ### [minor] `src/tests/test_hotplug_api.c:188` — T2 asserts handle monotonicity, contract only promises non-reuse `CHECK(h2 > h1)` — `hidapi.h` promises handles are unique, positive, and not reused while initialized; it never promises a later registration gets a numerically larger handle. A compliant backend with, e.g., randomized handles would fail this contract test. **Fix:** relax to `CHECK(h2 != h1)` (keeping `h2 > 0`), or make monotonicity contractual in `hidapi.h`. ### [minor] `src/tests/test_virtual_device_rawgadget.c:651-668` — `rg_unplug` can join a still-blocked int_in thread with no further signal The SIGUSR1 spin terminates as soon as `ep0_exited` is set. The int_in thread is only signalled *inside* that loop; if it is blocked in the blocking `EP_WRITE` ioctl (a queued input the host never read — precisely the failed-test case) it may never receive a signal, and the unconditional `pthread_join` (662) hangs until the CTest timeout kills the process, converting a clean failure into a 300 s timeout. **Fix:** gate the spin on both threads having exited (add `int_in_exited`), or keep signalling int_in until a timed join succeeds. ### [minor] `src/tests/test_virtual_device_rawgadget.c:682-702` — `rg_plug` has no already-plugged guard `test_virtual_device_replug()` calls `rg_plug()` unconditionally, and `rg_plug` resets `stop/fd/ep0_started/...` without checking the device is actually unplugged. `ensure_present()` in test_hotplug.c decides to replug based on *enumeration visibility*, not plug state, so a plugged-but-not-enumerating device leads to `rg_plug` on a live device: the old fd leaks and the two orphaned worker threads keep running with `stop` cleared and `fd` retargeted — fd/thread state corruption. **Fix:** begin `rg_plug` with `if (dev->fd >= 0 || dev->ep0_started) rg_unplug(dev);` (documented idempotent), making replug safe from any state. ### [minor] `src/tests/test_virtual_device_win.c:188-193` — a crashed run that left the HID child disabled turns later local runs into silent skips `create()` probes `hid_enumerate` and returns UNAVAILABLE when empty. `destroy()` best-effort re-enables the child, but a process killed by a CTest timeout — plausible mid-hotplug-test, exactly when the child is disabled — never runs destroy. The next local run finds nothing and skips (exit 77) even though the driver is installed, masking the tests until the device is re-enabled manually. (CI is unaffected: fresh runner each run.) **Fix:** in `create()`, before the probe, best-effort `locate_vhid_devnode()` + `find_hid_child()` + `CM_Enable_DevNode`, then probe with a brief re-poll. ### [minor] `src/tests/CMakeLists.txt:77,113-114,121`, `src/tests/README.md`, `.github/workflows/libusb-vhid-test.yml:67` — stale docs contradict the toggling this PR implements CMake comments still say only uhid implements presence toggling and that the rawgadget/vhidmini tests "self-skip until implemented"; the README tier-2 table marks `Hotplug_libusb`/`Hotplug_winapi` as future; the VM workflow step is still named "Run DeviceIO_libusb inside a VM". All are contradicted by this PR's own rawgadget `rg_plug`/`rg_unplug` and Windows child-PDO toggling, which the label-gated jobs actually run. **Fix:** update the three CMake comments, the README paragraph/table, and the workflow step name. ### [nit] `test_hotplug_api.c:276`, `test_virtual_device_uhid.c:47`, `test_virtual_device_rawgadget.c:180-189` — cross-thread flags are `volatile int`, not atomics Correct in practice for these one-way flags and invisible to ASan, but formal data races a future TSan leg would report. **Fix:** C11 `atomic_int` or the `__atomic_*` helpers `test_platform.h` already uses. --- Verified sound (checked, no issues): T6–T14 assertion logic against the contract (exactly-once with LEFT barriers, return-stops-pass with fresh-ENUMERATE barrier, pass-before-live path correlation, zero-window T11 proof, registration-order absence barriers, race-free one-sided T14); event-log machinery (deep-copy under mutex, seq == dispatch order, per-test invariant sweeps, VID/PID/serial filtering); complete skip semantics (exit 77 on no-hotplug, provider-unavailable, no-toggle, never-enumerates, non-elevated Windows; T8b second-device sub-skip); uhid provider lifecycle; Windows devnode location (ROOT-enumerator scan by INF hardware id) and child-PDO toggling; CMake targets/linkage/timeouts; label gating and driver artifact flow in both workflows. _Drafted with Claude Code._
Youw commented 2026-08-04 12:09:09 -05:00 (Migrated from github.com)

Re-review of fix commits aaf66c0 + c54ecc6

A fresh delta review (independent reviewer) verified aaf66c0 against all six findings and hunted for newly introduced defects:

  • T15 publish ordering — FIXED. Single locked section stores the results then sets done=1; the main thread waits on done (deadline-bounded, read under the lock); acted remains the run-once guard, so a second ARRIVED can neither double-store nor skip storing.
  • Handle-uniqueness assertion — FIXED (h2 != h1; no other monotonicity assumptions remain in either test file).
  • rawgadget teardown — FIXED. int_in_exited is set on the thread's single exit path, reset in the per-plug block, and the signal spin now requires both workers to have exited, still signalling each busy thread independently.
  • replug guard — FIXED as specified, but the guard itself introduced 1 new defect (below).
  • Windows create() re-enable + re-poll — FIXED. Helpers are defined above create(); every call site was checked — T8b's second-device create runs after ensure_present(), so the pre-probe re-enable is a no-op there, and no call site exists with the primary intentionally unplugged. (Noted behavior change: an absent device now costs ~3 s before UNAVAILABLE instead of an instant skip — bounded, acceptable against the 300 s test timeout.)
  • Stale docs — FIXED and verified against what the workflows actually run.

New defect found by the re-review (introduced by the new guard, then fixed in c54ecc6): test_virtual_device_create() callocs the device struct, leaving fd == 0, and the new already-plugged guard treated fd 0 as an open gadget fd — so every rawgadget create() deterministically closed stdin (close(0)), masked only because open("/dev/raw-gadget") then recycled fd 0. c54ecc6 puts the struct in the documented unplugged state (fd = -1) before rg_plug() inspects it, and also closes a pre-existing OOM-only gap the re-review surfaced (ep0_thread_fn's early return never published ep0_exited, which would have made rg_unplug spin its full signal budget against an already-dead thread).

Both follow-up hunks were verified against the re-review's prescription; gcc -fsyntax-only passes against the real raw_gadget.h UAPI header.

Drafted with Claude Code.

## Re-review of fix commits `aaf66c0` + `c54ecc6` A fresh delta review (independent reviewer) verified `aaf66c0` against all six findings and hunted for newly introduced defects: - **T15 publish ordering — FIXED.** Single locked section stores the results then sets `done=1`; the main thread waits on `done` (deadline-bounded, read under the lock); `acted` remains the run-once guard, so a second ARRIVED can neither double-store nor skip storing. - **Handle-uniqueness assertion — FIXED** (`h2 != h1`; no other monotonicity assumptions remain in either test file). - **rawgadget teardown — FIXED.** `int_in_exited` is set on the thread's single exit path, reset in the per-plug block, and the signal spin now requires *both* workers to have exited, still signalling each busy thread independently. - **replug guard — FIXED as specified, but the guard itself introduced 1 new defect** (below). - **Windows `create()` re-enable + re-poll — FIXED.** Helpers are defined above `create()`; every call site was checked — T8b's second-device create runs after `ensure_present()`, so the pre-probe re-enable is a no-op there, and no call site exists with the primary intentionally unplugged. (Noted behavior change: an absent device now costs ~3 s before UNAVAILABLE instead of an instant skip — bounded, acceptable against the 300 s test timeout.) - **Stale docs — FIXED** and verified against what the workflows actually run. **New defect found by the re-review** (introduced by the new guard, then fixed in `c54ecc6`): `test_virtual_device_create()` callocs the device struct, leaving `fd == 0`, and the new already-plugged guard treated fd 0 as an open gadget fd — so every rawgadget `create()` deterministically closed **stdin** (`close(0)`), masked only because `open("/dev/raw-gadget")` then recycled fd 0. `c54ecc6` puts the struct in the documented unplugged state (`fd = -1`) before `rg_plug()` inspects it, and also closes a pre-existing OOM-only gap the re-review surfaced (`ep0_thread_fn`'s early return never published `ep0_exited`, which would have made `rg_unplug` spin its full signal budget against an already-dead thread). Both follow-up hunks were verified against the re-review's prescription; `gcc -fsyntax-only` passes against the real `raw_gadget.h` UAPI header. _Drafted with Claude Code._
Youw commented 2026-09-08 15:50:01 -05:00 (Migrated from github.com)

Fresh review pass (2026-09-08)

Two independent reviews (Claude Fable 5.1 and gpt-6-astra), merged and deduplicated: 39 findings (1 major, 20 minor, 11 nit, 7 note).

Fixes landed in three commits:

  • 333a0da - event-mask and PID filters, payload and lifecycle coverage, atomics in test_platform.h, rawgadget signal ownership, exact hardware-id match for the Windows devnode, required-test skip rejection in the privileged jobs
  • cd008b0 - round-two findings: every gated scenario releases its gate before any assertion can wait on a parked callback, bounded helper joins, retired-handle tracking to catch late callbacks
  • fcb2578 - MSVC /W4 /WX fixes (shadowed loop variable, getenv deprecation), T15 no longer assumes both callbacks of a nested registration share one OS thread (the Windows backend delivers from a threadpool), and dummy_hcd.c is fetched through the authenticated contents API with retries (anonymous fetches hit HTTP 429)

Final adversarial verification (Fable, on cd008b0): 54 fixed, 2 partial (one base-merge item, one whose premise turned out wrong), 6 skipped as other-PR items. It also claimed a major: that the Windows virtual-device job cannot load the ASan runtime without an MSVC developer-environment step. Run 34273898724 contradicts that (the ASan-instrumented DeviceIO and HotplugAPI binaries ran and passed there), so no workflow change was made.

CI: the plain Builds matrix on this branch alone fails by design (the tests encode the new contract and run against the base backends). On the aggregate branch (#830) Builds is green and the device-backed suites pass on both Windows and libusb.

Drafted with Claude Code.

## Fresh review pass (2026-09-08) Two independent reviews (Claude Fable 5.1 and gpt-6-astra), merged and deduplicated: **39 findings** (1 major, 20 minor, 11 nit, 7 note). Fixes landed in three commits: - 333a0da - event-mask and PID filters, payload and lifecycle coverage, atomics in `test_platform.h`, rawgadget signal ownership, exact hardware-id match for the Windows devnode, required-test skip rejection in the privileged jobs - cd008b0 - round-two findings: every gated scenario releases its gate before any assertion can wait on a parked callback, bounded helper joins, retired-handle tracking to catch late callbacks - fcb2578 - MSVC `/W4 /WX` fixes (shadowed loop variable, `getenv` deprecation), T15 no longer assumes both callbacks of a nested registration share one OS thread (the Windows backend delivers from a threadpool), and `dummy_hcd.c` is fetched through the authenticated contents API with retries (anonymous fetches hit HTTP 429) Final adversarial verification (Fable, on cd008b0): 54 fixed, 2 partial (one base-merge item, one whose premise turned out wrong), 6 skipped as other-PR items. It also claimed a major: that the Windows virtual-device job cannot load the ASan runtime without an MSVC developer-environment step. Run 34273898724 contradicts that (the ASan-instrumented DeviceIO and HotplugAPI binaries ran and passed there), so no workflow change was made. CI: the plain Builds matrix on this branch alone fails by design (the tests encode the new contract and run against the base backends). On the aggregate branch (#830) Builds is green and the device-backed suites pass on both Windows and libusb. _Drafted with Claude Code._
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin 'hotplug-tests:hotplug-tests'
git switch 'hotplug-tests'

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch 'connection-callback'
git merge --no-ff 'hotplug-tests'
git switch 'hotplug-tests'
git rebase 'connection-callback'
git switch 'connection-callback'
git merge --ff-only 'hotplug-tests'
git switch 'hotplug-tests'
git rebase 'connection-callback'
git switch 'connection-callback'
git merge --no-ff 'hotplug-tests'
git switch 'connection-callback'
git merge --squash 'hotplug-tests'
git switch 'connection-callback'
git merge --ff-only 'hotplug-tests'
git switch 'connection-callback'
git merge 'hotplug-tests'
git push origin 'connection-callback'
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
gh-libusb/hidapi!826
No description provided.