Re-read the account when somebody would notice, and explain the device once - #153
Merged
Merged
Conversation
parawanderer
changed the base branch from
fix/a-dead-icloud-session-says-so
to
main
August 23, 2026 18:58
An iPad picks up a renamed AirTag in seconds. This app read the account every six hours, so a tag renamed on the owner's own iPad kept its old name here for the rest of the afternoon - which reads as broken rather than patient. Four ways it updates now, and the difference between them is who asked: - Settings, which opens the whole iCloud flow and reads everything - **My Devices, from the overflow menu** - new, and the one that fits "did the name I just changed arrive". It replaces the link item once linking is done, since only one of the two is ever what somebody wants - **on resume**, with a one-minute floor. The moment a stale name is noticed is the moment the app is opened, and resuming already refetched where the tags were while never asking what they were called - every fifteen minutes while the screen stays open, down from six hours Fifteen is not reckless: the location fetch beside it runs every *minute*, so four account reads an hour is not the expensive thing here. The floor on resume exists because resuming happens on every task switch, and a Python call for flicking between two apps is waste. **A refused sign-in is handled by who asked, per rule 14.** The menu item sends the user to sign in again - they are watching, and a tap that does nothing is indistinguishable from a broken button. Resume and the timer only log: deleting somebody's session out from under them, from a job nobody requested, the instant they open the app is its own bug. AccountReadPolicyTest moves to the JVM, where arithmetic and three booleans always belonged. That move is also what exposed the gap: adding a parameter to the constructor broke nothing in the JVM suite, because the only test of this class lived somewhere the JVM suite does not look. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The page describing what this app put on somebody's Apple account was shown every time the iCloud flow ran. It describes an event - a row appearing in their device list, titled after a Mac they do not own, beside a button offering to remove things they do not recognise, where removing it breaks their session. Worth a screen on the run that caused it. Re-reading a linked account causes none of that. The same page then explains something that did not happen, to somebody who read it the first time - and a page that appears when nothing has happened is one people learn to tap past, including on the run where it matters. So the results step ends in Done rather than Next when the app was already a member when the screen opened. The flag is set where that branch is taken, because from there on the flow is identical to a first connection and afterwards there is no way to tell which one this was. Its javadoc claimed "shown on every connect" and is corrected rather than left: a fresh install does need telling again, since each one adds another row with the same title and model, and that is a connect. A read is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AccountRefresher swallowed the refusal into an empty list, so the manual refresh could never act on it: its onError never fired and a read that failed looked exactly like a successful read of nothing. The "who asked" decision was sitting inside the one component that cannot know the answer. So it reports and the caller decides. The menu item somebody pressed sends them to sign in again; the periodic and on-resume reads log and touch nothing. Same failure, different callers, which is rule 14. The test that found it is the one asserting the menu item routes - it failed for the right reason, and only because it drove the real path rather than the refresher directly. Also drops a Toast matcher that hung this class. `inRoot(withDecorView(not(...)))` sends Espresso's root picker hunting for a window that is not up yet, retrying internally for seconds per attempt inside a retry loop - the same shape that took UnlinkTheAccountSettingTest to 6m33s. The run had to be killed after ten minutes at three tests of seven. A call count on the fake answers instantly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parawanderer
force-pushed
the
feat/reread-the-account-when-it-matters
branch
from
August 23, 2026 19:02
e488122 to
36be09b
Compare
parawanderer
temporarily deployed
to
Android Build
August 23, 2026 19:02 — with
GitHub Actions
Inactive
parawanderer
temporarily deployed
to
Android Build
August 23, 2026 19:02 — with
GitHub Actions
Inactive
ubrt
pushed a commit
to ubrt/OpenTagViewer
that referenced
this pull request
Aug 24, 2026
Every workflow filters on `pull_request: branches: ["main"]`, and that matches the base of the PR rather than the branch the work is on. A PR opened against another PR's branch therefore runs nothing. Worth writing down because of how it presents: the checks section is absent, not red and not pending, which reads as "nothing to run here" rather than "nothing ran". parawanderer#153 sat like that and looked fine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Six hours to notice a rename, when an iPad takes seconds
Renaming an AirTag on your own iPad shows up there almost immediately. This app re-read the
account every six hours, so the old name sat on screen for the rest of the afternoon — which
reads as the app being broken rather than patient.
Four ways it updates now, and the difference between them is who asked:
Fifteen minutes is not reckless: the location fetch beside it runs every minute, so four
account reads an hour is not the expensive thing here. The floor on resume exists because
resuming happens on every task switch, and a Python call for flicking between two apps is waste.
The menu item replaces "Link an account" once linking is done — only one of the two is ever what
somebody wants, and offering both reads as three ways in rather than two.
The device-registration page appeared every time
The page explaining what this app put on your Apple account — a row titled after a Mac you do not
own, next to a button offering to remove things you do not recognise, where removing it breaks
your session — was shown on every run of the iCloud flow.
It describes an event. Re-reading a linked account causes none of it, so the page was explaining
something that had not happened, to somebody who read it the first time. A page that turns up
when nothing has happened is one people learn to tap past, including on the run where it matters.
It now shows only when the connection actually registered a device. A fresh install still gets it,
because each one really does add another row.
A design flaw the tests found
AccountRefresherswallowed a refused sign-in into an empty list — so the manual refresh'sonErrornever fired, and a read that failed looked identical to a successful read of nothing.The "who asked" decision was sitting inside the one component that cannot know the answer.
It now reports, and each caller decides. Found only because the test drove the menu item rather
than the refresher directly.
Testing
AccountReadPolicyTestmoves to the JVM, where arithmetic and three booleans always belonged(rule 13). That move is also what exposed the gap: adding a parameter to the constructor broke
nothing in the JVM suite, because the only test of the class lived somewhere the JVM suite does
not look.
New:
RefreshingFromTheAccountTest(the menu item, including that an outage must not cost asign-in) and
TheDeviceNoteIsShownOnceTest(shown on a first connection, absent on a re-read, andthe button reads Done rather than Next).
One test hung the suite before it worked: a Toast matched with
inRoot(withDecorView(not(...)))sends Espresso's root picker hunting for a window that is not up yet, retrying for seconds per
attempt inside a retry loop. Killed after ten minutes at three tests of seven. A call count on the
fake answers instantly.
Also
AGENTS.md rule 14 — every call reaching the account has to answer "is this session dead?",
through the shared decision rather than its own, with the who-asked line written down.
Stacked on #151
Branched from
fix/a-dead-icloud-session-says-so, because a manual refresh that meets a deadsession needs the classification that PR adds. Merge #151 first.
🤖 Generated with Claude Code