Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions .claude/skills/uts-to-kotlin/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,9 +420,13 @@ Fix any compilation errors and recompile until clean. Common issues:

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

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

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

### Deviation patterns
### Test patterns for a diagnosed failure

Two patterns are for an **SDK deviation** (both write the spec-correct assertions); the third,
**spec-error fail-fast**, is for a **UTS spec error** and is not a deviation.

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

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

**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.

```kotlin
/**
* @UTS objects/unit/RTLC7c2/local-source-no-sitetimeserials-0
*/
@Test
fun `RTLC7c2 - LOCAL source does not write siteTimeserials`() = runTest {
// SPEC ERROR RTLC7c2: replayed ACK serial "t:1:0" contradicts the harness ("ack-0:0").
// Fix the UTS spec first — see deviations.md (UTS Spec Errors).
fail("UTS spec error RTLC7c2 — fix the spec first; see deviations.md")
}
```

**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.

### Deviations file

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

---

Expand Down
57 changes: 53 additions & 4 deletions .claude/skills/uts-to-kotlin/references/objects-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ doubt, that IDL is the source of truth; this doc is the applied version of it fo
11. [Message / operation types (`PublicAPI::ObjectMessage` →)](#11-messages)
12. [Errors & error codes](#12-errors)
13. [Internal-graph types (unit specs) — important caveat](#13-internal-graph)
14. [Worked example](#14-worked-example)
15. [Quick symbol index](#15-symbol-index)
14. [Integration-test helpers — REST fixture provisioning](#14-integration-helpers)
15. [Worked example](#15-worked-example)
16. [Quick symbol index](#16-symbol-index)

---

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

**Nested cause (`error.cause.code`).** `ErrorInfo` has no `cause` field, so a spec's nested
`error.cause.code` (e.g. `RTO20e`: top-level `92008` plus cause `90000`) lives on the **exception's** Java
cause — the objects layer sets it to the underlying `AblyException`. Read it by casting the cause:

```kotlin
assertEquals(92008, ex.errorInfo.code)
assertEquals(90000, assertIs<AblyException>(ex.cause).errorInfo.code)
```

---

## 13. Internal-graph types (unit specs) — important caveat <a id="13-internal-graph"></a>
Expand Down Expand Up @@ -554,9 +564,48 @@ wire `action` / `semantics` are integer enum codes — the builders emit the cod
> is implemented. (`buildPublicObjectMessage` does *not* depend on this — the message/operation layer is
> implemented, so those tests can run now.)

(For the **integration** tier's REST fixture helper — `provision_objects_via_rest` — see §14.)

---

## 14. Integration-test helpers — REST fixture provisioning (`standard_test_pool.md` → integration `Helpers.kt`) <a id="14-integration-helpers"></a>

Some objects **integration** specs (tier `integration/standard`) seed object state over REST *before* the
realtime client connects, via the spec's `## REST Fixture Provisioning` helper `provision_objects_via_rest`.
Its ably-java translation lives in
`uts/src/test/kotlin/io/ably/lib/uts/integration/standard/liveobjects/Helpers.kt` (package
`io.ably.lib.uts.integration.standard.liveobjects`) — **call it; don't hand-roll the REST request or payload
JSON.** (Currently only `objects/integration/RTPO15` uses it.) Unlike the unit helpers (§13), this needs no
reflection and no `:liveobjects` dependency — it compiles and runs against `:java`'s public `AblyRest`.

| Spec helper / operation shape | integration `Helpers.kt` |
|---|---|
| `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`) |
| op `{ mapSet: { key, value }, objectId/path }` | `mapSetOp(key, value, objectId = …, path = …, id = …)` |
| op `{ mapRemove: { key }, objectId/path }` | `mapRemoveOp(key, objectId = …, path = …, id = …)` |
| op `{ mapCreate: { semantics: 0, entries }, [objectId/path] }` | `mapCreateOp(entries: Map<String, JsonObject>, semantics = 0, objectId = …, path = …, id = …)` |
| op `{ counterCreate: { count }, [objectId/path] }` | `counterCreateOp(count, objectId = …, path = …, id = …)` |
| op `{ counterInc: { number }, objectId/path }` | `counterIncOp(number, objectId = …, path = …, id = …)` |
| value `{ string }` / `{ number }` / `{ boolean }` / `{ bytes }` / `{ objectId }` | `valueString` / `valueNumber` / `valueBoolean` / `valueBytes` / `valueObjectId` (each → `JsonObject`; `valueString` / `valueBytes` take an optional `encoding`) |

> **V2 REST format.** These builders follow the LiveObjects **V2** objects REST API (the OpenAPI is the
> source of truth): `POST /channels/{channel}/object` (**singular**), body is a single operation **or** a
> bare array (no `messages` wrapper), each op named by its payload key (`mapSet` / `mapRemove` / `mapCreate`
> / `counterInc` / `counterCreate`) with an `objectId`/`path` target (and optional idempotency `id`). The
> spec's `standard_test_pool.md` originally showed the legacy `POST …/objects` + `{ messages: [...] }`
> envelope on the legacy `sandbox-rest.ably.io` host; both were aligned upstream — to this V2 shape and to
> the canonical nonprod sandbox host `sandbox.realtime.ably-nonprod.net` — in ably/specification#497.
>
> **Sandbox host.** `provisionObjectsViaRest` sets `restHost = SandboxApp.sandboxHost`
> (`sandbox.realtime.ably-nonprod.net`) — the same nonprod host `SandboxApp` and the realtime clients use,
> **not** `environment="sandbox"` (which resolves to the legacy `sandbox-rest.ably.io`, and
> can't be combined with `restHost` per `Hosts.java` TO3k2/TO3k3). The REST call hits the live sandbox
> today; the realtime client it provisions for only *observes* the data once the SDK's OBJECT_SYNC +
> `RealtimeObject.get()` land.

---

## 14. Worked example <a id="14-worked-example"></a>
## 15. Worked example <a id="15-worked-example"></a>

Spec pseudocode (public-API style):

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

---

## 15. Quick symbol index <a id="15-symbol-index"></a>
## 16. Quick symbol index <a id="16-symbol-index"></a>

| ably-js / spec symbol | ably-java |
|---|---|
Expand Down
10 changes: 10 additions & 0 deletions uts/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,21 @@ tasks.withType<Test>().configureEach {
tasks.register<Test>("runUtsUnitTests") {
filter {
includeTestsMatching("io.ably.lib.uts.unit.*")
// liveobjects has no SDK implementation yet, so these translate-only tests fail at
// runtime with "LiveObjects plugin hasn't been installed". Exclude them from the run
// (they still compile via compileTestKotlin).
// TODO: This should be removed once liveobjects implementation is in place.
excludeTestsMatching("io.ably.lib.uts.unit.liveobjects.*")
}
}

tasks.register<Test>("runUtsIntegrationTests") {
filter {
includeTestsMatching("io.ably.lib.uts.integration.*")
// liveobjects has no SDK implementation yet — exclude the translate-only liveobjects
// tests (standard + proxy) from the run; they still compile via compileTestKotlin.
Comment on lines 55 to +59
// TODO: This should be removed once liveobjects implementation is in place.
excludeTestsMatching("io.ably.lib.uts.integration.standard.liveobjects.*")
excludeTestsMatching("io.ably.lib.uts.integration.proxy.liveobjects.*")
}
}
Loading
Loading