Add optional NDR device E2E harness

This commit is contained in:
Dev 2026-05-02 22:30:23 +03:00
parent 2567878921
commit 85a3be9a54
4 changed files with 350 additions and 0 deletions

1
.gitignore vendored
View File

@ -80,3 +80,4 @@ build.log
# Local configs
Local.xcconfig
.local/

View File

@ -43,3 +43,14 @@ This test suite uses an in-memory networking harness to make end-to-end and inte
- Enable broadcast flooding for Integration suites only:
- `MockBLEService.autoFloodEnabled = true`
## Optional Device Smoke
For a real macOS/iOS/Android NDR private-message smoke, use:
- `cp scripts/ndr-device-e2e.env.example .local/ndr-device-e2e.env`
- Fill device IDs and the Android checkout path in `.local/ndr-device-e2e.env`
- `scripts/ndr-device-e2e.sh run`
The script keeps local device IDs in gitignored `.local/`, launches configured
clients, captures logs, and prints a manual checklist for cross-device private
messages and late-subscription relay delivery.

View File

@ -0,0 +1,23 @@
# Copy to .local/ndr-device-e2e.env and fill local values there.
# The .local directory is gitignored.
# Physical iOS device UDID for xcodebuild/devicectl. Leave empty to skip iOS.
IOS_DEVICE_ID=
# Bundle identifier installed on the iOS device. If Local.xcconfig changes the
# default bundle ID, set the resolved ID here.
IOS_BUNDLE_ID=chat.bitchat
# Android checkout and adb serial. Leave ANDROID_SERIAL empty to skip Android.
ANDROID_REPO=../bitchat-android
ANDROID_SERIAL=
ANDROID_PACKAGE=com.bitchat.droid
ANDROID_ACTIVITY=com.bitchat.android.MainActivity
# Uncomment any of these to override auto-detection.
# RUN_MAC=1
# RUN_IOS=1
# RUN_ANDROID=1
# How long to keep collecting logs while the manual device smoke is running.
DURATION_SECONDS=240

315
scripts/ndr-device-e2e.sh Executable file
View File

@ -0,0 +1,315 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOCAL_ENV="${NDR_DEVICE_E2E_ENV:-$ROOT_DIR/.local/ndr-device-e2e.env}"
RUN_ROOT="${NDR_DEVICE_E2E_RUN_ROOT:-$ROOT_DIR/.local/ndr-device-e2e}"
COMMAND="${1:-run}"
if [[ $# -gt 0 ]]; then
shift
fi
usage() {
cat <<'EOF'
Usage:
scripts/ndr-device-e2e.sh run [--duration seconds]
scripts/ndr-device-e2e.sh preflight
scripts/ndr-device-e2e.sh checklist
Optional local config:
cp scripts/ndr-device-e2e.env.example .local/ndr-device-e2e.env
$EDITOR .local/ndr-device-e2e.env
What this does:
- Builds and launches the macOS app unless RUN_MAC=0.
- Builds, installs, and launches the iOS app when IOS_DEVICE_ID is set.
- Builds, installs, and launches the Android app when ANDROID_SERIAL is set.
- Captures macOS, iOS, and Android logs into .local/ndr-device-e2e/.
- Prints a manual checklist for cross-device NDR/private-message smoke.
The script intentionally does not contain local device names or IDs. Keep those
in .local/ndr-device-e2e.env, which is ignored by git.
EOF
}
log() {
printf '[ndr-device-e2e] %s\n' "$*"
}
die() {
printf '[ndr-device-e2e] error: %s\n' "$*" >&2
exit 1
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || die "missing command: $1"
}
load_env() {
if [[ -f "$LOCAL_ENV" ]]; then
# shellcheck disable=SC1090
source "$LOCAL_ENV"
fi
ANDROID_REPO="${ANDROID_REPO:-$ROOT_DIR/../bitchat-android}"
if [[ "$ANDROID_REPO" != /* ]]; then
ANDROID_REPO="$ROOT_DIR/$ANDROID_REPO"
fi
IOS_BUNDLE_ID="${IOS_BUNDLE_ID:-chat.bitchat}"
ANDROID_PACKAGE="${ANDROID_PACKAGE:-com.bitchat.droid}"
ANDROID_ACTIVITY="${ANDROID_ACTIVITY:-com.bitchat.android.MainActivity}"
DURATION_SECONDS="${DURATION_SECONDS:-240}"
RUN_MAC="${RUN_MAC:-1}"
if [[ -n "${IOS_DEVICE_ID:-}" ]]; then
RUN_IOS="${RUN_IOS:-1}"
else
RUN_IOS="${RUN_IOS:-0}"
fi
if [[ -n "${ANDROID_SERIAL:-}" ]]; then
RUN_ANDROID="${RUN_ANDROID:-1}"
else
RUN_ANDROID="${RUN_ANDROID:-0}"
fi
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--duration)
[[ $# -ge 2 ]] || die "--duration requires seconds"
DURATION_SECONDS="$2"
shift 2
;;
--help|-h)
usage
exit 0
;;
*)
die "unknown argument: $1"
;;
esac
done
}
preflight() {
need_cmd xcodebuild
need_cmd xcrun
if [[ "$RUN_ANDROID" == "1" ]]; then
need_cmd adb
[[ -d "$ANDROID_REPO" ]] || die "ANDROID_REPO does not exist: $ANDROID_REPO"
[[ -x "$ANDROID_REPO/gradlew" ]] || die "gradlew not executable in ANDROID_REPO: $ANDROID_REPO"
[[ -n "${ANDROID_SERIAL:-}" ]] || die "ANDROID_SERIAL must be set when RUN_ANDROID=1"
adb -s "$ANDROID_SERIAL" get-state >/dev/null
fi
if [[ "$RUN_IOS" == "1" ]]; then
[[ -n "${IOS_DEVICE_ID:-}" ]] || die "IOS_DEVICE_ID must be set when RUN_IOS=1"
fi
}
make_run_dir() {
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
RUN_DIR="$RUN_ROOT/$RUN_ID"
DERIVED_DATA="$RUN_DIR/DerivedData"
mkdir -p "$RUN_DIR"
log "writing logs to $RUN_DIR"
}
build_and_launch_mac() {
[[ "$RUN_MAC" == "1" ]] || return 0
log "building macOS app"
xcodebuild \
-project "$ROOT_DIR/bitchat.xcodeproj" \
-scheme "bitchat (macOS)" \
-configuration Debug \
-destination "platform=macOS" \
-derivedDataPath "$DERIVED_DATA/macos" \
CODE_SIGNING_ALLOWED=NO \
build \
-quiet
local app_path
app_path="$(find "$DERIVED_DATA/macos/Build/Products/Debug" -name 'bitchat.app' -type d -print -quit)"
[[ -n "$app_path" ]] || die "macOS app build did not produce bitchat.app"
log "launching macOS app"
open "$app_path"
}
build_install_launch_ios() {
[[ "$RUN_IOS" == "1" ]] || return 0
log "building iOS app for configured device"
xcodebuild \
-project "$ROOT_DIR/bitchat.xcodeproj" \
-scheme "bitchat (iOS)" \
-configuration Debug \
-destination "id=$IOS_DEVICE_ID" \
-derivedDataPath "$DERIVED_DATA/ios" \
build \
-quiet
local app_path
app_path="$(find "$DERIVED_DATA/ios/Build/Products/Debug-iphoneos" -name 'bitchat.app' -type d -print -quit)"
[[ -n "$app_path" ]] || die "iOS device build did not produce bitchat.app"
log "installing iOS app"
xcrun devicectl device install app --device "$IOS_DEVICE_ID" "$app_path" >/dev/null
log "launching iOS app"
xcrun devicectl device process launch --device "$IOS_DEVICE_ID" "$IOS_BUNDLE_ID" >/dev/null || true
}
build_install_launch_android() {
[[ "$RUN_ANDROID" == "1" ]] || return 0
log "building and installing Android app"
(
cd "$ANDROID_REPO"
./gradlew installDebug -Pandroid.injected.adb.device.serial="$ANDROID_SERIAL"
)
log "granting Android runtime permissions when possible"
for perm in \
android.permission.BLUETOOTH_SCAN \
android.permission.BLUETOOTH_CONNECT \
android.permission.BLUETOOTH_ADVERTISE \
android.permission.ACCESS_FINE_LOCATION \
android.permission.ACCESS_COARSE_LOCATION \
android.permission.POST_NOTIFICATIONS; do
adb -s "$ANDROID_SERIAL" shell pm grant "$ANDROID_PACKAGE" "$perm" >/dev/null 2>&1 || true
done
log "launching Android app"
adb -s "$ANDROID_SERIAL" shell am start -n "$ANDROID_PACKAGE/$ANDROID_ACTIVITY" >/dev/null
}
LOG_PIDS=()
start_logs() {
if [[ "$RUN_MAC" == "1" ]]; then
log "capturing macOS logs"
(log stream --style compact --predicate 'process == "bitchat"' >"$RUN_DIR/macos.log" 2>&1) &
LOG_PIDS+=("$!")
fi
if [[ "$RUN_IOS" == "1" ]]; then
log "capturing iOS logs"
(xcrun devicectl device log stream --device "$IOS_DEVICE_ID" --predicate 'process == "bitchat"' >"$RUN_DIR/ios.log" 2>&1) &
LOG_PIDS+=("$!")
fi
if [[ "$RUN_ANDROID" == "1" ]]; then
log "capturing Android logs"
adb -s "$ANDROID_SERIAL" logcat -c || true
(adb -s "$ANDROID_SERIAL" logcat -v time \
MainActivity:D \
NostrRelayManager:V \
NostrTransport:V \
NdrNostrService:V \
NostrDirectMessageHandler:V \
BluetoothMeshService:D \
MeshForegroundService:D \
'*:S' >"$RUN_DIR/android.log" 2>&1) &
LOG_PIDS+=("$!")
fi
}
stop_logs() {
local pid
for pid in "${LOG_PIDS[@]:-}"; do
kill "$pid" >/dev/null 2>&1 || true
wait "$pid" >/dev/null 2>&1 || true
done
}
print_checklist() {
cat <<'EOF'
Manual cross-device NDR smoke:
1. Bring the configured clients online: macOS, iOS device, and Android device.
2. Confirm each client has network permission and can connect to Nostr relays.
3. Establish/refresh mutual favorites over the app UI so NDR out-of-band invite
exchange can happen over the mesh path.
4. Send private messages in both directions for each pair:
- macOS <-> iOS
- macOS <-> Android
- iOS <-> Android
5. Force one client to quit, relaunch it, then send again to verify stored relay
events are processed after late subscription.
6. Watch for:
- successful NDR session creation
- kind 1060 publishes
- decrypted private messages
- repeated subscription churn or excessive relay reconnects
- failed/rejected relay publishes
EOF
}
summarize_one_log() {
local label="$1"
local path="$2"
[[ -f "$path" ]] || return 0
local ndr_count
local relay_count
local error_count
ndr_count="$(grep -Eci 'Ndr|double.?ratchet|kind[ =:]1060|decrypted' "$path" || true)"
relay_count="$(grep -Eci 'REQ|subscribe|subscription|relay|WebSocket|kind[ =:]1060' "$path" || true)"
error_count="$(grep -Eci 'error|failed|reject|panic|fatal|exception' "$path" || true)"
printf '%s: ndr=%s relay=%s errors=%s log=%s\n' \
"$label" "$ndr_count" "$relay_count" "$error_count" "$path"
}
summarize_logs() {
log "log summary"
summarize_one_log "macOS" "$RUN_DIR/macos.log"
summarize_one_log "iOS" "$RUN_DIR/ios.log"
summarize_one_log "Android" "$RUN_DIR/android.log"
}
run_all() {
preflight
make_run_dir
trap stop_logs EXIT
build_and_launch_mac
build_install_launch_ios
build_install_launch_android
start_logs
print_checklist
log "collecting logs for ${DURATION_SECONDS}s"
sleep "$DURATION_SECONDS"
stop_logs
summarize_logs
}
load_env
parse_args "$@"
case "$COMMAND" in
run)
run_all
;;
preflight)
preflight
log "preflight ok"
;;
checklist)
print_checklist
;;
help|--help|-h)
usage
;;
*)
usage >&2
exit 2
;;
esac