Skip to content

feat(cli): add an opt-in service probe layer to icloud doctor - #347

Open
MrJarnould wants to merge 1 commit into
timlaing:mainfrom
MrJarnould:feat/doctor-probe
Open

MrJarnould wants to merge 1 commit into
timlaing:mainfrom
MrJarnould:feat/doctor-probe

Conversation

@MrJarnould

Copy link
Copy Markdown

Proposed change

icloud doctor reads the service map Apple returned at login. That map only shows what Apple
advertises — a host can be advertised and perfectly reachable while the endpoint behind it
has been withdrawn, which is exactly what happened in #316. So the map alone would not have
caught the bug the command exists for.

--probe closes that gap by calling each service once with a read-only request:

                             Service probes
┌─────────────┬─────────────┬────────────────────────────────────┬──────┐
│ Status      │ Service     │ Read                               │ ms   │
├─────────────┼─────────────┼────────────────────────────────────┼──────┤
│ ok          │ account     │ reads storage usage                │ 703  │
│ ok          │ devices     │ refreshes Find My without locating │ 2760 │
│ unavailable │ files       │ reads the Ubiquity root            │ 138  │
│ ok          │ reminders   │ reads the reminders sync cursor    │ 820  │
└─────────────┴─────────────┴────────────────────────────────────┴──────┘

Probe notes:
  files: Apple reports this unavailable: Account migrated

Four decisions worth reviewing

Opt-in, and read-only. Every probe is a GET or the service's own documented refresh; none
writes. It costs one request per service, roughly ten seconds in total, which is why it is
behind a flag rather than on by default.

Find My is refreshed with locate=False deliberately. Counting or iterating the manager
refreshes with locate=True, which pings the user's hardware. A diagnostic must not cause
that as a side effect. There is a test asserting the call.

Outcomes are separated by whose problem they are. A withdrawn endpoint or an unexpected
error fails the run. A service Apple reports unavailable for this account — a Ubiquity library
migrated to iCloud Drive, say — or one asking for re-authentication is shown but does not,
because neither is a pyicloud defect. A service whose webservice key was already reported
missing is marked skipped rather than called, since doing so would only restate the map more
slowly.

The probe table cannot drift. A test asserts it covers exactly the services
pyicloud/endpoints.py names, in both directions, so a service can neither go silently
unchecked nor keep a probe after the inventory drops it. I verified it by introducing each
kind of drift rather than assuming it works.

What the live runs changed

Listing reminder lists took 28 seconds against a real account. Both zone-backed services
now read their sync cursor instead — 0.3s, and it proves the same reachability. The
happy-path test asserts which read was chosen, because a MagicMock answers either and the
test was otherwise passing for no reason.

The probe layer found a real bug on its first run. invites was the only service to fail
on a healthy account, which turned out to be a zone-wide CloudKit query against the shared
database. That is #338, fixed in #339, and invites now reports ok — the layer found it and
now confirms it.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New service (thank you!)
  • New feature (which adds functionality to an existing service)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests
  • Documentation or code sample

Example of code:

icloud doctor --probe
icloud doctor --probe --format json

Additional information

Cut from main and independent of #345 and #346.

Testing. 952 tests pass on Python 3.10, 3.11, 3.12, 3.13 and 3.14, run locally. Twelve are
new.

Verified live: all eleven services answer, files reports unavailable without failing the
run, exit code 0 with --probe and the honest "Nothing was called; add --probe" wording
without it.

One thing found while rebasing this onto the merged doctor command and worth calling out,
since it is the kind of thing a clean auto-merge hides: probing is gated on the session status,
not merely on the API being resolvable. Those two can disagree, and calling eleven services
with a session that reports unauthenticated produces a page of failures that say nothing about
the library — under a verdict stating nothing could be checked. Same gating the map findings
already have, with a test that fails without it.

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

  • Documentation added/updated to README

🤖 Generated with Claude Code

The service map only shows what Apple advertises. A host can be advertised
and reachable while the endpoint behind it has been withdrawn, which is
exactly what happened in timlaing#316 -- so the map alone would not have caught it.
`--probe` closes that gap by calling each service once with a read-only
request.

Every probe is a GET or the service's own documented refresh; none writes.
Find My is refreshed with locate=False on purpose, because counting or
iterating the manager pings the user's hardware, which a diagnostic must
not do as a side effect. A test pins that.

Outcomes are separated by whose problem they are. A withdrawn endpoint or
an unexpected error fails the run; a service Apple reports unavailable for
this account, or one asking for re-authentication, is shown but does not,
because neither is a pyicloud defect. Services whose webservice key was
already reported missing are skipped rather than called.

The probe table is kept honest the same way the inventory is: a test
asserts it covers exactly the services endpoints.py names, in both
directions.

Probing is gated on the session status, not just on the API being
resolvable -- the same reason the map findings are. The two can disagree,
and calling eleven services with a session that reports unauthenticated
produces a page of failures that say nothing about the library.

Listing reminder lists took 28s against a real account, so both
zone-backed services read their sync cursor instead; the happy-path test
asserts which read was chosen, because a MagicMock answers either.

Live: all eleven services answer, `files` reports unavailable without
failing the run (that account is migrated off Ubiquity), and `invites` now
passes -- it was the failure this layer originally found, fixed in timlaing#339.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: ca51ed1e-0232-453f-a702-18ea519f03bd

📥 Commits

Reviewing files that changed from the base of the PR and between 86c4bc9 and cfe7eb1.

📒 Files selected for processing (5)
  • README.md
  • pyicloud/cli/commands/doctor.py
  • pyicloud/diagnostics.py
  • tests/test_cmdline.py
  • tests/test_diagnostics.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
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