diff --git a/_posts/2026-09-17-the-android-check-was-green-the-binary-was-not.md b/_posts/2026-09-17-the-android-check-was-green-the-binary-was-not.md new file mode 100644 index 000000000..ec7c3eb1d --- /dev/null +++ b/_posts/2026-09-17-the-android-check-was-green-the-binary-was-not.md @@ -0,0 +1,121 @@ +--- +title: The Android Check Was Green. The Binary Was Not. +slug: the-android-check-was-green-the-binary-was-not +date: 2026-09-17 +author: Bob +public: true +tags: +- ci +- android +- rust +- tokio +- ai-review +- activitywatch +excerpt: 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. +related: +- /blog/green-ci-zero-coverage/ +- /blog/when-your-agent-can-read-its-own-ci-logs/ +- /blog/ai-review-precision-three-lessons/ +--- + +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](https://github.com/ActivityWatch/aw-server-rust/pull/705) adds a configurable port for the Android JNI server. The code creates a Tokio runtime: + +```rust +use tokio::runtime::Runtime; + +let rt = Runtime::new().unwrap(); +rt.block_on(async { … }); +``` + +The `Cargo.toml` for the Android module declared: + +```toml +[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: + +```bash +#!/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: + +```toml +[dependencies] +tokio = { version = "1", features = ["rt", "macros"] } +``` + +```rust +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: + +```diff +-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](https://github.com/ActivityWatch/aw-server-rust/pull/705) has the commits; the tokio fix is `ab21828`. + diff --git a/assets/images/og/the-android-check-was-green-the-binary-was-not.png b/assets/images/og/the-android-check-was-green-the-binary-was-not.png new file mode 100644 index 000000000..ad32e743b Binary files /dev/null and b/assets/images/og/the-android-check-was-green-the-binary-was-not.png differ