Skip to content

objects: UTS test-spec corrections from cross-SDK (js/java/cocoa) audit - #512

Draft
sacOO7 wants to merge 3 commits into
mainfrom
fix/objects-uts-cross-sdk-audit
Draft

objects: UTS test-spec corrections from cross-SDK (js/java/cocoa) audit#512
sacOO7 wants to merge 3 commits into
mainfrom
fix/objects-uts-cross-sdk-audit

Conversation

@sacOO7

@sacOO7 sacOO7 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Corrections and clarifications to the objects UTS unit test specs, surfaced while translating them into ably-java and cross-checked against ably-js and ably-cocoa source. Net effect: two non-portable/redundant test cases removed, and several places where the spec assumed a single SDK's idiom made explicit so every SDK can implement them faithfully.

No behavioural/normative change to the objects feature itself — these are test-spec fixes plus one editor's note.

Context

These emerged from a systematic ably-java translation of objects/unit. Where a test couldn't pass identically across SDKs, the SDK sources were compared (ably-js eventemitter.ts / realtimeobject.ts / objectspool.ts, ably-cocoa ARTEventEmitter.m, ably-java EventEmitter.java / DefaultRealtimeObject.kt) to decide whether the divergence was an SDK bug or a spec issue. In each case below it was the latter.

Changes

1. RTO17-RTO18 — remove the "re-attach after detach" sync-event scenario

realtime_object.md. The scenario asserted ["SYNCING","SYNCED"] after an unsolicited server DETACHED + re-ATTACHED. It is:

  • Redundant — at the objects layer a detach emits no sync events, and the re-attach drives the same onAttached → new-sync path (RTO4c) as the retained "re-sync on new ATTACHED" scenario.
  • Not portably expressible — in some SDKs an unsolicited server DETACHED transitions the channel to SUSPENDED (not DETACHED).

ably-js already folds it into "re-sync on new ATTACHED" (its own test header says so). Replaced with a NOTE explaining the omission.

2. RTO18d — remove the duplicate-listener UTS test; add an editor's note

realtime_object.md (test removed → tombstone note), objects-features.md (editor's note). Whether re-registering the same listener reference fires once or twice is a platform-idiomatic design choice, not a bug — verified in source:

SDK on mechanism Duplicate →
ably-js listeners.push(listener), no identity check fires twice
ably-cocoa fresh ARTEventListener per on: (block never the key) fires twice
ably-java registry keyed by listener instance fires once (dedupes)

It restates the general EventEmitter contract (RTE4), so it can't be made uniform without an ecosystem-wide RTE4 change. RTE4's "fires twice" is retained as the reference; SDKs that dedupe document it locally. The case isn't universally testable, so it's dropped from the shared suite.

3. Op-path LiveObjectUpdate — portability note

internal_live_counter.md, internal_live_map.md. Several tests assert on the update returned by applyOperation. Added a note that an SDK whose op path returns a boolean and delivers the update via the subscription/emit path (e.g. a typed SDK) may assert the emitted update event and/or resulting object state — equivalent observables. The sync path (replaceData) returns the update everywhere.

4. Positive read-after-send — quiescence barrier

path_object.md. RTPO13c5 / RTPO14 read local state immediately after send_to_client; inbound ops apply asynchronously, so added a poll_until barrier before the read (the spec's conventions previously covered only negative assertions).

5. objects_pool.md — document the SDK-internal detach-clear behaviour

Added a NOTE: on channel DETACHED/FAILED the objects data is cleared without emitting events, while SUSPENDED retains it. It is deliberately not a UTS requirement — no normative RTO point, and it isn't observable through the public API (access on DETACHED throws per RTO25b; re-sync replaces the pool regardless). Each SDK guards it with an SDK-local test.

6. internal_live_counter_api.md — assert the null-means-omitted contract

Where null isn't distinguishable from an omitted increment argument (e.g. JS increment(amount?)), assert the equivalence directly (increment(null) increments by the default of 1) so a later signature/coalescing change surfaces as a conscious decision.

Cross-SDK validation

  • ably-java: the affected objects/unit suite was regenerated/updated and runs green; the detach-clear behaviour (§5) is pinned by a new SDK-local test.
  • ably-js / ably-cocoa: the divergences in §1 and §2 were confirmed by reading their source; the reference behaviour retained in the spec matches ably-js.

Non-goals

  • No change to normative objects behaviour or the RTE4 EventEmitter contract.
  • Per-SDK local tests for the §5 detach-clear behaviour are left to each SDK.

Corrections and clarifications to the objects UTS unit test specs, surfaced while
translating them to ably-java and cross-checked against ably-js and ably-cocoa source.

- RTO17-RTO18 (realtime_object.md): remove the "re-attach after detach" sync-event
  scenario. It is redundant with "re-sync on new ATTACHED" (identical onAttached ->
  new-sync path) and not portably expressible - an unsolicited server DETACHED
  transitions the channel to SUSPENDED (not DETACHED) in some SDKs. ably-js already
  folds it into "re-sync on new ATTACHED".
- RTO18d (realtime_object.md, objects-features.md): remove the duplicate-listener UTS
  test and add an editor's note. Whether re-registering the same listener fires once or
  twice is a platform-idiomatic design choice (verified in source: ably-js and ably-cocoa
  append -> twice; ably-java de-duplicates by listener instance -> once), rooted in the
  general EventEmitter contract (RTE4). Not universally testable; each SDK pins its own.
- Op-path LiveObjectUpdate portability (internal_live_counter.md, internal_live_map.md):
  note that the update returned by applyOperation may be observed via the return value OR
  the emitted update event / resulting state, so event-based / typed SDKs can satisfy the
  same requirement.
- Read-after-send quiescence (path_object.md): add a poll_until barrier before the
  positive reads in RTPO13c5 / RTPO14 (inbound ops apply asynchronously).
- objects_pool.md: add a NOTE documenting the SDK-internal "DETACHED/FAILED clears objects
  data; SUSPENDED retains" behaviour and why it is deliberately not a UTS requirement
  (no normative point; unobservable via the public API), guarded by SDK-local tests.
- internal_live_counter_api.md: assert the null-means-omitted increment contract directly
  where null is not distinguishable from an omitted argument.
@github-actions
github-actions Bot temporarily deployed to staging/pull/512 July 29, 2026 09:33 Inactive

@paddybyers paddybyers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pls review the comments.

(Also fyi @mschristensen @VeskeR )

Comment thread specifications/objects-features.md Outdated
- `(RTO18b2)` `SYNCED`: Indicates that the [RTO17](#RTO17) sync state has transitioned to `SYNCED`
- `(RTO18c)` Registers the provided listener for the specified event
- `(RTO18d)` If `on` is called more than once with the same listener and event, the listener is added multiple times to the listener registry. As such, if the same listener is registered twice and an event is emitted once, the listener will be invoked twice
> **EDITOR'S NOTE — SDKs diverge on this by design; it is not universally testable.** RTO18d

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree with this approach of deleting the test spec. The UTS test specs should test the assertions in the specification. If some SDKs deviate from that specification, that is something we reflect in the tests for those SDKs, not the UTS tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is actually supposed to be in draft state, claude directly raised the PR.
I am doing changes locally 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree the UTS test spec should be kept.


Tests operate directly on InternalLiveCounter by calling `applyOperation()` and `replaceData()` with constructed messages. No channel or connection infrastructure is needed.

> **SDK portability note — op-path update return value.** Several tests below assert on the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should try to design the UTS test assertions so that they are as portable as possible. Can this be done here, or are you saying that the tests in the SDKs are necessarily different?

Comment thread uts/objects/unit/objects_pool.md Outdated
> `DefaultRealtimeObject.handleStateChange`), but it is **deliberately not a shared UTS requirement**:
> it has no normative `RTO` point, and it is not observable through the public API — access on a
> DETACHED channel throws (RTO25b), and on re-attach the RTO5c sync **replaces** the pool regardless of
> whether detach cleared it, so an SDK that skipped the clear would still be observably correct. It is a

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't SDKs nonetheless be implemented consistently?

Comment thread uts/objects/unit/path_object.md Outdated
build_map_set("map:prefs@1000", "back_ref", { objectId: "map:profile@1000" }, "99", "remote")
]))

# Quiescence barrier for a POSITIVE read-after-send: SDKs may apply inbound OBJECT messages

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I though that with apply-on-ack you should always be able to observe the effect of an operation once the publish completes?

@sacOO7
sacOO7 marked this pull request as draft July 29, 2026 10:24
@sacOO7

sacOO7 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

This PR is in progress locally, will update and mark it as ready for review

…c point + UTS test

Addresses the PR review on the objects UTS cross-SDK corrections.

- RTO18d (realtime_object.md, objects-features.md): restore the duplicate-listener UTS
  test (asserting the RTE4 "fires twice" contract) instead of removing it, and drop the
  editor's note from the spec. The UTS tests the spec's assertion; SDKs that de-duplicate
  listeners by instance (e.g. ably-java) may assert fires-once via an optional note on the
  test — the deviation stays with the SDK, not the UTS.

- RTO27 (objects-features.md, NEW): add an explicit normative point for the objects-data
  lifecycle on channel state transitions — on DETACHED/FAILED clear every pooled object's
  data without emitting update events (and clear the SyncObjectsPool); on SUSPENDED retain
  it. Placed in the channel-lifecycle cluster (after RTO5) and cross-referenced from RTO20e1.
  Closes a previously-unspecified gap both reference SDKs already implement (ably-js
  RealtimeObject.actOnChannelState, ably-java DefaultRealtimeObject.handleStateChange).

- RTO27 UTS test (realtime_object.md): add a white-box test that drives the internal
  channel-state handler directly and inspects the ObjectsPool (mirroring the
  internal_live_counter / internal_live_map tests), so the clear (not observable via the
  public API) and SUSPENDED (a connection-level state) are both testable at the unit tier.
  Reframe the objects_pool.md / realtime_object.md notes to reference RTO27.

- path_object.md: reword the RTPO13c5 / RTPO14 read-after-send quiescence barrier to clarify
  it guards an INBOUND server OBJECT message (no apply-on-ack point to await), not a local
  publish.
@github-actions
github-actions Bot temporarily deployed to staging/pull/512 July 29, 2026 13:00 Inactive
…ming conventions

Follow-up to the RTO27 work and the PR review.

- objects_pool.md: remove the DETACHED/FAILED detach-clear NOTE. It was a stopgap for a
  missing spec point; now that RTO27 is normative and has its own white-box test in
  realtime_object.md, the note is redundant (and was a RealtimeObject concern, not a bare
  ObjectsPool one).

- realtime_object.md: rename the RTO27 test's abstract white-box symbols to the UTS
  camelCase convention (objects_pool -> objectsPool, handle_channel_state ->
  processChannelState); drop the dangling "See objects_pool.md" pointer and the redundant
  RTO27 cross-reference from the no-re-attach-after-detach note, trimming that note to its
  two load-bearing reasons (redundant + not portably expressible); extend the header spec
  points to RTO22-RTO27 to match the file's contents.

- docs/writing-test-specs.md, README.md: add an "Identifier Naming" convention section
  (spec surface mirrors the specification's names — PascalCase types, camelCase
  fields/methods; behaviours the spec describes but doesn't name get a camelCase coinage;
  snake_case is reserved for test-harness constructs; wire/enum values keep protocol/spec
  casing). Validated against objects/realtime/rest UTS specs and the features.md/protocol.md
  parent specs.
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.

2 participants