Skip to content
Merged
7 changes: 6 additions & 1 deletion specifications/objects-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ Objects feature enables clients to store shared data as "objects" on a channel.
- `(RTO3a)` `ObjectsPool` is a `Dict<String, LiveObject>` - a map of `LiveObject`s keyed by [`objectId`](../features#OST2a) string
- `(RTO3b)` It must always contain an `InternalLiveMap` object with id `root`
- `(RTO3b1)` Upon initialization of the `ObjectsPool`, create a new `InternalLiveMap` per [RTLM4](#RTLM4) with `objectId` set to `root` and add it to the `ObjectsPool`
- `(RTO4)` When a channel `ATTACHED` `ProtocolMessage` is received, the client library must perform the following actions in order. The `ProtocolMessage` may contain a `HAS_OBJECTS` bit flag (see [TR3](../features#TR3)); note that some of the following actions are conditional on this flag.
- `(RTO4)` When a channel `ATTACHED` `ProtocolMessage` is received, the client library must perform the following actions in order. The `ProtocolMessage` may contain a `HAS_OBJECTS` bit flag (see [TR3](../features#TR3)); note that some of the following actions are conditional on this flag. For transitions to channel states other than `ATTACHED`, see [RTO27](#RTO27).
- `(RTO4c)` The [RTO17](#RTO17) sync state must transition to `SYNCING` if not already `SYNCING`
- `(RTO4d)` The `bufferedObjectOperations` list must be cleared without applying any buffered operations
- `(RTO4a)` If the `HAS_OBJECTS` flag is 1, the server will shortly perform an `OBJECT_SYNC` sequence as described in [RTO5](#RTO5). Note that this does not imply that objects are definitely present on the channel, only that there may be; the `OBJECT_SYNC` message may be empty
Expand Down Expand Up @@ -192,6 +192,11 @@ Objects feature enables clients to store shared data as "objects" on a channel.
- `(RTO5c5)` The `bufferedObjectOperations` list must be cleared
- `(RTO5c9)` The `appliedOnAckSerials` set ([RTO7b](#RTO7b)) must be cleared. A state sync causes the channel's LiveObjects data to be replaced, so after a state sync the `appliedOnAckSerials` no longer accurately describes which operations have been applied to the channel's LiveObjects data
- `(RTO5c8)` The [RTO17](#RTO17) sync state must transition to `SYNCED`
- `(RTO27)` When the channel transitions to a state other than `ATTACHED`, the client library must manage the stored objects data as follows (the `ATTACHED` transition is handled by [RTO4](#RTO4); for the effect of these transitions on an in-progress `publishAndApply`, see [RTO20e1](#RTO20e1)):
- `(RTO27a)` When the channel transitions to the `DETACHED` or `FAILED` state, the current state of the objects data can no longer be known, so the client library must:
- `(RTO27a1)` For every object in the internal `ObjectsPool`, clear its internal data, resetting it to the zero value for its type (an empty map, or a counter with value `0`), without emitting any `LiveObjectUpdate` events. The objects themselves remain in the `ObjectsPool`; only their data is cleared
- `(RTO27a2)` The `SyncObjectsPool` must be cleared
- `(RTO27b)` When the channel transitions to any other state (for example `SUSPENDED`, `INITIALIZED`, `ATTACHING`, or `DETACHING`), the client library must retain the stored objects data unchanged. In the `SUSPENDED` case in particular, the connection may still recover and the retained data remains a valid best-effort local copy
- `(RTO6)` Certain object operations may require creating a new object if one does not already exist in the internal `ObjectsPool` for the given `objectId`. This can be done as follows:
- `(RTO6a)` If an object with `objectId` exists in `ObjectsPool`, do not create a new object
- `(RTO6b)` The expected type of the object can be inferred from the provided `objectId`:
Expand Down
8 changes: 8 additions & 0 deletions uts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ ASSERT error.code == 40160
AWAIT_STATE client.connection.state == ConnectionState.connected
```

**Identifier naming.** Identifiers that name SDK/spec surface mirror the specification's own names and
casing — `PascalCase` types (`ObjectsPool`), `camelCase` fields and methods (`bufferedObjectOperations`,
`applyOperation`) — so they grep straight back to the spec. Behaviours the spec describes but does not
name get a `camelCase` coinage in the same style (`processAttached`, `processChannelState`), never
`snake_case`. `snake_case` is reserved for test-harness constructs (`setup_synced_channel`, `mock_ws`,
`send_to_client`, `build_*`, `poll_until`). Spec *file* names are `snake_case` (`objects_pool.md`) —
unrelated to symbol casing. Full rules: [docs/writing-test-specs.md](docs/writing-test-specs.md#identifier-naming).

Pseudocode maps to language idioms rather than prescribing exact syntax:

- **Absent values**: `== null` means the language-appropriate "absent" value — `undefined` in
Expand Down
41 changes: 31 additions & 10 deletions uts/docs/writing-derived-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,15 @@ If the translation is wrong, fix the test. No deviation entry needed.

If the UTS spec is correct per the features spec, and the test accurately translates it, then the SDK has a deviation. In this case:

- Keep the test, but adapt it to pass against the SDK's current behaviour
- Keep the test and handle it as an SDK deviation using one of the two patterns in section 3 — an **env-gated skip** or an **adapted assertion** (see there for which to choose)
- Document exactly what the spec requires vs what the SDK does
- Record it in the deviations file

### 3. Test patterns for a diagnosed failure

Once you've diagnosed a failure (section 2), there are three patterns. Two are for an **SDK deviation** (section 2c): **env-gated skip** keeps the spec-correct assertion but skips it unless explicitly enabled, and **adapted assertion** asserts the SDK's actual behaviour with the spec expectation in a comment. The third, **spec-error fail-fast**, is for a **UTS spec error** (section 2a): the spec itself is wrong, so there are no spec-correct assertions to write — the test fails fast and points at the fix instead. (A translation bug, section 2b, isn't a pattern — you just fix the test.)

**Env-gated skip** (preferred) — the test contains the correct spec assertion but is skipped by default. An environment variable enables it on demand:
**Env-gated skip** (preferred *for a deviation you expect to be fixed*) — the test contains the correct spec assertion but is skipped by default. An environment variable enables it on demand:
```
it("RSA7b - clientId from TokenDetails", function() {
// DEVIATION: see deviations.md
Expand All @@ -125,7 +125,7 @@ it("RSA7b - clientId from TokenDetails", function() {
assert client.auth.clientId == "token-client-id"
})
```
This has three advantages:
This has four advantages:
- Normal test runs stay green (deviations are skipped)
- Each deviation is individually reproducible: `RUN_DEVIATIONS=1 <test runner> --grep "RSA7b"`
- Issues filed against the SDK can link to a concrete reproduction command
Expand All @@ -141,9 +141,9 @@ it("RSC1b - no credentials raises error", function() {
assert error.code == 40160
})
```
Use this pattern when the SDK does *something* (just not the right thing) and you want to assert on the actual behaviour to prevent regressions. These tests pass in normal runs.
Use this pattern when the SDK does *something* (just not the right thing) and you want to assert on the actual behaviour to prevent regressions. These tests pass in normal runs. **Prefer this over an env-gated skip when the divergence is permanent or intentional** (the actual behaviour is stable and worth guarding) — a test that runs and asserts real behaviour catches regressions, whereas a spec-correct assertion that is skipped indefinitely verifies nothing. (Distinguish this from *idiomatic naming* — a differently-spelled public API is not a deviation at all — and from *internal-API shape* adaptation in white-box unit tests; see [Idiomatic translation vs genuine deviations](#idiomatic-translation-vs-genuine-deviations) under Recording deviations.)

**Spec-error fail-fast** — for a **UTS spec error** (section 2a), *not* an SDK deviation. An SDK deviation is a permanent fact, so its test stays green (env-gated) or asserts actual behaviour. A spec error is a bug in the source of truth that is fixable, so the opposite applies: the test must **fail immediately** with a message pointing to the deviations entry, forcing the spec to be corrected first rather than papered over:
**Spec-error fail-fast** — for a **UTS spec error** (section 2a), *not* an SDK deviation. An SDK deviation is a fact about the SDK as it stands, so its test stays green — skipped (env-gated) or asserting actual behaviour — and never blocks the suite. A spec error is a bug in the source of truth, so the opposite applies: the test must **fail immediately** with a message pointing to the deviations entry, forcing the spec to be corrected first rather than papered over:
```
it("RTLC7c2 - LOCAL source does not write siteTimeserials", function() {
// SPEC ERROR RTLC7c2: replayed ACK serial "t:1:0" contradicts the harness ("ack-0:0").
Expand Down Expand Up @@ -172,7 +172,7 @@ Test fails
| |
| NO --> Fix the test
| |
| YES --> SDK deviation. Adapt test, record in deviations file
| YES --> SDK deviation. Env-gated skip or adapted assertion (section 3); record in deviations file
```

---
Expand All @@ -185,16 +185,37 @@ When evaluating against an existing implementation, maintain a deviations file (
2. **What the spec says** — quote or paraphrase the features spec
3. **What the SDK does** — concrete observable behaviour
4. **Root cause** (if known) — file, function, mechanism
5. **Test impact** — which test(s) are affected and how they were adapted
5. **Test impact** — which test(s) are affected and how each was handled (env-gated skip, adapted assertion, fail-fast, or skipped stub)
6. **Status** *(for an SDK deviation)* — *open bug* (to be fixed) vs *intentional / SDK-wide deviation* (and why it won't be), so it's clear whether an issue should be filed
7. **Resolution** *(once resolved)* — the outcome after the deviation is resolved by analysis or a spec change (e.g. reclassified as a cross-SDK design divergence, or a spec point revised upstream); omit until there is one

Deviations are grouped into four sections:
Where a suite has recurring *internal-shape* differences (see **Adapted Tests** below), define a short **shape-deviation vocabulary** once — e.g. `S-1…S-n`, as the objects unit suite does in its mapping reference — and cite the tag from each affected entry rather than re-explaining the same structural difference per test.

Deviations are grouped into four sections. **Keep all four headings present in every `deviations.md`, in this order** — and mark an empty category `*(none)*` rather than deleting its heading. This way every entry sits unambiguously under one category, and a reader can tell an empty category (nothing found) from a forgotten one:
- **UTS Spec Errors** — the UTS spec itself is wrong (contradicts the features spec, or is internally inconsistent). The test **fails fast** pointing here (see "Spec-error fail-fast"); the fix belongs in the spec, not the SDK. Unlike the categories below, this is not an SDK problem.
- **Failing Tests** — SDK non-compliance where the spec-correct test is present but skipped (env-gated). These are the primary output — each maps to a potential issue to file.
- **Adapted Tests** — SDK non-compliance where the test was adapted to assert actual behaviour. The test passes but documents a genuine deviation.
- **Adapted Tests** — SDK non-compliance where the test asserts the SDK's *actual* behaviour instead of the spec's. It passes, guards against regressions, and documents the deviation. **Whenever the actual behaviour is stable and assertable, prefer this and record the test here** — a running adapted test is worth more than a spec-correct assertion skipped indefinitely. (Reserve *Failing Tests* for deviations you expect to be fixed, where preserving the spec-correct assertion as the target matters.)
- **Mock Infrastructure Limitations** — tests that can't be implemented due to missing mock capabilities (e.g. msgpack support). These are skipped stubs, not SDK deviations.

This file is valuable output. It gives the SDK team a precise catalogue of spec gaps, each with a failing test that can be turned on once the fix lands.

### Idiomatic translation vs genuine deviations

A test that doesn't read word-for-word like the pseudocode is **not** automatically a deviation. Before recording anything, decide which of three situations you're in — only the last two are deviations:

**1. Idiomatic naming / rendering — not a deviation. Just translate it; record nothing.**
The public API is legitimately spelled differently in this language: a different method name, a property or getter where the spec writes a call, an idiomatic enum value (`MapSemantics.LWW` vs the wire string `'lww'`), `toJson()` rendered as `toMap()` / `to_dict()`, or a keyword that must be escaped (`channel.object` → `` channel.`object` `` in Kotlin). This is ordinary translation — see Phase 1 § *Map pseudocode to language idioms*, the naming rules in `writing-test-specs.md` § *Identifier Naming*, and the SDK's own mapping reference (e.g. ably-java's `objects-mapping.md`). The behaviour is identical; only the spelling changed, so there is nothing to record.

**2. Public behaviour differs — a deviation, at any tier.**
The SDK produces an observably different *value or effect* through its public surface — e.g. a different error code (`40106` vs `40160`). Assert the actual behaviour with the spec expectation in a comment (the *adapted assertion* pattern above), or env-gate the spec-correct assertion (a *Failing Test*). This is the ordinary deviation case and applies at unit, integration and proxy tiers alike.

**3. Internal-API shape differs — a deviation, at the unit tier only.**
Some *unit* specs are deliberately white-box: they drive internal classes and methods directly (an internal apply/operation method, the object pool, an internal "update" object). Unlike the public API, internal APIs are **not** standardised across SDKs — a boolean return where the spec models a returned object, an event where it models a return value, a missing accessor — so a white-box unit test adapts to the SDK's internal surface. Two rules keep it honest:
- **Preserve the coverage** — assert the equivalent *observable* (the resulting object state, the emitted event, the tombstone flag, …). Adapting the *shape* of the check is fine; **dropping** it is not. If an internal difference genuinely removes an observable, that is a real deviation, not an adaptation.
- **Name recurring differences once** as a shape-deviation vocabulary (`S-1…S-n`) and cite the tag per test, so each entry stays about *that* test rather than re-explaining the mechanism.

Because **integration and proxy specs exercise only the public API**, case 3 never applies at those tiers — such a test must not reach into internal APIs to make itself pass. If one appears to need that, it is really case 2 (a public-behaviour deviation) or a translation bug. And never log case 1 (idiomatic naming) as a deviation — the deviations file is for behaviour the SDK gets *wrong*, not for how its API is spelled.

### Filing issues from deviations

Once the test suite is complete, classify the deviations into distinct issues grouped by root cause or theme — not one issue per test. For example, five tests that all fail because `auth.clientId` isn't derived from token details are one issue, not five.
Expand Down Expand Up @@ -224,7 +245,7 @@ When writing tests before an implementation exists:

### Check the SDK's API surface

Not everything in the UTS pseudocode maps 1:1 to every SDK. Before writing tests, verify that the API exists. If an API is missing or named differently, note it and adapt the test.
Not everything in the UTS pseudocode maps 1:1 to every SDK. Before writing tests, verify that the API exists. An API that is merely *named* differently is idiomatic translation — use the SDK's name and move on; there is nothing to record (see [Idiomatic translation vs genuine deviations](#idiomatic-translation-vs-genuine-deviations)). An API that is genuinely *missing* is different: note it — the test may be not-applicable to this SDK, or the absence may itself be a deviation to record.

### Required options vary by SDK

Expand Down
Loading
Loading