From 051bb95c92f1a8d7c219d370ece6f9febecdd58d Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 26 Jun 2026 16:02:58 +0530 Subject: [PATCH 1/5] fix(uts/objects): align REST fixture provisioning with V2 objects API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `provision_objects_via_rest` helper described the legacy pre-V2 REST shape (`POST .../objects` with a `{ "messages": [ { operation: { action, ... } } ] }` envelope), which no longer matches the LiveObjects objects REST API. Update it to the V2 contract (per the LiveObjects OpenAPI specification): - endpoint is singular `POST .../object`; - the body is a single operation object, or a bare JSON array (batch) — no `messages` envelope; - an operation is identified by its payload key (`mapSet`/`mapRemove`/`mapCreate`/ `counterInc`/`counterCreate`) with an `objectId` or `path` target (and optional idempotency `id`); values are `{string|number|boolean|bytes|objectId}`; - `mapCreate.semantics` is the integer 0 (LWW) with `{data:}`-wrapped entries. Also update the only call site (RTPO15 in objects_lifecycle_test.md) to the V2 operation shape so the spec stays internally consistent. Co-Authored-By: Claude Opus 4.8 (1M context) --- uts/objects/helpers/standard_test_pool.md | 19 +++++++++++++++++-- .../integration/objects_lifecycle_test.md | 7 ++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/uts/objects/helpers/standard_test_pool.md b/uts/objects/helpers/standard_test_pool.md index 093b1e996..d6ea09220 100644 --- a/uts/objects/helpers/standard_test_pool.md +++ b/uts/objects/helpers/standard_test_pool.md @@ -358,10 +358,25 @@ setup_synced_channel_no_ack(channel_name): For integration tests that need pre-existing object state before the test client connects, use the REST API to establish fixtures. +The objects REST API uses the **V2 format** (per the LiveObjects OpenAPI specification). A request publishes a single operation, or a batch of operations as a JSON array — there is **no** `{ "messages": [...] }` envelope. Each operation names its type via a payload key (`mapSet`, `mapRemove`, `mapCreate`, `counterInc`, `counterCreate`) and targets an object by `objectId` **or** `path`. Note the endpoint path is singular (`/object`). + ```pseudo provision_objects_via_rest(api_key, channel_name, operations): - POST https://sandbox-rest.ably.io/channels/{encode_uri_component(channel_name)}/objects + # operations: a single operation object, or an array of operation objects (batch) + POST https://sandbox-rest.ably.io/channels/{encode_uri_component(channel_name)}/object WITH Authorization: Basic {base64(api_key)} WITH Content-Type: application/json - WITH body: { "messages": operations } + WITH body: operations +``` + +Operation shapes (target by `objectId` or `path`; an optional `id` on any operation is an idempotency key): + +```pseudo +{ mapSet: { key: "", value: }, objectId|path: "" } +{ mapRemove: { key: "" }, objectId|path: "" } +{ mapCreate: { semantics: 0, entries: { "": { data: } } } [, objectId|path: ""] } # semantics 0 = LWW +{ counterCreate: { count: } [, objectId|path: ""] } +{ counterInc: { number: }, objectId|path: "" } # negative number = decrement ``` + +where `` is a primitive value object: `{ string: "..." }`, `{ number: ... }`, `{ boolean: ... }`, `{ bytes: "" }`, or a reference `{ objectId: "..." }` (`string`/`bytes` may also carry an optional `encoding`). diff --git a/uts/objects/integration/objects_lifecycle_test.md b/uts/objects/integration/objects_lifecycle_test.md index a72ae3b68..8ec8ee7ec 100644 --- a/uts/objects/integration/objects_lifecycle_test.md +++ b/uts/objects/integration/objects_lifecycle_test.md @@ -294,11 +294,8 @@ channel_name = "objects-rest-provision-" + random_id() // Provision data via REST before any realtime client connects provision_objects_via_rest(api_key, channel_name, [ { - operation: { - action: "MAP_SET", - objectId: "root", - mapSet: { key: "provisioned", value: { string: "from_rest" } } - } + mapSet: { key: "provisioned", value: { string: "from_rest" } }, + objectId: "root" } ]) ``` From 9883347670ea702f96cd3ca8a38c8f60269e4942 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 26 Jun 2026 16:56:02 +0530 Subject: [PATCH 2/5] fix(uts): align provision_objects_via_rest host with the nonprod sandbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The REST fixture-provisioning helper still POSTed to https://sandbox-rest.ably.io, the legacy prod-sandbox host. Every other objects integration spec — and the whole UTS integration corpus (realtime + rest) — provisions apps and routes clients via https://sandbox.realtime.ably-nonprod.net (raw HTTP) / endpoint: "nonprod:sandbox" (SDK clients). The objects integration specs were migrated to the nonprod host in e57d340e, but standard_test_pool.md was missed, leaving provision_objects_via_rest posting fixtures to a different backend than the app/key were provisioned on — so RTPO15 (rest-provisioned-data-sync) could never resolve. Point the helper at the canonical nonprod host to match. Co-Authored-By: Claude Opus 4.8 (1M context) --- uts/objects/helpers/standard_test_pool.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uts/objects/helpers/standard_test_pool.md b/uts/objects/helpers/standard_test_pool.md index d6ea09220..1c2eeff85 100644 --- a/uts/objects/helpers/standard_test_pool.md +++ b/uts/objects/helpers/standard_test_pool.md @@ -363,7 +363,7 @@ The objects REST API uses the **V2 format** (per the LiveObjects OpenAPI specifi ```pseudo provision_objects_via_rest(api_key, channel_name, operations): # operations: a single operation object, or an array of operation objects (batch) - POST https://sandbox-rest.ably.io/channels/{encode_uri_component(channel_name)}/object + POST https://sandbox.realtime.ably-nonprod.net/channels/{encode_uri_component(channel_name)}/object WITH Authorization: Basic {base64(api_key)} WITH Content-Type: application/json WITH body: operations From 356b57f07e2f9e5b483a205f6e9ce4db35ce8fc9 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 26 Jun 2026 17:01:19 +0530 Subject: [PATCH 3/5] fix(uts): route objects integration clients to the nonprod sandbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Realtime client option blocks in objects_lifecycle_test.md (10) and objects_sync_test.md (5) set no host, so the SDK falls back to the production endpoint (realtime.ably.io, REC1a) — meaning the clients never reach the nonprod sandbox the "Sandbox Setup" provisions the app on. Every other UTS integration spec (realtime + rest) configures clients with endpoint: "nonprod:sandbox"; add it here to match so the tests actually exercise the sandbox. Added missing auto-connect false to clientOptions --- .../integration/objects_lifecycle_test.md | 20 +++++++++---------- uts/objects/integration/objects_sync_test.md | 10 +++++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/uts/objects/integration/objects_lifecycle_test.md b/uts/objects/integration/objects_lifecycle_test.md index 8ec8ee7ec..4ed745d1c 100644 --- a/uts/objects/integration/objects_lifecycle_test.md +++ b/uts/objects/integration/objects_lifecycle_test.md @@ -54,8 +54,8 @@ propagates via the server and a second client sees the updated value. ```pseudo channel_name = "objects-lifecycle-" + random_id() -client_a = Realtime(options: { key: api_key, useBinaryProtocol: PROTOCOL == "msgpack" }) -client_b = Realtime(options: { key: api_key, useBinaryProtocol: PROTOCOL == "msgpack" }) +client_a = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false, useBinaryProtocol: PROTOCOL == "msgpack" }) +client_b = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false, useBinaryProtocol: PROTOCOL == "msgpack" }) client_a.connect() AWAIT_STATE client_a.connection.state == CONNECTED @@ -105,8 +105,8 @@ on the server. Second client syncs and reads the counter value. ```pseudo channel_name = "objects-counter-create-" + random_id() -client_a = Realtime(options: { key: api_key, useBinaryProtocol: PROTOCOL == "msgpack" }) -client_b = Realtime(options: { key: api_key, useBinaryProtocol: PROTOCOL == "msgpack" }) +client_a = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false, useBinaryProtocol: PROTOCOL == "msgpack" }) +client_b = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false, useBinaryProtocol: PROTOCOL == "msgpack" }) client_a.connect() AWAIT_STATE client_a.connection.state == CONNECTED @@ -152,8 +152,8 @@ The server applies the increment and propagates the updated value. ```pseudo channel_name = "objects-increment-" + random_id() -client_a = Realtime(options: { key: api_key, useBinaryProtocol: PROTOCOL == "msgpack" }) -client_b = Realtime(options: { key: api_key, useBinaryProtocol: PROTOCOL == "msgpack" }) +client_a = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false, useBinaryProtocol: PROTOCOL == "msgpack" }) +client_b = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false, useBinaryProtocol: PROTOCOL == "msgpack" }) client_a.connect() AWAIT_STATE client_a.connection.state == CONNECTED @@ -204,8 +204,8 @@ Second client can navigate into the nested map. ```pseudo channel_name = "objects-map-create-" + random_id() -client_a = Realtime(options: { key: api_key, useBinaryProtocol: PROTOCOL == "msgpack" }) -client_b = Realtime(options: { key: api_key, useBinaryProtocol: PROTOCOL == "msgpack" }) +client_a = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false, useBinaryProtocol: PROTOCOL == "msgpack" }) +client_b = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false, useBinaryProtocol: PROTOCOL == "msgpack" }) client_a.connect() AWAIT_STATE client_a.connection.state == CONNECTED @@ -254,7 +254,7 @@ after the sync sequence completes. ```pseudo channel_name = "objects-get-root-" + random_id() -client = Realtime(options: { key: api_key, useBinaryProtocol: PROTOCOL == "msgpack" }) +client = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false, useBinaryProtocol: PROTOCOL == "msgpack" }) client.connect() AWAIT_STATE client.connection.state == CONNECTED @@ -302,7 +302,7 @@ provision_objects_via_rest(api_key, channel_name, [ ### Test Steps ```pseudo -client = Realtime(options: { key: api_key, useBinaryProtocol: PROTOCOL == "msgpack" }) +client = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false, useBinaryProtocol: PROTOCOL == "msgpack" }) client.connect() AWAIT_STATE client.connection.state == CONNECTED diff --git a/uts/objects/integration/objects_sync_test.md b/uts/objects/integration/objects_sync_test.md index 16af7ecee..eb7513676 100644 --- a/uts/objects/integration/objects_sync_test.md +++ b/uts/objects/integration/objects_sync_test.md @@ -54,7 +54,7 @@ processes OBJECT_SYNC messages, then transitions to SYNCED. get() waits for SYNC ```pseudo channel_name = "objects-sync-" + random_id() -client = Realtime(options: { key: api_key, useBinaryProtocol: PROTOCOL == "msgpack" }) +client = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false, useBinaryProtocol: PROTOCOL == "msgpack" }) client.connect() AWAIT_STATE client.connection.state == CONNECTED @@ -89,8 +89,8 @@ client.close() ```pseudo channel_name = "objects-two-sync-" + random_id() -client_a = Realtime(options: { key: api_key, useBinaryProtocol: PROTOCOL == "msgpack" }) -client_b = Realtime(options: { key: api_key, useBinaryProtocol: PROTOCOL == "msgpack" }) +client_a = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false, useBinaryProtocol: PROTOCOL == "msgpack" }) +client_b = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false, useBinaryProtocol: PROTOCOL == "msgpack" }) client_a.connect() AWAIT_STATE client_a.connection.state == CONNECTED @@ -137,7 +137,7 @@ is re-populated from the server. ```pseudo channel_name = "objects-reattach-" + random_id() -client = Realtime(options: { key: api_key, useBinaryProtocol: PROTOCOL == "msgpack" }) +client = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false, useBinaryProtocol: PROTOCOL == "msgpack" }) client.connect() AWAIT_STATE client.connection.state == CONNECTED @@ -183,7 +183,7 @@ sends HAS_OBJECTS, sync completes, root is an empty LiveMap. ```pseudo channel_name = "objects-subscribe-only-" + random_id() -client = Realtime(options: { key: api_key, useBinaryProtocol: PROTOCOL == "msgpack" }) +client = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false, useBinaryProtocol: PROTOCOL == "msgpack" }) client.connect() AWAIT_STATE client.connection.state == CONNECTED From 4eb5e0dac8f15f9cb095999ec6555f0f97ce6b18 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 26 Jun 2026 17:17:44 +0530 Subject: [PATCH 4/5] Removed non-required `objects_batch_spec.md` from the liveobjects uts spec --- uts/objects/integration/objects_batch_test.md | 201 ------------------ 1 file changed, 201 deletions(-) delete mode 100644 uts/objects/integration/objects_batch_test.md diff --git a/uts/objects/integration/objects_batch_test.md b/uts/objects/integration/objects_batch_test.md deleted file mode 100644 index b3fd4d849..000000000 --- a/uts/objects/integration/objects_batch_test.md +++ /dev/null @@ -1,201 +0,0 @@ -# Objects Batch Integration Tests - -Spec points: `RTPO22`, `RTBC12`–`RTBC15` - -## Test Type -Integration test against Ably sandbox - -## Purpose - -Batch operations end-to-end — multiple mutations in a single publish, atomic -propagation to subscribers. Verifies that batch() groups multiple operations -into a single ProtocolMessage and the server processes and delivers them -correctly to other clients. - -## Sandbox Setup - -Tests run against the Ably Sandbox at `https://sandbox.realtime.ably-nonprod.net`. - -### App Provisioning - -```pseudo -BEFORE ALL TESTS: - response = POST https://sandbox.realtime.ably-nonprod.net/apps - WITH body from ably-common/test-resources/test-app-setup.json - - app_config = parse_json(response.body) - api_key = app_config.keys[0].key_str - app_id = app_config.app_id - -AFTER ALL TESTS: - DELETE https://sandbox.realtime.ably-nonprod.net/apps/{app_id} - WITH Authorization: Basic {api_key} -``` - -### Notes -- Each test uses a unique channel name - ---- - -## RTPO22 - Batch set of multiple keys arrives to second client - -**Test ID**: `objects/integration/RTPO22/batch-set-propagates-0` - -**Spec requirement:** batch() groups multiple mutations into a single publish. -All operations are delivered together to subscribers. - -### Setup -```pseudo -channel_name = "objects-batch-" + random_id() - -client_a = Realtime(options: { key: api_key }) -client_b = Realtime(options: { key: api_key }) - -client_a.connect() -AWAIT_STATE client_a.connection.state == CONNECTED - -client_b.connect() -AWAIT_STATE client_b.connection.state == CONNECTED - -channel_a = client_a.channels.get(channel_name, { modes: ["OBJECT_SUBSCRIBE", "OBJECT_PUBLISH"] }) -channel_b = client_b.channels.get(channel_name, { modes: ["OBJECT_SUBSCRIBE", "OBJECT_PUBLISH"] }) - -root_a = AWAIT channel_a.object.get() -root_b = AWAIT channel_b.object.get() -``` - -### Test Steps -```pseudo -AWAIT root_a.batch((ctx) => { - ctx.set("x", 1) - ctx.set("y", 2) - ctx.set("z", 3) -}) - -poll_until(root_b.get("x").value() == 1, timeout: 10s) -``` - -### Assertions -```pseudo -ASSERT root_b.get("x").value() == 1 -ASSERT root_b.get("y").value() == 2 -ASSERT root_b.get("z").value() == 3 -``` - -### Teardown -```pseudo -client_a.close() -client_b.close() -``` - ---- - -## RTPO22 - Batch with mixed operations (set + remove + increment) - -**Test ID**: `objects/integration/RTPO22/batch-mixed-ops-0` - -**Spec requirement:** Batch can contain different operation types published atomically. - -### Setup -```pseudo -channel_name = "objects-batch-mixed-" + random_id() - -client_a = Realtime(options: { key: api_key }) -client_b = Realtime(options: { key: api_key }) - -client_a.connect() -AWAIT_STATE client_a.connection.state == CONNECTED - -client_b.connect() -AWAIT_STATE client_b.connection.state == CONNECTED - -channel_a = client_a.channels.get(channel_name, { modes: ["OBJECT_SUBSCRIBE", "OBJECT_PUBLISH"] }) -channel_b = client_b.channels.get(channel_name, { modes: ["OBJECT_SUBSCRIBE", "OBJECT_PUBLISH"] }) - -root_a = AWAIT channel_a.object.get() -root_b = AWAIT channel_b.object.get() -``` - -### Test Steps -```pseudo -// Set up initial state -AWAIT root_a.set("to_remove", "temp") -AWAIT root_a.set("counter", LiveCounter.create(10)) -poll_until(root_b.get("to_remove").value() == "temp", timeout: 10s) -poll_until(root_b.get("counter").value() == 10, timeout: 10s) - -// Batch with mixed operations -AWAIT root_a.batch((ctx) => { - ctx.set("name", "Alice") - ctx.remove("to_remove") - child = ctx.get("counter") - child.increment(5) -}) - -poll_until(root_b.get("name").value() == "Alice", timeout: 10s) -``` - -### Assertions -```pseudo -ASSERT root_b.get("name").value() == "Alice" -ASSERT root_b.get("to_remove").value() == null -ASSERT root_b.get("counter").value() == 15 -``` - -### Teardown -```pseudo -client_a.close() -client_b.close() -``` - ---- - -## RTPO22 - Batch with LiveCounterValueType creates counter atomically - -**Test ID**: `objects/integration/RTPO22/batch-create-counter-0` - -**Spec requirement:** Batch containing LiveCounterValueType generates COUNTER_CREATE + -MAP_SET in a single publish. The server processes both atomically. - -### Setup -```pseudo -channel_name = "objects-batch-counter-" + random_id() - -client_a = Realtime(options: { key: api_key }) -client_b = Realtime(options: { key: api_key }) - -client_a.connect() -AWAIT_STATE client_a.connection.state == CONNECTED - -client_b.connect() -AWAIT_STATE client_b.connection.state == CONNECTED - -channel_a = client_a.channels.get(channel_name, { modes: ["OBJECT_SUBSCRIBE", "OBJECT_PUBLISH"] }) -channel_b = client_b.channels.get(channel_name, { modes: ["OBJECT_SUBSCRIBE", "OBJECT_PUBLISH"] }) - -root_a = AWAIT channel_a.object.get() -root_b = AWAIT channel_b.object.get() -``` - -### Test Steps -```pseudo -AWAIT root_a.batch((ctx) => { - ctx.set("batch_counter", LiveCounter.create(99)) - ctx.set("label", "created in batch") -}) - -poll_until(root_b.get("batch_counter").value() == 99, timeout: 10s) -``` - -### Assertions -```pseudo -ASSERT root_b.get("batch_counter").value() == 99 -ASSERT root_b.get("label").value() == "created in batch" -ASSERT root_b.get("batch_counter").instance() IS NOT null -``` - -### Teardown -```pseudo -client_a.close() -client_b.close() -``` From e10eefbd8cced6f15573df82452c656b3adbac10 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 26 Jun 2026 17:38:21 +0530 Subject: [PATCH 5/5] Added missing endpoint and autoConnect to objects_faults spec --- uts/objects/integration/proxy/objects_faults.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uts/objects/integration/proxy/objects_faults.md b/uts/objects/integration/proxy/objects_faults.md index b4e08fab8..adfb709e8 100644 --- a/uts/objects/integration/proxy/objects_faults.md +++ b/uts/objects/integration/proxy/objects_faults.md @@ -149,7 +149,7 @@ The mutations should be buffered and applied after the sync completes. channel_name = "objects-buffer-resync-" + random_id() // Client A: direct connection (no proxy), publishes mutations -client_a = Realtime(options: { key: api_key }) +client_a = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false }) client_a.connect() AWAIT_STATE client_a.connection.state == CONNECTED WITH timeout: 15 seconds @@ -391,7 +391,7 @@ sync completes. channel_name = "objects-publish-during-sync-" + random_id() // Client A: direct, no proxy -client_a = Realtime(options: { key: api_key }) +client_a = Realtime(options: { key: api_key, endpoint: "nonprod:sandbox", autoConnect: false }) client_a.connect() AWAIT_STATE client_a.connection.state == CONNECTED WITH timeout: 15 seconds