@durable-streams/client-conformance-tests 0.1.6 → 0.1.7

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 (29) hide show
  1. package/dist/adapters/typescript-adapter.cjs +75 -3
  2. package/dist/adapters/typescript-adapter.js +76 -4
  3. package/dist/{benchmark-runner-D-YSAvRy.js → benchmark-runner-CrE6JkbX.js} +86 -8
  4. package/dist/{benchmark-runner-BlKqhoXE.cjs → benchmark-runner-Db4he452.cjs} +87 -8
  5. package/dist/cli.cjs +1 -1
  6. package/dist/cli.js +1 -1
  7. package/dist/index.cjs +1 -1
  8. package/dist/index.d.cts +106 -6
  9. package/dist/index.d.ts +106 -6
  10. package/dist/index.js +1 -1
  11. package/dist/{protocol-3cf94Xyb.d.cts → protocol-D37G3c4e.d.cts} +80 -4
  12. package/dist/{protocol-DyEvTHPF.d.ts → protocol-Mcbiq3nQ.d.ts} +80 -4
  13. package/dist/protocol.d.cts +2 -2
  14. package/dist/protocol.d.ts +2 -2
  15. package/package.json +3 -3
  16. package/src/adapters/typescript-adapter.ts +127 -6
  17. package/src/protocol.ts +85 -1
  18. package/src/runner.ts +178 -13
  19. package/src/test-cases.ts +110 -3
  20. package/test-cases/consumer/error-handling.yaml +42 -0
  21. package/test-cases/consumer/offset-handling.yaml +209 -0
  22. package/test-cases/producer/idempotent/autoclaim.yaml +214 -0
  23. package/test-cases/producer/idempotent/batching.yaml +98 -0
  24. package/test-cases/producer/idempotent/concurrent-requests.yaml +100 -0
  25. package/test-cases/producer/idempotent/epoch-management.yaml +333 -0
  26. package/test-cases/producer/idempotent/error-handling.yaml +194 -0
  27. package/test-cases/producer/idempotent/multi-producer.yaml +322 -0
  28. package/test-cases/producer/idempotent/sequence-validation.yaml +339 -0
  29. package/test-cases/producer/idempotent-json-batching.yaml +134 -0
@@ -0,0 +1,322 @@
1
+ id: idempotent-multi-producer
2
+ name: Idempotent Producer - Multi-Producer Scenarios
3
+ description: |
4
+ Tests for multiple producers on the same stream and producer isolation.
5
+ Each (stream, producerId) has independent epoch/sequence state.
6
+ category: producer
7
+ tags:
8
+ - idempotent
9
+ - multi-producer
10
+ - isolation
11
+
12
+ tests:
13
+ - id: multiple-producers-independent
14
+ name: Multiple producers have independent state
15
+ description: Different producer IDs should have separate epoch/seq tracking
16
+ setup:
17
+ - action: create
18
+ as: streamPath
19
+ contentType: text/plain
20
+ operations:
21
+ # Producer A: seq=0
22
+ - action: server-append
23
+ path: ${streamPath}
24
+ data: "A0"
25
+ producerId: producer-A
26
+ producerEpoch: 0
27
+ producerSeq: 0
28
+ expect:
29
+ status: 200
30
+ # Producer B: seq=0 (independent)
31
+ - action: server-append
32
+ path: ${streamPath}
33
+ data: "B0"
34
+ producerId: producer-B
35
+ producerEpoch: 0
36
+ producerSeq: 0
37
+ expect:
38
+ status: 200
39
+ # Producer A: seq=1
40
+ - action: server-append
41
+ path: ${streamPath}
42
+ data: "A1"
43
+ producerId: producer-A
44
+ producerEpoch: 0
45
+ producerSeq: 1
46
+ expect:
47
+ status: 200
48
+ # Producer B: seq=1
49
+ - action: server-append
50
+ path: ${streamPath}
51
+ data: "B1"
52
+ producerId: producer-B
53
+ producerEpoch: 0
54
+ producerSeq: 1
55
+ expect:
56
+ status: 200
57
+
58
+ - id: producer-scope-per-stream
59
+ name: Same producer ID works independently on different streams
60
+ description: |
61
+ Producer state must be scoped per (stream, producerId), not globally
62
+ by producerId alone. A common implementation bug is to key producer
63
+ state only by producer ID, causing cross-stream interference.
64
+ setup:
65
+ - action: create
66
+ as: streamA
67
+ contentType: text/plain
68
+ - action: create
69
+ as: streamB
70
+ contentType: text/plain
71
+ operations:
72
+ # Same producer, epoch=0, seq=0 on stream A
73
+ - action: server-append
74
+ path: ${streamA}
75
+ data: "streamA-msg"
76
+ producerId: shared-producer
77
+ producerEpoch: 0
78
+ producerSeq: 0
79
+ expect:
80
+ status: 200
81
+ # Same producer, epoch=0, seq=0 on stream B - must be 200, not 204/403
82
+ - action: server-append
83
+ path: ${streamB}
84
+ data: "streamB-msg"
85
+ producerId: shared-producer
86
+ producerEpoch: 0
87
+ producerSeq: 0
88
+ expect:
89
+ status: 200
90
+ # Verify each stream has its own message
91
+ - action: read
92
+ path: ${streamA}
93
+ expect:
94
+ data: "streamA-msg"
95
+ upToDate: true
96
+ - action: read
97
+ path: ${streamB}
98
+ expect:
99
+ data: "streamB-msg"
100
+ upToDate: true
101
+
102
+ - id: producer-epoch-per-stream
103
+ name: Producer epochs are independent per stream
104
+ description: |
105
+ Epoch fencing on one stream must not affect the same producer on
106
+ another stream. Each (stream, producerId) has independent epoch state.
107
+ setup:
108
+ - action: create
109
+ as: streamA
110
+ contentType: text/plain
111
+ - action: create
112
+ as: streamB
113
+ contentType: text/plain
114
+ operations:
115
+ # Establish epoch=5 on stream A
116
+ - action: server-append
117
+ path: ${streamA}
118
+ data: "A-epoch5"
119
+ producerId: shared-producer
120
+ producerEpoch: 5
121
+ producerSeq: 0
122
+ expect:
123
+ status: 200
124
+ # epoch=0 on stream B should still work (different stream)
125
+ - action: server-append
126
+ path: ${streamB}
127
+ data: "B-epoch0"
128
+ producerId: shared-producer
129
+ producerEpoch: 0
130
+ producerSeq: 0
131
+ expect:
132
+ status: 200
133
+ # epoch=0 on stream A should be fenced
134
+ - action: server-append
135
+ path: ${streamA}
136
+ data: "A-epoch0-zombie"
137
+ producerId: shared-producer
138
+ producerEpoch: 0
139
+ producerSeq: 1
140
+ expect:
141
+ status: 403
142
+ producerEpoch: 5
143
+
144
+ - id: interleaved-producers-ordering
145
+ name: Interleaved producers maintain per-producer ordering
146
+ description: |
147
+ When multiple producers write interleaved, each producer's messages
148
+ should appear in their sequence order relative to each other.
149
+ setup:
150
+ - action: create
151
+ as: streamPath
152
+ contentType: text/plain
153
+ operations:
154
+ # Interleave writes from two producers
155
+ - action: server-append
156
+ path: ${streamPath}
157
+ data: "A0"
158
+ producerId: producer-A
159
+ producerEpoch: 0
160
+ producerSeq: 0
161
+ expect:
162
+ status: 200
163
+ - action: server-append
164
+ path: ${streamPath}
165
+ data: "B0"
166
+ producerId: producer-B
167
+ producerEpoch: 0
168
+ producerSeq: 0
169
+ expect:
170
+ status: 200
171
+ - action: server-append
172
+ path: ${streamPath}
173
+ data: "A1"
174
+ producerId: producer-A
175
+ producerEpoch: 0
176
+ producerSeq: 1
177
+ expect:
178
+ status: 200
179
+ - action: server-append
180
+ path: ${streamPath}
181
+ data: "B1"
182
+ producerId: producer-B
183
+ producerEpoch: 0
184
+ producerSeq: 1
185
+ expect:
186
+ status: 200
187
+ - action: server-append
188
+ path: ${streamPath}
189
+ data: "A2"
190
+ producerId: producer-A
191
+ producerEpoch: 0
192
+ producerSeq: 2
193
+ expect:
194
+ status: 200
195
+ # Read and verify A0 comes before A1 comes before A2
196
+ # and B0 comes before B1
197
+ - action: read
198
+ path: ${streamPath}
199
+ expect:
200
+ dataContainsAll:
201
+ - "A0"
202
+ - "A1"
203
+ - "A2"
204
+ - "B0"
205
+ - "B1"
206
+ upToDate: true
207
+
208
+ - id: delete-recreate-resets-producer-state
209
+ name: Deleting and recreating stream resets producer state
210
+ description: |
211
+ When a stream is deleted and recreated, producer state must be reset.
212
+ The new stream is a fresh log, so (epoch=0, seq=0) must be accepted
213
+ even if the same producer previously wrote to the old stream.
214
+ setup:
215
+ - action: create
216
+ as: streamPath
217
+ contentType: text/plain
218
+ operations:
219
+ # Write to original stream
220
+ - action: server-append
221
+ path: ${streamPath}
222
+ data: "original-msg"
223
+ producerId: test-producer
224
+ producerEpoch: 0
225
+ producerSeq: 0
226
+ expect:
227
+ status: 200
228
+ # Advance sequence
229
+ - action: server-append
230
+ path: ${streamPath}
231
+ data: "original-msg2"
232
+ producerId: test-producer
233
+ producerEpoch: 0
234
+ producerSeq: 1
235
+ expect:
236
+ status: 200
237
+ # Delete the stream
238
+ - action: delete
239
+ path: ${streamPath}
240
+ expect:
241
+ status: 200
242
+ # Recreate at same path
243
+ - action: create
244
+ path: ${streamPath}
245
+ contentType: text/plain
246
+ # Same producer, epoch=0, seq=0 must work (new stream)
247
+ - action: server-append
248
+ path: ${streamPath}
249
+ data: "recreated-msg"
250
+ producerId: test-producer
251
+ producerEpoch: 0
252
+ producerSeq: 0
253
+ expect:
254
+ status: 200
255
+ # Verify only the new message exists
256
+ - action: read
257
+ path: ${streamPath}
258
+ expect:
259
+ data: "recreated-msg"
260
+ upToDate: true
261
+
262
+ - id: producer-id-long
263
+ name: Long producer ID works
264
+ description: Very long producer IDs should be accepted
265
+ setup:
266
+ - action: create
267
+ as: streamPath
268
+ contentType: text/plain
269
+ operations:
270
+ - action: server-append
271
+ path: ${streamPath}
272
+ data: "long-id"
273
+ producerId: "very-long-producer-id-that-goes-on-for-quite-a-while-to-test-string-handling-in-the-server-implementation-and-make-sure-it-doesnt-truncate"
274
+ producerEpoch: 0
275
+ producerSeq: 0
276
+ expect:
277
+ status: 200
278
+ # Duplicate detection works with long ID
279
+ - action: server-append
280
+ path: ${streamPath}
281
+ data: "long-id"
282
+ producerId: "very-long-producer-id-that-goes-on-for-quite-a-while-to-test-string-handling-in-the-server-implementation-and-make-sure-it-doesnt-truncate"
283
+ producerEpoch: 0
284
+ producerSeq: 0
285
+ expect:
286
+ status: 204
287
+
288
+ - id: producer-id-with-special-chars
289
+ name: Producer ID with special characters works
290
+ description: Producer IDs can contain colons, slashes, and other special chars
291
+ setup:
292
+ - action: create
293
+ as: streamPath
294
+ contentType: text/plain
295
+ operations:
296
+ # Colon-separated ID (common pattern)
297
+ - action: server-append
298
+ path: ${streamPath}
299
+ data: "msg1"
300
+ producerId: "service:instance:123"
301
+ producerEpoch: 0
302
+ producerSeq: 0
303
+ expect:
304
+ status: 200
305
+ # Slash-separated ID (path-like)
306
+ - action: server-append
307
+ path: ${streamPath}
308
+ data: "msg2"
309
+ producerId: "region/zone/host"
310
+ producerEpoch: 0
311
+ producerSeq: 0
312
+ expect:
313
+ status: 200
314
+ # UUID format
315
+ - action: server-append
316
+ path: ${streamPath}
317
+ data: "msg3"
318
+ producerId: "550e8400-e29b-41d4-a716-446655440000"
319
+ producerEpoch: 0
320
+ producerSeq: 0
321
+ expect:
322
+ status: 200
@@ -0,0 +1,339 @@
1
+ id: idempotent-sequence-validation
2
+ name: Idempotent Producer - Sequence Validation
3
+ description: |
4
+ Tests for producer sequence number validation.
5
+ Sequences must start at 0 and increment monotonically within an epoch.
6
+ category: producer
7
+ tags:
8
+ - idempotent
9
+ - sequence
10
+
11
+ tests:
12
+ - id: first-append-accepted
13
+ name: First append with producer headers is accepted
14
+ description: Server should accept first append with epoch=0, seq=0
15
+ setup:
16
+ - action: create
17
+ as: streamPath
18
+ contentType: text/plain
19
+ operations:
20
+ - action: server-append
21
+ path: ${streamPath}
22
+ data: "hello"
23
+ producerId: test-producer
24
+ producerEpoch: 0
25
+ producerSeq: 0
26
+ expect:
27
+ status: 200
28
+ producerEpoch: 0
29
+
30
+ - id: sequential-sequences
31
+ name: Sequential producer sequences accepted
32
+ description: Server should accept monotonically increasing sequences
33
+ setup:
34
+ - action: create
35
+ as: streamPath
36
+ contentType: text/plain
37
+ operations:
38
+ - action: server-append
39
+ path: ${streamPath}
40
+ data: "msg0"
41
+ producerId: test-producer
42
+ producerEpoch: 0
43
+ producerSeq: 0
44
+ expect:
45
+ status: 200
46
+ - action: server-append
47
+ path: ${streamPath}
48
+ data: "msg1"
49
+ producerId: test-producer
50
+ producerEpoch: 0
51
+ producerSeq: 1
52
+ expect:
53
+ status: 200
54
+ - action: server-append
55
+ path: ${streamPath}
56
+ data: "msg2"
57
+ producerId: test-producer
58
+ producerEpoch: 0
59
+ producerSeq: 2
60
+ expect:
61
+ status: 200
62
+
63
+ - id: sequence-gap-rejected
64
+ name: Sequence gap returns 409
65
+ description: Skipping a sequence number should return 409 with expected/received seq
66
+ setup:
67
+ - action: create
68
+ as: streamPath
69
+ contentType: text/plain
70
+ operations:
71
+ - action: server-append
72
+ path: ${streamPath}
73
+ data: "msg0"
74
+ producerId: test-producer
75
+ producerEpoch: 0
76
+ producerSeq: 0
77
+ expect:
78
+ status: 200
79
+ # Skip seq=1, try seq=2
80
+ - action: server-append
81
+ path: ${streamPath}
82
+ data: "msg2"
83
+ producerId: test-producer
84
+ producerEpoch: 0
85
+ producerSeq: 2
86
+ expect:
87
+ status: 409
88
+ producerExpectedSeq: 1
89
+ producerReceivedSeq: 2
90
+
91
+ - id: new-producer-seq-not-zero
92
+ name: New producer starting with seq > 0 returns 409
93
+ description: A never-seen producer sending seq > 0 should get 409 (gap), not 400
94
+ setup:
95
+ - action: create
96
+ as: streamPath
97
+ contentType: text/plain
98
+ operations:
99
+ - action: server-append
100
+ path: ${streamPath}
101
+ data: "msg"
102
+ producerId: brand-new-producer
103
+ producerEpoch: 0
104
+ producerSeq: 5
105
+ expect:
106
+ status: 409
107
+ producerExpectedSeq: 0
108
+ producerReceivedSeq: 5
109
+
110
+ - id: duplicate-returns-204
111
+ name: Duplicate sequence returns 204 (idempotent success)
112
+ description: Retrying the same seq should return 204 without duplicating data
113
+ setup:
114
+ - action: create
115
+ as: streamPath
116
+ contentType: text/plain
117
+ operations:
118
+ - action: server-append
119
+ path: ${streamPath}
120
+ data: "hello"
121
+ producerId: test-producer
122
+ producerEpoch: 0
123
+ producerSeq: 0
124
+ expect:
125
+ status: 200
126
+ - action: server-append
127
+ path: ${streamPath}
128
+ data: "hello"
129
+ producerId: test-producer
130
+ producerEpoch: 0
131
+ producerSeq: 0
132
+ expect:
133
+ status: 204
134
+ duplicate: true
135
+ # Verify only one message in stream
136
+ - action: read
137
+ path: ${streamPath}
138
+ expect:
139
+ data: "hello"
140
+ upToDate: true
141
+
142
+ - id: duplicate-seq-zero-preserves-state
143
+ name: Duplicate of seq=0 does not corrupt state
144
+ description: Retrying seq=0 should not affect subsequent sequence tracking
145
+ setup:
146
+ - action: create
147
+ as: streamPath
148
+ contentType: text/plain
149
+ operations:
150
+ - action: server-append
151
+ path: ${streamPath}
152
+ data: "first"
153
+ producerId: test-producer
154
+ producerEpoch: 0
155
+ producerSeq: 0
156
+ expect:
157
+ status: 200
158
+ # Retry seq=0
159
+ - action: server-append
160
+ path: ${streamPath}
161
+ data: "first"
162
+ producerId: test-producer
163
+ producerEpoch: 0
164
+ producerSeq: 0
165
+ expect:
166
+ status: 204
167
+ # seq=1 should still work
168
+ - action: server-append
169
+ path: ${streamPath}
170
+ data: "second"
171
+ producerId: test-producer
172
+ producerEpoch: 0
173
+ producerSeq: 1
174
+ expect:
175
+ status: 200
176
+
177
+ - id: duplicate-ignores-different-data
178
+ name: Duplicate seq ignores payload differences
179
+ description: Same (producer, epoch, seq) returns 204 even with different data - dedup is by headers only
180
+ setup:
181
+ - action: create
182
+ as: streamPath
183
+ contentType: text/plain
184
+ operations:
185
+ - action: server-append
186
+ path: ${streamPath}
187
+ data: "original payload"
188
+ producerId: test-producer
189
+ producerEpoch: 0
190
+ producerSeq: 0
191
+ expect:
192
+ status: 200
193
+ # Retry with different data - should still be 204 (idempotent)
194
+ - action: server-append
195
+ path: ${streamPath}
196
+ data: "different payload"
197
+ producerId: test-producer
198
+ producerEpoch: 0
199
+ producerSeq: 0
200
+ expect:
201
+ status: 204
202
+ duplicate: true
203
+ # Verify only original data is in stream
204
+ - action: read
205
+ path: ${streamPath}
206
+ expect:
207
+ data: "original payload"
208
+ upToDate: true
209
+
210
+ - id: ordering-preserved-in-stream
211
+ name: Message ordering preserved in stream
212
+ description: Messages from idempotent producer maintain sequence order
213
+ setup:
214
+ - action: create
215
+ as: streamPath
216
+ contentType: application/json
217
+ operations:
218
+ - action: server-append
219
+ path: ${streamPath}
220
+ data: '{"seq": 0}'
221
+ producerId: order-test
222
+ producerEpoch: 0
223
+ producerSeq: 0
224
+ expect:
225
+ status: 200
226
+ - action: server-append
227
+ path: ${streamPath}
228
+ data: '{"seq": 1}'
229
+ producerId: order-test
230
+ producerEpoch: 0
231
+ producerSeq: 1
232
+ expect:
233
+ status: 200
234
+ - action: server-append
235
+ path: ${streamPath}
236
+ data: '{"seq": 2}'
237
+ producerId: order-test
238
+ producerEpoch: 0
239
+ producerSeq: 2
240
+ expect:
241
+ status: 200
242
+ - action: read
243
+ path: ${streamPath}
244
+ expect:
245
+ dataContainsAll:
246
+ - '"seq":0'
247
+ - '"seq":1'
248
+ - '"seq":2'
249
+ upToDate: true
250
+
251
+ - id: duplicate-old-sequence
252
+ name: Duplicate of old (non-tail) sequence returns 204
253
+ description: |
254
+ Retrying a sequence far back in history should still return 204.
255
+ This tests that the server tracks more than just the last sequence.
256
+ (Kafka distinguishes DuplicateSequenceException vs OutOfOrderSequenceException)
257
+ setup:
258
+ - action: create
259
+ as: streamPath
260
+ contentType: text/plain
261
+ operations:
262
+ # Send sequences 0-5
263
+ - action: server-append
264
+ path: ${streamPath}
265
+ data: "msg0"
266
+ producerId: test-producer
267
+ producerEpoch: 0
268
+ producerSeq: 0
269
+ expect:
270
+ status: 200
271
+ - action: server-append
272
+ path: ${streamPath}
273
+ data: "msg1"
274
+ producerId: test-producer
275
+ producerEpoch: 0
276
+ producerSeq: 1
277
+ expect:
278
+ status: 200
279
+ - action: server-append
280
+ path: ${streamPath}
281
+ data: "msg2"
282
+ producerId: test-producer
283
+ producerEpoch: 0
284
+ producerSeq: 2
285
+ expect:
286
+ status: 200
287
+ - action: server-append
288
+ path: ${streamPath}
289
+ data: "msg3"
290
+ producerId: test-producer
291
+ producerEpoch: 0
292
+ producerSeq: 3
293
+ expect:
294
+ status: 200
295
+ - action: server-append
296
+ path: ${streamPath}
297
+ data: "msg4"
298
+ producerId: test-producer
299
+ producerEpoch: 0
300
+ producerSeq: 4
301
+ expect:
302
+ status: 200
303
+ - action: server-append
304
+ path: ${streamPath}
305
+ data: "msg5"
306
+ producerId: test-producer
307
+ producerEpoch: 0
308
+ producerSeq: 5
309
+ expect:
310
+ status: 200
311
+ # Retry seq=1 (old, non-tail sequence)
312
+ - action: server-append
313
+ path: ${streamPath}
314
+ data: "msg1"
315
+ producerId: test-producer
316
+ producerEpoch: 0
317
+ producerSeq: 1
318
+ expect:
319
+ status: 204
320
+ duplicate: true
321
+ # Retry seq=0 (first sequence)
322
+ - action: server-append
323
+ path: ${streamPath}
324
+ data: "msg0"
325
+ producerId: test-producer
326
+ producerEpoch: 0
327
+ producerSeq: 0
328
+ expect:
329
+ status: 204
330
+ duplicate: true
331
+ # New sequence should still work
332
+ - action: server-append
333
+ path: ${streamPath}
334
+ data: "msg6"
335
+ producerId: test-producer
336
+ producerEpoch: 0
337
+ producerSeq: 6
338
+ expect:
339
+ status: 200