@durable-streams/client-conformance-tests 0.1.5 → 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.
- package/dist/adapters/typescript-adapter.cjs +75 -2
- package/dist/adapters/typescript-adapter.js +76 -3
- package/dist/{benchmark-runner-C_Yghc8f.js → benchmark-runner-CrE6JkbX.js} +106 -12
- package/dist/{benchmark-runner-CLAR9oLd.cjs → benchmark-runner-Db4he452.cjs} +107 -12
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +126 -11
- package/dist/index.d.ts +126 -11
- package/dist/index.js +1 -1
- package/dist/{protocol-3cf94Xyb.d.cts → protocol-D37G3c4e.d.cts} +80 -4
- package/dist/{protocol-DyEvTHPF.d.ts → protocol-Mcbiq3nQ.d.ts} +80 -4
- package/dist/protocol.d.cts +2 -2
- package/dist/protocol.d.ts +2 -2
- package/package.json +3 -3
- package/src/adapters/typescript-adapter.ts +127 -5
- package/src/protocol.ts +85 -1
- package/src/runner.ts +202 -17
- package/src/test-cases.ts +130 -8
- package/test-cases/consumer/error-handling.yaml +42 -0
- package/test-cases/consumer/fault-injection.yaml +202 -0
- package/test-cases/consumer/offset-handling.yaml +209 -0
- package/test-cases/producer/idempotent/autoclaim.yaml +214 -0
- package/test-cases/producer/idempotent/batching.yaml +98 -0
- package/test-cases/producer/idempotent/concurrent-requests.yaml +100 -0
- package/test-cases/producer/idempotent/epoch-management.yaml +333 -0
- package/test-cases/producer/idempotent/error-handling.yaml +194 -0
- package/test-cases/producer/idempotent/multi-producer.yaml +322 -0
- package/test-cases/producer/idempotent/sequence-validation.yaml +339 -0
- package/test-cases/producer/idempotent-json-batching.yaml +134 -0
package/src/runner.ts
CHANGED
|
@@ -366,6 +366,71 @@ async function executeOperation(
|
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
case `idempotent-append`: {
|
|
370
|
+
const path = resolveVariables(op.path, variables)
|
|
371
|
+
const data = resolveVariables(op.data, variables)
|
|
372
|
+
|
|
373
|
+
const result = await client.send(
|
|
374
|
+
{
|
|
375
|
+
type: `idempotent-append`,
|
|
376
|
+
path,
|
|
377
|
+
data,
|
|
378
|
+
producerId: op.producerId,
|
|
379
|
+
epoch: op.epoch ?? 0,
|
|
380
|
+
autoClaim: op.autoClaim ?? false,
|
|
381
|
+
headers: op.headers,
|
|
382
|
+
},
|
|
383
|
+
commandTimeout
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
if (verbose) {
|
|
387
|
+
console.log(
|
|
388
|
+
` idempotent-append ${path}: ${result.success ? `ok` : `failed`}`
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (
|
|
393
|
+
result.success &&
|
|
394
|
+
result.type === `idempotent-append` &&
|
|
395
|
+
op.expect?.storeOffsetAs
|
|
396
|
+
) {
|
|
397
|
+
variables.set(op.expect.storeOffsetAs, result.offset ?? ``)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return { result }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
case `idempotent-append-batch`: {
|
|
404
|
+
const path = resolveVariables(op.path, variables)
|
|
405
|
+
|
|
406
|
+
// Send items to client which will batch them internally
|
|
407
|
+
const items = op.items.map((item) =>
|
|
408
|
+
resolveVariables(item.data, variables)
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
const result = await client.send(
|
|
412
|
+
{
|
|
413
|
+
type: `idempotent-append-batch`,
|
|
414
|
+
path,
|
|
415
|
+
items,
|
|
416
|
+
producerId: op.producerId,
|
|
417
|
+
epoch: op.epoch ?? 0,
|
|
418
|
+
autoClaim: op.autoClaim ?? false,
|
|
419
|
+
maxInFlight: op.maxInFlight,
|
|
420
|
+
headers: op.headers,
|
|
421
|
+
},
|
|
422
|
+
commandTimeout
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
if (verbose) {
|
|
426
|
+
console.log(
|
|
427
|
+
` idempotent-append-batch ${path}: ${result.success ? `ok` : `failed`}`
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return { result }
|
|
432
|
+
}
|
|
433
|
+
|
|
369
434
|
case `read`: {
|
|
370
435
|
const path = resolveVariables(op.path, variables)
|
|
371
436
|
const offset = op.offset
|
|
@@ -538,7 +603,8 @@ async function executeOperation(
|
|
|
538
603
|
|
|
539
604
|
case `server-append`: {
|
|
540
605
|
// Direct HTTP append to server, bypassing client adapter
|
|
541
|
-
// Used for concurrent operations when adapter is blocked on a read
|
|
606
|
+
// Used for concurrent operations when adapter is blocked on a read,
|
|
607
|
+
// and for testing protocol-level behavior like idempotent producers
|
|
542
608
|
const path = resolveVariables(op.path, variables)
|
|
543
609
|
const data = resolveVariables(op.data, variables)
|
|
544
610
|
|
|
@@ -550,28 +616,72 @@ async function executeOperation(
|
|
|
550
616
|
const contentType =
|
|
551
617
|
headResponse.headers.get(`content-type`) ?? `application/octet-stream`
|
|
552
618
|
|
|
619
|
+
// Build headers, including producer headers if present
|
|
620
|
+
const headers: Record<string, string> = {
|
|
621
|
+
"content-type": contentType,
|
|
622
|
+
...op.headers,
|
|
623
|
+
}
|
|
624
|
+
if (op.producerId !== undefined) {
|
|
625
|
+
headers[`Producer-Id`] = op.producerId
|
|
626
|
+
}
|
|
627
|
+
if (op.producerEpoch !== undefined) {
|
|
628
|
+
headers[`Producer-Epoch`] = op.producerEpoch.toString()
|
|
629
|
+
}
|
|
630
|
+
if (op.producerSeq !== undefined) {
|
|
631
|
+
headers[`Producer-Seq`] = op.producerSeq.toString()
|
|
632
|
+
}
|
|
633
|
+
|
|
553
634
|
const response = await fetch(`${ctx.serverUrl}${path}`, {
|
|
554
635
|
method: `POST`,
|
|
555
636
|
body: data,
|
|
556
|
-
headers
|
|
557
|
-
"content-type": contentType,
|
|
558
|
-
...op.headers,
|
|
559
|
-
},
|
|
637
|
+
headers,
|
|
560
638
|
})
|
|
561
639
|
|
|
640
|
+
const status = response.status
|
|
641
|
+
const offset = response.headers.get(`Stream-Next-Offset`) ?? undefined
|
|
642
|
+
const duplicate = status === 204
|
|
643
|
+
const producerEpoch = response.headers.get(`Producer-Epoch`)
|
|
644
|
+
const producerSeq = response.headers.get(`Producer-Seq`)
|
|
645
|
+
const producerExpectedSeq = response.headers.get(
|
|
646
|
+
`Producer-Expected-Seq`
|
|
647
|
+
)
|
|
648
|
+
const producerReceivedSeq = response.headers.get(
|
|
649
|
+
`Producer-Received-Seq`
|
|
650
|
+
)
|
|
651
|
+
|
|
562
652
|
if (verbose) {
|
|
563
653
|
console.log(
|
|
564
|
-
` server-append ${path}: ${
|
|
654
|
+
` server-append ${path}: status=${status}${duplicate ? ` (duplicate)` : ``}`
|
|
565
655
|
)
|
|
566
656
|
}
|
|
567
657
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
658
|
+
// Build result for expectation verification
|
|
659
|
+
// success: true means we got a valid protocol response (even 403/409)
|
|
660
|
+
// The status field indicates the actual operation result
|
|
661
|
+
const result: TestResult = {
|
|
662
|
+
type: `append`,
|
|
663
|
+
success: true,
|
|
664
|
+
status,
|
|
665
|
+
offset,
|
|
666
|
+
duplicate,
|
|
667
|
+
producerEpoch: producerEpoch
|
|
668
|
+
? parseInt(producerEpoch, 10)
|
|
669
|
+
: undefined,
|
|
670
|
+
producerSeq: producerSeq ? parseInt(producerSeq, 10) : undefined,
|
|
671
|
+
producerExpectedSeq: producerExpectedSeq
|
|
672
|
+
? parseInt(producerExpectedSeq, 10)
|
|
673
|
+
: undefined,
|
|
674
|
+
producerReceivedSeq: producerReceivedSeq
|
|
675
|
+
? parseInt(producerReceivedSeq, 10)
|
|
676
|
+
: undefined,
|
|
572
677
|
}
|
|
573
678
|
|
|
574
|
-
|
|
679
|
+
// Store offset if requested
|
|
680
|
+
if (op.expect?.storeOffsetAs && offset) {
|
|
681
|
+
variables.set(op.expect.storeOffsetAs, offset)
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return { result }
|
|
575
685
|
} catch (err) {
|
|
576
686
|
return {
|
|
577
687
|
error: `Server append failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -599,7 +709,7 @@ async function executeOperation(
|
|
|
599
709
|
}
|
|
600
710
|
|
|
601
711
|
case `inject-error`: {
|
|
602
|
-
// Inject
|
|
712
|
+
// Inject a fault via the test server's control endpoint
|
|
603
713
|
const path = resolveVariables(op.path, variables)
|
|
604
714
|
|
|
605
715
|
try {
|
|
@@ -611,23 +721,43 @@ async function executeOperation(
|
|
|
611
721
|
status: op.status,
|
|
612
722
|
count: op.count ?? 1,
|
|
613
723
|
retryAfter: op.retryAfter,
|
|
724
|
+
// New fault injection parameters
|
|
725
|
+
delayMs: op.delayMs,
|
|
726
|
+
dropConnection: op.dropConnection,
|
|
727
|
+
truncateBodyBytes: op.truncateBodyBytes,
|
|
728
|
+
probability: op.probability,
|
|
729
|
+
method: op.method,
|
|
730
|
+
corruptBody: op.corruptBody,
|
|
731
|
+
jitterMs: op.jitterMs,
|
|
614
732
|
}),
|
|
615
733
|
})
|
|
616
734
|
|
|
735
|
+
// Build descriptive log message
|
|
736
|
+
const faultTypes = []
|
|
737
|
+
if (op.status != null) faultTypes.push(`status=${op.status}`)
|
|
738
|
+
if (op.delayMs != null) faultTypes.push(`delay=${op.delayMs}ms`)
|
|
739
|
+
if (op.jitterMs != null) faultTypes.push(`jitter=${op.jitterMs}ms`)
|
|
740
|
+
if (op.dropConnection) faultTypes.push(`dropConnection`)
|
|
741
|
+
if (op.truncateBodyBytes != null)
|
|
742
|
+
faultTypes.push(`truncate=${op.truncateBodyBytes}b`)
|
|
743
|
+
if (op.corruptBody) faultTypes.push(`corrupt`)
|
|
744
|
+
if (op.probability != null) faultTypes.push(`p=${op.probability}`)
|
|
745
|
+
const faultDesc = faultTypes.join(`,`) || `unknown`
|
|
746
|
+
|
|
617
747
|
if (verbose) {
|
|
618
748
|
console.log(
|
|
619
|
-
` inject-error ${path} ${
|
|
749
|
+
` inject-error ${path} [${faultDesc}]x${op.count ?? 1}: ${response.ok ? `ok` : `failed`}`
|
|
620
750
|
)
|
|
621
751
|
}
|
|
622
752
|
|
|
623
753
|
if (!response.ok) {
|
|
624
|
-
return { error: `Failed to inject
|
|
754
|
+
return { error: `Failed to inject fault: ${response.status}` }
|
|
625
755
|
}
|
|
626
756
|
|
|
627
757
|
return {}
|
|
628
758
|
} catch (err) {
|
|
629
759
|
return {
|
|
630
|
-
error: `Failed to inject
|
|
760
|
+
error: `Failed to inject fault: ${err instanceof Error ? err.message : String(err)}`,
|
|
631
761
|
}
|
|
632
762
|
}
|
|
633
763
|
}
|
|
@@ -776,6 +906,22 @@ function validateExpectation(
|
|
|
776
906
|
}
|
|
777
907
|
}
|
|
778
908
|
|
|
909
|
+
// Check dataExact - verifies exact messages in order
|
|
910
|
+
if (expect.dataExact !== undefined && isReadResult(result)) {
|
|
911
|
+
const expectedMessages = expect.dataExact as Array<string>
|
|
912
|
+
const actualMessages = result.chunks.map((c) => c.data)
|
|
913
|
+
|
|
914
|
+
if (actualMessages.length !== expectedMessages.length) {
|
|
915
|
+
return `Expected ${expectedMessages.length} messages, got ${actualMessages.length}. Expected: [${expectedMessages.join(`, `)}], got: [${actualMessages.join(`, `)}]`
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
for (let i = 0; i < expectedMessages.length; i++) {
|
|
919
|
+
if (actualMessages[i] !== expectedMessages[i]) {
|
|
920
|
+
return `Message ${i} mismatch: expected "${expectedMessages[i]}", got "${actualMessages[i]}"`
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
779
925
|
// Check upToDate
|
|
780
926
|
if (expect.upToDate !== undefined && isReadResult(result)) {
|
|
781
927
|
if (result.upToDate !== expect.upToDate) {
|
|
@@ -858,6 +1004,41 @@ function validateExpectation(
|
|
|
858
1004
|
}
|
|
859
1005
|
}
|
|
860
1006
|
|
|
1007
|
+
// Check duplicate (for idempotent producer 204 responses)
|
|
1008
|
+
if (expect.duplicate !== undefined && isAppendResult(result)) {
|
|
1009
|
+
if (result.duplicate !== expect.duplicate) {
|
|
1010
|
+
return `Expected duplicate=${expect.duplicate}, got ${result.duplicate}`
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Check producerEpoch (returned on 200/403)
|
|
1015
|
+
if (expect.producerEpoch !== undefined && isAppendResult(result)) {
|
|
1016
|
+
if (result.producerEpoch !== expect.producerEpoch) {
|
|
1017
|
+
return `Expected producerEpoch=${expect.producerEpoch}, got ${result.producerEpoch}`
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Check producerSeq (returned on 200/204 - highest accepted sequence)
|
|
1022
|
+
if (expect.producerSeq !== undefined && isAppendResult(result)) {
|
|
1023
|
+
if (result.producerSeq !== expect.producerSeq) {
|
|
1024
|
+
return `Expected producerSeq=${expect.producerSeq}, got ${result.producerSeq}`
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Check producerExpectedSeq (returned on 409 sequence gap)
|
|
1029
|
+
if (expect.producerExpectedSeq !== undefined && isAppendResult(result)) {
|
|
1030
|
+
if (result.producerExpectedSeq !== expect.producerExpectedSeq) {
|
|
1031
|
+
return `Expected producerExpectedSeq=${expect.producerExpectedSeq}, got ${result.producerExpectedSeq}`
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Check producerReceivedSeq (returned on 409 sequence gap)
|
|
1036
|
+
if (expect.producerReceivedSeq !== undefined && isAppendResult(result)) {
|
|
1037
|
+
if (result.producerReceivedSeq !== expect.producerReceivedSeq) {
|
|
1038
|
+
return `Expected producerReceivedSeq=${expect.producerReceivedSeq}, got ${result.producerReceivedSeq}`
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
861
1042
|
return null
|
|
862
1043
|
}
|
|
863
1044
|
|
|
@@ -1041,8 +1222,12 @@ export async function runConformanceTests(
|
|
|
1041
1222
|
const totalTests = countTests(suites)
|
|
1042
1223
|
console.log(`\nRunning ${totalTests} client conformance tests...\n`)
|
|
1043
1224
|
|
|
1044
|
-
// Start reference server
|
|
1045
|
-
|
|
1225
|
+
// Start reference server with short long-poll timeout for testing
|
|
1226
|
+
// Tests use timeoutMs: 1000, so server timeout should be shorter
|
|
1227
|
+
const server = new DurableStreamTestServer({
|
|
1228
|
+
port: options.serverPort ?? 0,
|
|
1229
|
+
longPollTimeout: 500, // 500ms timeout for testing
|
|
1230
|
+
})
|
|
1046
1231
|
await server.start()
|
|
1047
1232
|
const serverUrl = server.url
|
|
1048
1233
|
|
package/src/test-cases.ts
CHANGED
|
@@ -118,10 +118,16 @@ export interface AppendOperation {
|
|
|
118
118
|
data?: string
|
|
119
119
|
/** Binary data (base64 encoded) */
|
|
120
120
|
binaryData?: string
|
|
121
|
-
/** Sequence number for ordering */
|
|
121
|
+
/** Sequence number for ordering (Stream-Seq header) */
|
|
122
122
|
seq?: number
|
|
123
123
|
headers?: Record<string, string>
|
|
124
124
|
expect?: AppendExpectation
|
|
125
|
+
/** Producer ID for idempotent producers */
|
|
126
|
+
producerId?: string
|
|
127
|
+
/** Producer epoch for idempotent producers */
|
|
128
|
+
producerEpoch?: number
|
|
129
|
+
/** Producer sequence for idempotent producers */
|
|
130
|
+
producerSeq?: number
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
/**
|
|
@@ -140,6 +146,64 @@ export interface AppendBatchOperation {
|
|
|
140
146
|
expect?: AppendBatchExpectation
|
|
141
147
|
}
|
|
142
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Append via IdempotentProducer client (tests client-side exactly-once semantics).
|
|
151
|
+
*/
|
|
152
|
+
export interface IdempotentAppendOperation {
|
|
153
|
+
action: `idempotent-append`
|
|
154
|
+
path: string
|
|
155
|
+
/** Producer ID */
|
|
156
|
+
producerId: string
|
|
157
|
+
/** Producer epoch */
|
|
158
|
+
epoch?: number
|
|
159
|
+
/** Data to append (string or JSON for JSON streams) */
|
|
160
|
+
data: string
|
|
161
|
+
/** Auto-claim epoch on 403 */
|
|
162
|
+
autoClaim?: boolean
|
|
163
|
+
headers?: Record<string, string>
|
|
164
|
+
expect?: IdempotentAppendExpectation
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Batch append via IdempotentProducer client (tests client-side JSON batching).
|
|
169
|
+
*/
|
|
170
|
+
export interface IdempotentAppendBatchOperation {
|
|
171
|
+
action: `idempotent-append-batch`
|
|
172
|
+
path: string
|
|
173
|
+
/** Producer ID */
|
|
174
|
+
producerId: string
|
|
175
|
+
/** Producer epoch */
|
|
176
|
+
epoch?: number
|
|
177
|
+
/** Items to append (will be batched by the client) */
|
|
178
|
+
items: Array<{
|
|
179
|
+
data: string
|
|
180
|
+
}>
|
|
181
|
+
/** Auto-claim epoch on 403 */
|
|
182
|
+
autoClaim?: boolean
|
|
183
|
+
/** Max concurrent batches in flight (default 1, set higher to test 409 retry) */
|
|
184
|
+
maxInFlight?: number
|
|
185
|
+
headers?: Record<string, string>
|
|
186
|
+
expect?: IdempotentAppendBatchExpectation
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Expectation for idempotent-append operation.
|
|
191
|
+
*/
|
|
192
|
+
export interface IdempotentAppendExpectation extends BaseExpectation {
|
|
193
|
+
/** Expected duplicate flag */
|
|
194
|
+
duplicate?: boolean
|
|
195
|
+
/** Store the returned offset */
|
|
196
|
+
storeOffsetAs?: string
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Expectation for idempotent-append-batch operation.
|
|
201
|
+
*/
|
|
202
|
+
export interface IdempotentAppendBatchExpectation extends BaseExpectation {
|
|
203
|
+
/** All items should succeed */
|
|
204
|
+
allSucceed?: boolean
|
|
205
|
+
}
|
|
206
|
+
|
|
143
207
|
/**
|
|
144
208
|
* Read from a stream.
|
|
145
209
|
*/
|
|
@@ -223,13 +287,42 @@ export interface AssertOperation {
|
|
|
223
287
|
|
|
224
288
|
/**
|
|
225
289
|
* Append to stream via direct server HTTP (bypasses client adapter).
|
|
226
|
-
* Used for concurrent operations when adapter is blocked on a read
|
|
290
|
+
* Used for concurrent operations when adapter is blocked on a read,
|
|
291
|
+
* and for testing protocol-level behavior like idempotent producers.
|
|
227
292
|
*/
|
|
228
293
|
export interface ServerAppendOperation {
|
|
229
294
|
action: `server-append`
|
|
230
295
|
path: string
|
|
231
296
|
data: string
|
|
232
297
|
headers?: Record<string, string>
|
|
298
|
+
/** Producer ID for idempotent producers */
|
|
299
|
+
producerId?: string
|
|
300
|
+
/** Producer epoch for idempotent producers */
|
|
301
|
+
producerEpoch?: number
|
|
302
|
+
/** Producer sequence for idempotent producers */
|
|
303
|
+
producerSeq?: number
|
|
304
|
+
/** Expected result */
|
|
305
|
+
expect?: ServerAppendExpectation
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Expectation for server-append operation.
|
|
310
|
+
*/
|
|
311
|
+
export interface ServerAppendExpectation {
|
|
312
|
+
/** Expected HTTP status code */
|
|
313
|
+
status?: number
|
|
314
|
+
/** Store the returned offset */
|
|
315
|
+
storeOffsetAs?: string
|
|
316
|
+
/** Expected duplicate flag (true for 204 idempotent success) */
|
|
317
|
+
duplicate?: boolean
|
|
318
|
+
/** Expected producer epoch in response */
|
|
319
|
+
producerEpoch?: number
|
|
320
|
+
/** Expected producer seq in response (highest accepted sequence) */
|
|
321
|
+
producerSeq?: number
|
|
322
|
+
/** Expected producer expected seq (on 409 sequence gap) */
|
|
323
|
+
producerExpectedSeq?: number
|
|
324
|
+
/** Expected producer received seq (on 409 sequence gap) */
|
|
325
|
+
producerReceivedSeq?: number
|
|
233
326
|
}
|
|
234
327
|
|
|
235
328
|
/**
|
|
@@ -243,19 +336,34 @@ export interface AwaitOperation {
|
|
|
243
336
|
}
|
|
244
337
|
|
|
245
338
|
/**
|
|
246
|
-
* Inject
|
|
339
|
+
* Inject a fault to be triggered on the next N requests to a path.
|
|
247
340
|
* Used for testing retry/resilience behavior.
|
|
341
|
+
* Supports various fault types: errors, delays, connection drops, body corruption, etc.
|
|
248
342
|
*/
|
|
249
343
|
export interface InjectErrorOperation {
|
|
250
344
|
action: `inject-error`
|
|
251
|
-
/** Stream path to inject
|
|
345
|
+
/** Stream path to inject fault for */
|
|
252
346
|
path: string
|
|
253
|
-
/** HTTP status code to return */
|
|
254
|
-
status
|
|
255
|
-
/** Number of times to
|
|
347
|
+
/** HTTP status code to return (if set, returns error response) */
|
|
348
|
+
status?: number
|
|
349
|
+
/** Number of times to trigger this fault (default: 1) */
|
|
256
350
|
count?: number
|
|
257
351
|
/** Optional Retry-After header value (seconds) */
|
|
258
352
|
retryAfter?: number
|
|
353
|
+
/** Delay in milliseconds before responding */
|
|
354
|
+
delayMs?: number
|
|
355
|
+
/** Drop the connection after sending headers (simulates network failure) */
|
|
356
|
+
dropConnection?: boolean
|
|
357
|
+
/** Truncate response body to this many bytes */
|
|
358
|
+
truncateBodyBytes?: number
|
|
359
|
+
/** Probability of triggering fault (0-1, default 1.0 = always) */
|
|
360
|
+
probability?: number
|
|
361
|
+
/** Only match specific HTTP method (GET, POST, PUT, DELETE) */
|
|
362
|
+
method?: string
|
|
363
|
+
/** Corrupt the response body by flipping random bits */
|
|
364
|
+
corruptBody?: boolean
|
|
365
|
+
/** Add jitter to delay (random 0-jitterMs added to delayMs) */
|
|
366
|
+
jitterMs?: number
|
|
259
367
|
}
|
|
260
368
|
|
|
261
369
|
/**
|
|
@@ -305,6 +413,8 @@ export type TestOperation =
|
|
|
305
413
|
| ConnectOperation
|
|
306
414
|
| AppendOperation
|
|
307
415
|
| AppendBatchOperation
|
|
416
|
+
| IdempotentAppendOperation
|
|
417
|
+
| IdempotentAppendBatchOperation
|
|
308
418
|
| ReadOperation
|
|
309
419
|
| HeadOperation
|
|
310
420
|
| DeleteOperation
|
|
@@ -345,13 +455,23 @@ export interface ConnectExpectation extends BaseExpectation {
|
|
|
345
455
|
}
|
|
346
456
|
|
|
347
457
|
export interface AppendExpectation extends BaseExpectation {
|
|
348
|
-
status?: 200 | 404 | 409 | number
|
|
458
|
+
status?: 200 | 204 | 400 | 403 | 404 | 409 | number
|
|
349
459
|
/** Store the returned offset */
|
|
350
460
|
storeOffsetAs?: string
|
|
351
461
|
/** Expected headers that were sent (for dynamic header testing) */
|
|
352
462
|
headersSent?: Record<string, string>
|
|
353
463
|
/** Expected params that were sent (for dynamic param testing) */
|
|
354
464
|
paramsSent?: Record<string, string>
|
|
465
|
+
/** Expected duplicate flag (true for 204 idempotent success) */
|
|
466
|
+
duplicate?: boolean
|
|
467
|
+
/** Expected producer epoch in response */
|
|
468
|
+
producerEpoch?: number
|
|
469
|
+
/** Expected producer seq in response (highest accepted sequence) */
|
|
470
|
+
producerSeq?: number
|
|
471
|
+
/** Expected producer expected seq (on 409 sequence gap) */
|
|
472
|
+
producerExpectedSeq?: number
|
|
473
|
+
/** Expected producer received seq (on 409 sequence gap) */
|
|
474
|
+
producerReceivedSeq?: number
|
|
355
475
|
}
|
|
356
476
|
|
|
357
477
|
export interface AppendBatchExpectation extends BaseExpectation {
|
|
@@ -371,6 +491,8 @@ export interface ReadExpectation extends BaseExpectation {
|
|
|
371
491
|
dataContains?: string
|
|
372
492
|
/** Expected data to contain all of these substrings */
|
|
373
493
|
dataContainsAll?: Array<string>
|
|
494
|
+
/** Expected exact messages in order (for JSON streams, verifies each chunk) */
|
|
495
|
+
dataExact?: Array<string>
|
|
374
496
|
/** Expected number of chunks */
|
|
375
497
|
chunkCount?: number
|
|
376
498
|
/** Minimum number of chunks */
|
|
@@ -56,6 +56,48 @@ tests:
|
|
|
56
56
|
status: 400
|
|
57
57
|
errorCode: INVALID_OFFSET
|
|
58
58
|
|
|
59
|
+
- id: read-offset-now-nonexistent
|
|
60
|
+
name: Read with offset=now on non-existent stream
|
|
61
|
+
description: offset=now should not mask a missing stream - must return 404
|
|
62
|
+
operations:
|
|
63
|
+
- action: read
|
|
64
|
+
path: /nonexistent-offset-now-stream
|
|
65
|
+
offset: "now"
|
|
66
|
+
live: false
|
|
67
|
+
expect:
|
|
68
|
+
status: 404
|
|
69
|
+
errorCode: NOT_FOUND
|
|
70
|
+
|
|
71
|
+
- id: read-offset-now-nonexistent-longpoll
|
|
72
|
+
name: Long-poll with offset=now on non-existent stream
|
|
73
|
+
description: offset=now with long-poll should not mask a missing stream - must return 404
|
|
74
|
+
requires:
|
|
75
|
+
- longPoll
|
|
76
|
+
operations:
|
|
77
|
+
- action: read
|
|
78
|
+
path: /nonexistent-offset-now-longpoll
|
|
79
|
+
offset: "now"
|
|
80
|
+
live: long-poll
|
|
81
|
+
timeoutMs: 1000
|
|
82
|
+
expect:
|
|
83
|
+
status: 404
|
|
84
|
+
errorCode: NOT_FOUND
|
|
85
|
+
|
|
86
|
+
- id: read-offset-now-nonexistent-sse
|
|
87
|
+
name: SSE with offset=now on non-existent stream
|
|
88
|
+
description: offset=now with SSE should not mask a missing stream - must return 404
|
|
89
|
+
requires:
|
|
90
|
+
- sse
|
|
91
|
+
operations:
|
|
92
|
+
- action: read
|
|
93
|
+
path: /nonexistent-offset-now-sse
|
|
94
|
+
offset: "now"
|
|
95
|
+
live: sse
|
|
96
|
+
timeoutMs: 1000
|
|
97
|
+
expect:
|
|
98
|
+
status: 404
|
|
99
|
+
errorCode: NOT_FOUND
|
|
100
|
+
|
|
59
101
|
- id: read-future-offset
|
|
60
102
|
name: Read with future offset
|
|
61
103
|
description: Client should handle offset beyond stream end
|