Skip to content

Commit 2fba05e

Browse files
paddybyersclaude
andcommitted
Update UTS test specs to match LiveObjects path-based API spec (a397e34)
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>
1 parent 3de4a4f commit 2fba05e

17 files changed

Lines changed: 3552 additions & 1386 deletions

uts/objects/PLAN.md

Lines changed: 66 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Context
44

5-
The LiveObjects feature lets clients store shared CRDT data on realtime channels. The specification is at `specification/specifications/objects-features.md`specifically the path-based API version on branch `origin/AIT-30/liveobjects-path-based-api-spec` (with batch API additions on `origin/AIT-30/liveobjects-batch-api`).
5+
The LiveObjects feature lets clients store shared CRDT data on realtime channels. The specification is at `specification/specifications/objects-features.md` — the path-based API version squashed as commit `a397e34` ("LiveObjects path-based API spec").
66

77
An earlier attempt at UTS test specs exists in `uts/test/realtime/unit/objects/` (14 files). It was written against a different spec namespace (PO* vs RTPO*/RTINS*/RTLCV*/RTLMV*), used v5 wire format field names, had apply-on-ACK contradictions, and duplicated setup across files. We're doing a clean rewrite using the correct spec, informed by that earlier work.
88

@@ -12,7 +12,7 @@ All new test files go in `specification/uts/objects/`.
1212

1313
**Internal (not user-facing):** LiveObject, LiveCounter (CRDT counter), LiveMap (LWW map), ObjectsPool (sync state machine), RealtimeObject (channel orchestrator with publishAndApply)
1414

15-
**Public (user-facing):** PathObject (lazy path reference), Instance (identity-bound reference), LiveCounterValueType/LiveMapValueType (creation descriptors via static `create()` factories), BatchContext (atomic multi-op publish)
15+
**Public (user-facing):** PathObject (lazy path reference), Instance (identity-bound reference), LiveCounterValueType/LiveMapValueType (creation descriptors via static `create()` factories), PublicAPI::ObjectMessage/ObjectOperation (user-facing event metadata)
1616

1717
**Wire protocol v6:** `counterInc.number`, `mapSet.{key,value}`, `mapRemove.key`, `mapCreate.{semantics,entries}`, `counterCreateWithObjectId.{nonce,initialValue}`, `mapCreateWithObjectId.{nonce,initialValue}`
1818

@@ -30,39 +30,40 @@ All new test files go in `specification/uts/objects/`.
3030
### Pure Unit Tests (no mocks)
3131
| File | Spec Points | ~Tests |
3232
|------|-------------|--------|
33-
| `unit/live_counter.md` | RTLC1-4, RTLC6-9, RTLC14, RTLC16, RTLO3, RTLO4a, RTLO4e, RTLO5, RTLO6 | ~28 |
34-
| `unit/live_map.md` | RTLM1-9, RTLM14-16, RTLM18-19, RTLM22-25, RTLO3, RTLO4a, RTLO4e, RTLO5, RTLO6 | ~42 |
35-
| `unit/objects_pool.md` | RTO3-9 | ~35 |
33+
| `unit/live_counter.md` | RTLC1-4, RTLC6-9, RTLC14, RTLC16, RTLO3-6, RTLO4b4d-e | ~23 |
34+
| `unit/live_map.md` | RTLM1-9, RTLM14-16, RTLM18-19, RTLM22-25, RTLO3-6, RTLO4g-h, RTLO4e9 | ~38 |
35+
| `unit/objects_pool.md` | RTO3-9, RTO5c10 | ~28 |
3636
| `unit/object_id.md` | RTO14 | ~5 |
37-
| `unit/value_types.md` | RTLCV1-4, RTLMV1-4 (consumption generates ObjectMessages with v6 wire format) | ~19 |
37+
| `unit/value_types.md` | RTLCV1-4, RTLMV1-4 (evaluation generates ObjectMessages with v6 wire format) | ~19 |
38+
| `unit/parent_references.md` | RTLO3f, RTLO4f-h, RTO5c10 (parentReferences, getFullPaths, add/remove/rebuild) | ~20 |
39+
| `unit/public_object_message.md` | PAOM1-3, PAOOP1-3 (PublicAPI::ObjectMessage/ObjectOperation construction) | ~13 |
3840

3941
### Mock WebSocket Unit Tests
4042
| File | Spec Points | ~Tests |
4143
|------|-------------|--------|
42-
| `unit/realtime_object.md` | RTO2, RTO10, RTO15-20, RTO22-24 (sync events, publish, publishAndApply, mode checks, GC) | ~33 |
44+
| `unit/realtime_object.md` | RTO2, RTO10, RTO15-20, RTO22-26 (sync events, publish, publishAndApply, GC, RTO24/25/26 preconditions) | ~36 |
4345
| `unit/live_counter_api.md` | RTLC5, RTLC11-13 (value, increment, decrement through channel) | ~13 |
44-
| `unit/live_map_api.md` | RTLM5, RTLM10-13, RTLM20-21, RTLM24 (reads + mutations through channel, echoMessages check) | ~18 |
45-
| `unit/live_object_subscribe.md` | RTLO4b, RTLO4c (subscribe/unsubscribe on internal LiveObject) | ~8 |
46-
| `unit/path_object.md` | RTPO1-14 (navigation, value, instance, entries, compact, compactJson) | ~33 |
47-
| `unit/path_object_mutations.md` | RTPO15-18, RTPO3c2 (set, remove, increment, decrement, error on unresolvable path) | ~12 |
48-
| `unit/path_object_subscribe.md` | RTPO19-21, RTO24 (path subscriptions, depth filtering, path-following semantics, subscribeIterator) | ~20 |
49-
| `unit/instance.md` | RTINS1-18 (id, value, get, entries, size, compact, set, remove, increment, subscribe) | ~26 |
50-
| `unit/batch.md` | RTPO22, RTINS19, RTBC1-16 (batch entry, BatchContext methods, RootBatchContext flush/close) | ~20 |
46+
| `unit/live_map_api.md` | RTLM5, RTLM10-13, RTLM20-21, RTLM24, RTLCV4, RTLMV4 (reads + mutations, value type evaluation) | ~20 |
47+
| `unit/live_object_subscribe.md` | RTLO4b, RTLO4b4c3, RTLO4b4d-e, RTLO4b7 (subscribe, dispatch chain, tombstone cleanup, Subscription) | ~11 |
48+
| `unit/path_object.md` | RTPO1-14, RTO25 (navigation, value, instance, entries, compact, compactJson, access preconditions) | ~27 |
49+
| `unit/path_object_mutations.md` | RTPO15-18, RTPO3c2, RTO26 (set, remove, increment, decrement, write preconditions) | ~14 |
50+
| `unit/path_object_subscribe.md` | RTPO19, RTO24 (path subscriptions, depth filtering, dispatch, PAOM delivery) | ~22 |
51+
| `unit/instance.md` | RTINS1-16 (id, value, get, entries, size, compact, set, remove, increment, subscribe, RTO25/26) | ~21 |
5152

5253
### Integration Tests (sandbox)
5354
| File | Spec Points | ~Tests |
5455
|------|-------------|--------|
5556
| `integration/objects_lifecycle_test.md` | RTO23, RTPO15, RTPO17 (create objects, mutate via PathObject, read back, REST provisioning) | ~6 |
5657
| `integration/objects_sync_test.md` | RTO4, RTO5, RTO17 (attach, sync sequence, re-attach) | ~4 |
57-
| `integration/objects_batch_test.md` | RTPO22, RTBC12-15 (batch publish, atomic delivery) | ~3 |
58+
| ~~`integration/objects_batch_test.md`~~ | ~~Batch API not in current spec revision~~ | |
5859
| `integration/objects_gc_test.md` | RTO10, RTLM19 (behavioral GC verification with ADVANCE_TIME) | ~2 |
5960

6061
### Proxy Integration Tests
6162
| File | Spec Points | ~Tests |
6263
|------|-------------|--------|
6364
| `integration/proxy/objects_faults.md` | RTO5a2, RTO7, RTO8, RTO17, RTO20e (sync interruption, mutation buffering during re-sync, server-initiated detach, publish failure on FAILED channel, publish during delayed sync) | ~5 |
6465

65-
**Totals: ~21 files, ~330 tests**
66+
**Totals: ~20 files, ~310 tests**
6667

6768
---
6869

@@ -198,17 +199,17 @@ Pure function tests:
198199

199200
### `unit/value_types.md` -- LiveCounterValueType / LiveMapValueType
200201

201-
Tests the static `create()` factories and consumption procedure.
202+
Tests the static `create()` factories and evaluation procedure.
202203

203204
**LiveCounterValueType (RTLCV1-4):**
204205
1. `LiveCounter.create(42)` -> immutable LiveCounterValueType with count=42
205206
2. `LiveCounter.create()` -> count defaults to 0
206-
3. Consumption: validates count, builds CounterCreate, generates objectId, returns ObjectMessage with `counterCreateWithObjectId.{nonce, initialValue}`
207-
4. Non-number count throws 40003 during consumption
207+
3. Evaluation: validates count, builds CounterCreate, generates objectId, returns ObjectMessage with `counterCreateWithObjectId.{nonce, initialValue}`
208+
4. Non-number count throws 40003 during evaluation
208209

209210
**LiveMapValueType (RTLMV1-4):**
210211
1. `LiveMap.create({entries})` -> immutable LiveMapValueType
211-
2. Consumption: validates keys/values, builds entries, generates objectId, returns ObjectMessage with `mapCreateWithObjectId.{nonce, initialValue}`
212+
2. Evaluation: validates keys/values, builds entries, generates objectId, returns ObjectMessage with `mapCreateWithObjectId.{nonce, initialValue}`
212213
3. Nested value types: LiveMapValueType containing LiveCounterValueType -> depth-first ObjectMessage array (inner creates before outer)
213214
4. Retains local MapCreate/CounterCreate alongside wire format (RTLMV4j5/RTLCV4g5)
214215

@@ -253,14 +254,16 @@ Uses `setup_synced_channel()` from helper.
253254

254255
### `unit/path_object_subscribe.md` -- Path-Based Subscriptions
255256

256-
- **RTPO19:** subscribe returns Subscription, listener receives PathObjectSubscriptionEvent
257-
- **RTPO19b1:** depth filtering -- depth=1 (self only), depth=2 (self+children), undefined (all)
258-
- **RTPO19b1d:** non-positive depth throws 40003
259-
- **RTPO19e:** follows path not identity -- object replacement at path -> subscription tracks new object
260-
- **RTPO19f:** child events bubble up to parent subscription
261-
- **RTO24b3:** depth formula: `eventPath.length - subscriptionPath.length + 1 <= depth`
262-
- **RTO24b5:** listener exception caught, doesn't affect other listeners
263-
- **RTPO20:** unsubscribe deregisters
257+
- **RTPO19:** subscribe returns Subscription (RTPO19d), listener receives PathObjectSubscriptionEvent (RTPO19e)
258+
- **RTPO19b:** checks RTO25 access API preconditions
259+
- **RTPO19c1:** depth filtering -- depth=1 (self only), depth=2 (self+children), undefined (all)
260+
- **RTPO19c1a:** non-positive depth throws 40003
261+
- **RTPO19e2:** event.message carries PublicAPI::ObjectMessage when operation present
262+
- **RTPO19f:** follows path not identity -- object replacement at path -> subscription tracks new object
263+
- **RTO24b2a:** candidate path construction includes map update keys
264+
- **RTO24c1:** coverage rule: prefix match + depth constraint
265+
- **RTO24b2c:** listener exception caught, doesn't affect other listeners
266+
- **RTO24b1:** multi-path dispatch via getFullPaths
264267

265268
### `unit/instance.md` -- Identity-Bound Reference
266269

@@ -287,26 +290,29 @@ Uses `setup_synced_channel()` from helper.
287290
- **RTLM5:** get(key) returns resolved value
288291
- **RTLM10/RTLM11:** entries/keys/values iterate non-tombstoned entries
289292
- **RTLM12/RTLM13:** set/remove construct correct v6 wire ObjectMessages
290-
- **RTLM20:** set with LiveCounterValueType/LiveMapValueType consumes value type
293+
- **RTLM20:** set with LiveCounterValueType/LiveMapValueType evaluates value type
291294
- **RTLM20d/RTLM21d:** echoMessages=false uses publish instead of publishAndApply
292295
- **RTLM24:** clear constructs MAP_CLEAR ObjectMessage
293296

294297
### `unit/live_object_subscribe.md` -- Internal Subscription
295298

296-
- **RTLO4b:** subscribe(listener) registers on internal LiveObject
297-
- **RTLO4c:** unsubscribe removes listener
298-
- Events fire on applyOperation with update details
299+
- **RTLO4b:** subscribe(listener) registers on internal LiveObject, returns Subscription (RTLO4b7)
300+
- **RTLO4b4c3:** dispatch chain: direct listeners → path dispatch → tombstone cleanup
301+
- **RTLO4b4d/e:** LiveObjectUpdate carries objectMessage and tombstone fields
302+
- Subscription#unsubscribe deregisters (idempotent)
303+
- Tombstone update deregisters all direct listeners (RTLO4b4c3c)
299304

300-
### `unit/batch.md` -- Batch API
305+
### `unit/parent_references.md` -- parentReferences Tracking
301306

302-
- **RTPO22/RTINS19:** batch entry points -- resolve to LiveObject, create RootBatchContext, execute fn, flush
303-
- **RTPO22c/RTINS19c:** unresolvable path / non-LiveObject throws 92007
304-
- **RTBC3-11:** read methods delegate to Instance (id, value, get, entries, keys, values, size, compact, compactJson)
305-
- **RTBC4d:** get() wraps result via RootBatchContext#wrapInstance (memoized by objectId -- RTBC16c)
306-
- **RTBC12-15:** write methods (set, remove, increment, decrement) queue message constructors synchronously
307-
- **RTBC16d:** flush executes constructors, publishes all as single array via RTO15 (NOT publishAndApply)
308-
- **RTBC16e:** closed batch throws 40000 on any method call
309-
- **RTBC16f:** RootBatchContext closed after flush regardless of success/failure
307+
- **RTLO3f:** parentReferences initialized to empty Dict<String, Set<String>>
308+
- **RTLO4g/RTLO4h:** addParentReference/removeParentReference methods
309+
- **RTLO4f:** getFullPaths — DFS traversal of inverse parentReferences graph, simple paths only
310+
- **RTO5c10:** post-sync parentReferences rebuild from LiveMap entries
311+
312+
### `unit/public_object_message.md` -- User-Facing Event Types
313+
314+
- **PAOM1-3:** PublicAPI::ObjectMessage construction from internal ObjectMessage
315+
- **PAOOP1-3:** PublicAPI::ObjectOperation construction, mapCreate/counterCreate resolution from *WithObjectId variants
310316

311317
---
312318

@@ -341,23 +347,23 @@ onMessageFromClient: (msg) => {
341347
## Dependency Ordering (write order)
342348

343349
1. `helpers/standard_test_pool.md`
344-
2. `unit/live_counter.md` -- no dependencies
345-
3. `unit/live_map.md` -- no dependencies
346-
4. `unit/object_id.md` -- no dependencies
347-
5. `unit/objects_pool.md` -- uses LiveCounter/LiveMap concepts
348-
6. `unit/value_types.md` -- uses objectId generation
349-
7. `unit/realtime_object.md` -- uses helper, tests orchestration
350-
8. `unit/live_counter_api.md` -- uses helper
351-
9. `unit/live_map_api.md` -- uses helper
352-
10. `unit/live_object_subscribe.md` -- uses helper
353-
11. `unit/path_object.md` -- uses helper
354-
12. `unit/instance.md` -- uses helper
355-
13. `unit/path_object_mutations.md` -- uses helper
356-
14. `unit/path_object_subscribe.md` -- uses helper
357-
15. `unit/batch.md` -- uses helper, depends on PathObject/Instance concepts
358-
16. `integration/objects_lifecycle_test.md`
359-
17. `integration/objects_sync_test.md`
360-
18. `integration/objects_batch_test.md`
350+
2. `unit/parent_references.md` -- foundational for graph tracking
351+
3. `unit/public_object_message.md` -- standalone type construction
352+
4. `unit/live_counter.md` -- no dependencies
353+
5. `unit/live_map.md` -- no dependencies
354+
6. `unit/object_id.md` -- no dependencies
355+
7. `unit/objects_pool.md` -- uses LiveCounter/LiveMap concepts
356+
8. `unit/value_types.md` -- uses objectId generation
357+
9. `unit/realtime_object.md` -- uses helper, tests orchestration
358+
10. `unit/live_counter_api.md` -- uses helper
359+
11. `unit/live_map_api.md` -- uses helper
360+
12. `unit/live_object_subscribe.md` -- uses helper
361+
13. `unit/path_object.md` -- uses helper
362+
14. `unit/instance.md` -- uses helper
363+
15. `unit/path_object_mutations.md` -- uses helper
364+
16. `unit/path_object_subscribe.md` -- uses helper
365+
17. `integration/objects_lifecycle_test.md`
366+
18. `integration/objects_sync_test.md`
361367
19. `integration/objects_gc_test.md`
362368
20. `integration/proxy/objects_faults.md`
363369

@@ -370,8 +376,8 @@ onMessageFromClient: (msg) => {
370376
| Wire format v6 everywhere | Spec branch uses v6 field names; old v5 names are "replaced by" stubs |
371377
| `appliedOnAckSerials` on RealtimeObject (RTO7b), not on pool | Matches spec's placement; cleared at sync completion (RTO5c9) |
372378
| No REST test files | objects-features.md has no REST API spec points; REST used only for integration fixture provisioning |
373-
| `echoMessages` check retained on mutations | Spec retains RTLC12d, RTLM20d, RTLM21d |
374-
| Batch uses RTO15 (publish), NOT RTO20 (publishAndApply) | RTBC16d says "publishes ... using `RealtimeObject#publish`" -- batch does NOT apply locally on ACK |
379+
| `echoMessages` check moved to RTO26 | RTO26c checks echoMessages=false; callers (PathObject/Instance) enforce via RTO26 |
380+
| Batch API deferred | Not included in current spec revision (a397e34); may be added in a future spec update |
375381
| LiveObject/LiveMap/LiveCounter marked internal but still unit-tested | Direct testing of CRDT logic is essential; public API tests can't cover all edge cases |
376382
| Test IDs use `objects/unit/` prefix | Matches directory structure, not nested under `realtime/` |
377383
| Behavioral GC testing via ADVANCE_TIME | Verify GC through observable consequences (value becomes null, object recreatable) rather than internal pool state inspection |

uts/objects/helpers/standard_test_pool.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,20 @@ map:prefs@1000 (LiveMap, semantics: LWW)
3232
All map entries have timeserial `"t:0"` and `tombstone: false` unless otherwise noted.
3333
All objects have `siteTimeserials: { "aaa": "t:0" }` and `createOperationIsMerged: true` unless otherwise noted.
3434

35+
### Expected parentReferences after sync
36+
37+
After `setup_synced_channel` completes (including the RTO5c10 rebuild), each object's `parentReferences` should be:
38+
39+
| Object | parentReferences |
40+
|--------|-----------------|
41+
| `root` | `{}` (empty -- root is not referenced by any parent) |
42+
| `counter:score@1000` | `{ "root": {"score"} }` |
43+
| `map:profile@1000` | `{ "root": {"profile"} }` |
44+
| `counter:nested@1000` | `{ "map:profile@1000": {"nested_counter"} }` |
45+
| `map:prefs@1000` | `{ "map:profile@1000": {"prefs"} }` |
46+
47+
Only entries whose value is a `LiveObject` (i.e. `data.objectId` is present) contribute to parentReferences. Primitive-valued entries ("name", "age", "active", "data", "avatar", "email", "theme") do not.
48+
3549
---
3650

3751
## STANDARD_POOL_OBJECTS
@@ -216,12 +230,43 @@ build_object_state(objectId, siteTimeserials, opts):
216230
RETURN ObjectMessage(object: state)
217231
```
218232

233+
### ObjectMessage Builder (State wrapper)
234+
235+
Wraps an existing `ObjectState` in an `ObjectMessage` with the `object` field populated. Used when `replaceData` (RTLC6, RTLM6) needs an `ObjectMessage` rather than a bare `ObjectState`.
236+
237+
```pseudo
238+
build_object_message_with_state(objectState):
239+
RETURN ObjectMessage(object: objectState)
240+
```
241+
242+
### PublicAPI::ObjectMessage Builder
243+
244+
Constructs a `PublicAPI::ObjectMessage` from an internal `ObjectMessage` and a channel name, per PAOM3. Used by subscription tests that verify the user-facing message delivered to listeners.
245+
246+
```pseudo
247+
build_public_object_message(objectMessage, channelName):
248+
pub = PublicAPI::ObjectMessage()
249+
pub.channel = channelName
250+
pub.id = objectMessage.id
251+
pub.clientId = objectMessage.clientId
252+
pub.connectionId = objectMessage.connectionId
253+
pub.timestamp = objectMessage.timestamp
254+
pub.serial = objectMessage.serial
255+
pub.serialTimestamp = objectMessage.serialTimestamp
256+
pub.siteCode = objectMessage.siteCode
257+
pub.extras = objectMessage.extras
258+
pub.operation = PublicAPI::ObjectOperation from objectMessage.operation per PAOOP3
259+
RETURN pub
260+
```
261+
219262
---
220263

221264
## Standard Synced-Channel Setup
222265

223266
Used by all mock WebSocket test files. Creates a connected client with a synced channel containing the standard test pool.
224267

268+
After the OBJECT_SYNC sequence completes, the SDK rebuilds parentReferences per RTO5c10: reset all LiveObject parentReferences to empty (RTLO3f2), then iterate all LiveMap entries calling addParentReference (RTLO4g) for each entry whose value is a LiveObject. See "Expected parentReferences after sync" above for the resulting state.
269+
225270
```pseudo
226271
setup_synced_channel(channel_name):
227272
mock_ws = MockWebSocket(

0 commit comments

Comments
 (0)