Skip to content

Push activation: spec enhancements (token variants, updateToken, unified PATCH sync) + full UTS test suite - #513

Draft
paddybyers wants to merge 3 commits into
mainfrom
feat/uts-push
Draft

Push activation: spec enhancements (token variants, updateToken, unified PATCH sync) + full UTS test suite#513
paddybyers wants to merge 3 commits into
mainfrom
feat/uts-push

Conversation

@paddybyers

Copy link
Copy Markdown
Member

Two related workstreams in three commits, developed and validated together: a set of push-spec enhancements, and the complete portable (UTS) test suite for the push activation half of the RSH spec — previously untested (the completion-matrix rows for RSH2, RSH3, RSH4–5, RSH6, RSH8 and the push types were blank).

1. Spec enhancements (specifications/features.md)

Push#updateToken (RSH2f–RSH2f3, new)

Specifies the API through which the application delivers a rotated or additional platform token — established by ably-js in ably/ably-js#2267 but previously unspecced. Covers the validation guard and activation-required guard (both 40000, no side effects), and the apply-to-recipient + persist + GotPushDeviceDetails (RSH8g) flow. This is the delivery mechanism for FCM onTokenRefresh, APNs re-registration, and ActivityKit token updates.

APNs token slots on the recipient (PCP3a, new)

iOS has multiple flavours of device token — default, location-push, and Live Activity push-to-start. ably-cocoa has already shipped support (ARTAPNSDeviceTokenTypes, recipient carrying an apnsDeviceTokens map) with no spec backing — and the nested map doesn't fit PCP3's "map of string key/value pairs" wording. PCP3a codifies the shipped wire shape: apnsDeviceTokens (slot name → token) alongside/instead of the legacy deviceToken (≡ apnsDeviceTokens.default); well-known slots default/location/pushToStart, with slot names open/extensible (a token "type" is a registration slot/delivery route, not a capability set — the default token serves alert + background pushes selected per-request via apns-push-type, while other routes are forcibly distinct tokens; an open namespace absorbs future flavours, e.g. per-attributes-type push-to-start tokens whose granularity Apple doesn't document, without spec surgery).

Device-side variant lifecycle (RSH8l–RSH8l4, new)

Variants are recipient-related attributes for RSH8a persistence; registering/updating any variant is an RSH8g change whose sync carries the complete recipient (RSH8l2); RSH8i verification applies per variant; RSH8l4 (non-normative) records the deliberate exclusion of per-activity Live Activity update tokens — their lifetime is an activity instance, and updates flow over APNs broadcast channels (RSH1e), so only the device-scoped push-to-start token needs registering.

PushDeviceToken type (PDT1–PDT4, new)

{transportType: "fcm"|"apns"|"web", token, apnsTokenType?} + IDL for it and updateToken.

Unified PATCH registration sync (RSH3a2a3, RSH3d3b, RSH8f amended)

With RSH8l2 requiring every sync to carry the complete recipient, the payload distinction between the RSH3a2a3 re-activation PUT and the RSH3d3b changed-fields PATCH mostly disappears. RSH3d3b becomes the canonical sync (complete recipient + other changed attributes, e.g. a late-set clientId per RSH8d — resolving a pre-existing ambiguity in the RSH8e flow); RSH3a2a3 performs the same sync, with the full-body PUT retained as a permitted legacy equivalent — ably-java/ably-cocoa remain conformant unchanged, and ably-js (which PATCHes) becomes conformant. RSH8f generalised to accept a clientId from sync responses (verified: the PATCH response returns the full DeviceDetails).

RSH6a clarification

The X-Ably-DeviceToken value is the raw token, not base64-encoded, with a non-normative note that the historical base64 encoding (ably-java, ably-cocoa) is tolerated by the service but not conformant. Verified empirically against the sandbox: raw and base64 both accepted; garbage rejected (400/40005).

2. UTS test suite (10 spec files, 75 tests)

  • Helper: rest/unit/helpers/mock_push_platform.md — the portable push platform primitives (PushKeyValueStoragegetItem/setItem/removeItem + requestToken, matching the ably-js React Native seam from [AIT-850] feat: react native push plugin ably-js#2262), the standard ably.push.* storage keys and value encodings, install_push_platform() (consistent with the transport-mock conventions; injection mechanism SDK-specific by design), and the MockPushStorage mock (dump/seed/fault flags/onOperation with local-array capture).
  • Unit (7 files, 62 tests): the full RSH3 state-machine transition matrix incl. both registrar branches and RSH3d2c1 classification (push_activation_state_machine.md, 23), LocalDevice attributes + clientId lifecycle (local_device.md, 6), persistence/rehydration incl. RSH8a1 corruption discard (push_activation_persistence.md, 6), RSH4/RSH5 queue semantics incl. the spec's own worked example (push_activation_event_queue.md, 3), updateToken incl. the RSH8l2 variant tests (push_update_token.md, 12), device auth incl. the RSH1b/RSH1c own-device clauses (push_device_auth.md, 5), and the PCD/PCP/PCS types (types/push_types.md, 7). Black-box style throughout: public API + held mock requests; state observed via requests and settled persisted state.
  • Integration (7 tests): real-sandbox round-trips using storage pre-seeded with an ablyChannel recipient (rides RSH3a2c, so no FCM/APNs credentials needed), including end-to-end delivery of an admin publish and a device-auth server-acceptance check made load-bearing via a push-subscribe-capability key.
  • Proxy (6 tests): fault injection on the registration endpoints — the RSH3d2c1 classifications proven client-side by a direct admin client observing the registration survive an intercepted DELETE, rollback/retry flows, and the RSH4 queue against a delayed registration.

Validation

Every test is derived and validated in ably-js (full UTS unit suite: 1428 passing / 73 pending; genuine non-conformances recorded in its test/uts/deviations.md — 13 push entries) and ably-dart (fully conformant implementation of the extended spec, including the token variants: unit 1274 passing, integration + proxy 10 passing + 3 pending-deploy skips). Validation surfaced and fed back: one spec-test bug (5xx fault injection tripping RSC15 fallback retries — now a non-retriable 4xx), two ably-dart implementation bugs (fixed), several ably-js conformance gaps (recorded as deviations, fix PRs planned), and one server-side bug — the registration-update PATCH rejected any device whose stored recipient was ablyChannel — fixed in ably/realtime#8591; the affected derived tests are skipped pending its sandbox deploy (the specs carry "known server issue" notes).

Review points

  1. RSH8l4 cross-references RSH1e (APNs broadcast channels), which currently exists only on the AIT-794/live-activity branch — this PR should land after it, or the cross-ref be softened.
  2. PCP3a's open slot namespace constrains servers to tolerate unknown slots — flagging for explicit agreement.
  3. Remaining small gaps to close before undrafting (or as follow-ups): api-docstrings.md entries for updateToken/PushDeviceToken, and dedicated PDT type tests in push_types.md.

🤖 Generated with Claude Code

paddybyersand others added 3 commits August 7, 2026 18:15
…H sync
Extend the push spec to cover behaviour that has shipped in SDKs without
spec backing, and to unify the registration sync:
- RSH2f-RSH2f3: specify Push#updateToken(token: PushDeviceToken), the API
through which the application delivers rotated or additional platform
tokens (established by ably-js #2267): validation and activation-required
guards (40000), then apply-to-recipient + persist + GotPushDeviceDetails
per RSH8g.
- PCP3a: specify the apnsDeviceTokens token-slot map on APNs recipients,
as shipped by ably-cocoa. Well-known slots: default (equivalent to the
legacy deviceToken attribute), location, pushToStart. Slot names are
open/extensible; libraries must pass unknown slots through.
- RSH8l-RSH8l4: device-side token variants. Variants are recipient-related
attributes for RSH8a persistence; any variant change is an RSH8g push-
details change whose sync carries the complete recipient; RSH8i verifies
per variant. Non-normative note excluding per-activity Live Activity
update tokens (broadcast channels make them unnecessary; the reference
to RSH1e depends on the AIT-794/live-activity branch).
- PDT1-PDT4: the PushDeviceToken type (transportType, token, optional
apnsTokenType), plus IDL for it and updateToken.
- RSH3a2a3/RSH3d3b: unify the registration sync on PATCH. RSH3d3b becomes
the canonical sync (complete recipient + other changed attributes, eg a
late-set clientId per RSH8d); the RSH3a2a3 re-activation validation
performs the same sync, with the previous full-body PUT retained as a
permitted legacy equivalent (as implemented by ably-java/ably-cocoa).
- RSH8f: generalised to accept clientId from registration-sync responses,
noting the dependency on the endpoint returning the updated
DeviceDetails (to be confirmed server-side).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… types)
Add portable unit test specs for the push activation half of the RSH spec,
previously untested (completion matrix rows were blank):
- rest/unit/helpers/mock_push_platform.md: the portable push platform
primitives (PushKeyValueStorage getItem/setItem/removeItem, requestToken,
PushPlatformConfig, PushDeviceToken incl. apnsTokenType), the standard
ably.push.* storage keys and value encodings, and the MockPushStorage /
token-provider mocks (dump/seed/fault flags/onOperation with local-array
capture, consistent with the transport-mock conventions). Installation is
via install_push_platform(), mirroring install_mock(); the injection
mechanism is SDK-specific by design.
- rest/unit/push/: push_activation_state_machine.md (23 tests — RSH2a/b,
full RSH3 transition matrix, both registrar branches, RSH3d2c1
classification, rollbacks), local_device.md (6 — RSH8 attributes and
clientId lifecycle), push_activation_persistence.md (6 — RSH3h, RSH8a1
discard, RSH3a2c, RSH8b/c timing), push_activation_event_queue.md (3 —
RSH4 queue semantics incl. the spec's worked example, RSH5 ordering),
push_update_token.md (12 — RSH2f guards, RSH3d3 PATCH sync, callbacks,
races, RSH8l2 token variants), push_device_auth.md (5 — RSH6a/b and the
RSH1b/c own-device clauses).
- rest/unit/types/push_types.md (7 — PCD, PCP incl. PCP3a, PCS).
Tests are black-box (public API + held mock requests; state observed via
requests and settled persisted state). Registration syncs follow the
unified RSH3a2a3/RSH3d3b PATCH (with the legacy PUT allowance); the
token-variant tests anchor to the pending spec extension (RSH2f, RSH8l,
PCP3a, PDT1-4).
Validated by deriving all 62 tests in ably-js (test/uts, branch
feat/uts-push): full UTS unit suite 1428 passing / 73 pending, deviations
recorded in ably-js deviations.md. Validation surfaced one spec-test bug,
fixed here: the deregistration-rollback tests now inject a non-retriable
4xx instead of a 500, which would trip RSC15 fallback-host retries.
Also update docs: completion-status rows for RSH2-RSH8 and push types (+
PDT), README structure/counts, and freshness fixes in
writing-test-specs.md and proxy.md (proxy msgpack support per uts-proxy
v0.3.0 replaces the stale "SDK doesn't implement msgpack" convention;
File Organization now defers to README; push platform mock added to the
mock-infrastructure patterns).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the integration and proxy tiers of the push activation UTS suite, and
clarify RSH6a following empirical validation:
- rest/integration/push_activation.md (7 tests): real-sandbox activation
round-trips using storage pre-seeded with an ablyChannel recipient (per
RSH3a2c the platform requestToken is never consulted, so no FCM/APNs
credentials are needed): registration (POST body, server-issued
deviceIdentityToken), device-authenticated subscribeDevice by a fresh
client over persisted state (doubles as the RSH6a server-acceptance
check, made load-bearing via a push-subscribe-capability key),
deactivation, re-activation sync, end-to-end delivery of an admin
publish as an __ably_push__ message, registration rejection for an
invalid platform, and the updateToken fire-and-forget sync.
- rest/integration/proxy/push_activation.md (6 tests): fault injection on
the registration endpoints via uts-proxy — RSH3d2c1 status
classification (401/40005 classified as Deregistered purely client-side,
proven by a direct admin client observing the registration survive the
intercepted DELETE), deregistration-failure rollback and retry,
registration failure and retry, the RSH4 queue exercised against a
delayed registration, and sync-failure recovery via updatedCallback.
- specifications/features.md: RSH6a now states the header value is the raw
deviceIdentityToken, not base64-encoded, with a non-normative note that
the historical base64 encoding (ably-java, ably-cocoa) is tolerated by
the service but not conformant. Verified against the sandbox: raw and
base64 are both accepted; garbage values are rejected (400, 40005).
- Both specs carry a "known server issue" note for the
ablyChannel-recipient PATCH bug fixed by ably/realtime#8591: the
sync-exercising tests are specified against the fixed behaviour, and
derived tests skip with a reason referencing that PR until it deploys.
Derived and validated in ably-js (integration 6 passing + 1 pending;
proxy 3 passing + 3 pending, deviations recorded) and ably-dart
(integration + proxy: 10 passing + 3 pending-deploy skips; fully
conformant otherwise). Validation also surfaced two ably-dart
implementation bugs (fixed there) and the server-side PATCH bug above.
Also: completion-status rows and README structure/counts updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@paddybyers