Uh oh!
There was an error while loading. Please reload this page.
modifyZones per-zone failures (#431) + zones/changes metaSyncToken (#430) - #443
Conversation
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@## v1.0.0-beta.4 #443 +/- ##
=================================================
+ Coverage 81.64% 81.75% +0.10%
=================================================
Files 192 194 +2 Lines 4723 4751 +28 =================================================
+ Hits 3856 3884 +28
Misses 867 867
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
23639c1 to
caeb912CompareCovers the retro-fit Array+RecordResult helpers so codecov patch coverage meets the project threshold on #443. Co-authored-by: Cursor <cursoragent@cursor.com>
Covers the retro-fit Array+RecordResult helpers so codecov patch coverage meets the project threshold on #443. Co-authored-by: Cursor <cursoragent@cursor.com>
2e0ecc8 to
7018bd5Compare`ZonesModifyResponse.zones` items become `oneOf: [ZoneFetchFailure, Zone]`, matching `changes/database` and `changes/zone`. `zones/modify` is a batch endpoint whose realistic failure mode is partial, and Apple routes all four zone endpoints' failures through the same Zone Fetch Error Dictionary, so the error variant already exists — it just was not wired to this response. The failure variant is listed first, matching every other `oneOf` in the spec. `ZoneFetchFailure` requires `serverErrorCode`, so a success payload fails that branch and falls through to `Zone`. Also rewords the `zones/changes` request `syncToken` description (#433 part 3): it was described as "Meta-sync token", a name the spec does not use for the key. The key stays `syncToken` (#430); only the prose is corrected. Regenerating also repairs Sources/MistKitOpenAPI reproducibility: CodeFactor's bot alphabetized the Foundation imports in Client.swift/Types.swift in 61235b5, so `./Scripts/generate-openapi.sh` no longer reproduced the committed output. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xs1c8vvxjCxqZiStcmuPS2
`modifyZones` mapped every entry straight through as a success, so a batch where some zones failed was reported as a partial success naming no zone and discarding CloudKit's `serverErrorCode`/`reason`. It now returns a bare `[ZoneChangeResult]` — one entry per zone the server returned — mirroring how `modifyRecords` returns a bare `[RecordResult]`. `zones/modify` carries no batch-level metadata, so there is deliberately no `DatabaseChangesResult`-style wrapper struct. `ZoneChangeResult` / `ZoneOperationFailure` from #429 are reused rather than duplicated; the only new code is a second `init(from:)` overload keyed off the generated `ZonesModifyResponse.zonesPayloadPayload`. Two convenience-wrapper bugs fall out of this: - `createZone` threw a bare `.invalidResponse` with no code, reason or zone name when CloudKit rejected the create. It now calls `.get()`, throwing `.zoneOperationFailed` with the full failure. - `deleteZone` discarded the result entirely, so a `ZONE_NOT_FOUND` delete was reported to the caller as success. It now checks every entry. `.zones` / `.failures` accessors are added as concrete `Array` extensions (`[ZoneChangeResult]`, plus `.records`/`.failures` on `[RecordResult]`) rather than one generic extension over `OperationResult<Success, Target>`: Swift cannot bind free generic parameters in an extension's `where` clause. BREAKING: `modifyZones` returns `[ZoneChangeResult]`, not `[ZoneInfo]`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xs1c8vvxjCxqZiStcmuPS2
Adds a raw-dictionary `ResponseConfig.modifyZonesResponse(zones:)` builder — so a single response can mix success entries and zone error entries — mirroring `databaseChangesResponse(zones:syncToken:moreComing:)`, and a `makeService(zones:)` harness over it. New tests: a mixed batch keeps the successes and reports the failure with its zone name/code/reason; `.get()` on a failed entry throws `.zoneOperationFailed`; zone metadata survives the new success variant; `createZone` surfaces the `ZoneOperationFailure` instead of `.invalidResponse`; and `deleteZone` throws on `ZONE_NOT_FOUND` rather than reporting success. Verified the failure tests fail when the service is reverted to dropping error entries. `ZoneMetadataTests` now matches on the `oneOf` success variant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xs1c8vvxjCxqZiStcmuPS2
- `modify-zones` announces per-zone rejections on stderr (matching how `modify` reports per-record failures) and keeps stdout to the zones that were actually modified, so the JSON/CSV/table output stays machine-parseable. `outputResults` requires `Encodable` and `OperationResult` is `Sendable`-only, so the results cannot be rendered directly. - `webModifyZones` collapses the results all-or-nothing via `.get()`, matching the documented decision in `webLookupRecords`, so the web panel shows a rejection instead of silently returning fewer zones than were asked for. Its `[ZoneInfo]` signature is unchanged, so `WebBackend`, the routes and the mock backend need no changes. - `ModifyZonesPhase` asserted nothing about the results; it now fails the integration run when a create or the cleanup delete is rejected. Split into `createAndVerify` to stay under the cyclomatic-complexity limit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xs1c8vvxjCxqZiStcmuPS2
CodeFactor's bot committed to `Sources/MistKitOpenAPI/` in 61235b5, reordering imports in generated files, which broke `./Scripts/generate-openapi.sh` reproducibility until this branch regenerated them. Nothing checked that. `check-generated-openapi.yml` now regenerates and runs `git diff --exit-code Sources/MistKitOpenAPI/` on every PR. It builds the generator from `Scripts/OpenAPITools`, whose version is pinned in sync with mise.toml, so the check is self-contained. That fallback build leaves SwiftPM checkouts in `Scripts/OpenAPITools/.build`, which SwiftLint then walked (the bare `.build` exclude only matches the repo root one) — added as an explicit exclude. Docs: AGENTS.md's per-zone-failures paragraph now covers `zones/modify` and records the oneOf ordering rationale, the operations table names the new return type, and README points at `modifyZones` alongside `createZone`/`deleteZone`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xs1c8vvxjCxqZiStcmuPS2
A live container run (iCloud.com.brightdigit.MistDemo / development / private,
web-auth) proved `zones/changes` neither returns nor honors `syncToken`:
- The response's top-level keys are exactly `[moreComing, metaSyncToken,
zones]` — no `syncToken` at all.
- Round-tripping the same baseline token: sending `{"syncToken": …}` (what
MistKit sent) returned all 40 zones again — the key is silently ignored and
page one replays. Sending `{"metaSyncToken": …}` returned 0 zones — honored
and correctly advanced.
So `fetchZoneChanges` / `fetchAllZoneChanges` pagination has never worked. This
supersedes the description-only wording fix in the previous commit, which
assumed the mismatch was documentation rather than behavior.
Renames the wire key for `zones/changes` **only** — the request body property
and `ZoneChangesResponse` — and regenerates. `changes/database`, `changes/zone`
and `records/changes` legitimately use `syncToken` and are untouched.
Every Swift-facing name is deliberately unchanged: `ZoneChangesResult.syncToken`
and its `init(syncToken:)` label, and the `fetchZoneChanges(syncToken:)` /
`fetchAllZoneChanges(syncToken:)` argument labels. `MistKitOpenAPI` is an
`internal import`, so a wire-key rename is not source-breaking for consumers;
only the mapping in `ZoneChangesResult.init(from:)` and the request
construction in `CloudKitService+ZoneOperations.swift` change.
Adds `CloudKitServiceTests.FetchZoneChanges+WireFormat.swift`, which pins that
MistKit sends `metaSyncToken` and never `syncToken`, reads `metaSyncToken` in
preference to a decoy `syncToken`, and feeds the previous page's token back
under the honored key. Existing `zones/changes` fixtures were emitting the
wrong key and are updated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xs1c8vvxjCxqZiStcmuPS2Covers the retro-fit Array+RecordResult helpers so codecov patch coverage meets the project threshold on #443. Co-authored-by: Cursor <cursoragent@cursor.com>
7018bd5 to
a2055afCompareUh oh!
There was an error while loading. Please reload this page.
Closes#431. Closes#430. Also lands part 3 of #433 and a CI guard for generated output.
1.
modifyZonesper-zone failures (#431)zones/modifyis a batch endpoint whose realistic failure mode is partial — creating five zones where one already exists, deleting zones where one is missing.modifyZonesmapped every response entry straight through as a success, so those failures were reported to the caller as a shorter-than-expected array naming no zone, with CloudKit'sserverErrorCodeandreasondiscarded.Spec.
ZonesModifyResponse.zonesitems becomeoneOf: [ZoneFetchFailure, Zone]. No new schema was needed:.claude/memory/reference_cloudkit_zone_dictionary.mdrecords that all four zone endpoints —zones/modifyincluded — route failures through Apple's Zone Fetch Error Dictionary, already modeled asZoneFetchFailureforchanges/database/changes/zone. The failure variant is listed first, matching every otheroneOfin the file;ZoneFetchFailurerequiresserverErrorCode, so a success payload fails that branch and falls through toZone.Service.
modifyZonesreturns a bare[ZoneChangeResult], mirroringmodifyRecordsreturning a bare[RecordResult].zones/modifyhas no batch-level metadata, so there is deliberately noDatabaseChangesResult-style wrapper struct.ZoneChangeResult/ZoneOperationFailurefrom #429 are reused rather than duplicated — the only new decoding code is a secondinternal init(from:)overload on the existing generic constraint.modifyZones(_:database:)returns[ZoneChangeResult]instead of[ZoneInfo].createZone/deleteZonekeep their signatures. Verified: no call sites inExamples/BushelCloudorExamples/CelestraCloud, so no subrepo work is needed.Two real bugs fixed as a consequence
createZonethrew a bareCloudKitError.invalidResponse— no code, no reason, not even the zone name — when CloudKit rejected the create. It now calls.get()on the entry and throws.zoneOperationFailedcarrying the full failure.deleteZonedid_ = try await modifyZones(...), so aZONE_NOT_FOUNDdelete was reported to the caller as success. It now checks every entry..zones/.failuresaccessors — design decisionThe issue asks for
.zones/.failureslikeDatabaseChangesResulthas, and the plan was one genericextension Array where Element == OperationResult<Success, Target>so it would also retro-fit[RecordResult]. That cannot be expressed in Swift — an extension'swhereclause cannot bind free generic parameters (error: cannot find type 'Success' in scope). Falling back to concrete extensions, which also read better at the call site than a generic.successeswould:Array+ZoneChangeResult.swift→.zones/.failuresArray+RecordResult.swift→.records/.failures(the requested[RecordResult]retro-fit)[SubscriptionResult]was left alone as out of scope for this issue.2.
zones/changesusesmetaSyncToken— key renamed (#430, #433 part 3)This started as a description-only fix (#433 part 3): the request property was named
syncTokenbut described as "Meta-sync token from previous operation". A live container run then proved the key itself is wrong, so it became an actual rename.Live evidence
POST zones/changesagainstiCloud.com.brightdigit.MistDemo/development/ private, with web-auth credentials:[moreComing, metaSyncToken, zones]—syncTokenis absent.{"syncToken": …}(what MistKit sent) → all 40 zones returned again. The key is silently ignored and page one replays.{"metaSyncToken": …}→ 0 zones. Token honored, correctly advanced.So
fetchZoneChanges/fetchAllZoneChangespagination has never worked. Full evidence is in #430.What changed
The wire key is renamed for
zones/changesonly — the request body property andZoneChangesResponse.changes/database,changes/zoneandrecords/changesgenuinely usesyncTokenand are untouched.Not source-breaking. Every Swift-facing name is unchanged:
ZoneChangesResult.syncToken, itsinit(syncToken:)label, and thefetchZoneChanges(syncToken:)/fetchAllZoneChanges(syncToken:)argument labels.MistKitOpenAPIis aninternal import, so the rename is invisible to consumers; only two lines move — the mapping inZoneChangesResult.init(from:)and the request construction inCloudKitService+ZoneOperations.swift.New
CloudKitServiceTests.FetchZoneChanges+WireFormat.swiftpins all three directions against regression, using the existingResponseProvider.requestLogcapture:metaSyncTokenand neversyncToken.metaSyncTokenin preference to a decoysyncTokenin the same body.fetchAllZoneChangesfeeds the previous page's token back under the honored key — the actual zones/changes may use metaSyncToken, not syncToken — pagination possibly broken #430 bug.Existing
zones/changesfixtures were emitting the wrong key and are corrected.3. Generated-output reproducibility + CI guard
CodeFactor's bot committed to generated files in 61235b5, alphabetizing the
Foundationimports inSources/MistKitOpenAPI/Client.swiftandTypes.swift(movingimport struct Foundation.URLafter.Data/.Datein both#if os(Linux)arms). The generator emits URL/Data/Date order, so./Scripts/generate-openapi.shno longer reproduced the committed output. Regenerating on this branch repairs it — confirmed.New
.github/workflows/check-generated-openapi.ymlruns./Scripts/generate-openapi.shthengit diff --exit-code Sources/MistKitOpenAPI/on every PR, so this cannot recur silently. It builds the generator fromScripts/OpenAPITools, whose version is pinned in sync withmise.toml, so the job is self-contained (no mise, no network beyond SwiftPM).Note for @leogdion:
.codefactor.ymlalready listsSources/MistKitOpenAPI/**underexclude:, and the bot committed to those files anyway. The exclude appears to govern analysis, not the auto-fix commits — worth disabling auto-fix commits on the CodeFactor side as well; this workflow only catches the damage after the fact.Also excluded
Scripts/OpenAPITools/.buildin.swiftlint.yml: the generator fallback leaves SwiftPM checkouts there, and the bare.buildentry only matches the repo-root one, so a local lint run after using the fallback walked ~2,000 vendored files.Verification
./Scripts/generate-openapi.sh && git diff --exit-code Sources/MistKitOpenAPI/swift build && swift test(root)swift build && swift test(Examples/MistDemo)LINT_MODE=STRICT ./Scripts/lint.shSources/MistKitOpenAPI/Client.swiftafter the renameZoneChangesResponseis body-only)LINT_MODE=STRICT ./Scripts/lint.sh(Examples/MistDemo)Sanity-checked both changes by reverting them:
modifyZonesfailure tests fail (results.count == 2,results.failures.first, and thecreateZone/deleteZoneexpectations).syncToken(spec + regenerate + the two mapping lines) makes all threeWireFormattests fail, plus four pre-existingFetchZoneChangestests — includingfetchAllZoneChanges() accumulates zones across three pages, which is exactly the pagination path zones/changes may use metaSyncToken, not syncToken — pagination possibly broken #430 breaks.MistDemo call sites
ModifyZonesCommand—outputResults<T: Encodable>cannot renderOperationResult(Sendable-only). Rejections go to stderr as warnings, matching howModifyCommandreports per-record failures; stdout keeps the successful zones so JSON/CSV/table output stays machine-parseable.CloudKitService+WebBackend.webModifyZones— collapses all-or-nothing via.get(), matching the documented decision already inwebLookupRecords, so the web panel surfaces a rejection instead of silently returning fewer zones. Signature stays[ZoneInfo], soWebBackend,WebServer+Zones,MockBackendandWebServerTests+Zonesneeded no changes.ModifyZonesPhase— previously discarded results; now fails the integration run when the create or the cleanup delete is rejected.🤖 Generated with Claude Code