@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
@@ -190,7 +190,7 @@ export const smallMessageThroughputScenario: BenchmarkScenario = {
190
190
  createOperation: (ctx) => ({
191
191
  op: `throughput_append`,
192
192
  path: `${ctx.basePath}/throughput-small`,
193
- count: 10000,
193
+ count: 100000,
194
194
  size: 100,
195
195
  concurrency: 200,
196
196
  }),
@@ -239,9 +239,9 @@ export const readThroughputScenario: BenchmarkScenario = {
239
239
  expectedCount: ctx.setupData.expectedCount as number | undefined,
240
240
  }),
241
241
  setup: (ctx) => {
242
- // Expecting 10000 JSON messages to be pre-populated
243
- ctx.setupData.expectedCount = 10000
244
- return Promise.resolve({ data: { expectedCount: 10000 } })
242
+ // Expecting 100000 JSON messages to be pre-populated
243
+ ctx.setupData.expectedCount = 100000
244
+ return Promise.resolve({ data: { expectedCount: 100000 } })
245
245
  },
246
246
  }
247
247
 
package/src/cli.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { runConformanceTests } from "./runner.js"
13
- import { runBenchmarks } from "./benchmark-runner.js"
13
+ import { aggregateBenchmarkResults, runBenchmarks } from "./benchmark-runner.js"
14
14
  import type { RunnerOptions } from "./runner.js"
15
15
  import type { BenchmarkRunnerOptions } from "./benchmark-runner.js"
16
16
 
@@ -20,6 +20,7 @@ Durable Streams Client Conformance Test Suite
20
20
  Usage:
21
21
  npx @durable-streams/client-conformance-tests --run <adapter> [options]
22
22
  npx @durable-streams/client-conformance-tests --bench <adapter> [options]
23
+ npx @durable-streams/client-conformance-tests --report <dir>
23
24
 
24
25
  Arguments:
25
26
  <adapter> Path to client adapter executable, or "ts" for built-in TypeScript adapter
@@ -41,6 +42,10 @@ Benchmark Options:
41
42
  Can be specified multiple times
42
43
  --format <fmt> Output format: console, json, markdown (default: console)
43
44
 
45
+ Report Options:
46
+ --report <dir> Aggregate benchmark results from JSON files in directory
47
+ Each subdirectory should contain a benchmark-results.json file
48
+
44
49
  Common Options:
45
50
  --verbose Show detailed output for each operation
46
51
  --port <port> Port for reference server (default: random)
@@ -72,6 +77,10 @@ Benchmark Examples:
72
77
  # Output as JSON for CI
73
78
  npx @durable-streams/client-conformance-tests --bench ts --format json
74
79
 
80
+ Report Examples:
81
+ # Aggregate benchmark results from CI artifacts
82
+ npx @durable-streams/client-conformance-tests --report ./benchmark-results
83
+
75
84
  Implementing a Client Adapter:
76
85
  A client adapter is an executable that communicates via stdin/stdout using
77
86
  JSON-line protocol. See the documentation for the protocol specification
@@ -93,11 +102,13 @@ Implementing a Client Adapter:
93
102
  type ParsedOptions =
94
103
  | { mode: `conformance`; options: RunnerOptions }
95
104
  | { mode: `benchmark`; options: BenchmarkRunnerOptions }
105
+ | { mode: `report`; resultsDir: string }
96
106
  | null
97
107
 
98
108
  function parseArgs(args: Array<string>): ParsedOptions {
99
- let mode: `conformance` | `benchmark` | null = null
109
+ let mode: `conformance` | `benchmark` | `report` | null = null
100
110
  let clientAdapter = ``
111
+ let resultsDir = ``
101
112
 
102
113
  // Conformance-specific options
103
114
  const suites: Array<`producer` | `consumer` | `lifecycle`> = []
@@ -139,6 +150,14 @@ function parseArgs(args: Array<string>): ParsedOptions {
139
150
  return null
140
151
  }
141
152
  clientAdapter = args[i]!
153
+ } else if (arg === `--report`) {
154
+ mode = `report`
155
+ i++
156
+ if (i >= args.length) {
157
+ console.error(`Error: --report requires a directory path`)
158
+ return null
159
+ }
160
+ resultsDir = args[i]!
142
161
  } else if (arg === `--suite`) {
143
162
  i++
144
163
  if (i >= args.length) {
@@ -230,8 +249,26 @@ function parseArgs(args: Array<string>): ParsedOptions {
230
249
  }
231
250
 
232
251
  // Validate required options
233
- if (!mode || !clientAdapter) {
234
- console.error(`Error: --run <adapter> or --bench <adapter> is required`)
252
+ if (!mode) {
253
+ console.error(
254
+ `Error: --run <adapter>, --bench <adapter>, or --report <dir> is required`
255
+ )
256
+ console.log(`\nRun with --help for usage information`)
257
+ return null
258
+ }
259
+
260
+ if (mode === `report`) {
261
+ if (!resultsDir) {
262
+ console.error(`Error: --report requires a directory path`)
263
+ return null
264
+ }
265
+ return { mode: `report`, resultsDir }
266
+ }
267
+
268
+ if (!clientAdapter) {
269
+ console.error(
270
+ `Error: --run <adapter> or --bench <adapter> requires an adapter path`
271
+ )
235
272
  console.log(`\nRun with --help for usage information`)
236
273
  return null
237
274
  }
@@ -279,11 +316,15 @@ async function main(): Promise<void> {
279
316
  if (summary.failed > 0) {
280
317
  process.exit(1)
281
318
  }
282
- } else {
319
+ } else if (parsed.mode === `benchmark`) {
283
320
  const summary = await runBenchmarks(parsed.options)
284
321
  if (summary.failed > 0) {
285
322
  process.exit(1)
286
323
  }
324
+ } else {
325
+ // parsed.mode === `report`
326
+ const report = await aggregateBenchmarkResults(parsed.resultsDir)
327
+ console.log(report)
287
328
  }
288
329
  } catch (err) {
289
330
  console.error(`Error running ${parsed.mode}:`, err)
package/src/protocol.ts CHANGED
@@ -120,8 +120,8 @@ export interface ReadCommand {
120
120
  path: string
121
121
  /** Starting offset (opaque string from previous reads) */
122
122
  offset?: string
123
- /** Live mode: false for catch-up only, "long-poll" or "sse" for live */
124
- live?: false | `long-poll` | `sse`
123
+ /** Live mode: false for catch-up only, true for auto-select, "long-poll" or "sse" for explicit */
124
+ live?: false | true | `long-poll` | `sse`
125
125
  /** Timeout for long-poll in milliseconds */
126
126
  timeoutMs?: number
127
127
  /** Maximum number of chunks to read (for testing) */
@@ -196,6 +196,59 @@ export interface ClearDynamicCommand {
196
196
  type: `clear-dynamic`
197
197
  }
198
198
 
199
+ // =============================================================================
200
+ // Validation Commands
201
+ // =============================================================================
202
+
203
+ /**
204
+ * Test client-side input validation.
205
+ *
206
+ * This command tests that the client properly validates input parameters
207
+ * before making any network requests. The adapter should attempt to create
208
+ * the specified object with the given parameters and report whether
209
+ * validation passed or failed.
210
+ */
211
+ export interface ValidateCommand {
212
+ type: `validate`
213
+ /** What to validate */
214
+ target: ValidateTarget
215
+ }
216
+
217
+ /**
218
+ * Validation targets - what client-side validation to test.
219
+ */
220
+ export type ValidateTarget = ValidateRetryOptions | ValidateIdempotentProducer
221
+
222
+ /**
223
+ * Validate RetryOptions construction.
224
+ */
225
+ export interface ValidateRetryOptions {
226
+ target: `retry-options`
227
+ /** Max retries (should reject < 0) */
228
+ maxRetries?: number
229
+ /** Initial delay in ms (should reject <= 0) */
230
+ initialDelayMs?: number
231
+ /** Max delay in ms (should reject < initialDelayMs) */
232
+ maxDelayMs?: number
233
+ /** Backoff multiplier (should reject < 1.0) */
234
+ multiplier?: number
235
+ }
236
+
237
+ /**
238
+ * Validate IdempotentProducer construction.
239
+ */
240
+ export interface ValidateIdempotentProducer {
241
+ target: `idempotent-producer`
242
+ /** Producer ID (required, non-empty) */
243
+ producerId?: string
244
+ /** Starting epoch (should reject < 0) */
245
+ epoch?: number
246
+ /** Max batch bytes (should reject <= 0) */
247
+ maxBatchBytes?: number
248
+ /** Max batch items (should reject <= 0) */
249
+ maxBatchItems?: number
250
+ }
251
+
199
252
  // =============================================================================
200
253
  // Benchmark Commands
201
254
  // =============================================================================
@@ -289,6 +342,7 @@ export type TestCommand =
289
342
  | SetDynamicParamCommand
290
343
  | ClearDynamicCommand
291
344
  | BenchmarkCommand
345
+ | ValidateCommand
292
346
 
293
347
  // =============================================================================
294
348
  // Results (sent from client adapter to test runner via stdout)
@@ -312,10 +366,18 @@ export interface InitResult {
312
366
  sse?: boolean
313
367
  /** Supports long-poll mode */
314
368
  longPoll?: boolean
369
+ /** Supports auto mode (catch-up then auto-select SSE or long-poll) */
370
+ auto?: boolean
315
371
  /** Supports streaming reads */
316
372
  streaming?: boolean
317
373
  /** Supports dynamic headers/params (functions evaluated per-request) */
318
374
  dynamicHeaders?: boolean
375
+ /** Supports RetryOptions validation (PHP-specific) */
376
+ retryOptions?: boolean
377
+ /** Supports maxBatchItems option (PHP-specific) */
378
+ batchItems?: boolean
379
+ /** Rejects zero values as invalid (vs treating 0 as "use default" like Go) */
380
+ strictZeroValidation?: boolean
319
381
  }
320
382
  }
321
383
 
@@ -492,6 +554,14 @@ export interface ClearDynamicResult {
492
554
  success: true
493
555
  }
494
556
 
557
+ /**
558
+ * Successful validate result (validation passed).
559
+ */
560
+ export interface ValidateResult {
561
+ type: `validate`
562
+ success: true
563
+ }
564
+
495
565
  /**
496
566
  * Successful benchmark result with timing.
497
567
  */
@@ -549,6 +619,7 @@ export type TestResult =
549
619
  | SetDynamicHeaderResult
550
620
  | SetDynamicParamResult
551
621
  | ClearDynamicResult
622
+ | ValidateResult
552
623
  | BenchmarkResult
553
624
  | ErrorResult
554
625
 
@@ -622,6 +693,8 @@ export const ErrorCodes = {
622
693
  INTERNAL_ERROR: `INTERNAL_ERROR`,
623
694
  /** Operation not supported by this client */
624
695
  NOT_SUPPORTED: `NOT_SUPPORTED`,
696
+ /** Invalid argument passed to client API */
697
+ INVALID_ARGUMENT: `INVALID_ARGUMENT`,
625
698
  } as const
626
699
 
627
700
  export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]
package/src/runner.ts CHANGED
@@ -77,8 +77,12 @@ interface ClientFeatures {
77
77
  batching?: boolean
78
78
  sse?: boolean
79
79
  longPoll?: boolean
80
+ auto?: boolean
80
81
  streaming?: boolean
81
82
  dynamicHeaders?: boolean
83
+ retryOptions?: boolean
84
+ batchItems?: boolean
85
+ strictZeroValidation?: boolean
82
86
  }
83
87
 
84
88
  interface ExecutionContext {
@@ -300,7 +304,13 @@ async function executeOperation(
300
304
 
301
305
  case `append`: {
302
306
  const path = resolveVariables(op.path, variables)
303
- const data = op.data ? resolveVariables(op.data, variables) : ``
307
+ // Handle json property (YAML object) by stringifying, or use data string directly
308
+ const data =
309
+ op.json !== undefined
310
+ ? JSON.stringify(op.json)
311
+ : op.data
312
+ ? resolveVariables(op.data, variables)
313
+ : ``
304
314
 
305
315
  const result = await client.send(
306
316
  {
@@ -729,6 +739,7 @@ async function executeOperation(
729
739
  method: op.method,
730
740
  corruptBody: op.corruptBody,
731
741
  jitterMs: op.jitterMs,
742
+ injectSseEvent: op.injectSseEvent,
732
743
  }),
733
744
  })
734
745
 
@@ -742,6 +753,8 @@ async function executeOperation(
742
753
  faultTypes.push(`truncate=${op.truncateBodyBytes}b`)
743
754
  if (op.corruptBody) faultTypes.push(`corrupt`)
744
755
  if (op.probability != null) faultTypes.push(`p=${op.probability}`)
756
+ if (op.injectSseEvent)
757
+ faultTypes.push(`sse:${op.injectSseEvent.eventType}`)
745
758
  const faultDesc = faultTypes.join(`,`) || `unknown`
746
759
 
747
760
  if (verbose) {
@@ -835,6 +848,25 @@ async function executeOperation(
835
848
  return { result }
836
849
  }
837
850
 
851
+ case `validate`: {
852
+ const result = await client.send(
853
+ {
854
+ type: `validate`,
855
+ target: op.target,
856
+ },
857
+ commandTimeout
858
+ )
859
+
860
+ if (verbose) {
861
+ const targetType = op.target.target
862
+ console.log(
863
+ ` validate ${targetType}: ${result.success ? `ok` : `failed`}`
864
+ )
865
+ }
866
+
867
+ return { result }
868
+ }
869
+
838
870
  default:
839
871
  return { error: `Unknown operation: ${(op as TestOperation).action}` }
840
872
  }
@@ -879,6 +911,21 @@ function validateExpectation(
879
911
  }
880
912
  }
881
913
 
914
+ // Check error message contains expected strings
915
+ if (expect.messageContains !== undefined) {
916
+ if (result.success) {
917
+ return `Expected error with message containing ${JSON.stringify(expect.messageContains)}, but operation succeeded`
918
+ }
919
+ if (isErrorResult(result)) {
920
+ const missing = (expect.messageContains as Array<string>).filter(
921
+ (s) => !result.message.toLowerCase().includes(s.toLowerCase())
922
+ )
923
+ if (missing.length > 0) {
924
+ return `Expected error message to contain [${(expect.messageContains as Array<string>).join(`, `)}], missing: [${missing.join(`, `)}]. Actual message: "${result.message}"`
925
+ }
926
+ }
927
+ }
928
+
882
929
  // Check data (for read results)
883
930
  if (expect.data !== undefined && isReadResult(result)) {
884
931
  const actualData = result.chunks.map((c) => c.data).join(``)
@@ -1039,6 +1086,23 @@ function validateExpectation(
1039
1086
  }
1040
1087
  }
1041
1088
 
1089
+ // Check valid (for validation operations)
1090
+ if (expect.valid !== undefined) {
1091
+ if (expect.valid === true && !result.success) {
1092
+ return `Expected validation to pass, but it failed`
1093
+ }
1094
+ if (expect.valid === false && result.success) {
1095
+ return `Expected validation to fail, but it passed`
1096
+ }
1097
+ }
1098
+
1099
+ // Check errorContains (for validation operations with error message substring)
1100
+ if (expect.errorContains !== undefined && isErrorResult(result)) {
1101
+ if (!result.message.includes(expect.errorContains as string)) {
1102
+ return `Expected error message to contain "${expect.errorContains}", got "${result.message}"`
1103
+ }
1104
+ }
1105
+
1042
1106
  return null
1043
1107
  }
1044
1108
 
@@ -1051,9 +1115,16 @@ function featureToProperty(feature: string): keyof ClientFeatures | undefined {
1051
1115
  sse: `sse`,
1052
1116
  "long-poll": `longPoll`,
1053
1117
  longPoll: `longPoll`,
1118
+ auto: `auto`,
1054
1119
  streaming: `streaming`,
1055
1120
  dynamicHeaders: `dynamicHeaders`,
1056
1121
  "dynamic-headers": `dynamicHeaders`,
1122
+ retryOptions: `retryOptions`,
1123
+ "retry-options": `retryOptions`,
1124
+ batchItems: `batchItems`,
1125
+ "batch-items": `batchItems`,
1126
+ strictZeroValidation: `strictZeroValidation`,
1127
+ "strict-zero-validation": `strictZeroValidation`,
1057
1128
  }
1058
1129
  return map[feature]
1059
1130
  }
package/src/test-cases.ts CHANGED
@@ -116,6 +116,8 @@ export interface AppendOperation {
116
116
  path: string
117
117
  /** Data to append (string) */
118
118
  data?: string
119
+ /** JSON data to append (will be stringified) */
120
+ json?: unknown
119
121
  /** Binary data (base64 encoded) */
120
122
  binaryData?: string
121
123
  /** Sequence number for ordering (Stream-Seq header) */
@@ -364,6 +366,13 @@ export interface InjectErrorOperation {
364
366
  corruptBody?: boolean
365
367
  /** Add jitter to delay (random 0-jitterMs added to delayMs) */
366
368
  jitterMs?: number
369
+ /** Inject an SSE event with custom type and data (for testing SSE parsing) */
370
+ injectSseEvent?: {
371
+ /** Event type (e.g., "unknown", "control", "data") */
372
+ eventType: string
373
+ /** Event data (will be sent as-is) */
374
+ data: string
375
+ }
367
376
  }
368
377
 
369
378
  /**
@@ -405,6 +414,49 @@ export interface ClearDynamicOperation {
405
414
  action: `clear-dynamic`
406
415
  }
407
416
 
417
+ /**
418
+ * Validate client-side input parameters.
419
+ * Tests that clients properly validate inputs before making network requests.
420
+ */
421
+ export interface ValidateOperation {
422
+ action: `validate`
423
+ /** What to validate */
424
+ target: ValidateTarget
425
+ expect?: ValidateExpectation
426
+ }
427
+
428
+ /**
429
+ * Validation target types.
430
+ */
431
+ export type ValidateTarget =
432
+ | ValidateRetryOptionsTarget
433
+ | ValidateIdempotentProducerTarget
434
+
435
+ export interface ValidateRetryOptionsTarget {
436
+ target: `retry-options`
437
+ maxRetries?: number
438
+ initialDelayMs?: number
439
+ maxDelayMs?: number
440
+ multiplier?: number
441
+ }
442
+
443
+ export interface ValidateIdempotentProducerTarget {
444
+ target: `idempotent-producer`
445
+ producerId?: string
446
+ epoch?: number
447
+ maxBatchBytes?: number
448
+ maxBatchItems?: number
449
+ }
450
+
451
+ export interface ValidateExpectation {
452
+ /** If true, validation should pass */
453
+ valid?: boolean
454
+ /** Expected error code if validation fails */
455
+ errorCode?: string
456
+ /** Expected error message substring if validation fails */
457
+ errorContains?: string
458
+ }
459
+
408
460
  /**
409
461
  * All possible test operations.
410
462
  */
@@ -428,6 +480,7 @@ export type TestOperation =
428
480
  | SetDynamicHeaderOperation
429
481
  | SetDynamicParamOperation
430
482
  | ClearDynamicOperation
483
+ | ValidateOperation
431
484
 
432
485
  // =============================================================================
433
486
  // Expectations
@@ -441,6 +494,8 @@ interface BaseExpectation {
441
494
  status?: number
442
495
  /** Expected error code (if operation should fail) */
443
496
  errorCode?: string
497
+ /** Strings that should be present in error message (for context validation) */
498
+ messageContains?: Array<string>
444
499
  /** Store result in variable */
445
500
  storeAs?: string
446
501
  }
@@ -0,0 +1,67 @@
1
+ id: consumer-error-context
2
+ name: Consumer Error Context
3
+ description: |
4
+ Tests that error messages include helpful context (URL, status, etc.).
5
+ Catching bugs where errors lose context or provide unhelpful messages.
6
+ category: consumer
7
+ tags:
8
+ - errors
9
+ - context
10
+ - messages
11
+
12
+ tests:
13
+ - id: not-found-includes-path
14
+ name: 404 error includes stream path in message
15
+ description: Error message should include the stream path for debugging
16
+ operations:
17
+ - action: read
18
+ path: /error-context-nonexistent-stream
19
+ live: false
20
+ expect:
21
+ status: 404
22
+ errorCode: NOT_FOUND
23
+ messageContains:
24
+ - "error-context-nonexistent-stream"
25
+
26
+ - id: not-found-on-append-includes-path
27
+ name: 404 on append includes stream path in message
28
+ description: Append to non-existent stream should report the path
29
+ operations:
30
+ - action: append
31
+ path: /error-context-append-nonexistent
32
+ data: "test"
33
+ expect:
34
+ status: 404
35
+ errorCode: NOT_FOUND
36
+ messageContains:
37
+ - "error-context-append-nonexistent"
38
+
39
+ - id: invalid-offset-includes-context
40
+ name: Invalid offset error includes helpful context
41
+ description: Error for invalid offset should indicate the nature of the error
42
+ setup:
43
+ - action: create
44
+ as: streamPath
45
+ - action: append
46
+ path: ${streamPath}
47
+ data: "data"
48
+ operations:
49
+ - action: read
50
+ path: ${streamPath}
51
+ offset: "not-a-valid-offset-format"
52
+ live: false
53
+ expect:
54
+ status: 400
55
+ errorCode: INVALID_OFFSET
56
+
57
+ - id: head-not-found-includes-path
58
+ name: HEAD 404 error includes stream path
59
+ description: HEAD request to non-existent stream should include path in error
60
+ operations:
61
+ - action: head
62
+ path: /error-context-head-nonexistent
63
+ expect:
64
+ status: 404
65
+ errorCode: NOT_FOUND
66
+ messageContains:
67
+ - "error-context-head-nonexistent"
@@ -0,0 +1,115 @@
1
+ id: consumer-json-parsing-errors
2
+ name: JSON Parsing Error Handling
3
+ description: Tests that clients properly handle malformed JSON responses instead of silently failing
4
+ category: consumer
5
+ tags:
6
+ - json
7
+ - error-handling
8
+ - resilience
9
+ - fault-injection
10
+
11
+ tests:
12
+ - id: truncated-json-response
13
+ name: Client throws on truncated JSON response
14
+ description: Client should throw PARSE_ERROR for incomplete JSON, not return empty array
15
+ requiredFeatures: [json]
16
+ setup:
17
+ - action: create
18
+ as: streamPath
19
+ contentType: application/json
20
+ - action: append
21
+ path: ${streamPath}
22
+ json: { "key": "some long value that will be truncated" }
23
+ operations:
24
+ # Truncate the response mid-JSON (10 bytes is mid-object)
25
+ - action: inject-error
26
+ path: ${streamPath}
27
+ truncateBodyBytes: 10
28
+ method: GET
29
+ count: 1
30
+ - action: read
31
+ path: ${streamPath}
32
+ expect:
33
+ errorCode: PARSE_ERROR
34
+ cleanup:
35
+ - action: clear-errors
36
+
37
+ - id: corrupted-json-response
38
+ name: Client throws on corrupted JSON response
39
+ description: Client should throw PARSE_ERROR for invalid JSON, not return empty array
40
+ requiredFeatures: [json]
41
+ setup:
42
+ - action: create
43
+ as: streamPath
44
+ contentType: application/json
45
+ - action: append
46
+ path: ${streamPath}
47
+ json: { "valid": "json data here" }
48
+ operations:
49
+ # Corrupt the JSON by flipping bits
50
+ - action: inject-error
51
+ path: ${streamPath}
52
+ corruptBody: true
53
+ method: GET
54
+ count: 1
55
+ - action: read
56
+ path: ${streamPath}
57
+ expect:
58
+ errorCode: PARSE_ERROR
59
+ cleanup:
60
+ - action: clear-errors
61
+
62
+ - id: truncated-json-array
63
+ name: Client throws on truncated JSON array
64
+ description: Client should throw when JSON array is truncated mid-element
65
+ requiredFeatures: [json]
66
+ setup:
67
+ - action: create
68
+ as: streamPath
69
+ contentType: application/json
70
+ - action: append
71
+ path: ${streamPath}
72
+ json: [{ "item": 1 }, { "item": 2 }, { "item": 3 }]
73
+ operations:
74
+ # Truncate mid-array
75
+ - action: inject-error
76
+ path: ${streamPath}
77
+ truncateBodyBytes: 15
78
+ method: GET
79
+ count: 1
80
+ - action: read
81
+ path: ${streamPath}
82
+ expect:
83
+ errorCode: PARSE_ERROR
84
+ cleanup:
85
+ - action: clear-errors
86
+
87
+ - id: recovery-after-parse-error
88
+ name: Client recovers after parse error
89
+ description: After a parse error, subsequent requests should succeed
90
+ requiredFeatures: [json]
91
+ setup:
92
+ - action: create
93
+ as: streamPath
94
+ contentType: application/json
95
+ - action: append
96
+ path: ${streamPath}
97
+ json: { "data": "valid" }
98
+ operations:
99
+ # First request fails with truncated body
100
+ - action: inject-error
101
+ path: ${streamPath}
102
+ truncateBodyBytes: 5
103
+ method: GET
104
+ count: 1
105
+ - action: read
106
+ path: ${streamPath}
107
+ expect:
108
+ errorCode: PARSE_ERROR
109
+ # Second request should succeed (no more faults)
110
+ - action: read
111
+ path: ${streamPath}
112
+ expect:
113
+ status: 200
114
+ cleanup:
115
+ - action: clear-errors