@durable-streams/client-conformance-tests 0.1.8 → 0.1.9

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 (35) hide show
  1. package/dist/adapters/typescript-adapter.cjs +72 -22
  2. package/dist/adapters/typescript-adapter.js +72 -22
  3. package/dist/{benchmark-runner-CrE6JkbX.js → benchmark-runner-81waaCzs.js} +89 -9
  4. package/dist/{benchmark-runner-Db4he452.cjs → benchmark-runner-DliEfq9k.cjs} +93 -8
  5. package/dist/cli.cjs +41 -5
  6. package/dist/cli.js +41 -5
  7. package/dist/index.cjs +2 -2
  8. package/dist/index.d.cts +50 -3
  9. package/dist/index.d.ts +50 -3
  10. package/dist/index.js +2 -2
  11. package/dist/{protocol-qb83AeUH.js → protocol-1p0soayz.js} +2 -1
  12. package/dist/{protocol-D37G3c4e.d.cts → protocol-BxZTqJmO.d.cts} +67 -5
  13. package/dist/{protocol-XeAOKBD-.cjs → protocol-IioVPNaP.cjs} +2 -1
  14. package/dist/{protocol-Mcbiq3nQ.d.ts → protocol-JuFzdV5x.d.ts} +67 -5
  15. package/dist/protocol.cjs +1 -1
  16. package/dist/protocol.d.cts +2 -2
  17. package/dist/protocol.d.ts +2 -2
  18. package/dist/protocol.js +1 -1
  19. package/package.json +8 -3
  20. package/src/adapters/typescript-adapter.ts +110 -32
  21. package/src/benchmark-runner.ts +75 -1
  22. package/src/benchmark-scenarios.ts +4 -4
  23. package/src/cli.ts +46 -5
  24. package/src/protocol.ts +75 -2
  25. package/src/runner.ts +72 -1
  26. package/src/test-cases.ts +55 -0
  27. package/test-cases/consumer/error-context.yaml +67 -0
  28. package/test-cases/consumer/json-parsing-errors.yaml +115 -0
  29. package/test-cases/consumer/read-auto.yaml +155 -0
  30. package/test-cases/consumer/read-sse.yaml +24 -0
  31. package/test-cases/consumer/retry-resilience.yaml +28 -0
  32. package/test-cases/consumer/sse-parsing-errors.yaml +121 -0
  33. package/test-cases/producer/error-context.yaml +72 -0
  34. package/test-cases/producer/idempotent-json-batching.yaml +40 -0
  35. package/test-cases/validation/input-validation.yaml +192 -0
@@ -0,0 +1,155 @@
1
+ id: consumer-auto
2
+ name: Auto Live Mode
3
+ description: Tests for auto live mode that catches up first then transitions to live tailing
4
+ category: consumer
5
+ tags:
6
+ - core
7
+ - read
8
+ - auto
9
+ - live
10
+ requires:
11
+ - auto
12
+
13
+ tests:
14
+ - id: auto-catches-up-with-existing-data
15
+ name: Auto mode catches up with existing data first
16
+ description: Auto mode should read all existing data before entering live mode
17
+ setup:
18
+ - action: create
19
+ as: streamPath
20
+ - action: append
21
+ path: ${streamPath}
22
+ data: "data1"
23
+ - action: append
24
+ path: ${streamPath}
25
+ data: "data2"
26
+ - action: append
27
+ path: ${streamPath}
28
+ data: "data3"
29
+ operations:
30
+ - action: read
31
+ path: ${streamPath}
32
+ live: true
33
+ timeoutMs: 2000
34
+ waitForUpToDate: true
35
+ expect:
36
+ data: "data1data2data3"
37
+ upToDate: true
38
+
39
+ - id: auto-waits-for-new-data
40
+ name: Auto mode waits for new data after catching up
41
+ description: After catching up, auto mode should wait for new data
42
+ setup:
43
+ - action: create
44
+ as: streamPath
45
+ - action: append
46
+ path: ${streamPath}
47
+ data: "initial"
48
+ expect:
49
+ storeOffsetAs: initialOffset
50
+ operations:
51
+ # Start auto read in background (will catch up then wait)
52
+ - action: read
53
+ path: ${streamPath}
54
+ offset: ${initialOffset}
55
+ live: true
56
+ timeoutMs: 10000
57
+ background: true
58
+ as: readOp
59
+ # Wait for the read to start and catch up
60
+ - action: wait
61
+ ms: 300
62
+ # Append new data
63
+ - action: server-append
64
+ path: ${streamPath}
65
+ data: "new-data"
66
+ # Wait for the background read to receive the data
67
+ - action: await
68
+ ref: readOp
69
+ expect:
70
+ dataContains: "new-data"
71
+
72
+ - id: auto-returns-immediately-on-existing-data
73
+ name: Auto mode returns immediately with existing data
74
+ description: Auto mode should not delay when there's data to return
75
+ setup:
76
+ - action: create
77
+ as: streamPath
78
+ - action: append
79
+ path: ${streamPath}
80
+ data: "existing"
81
+ operations:
82
+ - action: read
83
+ path: ${streamPath}
84
+ live: true
85
+ timeoutMs: 1000
86
+ waitForUpToDate: true
87
+ expect:
88
+ data: "existing"
89
+ upToDate: true
90
+
91
+ - id: auto-resumes-from-offset
92
+ name: Auto mode resumes from provided offset
93
+ description: Auto mode should start from the given offset, not beginning
94
+ setup:
95
+ - action: create
96
+ as: streamPath
97
+ - action: append
98
+ path: ${streamPath}
99
+ data: "old-data"
100
+ expect:
101
+ storeOffsetAs: offset
102
+ - action: append
103
+ path: ${streamPath}
104
+ data: "new-data"
105
+ operations:
106
+ - action: read
107
+ path: ${streamPath}
108
+ offset: ${offset}
109
+ live: true
110
+ timeoutMs: 1000
111
+ waitForUpToDate: true
112
+ expect:
113
+ data: "new-data"
114
+ upToDate: true
115
+
116
+ - id: auto-empty-stream
117
+ name: Auto mode handles empty stream
118
+ description: Auto mode should handle empty streams gracefully
119
+ setup:
120
+ - action: create
121
+ as: streamPath
122
+ operations:
123
+ - action: read
124
+ path: ${streamPath}
125
+ live: true
126
+ timeoutMs: 500
127
+ waitForUpToDate: true
128
+ expect:
129
+ chunkCount: 0
130
+ upToDate: true
131
+
132
+ - id: auto-multiple-chunks
133
+ name: Auto mode collects multiple chunks
134
+ description: Auto mode should receive multiple data chunks
135
+ setup:
136
+ - action: create
137
+ as: streamPath
138
+ operations:
139
+ - action: append
140
+ path: ${streamPath}
141
+ data: "chunk1"
142
+ - action: append
143
+ path: ${streamPath}
144
+ data: "chunk2"
145
+ - action: append
146
+ path: ${streamPath}
147
+ data: "chunk3"
148
+ - action: read
149
+ path: ${streamPath}
150
+ live: true
151
+ timeoutMs: 2000
152
+ waitForUpToDate: true
153
+ expect:
154
+ data: "chunk1chunk2chunk3"
155
+ upToDate: true
@@ -230,3 +230,27 @@ tests:
230
230
  expect:
231
231
  data: "a\u0085b\u2028c\u2029d"
232
232
  minChunks: 1
233
+
234
+ - id: sse-strips-single-leading-space
235
+ name: SSE strips only single leading space
236
+ description: |
237
+ Per EventSource spec section 9.2.4: "If value starts with a
238
+ U+0020 SPACE character, remove it from value."
239
+ Only ONE space should be removed, preserving any additional spaces.
240
+ This prevents parsers from using trim() or similar which would
241
+ incorrectly strip all leading/trailing whitespace.
242
+ setup:
243
+ - action: create
244
+ as: streamPath
245
+ contentType: text/plain
246
+ - action: append
247
+ path: ${streamPath}
248
+ data: " two leading spaces"
249
+ operations:
250
+ - action: read
251
+ path: ${streamPath}
252
+ live: sse
253
+ waitForUpToDate: true
254
+ expect:
255
+ data: " two leading spaces"
256
+ minChunks: 1
@@ -158,3 +158,31 @@ tests:
158
158
  data: "retry-data"
159
159
  cleanup:
160
160
  - action: clear-errors
161
+
162
+ - id: retry-exhaustion-fails
163
+ name: Client fails after retry limit exhaustion
164
+ description: |
165
+ Client should eventually fail when transient errors persist beyond
166
+ the retry limit. This prevents infinite retry loops and ensures
167
+ clients surface errors to callers rather than hanging forever.
168
+ setup:
169
+ - action: create
170
+ as: streamPath
171
+ - action: append
172
+ path: ${streamPath}
173
+ data: "test-data"
174
+ operations:
175
+ # Inject many 500 errors - more than any reasonable retry limit
176
+ - action: inject-error
177
+ path: ${streamPath}
178
+ status: 500
179
+ count: 20
180
+ # Client should eventually give up and return an error
181
+ - action: read
182
+ path: ${streamPath}
183
+ expect:
184
+ error: true
185
+ # The error could be 500 (last failed attempt) or a client-specific
186
+ # retry exhaustion error - either is acceptable
187
+ cleanup:
188
+ - action: clear-errors
@@ -0,0 +1,121 @@
1
+ id: consumer-sse-parsing-errors
2
+ name: SSE Parsing Error Handling
3
+ description: Tests that clients properly handle malformed SSE events instead of silently failing
4
+ category: consumer
5
+ requires:
6
+ - sse
7
+ tags:
8
+ - sse
9
+ - error-handling
10
+ - resilience
11
+ - fault-injection
12
+
13
+ tests:
14
+ - id: unknown-sse-event-type
15
+ name: Client gracefully handles unknown SSE event type
16
+ description: Client should ignore unknown event types and continue processing
17
+ setup:
18
+ - action: create
19
+ as: streamPath
20
+ - action: append
21
+ path: ${streamPath}
22
+ data: "test-data"
23
+ operations:
24
+ # Inject an unknown SSE event type before the stream starts
25
+ - action: inject-error
26
+ path: ${streamPath}
27
+ injectSseEvent:
28
+ eventType: "unknown-future-type"
29
+ data: '{"some":"future-data"}'
30
+ count: 1
31
+ # Client should skip the unknown event and still read the actual data
32
+ - action: read
33
+ path: ${streamPath}
34
+ live: sse
35
+ maxChunks: 1
36
+ expect:
37
+ data: "test-data"
38
+ cleanup:
39
+ - action: clear-errors
40
+
41
+ - id: malformed-control-event-json
42
+ name: Client throws on malformed control event JSON
43
+ description: Control events contain critical offset data - malformed JSON should throw
44
+ setup:
45
+ - action: create
46
+ as: streamPath
47
+ - action: append
48
+ path: ${streamPath}
49
+ data: "data-before-error"
50
+ operations:
51
+ # Inject a control event with invalid JSON
52
+ - action: inject-error
53
+ path: ${streamPath}
54
+ injectSseEvent:
55
+ eventType: "control"
56
+ data: "{invalid json here"
57
+ count: 1
58
+ # Client should throw a parse error
59
+ - action: read
60
+ path: ${streamPath}
61
+ live: sse
62
+ maxChunks: 1
63
+ expect:
64
+ errorCode: PARSE_ERROR
65
+ cleanup:
66
+ - action: clear-errors
67
+
68
+ - id: empty-control-event-data
69
+ name: Client handles empty control event data
70
+ description: Empty control event should be handled gracefully
71
+ setup:
72
+ - action: create
73
+ as: streamPath
74
+ - action: append
75
+ path: ${streamPath}
76
+ data: "actual-data"
77
+ operations:
78
+ # Inject a control event with empty data
79
+ - action: inject-error
80
+ path: ${streamPath}
81
+ injectSseEvent:
82
+ eventType: "control"
83
+ data: ""
84
+ count: 1
85
+ # Client should throw because empty is not valid JSON
86
+ - action: read
87
+ path: ${streamPath}
88
+ live: sse
89
+ maxChunks: 1
90
+ expect:
91
+ errorCode: PARSE_ERROR
92
+ cleanup:
93
+ - action: clear-errors
94
+
95
+ - id: multiple-unknown-events
96
+ name: Client handles multiple unknown event types
97
+ description: Client should skip all unknown events and process valid ones
98
+ setup:
99
+ - action: create
100
+ as: streamPath
101
+ - action: append
102
+ path: ${streamPath}
103
+ data: "expected-data"
104
+ operations:
105
+ # Inject first unknown event
106
+ - action: inject-error
107
+ path: ${streamPath}
108
+ injectSseEvent:
109
+ eventType: "future-event-v2"
110
+ data: '{"version":2}'
111
+ count: 1
112
+ # Note: Only one injection per fault, so this tests single unknown event
113
+ # Client should still read the actual data
114
+ - action: read
115
+ path: ${streamPath}
116
+ live: sse
117
+ maxChunks: 1
118
+ expect:
119
+ data: "expected-data"
120
+ cleanup:
121
+ - action: clear-errors
@@ -0,0 +1,72 @@
1
+ id: producer-error-context
2
+ name: Producer Error Context
3
+ description: |
4
+ Tests that producer error messages include helpful context.
5
+ Ensures errors provide enough information for debugging.
6
+ Note: These tests validate server-level responses (status codes).
7
+ Client-level error message validation requires idempotent-append tests.
8
+ category: producer
9
+ tags:
10
+ - errors
11
+ - context
12
+ - messages
13
+
14
+ tests:
15
+ - id: sequence-conflict-returns-expected-seq
16
+ name: Sequence conflict returns expected sequence info
17
+ description: 409 sequence conflict should return expected/received seq headers
18
+ setup:
19
+ - action: create
20
+ as: streamPath
21
+ contentType: text/plain
22
+ operations:
23
+ # First append succeeds
24
+ - action: server-append
25
+ path: ${streamPath}
26
+ data: "msg0"
27
+ producerId: error-context-producer
28
+ producerEpoch: 0
29
+ producerSeq: 0
30
+ expect:
31
+ status: 200
32
+ # Skip seq=1, try seq=2 -> 409 gap
33
+ - action: server-append
34
+ path: ${streamPath}
35
+ data: "msg2"
36
+ producerId: error-context-producer
37
+ producerEpoch: 0
38
+ producerSeq: 2
39
+ expect:
40
+ status: 409
41
+ # Server returns expected/received seq in headers
42
+ producerExpectedSeq: 1
43
+ producerReceivedSeq: 2
44
+
45
+ - id: stale-epoch-returns-current-epoch
46
+ name: Stale epoch returns current epoch info
47
+ description: 403 stale epoch should return current epoch in header
48
+ setup:
49
+ - action: create
50
+ as: streamPath
51
+ contentType: text/plain
52
+ operations:
53
+ # Establish epoch 1
54
+ - action: server-append
55
+ path: ${streamPath}
56
+ data: "epoch1"
57
+ producerId: epoch-context-producer
58
+ producerEpoch: 1
59
+ producerSeq: 0
60
+ expect:
61
+ status: 200
62
+ # Try with stale epoch 0
63
+ - action: server-append
64
+ path: ${streamPath}
65
+ data: "stale"
66
+ producerId: epoch-context-producer
67
+ producerEpoch: 0
68
+ producerSeq: 0
69
+ expect:
70
+ status: 403
71
+ # Server returns current epoch in header
72
+ producerEpoch: 1
@@ -132,3 +132,43 @@ tests:
132
132
  expect:
133
133
  data: "Hello World"
134
134
  upToDate: true
135
+
136
+ - id: client-deduplication-works
137
+ name: Client IdempotentProducer correctly deduplicates
138
+ description: |
139
+ This test verifies that the CLIENT's IdempotentProducer implementation
140
+ correctly sends Producer-Id/Epoch/Seq headers so server-side deduplication
141
+ actually works. Without correct headers, retries would create duplicates.
142
+ setup:
143
+ - action: create
144
+ as: streamPath
145
+ contentType: application/json
146
+ operations:
147
+ # Send a batch of items - this creates epoch=0, seq=0
148
+ - action: idempotent-append-batch
149
+ path: ${streamPath}
150
+ producerId: dedup-test-producer
151
+ epoch: 0
152
+ items:
153
+ - data: '{"msg": "first"}'
154
+ - data: '{"msg": "second"}'
155
+ expect:
156
+ allSucceed: true
157
+ # Send the SAME batch again with a NEW producer instance at epoch=0
158
+ # If client sends correct headers, server should deduplicate
159
+ # If client sends wrong headers, this would create duplicate data
160
+ - action: idempotent-append-batch
161
+ path: ${streamPath}
162
+ producerId: dedup-test-producer
163
+ epoch: 0
164
+ items:
165
+ - data: '{"msg": "first"}'
166
+ - data: '{"msg": "second"}'
167
+ expect:
168
+ allSucceed: true
169
+ # Verify NO duplicates - should only have 2 messages, not 4
170
+ - action: read
171
+ path: ${streamPath}
172
+ expect:
173
+ data: '[{"msg":"first"},{"msg":"second"}]'
174
+ upToDate: true
@@ -0,0 +1,192 @@
1
+ id: input-validation
2
+ name: Client Input Validation
3
+ description: Tests for client-side input parameter validation
4
+ category: lifecycle
5
+ tags:
6
+ - validation
7
+ - core
8
+
9
+ tests:
10
+ # RetryOptions validation tests (PHP-specific - TS uses BackoffOptions)
11
+ - id: retry-options-valid
12
+ name: Valid RetryOptions passes validation
13
+ description: Default retry options should be accepted
14
+ requires:
15
+ - retryOptions
16
+ operations:
17
+ - action: validate
18
+ target:
19
+ target: retry-options
20
+ maxRetries: 3
21
+ initialDelayMs: 100
22
+ maxDelayMs: 5000
23
+ multiplier: 2.0
24
+ expect:
25
+ valid: true
26
+
27
+ - id: retry-options-negative-max-retries
28
+ name: Negative maxRetries rejected
29
+ description: Client should reject negative maxRetries
30
+ requires:
31
+ - retryOptions
32
+ operations:
33
+ - action: validate
34
+ target:
35
+ target: retry-options
36
+ maxRetries: -1
37
+ expect:
38
+ valid: false
39
+ errorCode: INVALID_ARGUMENT
40
+ errorContains: maxRetries
41
+
42
+ - id: retry-options-zero-initial-delay
43
+ name: Zero initialDelayMs rejected
44
+ description: Client should reject zero or negative initialDelayMs
45
+ requires:
46
+ - retryOptions
47
+ operations:
48
+ - action: validate
49
+ target:
50
+ target: retry-options
51
+ initialDelayMs: 0
52
+ expect:
53
+ valid: false
54
+ errorCode: INVALID_ARGUMENT
55
+ errorContains: initialDelayMs
56
+
57
+ - id: retry-options-negative-initial-delay
58
+ name: Negative initialDelayMs rejected
59
+ description: Client should reject negative initialDelayMs
60
+ requires:
61
+ - retryOptions
62
+ operations:
63
+ - action: validate
64
+ target:
65
+ target: retry-options
66
+ initialDelayMs: -100
67
+ expect:
68
+ valid: false
69
+ errorCode: INVALID_ARGUMENT
70
+ errorContains: initialDelayMs
71
+
72
+ - id: retry-options-max-less-than-initial
73
+ name: maxDelayMs less than initialDelayMs rejected
74
+ description: Client should reject maxDelayMs < initialDelayMs
75
+ requires:
76
+ - retryOptions
77
+ operations:
78
+ - action: validate
79
+ target:
80
+ target: retry-options
81
+ initialDelayMs: 1000
82
+ maxDelayMs: 500
83
+ expect:
84
+ valid: false
85
+ errorCode: INVALID_ARGUMENT
86
+ errorContains: maxDelayMs
87
+
88
+ - id: retry-options-multiplier-less-than-one
89
+ name: Multiplier less than 1.0 rejected
90
+ description: Client should reject multiplier < 1.0
91
+ requires:
92
+ - retryOptions
93
+ operations:
94
+ - action: validate
95
+ target:
96
+ target: retry-options
97
+ multiplier: 0.5
98
+ expect:
99
+ valid: false
100
+ errorCode: INVALID_ARGUMENT
101
+ errorContains: multiplier
102
+
103
+ # IdempotentProducer validation tests (shared across clients)
104
+ - id: producer-valid
105
+ name: Valid IdempotentProducer passes validation
106
+ description: Default producer options should be accepted
107
+ operations:
108
+ - action: validate
109
+ target:
110
+ target: idempotent-producer
111
+ producerId: test-producer
112
+ epoch: 0
113
+ maxBatchBytes: 1048576
114
+ expect:
115
+ valid: true
116
+
117
+ - id: producer-negative-epoch
118
+ name: Negative epoch rejected
119
+ description: Client should reject negative epoch
120
+ operations:
121
+ - action: validate
122
+ target:
123
+ target: idempotent-producer
124
+ producerId: test-producer
125
+ epoch: -1
126
+ expect:
127
+ valid: false
128
+ errorCode: INVALID_ARGUMENT
129
+ errorContains: epoch
130
+
131
+ - id: producer-zero-max-batch-bytes
132
+ name: Zero maxBatchBytes rejected
133
+ description: Client should reject zero maxBatchBytes (Go treats 0 as default)
134
+ requires:
135
+ - strictZeroValidation
136
+ operations:
137
+ - action: validate
138
+ target:
139
+ target: idempotent-producer
140
+ producerId: test-producer
141
+ maxBatchBytes: 0
142
+ expect:
143
+ valid: false
144
+ errorCode: INVALID_ARGUMENT
145
+ errorContains: maxBatchBytes
146
+
147
+ - id: producer-negative-max-batch-bytes
148
+ name: Negative maxBatchBytes rejected
149
+ description: Client should reject negative maxBatchBytes
150
+ operations:
151
+ - action: validate
152
+ target:
153
+ target: idempotent-producer
154
+ producerId: test-producer
155
+ maxBatchBytes: -1
156
+ expect:
157
+ valid: false
158
+ errorCode: INVALID_ARGUMENT
159
+ errorContains: maxBatchBytes
160
+
161
+ # maxBatchItems validation (PHP-specific - TS doesn't have this option)
162
+ - id: producer-zero-max-batch-items
163
+ name: Zero maxBatchItems rejected
164
+ description: Client should reject zero maxBatchItems
165
+ requires:
166
+ - batchItems
167
+ operations:
168
+ - action: validate
169
+ target:
170
+ target: idempotent-producer
171
+ producerId: test-producer
172
+ maxBatchItems: 0
173
+ expect:
174
+ valid: false
175
+ errorCode: INVALID_ARGUMENT
176
+ errorContains: maxBatchItems
177
+
178
+ - id: producer-negative-max-batch-items
179
+ name: Negative maxBatchItems rejected
180
+ description: Client should reject negative maxBatchItems
181
+ requires:
182
+ - batchItems
183
+ operations:
184
+ - action: validate
185
+ target:
186
+ target: idempotent-producer
187
+ producerId: test-producer
188
+ maxBatchItems: -1
189
+ expect:
190
+ valid: false
191
+ errorCode: INVALID_ARGUMENT
192
+ errorContains: maxBatchItems