Skip to content

[AIT-1147] Add objects UTS translation notes and first spec-derived LiveObjects integration tests - #2226

Closed
sacOO7 wants to merge 3 commits into
feature/liveobjects-public-apifrom
feature/liveobjects-uts-tests
Closed

[AIT-1147] Add objects UTS translation notes and first spec-derived LiveObjects integration tests#2226
sacOO7 wants to merge 3 commits into
feature/liveobjects-public-apifrom
feature/liveobjects-uts-tests

Conversation

@sacOO7

@sacOO7 sacOO7 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Builds on #2225 (hence the base branch) — the first UTS work against the new path-based LiveObjects public API.

🎯 What this PR does

Two things, one per commit:

  1. Authors the objects module translation notes for the uts-to-swift skill — references/objects-mapping.md goes from an intentionally-empty placeholder to the full ably-js ⇄ Swift rewrite table the skill requires before translating any objects spec.
  2. Ships the skill's first objects output: three spec-derived integration suites + the module's REST-provisioning helpers, deliberately compile-only (.disabled) until the path-based API is implemented — they pin the API shape today and become runnable by deleting one trait later.

📖 Commit 1 — the translation notes

The objects UTS specs are written against ably-js's dynamically-typed LiveObjects surface; ably-cocoa implements the spec's Typed-SDK variant with two Swift-idiomatic consolidations (a single Primitive view instead of six sub-types; Instance as an exhaustively-switchable enum). Nearly every spec line therefore needs a mechanical rewrite, and the notes are that rewrite table — 16 sections covering the three-layers disambiguation (creation value types vs public views vs internal CRDT nodes), typed-throws async mapping (including the deferred-AWAITTask bridge and its typed-throw erasure), the typed PathObject views, LiveMapValue literal conformances, wrong-type write failure patterns, subscriptions/sync-state events, the public ObjectMessage types, the full spec error-code table, internal-graph caveats with helper-authoring guidance for the unit tier, REST fixture provisioning, a worked example, and a quick symbol index.

Every API claim in the table was verified against the Swift source (a symbol-existence sweep of all referenced signatures), and the pseudocode forms were validated against the actual spec files (which surfaced, e.g., that specs write on(SYNCED, cb) constants and deferred x_future awaits).

As part of this, SKILL.md is scrubbed back to a module-agnostic workflow document: the LiveObjects plugin wiring, natively-async examples, and the stale "objects notes are a placeholder" claim move out; module-specific facts now live where they belong — in the module's translation notes, which the skill already routes to via uts-package-mapping.json.

🧪 Commit 2 — first spec-derived objects integration tests

Suite Spec Covers
ObjectsSyncTests objects_sync_test.md (RTO4/RTO5/RTO17) attach → OBJECT_SYNC → SYNCED, get() resolution, re-attach re-sync
ObjectsLifecycleTests objects_lifecycle_test.md root PathObject shape; REST-provisioned pre-existing data observed after sync
ObjectsGcTests objects_gc_test.md (RTO10/RTLM19) tombstoned counter recreatable under a new objectId

Plus module helpers in integration/standard/objects/helpers/:

  • ObjectsRestProvisioning.swift — the spec's provision_objects_via_rest against the V2 objects REST API (POST /channels/{channel}/object, singular; nonprod sandbox host), returning the created objectIds.
  • ObjectsIntegrationHelpers.swift — plugin-wired client options (objectsClientOptions(key:useBinaryProtocol:)), objects channel builder, non-throwing typed value() readers for pollUntil closures (SE-0230 try?-flattening makes inline reads a footgun), and counterInstanceId(at:).

Package.swift: the UTS test target gains the AblyLiveObjects dependency.

⏸️ Why the suites are .disabled

The path-based API is currently a skeleton — every Default* implementation traps via notImplemented(). Verified empirically: enabling a suite fatal-errors immediately (Fatal error: Not implemented: get()), and since fatalError kills the test process, un-disabled suites would crash the documented swift test --filter UTS workflow for the whole target. So each suite carries:

@Suite(.serialized, .disabled("The path-based LiveObjects public API is not yet implemented (every operation traps via notImplemented()). Compile-only until the implementation lands - then remove this trait."))

This is the UTS "translate-only" deliverable: the compiler keeps every spec-derived call site honest against the API as it evolves, and flipping the suites live is a one-line-per-file change once the implementation lands. The translation notes document this as a standing rule for all generated objects public-API suites.

✅ Verification

  • swift build --build-tests clean (Swift 6 language mode; no @available annotations needed in test code — SPM raises the test deployment target above the package floor).
  • swift test --filter UTS.Objects: all 12 parameterised cases skip cleanly with the disable reason; run green in 0.001s.
  • The existing UTS suites (realtime unit + integration) are unaffected.
  • Skill docs cross-validated three ways: kotlin-doc structural parity, symbol sweep against the Swift source, and pseudocode sampling of the actual uts/objects specs.

Summary by CodeRabbit

  • Documentation

    • Added comprehensive guidance for translating LiveObjects specifications to Swift, including async APIs, typed values, subscriptions, errors, and REST fixtures.
    • Clarified module helper conventions, plugin setup, completeness auditing, and Swift error-handling patterns.
  • Tests

    • Added integration coverage for LiveObjects synchronization, lifecycle, garbage collection, and fault recovery.
    • Added REST integration coverage for history, presence, and publishing across JSON and MessagePack protocols.
  • Known Limitations

    • Documented the unavailable publish-with-parameters capability.
    • LiveObjects path-based tests remain disabled until the public API is implemented.

sacOO7 added 2 commits July 17, 2026 18:12
…e-agnostic

references/objects-mapping.md goes from placeholder stub to the full
ably-js <-> Swift rewrite table for the objects UTS module (the path-based
LiveObjects API): the three-layers disambiguation, entry point, typed-throws
async mapping (incl. deferred-await Task bridging), the three typed PathObject
views with the Primitive consolidation, the Instance enum, LiveMapValue
literals, wrong-type write patterns, subscriptions/sync-state events,
public ObjectMessage types, the full spec error-code table, internal-graph
caveats and helper-authoring guidance, REST fixture provisioning, a worked
example, and a symbol index. Includes the runtime-status warning: the
path-based API is a skeleton (notImplemented() traps), so objects specs are
translate-only and generated suites must carry a .disabled trait.

SKILL.md stays a module-agnostic workflow document: the plugin wiring,
natively-async examples, and stale objects-placeholder note move out (the
concrete wiring now lives in the module's translation notes), replaced by
generic phrasing that defers to each module's notes file.
…disabled)

Three suites derived from uts/objects/integration via the uts-to-swift skill,
against the new path-based LiveObjects public API:

- ObjectsSyncTests (objects_sync_test.md, RTO4/RTO5/RTO17): attach ->
  OBJECT_SYNC -> SYNCED, get() resolution, re-attach re-sync.
- ObjectsLifecycleTests (objects_lifecycle_test.md): root PathObject shape,
  REST-provisioned pre-existing data observed after sync.
- ObjectsGcTests (objects_gc_test.md, RTO10/RTLM19): tombstoned counter is
  recreatable under a new objectId.

Module helpers in integration/standard/objects/helpers/:
ObjectsRestProvisioning.swift (the spec's provision_objects_via_rest against
the V2 objects REST API) and ObjectsIntegrationHelpers.swift (plugin-wired
client options, objects channel builder, non-throwing typed value() readers
for pollUntil closures, counterInstanceId).

The UTS target gains the AblyLiveObjects dependency in Package.swift.

Every suite carries a .disabled trait besides .serialized: the path-based
API's Default* implementations all trap via notImplemented() (verified: an
enabled run fatal-errors in get()), so these tests are the translate-only
deliverable - they pin the API shape at compile time and become runnable by
removing the trait once the implementation lands. swift build --build-tests
is clean and swift test --filter UTS.Objects skips all 12 cases cleanly.
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds UTS-to-Swift translation guidance and a complete LiveObjects mapping reference. Wires LiveObjects into UTS tests and adds disabled object synchronization, lifecycle, garbage-collection, proxy-fault, REST history, presence, and publish integration suites.

Changes

UTS translation and integration coverage

Layer / File(s) Summary
Translation guidance and objects mapping
.claude/skills/uts-to-swift/*, .claude/skills/uts-to-swift/references/objects-mapping.md
Updates translation rules for async APIs, plugins, helpers, polling, placeholders, and audit behavior; replaces the objects mapping placeholder with typed Swift translation guidance.
LiveObjects wiring and REST provisioning
Package.swift, Test/UTS/integration/standard/objects/helpers/*
Adds the LiveObjects test-target dependency and shared client, channel, typed-read, operation-builder, and REST provisioning helpers.
LiveObjects synchronization and lifecycle tests
Test/UTS/integration/standard/objects/ObjectsSyncTests.swift, ObjectsLifecycleTests.swift, ObjectsGcTests.swift
Adds disabled suites covering synchronization, propagation, attach/resync behavior, REST-seeded state, and tombstone recreation.
LiveObjects proxy fault tests
Test/UTS/integration/proxy/objects/ObjectsFaultsTests.swift
Adds disabled macOS tests for disconnects, delayed sync, detach recovery, buffered mutations, and failed sync waits.
REST history, presence, and publish coverage
Test/UTS/integration/standard/rest/*, Test/UTS/deviations.md
Adds sandbox tests for history, presence, and publishing across protocols, and documents the missing publish-with-params API deviation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: maratal, lawrence-forooghian

Poem

I’m a rabbit with tests in a burrow so neat,
LiveObjects hop through each sync and heartbeat.
REST messages sparkle, then presence appears,
Faults learn to recover without any fears.
With Swift notes and helpers aligned,
New paths and old APIs are easier to find.

🚥 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 accurately summarizes the main change: new objects UTS notes plus the first spec-derived LiveObjects integration tests.
Docstring Coverage ✅ Passed Docstring coverage is 98.53% 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/liveobjects-uts-tests

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/2226/features July 17, 2026 12:44 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2226/AblyLiveObjects July 17, 2026 12:45 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2226/jazzydoc July 17, 2026 12:48 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2226/markdown-api-reference July 17, 2026 12:48 Inactive
@sacOO7 sacOO7 changed the title Add objects UTS translation notes and first spec-derived LiveObjects integration tests [AIT-1147] Add objects UTS translation notes and first spec-derived LiveObjects integration tests Jul 21, 2026
Translate the UTS rest integration specs (uts/rest/integration/{publish,
history,presence}.md) into direct-sandbox suites under
Test/UTS/integration/standard/rest, each parameterised over the json/msgpack
protocol variants:

- PublishTests (RSL1d, RSL1n, RSL1k5, RSL1l1, RSL1m4) — RSL1l1 is gated
  behind RUN_DEVIATIONS and recorded in deviations.md (Failing Tests):
  ably-cocoa has no publish-with-params API.
- HistoryTests (RSL2, RSL2a, RSL2b1-b3)
- PresenceTests (RSP1, RSP3/RSP3a1-a2, RSP4/RSP4b1-b3, RSP5) — 17 cases,
  including cipher-decoding fixtures and realtime-provisioned members.

Tests are translated and compiled, not yet evaluated against the sandbox.

Also includes ObjectsFaultsTests, the first proxy-tier objects suite from
the earlier LiveObjects UTS work.

@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: 3

🤖 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 @.claude/skills/uts-to-swift/SKILL.md:
- Around line 938-942: Update the direct-sandbox template’s ARTClientOptions
construction to expose an explicit plugin-builder hook, requiring plugin-backed
modules to install their module helper’s plugin before accessing module entry
points. Preserve plain options for non-plugin modules and direct translators to
the existing Module helpers guidance rather than hand-wiring plugins per test.

In `@Test/UTS/integration/standard/objects/helpers/ObjectsRestProvisioning.swift`:
- Around line 63-96: Update withTarget to reject calls that provide both
objectId and path, and require exactly one target for operations that need an
existing object or path. Preserve targetless payloads for create operations such
as mapCreateOp and counterCreateOp, while ensuring mapSetOp, mapRemoveOp, and
counterIncOp cannot construct targetless payloads; propagate or surface
validation before sending the REST request.

In `@Test/UTS/integration/standard/objects/ObjectsLifecycleTests.swift`:
- Around line 44-56: Update the assertions in this lifecycle test to validate
the captured subscription events in eventsB, not only the value returned by
rootB.get. Assert that the expected greeting update was delivered through
rootB.subscribe while preserving the existing value assertion.
🪄 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 Plus

Run ID: f9d27634-df35-4936-acbb-77df8287157a

📥 Commits

Reviewing files that changed from the base of the PR and between 5c2d28b and 933c0e5.

📒 Files selected for processing (13)
  • .claude/skills/uts-to-swift/SKILL.md
  • .claude/skills/uts-to-swift/references/objects-mapping.md
  • Package.swift
  • Test/UTS/deviations.md
  • Test/UTS/integration/proxy/objects/ObjectsFaultsTests.swift
  • Test/UTS/integration/standard/objects/ObjectsGcTests.swift
  • Test/UTS/integration/standard/objects/ObjectsLifecycleTests.swift
  • Test/UTS/integration/standard/objects/ObjectsSyncTests.swift
  • Test/UTS/integration/standard/objects/helpers/ObjectsIntegrationHelpers.swift
  • Test/UTS/integration/standard/objects/helpers/ObjectsRestProvisioning.swift
  • Test/UTS/integration/standard/rest/HistoryTests.swift
  • Test/UTS/integration/standard/rest/PresenceTests.swift
  • Test/UTS/integration/standard/rest/PublishTests.swift

Comment on lines +938 to +942
For a **plugin-backed module**, the client options must also install the module's plugin — the
module's entry-point property traps without it. The module's translation notes name the exact
wiring, and the module packages it as a client-options builder in its module `helpers/` directory
(see **Module helpers** below) — use that builder; don't hand-wire the plugin per 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 | 🟠 Major | ⚡ Quick win

Make the plugin wiring explicit in the template.

The new rule requires plugin-backed suites to use a module helper, but the direct-sandbox template still constructs plain ARTClientOptions without installing the plugin. A translator following that template can generate tests that trap when accessing channel.object. Add a plugin-builder hook to the template or clearly mark the module-specific wiring step as mandatory.

🤖 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 @.claude/skills/uts-to-swift/SKILL.md around lines 938 - 942, Update the
direct-sandbox template’s ARTClientOptions construction to expose an explicit
plugin-builder hook, requiring plugin-backed modules to install their module
helper’s plugin before accessing module entry points. Preserve plain options for
non-plugin modules and direct translators to the existing Module helpers
guidance rather than hand-wiring plugins per test.

Comment on lines +63 to +96
func mapSetOp(key: String, value: [String: Any], objectId: String? = nil, path: String? = nil, id: String? = nil) -> [String: Any] {
withTarget(["mapSet": ["key": key, "value": value]], objectId: objectId, path: path, id: id)
}

/// `{ mapRemove: { key }, objectId|path, id? }` — remove `key` from the target map.
func mapRemoveOp(key: String, objectId: String? = nil, path: String? = nil, id: String? = nil) -> [String: Any] {
withTarget(["mapRemove": ["key": key]], objectId: objectId, path: path, id: id)
}

/// `{ mapCreate: { semantics, entries }, objectId|path?, id? }` — create a map (semantics 0 = LWW).
/// Entry values are wrapped as `{ data: <value> }` per the V2 schema. A create with no target makes
/// a standalone object.
func mapCreateOp(entries: [String: [String: Any]], semantics: Int = 0, objectId: String? = nil, path: String? = nil, id: String? = nil) -> [String: Any] {
let wrappedEntries = entries.mapValues { value in ["data": value] }
return withTarget(["mapCreate": ["semantics": semantics, "entries": wrappedEntries]], objectId: objectId, path: path, id: id)
}

/// `{ counterCreate: { count }, objectId|path?, id? }` — create a counter.
func counterCreateOp(count: Double, objectId: String? = nil, path: String? = nil, id: String? = nil) -> [String: Any] {
withTarget(["counterCreate": ["count": count]], objectId: objectId, path: path, id: id)
}

/// `{ counterInc: { number }, objectId|path, id? }` — increment the target counter
/// (a negative number decrements).
func counterIncOp(number: Double, objectId: String? = nil, path: String? = nil, id: String? = nil) -> [String: Any] {
withTarget(["counterInc": ["number": number]], objectId: objectId, path: path, id: id)
}

private func withTarget(_ operation: [String: Any], objectId: String?, path: String?, id: String?) -> [String: Any] {
var operation = operation
if let objectId { operation["objectId"] = objectId }
if let path { operation["path"] = path }
if let id { operation["id"] = id }
return operation

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

Validate REST operation targets before sending.

withTarget allows both objectId and path, and also allows neither. This lets mapSetOp, mapRemoveOp, and counterIncOp construct invalid V2 payloads; the failure is deferred to the sandbox as a generic HTTP error. Enforce mutual exclusivity in withTarget, and require one target for the non-create operations.

🤖 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 `@Test/UTS/integration/standard/objects/helpers/ObjectsRestProvisioning.swift`
around lines 63 - 96, Update withTarget to reject calls that provide both
objectId and path, and require exactly one target for operations that need an
existing object or path. Preserve targetless payloads for create operations such
as mapCreateOp and counterCreateOp, while ensuring mapSetOp, mapRemoveOp, and
counterIncOp cannot construct targetless payloads; propagate or surface
validation before sending the REST request.

Comment on lines +44 to +56
// Test Steps
// Client A sets a value
try await rootA.set(key: "greeting", value: "hello")

// Client B subscribes and waits for the update
let eventsB = Captured<PathObjectSubscriptionEvent>()
try rootB.subscribe { event in eventsB.append(event) }
guard await pollUntil("rootB.greeting == \"hello\"", timeout: 10, {
stringValue(at: rootB.get(key: "greeting")) == "hello"
}) else { return }

// Assertions
#expect(stringValue(at: rootB.get(key: "greeting")) == "hello")

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

Subscription events captured but never asserted.

eventsB is populated via rootB.subscribe (Line 50) but nothing in the Assertions block checks its contents — the test only re-reads the value via rootB.get. This leaves the subscription-delivery behavior itself unverified even though setting it up appears to be the point of this test.

✅ Proposed fix to assert on the captured subscription event
                     // Assertions
                     `#expect`(stringValue(at: rootB.get(key: "greeting")) == "hello")
+                    `#expect`(eventsB.values.contains { $0.update["greeting"] != nil })
📝 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
// Test Steps
// Client A sets a value
try await rootA.set(key: "greeting", value: "hello")
// Client B subscribes and waits for the update
let eventsB = Captured<PathObjectSubscriptionEvent>()
try rootB.subscribe { event in eventsB.append(event) }
guard await pollUntil("rootB.greeting == \"hello\"", timeout: 10, {
stringValue(at: rootB.get(key: "greeting")) == "hello"
}) else { return }
// Assertions
#expect(stringValue(at: rootB.get(key: "greeting")) == "hello")
// Test Steps
// Client A sets a value
try await rootA.set(key: "greeting", value: "hello")
// Client B subscribes and waits for the update
let eventsB = Captured<PathObjectSubscriptionEvent>()
try rootB.subscribe { event in eventsB.append(event) }
guard await pollUntil("rootB.greeting == \"hello\"", timeout: 10, {
stringValue(at: rootB.get(key: "greeting")) == "hello"
}) else { return }
// Assertions
`#expect`(stringValue(at: rootB.get(key: "greeting")) == "hello")
`#expect`(eventsB.values.contains { $0.update["greeting"] != nil })
🤖 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 `@Test/UTS/integration/standard/objects/ObjectsLifecycleTests.swift` around
lines 44 - 56, Update the assertions in this lifecycle test to validate the
captured subscription events in eventsB, not only the value returned by
rootB.get. Assert that the expected greeting update was delivered through
rootB.subscribe while preserving the existing value assertion.

@sacOO7

sacOO7 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Closing in favor of https://github.com/ably/ably-cocoa/pull/2235/changes

@sacOO7 sacOO7 closed this Jul 31, 2026
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.

1 participant