Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
ad9a601
Add ably-common submodule
paddybyers Mar 30, 2026
a7c3b21
Add initial UTS (Universal Test Spec) suite for REST API
paddybyers Mar 30, 2026
cc73996
Add UTS specs for Realtime connection lifecycle
paddybyers Mar 30, 2026
20ce164
Fix connection test specs to close WebSocket transport when necessary
paddybyers Mar 30, 2026
5dbbaaa
Refactor realtime test specs: extract mock WebSocket helper and add s…
paddybyers Mar 30, 2026
b023f40
Refactor test specs to use EXPECT_THROW instead of TRY/CATCH
paddybyers Mar 30, 2026
90a5c6a
Use unique channel names in test specs to prevent cross-test interfer…
paddybyers Mar 30, 2026
b898077
Rewrite heartbeat test specs and extend mock WebSocket helper
paddybyers Mar 30, 2026
405e9b7
Fix RTN15a (immediate reconnection) test approach and update skill
paddybyers Mar 30, 2026
affc02c
Fix minor issues in channel attach and state events test specs
paddybyers Mar 30, 2026
978c1d2
Add test specs for connection state transitions and channel properties
paddybyers Mar 30, 2026
845893b
Add test specs for realtime channel subscribe (RTL7/RTL8)
paddybyers Mar 30, 2026
e8861ee
Add test specs for realtime channel publish (RTL6)
paddybyers Mar 30, 2026
c4f4f1e
Add realtime entries for stats() and time() referencing existing REST…
paddybyers Mar 30, 2026
4aaa39a
Add test specs for Realtime.request() (RTN18)
paddybyers Mar 30, 2026
a5ef609
Extend realtime publish tests: queued messages and state transitions
paddybyers Mar 30, 2026
1b872a6
Add test specs for RSN1-4 (connection recovery) and RTL10 (realtime h…
paddybyers Mar 30, 2026
00669c2
Add test specs for client logging (LOG1-LOG3)
paddybyers Mar 30, 2026
eca184b
Add remaining auth test specs
paddybyers Mar 30, 2026
df30a3f
Add test specs for realtime presence (RTP)
paddybyers Mar 30, 2026
b4827d4
Fix path component encoding in test specs to use encode_uri_component()
paddybyers Mar 30, 2026
bac623c
Update presence test specs and add integration tests
paddybyers Mar 30, 2026
83b5b7c
Update write-test-spec skill to emphasise keeping specs in sync
paddybyers Mar 30, 2026
3993918
Add test specs for batch presence (RSP4)
paddybyers Mar 30, 2026
833c06a
Add test specs for token revocation (RSA10)
paddybyers Mar 30, 2026
467e9e2
Add test specs for RTL12 (channel UPDATE event handling)
paddybyers Mar 30, 2026
f5ace90
Add test specs for VCDIFF delta message encoding (RTL18/RTL19)
paddybyers Mar 30, 2026
d652549
Add test specs for channel attributes, whenState, timeouts, and auto-…
paddybyers Mar 30, 2026
d2e482d
Add test specs for mutable messages (RTL22/RTL23)
paddybyers Mar 30, 2026
cd98cd6
Add test specs for push admin (RSH1/RSH7)
paddybyers Mar 30, 2026
ac6899d
RSC8a/b: Add note clarifying relationship with RSC8c
paddybyers Mar 30, 2026
56ba0b1
RSC13: Improve timeout test pattern guidance
paddybyers Mar 30, 2026
1eaece2
RSC8e: Remove ambiguous error message assertion for Case 1
paddybyers Mar 30, 2026
3a9d4c2
RSA4b: Add note clarifying clientId detection timing
paddybyers Mar 30, 2026
cf427cc
RSA4b4: Clarify renewal limit — at most one retry per request
paddybyers Mar 30, 2026
b64ea85
RSA5/RSA6: Strengthen null default requirement to MUST
paddybyers Mar 30, 2026
976868b
RSC10b: Clarify that non-token 401 errors MUST NOT trigger renewal
paddybyers Mar 30, 2026
dea15d7
RSL1b: Clarify single message MAY be object or array
paddybyers Mar 30, 2026
ded265e
RSL6a: Document binary intermediate state in chained decoding
paddybyers Mar 30, 2026
43101be
TM3: Specify camelCase field names in JSON wire format
paddybyers Mar 30, 2026
163a566
RSP4a: Fix PresenceAction value mapping in history assertions
paddybyers Mar 30, 2026
a45c06f
REC1a, REC2c1: Document legacy vs new host patterns
paddybyers Mar 30, 2026
7747efc
Add msgpack-specific unit tests for data deserialization and error pa…
paddybyers Mar 30, 2026
c8cd7ac
Fix presence test specs: server echoes, wildcard clientId, RTL13b, RT…
paddybyers Mar 30, 2026
4606b5c
Fix integration test specs based on sandbox behavior
paddybyers Mar 30, 2026
955fac7
RSC7c: Add explicit fallbackHosts to fallback retry test setup
paddybyers Mar 30, 2026
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
Prev Previous commit
Next Next commit
Fix presence test specs: server echoes, wildcard clientId, RTL13b, RT…
…P2h2b

Fix several presence test spec issues: correct server echo expectations,
handle wildcard clientId constraints, and fix RTL13b (presence SYNC)
and RTP2h2b (presence re-entry) test logic.
  • Loading branch information
paddybyers committed Mar 30, 2026
commit c8cd7ac92c13195e4218c8341cae5270ddafcc6c
12 changes: 7 additions & 5 deletions uts/realtime/unit/presence/presence_sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,11 +392,13 @@ leave_events = map.endSync()

### Assertions
```pseudo
# Bob's ABSENT entry is cleaned up — no additional LEAVE emitted since
# bob was explicitly marked ABSENT (not stale-by-absence-from-sync)
# Implementation note: ABSENT members are simply deleted on endSync.
# The stale-member LEAVE events are only for members that were PRESENT
# but not updated during sync.
# Bob's ABSENT entry is cleaned up on endSync (RTP2h2b) — no synthesized
# LEAVE event is emitted for bob because he was explicitly marked ABSENT
# via a LEAVE message (not stale-by-absence-from-sync). ABSENT members
# are simply deleted on endSync without generating LEAVE events.
# Synthesized LEAVE events (RTP19) are only for PRESENT members that
# were not updated during sync (residuals).
ASSERT leave_events.length == 0
ASSERT map.get("c2:bob") IS null

# Alice survives
Expand Down
39 changes: 18 additions & 21 deletions uts/realtime/unit/presence/realtime_presence_channel_state.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,13 @@ state, then all presence actions that are still queued for send on that channel
RTP16b should be deleted from the queue, and any callback passed to the corresponding
presence method invocation should be called with an ErrorInfo indicating the failure.

**Note on ATTACHING → DETACHED transition:** Per RTL13b, when a channel is in the
ATTACHING state and receives a DETACHED ProtocolMessage from the server, the SDK
should retry the attach. If the retry fails or the connection is not in a state
that permits re-attach, the channel may transition to SUSPENDED rather than DETACHED.
The test below puts the channel into DETACHED state via an explicit `detach()` call
after a successful attach, which avoids the RTL13b retry path.

### Setup
```pseudo
channel_name = "test-RTL11-detached-${random_id()}"
Expand All @@ -576,7 +583,9 @@ mock_ws = MockWebSocket(
onConnectionAttempt: (conn) => conn.respond_with_success(CONNECTED_MESSAGE),
onMessageFromClient: (msg) => {
IF msg.action == ATTACH:
# Do NOT respond — leave channel in ATTACHING so presence queues
mock_ws.send_to_client(ProtocolMessage(action: ATTACHED, channel: channel_name))
ELSE IF msg.action == DETACH:
mock_ws.send_to_client(ProtocolMessage(action: DETACHED, channel: channel_name))
ELSE IF msg.action == PRESENCE:
captured_presence.append(msg)
}
Expand All @@ -592,33 +601,21 @@ channel = client.channels.get(channel_name)
client.connect()
AWAIT_STATE client.connection.state == ConnectionState.connected

# Start attach — channel goes to ATTACHING
channel.attach()
AWAIT_STATE channel.state == ChannelState.attaching

# Queue presence while channel is ATTACHING (per RTP16b)
enter_future = channel.presence.enter(data: "queued-enter")

# Verify nothing sent yet
ASSERT captured_presence.length == 0

# Server sends DETACHED — channel transitions to DETACHED
mock_ws.send_to_client(ProtocolMessage(
action: DETACHED,
channel: channel_name,
error: ErrorInfo(code: 90001, message: "Channel detached")
))
# Attach then detach to put channel in DETACHED state
AWAIT channel.attach()
AWAIT channel.detach()
ASSERT channel.state == ChannelState.detached

AWAIT_STATE channel.state == ChannelState.detached
# Attempting presence on a DETACHED channel should error immediately
AWAIT channel.presence.enter(data: "queued-enter") FAILS WITH error
```

### Assertions
```pseudo
# Queued presence was NOT sent
# No presence messages were sent
ASSERT captured_presence.length == 0

# The enter future completed with an error
AWAIT enter_future FAILS WITH error
# The enter completed with an error
ASSERT error IS ErrorInfo
ASSERT error.code IS NOT null
```
Expand Down
14 changes: 13 additions & 1 deletion uts/realtime/unit/presence/realtime_presence_enter.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ and `leaveClient` functions. These methods send PRESENCE ProtocolMessages to the
and handle ACK/NACK responses. Tests cover protocol message format, implicit channel
attach, connection state conditions, and error cases.

**Note on wildcard clientId:** Several tests use `clientId: "*"` (wildcard) which is
the Ably convention for clients permitted to act on behalf of any clientId via
`enterClient`/`updateClient`/`leaveClient`. Some SDKs may reject `"*"` at the
`ClientOptions` construction level. In such cases, adapt these tests to use a
concrete clientId (e.g., `"admin"`) and skip the client-side `enterClient` clientId
mismatch check (RTP15f), or configure the mock to accept any clientId.

---

## RTP8a, RTP8c - enter sends PRESENCE with ENTER action
Expand Down Expand Up @@ -250,6 +257,10 @@ ASSERT error IS NOT null
## RTP8j - enter with wildcard clientId errors

### Setup

Note: Some SDKs may reject wildcard clientId `"*"` at the `ClientOptions`
construction level rather than at `enter()` time. In that case, this test
validates that the error occurs at `ClientOptions` creation instead.
```pseudo
channel_name = "test-RTP8j-wild-${random_id()}"

Expand Down Expand Up @@ -826,7 +837,8 @@ mock_ws = MockWebSocket(
)
install_mock(mock_ws)

# Wildcard client to allow both enter() and enterClient()
# Wildcard clientId to allow both enter() and enterClient() on the same connection.
# See note in Purpose section about SDK-level wildcard validation.
client = Realtime(options: ClientOptions(key: "fake.key:secret", clientId: "*", autoConnect: false))
channel = client.channels.get(channel_name)
```
Expand Down
110 changes: 104 additions & 6 deletions uts/realtime/unit/presence/realtime_presence_reentry.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ RealtimePresence object maintains an internal PresenceMap (RTP17) of locally-ent
members. When the channel receives an ATTACHED ProtocolMessage (except when already
attached with RESUMED flag), it re-publishes an ENTER for each member in the internal map.

**Important:** The internal PresenceMap (LocalPresenceMap) is populated from server
PRESENCE echoes — messages with the current connection's connectionId — NOT directly
from the client's `enter()` or `enterClient()` calls. The server always echoes presence
events back to the originating client. Mock WebSocket setups must simulate this echo
for the LocalPresenceMap to contain any members for re-entry.

---

## RTP17i - Automatic re-entry on ATTACHED (non-RESUMED)
Expand Down Expand Up @@ -40,6 +46,25 @@ mock_ws = MockWebSocket(
ELSE IF msg.action == PRESENCE:
captured_presence.append(msg)
mock_ws.send_to_client(ProtocolMessage(action: ACK, msgSerial: msg.msgSerial, count: 1))
# Server echoes the presence event back to the client.
# This populates the LocalPresenceMap (RTP17) which is keyed by
# server echoes, not by the client's own enter() calls.
FOR idx, p IN enumerate(msg.presence):
mock_ws.send_to_client(ProtocolMessage(
action: PRESENCE,
channel: channel_name,
connectionId: "conn-${connection_count}",
presence: [
PresenceMessage(
action: p.action,
clientId: p.clientId OR "my-client",
connectionId: "conn-${connection_count}",
id: "conn-${connection_count}:${msg.msgSerial}:${idx}",
timestamp: NOW(),
data: p.data
)
]
))
}
)
install_mock(mock_ws)
Expand Down Expand Up @@ -108,12 +133,31 @@ mock_ws = MockWebSocket(
ELSE IF msg.action == PRESENCE:
captured_presence.append(msg)
mock_ws.send_to_client(ProtocolMessage(action: ACK, msgSerial: msg.msgSerial, count: 1))
# Server echoes the presence event back to populate LocalPresenceMap
FOR idx, p IN enumerate(msg.presence):
mock_ws.send_to_client(ProtocolMessage(
action: PRESENCE,
channel: channel_name,
connectionId: "conn-${connection_count}",
presence: [
PresenceMessage(
action: p.action,
clientId: p.clientId,
connectionId: "conn-${connection_count}",
id: "conn-${connection_count}:${msg.msgSerial}:${idx}",
timestamp: NOW(),
data: p.data
)
]
))
}
)
install_mock(mock_ws)

# Wildcard client to test enterClient with multiple members
client = Realtime(options: ClientOptions(key: "fake.key:secret", clientId: "*", autoConnect: false))
# Use a non-wildcard clientId that has enterClient permission.
# Note: Some SDKs reject wildcard clientId "*" at the ClientOptions level.
# Use a concrete clientId and rely on server-side permission for enterClient.
client = Realtime(options: ClientOptions(key: "fake.key:secret", clientId: "admin", autoConnect: false))
channel = client.channels.get(channel_name)
```

Expand All @@ -123,7 +167,7 @@ client.connect()
AWAIT_STATE client.connection.state == ConnectionState.connected
AWAIT channel.attach()

# Enter multiple members
# Enter multiple members via enterClient
AWAIT channel.presence.enterClient("alice", data: "alice-data")
AWAIT channel.presence.enterClient("bob", data: "bob-data")

Expand Down Expand Up @@ -188,6 +232,23 @@ mock_ws = MockWebSocket(
ELSE IF msg.action == PRESENCE:
captured_presence.append(msg)
mock_ws.send_to_client(ProtocolMessage(action: ACK, msgSerial: msg.msgSerial, count: 1))
# Server echoes the presence event back to populate LocalPresenceMap
FOR idx, p IN enumerate(msg.presence):
mock_ws.send_to_client(ProtocolMessage(
action: PRESENCE,
channel: channel_name,
connectionId: "conn-${connection_count}",
presence: [
PresenceMessage(
action: p.action,
clientId: p.clientId OR "my-client",
connectionId: "conn-${connection_count}",
id: "conn-${connection_count}:${msg.msgSerial}:${idx}",
timestamp: NOW(),
data: p.data
)
]
))
}
)
install_mock(mock_ws)
Expand Down Expand Up @@ -252,6 +313,23 @@ mock_ws = MockWebSocket(
ELSE IF msg.action == PRESENCE:
captured_presence.append(msg)
mock_ws.send_to_client(ProtocolMessage(action: ACK, msgSerial: msg.msgSerial, count: 1))
# Server echoes the presence event back to populate LocalPresenceMap
FOR idx, p IN enumerate(msg.presence):
mock_ws.send_to_client(ProtocolMessage(
action: PRESENCE,
channel: channel_name,
connectionId: "conn-1",
presence: [
PresenceMessage(
action: p.action,
clientId: p.clientId OR "my-client",
connectionId: "conn-1",
id: "conn-1:${msg.msgSerial}:${idx}",
timestamp: NOW(),
data: p.data
)
]
))
}
)
install_mock(mock_ws)
Expand Down Expand Up @@ -312,8 +390,24 @@ mock_ws = MockWebSocket(
mock_ws.send_to_client(ProtocolMessage(action: ATTACHED, channel: channel_name))
ELSE IF msg.action == PRESENCE:
IF connection_count == 1:
# First connection: ACK the enter
# First connection: ACK the enter and echo back the presence event
mock_ws.send_to_client(ProtocolMessage(action: ACK, msgSerial: msg.msgSerial, count: 1))
FOR idx, p IN enumerate(msg.presence):
mock_ws.send_to_client(ProtocolMessage(
action: PRESENCE,
channel: channel_name,
connectionId: "conn-1",
presence: [
PresenceMessage(
action: p.action,
clientId: p.clientId OR "my-client",
connectionId: "conn-1",
id: "conn-1:${msg.msgSerial}:${idx}",
timestamp: NOW(),
data: p.data
)
]
))
ELSE:
# Second connection: NACK the re-entry
mock_ws.send_to_client(ProtocolMessage(
Expand All @@ -338,10 +432,14 @@ AWAIT channel.attach()

AWAIT channel.presence.enter(data: "hello")

# Listen for channel UPDATE events
# Listen for channel UPDATE events with the re-entry failure error code.
# Note: The ATTACHED state change itself may also emit an UPDATE event
# (e.g., when transitioning from ATTACHED to ATTACHED with resumed=false).
# Filter for the specific 91004 error code to distinguish re-entry failure.
channel_events = []
channel.on(ChannelEvent.update, (change) => {
channel_events.append(change)
IF change.reason IS NOT null AND change.reason.code == 91004:
channel_events.append(change)
})

# Disconnect and reconnect — re-entry will be NACKed
Expand Down