Skip to content

Commit cdf3a7c

Browse files
committed
uts: updated skill to reference spec guide for writing tests
1 parent c9d79f2 commit cdf3a7c

1 file changed

Lines changed: 61 additions & 28 deletions

File tree

.claude/skills/uts-to-kotlin/SKILL.md

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ description: "Translate a UTS pseudocode test spec into Kotlin tests in the uts
33
allowed-tools: Bash, Read, Edit, Write
44
---
55

6-
You are translating a UTS pseudocode test spec file into a runnable Kotlin test in the `uts` module. Follow these steps in order.
6+
Translate the UTS pseudocode test spec at `$ARGUMENTS` into a runnable Kotlin test in the `uts` module.
7+
8+
Reference: [Writing Derived Tests](https://github.com/ably/specification/blob/main/uts/docs/writing-derived-tests.md)
79

810
---
911

1012
## Step 1 — Read the spec
1113

1214
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+
- All test cases each has a structured ID like `realtime/unit/RSA4c2/callback-error-connecting-disconnected-0` and a description
16+
- The protocol used (WebSocket for Realtime, HTTP for REST)
1517
- Any timer usage (`enable_fake_timers`, `ADVANCE_TIME`)
1618

1719
---
@@ -110,7 +112,6 @@ val refuseJob = launch {
110112
repeat(10) {
111113
fakeClock.advance(2.seconds)
112114
mockWs.awaitConnectionAttempt().respondWithRefused()
113-
...
114115
}
115116
}
116117
```
@@ -121,7 +122,7 @@ val mockHttp = MockHttpClient { onRequest = { req -> req.respondWith(200, body)
121122
val client = TestRestClient { install(mockHttp) }
122123
```
123124

124-
### Inspecting outgoing frames (client → server)
125+
### Inspecting outgoing frames
125126

126127
`ClientOptionsBuilder` sets `useBinaryProtocol = false`, so the SDK sends JSON text frames. Every outgoing frame is captured as `MockEvent.MessageFromClient` and queued in `awaitNextMessageFromClient()`.
127128

@@ -147,8 +148,8 @@ assertEquals("expected-serial", sent!!.message.channelSerial)
147148

148149
| Pseudocode | Kotlin |
149150
|---|---|
150-
| `conn.respond_with_success()` | `conn.respondWithSuccess()` (opens socket only) |
151-
| `conn.respond_with_success(msg)` | `conn.respondWithSuccess(msg)` (opens socket + delivers message) |
151+
| `conn.respond_with_success()` | `conn.respondWithSuccess()` |
152+
| `conn.respond_with_success(msg)` | `conn.respondWithSuccess(msg)` |
152153
| `conn.respond_with_refused()` | `conn.respondWithRefused()` |
153154
| `conn.respond_with_timeout()` | `conn.respondWithTimeout()` |
154155
| `conn.respond_with_dns_error()` | `conn.respondWithDnsError()` |
@@ -164,16 +165,16 @@ assertEquals("expected-serial", sent!!.message.channelSerial)
164165
|---|---|
165166
| `ProtocolMessage(action: CONNECTED, ...)` | `ProtocolMessage().apply { action = ProtocolMessage.Action.connected; ... }` |
166167
| `CONNECTED` / `DISCONNECTED` / `ERROR` / `HEARTBEAT` / `ATTACH` / `DETACHED` | `.connected` / `.disconnected` / `.error` / `.heartbeat` / `.attach` / `.detached` |
167-
| `ErrorInfo(code: X, statusCode: Y, message: "...")` | `ErrorInfo("...", Y, X)`note arg order: message, statusCode, code |
168+
| `ErrorInfo(code: X, statusCode: Y, message: "...")` | `ErrorInfo("...", Y, X)` — arg order: message, statusCode, code |
168169
| `ConnectionDetails(connectionKey: ..., maxIdleInterval: ..., connectionStateTtl: ...)` | `ConnectionDetails { connectionKey = "..."; maxIdleInterval = ...; connectionStateTtl = ... }` |
169170
| `ConnectionState.connected` etc. | `ConnectionState.connected`, `.disconnected`, `.suspended`, `.failed`, `.connecting`, `.closing`, `.closed` |
170171

171172
### Awaiting state
172173

173-
`AWAIT_STATE client.connection.state == ConnectionState.X` → use the top-level `awaitState()` helper from `io.ably.lib`:
174+
`AWAIT_STATE client.connection.state == ConnectionState.X` → use the top-level `awaitState()` helper:
174175

175176
```kotlin
176-
awaitState(client, ConnectionState.x) // default 5s timeout
177+
awaitState(client, ConnectionState.x) // default 5s timeout
177178
awaitState(client, ConnectionState.x, 10.seconds)
178179
```
179180

@@ -195,7 +196,7 @@ fakeClock.advance(30_000)
195196
fakeClock.advance(30.seconds)
196197
```
197198

198-
After `fakeClock.advance()` inside a coroutine, yield to let any newly dispatched coroutines run:
199+
After `fakeClock.advance()` inside a coroutine, yield to let newly dispatched coroutines run:
199200

200201
```kotlin
201202
fakeClock.advance(30.seconds)
@@ -295,9 +296,9 @@ class <Name>Test {
295296
```
296297

297298
Fix any compilation errors and recompile until clean. Common issues:
298-
- Missing imports (add them)
299-
- Method names differ from what you read in the mock files (use the exact names you read)
300-
- `ErrorInfo` constructor arg order is `(message, statusCode, code)` — not `(code, statusCode, message)`
299+
- Missing imports
300+
- Method names differ from what you read in the mock files (use the exact names from Step 3)
301+
- `ErrorInfo` constructor arg order is `(message, statusCode, code)`
301302

302303
---
303304

@@ -307,17 +308,49 @@ Fix any compilation errors and recompile until clean. Common issues:
307308
./gradlew :uts:test --tests "<package>.<ClassName>"
308309
```
309310

310-
Handle test failures:
311-
312-
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.
313-
2. **Translation error** (you misread the pseudocode): fix silently.
314-
3. **SDK deviation** (confirmed against `uts/spec/features.md` — SDK does not comply):
315-
- Wrap the failing assertion in an env gate:
316-
```kotlin
317-
if (System.getenv("RUN_DEVIATIONS") != null) {
318-
assertEquals(specCorrectValue, actualValue)
319-
}
320-
```
321-
- Add a comment explaining the deviation.
322-
- Append an entry to `uts/src/test/kotlin/io/ably/lib/deviations.md`:
323-
- Spec point, what spec requires, what SDK does, which test is affected.
311+
Handle test failures using this decision tree (see [reference doc](https://github.com/ably/specification/blob/main/uts/docs/writing-derived-tests.md) for full detail):
312+
313+
```
314+
Test fails
315+
|
316+
+-- Does UTS spec match features spec?
317+
| NO → fix test, record UTS spec error in deviations file
318+
| YES
319+
| +-- Does test accurately translate the UTS spec?
320+
| NO → fix the test (no deviation entry needed)
321+
| YES → SDK deviation — adapt test, record in deviations file
322+
```
323+
324+
### Deviation patterns
325+
326+
**Env-gated skip (preferred)** — test contains spec-correct assertions but is skipped by default:
327+
328+
```kotlin
329+
/**
330+
* @UTS realtime/unit/RSA4c2/callback-error-connecting-disconnected-0
331+
*/
332+
@Test
333+
fun `RSA4c2 - callback error connecting disconnected`() = runTest {
334+
// DEVIATION: see deviations.md
335+
if (System.getenv("RUN_DEVIATIONS") != null) return@runTest
336+
337+
// ... spec-correct setup and assertions ...
338+
}
339+
```
340+
341+
**Adapted assertion** — when you still want to assert on the SDK's actual behaviour to prevent regressions:
342+
343+
```kotlin
344+
// DEVIATION: spec requires error code 40106, SDK returns 40160 — see deviations.md
345+
assertEquals(40160, error.errorInfo.code)
346+
```
347+
348+
**Never use the accommodate-both pattern** (accept either spec or SDK behaviour). Every test must assert either spec behaviour or the SDK's actual behaviour — never both at once.
349+
350+
### Deviations file
351+
352+
Append to `uts/src/test/kotlin/io/ably/lib/deviations.md`. Each entry needs:
353+
1. The spec point (e.g. `RSA4c2`)
354+
2. What the spec says
355+
3. What the SDK does
356+
4. Which test is affected and how it was adapted

0 commit comments

Comments
 (0)