Skip to content

Commit ec7e6c4

Browse files
committed
uts: add uts-to-kotlin skill for translating UTS pseudocode to Kotlin tests
- Introduced a new skill for converting UTS pseudocode specs into runnable Kotlin tests. - Included detailed translation rules for pseudocode to Kotlin, mock setup, and assertions. - Added file templates and steps for compilation, testing, and handling deviations. - Enhanced developer workflow for UTS test authoring.
1 parent c56d084 commit ec7e6c4

1 file changed

Lines changed: 250 additions & 0 deletions

File tree

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
---
2+
description: "Translate a UTS pseudocode test spec into Kotlin tests in the uts module. Usage: /uts-to-kotlin <path-to-spec-file>"
3+
allowed-tools: Bash, Read, Edit, Write
4+
---
5+
6+
You are translating a UTS pseudocode test spec file into a runnable Kotlin test in the `uts` module. Follow these steps in order.
7+
8+
---
9+
10+
## Step 1 — Read the spec
11+
12+
Read the file at `$ARGUMENTS`. Identify:
13+
- All test cases (each has an ID like `RTN4a`, `RSC1`, etc. and a description)
14+
- The protocol/transport used (WebSocket for Realtime, HTTP for REST)
15+
- Any timer usage (`enable_fake_timers`, `ADVANCE_TIME`)
16+
17+
---
18+
19+
## Step 2 — Determine output path and package
20+
21+
Map the spec path to a test path:
22+
23+
| Spec location | Test location |
24+
|---|---|
25+
| `.../uts/test/rest/unit/<name>.md` | `uts/src/test/kotlin/io/ably/lib/rest/unit/<Name>Test.kt` |
26+
| `.../uts/test/realtime/unit/<sub>/<name>.md` | `uts/src/test/kotlin/io/ably/lib/realtime/unit/<sub>/<Name>Test.kt` |
27+
28+
Class name: take the file name, strip `_test` suffix, convert `snake_case``PascalCase`, append `Test`.
29+
30+
Example: `connection_state_machine_test.md``ConnectionStateMachineTest`
31+
32+
Package: derived from the output path under `kotlin/`.
33+
34+
---
35+
36+
## Step 3 — Read mock infrastructure files
37+
38+
Read ALL of these before generating any code (you need exact method signatures):
39+
40+
```
41+
uts/src/test/kotlin/io/ably/lib/test/mock/MockWebSocket.kt
42+
uts/src/test/kotlin/io/ably/lib/test/mock/MockHttpClient.kt
43+
uts/src/test/kotlin/io/ably/lib/test/mock/PendingRequest.kt
44+
uts/src/test/kotlin/io/ably/lib/test/mock/PendingConnection.kt
45+
uts/src/test/kotlin/io/ably/lib/test/mock/FakeClock.kt
46+
uts/src/test/kotlin/io/ably/lib/test/mock/MockEvent.kt
47+
```
48+
49+
---
50+
51+
## Step 4 — Generate the Kotlin test file
52+
53+
Apply the translation rules below, then write the file.
54+
55+
### Client construction
56+
57+
| Pseudocode | Kotlin |
58+
|---|---|
59+
| `Rest(options: ClientOptions(key: "..."))` | `AblyRest(DebugOptions("..."))` |
60+
| `Realtime(options: ClientOptions(key: "...", autoConnect: false))` | `DebugOptions("...").apply { autoConnect = false }.let { AblyRealtime(it) }` |
61+
| `ClientOptions(token: "...", autoConnect: false)` | `DebugOptions().apply { token = "..."; autoConnect = false }` |
62+
63+
### Mock setup — CRITICAL
64+
65+
The pseudocode uses callback-style (`onConnectionAttempt: (conn) => {...}`) but Kotlin mocks use **coroutine await-style**. Each callback body becomes a `launch { ... }` block started **before** the SDK client is created or connected.
66+
67+
| Pseudocode | Kotlin |
68+
|---|---|
69+
| `mock_http = MockHttpClient(...)` + `install_mock(mock_http)` | `val mock = MockHttpClient(); mock.installOn(options)` |
70+
| `mock_ws = MockWebSocket(...)` + `install_mock(mock_ws)` | `val mock = MockWebSocket(); mock.installOn(options)` |
71+
| `onConnectionAttempt: (conn) => { conn.respond_with_success() }` | `launch { val conn = mock.awaitConnectionAttempt(); conn.respondWithSuccess() }` |
72+
| `onRequest: (req) => { req.respond_with(200, body) }` | `launch { val req = mock.awaitRequest(); req.respondWith(200, body) }` |
73+
| Repeated connection attempts | `launch { repeat(N) { val conn = mock.awaitConnectionAttempt(); conn.respondWithRefused() } }` |
74+
| `enable_fake_timers()` | `val clock = FakeClock(); options.clock = clock` (before client construction) |
75+
76+
### Connection/request actions
77+
78+
| Pseudocode | Kotlin |
79+
|---|---|
80+
| `conn.respond_with_success()` | `conn.respondWithSuccess()` |
81+
| `conn.respond_with_refused()` | `conn.respondWithRefused()` |
82+
| `conn.respond_with_timeout()` | `conn.respondWithTimeout()` |
83+
| `conn.respond_with_dns_error()` | `conn.respondWithDnsError()` |
84+
| `conn.send_to_client(msg)` | `mock.sendToClient(msg)` (after `respondWithSuccess()`) |
85+
| `conn.send_to_client_and_close(msg)` | `mock.sendToClientAndClose(msg)` |
86+
| `mock_ws.simulate_disconnect()` | `mock.simulateDisconnect()` |
87+
| `req.respond_with(200, {...})` | `req.respondWith(200, mapOf(...))` |
88+
| `req.respond_with_timeout()` | `req.respondWithTimeout()` |
89+
90+
### Protocol messages and types
91+
92+
| Pseudocode | Kotlin |
93+
|---|---|
94+
| `ProtocolMessage(action: CONNECTED, ...)` | `ProtocolMessage().apply { action = ProtocolMessage.Action.connected; ... }` |
95+
| `CONNECTED` / `DISCONNECTED` / `ERROR` / `HEARTBEAT` / `ATTACH` / `DETACHED` | `.connected` / `.disconnected` / `.error` / `.heartbeat` / `.attach` / `.detached` |
96+
| `ErrorInfo(code: X, statusCode: Y, message: "...")` | `ErrorInfo("...", X, Y)` |
97+
| `ConnectionDetails(connectionKey: ..., maxIdleInterval: ..., connectionStateTtl: ...)` | `ConnectionDetails().apply { connectionKey = "..."; maxIdleInterval = ...; connectionStateTtl = ... }` |
98+
| `ConnectionState.connected` etc. | `ConnectionState.connected`, `.disconnected`, `.suspended`, `.failed`, `.connecting`, `.closing`, `.closed` |
99+
100+
### Awaiting state
101+
102+
`AWAIT_STATE client.connection.state == ConnectionState.X WITH timeout: N seconds` → call the `awaitState()` helper (included in the file template below):
103+
104+
```kotlin
105+
awaitState(client, ConnectionState.x, timeoutMs = N * 1000L)
106+
```
107+
108+
### Timer control
109+
110+
| Pseudocode | Kotlin |
111+
|---|---|
112+
| `enable_fake_timers()` | `val clock = FakeClock()` then `options.clock = clock` |
113+
| `ADVANCE_TIME(ms)` | `clock.advance(ms)` |
114+
115+
After `clock.advance()`, always yield to let the SDK's timer callbacks dispatch:
116+
117+
```kotlin
118+
clock.advance(30_000)
119+
yield()
120+
```
121+
122+
### Assertions
123+
124+
| Pseudocode | Kotlin |
125+
|---|---|
126+
| `ASSERT x == y` | `assertEquals(y, x)` |
127+
| `ASSERT x IS NOT null` | `assertNotNull(x)` |
128+
| `ASSERT x IS null` | `assertNull(x)` |
129+
| `ASSERT x IS Auth` | `assertIs<Auth>(x)` |
130+
| `ASSERT "key" IN map` | `assertContains(map, "key")` |
131+
| `ASSERT x matches pattern "..."` | `assertTrue(x.matches(Regex("...")))` |
132+
| `ASSERT list CONTAINS_IN_ORDER [a, b, c]` | `val it = list.iterator(); assertEquals(a, it.next()); assertEquals(b, it.next()); ...` |
133+
| `AWAIT expr FAILS WITH error` | `val error = assertFailsWith<AblyException> { expr }; assertEquals(..., error.errorInfo.code)` |
134+
| `ASSERT list.length == N` | `assertEquals(N, list.size)` |
135+
136+
### Test naming
137+
138+
- Method name: backtick string `` `<spec-id> - <description>` ``
139+
- Add `// UTS: <test-id>` comment on the line immediately above `@Test`
140+
- Use `runTest { }` from `kotlinx.coroutines.test` for all async tests
141+
142+
### File template
143+
144+
```kotlin
145+
package io.ably.lib.<category>.unit[.<subcategory>]
146+
147+
import io.ably.lib.debug.DebugOptions
148+
import io.ably.lib.realtime.AblyRealtime // or AblyRest for REST tests
149+
import io.ably.lib.realtime.ConnectionState
150+
import io.ably.lib.realtime.ConnectionStateListener
151+
import io.ably.lib.test.mock.FakeClock
152+
import io.ably.lib.test.mock.MockWebSocket // or MockHttpClient
153+
import io.ably.lib.types.ProtocolMessage
154+
import io.ably.lib.types.ErrorInfo
155+
import kotlinx.coroutines.launch
156+
import kotlinx.coroutines.suspendCancellableCoroutine
157+
import kotlinx.coroutines.test.runTest
158+
import kotlinx.coroutines.withTimeout
159+
import kotlinx.coroutines.yield
160+
import kotlin.coroutines.resume
161+
import kotlin.test.*
162+
163+
class <Name>Test {
164+
165+
@AfterTest
166+
fun tearDown() {
167+
// close any clients opened in each test (declare them at test scope, not class scope)
168+
}
169+
170+
// UTS: <test-id>
171+
@Test
172+
fun `<spec-id> - <description>`() = runTest {
173+
val mock = MockWebSocket()
174+
val options = DebugOptions("appId.keyId:keySecret").apply {
175+
autoConnect = false
176+
mock.installOn(this)
177+
}
178+
179+
launch {
180+
val conn = mock.awaitConnectionAttempt()
181+
conn.respondWithSuccess()
182+
mock.sendToClient(ProtocolMessage().apply {
183+
action = ProtocolMessage.Action.connected
184+
connectionId = "test-connection-id"
185+
connectionKey = "test-key"
186+
})
187+
}
188+
189+
val client = AblyRealtime(options)
190+
client.connect()
191+
awaitState(client, ConnectionState.connected)
192+
193+
assertEquals(ConnectionState.connected, client.connection.state)
194+
client.close()
195+
}
196+
197+
private suspend fun awaitState(
198+
client: AblyRealtime,
199+
target: ConnectionState,
200+
timeoutMs: Long = 5000
201+
) {
202+
if (client.connection.state == target) return
203+
withTimeout(timeoutMs) {
204+
suspendCancellableCoroutine { cont ->
205+
val listener = ConnectionStateListener { change ->
206+
if (change.current == target && cont.isActive) cont.resume(Unit)
207+
}
208+
client.connection.on(listener)
209+
cont.invokeOnCancellation { client.connection.off(listener) }
210+
}
211+
}
212+
}
213+
}
214+
```
215+
216+
---
217+
218+
## Step 5 — Compile
219+
220+
```bash
221+
./gradlew :uts:compileTestKotlin
222+
```
223+
224+
Fix any compilation errors and recompile until clean. Common issues:
225+
- Missing imports (add them)
226+
- Method names differ from what you read in the mock files (use the exact names you read)
227+
- `Scheduled` is a top-level class in `FakeClock`, not nested inside `FakeNamedTimer`
228+
229+
---
230+
231+
## Step 6 — Run tests
232+
233+
```bash
234+
./gradlew :uts:test --tests "<package>.<ClassName>Test"
235+
```
236+
237+
Handle test failures:
238+
239+
1. **UTS spec error** (pseudocode itself is wrong): fix the test to match what the spec intends, add a `// NOTE: spec pseudocode had X, corrected to Y` comment.
240+
2. **Translation error** (you misread the pseudocode): fix silently.
241+
3. **SDK deviation** (confirmed against `uts/spec/features.md` — SDK does not comply):
242+
- Wrap the failing assertion in an env gate:
243+
```kotlin
244+
if (System.getenv("RUN_DEVIATIONS") != null) {
245+
assertEquals(specCorrectValue, actualValue)
246+
}
247+
```
248+
- Add a comment explaining the deviation.
249+
- Append an entry to `uts/src/test/kotlin/io/ably/lib/deviations.md`:
250+
- Spec point, what spec requires, what SDK does, which test is affected.

0 commit comments

Comments
 (0)