Skip to content

Commit 39e8d09

Browse files
authored
Merge pull request #1222 from ably/feature/uts-liveobjects-unit-tests
[AIT-1008] Translate LiveObjects `objects` UTS test specs to Kotlin tests
2 parents e2ad9b5 + f255792 commit 39e8d09

19 files changed

Lines changed: 5662 additions & 15 deletions

.claude/skills/uts-to-kotlin/SKILL.md

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -420,9 +420,13 @@ Fix any compilation errors and recompile until clean. Common issues:
420420

421421
## Step 6 — Run tests *(evaluate mode only)*
422422

423-
Skip this whole step in "translate only" mode. In "translate and evaluate" mode, run the test and **keep
424-
fixing until it passes** — either the spec-correct assertion passes, or it's deliberately gated/adapted as a
425-
documented deviation (below). A red test is never an acceptable end state here.
423+
Skip this whole step in "translate only" mode. In "translate and evaluate" mode, run the test and resolve
424+
every failure via the decision tree below. Each test must end in exactly one of these states — never an
425+
**unexplained** red:
426+
- the spec-correct assertion **passes**; or
427+
- a documented **SDK deviation** — env-gated or adapted, stays green (SDK ≠ spec; fix belongs in the SDK); or
428+
- a documented **UTS spec error****fails fast** (the spec is wrong; fix belongs in the spec). This is the
429+
one acceptable red.
426430

427431
Use the per-tier task that matches the chosen tier (both are registered in `uts/build.gradle.kts`), and the
428432
resolver's `package` + the spec's `className` for the `--tests` filter:
@@ -442,15 +446,19 @@ Handle test failures using this decision tree (the **Required reading** doc you
442446
```
443447
Test fails
444448
|
445-
+-- Does UTS spec match features spec?
446-
| NO → fix test, record UTS spec error in deviations file
449+
+-- Does UTS spec match features spec? (fetch the features spec — see Required reading)
450+
| NO → UTS SPEC ERROR — fix the spec at source, not the test. Record in deviations.md
451+
| (UTS Spec Errors) + emit a fail-fast test (below) so it's fixed early.
447452
| YES
448453
| +-- Does test accurately translate the UTS spec?
449454
| NO → fix the test (no deviation entry needed)
450455
| YES → SDK deviation — adapt test, record in deviations file
451456
```
452457

453-
### Deviation patterns
458+
### Test patterns for a diagnosed failure
459+
460+
Two patterns are for an **SDK deviation** (both write the spec-correct assertions); the third,
461+
**spec-error fail-fast**, is for a **UTS spec error** and is not a deviation.
454462

455463
**Env-gated skip (preferred)** — test contains spec-correct assertions but is skipped by default:
456464

@@ -474,15 +482,28 @@ fun `RSA4c2 - callback error connecting disconnected`() = runTest {
474482
assertEquals(40160, error.errorInfo.code)
475483
```
476484

485+
**Spec-error fail-fast** *(UTS spec error — NOT an SDK deviation)* — when the decision tree's first branch is **NO** (the UTS spec contradicts the features spec, or is internally inconsistent), the spec is the fixable source of truth, so the test must **fail loudly** pointing at the deviation entry, rather than be quietly adapted to green. This is the opposite of the SDK-deviation patterns above (which stay green / assert actual behaviour) — failing fast is the forcing function that gets the spec fixed early.
486+
487+
```kotlin
488+
/**
489+
* @UTS objects/unit/RTLC7c2/local-source-no-sitetimeserials-0
490+
*/
491+
@Test
492+
fun `RTLC7c2 - LOCAL source does not write siteTimeserials`() = runTest {
493+
// SPEC ERROR RTLC7c2: replayed ACK serial "t:1:0" contradicts the harness ("ack-0:0").
494+
// Fix the UTS spec first — see deviations.md (UTS Spec Errors).
495+
fail("UTS spec error RTLC7c2 — fix the spec first; see deviations.md")
496+
}
497+
```
498+
477499
**Never use the accommodate-both pattern** (accept either spec or SDK behaviour). Every test must assert either spec behaviour or the SDK's actual behaviour — never both at once.
478500

479501
### Deviations file
480502

481-
Append to `uts/src/test/kotlin/io/ably/lib/uts/deviations.md`. Each entry needs:
482-
1. The spec point (e.g. `RSA4c2`)
483-
2. What the spec says
484-
3. What the SDK does
485-
4. Which test is affected and how it was adapted
503+
Append to `uts/src/test/kotlin/io/ably/lib/uts/deviations.md`, using the manual's **Recording deviations**
504+
entry format and sections. The ably-java-specific mapping: a **UTS Spec Error** (test fails fast — fix in
505+
the spec) goes under the manual's *UTS Spec Errors* section; an **SDK deviation** (env-gated/adapted — fix
506+
in the SDK) goes under *Failing Tests* / *Adapted Tests*.
486507

487508
---
488509

.claude/skills/uts-to-kotlin/references/objects-mapping.md

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ doubt, that IDL is the source of truth; this doc is the applied version of it fo
2727
11. [Message / operation types (`PublicAPI::ObjectMessage` →)](#11-messages)
2828
12. [Errors & error codes](#12-errors)
2929
13. [Internal-graph types (unit specs) — important caveat](#13-internal-graph)
30-
14. [Worked example](#14-worked-example)
31-
15. [Quick symbol index](#15-symbol-index)
30+
14. [Integration-test helpers — REST fixture provisioning](#14-integration-helpers)
31+
15. [Worked example](#15-worked-example)
32+
16. [Quick symbol index](#16-symbol-index)
3233

3334
---
3435

@@ -481,6 +482,15 @@ Assert the code as a plain int — `assertEquals(90001, ex.errorInfo.code)` —
481482
tags). The `90000` a spec injects via a mocked `ERROR`/`DETACHED` `ProtocolMessage` is the channel-level
482483
error, not an objects code — it's what drives the channel into the state that makes the objects call fail.
483484

485+
**Nested cause (`error.cause.code`).** `ErrorInfo` has no `cause` field, so a spec's nested
486+
`error.cause.code` (e.g. `RTO20e`: top-level `92008` plus cause `90000`) lives on the **exception's** Java
487+
cause — the objects layer sets it to the underlying `AblyException`. Read it by casting the cause:
488+
489+
```kotlin
490+
assertEquals(92008, ex.errorInfo.code)
491+
assertEquals(90000, assertIs<AblyException>(ex.cause).errorInfo.code)
492+
```
493+
484494
---
485495

486496
## 13. Internal-graph types (unit specs) — important caveat <a id="13-internal-graph"></a>
@@ -554,9 +564,48 @@ wire `action` / `semantics` are integer enum codes — the builders emit the cod
554564
> is implemented. (`buildPublicObjectMessage` does *not* depend on this — the message/operation layer is
555565
> implemented, so those tests can run now.)
556566
567+
(For the **integration** tier's REST fixture helper — `provision_objects_via_rest` — see §14.)
568+
569+
---
570+
571+
## 14. Integration-test helpers — REST fixture provisioning (`standard_test_pool.md` → integration `Helpers.kt`) <a id="14-integration-helpers"></a>
572+
573+
Some objects **integration** specs (tier `integration/standard`) seed object state over REST *before* the
574+
realtime client connects, via the spec's `## REST Fixture Provisioning` helper `provision_objects_via_rest`.
575+
Its ably-java translation lives in
576+
`uts/src/test/kotlin/io/ably/lib/uts/integration/standard/liveobjects/Helpers.kt` (package
577+
`io.ably.lib.uts.integration.standard.liveobjects`) — **call it; don't hand-roll the REST request or payload
578+
JSON.** (Currently only `objects/integration/RTPO15` uses it.) Unlike the unit helpers (§13), this needs no
579+
reflection and no `:liveobjects` dependency — it compiles and runs against `:java`'s public `AblyRest`.
580+
581+
| Spec helper / operation shape | integration `Helpers.kt` |
582+
|---|---|
583+
| `provision_objects_via_rest(api_key, channel_name, operations)` | `provisionObjectsViaRest(apiKey, channelName, operations: List<JsonObject>): List<String>` (POSTs the op(s); returns created/updated `objectIds`) |
584+
| op `{ mapSet: { key, value }, objectId/path }` | `mapSetOp(key, value, objectId = …, path = …, id = …)` |
585+
| op `{ mapRemove: { key }, objectId/path }` | `mapRemoveOp(key, objectId = …, path = …, id = …)` |
586+
| op `{ mapCreate: { semantics: 0, entries }, [objectId/path] }` | `mapCreateOp(entries: Map<String, JsonObject>, semantics = 0, objectId = …, path = …, id = …)` |
587+
| op `{ counterCreate: { count }, [objectId/path] }` | `counterCreateOp(count, objectId = …, path = …, id = …)` |
588+
| op `{ counterInc: { number }, objectId/path }` | `counterIncOp(number, objectId = …, path = …, id = …)` |
589+
| value `{ string }` / `{ number }` / `{ boolean }` / `{ bytes }` / `{ objectId }` | `valueString` / `valueNumber` / `valueBoolean` / `valueBytes` / `valueObjectId` (each → `JsonObject`; `valueString` / `valueBytes` take an optional `encoding`) |
590+
591+
> **V2 REST format.** These builders follow the LiveObjects **V2** objects REST API (the OpenAPI is the
592+
> source of truth): `POST /channels/{channel}/object` (**singular**), body is a single operation **or** a
593+
> bare array (no `messages` wrapper), each op named by its payload key (`mapSet` / `mapRemove` / `mapCreate`
594+
> / `counterInc` / `counterCreate`) with an `objectId`/`path` target (and optional idempotency `id`). The
595+
> spec's `standard_test_pool.md` originally showed the legacy `POST …/objects` + `{ messages: [...] }`
596+
> envelope on the legacy `sandbox-rest.ably.io` host; both were aligned upstream — to this V2 shape and to
597+
> the canonical nonprod sandbox host `sandbox.realtime.ably-nonprod.net` — in ably/specification#497.
598+
>
599+
> **Sandbox host.** `provisionObjectsViaRest` sets `restHost = SandboxApp.sandboxHost`
600+
> (`sandbox.realtime.ably-nonprod.net`) — the same nonprod host `SandboxApp` and the realtime clients use,
601+
> **not** `environment="sandbox"` (which resolves to the legacy `sandbox-rest.ably.io`, and
602+
> can't be combined with `restHost` per `Hosts.java` TO3k2/TO3k3). The REST call hits the live sandbox
603+
> today; the realtime client it provisions for only *observes* the data once the SDK's OBJECT_SYNC +
604+
> `RealtimeObject.get()` land.
605+
557606
---
558607

559-
## 14. Worked example <a id="14-worked-example"></a>
608+
## 15. Worked example <a id="15-worked-example"></a>
560609

561610
Spec pseudocode (public-API style):
562611

@@ -597,7 +646,7 @@ wrapped in `LiveMapValue.of`; `at(...)` followed by `asLiveCounter()` before cou
597646

598647
---
599648

600-
## 15. Quick symbol index <a id="15-symbol-index"></a>
649+
## 16. Quick symbol index <a id="16-symbol-index"></a>
601650

602651
| ably-js / spec symbol | ably-java |
603652
|---|---|

uts/build.gradle.kts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,21 @@ tasks.withType<Test>().configureEach {
4444
tasks.register<Test>("runUtsUnitTests") {
4545
filter {
4646
includeTestsMatching("io.ably.lib.uts.unit.*")
47+
// liveobjects has no SDK implementation yet, so these translate-only tests fail at
48+
// runtime with "LiveObjects plugin hasn't been installed". Exclude them from the run
49+
// (they still compile via compileTestKotlin).
50+
// TODO: This should be removed once liveobjects implementation is in place.
51+
excludeTestsMatching("io.ably.lib.uts.unit.liveobjects.*")
4752
}
4853
}
4954

5055
tasks.register<Test>("runUtsIntegrationTests") {
5156
filter {
5257
includeTestsMatching("io.ably.lib.uts.integration.*")
58+
// liveobjects has no SDK implementation yet — exclude the translate-only liveobjects
59+
// tests (standard + proxy) from the run; they still compile via compileTestKotlin.
60+
// TODO: This should be removed once liveobjects implementation is in place.
61+
excludeTestsMatching("io.ably.lib.uts.integration.standard.liveobjects.*")
62+
excludeTestsMatching("io.ably.lib.uts.integration.proxy.liveobjects.*")
5363
}
5464
}

0 commit comments

Comments
 (0)