Skip to content

Commit 0ba7ed7

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 91e2242 commit 0ba7ed7

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"` |
@@ -86,6 +88,33 @@ The assertions are sound; the points they are filed under are not.
8688

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

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

91120
- `RSP5/decode-cipher-channel-7`: the ciphertext is 32 bytes, an IV plus one AES-CBC
@@ -148,9 +177,10 @@ the mark is the only change needed once the SDK behaviour lands.
148177
| Spec points | Missing | Tests |
149178
|---|---|---|
150179

180+
| 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 |
151181
| RSP3a2, RSP3a3 | `clientId` and `connectionId` filters on `RestPresence#get`. `Presence.get` takes only `limit`, while `Presence.history` does take its documented params | 3 |
152182
| TP5 | `size` on `PresenceMessage`. The related `maxMessageSize` gap is adapted rather than gated, below; `features.md` TM6 has no UTS test | 1 |
153-
183+
| 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 |
154184
| 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 |
155185
| 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 |
156186
| 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 |
@@ -174,10 +204,13 @@ comment above. These run, so they guard against regression.
174204

175205
| Spec points | Specification | ably-python | Status |
176206
|---|---|---|---|
177-
207+
| 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 |
208+
| 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 |
209+
| 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 |
178210
| RSC1b | Error code 40106 | A bare `ValueError` from `AblyRest.__init__` with an informative message, not an `AblyException`, so there is no code | Open bug |
179211
| 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 |
180212
| REC1b1, REC1c1 | Code 40000, or a message containing "invalid" or "conflict" | 400/40106 with a specific message. The features spec mandates no code | Cosmetic |
213+
| RSAN1a3 | Code 40003 for a missing `Annotation.type` | 400/40000 | Cosmetic; worth aligning cross-SDK |
181214

182215
| HP6 | `errorCode` is a number | The raw header string, `'40101'` | Open bug, trivial |
183216
| 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 |
@@ -186,6 +219,8 @@ comment above. These run, so they guard against regression.
186219
| 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 |
187220
| 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 |
188221

222+
| 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 |
223+
| 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 |
189224
| 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 |
190225
| 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 |
191226

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

Whitespace-only changes.

0 commit comments

Comments
 (0)