Skip to content

Add UTS test specs for LiveObjects path-based API#473

Draft
paddybyers wants to merge 3 commits into
mainfrom
uts-liveobjects
Draft

Add UTS test specs for LiveObjects path-based API#473
paddybyers wants to merge 3 commits into
mainfrom
uts-liveobjects

Conversation

@paddybyers
Copy link
Copy Markdown
Member

Summary

  • Adds portable UTS test specs for the LiveObjects path-based API (~330 tests across 21 files)
  • Covers PathObject, Instance, batch operations, LiveCounter/LiveMap CRDT internals, ObjectsPool sync state machine, RealtimeObject lifecycle, value types, subscriptions, and integration scenarios
  • Specs are language-independent pseudocode at uts/objects/
  • Corresponding ably-js translations: Add UTS tests for LiveObjects path-based API ably-js#2219

Test plan

  • ably-js translations of these specs pass (290 tests, 1 pending for unimplemented LiveMap#clear())

🤖 Generated with Claude Code

@github-actions github-actions Bot temporarily deployed to staging/pull/473 May 13, 2026 12:52 Inactive
@paddybyers paddybyers requested a review from ttypic May 13, 2026 13:48
@github-actions github-actions Bot temporarily deployed to staging/pull/473 May 14, 2026 07:18 Inactive
@paddybyers paddybyers force-pushed the uts-integration-proxy branch from f355624 to 2e12074 Compare May 14, 2026 20:17
Base automatically changed from uts-integration-proxy to main May 14, 2026 20:18
@github-actions github-actions Bot temporarily deployed to staging/pull/473 May 14, 2026 20:36 Inactive
@paddybyers paddybyers requested a review from mschristensen May 15, 2026 13:58
paddybyers and others added 3 commits May 28, 2026 14:08
Complete portable test suite covering the LiveObjects path-based API:
21 files across unit tests (pure + mock WebSocket), integration tests
(sandbox), and proxy integration tests. Covers PathObject, Instance,
BatchContext, LiveCounter/LiveMap CRDTs, ObjectsPool sync state machine,
value types, subscriptions, and GC. Includes table-driven validation
tests, bytes/binary data coverage, and REST fixture provisioning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove client-side allocated_port/port_base patterns from all proxy
test specs and helper docs. Port is now auto-assigned by the proxy
when omitted from create_proxy_session(). Matches uts-proxy v0.2.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Align all ~330 LiveObjects UTS test specs with the squashed spec revision
a397e34 (LiveObjects path-based API). Key changes:

- Add parent_references.md (20 tests): RTLO3f, RTLO4g/4h, RTLO4f, RTO5c10
- Add public_object_message.md (13 tests): PAOM1-3, PAOOP1-3
- Thread ObjectMessage through all CRDT operations and LiveObjectUpdate
- Add RTO25 (access preconditions) and RTO26 (write preconditions)
- Update subscription model: subscribe returns Subscription object
- Add RTO24 (PathObjectSubscriptionRegister) dispatch tests
- Add parentReferences maintenance tests to live_map.md (+8 tests)
- Add post-sync parentReferences rebuild tests to objects_pool.md (+3)
- Rename "consume"/"consumption" to "evaluate"/"evaluation" in value_types
- Remove batch.md (Batch API deferred from current spec revision)
- Remove subscribeIterator and LiveObject#unsubscribe tests
- Update PLAN.md to reflect new file structure and test counts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ASSERT attach_frames.length == 1
```

## Protocol Message Action Numbers
Copy link
Copy Markdown
Collaborator

@sacOO7 sacOO7 Jun 1, 2026

Choose a reason for hiding this comment

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

Need to add message action for OBJECT RTO8 and OBJECT_SYNC RTO4a
Also need to double check updating uts-proxy implementation to accommodate for injecting Objects messages specific faults

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

All message actions available at TR2

@@ -157,7 +157,6 @@ Proxy tests additionally set up a proxy session per test or group of tests. See
BEFORE EACH TEST:
session = create_proxy_session(
endpoint: "nonprod:sandbox",
Copy link
Copy Markdown
Collaborator

@sacOO7 sacOO7 Jun 1, 2026

Choose a reason for hiding this comment

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

The endpoint client option is not available in ably-java, and I'm not sure whether it's supported in ably-swift ( Open PR for ably-cocoa endpoint clientOption ).

As far as I know, endpoint is only available in ably-js and ably-go ( I contributed to the ably-go PR that added support for it).

To ensure broader SDK compatibility, we should explicitly configure both restHost and realtimeHost instead of relying on endpoint.

Copy link
Copy Markdown
Collaborator

@sacOO7 sacOO7 Jun 1, 2026

Choose a reason for hiding this comment

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

This needs to be updated at all places wherever endpoint is used

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Or we should add endpoint support to both ably-java and ably-cocoa ( PR already open )

Copy link
Copy Markdown
Collaborator

@sacOO7 sacOO7 Jun 2, 2026

Choose a reason for hiding this comment

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

It was decided in the standup to use old restHost and realtimeHost in the SDKs if endpoint clientOption doesn't exist.
This will be added for the given SDK uts-to-tests claude skill.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(Optional) We can add it as a note somewhere in uts/README.md

@@ -20,7 +20,6 @@
# 1. Create a proxy session with rules
session = create_proxy_session(
endpoint: "nonprod:sandbox",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Similar to #473 (comment)

@@ -0,0 +1,201 @@
# Objects Batch Integration Tests

Spec points: `RTPO22`, `RTBC12`–`RTBC15`
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Are we planning to add UTS test specs for in progress PR for LiveObjects BatchContext -> https://github.com/ably/specification/pull/471/changes.
Currently, PR for spec points RTBC12RTBC15 not merged yet : (

Copy link
Copy Markdown
Collaborator

@sacOO7 sacOO7 Jun 2, 2026

Choose a reason for hiding this comment

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

As per standup discussion, we will only respect liveobjects spec landed on the main branch. So, we should get rid of uts/objects/integration/objects_batch_test.md.

Copy link
Copy Markdown
Collaborator

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

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

  • We also need to update Spec File Counts section at uts/README.md.
  • Looks like the Directory structure section needs an update as well. We could potentially remove it altogether—I’m not sure it serves much purpose anymore. WDYT?


```pseudo
BEFORE ALL TESTS:
response = POST https://sandbox-rest.ably.io/apps
Copy link
Copy Markdown
Collaborator

@sacOO7 sacOO7 Jun 2, 2026

Choose a reason for hiding this comment

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

Blocker — wrong sandbox host.

https://sandbox-rest.ably.io is not the host the UTS suite uses. Every other integration file in the repo (and uts/docs/integration-testing.md) provisions apps against https://sandbox.realtime.ably-nonprod.net. The same string also appears at L16 and L30 in this file.

Convention: uts/docs/integration-testing.md#sandbox-setupBEFORE ALL TESTS: response = POST https://sandbox.realtime.ably-nonprod.net/apps.

Reasoning: as-is this test would either 404 or hit an unintended environment. The proxy integration file uts/objects/integration/proxy/objects_faults.md already uses the correct host, so the PR is internally inconsistent.

Proposed fix: replace all three occurrences (L16, L22/L23, L30) of https://sandbox-rest.ably.io with https://sandbox.realtime.ably-nonprod.net.


```pseudo
BEFORE ALL TESTS:
response = POST https://sandbox-rest.ably.io/apps
Copy link
Copy Markdown
Collaborator

@sacOO7 sacOO7 Jun 2, 2026

Choose a reason for hiding this comment

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

Similar to above comment #473 (comment)

Convention: uts/docs/integration-testing.md#sandbox-setupBEFORE ALL TESTS: response = POST https://sandbox.realtime.ably-nonprod.net/apps.

Proposed fix: replace all three occurrences (L16, L22/L23, L30) of https://sandbox-rest.ably.io with https://sandbox.realtime.ably-nonprod.net.


```pseudo
BEFORE ALL TESTS:
response = POST https://sandbox-rest.ably.io/apps
Copy link
Copy Markdown
Collaborator

@sacOO7 sacOO7 Jun 2, 2026

Choose a reason for hiding this comment

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

Similar to above comment #473 (comment)

Convention: uts/docs/integration-testing.md#sandbox-setupBEFORE ALL TESTS: response = POST https://sandbox.realtime.ably-nonprod.net/apps.

Proposed fix: replace all three occurrences (L16, L22/L23, L30) of https://sandbox-rest.ably.io with https://sandbox.realtime.ably-nonprod.net.


```pseudo
BEFORE ALL TESTS:
response = POST https://sandbox-rest.ably.io/apps
Copy link
Copy Markdown
Collaborator

@sacOO7 sacOO7 Jun 2, 2026

Choose a reason for hiding this comment

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

Similar to above comment #473 (comment)

Convention: uts/docs/integration-testing.md#sandbox-setupBEFORE ALL TESTS: response = POST https://sandbox.realtime.ably-nonprod.net/apps.

Proposed fix: replace all three occurrences (L16, L22/L23, L30) of https://sandbox-rest.ably.io with https://sandbox.realtime.ably-nonprod.net.


### Test Steps
```pseudo
AWAIT root.increment(10)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocker — root.increment(...) on a PathObject rooted at /.

root is the PathObject returned by channel.object.get() with path: [] (RTO23d). When PathObject#increment is invoked, it resolves the path to the root LiveMap, then per RTPO17e the increment-on-non-counter branch throws an ErrorInfo with code: 92007 before the test reaches any subsequent assertion.

Affected lines in this file (all 10 verified): L258, L304, L348, L376, L408, L1040, L1074, L1098, L1131, L1166. The same bug recurs at five sites in uts/objects/unit/live_counter_api.md — see the comment there.

Reasoning: every one of these tests intends to exercise an operation on the counter stored at root["score"]. The fix is to navigate to that counter first via PathObject#get, then call the mutation on the returned PathObject.

Proposed fix (mechanical):

sed -i '' 's/root\.increment(/root.get("score").increment(/g; \
           s/root\.decrement(/root.get("score").decrement(/g' \
  uts/objects/unit/realtime_object.md \
  uts/objects/unit/live_counter_api.md

⚠️ After running, review uts/objects/unit/path_object_mutations.md is untouched — L219, L298 in that file intentionally call root.increment(...) to test the 92007 throw (this is in scope of RTPO17e negative-path coverage, not in scope of this fix).


### Test Steps
```pseudo
AWAIT root.increment(25)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocker — same root.increment(...) / root.decrement(...) bug.

Affected lines in this file: L82, L109, L140, L188, L257. See the matching comment on uts/objects/unit/realtime_object.md L258 for full reasoning and a sed one-liner that fixes both files.

Spec: RTPO17e (PathObject#increment on a non-counter throws 92007).

After the fix the existing assertion at L90 (ASSERT obj_msg.operation.objectId == "counter:score@1000") will actually be checking the right thing — currently the test never gets past root.increment(25).

### Test Steps
```pseudo
instance = root.instance()
AWAIT instance.clear()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocker — instance.clear() does not exist in the public API.

grep -n "clear" specifications/objects-features.md returns only the clearTimeserial field and the MAP_CLEAR inbound operation (RTLM24). RTLM24 describes how an incoming MAP_CLEAR is applied to a LiveMap — it does not define a clear() method on Instance, PathObject, or LiveMap.

The IDL block at the bottom of objects-features.md confirms: no clear() member on Instance (RTINS), PathObject (RTPO), or LiveMap (RTLM).

Reasoning: as written, this test references an API surface that doesn't exist. Derived SDKs will not be able to translate it.

Proposed fix: delete this section (L428–L451). Optionally insert a placeholder:

## RTLM24 - MAP_CLEAR application
**Status:** No outbound API test — `MAP_CLEAR` is currently inbound-only.
See `live_map.md` for the inbound-application coverage of [RTLM24](https://sdk.ably.com/builds/ably/specification/main/objects-features/#RTLM24).

If a public clear() is desired, that's a spec change — open an issue and re-add the test once spec'd.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe we should be specific about what we really want to clear from LiveMap

})
channel.attach()
AWAIT_STATE channel.state == DETACHED
root_path = channel.object.getRoot()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocker — channel.object.getRoot() does not exist.

RealtimeObject exposes get() (RTO23), not getRoot(). The IDL has no getRoot member.

A secondary problem: even if you swap in get(), the test setup forces the channel to DETACHED at L94 before calling it — and RTO23b / RTO25b require get() to throw 90001 on a DETACHED channel. So the test never reaches the root.subscribe(...) step it's trying to exercise.

Proposed fix: restructure so the PathObject is obtained while the channel is attached, then detach:

{ client, channel, root, mock_ws } = AWAIT setup_synced_channel("test")

# Now detach AFTER root is obtained
mock_ws.send_to_client(ProtocolMessage(
  action: DETACHED, channel: "test",
  error: { code: 90000, statusCode: 400, message: "Channel detached" }
))
AWAIT_STATE channel.state == DETACHED

### Test Steps
root.subscribe((event) => {}) FAILS WITH error

### Assertions
ASSERT error.code == 90001
ASSERT error.statusCode == 400

This actually tests the RTPO19b precondition (subscribe-on-detached fails).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We have migrated to channel.object.get() from channel.object.getRoot(), all relevant references should be updated in all test files

### Assertions
```pseudo
ASSERT updates.length == 1
ASSERT updates[0].objectMessage IS NOT null
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocker — assertions reference fields of the wrong event type.

The file header documents this as RTLO4b* coverage (LiveObject#subscribe), but every test in the file calls instance.subscribe(...) (Instance#subscribe, RTINS16). The two emit different event types:

  • LiveObject#subscribe (RTLO4b) emits a LiveObjectUpdate with fields tombstone, noop, objectMessage, update (RTLO4b4)
  • Instance#subscribe (RTINS16) emits an InstanceSubscriptionEvent of shape { object: Instance, message: PublicAPI::ObjectMessage? } (RTINS16e)

This file asserts updates[0].tombstone == true (L353, L385), updates[0].objectMessage.serial == "99" (L316), etc. — those are LiveObjectUpdate fields, not present on InstanceSubscriptionEvent.

Proposed fix: add a test seam to helpers/standard_test_pool.md that exposes the internal LiveObject so we can subscribe on it directly:

get_live_object(channel, object_id):
  # Internal test seam; SDKs expose this via package-private hooks.
  RETURN channel._realtimeObject._pool[object_id]

Then replace each instance.subscribe(...) with get_live_object(channel, "counter:score@1000").subscribe(...). The current assertions then check the right type.

Bonus bug at L147–179 (RTLO4b4c1 noop-no-trigger-0): the COUNTER_INC noop reuses the same serial "01" as the prior message, so RTLO4a6 (newness check) suppresses it before RTLO4b4c1 (noop suppression) can fire. Advance the serial to "02" so only the noop path can be the reason the listener wasn't called.

### Setup
```pseudo
{ client, channel, root, mock_ws } = AWAIT setup_synced_channel("test")
site_serials_before = root.get("score").instance()._liveObject.siteTimeserials
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocker — reaches into a private/internal field via _liveObject.

root.get("score").instance()._liveObject.siteTimeserials — there is no public path from Instance to LiveObject.siteTimeserials in RTINS3-RTINS16 or RTLO3. The _liveObject field is invented by this test.

Spec being verified is RTO20f ("Apply-on-ACK does not update siteTimeserials"). That requirement is already covered by observable behaviour:

  • live_counter.md → RTLC7c/local-source-no-serial-update-0 at L391 already proves that LOCAL-source applyOperation doesn't write siteTimeserials.

Proposed fix (preferred): delete this RTO20f test (L1057–L1081) as redundant. The pure-unit test at live_counter.md L391 covers the same requirement without internal access.

Alternative: verify the requirement through observable behaviour — after a local increment, send an inbound COUNTER_INC echo with a different siteCode + serial. The counter should still apply it (because LOCAL did not claim that site's serial). No private-field access needed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we can keep this test, provided _liveObject is exposed in test suite in downsteam SDKs


```pseudo
provision_objects_via_rest(api_key, channel_name, operations):
POST https://sandbox-rest.ably.io/channels/{encode_uri_component(channel_name)}/objects
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

High — provision_objects_via_rest calls an unspecified REST endpoint.

The spec (https://sdk.ably.com/builds/ably/specification/main/objects-features/#) has no /channels/{name}/objects REST surface — grep "REST" specifications/objects-features.md returns nothing. The endpoint exists in ably-js's test apparatus, but it's not part of any SDK spec. PLAN.md L20 acknowledges this.

The only consumer is objects_lifecycle_test.md → RTPO15/rest-provisioned-data-sync-0 (L278).

Reasoning: derived tests in other SDKs cannot translate this — there is no RestClient method that corresponds.

Proposed fix (preferred): rewrite RTPO15/rest-provisioned-data-sync-0 to provision via a side realtime client (same pattern as objects_sync_test.md → RTO5-RTO17/two-clients-sync-0):

provisioner = Realtime(options: { key: api_key })
AWAIT_STATE provisioner.connection.state == CONNECTED
prov_channel = provisioner.channels.get(channel_name, { modes: ["OBJECT_PUBLISH", "OBJECT_SUBSCRIBE"] })
prov_root = AWAIT prov_channel.object.get()
AWAIT prov_root.set("provisioned", "from_rest")
provisioner.close()

Then delete provision_objects_via_rest from this helper file.

Alternative: delete provision_objects_via_rest and the dependent test, add a // TODO: REST provisioning when spec'd note.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since there is only a single consumer, does it make sense to add helper in uts/objects/helpers/standard_test_pool.md? Also, since this is related to REST api, are we planning to include it in current UTS test spec

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.

Rest api spec doesn't block us and uts suggests making simple request call. I believe everything is correct here, no need to change

### Setup
```pseudo
pool = ObjectsPool()
pool.appliedOnAckSerials = {"serial-1", "serial-2"}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

High — appliedOnAckSerials / bufferedObjectOperations are tested as members of ObjectsPool, but the spec puts them on RealtimeObject.

Spec:

  • RTO7a: "The RealtimeObject instance has an internal attribute bufferedObjectOperations"
  • RTO7b: "The RealtimeObject instance has an internal attribute appliedOnAckSerials"

PLAN.md L376 corroborates: "appliedOnAckSerials on RealtimeObject (RTO7b), not on pool".

Yet this file references both as pool members at: L302, L320, L352, L396, L439, L456, L483, L744, L756, L819, L865.

Reasoning: when ported, an SDK following the spec will not have these as ObjectsPool members and the test will not compile or will silently bind to nothing.

Proposed fix: add a RealtimeObject test seam in the affected setups:

pool = ObjectsPool()
realtime_object = RealtimeObject(pool: pool)

Then replace every pool.appliedOnAckSerials with realtime_object.appliedOnAckSerials, and pool.bufferedObjectOperations with realtime_object.bufferedObjectOperations. About 10 lines, mechanical.

If implementations choose to colocate the structures on the pool for locality, document that delegation in PLAN — but tests and PLAN should not contradict each other.

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.

true, generated js test also use RealtimeObject. On the setup phase it use setupSyncedChannel helper, worth to update tests here

```pseudo
channel_name = "objects-lifecycle-" + random_id()

client_a = Realtime(options: { key: api_key })
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

High — direct sandbox tests use a sub-standard ClientOptions block.

uts/docs/integration-testing.md L165–176 prescribes:

client = Realtime(options: ClientOptions(
  key: api_key,
  endpoint: "nonprod:sandbox",     # or restHost/realtimeHost per the open PR thread
  useBinaryProtocol: false,
  autoConnect: false
))

This file (and objects_sync_test.md, objects_gc_test.md, objects_batch_test.md) use Realtime(options: { key: api_key }) only — missing useBinaryProtocol, autoConnect, and any sandbox host configuration. Verified locations: this file L50–51, L101–102, L148–149, L200–201, L250, L301; plus the three sibling files.

Consequences:

  • No host config: client falls back to production endpoint (REC1a default → realtime.ably.io). The "Sandbox Setup" header in this file claims sandbox but the client never reaches it.
  • useBinaryProtocol missing: JS defaults to false, but Java/.NET/Cocoa default to msgpack. Tests will diverge across SDKs.
  • autoConnect missing: subsequent client.connect() is a no-op when autoConnect defaults to true.

Proposed fix (and to avoid the endpoint portability issue raised on uts/docs/integration-testing.md L159):

client = Realtime(options: ClientOptions(
  key: api_key,
  restHost: "sandbox.realtime.ably-nonprod.net",
  realtimeHost: "sandbox.realtime.ably-nonprod.net",
  useBinaryProtocol: false,
  autoConnect: false
))
client.connect()
AWAIT_STATE client.connection.state == CONNECTED

Or factor into a helpers/standard_test_pool.md helper like connect_sandbox_client(). ~20 call sites across 4 files.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah, will be helpful to have connect_sandbox_client inside helpers/standard_test_pool.md. It will be reused by all integration/integration-proxy tests.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We can also have ready to use get_connected_sandbox_client and get_channel_objects helper methods in helpers/standard_test_pool.md which returns connected client along with channel.objects reference for given channelName. This will avoid tons of repetition in integration/integration-proxy tests.


Spec points: `RTO23`, `RTPO15`, `RTPO17`

## Test Type
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

High — data-path integration tests need ## Protocol Variants.

uts/docs/integration-testing.md L262: "Integration tests that exercise the data encoding/decoding path must run with both JSON and msgpack". uts/docs/writing-test-specs.md L17 echoes this.

LiveObjects tests are squarely data-path: every root.set(...), counter.increment(...), and OBJECT_SYNC fixture passes through the SDK's wire-encoding layer. OM4 encoding rules in objects-features.md apply per protocol — e.g. bytes is base64 in JSON, raw binary in msgpack.

Three files need this annotation: this file, objects_sync_test.md, objects_gc_test.md.
(objects_faults.md is proxy and correctly JSON-only; objects_batch_test.md is forward-looking per §0.2 — defer.)

Proposed fix: immediately after ## Test Type, insert:

## Protocol Variants
json, msgpack

Each test in this file runs once per protocol variant. The `PROTOCOL` variable
is set to `"json"` or `"msgpack"` for the current run. Client options should set
`useBinaryProtocol: PROTOCOL == "msgpack"`.

Then update the ClientOptions block (after §0.11's fix) to set useBinaryProtocol: PROTOCOL == "msgpack". Also extend the "Annotated specs" list in integration-testing.md L290–306 with a new LiveObjects: heading.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah, we need to add Protocol Variants to all liveobjects tests involving data intensive operations.


---

## RTO5c9, RTO20 - appliedOnAckSerials cleared on re-sync
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

High — assertion does not actually prove appliedOnAckSerials was cleared.

The test asserts score == 100 after re-sync. That passes whether the set was cleared or not — the OBJECT_SYNC overwrites score regardless. So the test would silently still pass if RTO5c9 was never implemented.

Reasoning: RTO5c9 requires clearing appliedOnAckSerials at sync completion specifically so that subsequent inbound OBJECTs with previously-deduped serials are no longer treated as echoes.

Proposed fix: after re-sync, replay the previously-applied-on-ACK serial as an inbound OBJECT and verify it now APPLIES rather than dedupes:

AWAIT root.get("score").increment(10)   # local apply via ACK, serial "ack-0:0" added
# Trigger re-sync (clears appliedOnAckSerials per RTO5c9)
mock_ws.send_to_client(ProtocolMessage(action: ATTACHED, ... channelSerial: "sync2:cursor", flags: HAS_OBJECTS))
mock_ws.send_to_client(build_object_sync_message("test", "sync2:", STANDARD_POOL_OBJECTS))
# Replay the same serial as an inbound OBJECT — should APPLY (not dedup)
mock_ws.send_to_client(build_object_message("test", [
  build_counter_inc("counter:score@1000", 10, "ack-0:0", "test-site")
]))
poll_until(root.get("score").value() == 110, timeout: 5s)
ASSERT root.get("score").value() == 110   # proves dedup didn't fire — set was cleared

If the dedup set was NOT cleared, the inbound echo would dedup and the value would stay at 100.

Comment thread uts/objects/unit/realtime_object.md

### Setup
```pseudo
enable_fake_timers()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

High — enable_fake_timers() in an integration test does not affect the sandbox.

Both tests in this file (L50, L103) call enable_fake_timers() then ADVANCE_TIME(86400000 + 300000). Fake timers only manipulate the SDK's view of time. The sandbox server has its own clock, GC scheduler, and grace period — the ADVANCE_TIME call has no effect on it.

Reasoning: RTO10 GC depends on server-side state and the server's objectsGCGracePeriod returned via ConnectionDetails (RTO10b1). An integration test that doesn't actually wait can't observe real GC.

Proposed fix — pick one:

(a) Move to mock-WS unit — convert both tests to use setup_synced_channel with enable_fake_timers() properly. The mock loop respects fake timers; the sandbox doesn't. Move to uts/objects/unit/realtime_object.md (under the existing RTO10 heading) or a new uts/objects/unit/gc_test.md.

(b) Keep as integration but use a short real-time wait — configure the sandbox app with objectsGCGracePeriod: 5000 in BEFORE ALL TESTS setup, then real-wait 5+ seconds. Slower test, but actually exercises end-to-end GC.

Additionally, the comment on L68 — // Remove it (tombstones the entry and the object) — is wrong. root.remove("counter") is a MAP_REMOVE, which tombstones the entry per RTLM8, not the counter object. The counter object lives on. Rename the test to RTLM19/tombstoned-entry-gc-recreate-0 and update the comment to reflect entry-level GC (RTLM19).


---

## RTO20e - publishAndApply fails when channel enters FAILED during SYNCING
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

High — error.cause is not asserted, even though RTO20e1 requires it.

RTO20e1: "cause set to the RealtimeChannel.errorReason if it is set". This test injects a channel-level ERROR with code: 90000 but the assertion block (around L367) only checks error.code == 92008 — it doesn't verify the cause chain.

Reasoning: SDKs that implement 92008 without populating cause would pass this test, missing a spec-required field.

Proposed fix: after the existing assertions, add:

ASSERT error.cause IS NOT null
ASSERT error.cause.code == 90000   # the injected channel error surfaces as cause

Copy link
Copy Markdown
Collaborator

@sacOO7 sacOO7 Jun 2, 2026

Choose a reason for hiding this comment

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

Optional, but better to expose underlying cause for better debugging.


---

## RTLCV3c - No validation at creation time
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.

I believe this is very js-specific test and it simply can't be implemented in any statically typed language, let's delete it

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.

3 participants