@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
@@ -0,0 +1,656 @@
1
+ /**
2
+ * Protocol types for client conformance testing.
3
+ *
4
+ * This module defines the stdin/stdout protocol used for communication
5
+ * between the test runner and client adapters in any language.
6
+ *
7
+ * Communication is line-based JSON over stdin/stdout:
8
+ * - Test runner writes TestCommand as JSON line to client's stdin
9
+ * - Client writes TestResult as JSON line to stdout
10
+ * - Each command expects exactly one result
11
+ */
12
+
13
+ // =============================================================================
14
+ // Commands (sent from test runner to client adapter via stdin)
15
+ // =============================================================================
16
+
17
+ /**
18
+ * Initialize the client adapter with configuration.
19
+ * Must be the first command sent.
20
+ */
21
+ export interface InitCommand {
22
+ type: `init`
23
+ /** Base URL of the reference server */
24
+ serverUrl: string
25
+ /** Optional timeout in milliseconds for operations */
26
+ timeoutMs?: number
27
+ }
28
+
29
+ /**
30
+ * Create a new stream (PUT request).
31
+ */
32
+ export interface CreateCommand {
33
+ type: `create`
34
+ /** Full URL path for the stream (relative to serverUrl) */
35
+ path: string
36
+ /** Content type for the stream */
37
+ contentType?: string
38
+ /** Optional TTL in seconds */
39
+ ttlSeconds?: number
40
+ /** Optional absolute expiry timestamp (ISO 8601) */
41
+ expiresAt?: string
42
+ /** Custom headers to include */
43
+ headers?: Record<string, string>
44
+ }
45
+
46
+ /**
47
+ * Connect to an existing stream without creating it.
48
+ */
49
+ export interface ConnectCommand {
50
+ type: `connect`
51
+ path: string
52
+ headers?: Record<string, string>
53
+ }
54
+
55
+ /**
56
+ * Append data to a stream (POST request).
57
+ */
58
+ export interface AppendCommand {
59
+ type: `append`
60
+ path: string
61
+ /** Data to append - string for text, base64 for binary */
62
+ data: string
63
+ /** Whether data is base64 encoded binary */
64
+ binary?: boolean
65
+ /** Optional sequence number for ordering */
66
+ seq?: number
67
+ /** Custom headers to include */
68
+ headers?: Record<string, string>
69
+ }
70
+
71
+ /**
72
+ * Read from a stream (GET request).
73
+ */
74
+ export interface ReadCommand {
75
+ type: `read`
76
+ path: string
77
+ /** Starting offset (opaque string from previous reads) */
78
+ offset?: string
79
+ /** Live mode: false for catch-up only, "long-poll" or "sse" for live */
80
+ live?: false | `long-poll` | `sse`
81
+ /** Timeout for long-poll in milliseconds */
82
+ timeoutMs?: number
83
+ /** Maximum number of chunks to read (for testing) */
84
+ maxChunks?: number
85
+ /** Whether to wait until up-to-date before returning */
86
+ waitForUpToDate?: boolean
87
+ /** Custom headers to include */
88
+ headers?: Record<string, string>
89
+ }
90
+
91
+ /**
92
+ * Get stream metadata (HEAD request).
93
+ */
94
+ export interface HeadCommand {
95
+ type: `head`
96
+ path: string
97
+ headers?: Record<string, string>
98
+ }
99
+
100
+ /**
101
+ * Delete a stream (DELETE request).
102
+ */
103
+ export interface DeleteCommand {
104
+ type: `delete`
105
+ path: string
106
+ headers?: Record<string, string>
107
+ }
108
+
109
+ /**
110
+ * Shutdown the client adapter gracefully.
111
+ */
112
+ export interface ShutdownCommand {
113
+ type: `shutdown`
114
+ }
115
+
116
+ // =============================================================================
117
+ // Dynamic Headers/Params Commands
118
+ // =============================================================================
119
+
120
+ /**
121
+ * Configure a dynamic header that is evaluated per-request.
122
+ * The adapter should store this and apply it to subsequent operations.
123
+ *
124
+ * This tests the client's ability to support header functions for scenarios
125
+ * like OAuth token refresh, request correlation IDs, etc.
126
+ */
127
+ export interface SetDynamicHeaderCommand {
128
+ type: `set-dynamic-header`
129
+ /** Header name to set */
130
+ name: string
131
+ /** Type of dynamic value */
132
+ valueType: `counter` | `timestamp` | `token`
133
+ /** Initial value (for token type) */
134
+ initialValue?: string
135
+ }
136
+
137
+ /**
138
+ * Configure a dynamic URL parameter that is evaluated per-request.
139
+ */
140
+ export interface SetDynamicParamCommand {
141
+ type: `set-dynamic-param`
142
+ /** Param name to set */
143
+ name: string
144
+ /** Type of dynamic value */
145
+ valueType: `counter` | `timestamp`
146
+ }
147
+
148
+ /**
149
+ * Clear all dynamic headers and params.
150
+ */
151
+ export interface ClearDynamicCommand {
152
+ type: `clear-dynamic`
153
+ }
154
+
155
+ // =============================================================================
156
+ // Benchmark Commands
157
+ // =============================================================================
158
+
159
+ /**
160
+ * Execute a timed benchmark operation.
161
+ * The adapter times the operation internally using high-resolution timing.
162
+ */
163
+ export interface BenchmarkCommand {
164
+ type: `benchmark`
165
+ /** Unique ID for this benchmark iteration */
166
+ iterationId: string
167
+ /** The operation to benchmark */
168
+ operation: BenchmarkOperation
169
+ }
170
+
171
+ /**
172
+ * Benchmark operation types - what to measure.
173
+ */
174
+ export type BenchmarkOperation =
175
+ | BenchmarkAppendOp
176
+ | BenchmarkReadOp
177
+ | BenchmarkRoundtripOp
178
+ | BenchmarkCreateOp
179
+ | BenchmarkThroughputAppendOp
180
+ | BenchmarkThroughputReadOp
181
+
182
+ export interface BenchmarkAppendOp {
183
+ op: `append`
184
+ path: string
185
+ /** Size in bytes - adapter generates random payload */
186
+ size: number
187
+ }
188
+
189
+ export interface BenchmarkReadOp {
190
+ op: `read`
191
+ path: string
192
+ offset?: string
193
+ }
194
+
195
+ export interface BenchmarkRoundtripOp {
196
+ op: `roundtrip`
197
+ path: string
198
+ /** Size in bytes */
199
+ size: number
200
+ /** Live mode for reading */
201
+ live?: `long-poll` | `sse`
202
+ /** Content type for SSE compatibility */
203
+ contentType?: string
204
+ }
205
+
206
+ export interface BenchmarkCreateOp {
207
+ op: `create`
208
+ path: string
209
+ contentType?: string
210
+ }
211
+
212
+ export interface BenchmarkThroughputAppendOp {
213
+ op: `throughput_append`
214
+ path: string
215
+ /** Number of messages to send */
216
+ count: number
217
+ /** Size per message in bytes */
218
+ size: number
219
+ /** Concurrency level */
220
+ concurrency: number
221
+ }
222
+
223
+ export interface BenchmarkThroughputReadOp {
224
+ op: `throughput_read`
225
+ path: string
226
+ /** Expected number of JSON messages to read and parse */
227
+ expectedCount?: number
228
+ }
229
+
230
+ /**
231
+ * All possible commands from test runner to client.
232
+ */
233
+ export type TestCommand =
234
+ | InitCommand
235
+ | CreateCommand
236
+ | ConnectCommand
237
+ | AppendCommand
238
+ | ReadCommand
239
+ | HeadCommand
240
+ | DeleteCommand
241
+ | ShutdownCommand
242
+ | SetDynamicHeaderCommand
243
+ | SetDynamicParamCommand
244
+ | ClearDynamicCommand
245
+ | BenchmarkCommand
246
+
247
+ // =============================================================================
248
+ // Results (sent from client adapter to test runner via stdout)
249
+ // =============================================================================
250
+
251
+ /**
252
+ * Successful initialization result.
253
+ */
254
+ export interface InitResult {
255
+ type: `init`
256
+ success: true
257
+ /** Client implementation name (e.g., "typescript", "python", "go") */
258
+ clientName: string
259
+ /** Client implementation version */
260
+ clientVersion: string
261
+ /** Supported features */
262
+ features?: {
263
+ /** Supports automatic batching */
264
+ batching?: boolean
265
+ /** Supports SSE mode */
266
+ sse?: boolean
267
+ /** Supports long-poll mode */
268
+ longPoll?: boolean
269
+ /** Supports streaming reads */
270
+ streaming?: boolean
271
+ /** Supports dynamic headers/params (functions evaluated per-request) */
272
+ dynamicHeaders?: boolean
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Successful create result.
278
+ */
279
+ export interface CreateResult {
280
+ type: `create`
281
+ success: true
282
+ /** HTTP status code received */
283
+ status: number
284
+ /** Stream offset after creation */
285
+ offset?: string
286
+ /** Response headers of interest */
287
+ headers?: Record<string, string>
288
+ }
289
+
290
+ /**
291
+ * Successful connect result.
292
+ */
293
+ export interface ConnectResult {
294
+ type: `connect`
295
+ success: true
296
+ status: number
297
+ offset?: string
298
+ headers?: Record<string, string>
299
+ }
300
+
301
+ /**
302
+ * Successful append result.
303
+ */
304
+ export interface AppendResult {
305
+ type: `append`
306
+ success: true
307
+ status: number
308
+ /** New offset after append */
309
+ offset?: string
310
+ /** Response headers */
311
+ headers?: Record<string, string>
312
+ /** Headers that were sent in the request (for dynamic header testing) */
313
+ headersSent?: Record<string, string>
314
+ /** Params that were sent in the request (for dynamic param testing) */
315
+ paramsSent?: Record<string, string>
316
+ }
317
+
318
+ /**
319
+ * A chunk of data read from the stream.
320
+ */
321
+ export interface ReadChunk {
322
+ /** Data content - string for text, base64 for binary */
323
+ data: string
324
+ /** Whether data is base64 encoded */
325
+ binary?: boolean
326
+ /** Offset of this chunk */
327
+ offset?: string
328
+ }
329
+
330
+ /**
331
+ * Successful read result.
332
+ */
333
+ export interface ReadResult {
334
+ type: `read`
335
+ success: true
336
+ status: number
337
+ /** Chunks of data read */
338
+ chunks: Array<ReadChunk>
339
+ /** Final offset after reading */
340
+ offset?: string
341
+ /** Whether stream is up-to-date (caught up to head) */
342
+ upToDate?: boolean
343
+ /** Cursor value if provided */
344
+ cursor?: string
345
+ /** Response headers */
346
+ headers?: Record<string, string>
347
+ /** Headers that were sent in the request (for dynamic header testing) */
348
+ headersSent?: Record<string, string>
349
+ /** Params that were sent in the request (for dynamic param testing) */
350
+ paramsSent?: Record<string, string>
351
+ }
352
+
353
+ /**
354
+ * Successful head result.
355
+ */
356
+ export interface HeadResult {
357
+ type: `head`
358
+ success: true
359
+ status: number
360
+ /** Current tail offset */
361
+ offset?: string
362
+ /** Stream content type */
363
+ contentType?: string
364
+ /** TTL remaining in seconds */
365
+ ttlSeconds?: number
366
+ /** Absolute expiry (ISO 8601) */
367
+ expiresAt?: string
368
+ headers?: Record<string, string>
369
+ }
370
+
371
+ /**
372
+ * Successful delete result.
373
+ */
374
+ export interface DeleteResult {
375
+ type: `delete`
376
+ success: true
377
+ status: number
378
+ headers?: Record<string, string>
379
+ }
380
+
381
+ /**
382
+ * Successful shutdown result.
383
+ */
384
+ export interface ShutdownResult {
385
+ type: `shutdown`
386
+ success: true
387
+ }
388
+
389
+ /**
390
+ * Successful set-dynamic-header result.
391
+ */
392
+ export interface SetDynamicHeaderResult {
393
+ type: `set-dynamic-header`
394
+ success: true
395
+ }
396
+
397
+ /**
398
+ * Successful set-dynamic-param result.
399
+ */
400
+ export interface SetDynamicParamResult {
401
+ type: `set-dynamic-param`
402
+ success: true
403
+ }
404
+
405
+ /**
406
+ * Successful clear-dynamic result.
407
+ */
408
+ export interface ClearDynamicResult {
409
+ type: `clear-dynamic`
410
+ success: true
411
+ }
412
+
413
+ /**
414
+ * Successful benchmark result with timing.
415
+ */
416
+ export interface BenchmarkResult {
417
+ type: `benchmark`
418
+ success: true
419
+ iterationId: string
420
+ /** Timing in nanoseconds (as string since bigint doesn't JSON serialize) */
421
+ durationNs: string
422
+ /** Optional metrics */
423
+ metrics?: {
424
+ /** Bytes transferred */
425
+ bytesTransferred?: number
426
+ /** Messages processed */
427
+ messagesProcessed?: number
428
+ /** Operations per second (for throughput tests) */
429
+ opsPerSecond?: number
430
+ /** Bytes per second (for throughput tests) */
431
+ bytesPerSecond?: number
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Error result for any failed operation.
437
+ */
438
+ export interface ErrorResult {
439
+ type: `error`
440
+ success: false
441
+ /** Original command type that failed */
442
+ commandType: TestCommand[`type`]
443
+ /** HTTP status code if available */
444
+ status?: number
445
+ /** Error code (e.g., "NETWORK_ERROR", "TIMEOUT", "CONFLICT") */
446
+ errorCode: string
447
+ /** Human-readable error message */
448
+ message: string
449
+ /** Additional error details */
450
+ details?: Record<string, unknown>
451
+ }
452
+
453
+ /**
454
+ * All possible results from client to test runner.
455
+ */
456
+ export type TestResult =
457
+ | InitResult
458
+ | CreateResult
459
+ | ConnectResult
460
+ | AppendResult
461
+ | ReadResult
462
+ | HeadResult
463
+ | DeleteResult
464
+ | ShutdownResult
465
+ | SetDynamicHeaderResult
466
+ | SetDynamicParamResult
467
+ | ClearDynamicResult
468
+ | BenchmarkResult
469
+ | ErrorResult
470
+
471
+ // =============================================================================
472
+ // Utilities
473
+ // =============================================================================
474
+
475
+ /**
476
+ * Parse a JSON line into a TestCommand.
477
+ */
478
+ export function parseCommand(line: string): TestCommand {
479
+ return JSON.parse(line) as TestCommand
480
+ }
481
+
482
+ /**
483
+ * Serialize a TestResult to a JSON line.
484
+ */
485
+ export function serializeResult(result: TestResult): string {
486
+ return JSON.stringify(result)
487
+ }
488
+
489
+ /**
490
+ * Parse a JSON line into a TestResult.
491
+ */
492
+ export function parseResult(line: string): TestResult {
493
+ return JSON.parse(line) as TestResult
494
+ }
495
+
496
+ /**
497
+ * Serialize a TestCommand to a JSON line.
498
+ */
499
+ export function serializeCommand(command: TestCommand): string {
500
+ return JSON.stringify(command)
501
+ }
502
+
503
+ /**
504
+ * Encode binary data to base64 for transmission.
505
+ */
506
+ export function encodeBase64(data: Uint8Array): string {
507
+ return Buffer.from(data).toString(`base64`)
508
+ }
509
+
510
+ /**
511
+ * Decode base64 string back to binary data.
512
+ */
513
+ export function decodeBase64(encoded: string): Uint8Array {
514
+ return new Uint8Array(Buffer.from(encoded, `base64`))
515
+ }
516
+
517
+ /**
518
+ * Standard error codes for ErrorResult.
519
+ */
520
+ export const ErrorCodes = {
521
+ /** Network connection failed */
522
+ NETWORK_ERROR: `NETWORK_ERROR`,
523
+ /** Operation timed out */
524
+ TIMEOUT: `TIMEOUT`,
525
+ /** Stream already exists (409 Conflict) */
526
+ CONFLICT: `CONFLICT`,
527
+ /** Stream not found (404) */
528
+ NOT_FOUND: `NOT_FOUND`,
529
+ /** Sequence number conflict (409) */
530
+ SEQUENCE_CONFLICT: `SEQUENCE_CONFLICT`,
531
+ /** Invalid offset format */
532
+ INVALID_OFFSET: `INVALID_OFFSET`,
533
+ /** Server returned unexpected status */
534
+ UNEXPECTED_STATUS: `UNEXPECTED_STATUS`,
535
+ /** Failed to parse response */
536
+ PARSE_ERROR: `PARSE_ERROR`,
537
+ /** Client internal error */
538
+ INTERNAL_ERROR: `INTERNAL_ERROR`,
539
+ /** Operation not supported by this client */
540
+ NOT_SUPPORTED: `NOT_SUPPORTED`,
541
+ } as const
542
+
543
+ export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]
544
+
545
+ // =============================================================================
546
+ // Benchmark Statistics
547
+ // =============================================================================
548
+
549
+ /**
550
+ * Statistical summary of benchmark results.
551
+ */
552
+ export interface BenchmarkStats {
553
+ /** Minimum value in milliseconds */
554
+ min: number
555
+ /** Maximum value in milliseconds */
556
+ max: number
557
+ /** Arithmetic mean in milliseconds */
558
+ mean: number
559
+ /** Median (p50) in milliseconds */
560
+ median: number
561
+ /** 75th percentile in milliseconds */
562
+ p75: number
563
+ /** 95th percentile in milliseconds */
564
+ p95: number
565
+ /** 99th percentile in milliseconds */
566
+ p99: number
567
+ /** Standard deviation in milliseconds */
568
+ stdDev: number
569
+ /** Margin of error (95% confidence) in milliseconds */
570
+ marginOfError: number
571
+ /** Number of samples */
572
+ sampleCount: number
573
+ }
574
+
575
+ /**
576
+ * Calculate statistics from an array of durations in nanoseconds.
577
+ */
578
+ export function calculateStats(durationsNs: Array<bigint>): BenchmarkStats {
579
+ if (durationsNs.length === 0) {
580
+ return {
581
+ min: 0,
582
+ max: 0,
583
+ mean: 0,
584
+ median: 0,
585
+ p75: 0,
586
+ p95: 0,
587
+ p99: 0,
588
+ stdDev: 0,
589
+ marginOfError: 0,
590
+ sampleCount: 0,
591
+ }
592
+ }
593
+
594
+ // Convert to milliseconds for statistics
595
+ const samplesMs = durationsNs.map((ns) => Number(ns) / 1_000_000)
596
+ const sorted = [...samplesMs].sort((a, b) => a - b)
597
+ const n = sorted.length
598
+
599
+ const min = sorted[0]!
600
+ const max = sorted[n - 1]!
601
+ const mean = samplesMs.reduce((a, b) => a + b, 0) / n
602
+
603
+ // Percentiles (nearest rank method, 0-based indexing)
604
+ const percentile = (p: number) => {
605
+ const idx = Math.floor((n - 1) * p)
606
+ return sorted[idx]!
607
+ }
608
+
609
+ const median = percentile(0.5)
610
+ const p75 = percentile(0.75)
611
+ const p95 = percentile(0.95)
612
+ const p99 = percentile(0.99)
613
+
614
+ // Standard deviation
615
+ const squaredDiffs = samplesMs.map((v) => Math.pow(v - mean, 2))
616
+ const variance = squaredDiffs.reduce((a, b) => a + b, 0) / n
617
+ const stdDev = Math.sqrt(variance)
618
+
619
+ // Margin of error (95% confidence, z = 1.96)
620
+ const marginOfError = (1.96 * stdDev) / Math.sqrt(n)
621
+
622
+ return {
623
+ min,
624
+ max,
625
+ mean,
626
+ median,
627
+ p75,
628
+ p95,
629
+ p99,
630
+ stdDev,
631
+ marginOfError,
632
+ sampleCount: n,
633
+ }
634
+ }
635
+
636
+ /**
637
+ * Format a BenchmarkStats object for display.
638
+ */
639
+ export function formatStats(
640
+ stats: BenchmarkStats,
641
+ unit = `ms`
642
+ ): Record<string, string> {
643
+ const fmt = (v: number) => `${v.toFixed(2)} ${unit}`
644
+ return {
645
+ Min: fmt(stats.min),
646
+ Max: fmt(stats.max),
647
+ Mean: fmt(stats.mean),
648
+ Median: fmt(stats.median),
649
+ P75: fmt(stats.p75),
650
+ P95: fmt(stats.p95),
651
+ P99: fmt(stats.p99),
652
+ StdDev: fmt(stats.stdDev),
653
+ "Margin of Error": fmt(stats.marginOfError),
654
+ Samples: stats.sampleCount.toString(),
655
+ }
656
+ }