Skip to content

XERK-281: silence the in-app updater in Android unit tests - #517

Merged
xerhab merged 1 commit into
mainfrom
XERK-281
Aug 26, 2026
Merged

XERK-281: silence the in-app updater in Android unit tests#517
xerhab merged 1 commit into
mainfrom
XERK-281

Conversation

@xerhab

@xerhab xerhab commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

What & why

TurmaApplication.onCreate fires a fire-and-forget Updater.check() (the XERK-11 in-app updater), which fetches https://api.github.com/repos/…/releases. Robolectric rebuilds the Application per test METHOD, so the updater's ~15-minute throttle never applies and every test hits live api.github.com — measured 45 anonymous CONNECTs per :app:testDebugUnitTest run, from shared-IP CI runners, against GitHub's 60 req/hour/IP unauthenticated limit. It also showed up as an "Update available" banner in a Robolectric screen test. Filed by the QA pass on XERK-262 (PR #439); Low because the suite never depended on that egress, but unsolicited third-party traffic worth removing.

The fix — Option A (the ticket's preferred, smallest seam)

  • net/Updater.ktcheck() no-ops at its very top, before the throttle/force branch, when the turma.updater.disabled system property is "true". New const DISABLE_PROPERTY. Nothing in production sets it, so production behavior is unchanged (a real device's start-of-app check is untouched).
  • app/build.gradle.ktstestOptions.unitTests.all { it.systemProperty("turma.updater.disabled", "true") } sets it for the whole test JVM.
  • net/UpdaterTestSeamTest.kt — regression test pinning both halves: the property reaches the test JVM, and check(force=true) short-circuits under it. The no-op is asserted on lastCheckAt (0 iff check() never proceeded past its guards), not on state — a check that ran and whose fetch failed (offline/blackholed CI) also lands on Hidden, so a state assertion would be a tautology that passes with the guard removed. lastCheckAt is exposed @VisibleForTesting for this.
  • .claude/rules/android.md — harness rule added so the egress is not reintroduced.

No user-facing surface changes → no android/PARITY.md or web-side change needed.

Verification (adversarial QA + delta, both ran the build)

  • gradle :app:testDebugUnitTestgreen, 450 tests / 0 failures; UpdaterTestSeamTest 2/2.
  • Ticket's own repro (CONNECT-logging blackhole proxy via JAVA_TOOL_OPTIONS): api.github.com CONNECTs 45 → 0 with the fix.
  • Mutation checks prove the test guards the seam, not just the wiring:
    • remove the it.systemProperty(...) gradle line → disablePropertyIsSetForTheTestJvm fails and 45 CONNECTs return;
    • remove the production guard in check()checkNoOpsUnderTheDisableProperty fails on lastCheckAt (expected:<0> but was:<…>). (First QA pass caught that an earlier state-only assertion did not catch this; fixed and re-verified PASS.)
  • Not verified: real device/emulator app-start egress (no emulator gate in this repo, per android.md) and the live fetchLatest I/O path (deliberately blackholed — the suite must not depend on egress). Both unchanged by this diff.

TurmaApplication.onCreate fires a fire-and-forget Updater.check(), and
Robolectric rebuilds the Application per test METHOD, so the updater's
~15-min throttle never applies — every test hit live api.github.com
(measured 45 anonymous CONNECTs per suite, from shared-IP CI runners,
against GitHub's 60 req/hour/IP unauthenticated limit).

Give check() a quiet-in-tests seam (Option A from the ticket, the
smallest one, mirroring the recordCoercion seam XERK-262 added hub-side):
check() no-ops at its very top — before the throttle/force branch — when
the `turma.updater.disabled` system property is "true", and
app/build.gradle.kts's testOptions sets it JVM-wide for the test task.
Nothing in production sets it, so production behavior is unchanged.

Regression test (net/UpdaterTestSeamTest) pins both halves: the property
reaches the test JVM, and check() short-circuits under it. The no-op is
asserted on lastCheckAt (0 iff check() never proceeded), not on state —
a check that RAN and whose fetch failed also lands on Hidden, so state
alone would pass with the guard removed. lastCheckAt is exposed
@VisibleForTesting for this.

Harness rule added to .claude/rules/android.md so it is not reintroduced.

Verified: gradle :app:testDebugUnitTest green (450 tests); the ticket's
CONNECT-proxy repro shows api.github.com calls drop 45 -> 0; and removing
either the gradle line or the production guard makes the suite fail.
@xerhab
xerhab merged commit 484bcd0 into main Aug 26, 2026
5 checks passed
@xerhab
xerhab deleted the XERK-281 branch August 26, 2026 12:45
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