Conversation
Adds an opt-in iCloud backup so moving progress between Macs no longer means carrying an exported file by hand. Each Mac mirrors its own save into iCloud Drive; another Mac lists those backups and restores one on an explicit click. Transport is a plain file under ~/Library/Mobile Documents/com~apple~CloudDocs. CloudKit, NSUbiquitousKeyValueStore and ubiquity containers all require com.apple.developer.icloud-* entitlements that macOS validates against a provisioning profile from a paid developer team; this app is signed with a self-signed certificate and has no entitlements file, so those APIs would silently no-op. The app is not sandboxed, so the iCloud Drive path works with no entitlements at all. Restore is deliberately manual. CompanionState holds fields that cannot be merged without per-device delta tracking that does not exist -- active, eggTier and pendingHatchID are single-valued, inventory and spentTokens are counts. Auto-applying a remote save would reproduce the cross-machine last-writer-wins loss already recorded in the defect log, without even the SingleInstance guard. Each Mac writes save-<deviceID>.json so iCloud never has to resolve a conflict, and restore reuses the existing import path: SaveTransfer.decode, the confirm dialog with Cancel as the default button, backupStateBeforeImport, and rebasedForThisDevice. CompanionStore.save() runs on the 120s refresh tick with no dirty check, so the mirror gates writes twice: an interval floor checked before encoding, then a content comparison against a sortedKeys encoding of the state. The default encoder's key order is not stable for inventory, candyGrantTier or collectedFinals, and the envelope's exportedAt differs on every call, so neither can be used for change detection. Write failures are not recorded, leaving the next tick to retry. Off by default. Coverage on the new file is 98.6%; each gate was verified by disabling it and confirming its guard test fails.
…le them A @mainactor XCTestCase's synchronous setUp/tearDown is nonisolated in release Swift, so touching main-actor stored properties from it is rejected. ICloudSaveMirrorTests did exactly that and broke the build on CI. The same diagnostic has different severity per toolchain: locally (6.3 / 6.5-dev) it is a warning and swift test passes, while CI (6.1.2) makes it an error and compilation dies. Local green was never evidence here -- the only signal before pushing was one warning buried in build output. The rule already existed as a comment in UsageStoreTests, and the next test file added broke it anyway, so prose was not enough. Adds a source scan, testMainActorTestClassesKeepMutableFixturesNonisolated, that walks Tests/ and fails on any @mainactor XCTestCase with a synchronous setUp/tearDown holding a mutable stored property that is not nonisolated(unsafe). Verified by removing the annotation from ICloudSaveMirrorTests and again from UsageStoreTests, confirming it flags both rather than one. Also silences an unused-result warning on makeMirror, and records the toolchain severity split in the defect log.
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.
What
Adds an opt-in iCloud backup to Settings → Backup & Transfer. When it is on, this Mac
mirrors its save into iCloud Drive automatically; another Mac lists the backups it finds
there and restores one on an explicit click. Moving progress between Macs no longer means
carrying an exported file by hand.
Off by default.
Why a plain file and not CloudKit
CloudKit,
NSUbiquitousKeyValueStoreandFileManager.url(forUbiquityContainerIdentifier:)all require
com.apple.developer.icloud-*entitlements, and macOS validates those against aprovisioning profile issued to a paid developer team. This app is signed with the self-signed
certificate from
scripts/create-signing-cert.sh, has no Team ID and no.entitlementsfileat all, so those APIs do not fail loudly — they return
niland no-op. The build succeeds andthe feature silently does nothing, which is exactly the kind of thing review does not catch.
The app is not sandboxed, so writing directly into
~/Library/Mobile Documents/com~apple~CloudDocs/works with zero entitlements and the iCloudDrive daemon handles the sync. That is the transport here. I have written this up in
docs/reference/defect-log.mdso the CloudKit route is not attempted again before the signingsetup changes.
Why restore is manual, not automatic
CompanionStatehas fields that cannot be merged without per-device delta tracking that doesnot exist:
active,eggTierandpendingHatchIDare single-valued, andinventory/spentTokensare counts, so a naive merge double-spends. Auto-applying a remote save wouldreproduce the last-writer-wins loss already recorded in the defect log for duplicate
instances — only across machines, and without even the
SingleInstanceguard to stop it.So each Mac writes its own
save-<deviceID>.json(a shared filename would make two Macs fightover one path and produce iCloud conflict copies), and applying one is always a user action
that goes through the existing import path:
SaveTransfer.decode→ the confirm dialog withCancel as the default button →
backupStateBeforeImport→rebasedForThisDevice. No secondapply path was created.
Write cadence
CompanionStore.save()runs on the 120s refresh tick with no dirty check, so mirroring everycall would be up to 720 uploads a day. Two gates, in this order:
nothing. No trailing timer is needed — the 120s heartbeat is the trailing timer, so a change
made inside the window lands on the next tick after it expires.
.sortedKeysencoding of the state.save()'s own bytescannot be reused: the default
JSONEncoderhas no stable key order forinventory,candyGrantTierorcollectedFinals, so identical state yields different bytes. Theenvelope cannot be compared either — its
exportedAtdiffers on every call.Write failures are deliberately not recorded, so the next tick retries instead of assuming
success and dropping the change.
Not synced
UserDefaultssettings,session-key.json(a plaintext credential), and every regenerablecache (
usage-cache.json,base-index.json,sprites/,last-snapshot.json). One file isthe save.
Testing
20 new tests in
ICloudSaveMirrorTests.ICloudSaveMirror.swiftis added toLOGIC_COREintest-gate.sh; it sits at 98.6% line coverage (the only uncovered line is the SwiftUIIdentifiableid, exercised byForEach), andLOGIC_COREoverall is 91.5% against the 75floor.
Every gate was injection-checked — disabled one at a time, confirming the matching guard test
goes red rather than merely passing:
testUnchangedStateIsNotRewrittenEvenAfterTheIntervaltestChangedStateInsideTheIntervalIsHeld.sortedKeystestCanonicalEncodingIsStableForEqualStatesave()calltestCompanionStoreSaveMirrorsToICloudTwo-instance simulation
Ran two real bundled instances on one machine with separate
PTB_STATE_DIRvalues and ashared
PTB_ICLOUD_DIR, using distinct bundle identifiers so each gets its ownUserDefaultsdomain and therefore its own device ID.
Two files, no conflict copies.
remoteSavesrun against that folder: B sees exactly oneremote save (A's, dex=2), A sees exactly one (B's, dex=0); each excludes its own.
Applying A's real envelope to a copy of B's live state file:
Progress carried over including the shiny; the device ledger rebased to this machine's number
instead of importing A's, which is the silent-usage-loss trap
rebasedForThisDeviceexists toavoid; the pre-import backup was written.
Cadence sampled every 20s for 13 minutes while the instance was actively accruing tokens:
exactly two writes, 12:01:32 and 12:07:32. The 120s ticks in between were suppressed by the
floor, and the write landed on the first heartbeat past it.
Notes for the reviewer
sourceDevice, so two Macs sharing a name render identically andonly the date disambiguates. Left as is; worth revisiting if it bites.
PTB_ICLOUD_DIRlives inAppStatePaths.swifton purpose — that file is already on theallowlist in
UsageEnvironmentTests.testNoProviderReadsUsageLocationEnvDirectly, so readingthe variable from a new file would have failed that source scan.
assets/is still needed before release; the release gate hard-fails aUI
feat:commit without a newly added asset. README (en/ko/ja) and the landing page alsoneed the feature added.
LocalUsageCacheTests.testDateHelpersandtestEnrichmentScanStartCatchesWeekStraddlingMonthBoundaryfail on my machine before thisbranch (
en_THlocale resolves 2026 as Buddhist-era 1483). Pre-existing and unrelated; CI onmacos-15is unaffected.