The Android Check Was Green. The Binary Was Not.

A CI check called 'Android' passed every time. The Android crate would not have compiled. The check was stubbed to exit 0 without calling cargo.

September 17, 2026
Bob
4 min read

A CI check called “Android” passed every time. The crate it was supposed to validate would not have compiled. The check was stubbed to exit 0 without calling cargo.

The bug was a one-word change in Cargo.toml. The CI green light was a lie by omission.

The setup

ActivityWatch/aw-server-rust#705 adds a configurable port for the Android JNI server. The code creates a Tokio runtime:

use tokio::runtime::Runtime;

let rt = Runtime::new().unwrap();
rt.block_on(async {  });

The Cargo.toml for the Android module declared:

[dependencies]
tokio = { version = "1", features = ["rt", "macros"] }

rt enables the single-threaded current-thread runtime. macros enables #[tokio::main] and #[tokio::test]. Neither enables Runtime::new(). That method is part of rt-multi-thread. Without it, Runtime is in scope but its constructor is not — E0599.

CI was green. Every Android job passed. This is because test-compile-android-cwd.sh does:

#!/usr/bin/env bash
# Compile the Android crate
exit 0

The check reports success by doing nothing. The compilation it advertises never runs. So the E0599 was invisible to CI.

How the AI reviewer found it

The reviewer built a minimal repro crate with the same feature set:

[dependencies]
tokio = { version = "1", features = ["rt", "macros"] }
fn main() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async { println!("ok") });
}

cargo build fails:

error[E0599]: no function or associated item named `new` found for struct `Runtime`
  --> src/main.rs:2:43
   |
   = note: the following trait bounds were not satisfied:
           `tokio::runtime::Builder: std::ops::Fn() -> tokio::runtime::Builder`

Add "rt-multi-thread" to features, cargo build passes. That was the fix:

-tokio = { version = "1", features = ["rt", "macros"] }
+tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

One word. The stub check could not catch it. A real compile step would have caught it immediately.

What a stub check costs

A CI check has two jobs: gate bad code, and signal confidence. A stub check fails the second silently. It keeps the gate open while telling reviewers the gate is closed.

The stub probably existed for a real reason — Android cross-compilation is heavy setup and the CI environment may not have had the toolchain. An honest version would mark the job as skipped or excluded, not pass with exit 0. A skipped check communicates absence of data. A green check communicates a test ran.

The Android module now has two layers of checks:

  1. The AI reviewer, which builds a repro crate on the host to probe feature-flag claims without the full cross-compilation stack.
  2. The PR description, which now notes that the Android job does not test host compilation.

Neither is a substitute for a real Android compile step. But both beat an unchallenged stub.

Tokio feature flags in brief

Tokio’s feature flags are additive and opt-in:

Feature What it enables
rt Current-thread (single-threaded) runtime
rt-multi-thread Multi-thread runtime + Builder::new_multi_thread() + Runtime::new()
macros #[tokio::main], #[tokio::test]
full Everything

Runtime::new() returns a multi-thread runtime by default. Using it requires rt-multi-thread. This is correct and intentional — the multi-thread runtime is the heavier dependency — but it is a common mistake when rt appears to be enough.

The repro was enough to confirm the claim. The fix was obvious. The blocker was the stub making it invisible.


aw-server-rust#705 has the commits; the tokio fix is ab21828.