# Physical-device and cross-client release gate This runbook turns Milestone 10 into a repeatable release procedure. The gate uses a host-side CLI and USB/ADB as its control channel, so control traffic never shares BLE, Wi-Fi Aware, Nostr, or Tor with the system under test. The gate cannot pass without the required physical devices and counterpart clients. A pending or blocked result is useful diagnostic evidence, but it is not release approval. ## Safety and privacy rules - Use only disposable lab app data, identities, nicknames, messages, and files. - Never use a personal Nostr account or a production relay. - Do not put device serials, UDIDs, Bluetooth/MAC/IP addresses, peer IDs, usernames, email addresses, local home paths, or message contents in a result, trace, filename, issue, commit, or release artifact. - Device selectors may be supplied to ADB commands as ephemeral inputs. The tooling emits only logical aliases such as `android-current`. - Models, manufacturer classes, Android API levels, negotiated MTU classes, client versions, commit hashes, aggregate counts, durations, and stable failure reason codes are allowed. - Do not archive raw logcat. Convert observations to the structured, privacy-checked trace format, and keep any raw diagnostic capture local until it has been reviewed and sanitized. The validator rejects known identifying fields and values before a passing bundle can be created. ## Required lab Prepare: - At least three physical Android devices for three-hop relay testing. - At least two Android API levels and two manufacturer classes. - BLE central and peripheral support on every Android device. - At least one Android 13+ device with Wi-Fi Aware. - One physical device running the current iOS client. - The last supported Android client. - The release-candidate APK built from one exact full Git commit. - A local, disposable Nostr relay/Tor fixture with production network access blocked. One physical handset may be reused for the legacy-client phase after the current-client evidence for that slot is complete, but the matrix must keep the logical aliases and installed client versions unambiguous. ## 1. Verify the deterministic gate From the repository root: ```sh ./gradlew clientRewriteContractTest checkChangedLineCoverage lintDebug python3 tools/release_gate/release_gate.py validate-manifest ``` Do not begin device work from a dirty tree or a build whose deterministic gate does not pass. ## 2. Create the device matrix Copy `tools/release_gate/device-matrix.example.json` to an ignored working directory under `release-gate-results/`. Replace every template value and set both current-Android commit fields to the exact full commit under test. Probe Android capabilities without storing the ADB selector: ```sh python3 tools/release_gate/android_lab.py probe \ --serial "$BITCHAT_ADB_SELECTOR" \ --alias android-current ``` Copy only the returned logical metadata into the matrix. Validate it: ```sh python3 tools/release_gate/release_gate.py validate-matrix \ --matrix release-gate-results/device-matrix.json \ --commit "$BITCHAT_RELEASE_COMMIT" ``` The matrix validator enforces physical devices, three Android participants, two API levels, two manufacturer classes, Wi-Fi Aware, BLE roles, iOS, and explicit current/legacy client versions. ## 3. Initialize disposable fixtures ```sh python3 tools/release_gate/release_gate.py init \ --matrix release-gate-results/device-matrix.json \ --commit "$BITCHAT_RELEASE_COMMIT" \ --run-id rc-lab-01 \ --output release-gate-results/rc-lab-01 ``` Initialization pins the scenario and fixture manifests, creates every scenario as `pending`, and generates deterministic: - zero-byte and small files; - a Unicode-named medium file; - sparse exact-maximum and oversized boundary files. The fixture manifest records size and SHA-256. The final archive contains the manifest, not the large fixture bodies. Clear only the disposable app data on each selected lab device: ```sh python3 tools/release_gate/android_lab.py prepare \ --serial "$BITCHAT_ADB_SELECTOR" \ --confirm-disposable-app-data ``` This stops the app and runs package-data cleanup. The explicit confirmation is required because the operation is destructive to that app's local data. ## 4. Execute scenarios The canonical scenario list is `tools/release_gate/scenarios.json`. It contains 27 mandatory scenarios: - the complete physical transport matrix; - Android API/manufacturer/permission/background coverage; - 11 Android-to-Android workflows; - 8 cross-client/backward-compatibility workflows; - 6 background and endurance workflows. For each scenario: 1. Confirm the listed participants and capabilities. 2. Perform the corresponding steps in [device-transport-test-matrix.md](device-transport-test-matrix.md) and the Milestone 10 checklist. 3. Record connection, lifecycle, transport, receipt, resource, and terminal state as aggregate evidence. 4. Append at least one structured trace event. 5. Mark the scenario `pass`, `fail`, `blocked`, or `unsupported`. Record evidence with the exact keys declared by the scenario: ```sh python3 tools/release_gate/release_gate.py record \ --run release-gate-results/rc-lab-01 \ --scenario A2A-001 \ --status pass \ --evidence connection-transitions=4 \ --evidence packet-correlation-count=6 \ --evidence failure-reasons=none ``` Append a privacy-safe trace event: ```sh python3 tools/release_gate/release_gate.py trace \ --run release-gate-results/rc-lab-01 \ --scenario A2A-001 \ --source android-current \ --event reconnect-terminal \ --outcome pass \ --metric reconnect-count=1 \ --metric duplicate-delivery-count=0 ``` Capture resource snapshots during endurance work: ```sh python3 tools/release_gate/android_lab.py snapshot \ --serial "$BITCHAT_ADB_SELECTOR" \ --alias android-current \ --run release-gate-results/rc-lab-01 \ --scenario END-003 ``` Use run-local sequential correlation labels while observing packets; archive only aggregate correlation counts. Record failures with a stable reason code, file a regression issue, and preserve the incomplete artifact. ## 5. Endurance requirements - `END-001` requires at least 240 minutes. - `END-002` requires at least 50 large-transfer/cancellation cycles. - Sample memory, threads, file descriptors, wake locks, connection counts, and late callbacks at consistent intervals. - A passing result requires bounded resource behavior and a clean terminal state; merely completing the time window is insufficient. The validator rejects shorter durations and cycle counts. ## 6. Inspect progress and validate During a run: ```sh python3 tools/release_gate/release_gate.py validate \ --run release-gate-results/rc-lab-01 \ --allow-incomplete python3 tools/release_gate/release_gate.py summary \ --run release-gate-results/rc-lab-01 ``` The release validator, without `--allow-incomplete`, requires: - every scenario to be `pass`; - every declared evidence field; - at least one structured trace per scenario; - the pinned scenario and fixture manifests; - the exact client commit and complete device matrix; - endurance minimums; - a completion timestamp; - no detected identifying fields or values. `unsupported`, `blocked`, and `pending` never satisfy release approval. ## 7. Archive release approval After the complete validator passes: ```sh python3 tools/release_gate/release_gate.py bundle \ --run release-gate-results/rc-lab-01 \ --output release-gate-results/rc-lab-01.zip ``` The deterministic archive contains the scenario manifest, device/client matrix, results, structured trace, fixture manifest, Markdown summary, and `SHA256SUMS`. Attach it to the release approval record without renaming fields or adding raw diagnostics. Finally, clean the disposable app data with the same confirmed `cleanup` command and stop the local relay/Tor fixture. ## Failure handling - `fail`: behavior violated a contract. Record a stable reason code, file a bug, add a deterministic regression where possible, fix it, and rerun the affected scenario plus dependent scenarios. - `blocked`: required lab infrastructure or counterpart client was unavailable. Preserve the artifact and do not approve release. - `unsupported`: the selected device lacks a capability. Because the defined matrix requires Wi-Fi Aware, replace the device or matrix; unsupported does not waive a mandatory scenario. - A flaky result is a failure until its cause is understood. Never average retries into a pass. ## Appendix: mesh lab (ADB test hooks, debug builds) For day-to-day development there is a lighter-weight harness that drives a debug-only broadcast receiver (`app/src/debug/`, never shipped in release) exposing mesh operations over ADB: scan, connect, Noise handshake, DMs, public broadcast, announce, file send/receive, BLE toggle, state dumps, and raw packet injection. Results are JSON files in the app sandbox polled by the host (`cache/testhook/results/.json`, also logged under tag `TestHook`). ### Prerequisites - A JDK (e.g. the one bundled with Android Studio; set `JAVA_HOME`) and the Android SDK platform-tools. `adb` must be on `PATH` or `ANDROID_HOME` set. - Python 3.10+ on the host. No third-party packages are required. - **Two physical Android devices** (API 26+, BLE) with USB debugging enabled, both plugged into the host. Emulators are not supported (BLE mesh). - Verify both are visible: `adb devices` → note the serials. ### Device preparation (important) Keep both phones **unlocked with the screen on** for the whole run. A locked or dozing device forces the app into POWER_SAVER (1 s BLE scan per 60 s), which makes discovery and handshakes take minutes and will flake every scenario. The harness runs `wake()` (dismiss keyguard, stretch screen timeout) during `setup`, but it cannot defeat a secure lock screen — unlock the devices manually first. Note that `svc power stayon` only helps while a device is actually charging. ### Build and set up ```sh ./gradlew assembleDebug python3 tools/release_gate/mesh_lab.py setup \ --serial-a --serial-b \ --apk app/build/outputs/apk/debug/app-arm64-v8a-debug.apk ``` `setup` cycles Bluetooth, installs the APK, clears app data, grants all runtime permissions, wakes and launches the app, sets deterministic nicknames (`alice`/`bob`), and waits for mutual peer discovery. It is safe (and recommended) to rerun `setup` before each scenario batch; `--apk` may be omitted if the current build is already installed. ### Run scenarios ```sh python3 tools/release_gate/mesh_lab.py scenario all \ --serial-a --serial-b --out /tmp/meshlab-evidence ``` | Scenario | What it asserts | |---|---| | `dm` | Noise handshake both ways, encrypted DM round trips with content match | | `favorite_verification` | favorite signal, orange-outline/filled mutual state, and peer fingerprint verification | | `broadcast` | public mesh message A→B | | `file` | 1 KB broadcast file, receiver SHA-256 matches fixture | | `file_oversize` | >256-fragment broadcast file is rejected sender-side, receiver sees nothing | | `file_private` | Noise-encrypted private file, digest match | | `media_private` | private-chat contact ID resolves to the live mesh peer; voice, image, and generic-file digests match | | `raw` | raw packet injection is accepted by the mesh | | `session_recovery` | force-stop B mid-session: identity persists, re-handshake, DMs flow again | | `identity_reset` | pm clear B mid-session: new identity, rediscovery, handshake, DMs | | `all` | every scenario above in sequence | Each run writes `-evidence.json` to `--out` (digests, timings, session states, logcat excerpts on failure) and exits non-zero on failure. Evidence is a local diagnostic artifact; it may contain lab peer IDs and is not privacy-checked like release-gate bundles — do not publish it. ### Ad-hoc commands Any hook command can be sent to one device directly: ```sh python3 tools/release_gate/mesh_lab.py cmd --serial scan --extra timeout_ms=30000 python3 tools/release_gate/mesh_lab.py cmd --serial handshake --extra peer= python3 tools/release_gate/mesh_lab.py cmd --serial state # full mesh dump ``` See `TestHookDriver.kt` for the full command set (`ping`, `start`, `stop`, `whoami`, `set_nickname`, `scan`, `peers`, `connect`, `handshake`, `session`, `announce`, `broadcast_msg`, `dm_send`, `dm_recv`, `msg_recv`, `favorite_set`, `favorite_status`, `verification_set`, `verification_status`, `file_send`, `file_recv`, `file_cancel`, `raw_send`, `ble`, `state`, `clear_results`). ### Troubleshooting - **Discovery/handshake timeouts**: almost always a locked or dozing phone — unlock both devices and rerun `setup`. `cmd ... state` shows `App In Background: true` and the BLE duty cycle when this is the cause. - **Stale app state after many churn runs**: `svc bluetooth disable/enable` on both devices (done automatically by `setup`) clears zombie GATT links. - **Watch the wire**: `adb -s logcat -s TestHook MessageHandler FragmentManager BitchatFilePacket` shows commands, results, decrypt failures, fragment rejects, and saved incoming files in real time. - Results also persist on-device at `run-as com.bitchat.droid cat cache/testhook/results/.json`. Unlike the release gate, this harness is a development aid: it prints raw diagnostics and does not produce a privacy-checked approval bundle.