Skip to content

Commit b877c1c

Browse files
owenpearsonclaude
andcommitted
test: derive the channel unit specs
Covers channel/publish.md, publish_result.md, history.md, get_message.md, update_delete_message.md, message_versions.md, annotations.md, idempotency.md and rest_channel_attributes.md. Three tests carry the deviation mark: the two covering RestChannel#setOptions, which the REST channel's options setter cannot accept, and RSL1i, where a REST publish never checks the message size. Two more assert what features.md contradicts and carry the spec_error mark. The adapted tests record the channel name encoding as it stands, including the space that travels as `+` and the unescaped name in Channel#status. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 65e4be2 commit b877c1c

11 files changed

Lines changed: 2128 additions & 2 deletions

‎test/uts/deviations.md‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ The tests gated this way:
3333

3434
| Test | Spec error |
3535
|---|---|
36+
| `test_rsl1a_publish_message_array` | RSL1c - an object payload asserted to travel unstringified |
37+
| `test_rsl1k_mixed_ids_in_batch` | RSL1k - an absent id in a mixed batch asserted to be generated |
3638

3739
| `test_rsp4_history_pagination` | RSP4 - wire action 4 asserted to be LEAVE |
3840
| `test_tp3_presence_to_json` | TP3 - an outgoing action asserted as the string `"enter"` |
@@ -66,6 +68,33 @@ The assertions are sound; the points they are filed under are not.
6668

6769
The cost is coverage: TI2, TI3 and TI5 *as specified* are untested by the suite.
6870

71+
### Generated message IDs are asserted to be URL-safe base64
72+
73+
`RSL1k2/message-id-format-0` and `RSAN1c4/idempotent-id-generated-0` assert
74+
`[A-Za-z0-9_-]+`. `features.md` RSL1k1 requires only "base64-encoding a sequence of
75+
at least 9 bytes" and names no alphabet, so the specification is stricter than its
76+
source and rejects a conforming SDK's ids at random. ably-js encodes with the
77+
standard alphabet and would fail both.
78+
79+
ably-python now encodes URL-safe, so both tests are derived and pass. That settles
80+
the tests, not the specification: RSL1k1 still needs either to name the alphabet or
81+
to let the tests accept both.
82+
83+
### Wire encodings contradict the features spec
84+
85+
- `publish.md` RSL1c asserts `body[1]["data"] == {"key": "value"}`. RSL4c3 and RSL4d3
86+
require an object payload to be JSON-stringified with `encoding` set to `"json"`.
87+
- `publish.md` RSL1e asserts exact equality against a whole message body, which
88+
cannot hold while idempotent publishing (RSL1k1, on by default) adds an `id`.
89+
- `RSL1k/mixed-ids-in-batch-1` expects a generated id for the id-less message in a
90+
mixed batch. RSL1k1 generates ids only when every message lacks one, and RSL1k3
91+
requires ids "present or absent" to be preserved.
92+
- `batch_publish.md` RSC22c6 asserts base64 with `encoding: "base64"` for binary
93+
payloads, which is the RSL4d1 JSON-protocol branch. The test never sets
94+
`useBinaryProtocol: false` and TO3f defaults it true, so RSL4c1 governs.
95+
- `auth_scheme.md` asserts a raw `Bearer <token>`. RSA3b makes Base64 optional, and
96+
ably-python and ably-js both encode.
97+
6998
### Fixtures that cannot hold their stated values
7099

71100
- `RSP5/decode-cipher-channel-7`: the ciphertext is 32 bytes, an IV plus one AES-CBC
@@ -128,9 +157,10 @@ the mark is the only change needed once the SDK behaviour lands.
128157
| Spec points | Missing | Tests |
129158
|---|---|---|
130159

160+
| RSL7 | `RestChannel#setOptions`. The realtime channel implements it; the REST `options` setter expects the kwargs dict `Channels.get` collected, so a `ChannelOptions` raises `TypeError` | 2 |
131161
| RSP3a2, RSP3a3 | `clientId` and `connectionId` filters on `RestPresence#get`. `Presence.get` takes only `limit`, while `Presence.history` does take its documented params | 3 |
132162
| TP5 | `size` on `PresenceMessage`. The related `maxMessageSize` gap is adapted rather than gated, below; `features.md` TM6 has no UTS test | 1 |
133-
163+
| RSL1i | REST publish never calls `validate_message_size`. The helper exists and is correct, but only `ably/realtime/channel.py:423` calls it, so an oversized REST publish goes out | 1 |
134164
| RSC2, RSC3, RSC4, TO3b, TO3c, TO3c2 | `log_handler` as a client option, and any use of `log_level` — it is stored on `Options` and read by nothing | 4 |
135165
| TI4, TI1/TI5 | `href` anywhere in the SDK, and `cause` when deserialising. `AblyException.from_dict` and `raise_for_response` read only `message`, `statusCode` and `code`, so both fields are dropped from server errors | 2 |
136166
| TP3a, TP3d, TP3g | Presence attributes defaulted from the encapsulating ProtocolMessage. There is no ProtocolMessage type; `ably/realtime/channel.py:751-761` passes the presence array through without context. Matters for synthesized-leave detection and `memberKey` | 3 |
@@ -154,10 +184,13 @@ comment above. These run, so they guard against regression.
154184

155185
| Spec points | Specification | ably-python | Status |
156186
|---|---|---|---|
157-
187+
| RSL2 | A space in a channel name is `%20` | `+`, from `parse.quote_plus` in `Channel.__init__`. `quote_plus` is form encoding, and a `+` in a URL *path* is a literal plus, so the name reaching the server is altered | Open bug, and a genuine correctness issue |
188+
| RSL8 | `Channel#status` URI-encodes the channel id | `status()` interpolates the name with no escaping at all. `a/b` addresses the wrong resource, `a?b` truncates the name into a query string, `a#b` becomes a fragment | Open bug |
189+
| RSL2, RSL11b, RSL15b | `:` is `%3A` | Left literal, from `safe=':'`. RFC 3986 allows `:` in a path segment and Ably uses it for namespaces, so the server receives the same value | Intentional |
158190
| RSC1b | Error code 40106 | A bare `ValueError` from `AblyRest.__init__` with an informative message, not an `AblyException`, so there is no code | Open bug |
159191
| RSC18 | The constructor rejects basic auth over HTTP | Construction succeeds; 40103 is raised from `make_request` when a request needing Basic Auth is attempted, and no request goes out. RSA1/RSC18 say only "any attempt to use" | Compliant; the UTS is stricter than its source |
160192
| REC1b1, REC1c1 | Code 40000, or a message containing "invalid" or "conflict" | 400/40106 with a specific message. The features spec mandates no code | Cosmetic |
193+
| RSAN1a3 | Code 40003 for a missing `Annotation.type` | 400/40000 | Cosmetic; worth aligning cross-SDK |
161194

162195
| HP6 | `errorCode` is a number | The raw header string, `'40101'` | Open bug, trivial |
163196
| HP8 | `headers` is a map | A list of `(name, value)` pairs, so the lookup the spec describes is impossible without converting, and case-insensitivity is lost | Open bug; changing the return type is breaking |
@@ -166,6 +199,8 @@ comment above. These run, so they guard against regression.
166199
| TI | `ErrorInfo` equality by attributes | No `__eq__`, so errors compare by identity. Python exceptions conventionally do, and the requirement appears nowhere in `features.md`. Adding `__eq__` without `__hash__` would make `AblyException` unhashable and break any caller that puts one in a set | Intentional |
167200
| TD5, RSA16a | `capability` is stringified JSON | A `Capability` object, a public convenience type used throughout `auth`. Narrowing the return type to `str` would break every caller that indexes or mutates it, so it is reserved for a future major | Intentional; a breaking change to align |
168201

202+
| CHM2 | Missing metrics default to 0 | `ChannelMetrics.from_dict` uses a bare `obj.get(name)`, so any omitted metric parses as `None` | Open bug, broader than CHM2g/h |
203+
| CHM2g, CHM2h | `objectPublishers` and `objectSubscribers` on `ChannelMetrics` | Neither is modelled, so both are dropped on parsing. The test asserts their absence, and turns red once they are added | Open bug |
169204
| TO3l8 | `maxMessageSize` is a client option, default 65536 | Rejected by `Options.__init__`. `ably/realtime/channel.py:422` reads it with `getattr(..., 65536)`, so the default holds but cannot be configured, nor overridden by `connectionDetails` (CD2c) | Open bug |
170205
| TO3l1, TO3l5 | `httpRequestTimeout` and `httpMaxRetryCount` carry their defaults on the options object | Left unset; the effective defaults are applied downstream by `Http` and by `Options.__get_hosts`. The spec's values are milliseconds, while ably-python's `http_request_timeout` is seconds | Intentional |
171206

‎test/uts/rest/unit/channel/__init__.py‎

Whitespace-only changes.

0 commit comments

Comments
 (0)