objects: UTS test-spec corrections from cross-SDK (js/java/cocoa) audit - #512
objects: UTS test-spec corrections from cross-SDK (js/java/cocoa) audit#512sacOO7 wants to merge 3 commits into
Conversation
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.
paddybyers
left a comment
There was a problem hiding this comment.
Pls review the comments.
(Also fyi @mschristensen @VeskeR )
| - `(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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Yeah, this is actually supposed to be in draft state, claude directly raised the PR.
I am doing changes locally 👍
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
| > `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 |
There was a problem hiding this comment.
Shouldn't SDKs nonetheless be implemented consistently?
| 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 |
There was a problem hiding this comment.
I though that with apply-on-ack you should always be able to observe the effect of an operation once the publish completes?
|
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.
…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.
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-jseventemitter.ts/realtimeobject.ts/objectspool.ts, ably-cocoaARTEventEmitter.m, ably-javaEventEmitter.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 serverDETACHED+ re-ATTACHED. It is:onAttached → new-syncpath (RTO4c) as the retained "re-sync on new ATTACHED" scenario.DETACHEDtransitions the channel toSUSPENDED(notDETACHED).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:onmechanismlisteners.push(listener), no identity checkARTEventListenerperon:(block never the key)It restates the general
EventEmittercontract (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 noteinternal_live_counter.md,internal_live_map.md. Several tests assert on the update returned byapplyOperation. 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 aftersend_to_client; inbound ops apply asynchronously, so added apoll_untilbarrier 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/FAILEDthe objects data is cleared without emitting events, whileSUSPENDEDretains it. It is deliberately not a UTS requirement — no normativeRTOpoint, and it isn't observable through the public API (access onDETACHEDthrows 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
nullisn't distinguishable from an omitted increment argument (e.g. JSincrement(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
objects/unitsuite was regenerated/updated and runs green; the detach-clear behaviour (§5) is pinned by a new SDK-local test.Non-goals
EventEmittercontract.