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 (PushKeyValueStorage getItem/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

paddybyers and 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 free to 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