Files
minstrel/android/app
bvandeusenandClaude Opus 5 68136c64c0
android / Build + lint + test (push) Successful in 3m55s
fix(android): decide updates on the ordering key, not the version name
The app compared NAMES while Android installs by versionCode, with nothing
keeping the two orderings consistent. So it could offer a build the platform
then refused as a downgrade, or stay silent about one it would have
accepted. The offer and the install were asking different questions.

Both consumers — the shell banner and the About card — now route through
one isUpdateAvailable(): decide on the ordering key whenever the server
reports one, since that is the same value the package installer compares,
so an offer implies an install that will actually be accepted. Name
comparison survives only as the fallback for a server predating the field.

isVersionNewer is deliberately untouched. It already degrades per segment
and is not what was broken; rewriting it while nearby would have put the
fallback path at risk for no gain.

code is nullable on the wire, and that is load-bearing rather than
stylistic. The app's Json sets coerceInputValues = true, which replaces a
JSON null with the declared default on a NON-nullable property — so
`val code: Long = 0` would have turned "this server reports no ordering
key" into "its key is 0" silently, ranking every such server as infinitely
behind and offering its build to everyone forever. Reading the field
declaration alone would never show that; it lives in AppModule.

A third caller turned up during the sweep and was deliberately left alone.
NetworkStatusController compares the /healthz minClientVersion, which is a
server-declared compatibility floor rather than the bundled APK — there is
no ordering key on that wire at all, so names remain the only thing it can
compare. Different question, correctly still using the old helper.

The update channel had no tests whatsoever before this, which is worth
stating: the thing deciding whether anyone is ever offered an update fails
silently in both directions. The new suite pins that the key wins when it
disagrees with the name, that a null key falls back rather than reading as
zero, the recorded migration constraint (a new-scheme name outranks an
old-scheme one across a day boundary but NOT within the same day), and the
degradation cases — including that an unparseable DECIDING segment reads as
zero and loses, which is why the channel must never live inside the name.

Every assertion was checked against the real comparison by mirroring it,
rather than from reading it: two of my first-draft comments described the
wrong mechanism and were corrected on the evidence.

Step 4 of 5 — Scribe task #3811, milestone #390.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH
2026-09-09 22:01:26 -04:00
..