@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,333 @@
1
+ id: idempotent-epoch-management
2
+ name: Idempotent Producer - Epoch Management
3
+ description: |
4
+ Tests for producer epoch validation and zombie fencing.
5
+ Epochs enable split-brain protection - only the latest instance can write.
6
+ category: producer
7
+ tags:
8
+ - idempotent
9
+ - epoch
10
+ - fencing
11
+
12
+ tests:
13
+ - id: epoch-upgrade-accepted
14
+ name: Epoch upgrade resets sequence to 0
15
+ description: New epoch must start at seq=0 and should be accepted
16
+ setup:
17
+ - action: create
18
+ as: streamPath
19
+ contentType: text/plain
20
+ operations:
21
+ - action: server-append
22
+ path: ${streamPath}
23
+ data: "epoch0-msg0"
24
+ producerId: test-producer
25
+ producerEpoch: 0
26
+ producerSeq: 0
27
+ expect:
28
+ status: 200
29
+ - action: server-append
30
+ path: ${streamPath}
31
+ data: "epoch0-msg1"
32
+ producerId: test-producer
33
+ producerEpoch: 0
34
+ producerSeq: 1
35
+ expect:
36
+ status: 200
37
+ # Upgrade to epoch=1
38
+ - action: server-append
39
+ path: ${streamPath}
40
+ data: "epoch1-msg0"
41
+ producerId: test-producer
42
+ producerEpoch: 1
43
+ producerSeq: 0
44
+ expect:
45
+ status: 200
46
+ producerEpoch: 1
47
+
48
+ - id: stale-epoch-rejected
49
+ name: Stale epoch returns 403 (zombie fencing)
50
+ description: Old epoch should be rejected with 403 and current epoch in response
51
+ setup:
52
+ - action: create
53
+ as: streamPath
54
+ contentType: text/plain
55
+ operations:
56
+ # Establish epoch=1
57
+ - action: server-append
58
+ path: ${streamPath}
59
+ data: "msg"
60
+ producerId: test-producer
61
+ producerEpoch: 1
62
+ producerSeq: 0
63
+ expect:
64
+ status: 200
65
+ # Try to write with stale epoch=0
66
+ - action: server-append
67
+ path: ${streamPath}
68
+ data: "zombie"
69
+ producerId: test-producer
70
+ producerEpoch: 0
71
+ producerSeq: 0
72
+ expect:
73
+ status: 403
74
+ producerEpoch: 1
75
+
76
+ - id: epoch-rollback-rejected
77
+ name: Epoch rollback is rejected
78
+ description: Cannot go back to a lower epoch
79
+ setup:
80
+ - action: create
81
+ as: streamPath
82
+ contentType: text/plain
83
+ operations:
84
+ # Establish epoch=2
85
+ - action: server-append
86
+ path: ${streamPath}
87
+ data: "msg"
88
+ producerId: test-producer
89
+ producerEpoch: 2
90
+ producerSeq: 0
91
+ expect:
92
+ status: 200
93
+ # Try epoch=1 (rollback)
94
+ - action: server-append
95
+ path: ${streamPath}
96
+ data: "rollback"
97
+ producerId: test-producer
98
+ producerEpoch: 1
99
+ producerSeq: 0
100
+ expect:
101
+ status: 403
102
+
103
+ - id: epoch-increase-requires-seq-zero
104
+ name: Epoch increase with seq != 0 is rejected
105
+ description: New epoch must start at seq=0
106
+ setup:
107
+ - action: create
108
+ as: streamPath
109
+ contentType: text/plain
110
+ operations:
111
+ - action: server-append
112
+ path: ${streamPath}
113
+ data: "msg"
114
+ producerId: test-producer
115
+ producerEpoch: 0
116
+ producerSeq: 0
117
+ expect:
118
+ status: 200
119
+ # Try epoch=1 with seq=5
120
+ - action: server-append
121
+ path: ${streamPath}
122
+ data: "bad"
123
+ producerId: test-producer
124
+ producerEpoch: 1
125
+ producerSeq: 5
126
+ expect:
127
+ status: 400
128
+
129
+ - id: epoch-gap-allowed
130
+ name: Epoch can skip values (gap allowed)
131
+ description: |
132
+ Epochs don't need to be sequential. Jumping from epoch=0 to epoch=10
133
+ should be allowed (unlike sequences which must be sequential).
134
+ setup:
135
+ - action: create
136
+ as: streamPath
137
+ contentType: text/plain
138
+ operations:
139
+ - action: server-append
140
+ path: ${streamPath}
141
+ data: "epoch0"
142
+ producerId: test-producer
143
+ producerEpoch: 0
144
+ producerSeq: 0
145
+ expect:
146
+ status: 200
147
+ # Jump to epoch=10 (skipping 1-9)
148
+ - action: server-append
149
+ path: ${streamPath}
150
+ data: "epoch10"
151
+ producerId: test-producer
152
+ producerEpoch: 10
153
+ producerSeq: 0
154
+ expect:
155
+ status: 200
156
+ # Previous epochs now fenced
157
+ - action: server-append
158
+ path: ${streamPath}
159
+ data: "epoch5"
160
+ producerId: test-producer
161
+ producerEpoch: 5
162
+ producerSeq: 0
163
+ expect:
164
+ status: 403
165
+ producerEpoch: 10
166
+
167
+ - id: epoch-zero-after-higher-epoch
168
+ name: Epoch 0 rejected after higher epoch established
169
+ description: |
170
+ Once a higher epoch is established, epoch=0 should be rejected.
171
+ This is critical for zombie fencing.
172
+ setup:
173
+ - action: create
174
+ as: streamPath
175
+ contentType: text/plain
176
+ operations:
177
+ # Establish epoch=5
178
+ - action: server-append
179
+ path: ${streamPath}
180
+ data: "epoch5"
181
+ producerId: test-producer
182
+ producerEpoch: 5
183
+ producerSeq: 0
184
+ expect:
185
+ status: 200
186
+ # Epoch 0 should be fenced
187
+ - action: server-append
188
+ path: ${streamPath}
189
+ data: "epoch0-zombie"
190
+ producerId: test-producer
191
+ producerEpoch: 0
192
+ producerSeq: 0
193
+ expect:
194
+ status: 403
195
+ producerEpoch: 5
196
+ # Epoch 4 should also be fenced
197
+ - action: server-append
198
+ path: ${streamPath}
199
+ data: "epoch4-zombie"
200
+ producerId: test-producer
201
+ producerEpoch: 4
202
+ producerSeq: 0
203
+ expect:
204
+ status: 403
205
+ producerEpoch: 5
206
+
207
+ - id: split-brain-fencing
208
+ name: Split-brain fencing scenario
209
+ description: Old producer instance (zombie) should be fenced when new instance claims higher epoch
210
+ setup:
211
+ - action: create
212
+ as: streamPath
213
+ contentType: text/plain
214
+ operations:
215
+ # Producer A (original): epoch=0
216
+ - action: server-append
217
+ path: ${streamPath}
218
+ data: "A0"
219
+ producerId: shared-producer
220
+ producerEpoch: 0
221
+ producerSeq: 0
222
+ expect:
223
+ status: 200
224
+ # Producer B (new instance): claims epoch=1
225
+ - action: server-append
226
+ path: ${streamPath}
227
+ data: "B0"
228
+ producerId: shared-producer
229
+ producerEpoch: 1
230
+ producerSeq: 0
231
+ expect:
232
+ status: 200
233
+ # Producer A (zombie): tries epoch=0, seq=1 - should be fenced
234
+ - action: server-append
235
+ path: ${streamPath}
236
+ data: "A1"
237
+ producerId: shared-producer
238
+ producerEpoch: 0
239
+ producerSeq: 1
240
+ expect:
241
+ status: 403
242
+ producerEpoch: 1
243
+
244
+ - id: large-epoch-numbers
245
+ name: Large epoch numbers handled correctly
246
+ description: Test that large epoch values work correctly
247
+ setup:
248
+ - action: create
249
+ as: streamPath
250
+ contentType: text/plain
251
+ operations:
252
+ - action: server-append
253
+ path: ${streamPath}
254
+ data: "large-epoch"
255
+ producerId: test-producer
256
+ producerEpoch: 2147483640
257
+ producerSeq: 0
258
+ expect:
259
+ status: 200
260
+ # Stale epoch should still be rejected
261
+ - action: server-append
262
+ path: ${streamPath}
263
+ data: "stale"
264
+ producerId: test-producer
265
+ producerEpoch: 100
266
+ producerSeq: 0
267
+ expect:
268
+ status: 403
269
+ producerEpoch: 2147483640
270
+
271
+ - id: sequence-resets-on-epoch-upgrade
272
+ name: Each epoch has independent sequence space
273
+ description: |
274
+ Sequence numbers reset to 0 with each epoch upgrade.
275
+ The old epoch's sequences don't affect the new epoch.
276
+ setup:
277
+ - action: create
278
+ as: streamPath
279
+ contentType: text/plain
280
+ operations:
281
+ # Epoch 0: sequences 0, 1, 2
282
+ - action: server-append
283
+ path: ${streamPath}
284
+ data: "e0s0"
285
+ producerId: test-producer
286
+ producerEpoch: 0
287
+ producerSeq: 0
288
+ expect:
289
+ status: 200
290
+ - action: server-append
291
+ path: ${streamPath}
292
+ data: "e0s1"
293
+ producerId: test-producer
294
+ producerEpoch: 0
295
+ producerSeq: 1
296
+ expect:
297
+ status: 200
298
+ - action: server-append
299
+ path: ${streamPath}
300
+ data: "e0s2"
301
+ producerId: test-producer
302
+ producerEpoch: 0
303
+ producerSeq: 2
304
+ expect:
305
+ status: 200
306
+ # Epoch 1: starts fresh at seq=0
307
+ - action: server-append
308
+ path: ${streamPath}
309
+ data: "e1s0"
310
+ producerId: test-producer
311
+ producerEpoch: 1
312
+ producerSeq: 0
313
+ expect:
314
+ status: 200
315
+ - action: server-append
316
+ path: ${streamPath}
317
+ data: "e1s1"
318
+ producerId: test-producer
319
+ producerEpoch: 1
320
+ producerSeq: 1
321
+ expect:
322
+ status: 200
323
+ # Verify all 5 messages present
324
+ - action: read
325
+ path: ${streamPath}
326
+ expect:
327
+ dataContainsAll:
328
+ - "e0s0"
329
+ - "e0s1"
330
+ - "e0s2"
331
+ - "e1s0"
332
+ - "e1s1"
333
+ upToDate: true
@@ -0,0 +1,194 @@
1
+ id: idempotent-error-handling
2
+ name: Idempotent Producer - Error Handling
3
+ description: |
4
+ Tests for producer header validation and error conditions.
5
+ Includes atomicity guarantees when appends fail.
6
+ category: producer
7
+ tags:
8
+ - idempotent
9
+ - validation
10
+ - errors
11
+
12
+ tests:
13
+ - id: partial-headers-rejected
14
+ name: Partial producer headers are rejected
15
+ description: All three producer headers must be provided together
16
+ setup:
17
+ - action: create
18
+ as: streamPath
19
+ contentType: text/plain
20
+ operations:
21
+ # Only Producer-Id
22
+ - action: server-append
23
+ path: ${streamPath}
24
+ data: "msg"
25
+ headers:
26
+ Producer-Id: test-producer
27
+ expect:
28
+ status: 400
29
+ # Missing Producer-Seq
30
+ - action: server-append
31
+ path: ${streamPath}
32
+ data: "msg"
33
+ headers:
34
+ Producer-Id: test-producer
35
+ Producer-Epoch: "0"
36
+ expect:
37
+ status: 400
38
+
39
+ - id: invalid-integer-formats-rejected
40
+ name: Invalid integer formats in producer headers are rejected
41
+ description: Server should reject non-integer values like "1abc" or "1e3"
42
+ setup:
43
+ - action: create
44
+ as: streamPath
45
+ contentType: text/plain
46
+ operations:
47
+ # Trailing junk in Producer-Seq
48
+ - action: server-append
49
+ path: ${streamPath}
50
+ data: "msg"
51
+ headers:
52
+ Producer-Id: test-producer
53
+ Producer-Epoch: "0"
54
+ Producer-Seq: "1abc"
55
+ expect:
56
+ status: 400
57
+ # Scientific notation
58
+ - action: server-append
59
+ path: ${streamPath}
60
+ data: "msg"
61
+ headers:
62
+ Producer-Id: test-producer
63
+ Producer-Epoch: "1e3"
64
+ Producer-Seq: "0"
65
+ expect:
66
+ status: 400
67
+ # Negative value
68
+ - action: server-append
69
+ path: ${streamPath}
70
+ data: "msg"
71
+ headers:
72
+ Producer-Id: test-producer
73
+ Producer-Epoch: "-1"
74
+ Producer-Seq: "0"
75
+ expect:
76
+ status: 400
77
+
78
+ - id: empty-producer-id-rejected
79
+ name: Empty producer ID is rejected
80
+ description: Producer-Id header with empty string should be rejected
81
+ setup:
82
+ - action: create
83
+ as: streamPath
84
+ contentType: text/plain
85
+ operations:
86
+ - action: server-append
87
+ path: ${streamPath}
88
+ data: "msg"
89
+ headers:
90
+ Producer-Id: ""
91
+ Producer-Epoch: "0"
92
+ Producer-Seq: "0"
93
+ expect:
94
+ status: 400
95
+
96
+ - id: failed-append-does-not-advance-producer-state
97
+ name: Failed append does not advance producer state (atomicity)
98
+ description: If append fails after producer validation (e.g., invalid JSON), producer seq should not advance
99
+ setup:
100
+ - action: create
101
+ as: streamPath
102
+ contentType: application/json
103
+ operations:
104
+ # First append succeeds
105
+ - action: server-append
106
+ path: ${streamPath}
107
+ data: '{"msg": 1}'
108
+ producerId: test-producer
109
+ producerEpoch: 0
110
+ producerSeq: 0
111
+ expect:
112
+ status: 200
113
+ # Second append with invalid JSON - should fail
114
+ - action: server-append
115
+ path: ${streamPath}
116
+ data: "not valid json"
117
+ producerId: test-producer
118
+ producerEpoch: 0
119
+ producerSeq: 1
120
+ expect:
121
+ status: 400
122
+ # Retry seq=1 with valid JSON - should succeed (not be rejected as duplicate or gap)
123
+ # If producer state was incorrectly advanced, this would fail
124
+ - action: server-append
125
+ path: ${streamPath}
126
+ data: '{"msg": 2}'
127
+ producerId: test-producer
128
+ producerEpoch: 0
129
+ producerSeq: 1
130
+ expect:
131
+ status: 200
132
+ # seq=2 should also work
133
+ - action: server-append
134
+ path: ${streamPath}
135
+ data: '{"msg": 3}'
136
+ producerId: test-producer
137
+ producerEpoch: 0
138
+ producerSeq: 2
139
+ expect:
140
+ status: 200
141
+
142
+ - id: gap-fill-resume
143
+ name: Gap can be filled and producer can resume
144
+ description: |
145
+ After a 409 gap error, the producer should be able to fill the gap
146
+ and then continue. Some implementations accidentally "lock out" a
147
+ sequence once a gap was observed, or mutate state on the 409 path.
148
+ setup:
149
+ - action: create
150
+ as: streamPath
151
+ contentType: text/plain
152
+ operations:
153
+ # seq=0 succeeds
154
+ - action: server-append
155
+ path: ${streamPath}
156
+ data: "msg0"
157
+ producerId: test-producer
158
+ producerEpoch: 0
159
+ producerSeq: 0
160
+ expect:
161
+ status: 200
162
+ # seq=2 creates a gap -> 409
163
+ - action: server-append
164
+ path: ${streamPath}
165
+ data: "msg2"
166
+ producerId: test-producer
167
+ producerEpoch: 0
168
+ producerSeq: 2
169
+ expect:
170
+ status: 409
171
+ # Fill the gap with seq=1
172
+ - action: server-append
173
+ path: ${streamPath}
174
+ data: "msg1"
175
+ producerId: test-producer
176
+ producerEpoch: 0
177
+ producerSeq: 1
178
+ expect:
179
+ status: 200
180
+ # Now seq=2 should succeed
181
+ - action: server-append
182
+ path: ${streamPath}
183
+ data: "msg2"
184
+ producerId: test-producer
185
+ producerEpoch: 0
186
+ producerSeq: 2
187
+ expect:
188
+ status: 200
189
+ # Verify correct order in stream (text/plain concatenates all data)
190
+ - action: read
191
+ path: ${streamPath}
192
+ expect:
193
+ data: "msg0msg1msg2"
194
+ upToDate: true