diff --git a/uts/objects/helpers/standard_test_pool.md b/uts/objects/helpers/standard_test_pool.md index 093b1e996..1c2eeff85 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.realtime.ably-nonprod.net/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_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() -``` diff --git a/uts/objects/integration/objects_lifecycle_test.md b/uts/objects/integration/objects_lifecycle_test.md index a72ae3b68..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 @@ -294,18 +294,15 @@ 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" } ]) ``` ### 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 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