Files
amethyst/tools/tor-network-tests/README.md
T
Vitor PamplonaandClaude Opus 4.8 d9066572cd test(tor): add repeatable on-device networking scenario suite
The 60s bootstrap timeout this branch adds is invisible to every JVM test —
it only manifests against a real radio. Add a device-driven harness that drives
the network transitions that have historically wedged Tor and asserts the
lifecycle invariants from logcat, so the behavior can be re-verified whenever
Arti is bumped or the Tor management path changes.

tools/tor-network-tests/run.sh — adb-driven runner, one function per scenario,
PASS/FAIL per check, restores a clean network state on exit:
  - cold_start        Active reached + no pre-ready dial storm (the #3223 gate)
  - offline_bootstrap empty cache + airplane: asserts the bootstrap is bounded
                      ('bootstrap timed out' logged), then recovers on restore.
                      This is the regression test for this PR — it FAILS on a
                      build without the timeout (create_bootstrapped blocks 95s+).
  - wifi_cellular     WiFi->Cellular handover recovers to Active
  - airplane          offline pauses relays cleanly; restore recovers
  - pause_resume      backgrounding winds relays down (~30s); resume reconnects

README.md documents each scenario's rationale, device setup, when to re-run
(new Arti version, TorService/TorManager/dial-gate changes), and how the suite
relates to TorManagerTest / TorCircuitHealthTrackerTest / the instrumented test.

Verified on a Pixel emulator (WiFi + Cellular): all scenarios pass on this
branch; offline_bootstrap fails on main (no timeout), confirming the suite
discriminates fixed from broken.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:44:06 -04:00

130 lines
6.3 KiB
Markdown

# Tor networking scenario suite
A repeatable, device-driven test harness for Amethyst's embedded Tor (Arti)
across the network transitions that have historically broken it. These checks
live outside the JVM/instrumented test tree on purpose: they manipulate the
**device's network** (airplane mode, WiFi/cellular, app lifecycle) and assert on
the real `ArtiNative` bootstrap — things a unit test can't reach.
## When to run this
Run the suite whenever you touch anything in the Tor management path, and treat
a green run as a release gate for those changes:
- **Bumping Arti** (`tools/arti-build`, the rebuilt `libarti_android.so`) — a new
Arti version can change bootstrap timing, the SOCKS error mapping, or guard
handling. This is the most important trigger.
- **`TorService.kt`** — lifecycle (`start`/`stop`/`reset`/`resetWithCleanState`),
the bootstrap-timeout handling (`ARTI_ERROR_BOOTSTRAP_TIMEOUT`), or cache/state
wiping.
- **`TorManager.kt`** — the self-heal watchdog, `onNetworkChange`, cooldowns, or
status routing.
- **The dial gate** — `WebsocketBuilder.canConnect` / the `canDial` wiring in
`AppModules.kt` that holds Tor-routed relays until the SOCKS port is ready.
- **The connectivity layer** — `ConnectivityManager` / `RelayProxyClientConnector`
(pause/resume, reconnect-on-change).
## How it relates to the other tests
| Layer | Where | Covers |
|-------|-------|--------|
| Pure logic (fast, deterministic) | `amethyst/src/test/.../tor/TorManagerTest.kt` | watchdog, cooldown, `onNetworkChange`, status routing — virtual time, in-memory fakes, no Arti |
| Pure logic (fast) | `amethyst/src/test/.../service/relayClient/TorCircuitHealthTrackerTest.kt` | the Active-but-failing discriminator (companion circuit-health PR) |
| Real Arti, single process | `amethyst/src/androidTest/.../tor/TorBootstrapInstrumentedTest.kt` (`@Ignore`'d) | real `initialize``create_bootstrapped` → SOCKS round-trip + `destroy` |
| **Real Arti + real network transitions** | **this suite** | **bootstrap bounding, network change, airplane, pause/resume — end to end** |
The unit tests are the first line of defense (run in CI). This suite is the
end-to-end backstop you run by hand (or in a device lab) before shipping a Tor
change, because the failures it catches only appear with a real radio.
## Device setup (once)
1. Connect **one** device or emulator with internet egress to the Tor network.
An emulator that exposes both WiFi and Cellular (e.g. a stock Pixel AVD) is
ideal — the `wifi_cellular` scenario needs a second transport to fail over to.
2. Install the **debug** build: `./gradlew :amethyst:installPlayDebug`.
3. In the app, set **Tor = INTERNAL** and enable routing relays over Tor
(Settings → Network/Tor). The suite does **not** change Tor settings; it only
manipulates the network and the app lifecycle, so it tests the config the user
actually runs.
## Running
```bash
# full suite
tools/tor-network-tests/run.sh
# a single scenario
tools/tor-network-tests/run.sh offline_bootstrap
# override the package (e.g. a different flavor)
AMETHYST_PKG=com.vitorpamplona.amethyst.debug tools/tor-network-tests/run.sh
```
Exit code is `0` only if every selected scenario passes. The suite restores a
clean network state (airplane off, WiFi+data on) when it finishes.
## Scenarios
Each scenario clears logcat, drives a transition, then asserts on Tor's lifecycle
log markers (`SOCKS proxy active`, `bootstrap timed out`, `onNetworkChange`,
`Pausing/Resuming Relay Services`, `OnOpen`).
### `cold_start`
Force-restart on a healthy network. **Asserts:** Tor reaches `Active`, and the
number of Tor-routed dials issued *before* the SOCKS port was ready stays ~0
(the #3223 gate). A spike here means the gate regressed and the pool is hammering
a dead proxy during bootstrap.
### `offline_bootstrap` — the headline regression test for this PR
Wipe Arti state (empty cache), enable airplane mode, then cold-start so
`create_bootstrapped` runs with no network and nothing cached. **Asserts:** the
native bootstrap is *bounded* — a `bootstrap timed out` line appears (the 60s
`tokio::time::timeout` releasing `lifecycleMutex` so the self-heal watchdog can
run), instead of blocking for many minutes. Then restore the network and assert
recovery to `Active`.
> Without the timeout (`fix: bound Arti bootstrap with a 60s timeout`, this PR),
> `create_bootstrapped` blocks indefinitely holding the lifecycle lock — observed
> wedging at 95s+ on an emulator — and this scenario fails with "no 'bootstrap
> timed out'". That regression is invisible to every JVM test, which is why this
> suite exists.
### `wifi_cellular`
With Tor Active, disable WiFi (the default route fails over to cellular), wait,
re-enable. **Asserts:** the transport change drives `onNetworkChange` and Tor
returns to `Active`. Catches the "stale guards/circuits from the old network"
class of wedge.
### `airplane`
Toggle airplane mode on (full offline) then off. **Asserts:** relays pause
cleanly on connectivity loss (no Tor thrash while offline) and Tor + relays
recover on restore.
### `pause_resume`
Background the app (HOME), wait for the `WhileSubscribed(30s)` wind-down, then
foreground it. **Asserts:** relays pause (`Pausing Relay Services`) while
backgrounded and reconnect (`OnOpen` increases) on resume. The reconnection burst
on resume is the same shape as the post-Active warmup burst, so this also guards
against the circuit-health self-heal firing a false positive on resume.
## Interpreting failures
The summary prints `PASS`/`FAIL` per check with a one-line reason. Common ones:
- `offline_bootstrap` FAIL "no 'bootstrap timed out'" → the native timeout isn't
in the installed `.so` (rebuild `libarti_android.so` from `tools/arti-build`,
or you're on a branch without the fix).
- `cold_start` FAIL "pre-ready doomed dials >5" → the `canDial`/`canConnect` gate
regressed.
- `wifi_cellular` / `airplane` FAIL "did not reach Active" → bootstrap is slow or
wedged on the new network; pull the full log and look for repeated
`ExitTimeout` / `AllGuardsDown`.
For a deeper look at any run, dump the lifecycle directly:
```bash
adb logcat -d | grep -E "TorService|TorManager|ManageRelayServices" \
| grep -iE "Initializing|bootstrap|proxy active|timed out|self-heal|reset|onNetworkChange|Pausing|Resuming"
```