Android Testing Infrastructure & Bug Fixes#3
Merged
Conversation
Comprehensive documentation covering the dual-process architecture (React Native + embedded Node.js), IPC protocol over Unix domain sockets, Android foreground service lifecycle, directory structure, data flow, and platform status. https://claude.ai/code/session_01JvQt9SCcroodTwtW4NhiC7
Documents what CoMapeo is, the relationship to Mapeo, and how this module fits into the broader ecosystem (@comapeo/core, @comapeo/ipc, comapeo-mobile, comapeo-desktop, comapeo-cloud). Explains how the messagePort connects to @comapeo/ipc's createMapeoServer/Client pattern. https://claude.ai/code/session_01JvQt9SCcroodTwtW4NhiC7
- Add testID props to example app for element targeting - Create 5 Maestro flow files testing: app launch, state transitions, Node.js process startup, IPC round-trip (1000 messages), and multi-round messaging stability - Add e2e runner script with emulator management, APK build/install, and single-flow or full-suite execution modes - Add e2e:android script to example/package.json - Document setup, prerequisites, and CI integration https://claude.ai/code/session_01JvQt9SCcroodTwtW4NhiC7
Three-layer test infrastructure:
1. JVM unit tests (android/src/test/) - no device needed:
- MessageFramingTest: length-prefix protocol encoding/decoding,
buffer reuse correctness, unicode handling, multi-frame sequences
2. Android instrumented tests (android/src/androidTest/) - requires device:
- NodeJSIPCTest: IPC protocol against mock LocalServerSocket (connect,
send, receive, echo round-trip, 100-message burst, 64KB messages,
delayed socket creation, server disconnect handling)
- ServiceLifecycleTest: foreground service via intents (start, stop,
separate process verification, START_STICKY restart after kill,
socket file lifecycle, notification presence)
- ShutdownPathTest: graceful shutdown, process kill recovery, stop
during startup race, 3x start/stop cycles, force-stop cleanup
- WatchForFileTest: FileObserver suspension, cancellation, TOCTOU
scenarios, wrong-file filtering, parent directory creation
3. Runner script (e2e/run-instrumented-tests.sh) with --class filtering,
--unit-only mode, and automatic emulator boot
https://claude.ai/code/session_01JvQt9SCcroodTwtW4NhiC7
- GitHub Actions workflow (.github/workflows/android-tests.yml) with two jobs: - JVM unit tests: runs without emulator, caches gradle + nodejs-mobile - Instrumented tests: uses reactivecircus/android-emulator-runner with KVM - Both jobs: expo prebuild, download nodejs-mobile binaries, auto-discover the Expo-autolinked Gradle module name - Download script (scripts/download-nodejs-mobile.sh): - Fetches nodejs-mobile prebuilt Android binaries from GitHub releases - Supports version override via argument or NODEJS_MOBILE_VERSION env - Caches with .version marker to skip redundant downloads - Verifies expected structure (libnode.so per ABI + node headers) - Fix NodeJSIPCTest: LocalServerSocket(String) uses abstract namespace but NodeJSIPC connects to FILESYSTEM namespace. Fixed by binding a LocalSocket to the filesystem address and passing its FD to LocalServerSocket. - Remove unused imports across test files https://claude.ai/code/session_01JvQt9SCcroodTwtW4NhiC7
When no comapeo module is found by the initial grep, the fallback diagnostic grep also returns exit code 1 (no matches), causing the script to fail under bash -e before the fallback MODULE_NAME is set. Add || true to both grep pipelines so the fallback path works. https://claude.ai/code/session_01JvQt9SCcroodTwtW4NhiC7
The gradle-wrapper.jar is gitignored and not available in CI, causing "Could not find or load main class org.gradle.wrapper.GradleWrapperMain". Use gradle/actions/setup-gradle and generate the wrapper before running any ./gradlew commands. https://claude.ai/code/session_01JvQt9SCcroodTwtW4NhiC7
Running `gradle wrapper` inside example/android causes Gradle 9.3.1 to compile the project's Expo/RN Kotlin plugins against its bundled Kotlin 2.2.0 stdlib, which is incompatible with the older Kotlin versions those plugins expect. Generate the wrapper in an empty temp dir instead and copy just the jar file. https://claude.ai/code/session_01JvQt9SCcroodTwtW4NhiC7
Remove ServiceLifecycleTest and ShutdownPathTest which can't work as library-level instrumented tests (test APK runs as different uid and can't start non-exported service in example app). Remove unused IServiceCallback reference left after AIDL removal. Simplify CI by removing broken Gradle wrapper generation step (expo prebuild already generates it) and fragile module name discovery. Remaining tests (16 instrumented + JVM) all pass locally: - MessageFramingTest: length-prefixed framing protocol - NodeJSIPCTest: IPC socket communication - WatchForFileTest: file watching utility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move service lifecycle and shutdown path tests to the example app's androidTest (same UID as the service) to fix SecurityException when starting the unexported ComapeoCoreService. Tests that require a full Node.js backend are @ignore'd for now — they'll be enabled once the app is fully built with the JS bundle. Passing tests (4 app + 16 library + JVM): - ServiceLifecycleTest: start, separate process, stop - ShutdownPathTest: stop during startup - NodeJSIPCTest: IPC socket communication (9 tests) - WatchForFileTest: file watching utility (7 tests) - MessageFramingTest: framing protocol (JVM) Also: - Check in example/android/ (remove from .gitignore) so we can add androidTest files without expo prebuild regeneration - Remove expo prebuild step from CI (no longer needed) - Add :app:connectedDebugAndroidTest to CI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The jar is required to bootstrap Gradle builds. It was being excluded by a global ~/.gitignore_global rule for *.jar files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add jlumbroso/free-disk-space to remove dotnet, haskell, large packages, and docker images (~15-20GB freed) - Set disk-size: 2048M on emulator (down from default 7.4GB) - Remove unused pre-installed NDKs and platform versions - Use API level 30 for emulator (smaller system image) - Restore npm install (needed for Gradle's node-based module resolution) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…sion Gradle auto-downloads the NDK version specified in build.gradle, so manual sdkmanager install was redundant and was installing the wrong version (27.0.12077973 vs 27.1.12297006 that Gradle actually needs). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The android-emulator-runner action runs each line of the script as a separate shell command, so cd and gradlew must be on the same line. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
serviceRunsInSeparateProcess and stopActionStopsService both require the service to remain running, which needs the full JS backend bundle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Gradle task to auto-install nodejs-project node_modules before asset merging, so the Node.js backend can find its dependencies - Remove all @ignore annotations — all 9 tests now pass - Fix test cleanup race condition: stop using startServiceWithAction(STOP) in stopServiceAndWait() as it starts the service process and queues intents that race with the test's USER_FOREGROUND intent Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The CI emulator (x86_64 software rendering) is significantly slower than local ARM emulators. Wait 5 seconds after service registers as running to let Node.js fully initialize before testing further actions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addresses 11 bugs identified in code review: - ComapeoCoreService: reset isServiceStarted on stop/destroy, replace runBlocking with serviceScope.launch to prevent ANR on main thread - NodeJSIPC: break send loop on IOException instead of infinite retry, allow reconnection from error state, close/recreate sendChannel on disconnect/connect, expose connectionState property - NodeJSService: replace lateinit ipc with CompletableDeferred to prevent UninitializedPropertyAccessException race, return early in stop() when not running instead of throwing, separate destroy() from finally block - ComapeoCoreReactActivityLifecycleListener: safe-call nullable activity - watchForFile: start FileObserver before checking existence to fix TOCTOU race, add withTimeout wrapper (default 30s) - control-rpc.js: declare missing count variable - ComapeoCoreModule: return actual IPC connection state from getState() Adds JVM unit tests for CompletableDeferred and timeout patterns, and instrumented tests for stop-then-restart and waitForFile behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The async onDestroy coroutine could call Process.killProcess() after a new service instance had already started in the same process, causing "startForegroundService did not call startForeground" crashes. Track active instance count so killProcess is skipped when a new instance is alive. Also change stopThenRestartWorks test to use stopServiceAndWait() (process kill) instead of STOP action to avoid race on slow CI emulators. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tract Android requires startForeground() to be called within 5 seconds every time startForegroundService() is invoked, even if the service is already in the foreground. The old code returned early from startService() when isServiceStarted was true, skipping the startForeground() call and causing a RemoteServiceException crash on the second USER_FOREGROUND. Move startForeground() before the isServiceStarted guard so it is always called, while still only starting Node.js once. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two changes to improve instrumented test performance: 1. Switch from google_apis to google_atd (Automated Test Device) target. ATD images strip unnecessary apps and services, resulting in faster boot and lower resource usage. Available at API 30. 2. Add AVD snapshot caching. On first run, the emulator boots and saves a snapshot to the GitHub Actions cache. Subsequent runs boot from the cached snapshot instead of cold-booting, saving 1-3 minutes. Also removed profile and disk-size/heap-size overrides since ATD images have appropriate defaults and don't need a device profile. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
) - Enable org.gradle.parallel=true and org.gradle.caching=true in gradle.properties - Pass -PreactNativeArchitectures=x86_64 to all gradlew invocations in CI, reducing native build targets from 4 to 1 (~67% native build reduction) https://claude.ai/code/session_019Lbxh6CTzAuSh5YvP6tkGj Co-authored-by: Claude <noreply@anthropic.com>
gmaclennan
added a commit
that referenced
this pull request
Mar 12, 2026
* origin/main: Android Testing Infrastructure & Bug Fixes (#3) # Conflicts: # .github/workflows/android-tests.yml # android/build.gradle # android/src/main/assets/nodejs-project/lib/control-rpc.js # android/src/main/java/com/comapeo/core/ComapeoCoreModule.kt # android/src/main/java/com/comapeo/core/ComapeoCoreService.kt # android/src/main/java/com/comapeo/core/NodeJSService.kt # example/App.tsx
gmaclennan
added a commit
that referenced
this pull request
May 6, 2026
Addresses the high- and medium-priority issues from the review subagent's pass over Phase 1+2a+2b: Boot transaction lifecycle (was: review #1, #15) - applyAndEmit now closes bootTx and drains in-flight phase spans on STOPPING / STOPPED transitions too — not just STARTED / ERROR. stop()-from-STARTING transitions to STOPPING (rule 3 of deriveLifecycleState) and bypassed both terminals; destroy() forcing STOPPED-via-stopRequested did the same. Status mapped: STARTED→ok, ERROR→ internal_error, STOPPING/STOPPED→cancelled. - startBootTransaction now passes a TransactionContext carrying TracesSamplingDecision(true, 1.0). The previous TransactionOptions-only setup didn't actually force sampling, so with the SDK default tracesSampleRate=0.0 the boot transaction was dropped before reaching the wire. SentryConfig misconfig handling (was: review #3) - Both Kotlin and Swift readers used to crash on (DSN-set, environment-missing) — meant to be "fail loud" but a stale prebuild from before the validation was added would crash every cold start with no recovery. Now log loud (System.err on Android since android.util.Log isn't mocked on JVM tests; NSLog on iOS) and return null (Sentry off). Updated test renamed to assert "returns null, doesn't throw". Span op/description ordering (was: review #19) - transaction.startChild(op, description) — op is the indexed dashboard column. Was passing ("boot", "boot.<phase>"), swapped to ("boot.<phase>", human-readable description) so the dashboard groups by the phase taxonomy that matches the bench backend's boot-spans.js helper. Plugin idempotency (was: review #7) - Previously, dropping `props.sentry` from the plugin registration left stale meta-data / plist entries from a previous prebuild (with `expo prebuild --no-clean`). Plugin now passes through a no-Sentry cleanup mod that strips every key it owns; consumer-owned keys (e.g. io.sentry.* set by @sentry/react-native's plugin) are untouched. messageerror payload truncation (was: review #8) - src/sentry.ts now truncates the wrapped error message to 256 chars before forwarding to captureException. The control-frame parser surfaces offending input verbatim, which can include arbitrary bytes from a corrupted frame — truncating keeps Sentry events small and readable. IPC + SEND_ERROR_NATIVE breadcrumbs/events (was: review #9, #10) - NodeJSIPC's onConnectionStateChange callback wired in the FGS-side controlIpc construction; emits comapeo.ipc breadcrumbs at info (warning on Error). Per §7.4.5. - SEND_ERROR_NATIVE_TIMEOUT_MS firing now captures a level=warning event with timeout:errorNativeForward tag. Per §7.4.4. Logging swallowed surprises (was: review low-priority) - SentryFgsBridge's empty `catch (t: Throwable) {}` blocks now Log.w so debug builds notice swallowed bridge / SDK bugs. Post-init bridge tests (was: review #6) - New SentryFgsBridgeImplTest spins up a real Sentry hub via the cross-platform Sentry.init(SentryOptions) path with an in-memory ITransport. Covers: addBreadcrumb (no envelope on its own), captureException + captureMessage (envelope enqueued), startBootTransaction with global tracesSampleRate=0.0 (must still reach transport thanks to the TracesSamplingDecision override — regression test for the §15 bug above), boot span lifecycle, finishSpan with cancelled status, unknown level fallback to INFO. All Sentry-related tests pass: 25 cases across SentryConfigTest (8), SentryFgsBridgeTest (10), SentryFgsBridgeImplTest (7). Verified locally: - npm run lint clean - npx tsc --noEmit clean - ./gradlew :comapeo-core-react-native:testDebugUnitTest passes - ./gradlew :comapeo-core-react-native:compileDebugKotlin succeeds with sentry-android on the compile classpath
gmaclennan
added a commit
that referenced
this pull request
Jun 22, 2026
## Optic Release Automation This **draft** PR is opened by Github action [optic-release-automation-action](https://github.com/nearform-actions/optic-release-automation-action). A new **draft** GitHub release [v1.0.0-pre.2](https://github.com/digidem/comapeo-core-react-native/releases/tag/untagged-c499977757c9745e56b2) has been created. Release author: @gmaclennan #### If you want to go ahead with the release, please merge this PR. When you merge: - The GitHub release will be published - The npm package with tag pre will be published according to the publishing rules you have configured - No major or minor tags will be updated as configured #### If you close the PR - The new draft release will be deleted and nothing will change ## What's Changed * Android Testing Infrastructure & Bug Fixes by @gmaclennan in #3 * chore: prebuild example/android; harden instrumented tests by @gmaclennan in #10 * Integrate @comapeo/core via IPC over Unix sockets by @gmaclennan in #5 * chore: adjust repo setup by @achou11 in #12 * chore: minor fixes based on expo-doctor by @achou11 in #13 * Add iOS support & test infrastructure by @gmaclennan in #6 * chore: add architecture docs & plans by @gmaclennan in #11 * update some native deps used in backend by @achou11 in #14 * iOS Phase 1: unified JS bundle + smoke test (simulator-only) by @gmaclennan in #15 * iOS Phase 2: xcframework Embed & Sign for native addons by @gmaclennan in #16 * Phase 2 Android: jniLibs packaging + unified rollup loader plugin by @gmaclennan in #17 * chore: post-Phase-2 cleanup — comments, plan docs, agents.md by @gmaclennan in #33 * android: read abiFilters from reactNativeArchitectures (#30) by @gmaclennan in #35 * refactor: simplify build-backend.ts; rollup writes directly to native asset trees by @gmaclennan in #34 * chore: fix eslint configuration by @achou11 in #41 * android: audit 16 KB page alignment on every shipped .so by @gmaclennan in #43 * Add rootkey persistence and lifecycle state management by @gmaclennan in #36 * chore: move example app into apps directory by @achou11 in #18 * refactor: per-component lifecycle state with derived ComapeoState by @gmaclennan in #47 * android: fold waitForFile into connect retry loop by @gmaclennan in #52 * chore: add e2e testing app by @achou11 in #49 * fix(android): drop setUnlockedDeviceRequired from rootkey wrapper key by @gmaclennan in #57 * fix(backend): cache stopping/error frames for late joiners by @gmaclennan in #58 * fix(ios-tests): wait for STOPPING before signalling node exit by @gmaclennan in #59 * fix(android): drain JNI stdio pumps before returning from node::Start by @gmaclennan in #60 * Sentry integration: Phase 1 + Phase 2a + Phase 2b by @gmaclennan in #54 * feat(backend): polywasm-backed undici on iOS, re-enable maps plugin by @gmaclennan in #62 * ci: drop unreliable Android emulator snapshot caching by @gmaclennan in #64 * feat(sentry): land Phase 3 — backend loader + RPC tracing by @gmaclennan in #63 * fix(ios-tests): serialise STOPPING/STOPPED observers in testFullLifecycleStateTransitions by @gmaclennan in #71 * use npm list instead of custom traversal to get native module versions by @achou11 in #70 * feat(sentry): land Phases 6 + 7a — Android exit reasons & iOS MetricKit app-exit telemetry by @gmaclennan in #72 * fix(sentry): make exit telemetry lossless and stop cross-process clobbering by @gmaclennan in #84 * chore(e2e): add e2e tests on browserstack via Maestro by @achou11 in #56 * feat(sentry): migrate to @sentry/react-native v8; exit telemetry as Application Metrics by @gmaclennan in #73 * Map server integration by @gmaclennan in #86 * chore(deps): upgrade to Expo SDK 56 (React Native 0.85) by @gmaclennan in #87 * chore(ci): add release workflow by @gmaclennan in #90 * chore: fix npm script and release build script by @gmaclennan in #91 * chore(pack): don't try to package build files by @gmaclennan in #92 * fix: start fastify listening by @gmaclennan in #93 * perf(backend): switch bundler from rollup to rolldown by @gmaclennan in #94 * fix(ci): ignore-scripts in ios npm installs by @gmaclennan in #96 * fix(ci): replace --ignore-scripts with npm strict-allow-scripts allowlist by @gmaclennan in #106 * feat(config): let the consuming app supply the default project config by @gmaclennan in #95 * chore(release): merge prerelease branch. by @gmaclennan in #110 ## New Contributors * @achou11 made their first contribution in #12 **Full Changelog**: https://github.com/digidem/comapeo-core-react-native/commits/v1.0.0-pre.2 <!-- <release-meta>{"id":342868678,"version":"v1.0.0-pre.2","npmTag":"pre","opticUrl":"https://optic-zf3votdk5a-ew.a.run.app/api/generate/"}</release-meta> -->
gmaclennan
added a commit
that referenced
this pull request
Jun 22, 2026
## Optic Release Automation This **draft** PR is opened by Github action [optic-release-automation-action](https://github.com/nearform-actions/optic-release-automation-action). A new **draft** GitHub release [v1.0.0-pre.2](https://github.com/digidem/comapeo-core-react-native/releases/tag/untagged-352a6c41c12fd02dec37) has been created. Release author: @gmaclennan #### If you want to go ahead with the release, please merge this PR. When you merge: - The GitHub release will be published - The npm package with tag pre will be published according to the publishing rules you have configured - No major or minor tags will be updated as configured #### If you close the PR - The new draft release will be deleted and nothing will change <!-- Release notes generated using configuration in .github/release.yml at 7fe80b4 --> ## What's Changed ### 🚀 Features * Integrate @comapeo/core via IPC over Unix sockets by @gmaclennan in #5 * Add iOS support & test infrastructure by @gmaclennan in #6 * iOS Phase 1: unified JS bundle + smoke test (simulator-only) by @gmaclennan in #15 * iOS Phase 2: xcframework Embed & Sign for native addons by @gmaclennan in #16 * Phase 2 Android: jniLibs packaging + unified rollup loader plugin by @gmaclennan in #17 * android: read abiFilters from reactNativeArchitectures (#30) by @gmaclennan in #35 * Add rootkey persistence and lifecycle state management by @gmaclennan in #36 * Sentry integration: Phase 1 + Phase 2a + Phase 2b by @gmaclennan in #54 * feat(backend): polywasm-backed undici on iOS, re-enable maps plugin by @gmaclennan in #62 * feat(sentry): land Phase 3 — backend loader + RPC tracing by @gmaclennan in #63 * feat(sentry): land Phases 6 + 7a — Android exit reasons & iOS MetricKit app-exit telemetry by @gmaclennan in #72 * feat(sentry): migrate to @sentry/react-native v8; exit telemetry as Application Metrics by @gmaclennan in #73 * Map server integration by @gmaclennan in #86 * feat(config): let the consuming app supply the default project config by @gmaclennan in #95 ### 🐛 Bug Fixes * fix(android): drop setUnlockedDeviceRequired from rootkey wrapper key by @gmaclennan in #57 * fix(backend): cache stopping/error frames for late joiners by @gmaclennan in #58 * fix(ios-tests): wait for STOPPING before signalling node exit by @gmaclennan in #59 * fix(android): drain JNI stdio pumps before returning from node::Start by @gmaclennan in #60 * fix(ios-tests): serialise STOPPING/STOPPED observers in testFullLifecycleStateTransitions by @gmaclennan in #71 * fix(sentry): make exit telemetry lossless and stop cross-process clobbering by @gmaclennan in #84 * fix: start fastify listening by @gmaclennan in #93 * fix(ci): ignore-scripts in ios npm installs by @gmaclennan in #96 * fix(ci): replace --ignore-scripts with npm strict-allow-scripts allowlist by @gmaclennan in #106 * fix(release): stop `npm pack --dry-run` leaking dry-run into backend install by @gmaclennan in #129 ### ⚡ Performance * perf(backend): switch bundler from rollup to rolldown by @gmaclennan in #94 ### ⬆️ Dependencies * update some native deps used in backend by @achou11 in #14 * chore(deps): upgrade to Expo SDK 56 (React Native 0.85) by @gmaclennan in #87 ### 🏗️ Maintenance * Android Testing Infrastructure & Bug Fixes by @gmaclennan in #3 * chore: prebuild example/android; harden instrumented tests by @gmaclennan in #10 * chore: adjust repo setup by @achou11 in #12 * chore: minor fixes based on expo-doctor by @achou11 in #13 * chore: add architecture docs & plans by @gmaclennan in #11 * chore: post-Phase-2 cleanup — comments, plan docs, agents.md by @gmaclennan in #33 * refactor: simplify build-backend.ts; rollup writes directly to native asset trees by @gmaclennan in #34 * chore: fix eslint configuration by @achou11 in #41 * android: audit 16 KB page alignment on every shipped .so by @gmaclennan in #43 * chore: move example app into apps directory by @achou11 in #18 * refactor: per-component lifecycle state with derived ComapeoState by @gmaclennan in #47 * android: fold waitForFile into connect retry loop by @gmaclennan in #52 * chore: add e2e testing app by @achou11 in #49 * ci: drop unreliable Android emulator snapshot caching by @gmaclennan in #64 * use npm list instead of custom traversal to get native module versions by @achou11 in #70 * chore(e2e): add e2e tests on browserstack via Maestro by @achou11 in #56 * chore(ci): add release workflow by @gmaclennan in #90 * chore: fix npm script and release build script by @gmaclennan in #91 * chore(pack): don't try to package build files by @gmaclennan in #92 * chore(release): merge prerelease branch. by @gmaclennan in #110 * ci(e2e): retry BrowserStack builds on infra-class flakes by @gmaclennan in #113 ### Other Changes * ci: derive changelog labels from PR titles + add Dependabot by @gmaclennan in #114 ## New Contributors * @achou11 made their first contribution in #12 * @optic-release-automation[bot] made their first contribution in #112 **Full Changelog**: https://github.com/digidem/comapeo-core-react-native/commits/v1.0.0-pre.2 <!-- <release-meta>{"id":342970724,"version":"v1.0.0-pre.2","npmTag":"pre","opticUrl":"https://optic-zf3votdk5a-ew.a.run.app/api/generate/"}</release-meta> -->
gmaclennan
added a commit
that referenced
this pull request
Jun 23, 2026
…ripts/ The top-level e2e/ folder ran integration tests (against apps/integration), while the real end-to-end suite lives in maestro/ + apps/e2e — a confusing inversion. The e2e/.maestro flows + run-e2e.sh were the original local Maestro POC (untouched since #3), never wired into CI, and fully redundant: the lifecycle/IPC behaviour they exercised is gated by the instrumented tests (ServiceLifecycleTest, NodeJSIPCTest), and the full real-device path is gated by the apps/e2e BrowserStack suite. Their only unique coverage was a throughput number on a throwaway demo UI. - Delete e2e/.maestro/ (5 flows) and e2e/run-e2e.sh. - Move e2e/run-instrumented-tests.sh → scripts/run-instrumented-tests.sh (the local Android instrumented-test runner; same depth, paths unchanged). - Drop the now-broken apps/integration `e2e:android` script and repoint `test:android` / `test:android:unit` at scripts/. - e2e/README.md is removed; local-run guidance now lives in docs/TESTING.md (§7.2 runs the e2e suite locally; §8 the cheap layers + instrumented). - Update agents.md, CONTRIBUTING.md, and the sentry plan for the new path. Result: no e2e/ ↔ maestro/ inversion, one fewer test app's scaffolding to maintain, and maestro/ is unambiguously the sole e2e flow.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This PR adds a comprehensive Android testing suite (JVM unit tests, instrumented tests, and Maestro e2e tests) along with a CI workflow, and fixes 11 bugs in the Android service layer discovered during code review.
Bug Fixes
Service lifecycle (
ComapeoCoreService,NodeJSService):isServiceStartedflag on stop/destroy so the service can be cleanly restartedrunBlockingwithserviceScope.launchin service start/stop to prevent ANR on the main threadlateinit var ipcwithCompletableDeferredto eliminateUninitializedPropertyAccessExceptionrace conditionsstop()when the service isn't running, instead of throwingdestroy()from thefinallyblock to prevent double-executionIPC (
NodeJSIPC):IOExceptioninstead of retrying infinitelysendChannelon disconnect/reconnectconnectionStatepropertyFile watching (
watchForFile):FileObserverbefore checking file existence to eliminate the TOCTOU race conditionwithTimeoutwrapper (default 30s) to prevent hangsOther:
ComapeoCoreReactActivityLifecycleListener: use safe-call on nullable activityComapeoCoreModule: return actual IPC connection state fromgetState()instead of a hardcoded valuecontrol-rpc.js: declare missingcountvariableNew Tests
JVM unit tests (run without a device):
MessageFramingTest— 203 lines covering the IPC framing protocolCompletableDeferredStopTest— tests deferred stop/restart patternsWatchForFileTimeoutTest— tests file-watch timeout behaviourInstrumented tests (run on emulator/device):
NodeJSIPCTest— 9 tests for IPC socket communicationWatchForFileTest— 7 tests for the file-watching utilityServiceLifecycleTest— service start, separate-process check, stopShutdownPathTest— stop during startup, stop then restartMaestro e2e tests (full app flows):
app-launch.yaml,node-process-starts.yaml,ipc-roundtrip.yaml,send-multiple-rounds.yaml,state-transitions.yamlCI Workflow (
.github/workflows/android-tests.yml)main(fast, no emulator needed)nodejs-mobilebinaries keyed by version to speed up runsnodejs-projectnode_modules via a Gradle task before asset merging, so the Node.js backend has its dependencies available during testsOther Changes
scripts/download-nodejs-mobile.sh— helper script to download pre-built Node.js Mobile binariesexample/android/checked in (removed from.gitignore) so instrumented test files are tracked without requiringexpo prebuildexample/android/gradle/wrapper/gradle-wrapper.jarchecked in (was excluded by a global gitignore rule)agents.mdadded, documenting the codebase architecture and CoMapeo ecosystem context