TapExtension
Per-test device sessions for JUnit 5, over the coroutine client. Before each test it maps every declared role to a serial (rotating the starting device from test to test; a single-role test also moves on to the next device when one is busy), creates the per-test root Job, and opens one driver session per role — in sorted serial order, waiting up to tap.acquireTimeoutSeconds for a device another session holds, so two multi-device tests can never deadlock — all as children of the root job. The test body runs only inside tapTest, which binds this state and installs the SDK TapContext; Device calls outside it fail with a usage error, so plain metadata-only tests stay possible but cannot touch live devices.
JUnit timeout/interruption cancels the root job (via tapTest's runBlocking); sibling failure in coroutineScope/async cancels the other device's in-flight RPC by structured rules. After the test (or setup) it captures failure artifacts while sessions are still live when possible, then cancels the root job and performs bounded non-cancellable cleanup, preserving the primary failure and suppressing cleanup failures into it. With @TapTest(deviceLifetime = PER_CLASS) the devices are kept in the class store instead, probed before each reuse, and detached in afterAll.