@durable-streams/client-conformance-tests 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +451 -0
  2. package/dist/adapters/typescript-adapter.d.ts +1 -0
  3. package/dist/adapters/typescript-adapter.js +586 -0
  4. package/dist/benchmark-runner-C_Yghc8f.js +1333 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +265 -0
  7. package/dist/index.d.ts +508 -0
  8. package/dist/index.js +4 -0
  9. package/dist/protocol-DyEvTHPF.d.ts +472 -0
  10. package/dist/protocol-qb83AeUH.js +120 -0
  11. package/dist/protocol.d.ts +2 -0
  12. package/dist/protocol.js +3 -0
  13. package/package.json +53 -0
  14. package/src/adapters/typescript-adapter.ts +848 -0
  15. package/src/benchmark-runner.ts +860 -0
  16. package/src/benchmark-scenarios.ts +311 -0
  17. package/src/cli.ts +294 -0
  18. package/src/index.ts +50 -0
  19. package/src/protocol.ts +656 -0
  20. package/src/runner.ts +1191 -0
  21. package/src/test-cases.ts +475 -0
  22. package/test-cases/consumer/cache-headers.yaml +150 -0
  23. package/test-cases/consumer/error-handling.yaml +108 -0
  24. package/test-cases/consumer/message-ordering.yaml +209 -0
  25. package/test-cases/consumer/offset-handling.yaml +209 -0
  26. package/test-cases/consumer/offset-resumption.yaml +197 -0
  27. package/test-cases/consumer/read-catchup.yaml +173 -0
  28. package/test-cases/consumer/read-longpoll.yaml +132 -0
  29. package/test-cases/consumer/read-sse.yaml +145 -0
  30. package/test-cases/consumer/retry-resilience.yaml +160 -0
  31. package/test-cases/consumer/streaming-equivalence.yaml +226 -0
  32. package/test-cases/lifecycle/dynamic-headers.yaml +147 -0
  33. package/test-cases/lifecycle/headers-params.yaml +117 -0
  34. package/test-cases/lifecycle/stream-lifecycle.yaml +148 -0
  35. package/test-cases/producer/append-data.yaml +142 -0
  36. package/test-cases/producer/batching.yaml +112 -0
  37. package/test-cases/producer/create-stream.yaml +87 -0
  38. package/test-cases/producer/error-handling.yaml +90 -0
  39. package/test-cases/producer/sequence-ordering.yaml +148 -0
package/README.md ADDED
@@ -0,0 +1,451 @@
1
+ # @durable-streams/client-conformance-tests
2
+
3
+ Conformance test suite for Durable Streams client implementations (producer and consumer).
4
+
5
+ This package provides a comprehensive test suite to verify that a client correctly implements the [Durable Streams protocol](../../PROTOCOL.md) across any programming language.
6
+
7
+ ## How It Works
8
+
9
+ The conformance suite uses a **language-agnostic architecture** inspired by [ConnectRPC Conformance](https://github.com/connectrpc/conformance) and [AWS Smithy Protocol Tests](https://smithy.io/2.0/additional-specs/http-protocol-compliance-tests.html):
10
+
11
+ ```
12
+ ┌─────────────────────────────────────────────────────────────────┐
13
+ │ Test Runner (Node.js) │
14
+ │ - Reads test cases from YAML │
15
+ │ - Manages reference server lifecycle │
16
+ │ - Orchestrates client adapter process │
17
+ │ - Compares results against expectations │
18
+ └────────────────────────┬────────────────────────────────────────┘
19
+ │ stdin/stdout (JSON lines)
20
+
21
+ ┌─────────────────────────────────────────────────────────────────┐
22
+ │ Client Adapter (any language) │
23
+ │ - Reads test commands from stdin │
24
+ │ - Uses native SDK to execute operations │
25
+ │ - Reports results to stdout │
26
+ └─────────────────────────────────────────────────────────────────┘
27
+ │ HTTP
28
+
29
+ ┌─────────────────────────────────────────────────────────────────┐
30
+ │ Reference Server (TypeScript) │
31
+ │ - Full protocol compliance │
32
+ │ - Validates client behavior │
33
+ └─────────────────────────────────────────────────────────────────┘
34
+ ```
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ npm install @durable-streams/client-conformance-tests
40
+ # or
41
+ pnpm add @durable-streams/client-conformance-tests
42
+ ```
43
+
44
+ ## CLI Usage
45
+
46
+ ### Test the TypeScript Client
47
+
48
+ ```bash
49
+ npx @durable-streams/client-conformance-tests --run ts
50
+ ```
51
+
52
+ ### Test a Custom Client Adapter
53
+
54
+ ```bash
55
+ # Python client
56
+ npx @durable-streams/client-conformance-tests --run ./my-python-adapter.py
57
+
58
+ # Go client
59
+ npx @durable-streams/client-conformance-tests --run ./my-go-adapter
60
+
61
+ # Any executable
62
+ npx @durable-streams/client-conformance-tests --run /path/to/adapter
63
+ ```
64
+
65
+ ### CLI Options
66
+
67
+ ```
68
+ Usage:
69
+ npx @durable-streams/client-conformance-tests --run <adapter> [options]
70
+
71
+ Options:
72
+ --suite <name> Run only specific suite(s): producer, consumer, lifecycle
73
+ --tag <name> Run only tests with specific tag(s)
74
+ --verbose Show detailed output for each operation
75
+ --fail-fast Stop on first test failure
76
+ --timeout <ms> Timeout for each test in milliseconds (default: 30000)
77
+ --port <port> Port for reference server (default: random)
78
+ --help, -h Show help message
79
+ ```
80
+
81
+ ### Examples
82
+
83
+ ```bash
84
+ # Test only producer functionality
85
+ npx @durable-streams/client-conformance-tests --run ts --suite producer
86
+
87
+ # Test only consumer functionality
88
+ npx @durable-streams/client-conformance-tests --run ./python-client --suite consumer
89
+
90
+ # Test core functionality with verbose output
91
+ npx @durable-streams/client-conformance-tests --run ts --tag core --verbose
92
+
93
+ # Stop on first failure
94
+ npx @durable-streams/client-conformance-tests --run ts --fail-fast
95
+ ```
96
+
97
+ ## Programmatic Usage
98
+
99
+ ```typescript
100
+ import { runConformanceTests } from "@durable-streams/client-conformance-tests"
101
+
102
+ const summary = await runConformanceTests({
103
+ clientAdapter: "ts", // or path to your adapter
104
+ suites: ["producer", "consumer"],
105
+ verbose: true,
106
+ })
107
+
108
+ console.log(`Passed: ${summary.passed}/${summary.total}`)
109
+ ```
110
+
111
+ ## Implementing a Client Adapter
112
+
113
+ A client adapter is an executable that communicates with the test runner via stdin/stdout using a JSON-line protocol.
114
+
115
+ ### Protocol Overview
116
+
117
+ 1. Test runner starts your adapter as a subprocess
118
+ 2. Runner sends JSON commands to stdin (one per line)
119
+ 3. Adapter executes commands using your client SDK
120
+ 4. Adapter sends JSON results to stdout (one per line)
121
+
122
+ ### Commands and Results
123
+
124
+ #### Init Command (first command, always sent)
125
+
126
+ ```json
127
+ // Command (stdin)
128
+ {"type":"init","serverUrl":"http://localhost:3000"}
129
+
130
+ // Result (stdout)
131
+ {"type":"init","success":true,"clientName":"my-client","clientVersion":"1.0.0","features":{"batching":true,"sse":true,"longPoll":true}}
132
+ ```
133
+
134
+ #### Create Command
135
+
136
+ ```json
137
+ // Command
138
+ {"type":"create","path":"/my-stream","contentType":"text/plain"}
139
+
140
+ // Success Result
141
+ {"type":"create","success":true,"status":201,"offset":"0"}
142
+
143
+ // Error Result
144
+ {"type":"error","success":false,"commandType":"create","status":409,"errorCode":"CONFLICT","message":"Stream already exists"}
145
+ ```
146
+
147
+ #### Append Command
148
+
149
+ ```json
150
+ // Command
151
+ {"type":"append","path":"/my-stream","data":"Hello, World!","seq":1}
152
+
153
+ // Success Result
154
+ {"type":"append","success":true,"status":200,"offset":"13"}
155
+ ```
156
+
157
+ #### Read Command
158
+
159
+ ```json
160
+ // Command
161
+ {"type":"read","path":"/my-stream","offset":"0","live":"long-poll","timeoutMs":5000}
162
+
163
+ // Success Result
164
+ {"type":"read","success":true,"status":200,"chunks":[{"data":"Hello, World!","offset":"13"}],"offset":"13","upToDate":true}
165
+ ```
166
+
167
+ #### Head Command
168
+
169
+ ```json
170
+ // Command
171
+ {"type":"head","path":"/my-stream"}
172
+
173
+ // Success Result
174
+ {"type":"head","success":true,"status":200,"offset":"13","contentType":"text/plain"}
175
+ ```
176
+
177
+ #### Delete Command
178
+
179
+ ```json
180
+ // Command
181
+ {"type":"delete","path":"/my-stream"}
182
+
183
+ // Success Result
184
+ {"type":"delete","success":true,"status":200}
185
+ ```
186
+
187
+ #### Shutdown Command
188
+
189
+ ```json
190
+ // Command
191
+ {"type":"shutdown"}
192
+
193
+ // Result
194
+ {"type":"shutdown","success":true}
195
+ ```
196
+
197
+ ### Error Codes
198
+
199
+ Use these standard error codes in error results:
200
+
201
+ - `NETWORK_ERROR` - Network connection failed
202
+ - `TIMEOUT` - Operation timed out
203
+ - `CONFLICT` - Stream already exists (409)
204
+ - `NOT_FOUND` - Stream not found (404)
205
+ - `SEQUENCE_CONFLICT` - Sequence number conflict (409)
206
+ - `INVALID_OFFSET` - Invalid offset format
207
+ - `UNEXPECTED_STATUS` - Unexpected HTTP status
208
+ - `PARSE_ERROR` - Failed to parse response
209
+ - `INTERNAL_ERROR` - Client internal error
210
+ - `NOT_SUPPORTED` - Operation not supported
211
+
212
+ ### Example: Python Adapter
213
+
214
+ ```python
215
+ #!/usr/bin/env python3
216
+ import sys
217
+ import json
218
+ from durable_streams import DurableStream, DurableStreamError
219
+
220
+ def main():
221
+ server_url = ""
222
+
223
+ for line in sys.stdin:
224
+ if not line.strip():
225
+ continue
226
+
227
+ command = json.loads(line)
228
+ result = handle_command(command, server_url)
229
+
230
+ if command["type"] == "init":
231
+ server_url = command["serverUrl"]
232
+
233
+ print(json.dumps(result), flush=True)
234
+
235
+ if command["type"] == "shutdown":
236
+ break
237
+
238
+ def handle_command(cmd, server_url):
239
+ try:
240
+ if cmd["type"] == "init":
241
+ return {
242
+ "type": "init",
243
+ "success": True,
244
+ "clientName": "durable-streams-python",
245
+ "clientVersion": "0.1.0",
246
+ "features": {"batching": False, "sse": True, "longPoll": True}
247
+ }
248
+
249
+ elif cmd["type"] == "create":
250
+ url = f"{server_url}{cmd['path']}"
251
+ stream = DurableStream.create(url, content_type=cmd.get("contentType"))
252
+ return {"type": "create", "success": True, "status": 201}
253
+
254
+ elif cmd["type"] == "append":
255
+ url = f"{server_url}{cmd['path']}"
256
+ stream = DurableStream(url)
257
+ stream.append(cmd["data"], seq=cmd.get("seq"))
258
+ return {"type": "append", "success": True, "status": 200}
259
+
260
+ elif cmd["type"] == "read":
261
+ url = f"{server_url}{cmd['path']}"
262
+ # ... implement read logic
263
+ return {"type": "read", "success": True, "status": 200, "chunks": [], "upToDate": True}
264
+
265
+ elif cmd["type"] == "head":
266
+ url = f"{server_url}{cmd['path']}"
267
+ result = DurableStream.head(url)
268
+ return {"type": "head", "success": True, "status": 200, "offset": result.offset}
269
+
270
+ elif cmd["type"] == "delete":
271
+ url = f"{server_url}{cmd['path']}"
272
+ DurableStream.delete(url)
273
+ return {"type": "delete", "success": True, "status": 200}
274
+
275
+ elif cmd["type"] == "shutdown":
276
+ return {"type": "shutdown", "success": True}
277
+
278
+ except DurableStreamError as e:
279
+ return {
280
+ "type": "error",
281
+ "success": False,
282
+ "commandType": cmd["type"],
283
+ "errorCode": map_error_code(e),
284
+ "message": str(e)
285
+ }
286
+
287
+ def map_error_code(error):
288
+ # Map your client's error types to standard codes
289
+ if error.status == 404:
290
+ return "NOT_FOUND"
291
+ elif error.status == 409:
292
+ return "CONFLICT"
293
+ return "INTERNAL_ERROR"
294
+
295
+ if __name__ == "__main__":
296
+ main()
297
+ ```
298
+
299
+ ### Example: Go Adapter
300
+
301
+ ```go
302
+ package main
303
+
304
+ import (
305
+ "bufio"
306
+ "encoding/json"
307
+ "fmt"
308
+ "os"
309
+
310
+ durable "github.com/durable-streams/go-client"
311
+ )
312
+
313
+ type Command struct {
314
+ Type string `json:"type"`
315
+ ServerURL string `json:"serverUrl,omitempty"`
316
+ Path string `json:"path,omitempty"`
317
+ Data string `json:"data,omitempty"`
318
+ // ... other fields
319
+ }
320
+
321
+ type Result struct {
322
+ Type string `json:"type"`
323
+ Success bool `json:"success"`
324
+ Status int `json:"status,omitempty"`
325
+ // ... other fields
326
+ }
327
+
328
+ func main() {
329
+ scanner := bufio.NewScanner(os.Stdin)
330
+ var serverURL string
331
+
332
+ for scanner.Scan() {
333
+ line := scanner.Text()
334
+ if line == "" {
335
+ continue
336
+ }
337
+
338
+ var cmd Command
339
+ json.Unmarshal([]byte(line), &cmd)
340
+
341
+ result := handleCommand(cmd, serverURL)
342
+
343
+ if cmd.Type == "init" {
344
+ serverURL = cmd.ServerURL
345
+ }
346
+
347
+ output, _ := json.Marshal(result)
348
+ fmt.Println(string(output))
349
+
350
+ if cmd.Type == "shutdown" {
351
+ break
352
+ }
353
+ }
354
+ }
355
+
356
+ func handleCommand(cmd Command, serverURL string) Result {
357
+ switch cmd.Type {
358
+ case "init":
359
+ return Result{
360
+ Type: "init",
361
+ Success: true,
362
+ // ... client info
363
+ }
364
+ case "create":
365
+ // Use your Go client SDK
366
+ return Result{Type: "create", Success: true, Status: 201}
367
+ // ... handle other commands
368
+ }
369
+ return Result{Type: "error", Success: false}
370
+ }
371
+ ```
372
+
373
+ ## Test Coverage
374
+
375
+ The conformance test suite covers:
376
+
377
+ ### Producer Tests
378
+
379
+ - **Stream Creation** - Create, idempotency, content types, TTL
380
+ - **Append Operations** - String/binary data, unicode, large payloads
381
+ - **Sequence Ordering** - Monotonic sequences, conflict detection
382
+ - **Batching** - Concurrent appends, order preservation
383
+ - **Error Handling** - 404s, 409s, network errors
384
+
385
+ ### Consumer Tests
386
+
387
+ - **Catch-up Reads** - Empty/full streams, offset resumption
388
+ - **Long-Poll** - Waiting for data, timeouts
389
+ - **SSE Mode** - Event streaming, reconnection
390
+ - **Offset Handling** - Monotonicity, byte-exactness
391
+ - **Error Handling** - Invalid offsets, deleted streams
392
+
393
+ ### Lifecycle Tests
394
+
395
+ - **Full Lifecycle** - Create, append, read, delete
396
+ - **Headers/Params** - Custom headers, auth tokens
397
+ - **Metadata** - HEAD requests, content types
398
+
399
+ ## Adding New Test Cases
400
+
401
+ Test cases are defined in YAML files in the `test-cases/` directory:
402
+
403
+ ```yaml
404
+ id: my-new-tests
405
+ name: My New Tests
406
+ description: Tests for new functionality
407
+ category: producer # or consumer, lifecycle
408
+ tags:
409
+ - core
410
+ - custom
411
+
412
+ tests:
413
+ - id: my-test
414
+ name: My test case
415
+ description: What this test verifies
416
+ setup:
417
+ - action: create
418
+ as: streamPath
419
+ operations:
420
+ - action: append
421
+ path: ${streamPath}
422
+ data: "test data"
423
+ expect:
424
+ status: 200
425
+ - action: read
426
+ path: ${streamPath}
427
+ expect:
428
+ data: "test data"
429
+ upToDate: true
430
+ cleanup:
431
+ - action: delete
432
+ path: ${streamPath}
433
+ ```
434
+
435
+ ## Protocol Types
436
+
437
+ For TypeScript/JavaScript adapters, you can import the protocol types:
438
+
439
+ ```typescript
440
+ import {
441
+ type TestCommand,
442
+ type TestResult,
443
+ parseCommand,
444
+ serializeResult,
445
+ ErrorCodes,
446
+ } from "@durable-streams/client-conformance-tests/protocol"
447
+ ```
448
+
449
+ ## License
450
+
451
+ Apache 2.0
@@ -0,0 +1 @@
1
+ export { };