Skip to content

[AIT-1008] Translate LiveObjects objects UTS test specs to Kotlin tests#1222

Open
sacOO7 wants to merge 10 commits into
feature/path-based-liveobjects-implementationfrom
feature/uts-liveobjects-unit-tests
Open

[AIT-1008] Translate LiveObjects objects UTS test specs to Kotlin tests#1222
sacOO7 wants to merge 10 commits into
feature/path-based-liveobjects-implementationfrom
feature/uts-liveobjects-unit-tests

Conversation

@sacOO7

@sacOO7 sacOO7 commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Translates the objects (LiveObjects) UTS unit specs into runnable Kotlin tests in the uts
module, using the uts-to-kotlin skill. Of the 15 objects/unit specs, 10 are translated
(181 tests)
and 5 are blocked by missing internal implementation (documented, not silently
skipped).

This is translate-only: every test compiles (./gradlew :uts:compileTestKotlin is green). Most
run only once the LiveObjects engine (OBJECT_SYNC processing + RealtimeObject.get()) lands; the
message-layer and value-type construction tests run today.

Base branch: feature/liveobjects-uts-tests.

What's included

10 translated spec files — 181 @Test methods

Spec (objects/unit/…) Test class Tests Layer
instance.md InstanceTest 20 Public view (Instance)
path_object.md PathObjectTest 30 Public view (PathObject)
path_object_mutations.md PathObjectMutationsTest 13 Public view
path_object_subscribe.md PathObjectSubscribeTest 22 Public view
live_counter_api.md LiveCounterApiTest 7 Public view
live_map_api.md LiveMapApiTest 15 Public view
live_object_subscribe.md LiveObjectSubscribeTest 11 Public view
public_object_message.md PublicObjectMessageTest 13 Public message layer
realtime_object.md RealtimeObjectTest 33 Mixed (public get() + sync events)
value_types.md ValueTypesTest 17 Mixed (public create surface)

Each test carries a verbatim @UTS objects/unit/… tag tracing it to the source spec point. All files
follow the uts conventions (no star imports, runTest, shared helpers.kt).

Deviationsuts/src/test/kotlin/io/ably/lib/uts/deviations.md
Per-test divergences where ably-java (a typed SDK) differs from the spec's dynamic API, e.g.:

  • wrong-type Instance cast throws IllegalStateException rather than ErrorInfo 92007;
  • value()/size() partitioned off the wrong-typed view (not expressible);
  • compact() unimplemented → compactJson() used;
  • invalid-input cases the type system rejects at compile time;
  • internal wire-message-shape assertions adapted to observable public effects.

Blocked specsuts/src/test/kotlin/io/ably/lib/uts/private_deviations.md
The 5 specs that could not be translated, with the reasons and a recommended path forward.

The 5 blocked specs

live_counter.md, live_map.md, object_id.md, objects_pool.md, parent_references.md assert on the
internal CRDT graph (live nodes + applyOperation, siteTimeserials, the ObjectsPool, object-id
generation, parent references). Two blockers:

  1. Not implemented yet (primary). :liveobjects currently ships only the public view layer; the
    CRDT engine these specs test does not exist (ObjectsPool, generateObjectId, applyOperation, … =
    0 references). There is nothing to assert against.
  2. Cross-module internal visibility (secondary). Once implemented, those symbols will be Kotlin
    internal and unreachable from the :uts test module at compile time.

private_deviations.md lays out the realistic options for when the engine lands — the recommended one
(to keep tests under uts/unit) is a java-test-fixtures bridge in :liveobjects exposing a small
public inspection surface; the lowest-ceremony alternative is authoring them in :liveobjects/src/test.

Notes / caveats

  • Translate-only: tests are not run in CI here; the gate is compilation. deviations.md entries that
    document SDK behaviour become live once the engine is implemented.
  • A few not-expressible cases (inputs the typed signatures reject at compile time) are intentionally
    documented no-op test bodies, paired with a // DEVIATION note + deviations.md entry.
  • LiveCounterApiTest / LiveMapApiTest contain a few wire-shape assertions read reflectively /
    via observable round-trip; these are unverified until the SDK publish path lands (flagged in the code).

How it was produced

Driven by the uts-to-kotlin skill (per-spec: read the objects mapping doc + infra, translate, compile
once for the whole module, review against the spec). The bulk batch was fanned out with a multi-agent
workflow (one agent per spec, parallel), then compiled and fixed centrally.

Testing

./gradlew :uts:compileTestKotlin   # BUILD SUCCESSFUL — all 10 classes compile

Update — integration & proxy tiers added

Extends this PR beyond the unit tier: the direct-sandbox integration and proxy integration objects specs are now translated. Still translate-only (compiles; runs once the LiveObjects engine lands and against the sandbox). With these, all non-blocked objects specs across all three tiers are translated — only the 5 internal-graph unit specs remain blocked (above).

Integration tier — direct sandbox (integration/standard/liveobjects/)

Real-sandbox happy path: connect → sync → create/mutate via PathObject → verify propagation to a second client; no fault injection. Each case runs once per protocol variant (JSON + msgpack) via a useBinaryProtocol @ParameterizedTest.

Spec (objects/integration/…) Test class Spec cases Spec points
objects_lifecycle_test.md ObjectsLifecycleTest 6 RTO23, RTPO15, RTPO17
objects_sync_test.md ObjectsSyncTest 4 RTO4, RTO5, RTO17

Clients hit the nonprod sandbox (sandbox.realtime.ably-nonprod.net via ProxyManager.sandboxRealtimeHost); a throwaway app is provisioned with SandboxApp in @BeforeAll/@AfterAll.

Proxy tier — sandbox + fault injection (integration/proxy/liveobjects/)

Routes the SDK through the programmable uts-proxy to inject transport faults (disconnect mid-OBJECT_SYNC, delayed sync, server-initiated DETACHED, channel ERRORFAILED). JSON-only (the proxy inspects only text frames).

Spec (objects/integration/proxy/…) Test class Spec cases Spec points
objects_faults.md ObjectsFaultsTest 5 RTO5a2, RTO7, RTO8, RTO17, RTO20e

Other changes

  • Integration REST fixture helperintegration/standard/liveobjects/helpers.kt: provisionObjectsViaRest(...) plus op/value builders, the ably-java translation of standard_test_pool.md's provision_objects_via_rest. Built to the V2 objects REST contract (POST /channels/{ch}/object, payload-key ops, no {messages} envelope), targeting the nonprod sandbox host via restHost. Used by RTPO15's rest-provisioned-data-sync case; compiles against :java only (public AblyRest).
  • junit-jupiter-params added to uts/build.gradle.kts — enables the protocol-variant @ParameterizedTest in the integration tier (version managed by the JUnit BOM already on the test classpath).
  • objects-mapping.md §14 — documents the integration REST-helper mapping in the uts-to-kotlin skill reference.

Related

The V2 + nonprod-host alignment of the UTS source spec is tracked upstream in ably/specification#497 (the provision_objects_via_rest host/shape and the objects integration client endpoints).

Testing

./gradlew :uts:compileTestKotlin   # BUILD SUCCESSFUL — unit + integration + proxy all compile

Summary by CodeRabbit

  • New Features

    • Expanded LiveObjects verification across sync lifecycle, object navigation, subscriptions, mutations (including counters and maps), and REST V2 provisioning scenarios.
    • Added new integration test coverage for reconnect, detach/reattach, and fault injection during syncing.
  • Bug Fixes

    • Improved guidance for asserting nested error-cause codes.
    • Refined test execution to exclude translate-only LiveObjects subsets while keeping them compiled.
  • Documentation

    • Updated LiveObjects spec-to-UTS guidance, deviation notes, and added notes for previously untranslatable (“private deviation”) cases.
    • Tightened the UTS generation “evaluate mode” fail-fast rules and templates.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 200f0132-167f-4ef0-b871-510bd7a3855e

📥 Commits

Reviewing files that changed from the base of the PR and between dcf733e and f255792.

📒 Files selected for processing (1)
  • .claude/skills/uts-to-kotlin/SKILL.md

Walkthrough

Adds LiveObjects Kotlin UTS unit and integration tests, REST provisioning helpers, deviation documentation updates, and Gradle exclusions for translate-only LiveObjects tests.

Changes

LiveObjects UTS Kotlin test suite

Layer / File(s) Summary
Deviation and private-deviation documentation
.claude/skills/uts-to-kotlin/references/objects-mapping.md, uts/src/test/kotlin/io/ably/lib/uts/deviations.md, uts/src/test/kotlin/io/ably/lib/uts/private_deviations.md
Adds deviation entries covering Instance, PathObject, LiveMap, LiveCounter, value types, and RealtimeObject spec points. Adds private_deviations.md documenting blocked specs requiring internal CRDT access. Updates objects-mapping.md with nested error.cause.code guidance and a new REST provisioning section.
Build: exclude LiveObjects tests from translate-only runner
uts/build.gradle.kts
Adds exclusion filters for LiveObjects unit and integration test packages to runUtsUnitTests and runUtsIntegrationTests Gradle tasks.
Unit: ValueTypesTest
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/ValueTypesTest.kt
Tests public construction of LiveCounter/LiveMap value types and table-driven LiveMapValue union discriminant/accessor mapping, with deviation placeholders for internal evaluation assertions.
Unit: PublicObjectMessageTest
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/PublicObjectMessageTest.kt
Tests wire ObjectMessage JSON-to-public field mapping for all operation variants (PAOM3/PAOOP3), plus a reflection-based helper for derived-create payloads.
Unit: InstanceTest
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/InstanceTest.kt
Tests typed id/value/get/entries/size/compactJson accessors, set/remove/increment/decrement delegation, fast-fail cast failures, and subscription event delivery/identity/unsubscribe.
Unit: LiveObjectSubscribeTest
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/LiveObjectSubscribeTest.kt
Tests listener registration, Subscription return, unsubscribe stopping delivery, noop suppression, tombstone deregistration, and InstanceSubscriptionEvent message field mapping.
Unit: LiveCounterApiTest
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/LiveCounterApiTest.kt
Tests counter value reads, increment/decrement local effects, outbound COUNTER_INC wire fields via reflection, subscription event ObjectMessage contents, and runtime rejection of NaN/Infinity.
Unit: LiveMapApiTest
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/LiveMapApiTest.kt
Tests get/size/entries/keys typed reads, set for all LiveMapValue types with ACK round-trip, remove effects, and invalid value type no-op documentation.
Unit: PathObjectTest
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/PathObjectTest.kt
Tests path dot-escaping, get/at navigation, typed value() conversions, collection accessors, compactJson() with cycle encoding via objectId markers, base64 bytes, and path-resolution failure cases.
Unit: PathObjectMutationsTest
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/PathObjectMutationsTest.kt
Tests set/remove/increment/decrement delegation, wrong-type cast failures (error code 92007), and unresolvable path failures (error code 92005).
Unit: PathObjectSubscribeTest
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/PathObjectSubscribeTest.kt
Tests RTPO19 subscription delivery, depth validation/filtering, unsubscribe, event payload correctness, MAP_CLEAR/bubbling, and RTO24 dispatch: prefix mismatch, candidate path construction, listener isolation, multi-path dispatch, and exactly-once firing.
Unit: RealtimeObjectTest
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/RealtimeObjectTest.kt
Tests root get(), mode enforcement, publishAndApply local-apply/SYNCING-wait/FAILED-failure, sync-state event sequences, access/write API preconditions, GC grace period via FakeClock, echo deduplication, siteTimeserials correctness, re-sync serial clearing, and subscription firing on ACK-apply.
Integration helpers: REST V2 provisioning
uts/src/test/kotlin/io/ably/lib/uts/integration/standard/liveobjects/Helpers.kt
Adds primitive value builders, operation builders (mapSet/mapRemove/mapCreate/counterCreate/counterInc), and provisionObjectsViaRest that POSTs to the sandbox REST V2 endpoint.
Integration: ObjectsSyncTest
uts/src/test/kotlin/io/ably/lib/uts/integration/standard/liveobjects/ObjectsSyncTest.kt
Parameterized integration tests (binary/text protocol) for RTO4/RTO5 attach-triggers-sync, two-client convergence, re-attach re-sync, and subscribe-only empty-pool assertions.
Integration: ObjectsLifecycleTest
uts/src/test/kotlin/io/ably/lib/uts/integration/standard/liveobjects/ObjectsLifecycleTest.kt
Parameterized integration tests for primitive/LiveCounter/LiveMap propagation between two clients, counter increment convergence, get() sync semantics, and sync of REST-provisioned pre-existing data.
Integration: ObjectsFaultsTest (proxy)
uts/src/test/kotlin/io/ably/lib/uts/integration/proxy/liveobjects/ObjectsFaultsTest.kt
Proxy fault-injection tests for sync interrupted by disconnect/reconnect, mutations buffered during re-sync, server-initiated detach re-sync, publishAndApply failure on FAILED channel with injected error cause, and publish-during-sync echo after delayed OBJECT_SYNC.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • ably/ably-java#1213: Implements the LiveObjects public API surface (PathObject, typed Instance/LiveMapValue, subscriptions, error codes) that this PR's unit and integration tests directly exercise.

Suggested reviewers

  • ttypic

🐇 Hoppity-hop, the tests are here,
PathObjects and counters, all crystal clear!
Deviations noted, the CRDT awaits,
Proxy faults injected through sandbox gates.
From REST provisioned maps to sync re-done—
The rabbit declares: green tests have won! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: translating LiveObjects objects UTS specs into Kotlin tests.
Docstring Coverage ✅ Passed Docstring coverage is 92.17% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/uts-liveobjects-unit-tests

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot temporarily deployed to staging/pull/1222/features June 25, 2026 21:00 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/javadoc June 25, 2026 21:01 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/features June 26, 2026 09:51 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/javadoc June 26, 2026 09:52 Inactive
@sacOO7 sacOO7 changed the title Generated unit tests using uts-to-kotlin skill Translate LiveObjects objects UTS unit specs to Kotlin (10/15 specs, 181 tests) Jun 26, 2026
@sacOO7 sacOO7 changed the title Translate LiveObjects objects UTS unit specs to Kotlin (10/15 specs, 181 tests) Translate LiveObjects objects UTS unit specs to Kotlin tests Jun 26, 2026
…s.md

Record the five objects/unit UTS specs (live_counter, live_map, object_id,
objects_pool, parent_references) that cannot be translated into the :uts
module. They assert on the internal CRDT graph (ObjectsPool, live nodes,
applyOperation, object-id generation, parent references), which is (a) not yet
implemented in :liveobjects and (b) Kotlin-`internal`, so unreachable from the
:uts test module at compile time.

Covers: status of all 15 objects/unit specs (10 translated, 5 blocked), why
these target internals, the two blockers, and the realistic visibility options
(java-test-fixtures bridge, tests in :liveobjects/src/test, or reflection) with
their trade-offs and a recommendation.
@sacOO7 sacOO7 force-pushed the feature/uts-liveobjects-unit-tests branch from 18a9d0c to 43a3ab2 Compare June 26, 2026 09:54
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/features June 26, 2026 09:55 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/javadoc June 26, 2026 09:56 Inactive
Add the integration-tier translation of standard_test_pool.md's REST fixture
provisioning (`provision_objects_via_rest`) under
uts/.../integration/standard/liveobjects/helpers.kt: `provisionObjectsViaRest`
plus op builders (mapSet/mapRemove/mapCreate/counterCreate/counterInc) and value
builders (string/number/boolean/bytes/objectId).

The helper follows the LiveObjects V2 objects REST API (per the OpenAPI), not the
legacy pseudocode: POST /channels/{channel}/object (singular), body is a single
operation or a bare array (no "messages" envelope), each op identified by its
payload key with an objectId/path target. Compiles against :java only
(AblyRest + HttpUtils); used by objects/integration/RTPO15.

Document it in the uts-to-kotlin skill's objects-mapping.md as a new section
("14. Integration-test helpers"), promoted out of the unit-only internal-graph
section, with the TOC renumbered accordingly.
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/features June 26, 2026 10:52 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/javadoc June 26, 2026 10:54 Inactive
@sacOO7 sacOO7 changed the title Translate LiveObjects objects UTS unit specs to Kotlin tests Translate LiveObjects objects UTS test specs to Kotlin tests Jun 26, 2026
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/features June 26, 2026 12:13 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/javadoc June 26, 2026 12:14 Inactive
sacOO7 added 2 commits June 30, 2026 00:03
Applied on top of the merge of feature/liveobjects-uts-tests:
- Point the liveobjects integration/proxy tests (ObjectsFaultsTest,
  ObjectsLifecycleTest, ObjectsSyncTest) at SandboxApp.sandboxHost,
  matching the consolidated sandbox-host constant.
- Update objects-mapping.md (§12 nested-cause, §14 sandbox host / V2 REST)
  and the unit liveobjects tests for the same refactor.
- Rename integration liveobjects helpers.kt -> Helpers.kt (case + content).

Compiles clean (:uts:compileTestKotlin). The liveobjects tests remain
translate-only at runtime (pending LiveObjects plugin wiring in the test
harness); the realtime tiers pass.
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/features June 29, 2026 18:55 Inactive
@sacOO7 sacOO7 changed the title Translate LiveObjects objects UTS test specs to Kotlin tests [AIT-1008] Translate LiveObjects objects UTS test specs to Kotlin tests Jun 29, 2026
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/javadoc June 29, 2026 18:56 Inactive
Base automatically changed from feature/liveobjects-uts-tests to feature/path-based-liveobjects-implementation June 30, 2026 10:29
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/features June 30, 2026 10:44 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/javadoc June 30, 2026 10:45 Inactive
Running the Step 7 faithfulness audit (audit_translation.py) across the objects
module surfaced three undocumented intentional omissions in the unit tests. Each
is benign but was not recorded as a deviation; add entries so the audit
reconciles clean:

- RTPO10/10d/11/11d — `keys()`/`values()` "IS Array" is a static-type tautology
  (typed Iterable return), so the type-check line is omitted; count/membership
  are asserted (PathObjectTest).
- RTO23f — `get()` result "IS PathObject" is guaranteed by the LiveMapPathObject
  return type; the runtime type-check is omitted (RealtimeObjectTest).
- RTLC12b/c/d — relocated to RTO26 with no executable spec content, so the marker
  entry is intentionally not translated (LiveCounterApiTest).

Integration and proxy objects tests audited clean; no deviations needed there.
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/features June 30, 2026 11:04 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/javadoc June 30, 2026 11:05 Inactive
…nted

The liveobjects SDK has no implementation yet, so the translate-only liveobjects
tests fail at runtime with "LiveObjects plugin hasn't been installed". Exclude
them from runUtsUnitTests and runUtsIntegrationTests (unit + integration standard
and proxy) so the CI gates pass; they still compile via compileTestKotlin.

Marked with a TODO to remove the exclusions once the liveobjects implementation
is in place.
@sacOO7 sacOO7 marked this pull request as ready for review June 30, 2026 11:08
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/features June 30, 2026 11:09 Inactive
@sacOO7 sacOO7 requested a review from Copilot June 30, 2026 11:10
@github-actions github-actions Bot temporarily deployed to staging/pull/1222/javadoc June 30, 2026 11:10 Inactive

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Translates the LiveObjects objects UTS specs into Kotlin tests under the :uts module (unit + standard integration + proxy integration), including documentation of deviations/blocked specs and minimal Gradle wiring to support the new test styles.

Changes:

  • Adds translated LiveObjects unit tests (io.ably.lib.uts.unit.liveobjects.*) plus deviations documentation and a write-up for currently blocked internal-graph specs.
  • Adds translated LiveObjects integration tests for direct sandbox and proxy fault-injection tiers, including a REST provisioning helper for seeding objects via the V2 REST contract.
  • Updates :uts Gradle configuration to support JUnit parameterized tests and to exclude translate-only LiveObjects tests from the tiered UTS tasks.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/ValueTypesTest.kt Unit tests for value-type construction and LiveMapValue union behaviour, with documented deviations where wire/evaluate internals aren’t public.
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/RealtimeObjectTest.kt Unit tests for RealtimeObject.get() preconditions, sync events, publish-and-apply observable effects, and GC behaviour (translate-only until engine lands).
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/PublicObjectMessageTest.kt Unit tests for public ObjectMessage/ObjectOperation construction, using reflection to reach internal wire/public conversion.
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/PathObjectTest.kt Unit tests covering read/navigation APIs on PathObject and snapshot (compactJson) behaviour.
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/PathObjectSubscribeTest.kt Unit tests for path subscriptions and depth/prefix dispatch rules (translate-only until engine lands).
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/PathObjectMutationsTest.kt Unit tests for map/counter mutation APIs via typed path views and expected error codes.
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/LiveObjectSubscribeTest.kt Unit tests for Instance.subscribe(...) delivery semantics and tombstone/noop adaptations.
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/LiveMapApiTest.kt Unit tests for LiveMap public API behaviour, asserting observable effects where wire-shape assertions are internal.
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/LiveCounterApiTest.kt Unit tests for LiveCounter public API behaviour; includes reflection helpers to inspect outbound wire objects.
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/InstanceTest.kt Unit tests for typed Instance API behaviour with deviations for cast-time failures vs spec error codes.
uts/src/test/kotlin/io/ably/lib/uts/private_deviations.md Documents the 5 unit specs blocked on missing internal CRDT engine + cross-module internal visibility.
uts/src/test/kotlin/io/ably/lib/uts/integration/standard/liveobjects/ObjectsSyncTest.kt Standard integration tests against sandbox for attach→sync→get and multi-client sync behaviours.
uts/src/test/kotlin/io/ably/lib/uts/integration/standard/liveobjects/ObjectsLifecycleTest.kt Standard integration tests for end-to-end lifecycle (create/mutate/propagate), including REST-provisioned seed case.
uts/src/test/kotlin/io/ably/lib/uts/integration/standard/liveobjects/Helpers.kt Integration helper implementing REST provisioning against the V2 objects REST API contract.
uts/src/test/kotlin/io/ably/lib/uts/integration/proxy/liveobjects/ObjectsFaultsTest.kt Proxy integration tests injecting transport/channel faults during sync/mutation flows.
uts/src/test/kotlin/io/ably/lib/uts/deviations.md Adds detailed deviation entries for LiveObjects UTS translations.
uts/build.gradle.kts Adds JUnit params dependency and excludes translate-only LiveObjects tests from tiered UTS tasks.
.claude/skills/uts-to-kotlin/references/objects-mapping.md Updates the mapping reference doc with integration REST provisioning helper guidance and error-cause notes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread uts/build.gradle.kts
Comment on lines 55 to +59
tasks.register<Test>("runUtsIntegrationTests") {
filter {
includeTestsMatching("io.ably.lib.uts.integration.*")
// liveobjects has no SDK implementation yet — exclude the translate-only liveobjects
// tests (standard + proxy) from the run; they still compile via compileTestKotlin.
Comment on lines +123 to +153
val rest = AblyRest(
ClientOptions().apply {
key = apiKey
// Target the same nonprod sandbox host that SandboxApp and the realtime clients use
// (sandbox.realtime.ably-nonprod.net) — NOT environment="sandbox", which resolves to the
// legacy prod-sandbox host sandbox-rest.ably.io (Hosts.java also forbids setting both
// environment and restHost). Matches standard_test_pool.md (ably/specification#497).
restHost = SandboxApp.sandboxHost
useBinaryProtocol = false
},
)

val path = "/channels/$channelName/object" // V2: singular `object`
val body: JsonElement =
if (operations.size == 1) operations[0] else JsonArray().apply { operations.forEach { add(it) } }

val response = rest.request(
"POST",
path,
null,
HttpUtils.requestBodyFromGson(body, rest.options.useBinaryProtocol),
null,
)
check(response.success) {
"REST objects provisioning failed: HTTP ${response.statusCode} ${response.errorMessage}"
}

return response.items().flatMap { item ->
item.asJsonObject.get("objectIds")?.asJsonArray?.map { it.asString } ?: emptyList()
}
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🧹 Nitpick comments (1)
uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/PublicObjectMessageTest.kt (1)

225-246: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid hard-coding internal wire action ordinals here.

0 and 3 are protocol-internal encodings. If they drift, toObjectMessage(...) will deserialize the wrong variant and these tests stop exercising the derived-create path. Derive the action value from the builder-generated direct-create JSON (or a shared helper) so this synthetic payload stays coupled to the same wire contract.

Also applies to: 262-280

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/PublicObjectMessageTest.kt`
around lines 225 - 246, The synthetic payload in the derived-create tests is
hard-coding protocol wire ordinals for the operation action, which can desync
from the real contract. Update the test setup around
publicMessageWithDerivedCreate and the related cases in the same block to source
the action value from builder-generated direct-create JSON or a shared helper
instead of literals. Keep the derived-create payload coupled to the same JSON
shape used by the production builder so toObjectMessage(...) continues to
exercise the intended variant even if the wire encoding changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@uts/src/test/kotlin/io/ably/lib/uts/integration/proxy/liveobjects/ObjectsFaultsTest.kt`:
- Around line 141-147: The test currently publishes the mutation while clientB
is fully disconnected, so it does not exercise buffering during the re-sync
window. Update ObjectsFaultsTest to keep clientB in the SYNCING state after
reconnecting, then publish rootA.set(...) before the delayed OBJECT_SYNC
completes, and only assert on rootB after the sync finishes. Use the existing
clientB, rootA, rootB, awaitState, and objectChannel flow to make the timing
explicit.
- Around line 212-230: The current test only injects the channel error after
`channel.`object`.get().await()` has already finished syncing, so it never
exercises the `FAILED during SYNCING` case. Update `ObjectsFaultsTest` around
the `root = ...get().await()` / `session.triggerAction(...)` flow so the failure
is injected while the `object` sync is still in progress, and assert that the
`root.set(...)` or `publishAndApply` path is interrupted by the transition to
`ChannelState.failed` during sync rather than after sync has completed.
- Around line 271-275: The test in ObjectsFaultsTest should wait for client B to
actually enter the attach/sync window before calling rootA.set, because
publishing immediately after channelB.attach can race and hide the buffering
behavior. Add a synchronization barrier after channelB.attach in the test flow,
using the existing attach/sync path around objectChannel and channelB so the
mutation is sent only once B is confirmed to be syncing. Keep the assertion
logic unchanged, but ensure the mutation happens while B is genuinely mid-sync.

In
`@uts/src/test/kotlin/io/ably/lib/uts/integration/standard/liveobjects/Helpers.kt`:
- Around line 52-58: The `operation()` helper currently permits both `objectId`
and `path` to be set, or neither, which can produce invalid fixtures. Update the
`operation(objectId, path, id, build)` builder to validate that exactly one REST
target is provided before constructing the `JsonObject`, and fail fast if the
inputs violate that rule. Use the existing `operation` helper as the enforcement
point so all callers inherit the V2 contract (`objectId` XOR `path`).

In `@uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/InstanceTest.kt`:
- Around line 333-347: The current test in InstanceTest is not proving
identity-based routing because the final id assertion can pass even if the
callback fired for the repoint operation instead of the original counter. Update
the assertions around the events list to validate the delivered event itself,
using the event payload from the listener (such as events[0].getObject() and/or
events[0].getMessage().operation.objectId), and make the test explicitly verify
that the first MAP_SET on root.score did not trigger a callback before the
counter increment does.
- Around line 309-320: The unsubscribe test in `RTINS16f - subscribe returns
Subscription for deregistration` is asserting `events.size == 0` immediately
after `mockWs.sendToClient(...)`, which can miss late asynchronous delivery.
Update the test to wait through the dispatch window in `runTest` before the
final assertion, using an observable processing milestone or a short wait, so it
verifies no event is delivered after `sub.unsubscribe()`. Keep the check tied to
`InstanceSubscriptionEvent`, `sub.unsubscribe()`, and the `events` list.

In
`@uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/LiveObjectSubscribeTest.kt`:
- Around line 77-88: The negative subscription checks in LiveObjectSubscribeTest
are racing the dispatcher because they only wait for the first update before
asserting no more deliveries. Update the unsubscribe/noop/tombstone assertions
in the affected test cases to wait for the relevant callbacks to settle,
especially both listeners in the tombstone flow, using the existing helpers
around updates and pollUntil so the counts remain stable for a short window
before asserting no further messages were delivered.

In
`@uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/PathObjectSubscribeTest.kt`:
- Around line 126-130: The negative subscription checks in
PathObjectSubscribeTest are racy because the test asserts listener counts
immediately after async sends, so late callbacks can arrive after the assertion.
Update the affected scenarios to use the existing pollUntil(...) pattern or an
equivalent bounded quiet-period wait after mockWs.sendToClient and before
checking events.size, covering the depth-filtering, unsubscribe,
prefix-mismatch, and exactly-once assertions. Keep the assertions in the same
test methods but ensure the count stays unchanged for a short stable period
before validating it.

In `@uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/RealtimeObjectTest.kt`:
- Around line 941-944: The re-sync assertion in RealtimeObjectTest is using the
wrong serial/site, so it does not actually verify the apply-on-ACK clearing
behavior. Update the replay in the test to reuse the same ACK serial and site
identifier that were used earlier when the mutation was applied on ACK, and keep
the check in the same test flow around buildObjectMessage/buildCounterInc so it
fails if appliedOnAckSerials is not cleared during re-sync.
- Around line 429-438: The unsubscribe/resubscribe check in RealtimeObjectTest
is asserting too early, so a late callback could still arrive after the test
passes. Update the test around the mocked sync messages and the callCount
assertion to wait for async delivery to settle, using the existing
pollUntil(...) pattern already used elsewhere in this file, and keep the
assertion on callCount in place only after that drain.
- Around line 329-347: The test setup in RTO20e1 is sending a detached protocol
event even though the scenario is meant to cover a channel entering FAILED
during sync wait. Update the stimulus in `RealtimeObjectTest` to use the FAILED
state transition instead of `ProtocolMessage.Action.detached`, and keep the
existing assertions around `publishAndApply` so the test truly exercises
failed-state handling in
`setupSyncedChannel`/`root.get("score").asLiveCounter().increment(10)`.
- Around line 888-892: The siteTimeserials replay test is using a different
tuple than the helper’s ACK tuple, so it can miss a bug in apply-on-ACK updates.
In RealtimeObjectTest, update the inbound COUNTER_INC in the siteTimeserials
scenario to replay the actual ACK tuple used by the sibling ACK tests and the
helper’s ACK path, keeping the message aligned with the ACK that was already
seen. Use the existing test helpers and identifiers around buildObjectMessage,
buildCounterInc, and the siteTimeserials assertions to locate and adjust the
message payload.
- Around line 304-322: The test `RTO20e - publishAndApply waits for SYNCED
during SYNCING` only checks the final post-sync result, so it does not verify
that `increment(10)` was actually deferred while the channel was in `SYNCING`.
In `RealtimeObjectTest`, after calling
`root.get("score").asLiveCounter().increment(10)` and before sending the sync
message, add an assertion that the returned future is still not completed and/or
that the counter value is still the pre-sync value. Keep the existing
`incFuture` and `buildObjectSyncMessage` flow, but insert the pre-sync
pending-state check so the test proves `publishAndApply` waits for `SYNCED`.
- Around line 1029-1033: The “initial attach” scenario in RealtimeObjectTest is
starting from a pre-synced channel, so it never exercises a real first-time
attach path. Update the scenario setup in the relevant Scenario list (including
the one reused in the later block) so the “initial attach” case begins from an
unattached/unsynced channel state, then calls channel.attach() and validates the
expected SYNCING/SYNCED events from that true initial attach flow.
- Around line 191-199: The `RealtimeObjectTest` case is a documented deviation
that cannot be exercised through the public API, so it should not remain a
passing `@Test`. Update the `RTO15 - publish sends OBJECT ProtocolMessage`
method to be ignored/disabled, or remove the test annotation entirely and keep
the note only in the deviation documentation, so the test matrix does not report
a false green for an unexpressible scenario.

---

Nitpick comments:
In
`@uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/PublicObjectMessageTest.kt`:
- Around line 225-246: The synthetic payload in the derived-create tests is
hard-coding protocol wire ordinals for the operation action, which can desync
from the real contract. Update the test setup around
publicMessageWithDerivedCreate and the related cases in the same block to source
the action value from builder-generated direct-create JSON or a shared helper
instead of literals. Keep the derived-create payload coupled to the same JSON
shape used by the production builder so toObjectMessage(...) continues to
exercise the intended variant even if the wire encoding changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0e7a322c-f3d2-4391-bda5-27803981543b

📥 Commits

Reviewing files that changed from the base of the PR and between e2ad9b5 and dcf733e.

📒 Files selected for processing (18)
  • .claude/skills/uts-to-kotlin/references/objects-mapping.md
  • uts/build.gradle.kts
  • uts/src/test/kotlin/io/ably/lib/uts/deviations.md
  • uts/src/test/kotlin/io/ably/lib/uts/integration/proxy/liveobjects/ObjectsFaultsTest.kt
  • uts/src/test/kotlin/io/ably/lib/uts/integration/standard/liveobjects/Helpers.kt
  • uts/src/test/kotlin/io/ably/lib/uts/integration/standard/liveobjects/ObjectsLifecycleTest.kt
  • uts/src/test/kotlin/io/ably/lib/uts/integration/standard/liveobjects/ObjectsSyncTest.kt
  • uts/src/test/kotlin/io/ably/lib/uts/private_deviations.md
  • uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/InstanceTest.kt
  • uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/LiveCounterApiTest.kt
  • uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/LiveMapApiTest.kt
  • uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/LiveObjectSubscribeTest.kt
  • uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/PathObjectMutationsTest.kt
  • uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/PathObjectSubscribeTest.kt
  • uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/PathObjectTest.kt
  • uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/PublicObjectMessageTest.kt
  • uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/RealtimeObjectTest.kt
  • uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/ValueTypesTest.kt

Comment on lines +141 to +147
// While B is disconnected, A publishes a mutation
rootA.set("key1", LiveMapValue.of("updated_during_disconnect")).await()

// Client B reconnects and re-syncs; the mutation should be visible
awaitState(clientB, ConnectionState.connected, 30.seconds)
rootB = withTimeout(15.seconds) { objectChannel(clientB, channelName).`object`.get().await() }
pollUntil(15.seconds) { rootB.get("key1").asString().value() == "updated_during_disconnect" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

This never publishes while client B is re-syncing.

At Line 142 the mutation is sent while clientB is fully disconnected. By the time B reconnects, the new value can arrive entirely via the next OBJECT_SYNC snapshot, so this test can pass even if buffering during the re-sync window is broken. Hold B in SYNCING after reconnect, then publish from A before the delayed sync completes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@uts/src/test/kotlin/io/ably/lib/uts/integration/proxy/liveobjects/ObjectsFaultsTest.kt`
around lines 141 - 147, The test currently publishes the mutation while clientB
is fully disconnected, so it does not exercise buffering during the re-sync
window. Update ObjectsFaultsTest to keep clientB in the SYNCING state after
reconnecting, then publish rootA.set(...) before the delayed OBJECT_SYNC
completes, and only assert on rootB after the sync finishes. Use the existing
clientB, rootA, rootB, awaitState, and objectChannel flow to make the timing
explicit.

Comment on lines +212 to +230
val channel = objectChannel(client, channelName)
val root = withTimeout(15.seconds) { channel.`object`.get().await() }

// Inject a channel ERROR (action 9) to transition the channel to FAILED.
session.triggerAction(
mapOf(
"type" to "inject_to_client",
"message" to mapOf(
"action" to 9,
"channel" to channelName,
"error" to mapOf("statusCode" to 400, "code" to 90000, "message" to "injected error"),
),
),
)
awaitChannelState(channel, ChannelState.failed, 15.seconds)

// A mutation (publishAndApply internally) must fail since the channel is FAILED.
val error = assertFailsWith<AblyException> {
root.set("key", LiveMapValue.of("value")).await()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

The FAILED during SYNCING path is not exercised here.

channel.object.get().await() at Line 213 only resolves after sync has completed, so the injected channel error at Lines 216-225 happens after the SYNCING phase. This still checks "mutations fail on a FAILED channel", but it does not cover the RTO20e timing where publishAndApply is interrupted by a transition to FAILED during sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@uts/src/test/kotlin/io/ably/lib/uts/integration/proxy/liveobjects/ObjectsFaultsTest.kt`
around lines 212 - 230, The current test only injects the channel error after
`channel.`object`.get().await()` has already finished syncing, so it never
exercises the `FAILED during SYNCING` case. Update `ObjectsFaultsTest` around
the `root = ...get().await()` / `session.triggerAction(...)` flow so the failure
is injected while the `object` sync is still in progress, and assert that the
`root.set(...)` or `publishAndApply` path is interrupted by the transition to
`ChannelState.failed` during sync rather than after sync has completed.

Comment on lines +271 to +275
val channelB = objectChannel(clientB, channelName)
channelB.attach()

// While B is syncing, A publishes a mutation.
rootA.set("existing", LiveMapValue.of("after")).await()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Wait until client B is actually in the attach/sync window before publishing.

The mutation at Line 275 is sent immediately after channelB.attach(). If that write wins the race before B's attach is established, the "after" value can be folded into the initial sync snapshot and this test still passes without proving the echo was buffered until sync completion. Add a barrier before publishing.

Proposed fix
             clientB.connect()
             awaitState(clientB, ConnectionState.connected, 15.seconds)
             val channelB = objectChannel(clientB, channelName)
             channelB.attach()
+            awaitChannelState(channelB, ChannelState.attached, 15.seconds)
 
             // While B is syncing, A publishes a mutation.
             rootA.set("existing", LiveMapValue.of("after")).await()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val channelB = objectChannel(clientB, channelName)
channelB.attach()
// While B is syncing, A publishes a mutation.
rootA.set("existing", LiveMapValue.of("after")).await()
val channelB = objectChannel(clientB, channelName)
channelB.attach()
awaitChannelState(channelB, ChannelState.attached, 15.seconds)
// While B is syncing, A publishes a mutation.
rootA.set("existing", LiveMapValue.of("after")).await()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@uts/src/test/kotlin/io/ably/lib/uts/integration/proxy/liveobjects/ObjectsFaultsTest.kt`
around lines 271 - 275, The test in ObjectsFaultsTest should wait for client B
to actually enter the attach/sync window before calling rootA.set, because
publishing immediately after channelB.attach can race and hide the buffering
behavior. Add a synchronization barrier after channelB.attach in the test flow,
using the existing attach/sync path around objectChannel and channelB so the
mutation is sent only once B is confirmed to be syncing. Keep the assertion
logic unchanged, but ensure the mutation happens while B is genuinely mid-sync.

Comment on lines +52 to +58
private fun operation(objectId: String?, path: String?, id: String?, build: JsonObject.() -> Unit): JsonObject =
JsonObject().apply {
id?.let { addProperty("id", it) } // OperationBase.id — idempotency key
objectId?.let { addProperty("objectId", it) }
path?.let { addProperty("path", it) }
build()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Enforce exactly one REST target here.

The V2 contract in this file is objectId or path, but operation() currently allows both or neither. That makes every public builder capable of emitting malformed fixtures and pushes the failure into the REST call instead of failing fast at the helper boundary.

Proposed fix
-private fun operation(objectId: String?, path: String?, id: String?, build: JsonObject.() -> Unit): JsonObject =
-    JsonObject().apply {
-        id?.let { addProperty("id", it) } // OperationBase.id — idempotency key
-        objectId?.let { addProperty("objectId", it) }
-        path?.let { addProperty("path", it) }
-        build()
-    }
+private fun operation(objectId: String?, path: String?, id: String?, build: JsonObject.() -> Unit): JsonObject {
+    require((objectId == null) != (path == null)) {
+        "exactly one of objectId or path must be provided"
+    }
+    return JsonObject().apply {
+        id?.let { addProperty("id", it) } // OperationBase.id — idempotency key
+        objectId?.let { addProperty("objectId", it) }
+        path?.let { addProperty("path", it) }
+        build()
+    }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun operation(objectId: String?, path: String?, id: String?, build: JsonObject.() -> Unit): JsonObject =
JsonObject().apply {
id?.let { addProperty("id", it) } // OperationBase.id — idempotency key
objectId?.let { addProperty("objectId", it) }
path?.let { addProperty("path", it) }
build()
}
private fun operation(objectId: String?, path: String?, id: String?, build: JsonObject.() -> Unit): JsonObject {
require((objectId == null) != (path == null)) {
"exactly one of objectId or path must be provided"
}
return JsonObject().apply {
id?.let { addProperty("id", it) } // OperationBase.id — idempotency key
objectId?.let { addProperty("objectId", it) }
path?.let { addProperty("path", it) }
build()
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@uts/src/test/kotlin/io/ably/lib/uts/integration/standard/liveobjects/Helpers.kt`
around lines 52 - 58, The `operation()` helper currently permits both `objectId`
and `path` to be set, or neither, which can produce invalid fixtures. Update the
`operation(objectId, path, id, build)` builder to validate that exactly one REST
target is provided before constructing the `JsonObject`, and fail fast if the
inputs violate that rule. Use the existing `operation` helper as the enforcement
point so all callers inherit the V2 contract (`objectId` XOR `path`).

Comment on lines +309 to +320
fun `RTINS16f - subscribe returns Subscription for deregistration`() = runTest {
val (_, _, root, mockWs) = setupSyncedChannel("test")
val counterInst = root.get("score").instance()!!.asLiveCounter()
val events = mutableListOf<InstanceSubscriptionEvent>()
val sub = counterInst.subscribe(InstanceListener { events.add(it) })
sub.unsubscribe()

mockWs.sendToClient(
buildObjectMessage("test", listOf(buildCounterInc("counter:score@1000", 7, "99", "remote"))),
)

assertEquals(0, events.size)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Wait through the dispatch window before concluding unsubscribe worked.

After sendToClient(...) this test asserts events.size == 0 immediately. If callback delivery is asynchronous, it can pass even when unsubscribe is broken and the event arrives a tick later. Gate the final assertion on some observable processing milestone, or hold the count steady across a short wait before asserting no delivery.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/InstanceTest.kt` around
lines 309 - 320, The unsubscribe test in `RTINS16f - subscribe returns
Subscription for deregistration` is asserting `events.size == 0` immediately
after `mockWs.sendToClient(...)`, which can miss late asynchronous delivery.
Update the test to wait through the dispatch window in `runTest` before the
final assertion, using an observable processing milestone or a short wait, so it
verifies no event is delivered after `sub.unsubscribe()`. Keep the check tied to
`InstanceSubscriptionEvent`, `sub.unsubscribe()`, and the `events` list.

Comment on lines +329 to +347
fun `RTO20e1 - publishAndApply fails when channel enters FAILED during sync wait`() = runTest {
val (_, _, root, mockWs) = setupSyncedChannel("test")

mockWs.sendToClient(
ProtocolMessage(ProtocolMessage.Action.attached).apply {
this.channel = "test"
channelSerial = "sync2:cursor"
setFlag(ProtocolMessage.Flag.has_objects)
},
)

val incFuture = root.get("score").asLiveCounter().increment(10)

mockWs.sendToClient(
ProtocolMessage(ProtocolMessage.Action.detached).apply {
this.channel = "test"
error = ErrorInfo("Channel detached", 400, 90000)
},
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The stimulus here is DETACHED, not FAILED.

The test name/spec says “channel enters FAILED”, but Line 343 injects ProtocolMessage.Action.detached. That only exercises detach-while-syncing, so a regression in failed-state handling would be missed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/RealtimeObjectTest.kt`
around lines 329 - 347, The test setup in RTO20e1 is sending a detached protocol
event even though the scenario is meant to cover a channel entering FAILED
during sync wait. Update the stimulus in `RealtimeObjectTest` to use the FAILED
state transition instead of `ProtocolMessage.Action.detached`, and keep the
existing assertions around `publishAndApply` so the test truly exercises
failed-state handling in
`setupSyncedChannel`/`root.get("score").asLiveCounter().increment(10)`.

Comment on lines +429 to +438
mockWs.sendToClient(
ProtocolMessage(ProtocolMessage.Action.attached).apply {
this.channel = "test"
channelSerial = "sync2:cursor"
setFlag(ProtocolMessage.Flag.has_objects)
},
)
mockWs.sendToClient(buildObjectSyncMessage("test", "sync2:", STANDARD_POOL_OBJECTS))

assertEquals(0, callCount)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Wait long enough to prove the listener stays unsubscribed.

This assertion runs immediately after queueing the re-sync messages. Elsewhere in this file you already use pollUntil(...) for async delivery; without a similar drain/wait here, a late buggy callback can fire after the test has already passed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/RealtimeObjectTest.kt`
around lines 429 - 438, The unsubscribe/resubscribe check in RealtimeObjectTest
is asserting too early, so a late callback could still arrive after the test
passes. Update the test around the mocked sync messages and the callCount
assertion to wait for async delivery to settle, using the existing
pollUntil(...) pattern already used elsewhere in this file, and keep the
assertion on callCount in place only after that drain.

Comment on lines +888 to +892
// Inbound COUNTER_INC from siteCode "test" with serial "t:1:0" (same as the ACK). If LOCAL had
// incorrectly written siteTimeserials, the newness check would reject this as stale.
mockWs.sendToClient(
buildObjectMessage("test", listOf(buildCounterInc("counter:score@1000", 10, "t:1:0", "test"))),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Replay the actual ACK tuple in the siteTimeserials test.

This comment says the inbound message uses “the same as the ACK”, but sibling ACK tests in this file use the helper’s ACK tuple (ack-0:0 / test-site), while this test injects t:1:0 / test. Because the tuple is different, the assertion still passes even if apply-on-ACK incorrectly updates siteTimeserials.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/RealtimeObjectTest.kt`
around lines 888 - 892, The siteTimeserials replay test is using a different
tuple than the helper’s ACK tuple, so it can miss a bug in apply-on-ACK updates.
In RealtimeObjectTest, update the inbound COUNTER_INC in the siteTimeserials
scenario to replay the actual ACK tuple used by the sibling ACK tests and the
helper’s ACK path, keeping the message aligned with the ACK that was already
seen. Use the existing test helpers and identifiers around buildObjectMessage,
buildCounterInc, and the siteTimeserials assertions to locate and adjust the
message payload.

Comment on lines +941 to +944
// Replay the same serial used for apply-on-ACK. If cleared, this applies normally.
mockWs.sendToClient(
buildObjectMessage("test", listOf(buildCounterInc("counter:score@1000", 10, "t:1:0", "test"))),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This re-sync check is replaying the wrong serial.

The test claims to replay “the same serial used for apply-on-ACK”, but it injects t:1:0 / test instead of the ACK serial/site used earlier. If appliedOnAckSerials were never cleared on re-sync, this message would still apply, so the regression would go undetected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/RealtimeObjectTest.kt`
around lines 941 - 944, The re-sync assertion in RealtimeObjectTest is using the
wrong serial/site, so it does not actually verify the apply-on-ACK clearing
behavior. Update the replay in the test to reuse the same ACK serial and site
identifier that were used earlier when the mutation was applied on ACK, and keep
the check in the same test flow around buildObjectMessage/buildCounterInc so it
fails if appliedOnAckSerials is not cleared during re-sync.

Comment on lines +1029 to +1033
Scenario(
name = "initial attach",
trigger = { channel, _ -> channel.attach() },
expectedEvents = listOf("SYNCING", "SYNCED"),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The “initial attach” scenario starts from an already-synced channel.

Each iteration uses setupSyncedChannel("test"), so the "initial attach" case never exercises an actual initial attach. Calling channel.attach() from that state is either a no-op or a different path, which makes this scenario unable to validate the event sequence it names.

Also applies to: 1079-1088

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@uts/src/test/kotlin/io/ably/lib/uts/unit/liveobjects/RealtimeObjectTest.kt`
around lines 1029 - 1033, The “initial attach” scenario in RealtimeObjectTest is
starting from a pre-synced channel, so it never exercises a real first-time
attach path. Update the scenario setup in the relevant Scenario list (including
the one reused in the later block) so the “initial attach” case begins from an
unattached/unsynced channel state, then calls channel.attach() and validates the
expected SYNCING/SYNCED events from that true initial attach flow.

Align the skill's Step 6 with the spec manual (ably/specification#498) so a
diagnosed UTS spec error is handled spec-first instead of being quietly
adapted to green:

- Step 6 intro now names three acceptable end-states (spec-correct pass,
  SDK deviation stays green, UTS spec error fails fast) — removes the
  earlier "a red test is never acceptable" contradiction.
- Decision tree NO-branch: fix the spec at source + record under
  deviations.md "UTS Spec Errors" + emit a fail-fast test.
- Add the "spec-error fail-fast" Kotlin pattern (mirrors the manual's JS
  example) and retitle the section to "Test patterns for a diagnosed
  failure" since fail-fast is not a deviation.
- Defer the deviations-file entry format to the manual's "Recording
  deviations" sections (removes the divergent inline field list).
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