Skip to content

test(android): instrumented suite on a managed device in CI; fix framed error envelopes read as success - #888

Merged
cryptskii merged 3 commits into
mainfrom
test/android-instrumented-managed-device
Sep 14, 2026
Merged

cryptskii merged 3 commits into
mainfrom
test/android-instrumented-managed-device

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

What

Owner ruling 2026-09-14 (HY2): fix the instrumented Android tests and give them a CI job, rather than delete them. The seven files under app/src/androidTest had not compiled for a long time, and no workflow ran them. This PR makes them compile, runs the device-local suite on a Gradle managed device in CI, and fixes the defects that running it exposed. One of those defects is in production code.

A production bug the suite found

Every rejected envelope read as accepted on device. processEnvelopeV3 returns the ingress response with its leading 0x03 frame byte, and the bridge's own error builders frame theirs the same way. isErrorEnvelope decoded those bytes directly. The canonical decoder refuses the frame byte, so the detector reported every response as "not an error". The vector suite's case_0001_proof_cap_over expects PROOF_TOO_LARGE and read ACCEPT.

Fix: dsm_sdk::envelope::transport::error_code_of_transport_bytes strips the frame byte when present, as the request path already does. The JNI export delegates to it. The function lives in the host-compilable module, so a host test covers it:

  • an_error_envelope_is_detected_framed_and_bare: framed and bare error envelopes both report their code; an empty buffer and a lone frame byte report none.
  • Mutation control: removing the frame-byte strip turns the test red ("framed error"). Restored, and the file is byte-identical to the pre-mutant copy.
  • On the managed device, VectorTests.vectorsV1 now passes.

A deployment finding (reported, not changed)

The committed dsm_client/frontend/public/dsm_env_config.toml fails the SDK's strict loader: TOML parse error at line 30, column 1. NodeConfig requires register_incarnation, and the committed file's [[nodes]] entries predate it. It still lists the retired three-node fleet. Any APK built from a clean checkout ships this file and fails SDK init; builds from the owner's working copy parse. The file is an owner-protected environment config, so this PR does not touch it. The instrumented suite no longer depends on it.

Test repairs

  • SoFi harnesses (SoFiTestHelpers, SoFiTradeRealHwTest, SoFiCrossDeviceOwnerTest, SoFiCrossDeviceTraderTest): the u128 reserve fields are reserved in the proto. The tests now fund an AMM vault through DlvInstantiateV1.funding_legs (two legs, lex order, non-zero), read the uint64 reserve_a/reserve_b, and assert the hop's 32-byte parent_binding in place of the removed reserves digest. These need two paired phones and the live fleet, so they carry a new @RealHardware annotation.
  • AndroidLayerProofTest:
    • ensureGenesis never called initSdk, the step that installs the app router, so every routed call answered "app router not installed". It now calls it, and installs a test-only androidTest/assets/dsm_env_config.instrumented.toml (loopback node, allow_localhost = true) from the instrumentation context instead of the app's bundled config.
    • The test called four bridge methods the Kotlin bridge does not dispatch: hasIdentityDirect, getPersistedGenesisHash, getBluetoothStatus, getPersistedDeviceId. Where a current method exists, the tests use it: getGenesisHashBin, and getDeviceIdBin with a 32-byte assertion, because an empty success is not an identity. Where the capability is gone, the tests are deleted: t20 and t21 (identity presence), t33 (Bluetooth status), t41 (the device-id alias).
    • t40 claims the faucet from the live fleet, so it is @RealHardware.
  • BleEventRelayPersistenceTest: the flush tests never marked the bridge ready, so flushPersisted correctly left every event in place and the tests asserted zero. They now mark it ready. A new testResetBridgeReady hook (@VisibleForTesting) keeps tests independent, and a new test proves the not-ready path keeps events.

CI

New job Android Instrumented Tests (managed device) on the android layer:

  • KVM enabled, JDK 17.
  • The Rust toolchain with all three Android targets, and make android-libs. refreshDsmJniLibs refuses to package unless every abiFilters ABI has a library, so an x86_64-only build would fail before any test ran.
  • ./gradlew :app:pixel6Api34DebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.notAnnotation=com.dsm.wallet.RealHardware.
  • Reports uploaded as an artifact.
  • The managed device is pixel6Api34, a Pixel 6 on API 34 with the aosp-atd image, in testOptions.managedDevices.

Verification

  • ./gradlew :app:compileDebugAndroidTestKotlin: clean.
  • Managed device, locally, with the app's web assets moved aside to match the CI job (which builds no frontend): 45 of 45 pass (BUILD SUCCESSFUL). That is 50 tests, minus the four deleted, minus t40, now @RealHardware; the three SoFi harness classes are excluded the same way. Failures per run as each fix landed: 30 (config asset missing, relay never marked ready, frame byte); 28 (app router never installed); 27 (the bundled config fails the strict loader); 10 (four methods absent from the bridge); 0.
  • Host: cargo test -p dsm_sdk --release --lib envelope::transport::tests::an_error_envelope_is_detected_framed_and_bare green; mutation red; restored green; cargo fmt --check clean; cargo clippy --locked -p dsm_sdk --all-targets -- -D warnings: zero diagnostics; ./scripts/check-spdx.sh: OK.
  • The new CI job's first run is on this PR.

Not in this PR

The committed environment config above; that is the owner's file. The four-phone SoFi harnesses still need a hands-on run.

…in CI, and caught a framed error envelope read as success

The seven androidTest files had not compiled for a long time and no workflow
ran them. Running them on a Gradle managed device found one production bug.

Production: isErrorEnvelope decoded the bytes as they cross JNI, but every
envelope the bridge hands Kotlin carries the 0x03 frame byte, which the
canonical decoder refuses; every rejection therefore read as "not an error"
(vector case_0001_proof_cap_over read ACCEPT instead of PROOF_TOO_LARGE).
dsm_sdk::envelope::transport::error_code_of_transport_bytes strips the
frame byte when present; the JNI export delegates to it. Host test
an_error_envelope_is_detected_framed_and_bare; mutation control (no strip)
red, restored.

Tests: SoFi harnesses fund AMM vaults through funding_legs, read the u64
reserves and assert the hop's parent_binding, and carry @RealHardware
(two phones + live fleet). AndroidLayerProofTest calls initSdk (router
install), installs a test-only env config from the instrumentation assets,
retargets calls to methods the bridge dispatches (getDeviceIdBin with a
32-byte assertion, getGenesisHashBin), deletes tests for removed
capabilities (identity presence, Bluetooth status, device-id alias), and
marks the faucet test @RealHardware. BleEventRelayPersistenceTest marks the
bridge ready before flushing, adds a not-ready test and a reset hook.

CI: android-instrumented-tests builds every packaged ABI (make
android-libs; refreshDsmJniLibs requires all three) and runs
pixel6Api34DebugAndroidTest excluding @RealHardware. Locally, with the app's
web assets moved aside as in CI: 45 of 45 pass.

Not changed: the committed dsm_env_config.toml fails the SDK's strict
loader (nodes lack register_incarnation); it is an owner-protected file.
… in the shell

The managed-device job set ANDROID_NDK_HOME from ${{ env.ANDROID_NDK_LATEST_HOME }}.
An env-context expression reads only workflow-defined variables, not the runner
image's, so it evaluated to an empty string and cargo-ndk refused to detect an
NDK. The job now accepts SDK licenses first, installs the ndkVersion that
app/build.gradle.kts pins (the same NDK its CMake build uses), and exports that
path through GITHUB_ENV before make android-libs.
With the NDK resolved, make android-libs reached the dsm build script, which
compiles the protocol with prost and failed with "Could not find protoc". The
job now installs protobuf-compiler the same way every other Rust job does.
@cryptskii
cryptskii merged commit f6fb913 into main Sep 14, 2026
20 of 21 checks passed
@cryptskii
cryptskii deleted the test/android-instrumented-managed-device branch September 14, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant