# 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/<id>.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-1> --serial-b <serial-2> \
  --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-1> --serial-b <serial-2> --out /tmp/meshlab-evidence
```

| Scenario | What it asserts |
|---|---|
| `dm` | Noise handshake both ways, encrypted DM round trips with content match |
| `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 `<scenario>-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 <serial> scan --extra timeout_ms=30000
python3 tools/release_gate/mesh_lab.py cmd --serial <serial> handshake --extra peer=<peer-id>
python3 tools/release_gate/mesh_lab.py cmd --serial <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`, `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 <serial> 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/<id>.json`.

Unlike the release gate, this harness is a development aid: it prints raw
diagnostics and does not produce a privacy-checked approval bundle.
