fix(android): make the sync chip's state decision resolvable without an Application#859
Open
vishk23 wants to merge 1 commit into
Open
fix(android): make the sync chip's state decision resolvable without an Application#859vishk23 wants to merge 1 commit into
vishk23 wants to merge 1 commit into
Conversation
…an Application
`SyncChipStateTest.lastSyncedAt_takesPriorityOverHistorySyncExperimental` fails on
main — the only failure in the suite (3060 tests, 1 failure):
java.lang.IllegalStateException: NoopApplication is not attached
at com.noop.NoopApplication$Companion.localizedString(NoopApplication.kt:134)
at com.noop.ui.UiStringsKt.uiString(UiStrings.kt:16)
at com.noop.ui.TodayScoringKt.shortSyncAgo(TodayScoring.kt:412)
at com.noop.ui.SyncChipState$Companion.resolve(TodayScoring.kt:399)
`SyncChipState.resolve` is documented "Pure + unit-tested" but reached for two
ambient dependencies: the process-wide Application's resources (for the translated
"now" word) and the system clock. These are plain JVM tests with no Robolectric, so
no Application is ever attached and the resource lookup throws.
Only the `< 60s` branch of `shortSyncAgo` needs a word — every other bucket is
digits plus a unit letter — which is why five of the six cases passed and the sixth,
the one with a 5-second-old sync, did not. Same reason `lastSyncedAt_isSyncedWith-
AgeText` survives at 65 seconds.
Hoist both dependencies to parameters. `SyncStatusChip` — a composable that already
depends on the clock and the string catalog — resolves them and hands them down, so
the decision itself becomes a genuinely pure function. Same injected-clock style as
`recordingStateFor` directly above it, which already takes its own `nowSec`.
No behaviour change: the clock is still read at composition time, exactly as
`shortSyncAgo` did on every recomposition, and the chip renders identical text.
Tests are now deterministic (fixed `nowSec` rather than `System.currentTimeMillis()`)
and gain a case for the sub-minute branch, which was previously impossible to reach
from a unit test — it is the regression guard against the lookup moving back inside.
The Swift twin is deliberately unchanged. Its `shortAgo` resolves its own clock and
`String(localized:)` and is fine doing so: XCTest runs against a real bundle, and
`SyncChipStateTests` passes. The two signatures now differ on purpose; the priority
order they encode does not. Noted in the KDoc so it does not get "fixed" back.
Verified locally: 3061 tests, 0 failures (was 3060 / 1), 381 test classes both
before and after. i18n-coverage and source-hygiene gates both green. android.yml is
disabled by default, which is why CI never caught this.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to the note I left in review on #738 — kept separate so an unrelated UI test fix doesn't ride along with a sleep-staging change.
The failure
SyncChipStateTest.lastSyncedAt_takesPriorityOverHistorySyncExperimentalfails onmain(verified at cca23d6 on a clean checkout). It is the only failure in the suite — 3060 tests, 1 failure.Repro:
Root cause
SyncChipState.resolveis documentedPure + unit-tested, but it reached for two ambient dependencies: the process-wide Application's resources (viashortSyncAgo→uiString→NoopApplication.localizedString) and the system clock. These are plain JVM tests with no Robolectric, so no Application is ever attached and the resource lookup throws.What made it look arbitrary: only the
< 60sbranch ofshortSyncAgoneeds a translated word — every other bucket is digits plus a unit letter. So five of the six cases passed, and the one with a 5-second-old sync did not. It is also whylastSyncedAt_isSyncedWithAgeTextsurvives: it uses 65 seconds and lands in theNmbranch.The fix
Hoist both dependencies to parameters.
SyncStatusChip— a composable that already depends on the clock and the string catalog — resolves them and passes them down, so the decision itself becomes genuinely pure. This is the same injected-clock shape asrecordingStateFordirectly above it, which already takes its ownnowSec.No behaviour change. The clock is still read at composition time, exactly as
shortSyncAgodid on every recomposition, and the chip renders identical text in every bucket.The tests become deterministic (a fixed
nowSecinstead ofSystem.currentTimeMillis()) and gain one case for the sub-minute branch — previously impossible to reach from a unit test, and now the regression guard against the lookup migrating back inside.On the parity contract
The Swift twin is deliberately unchanged, calling that out per CLAUDE.md.
SyncChipState.shortAgoresolves its ownDate()andString(localized:)and is fine doing so — XCTest runs against a real bundle, andSyncChipStateTestspasses. The two signatures now differ on purpose; the four cases and the priority order they encode do not. There is a KDoc note saying so, so it doesn't get "restored" later.Verification
Measured on this machine by running the full suite against a pristine
upstream/maintree, then against the patch. The+1is the new sub-minute case; no test was dropped.i18n-coverageandsource-hygiene(the two workflows that actually run) are both green locally:The
l10n_today_screen_sync_chip_now_c9bc849acatalog entry is still referenced, just fromTodayScreen.ktnow.Why CI didn't catch it
android.ymlis disabled by default, so nothing runs the Android unit tests on a PR. Worth a separate conversation about whether thetestFullDebugUnitTestjob is cheap enough to enable on its own (it is pure JVM — ~2 min here, no emulator, no SDK-heavy steps) even while the full compile job stays off.