@@ -394,6 +394,25 @@ describe('RequestBuilder', () => {
394394 expect ( url . searchParams . get ( 'filter[validField]' ) ) . toBe ( 'test' ) ;
395395 } ) ;
396396
397+ it ( 'should handle arrays correctly within objects' , async ( ) => {
398+ const params = {
399+ pathParam : 'test-value' ,
400+ filter : {
401+ arrayField : [ 1 , 2 , 3 ] ,
402+ stringArray : [ 'a' , 'b' , 'c' ] ,
403+ mixed : [ 'string' , 42 , true ] ,
404+ } ,
405+ } ;
406+
407+ const result = await builder . execute ( params , { dryRun : true } ) ;
408+ const url = new URL ( result . url as string ) ;
409+
410+ // Arrays should be converted to JSON strings
411+ expect ( url . searchParams . get ( 'filter[arrayField]' ) ) . toBe ( '[1,2,3]' ) ;
412+ expect ( url . searchParams . get ( 'filter[stringArray]' ) ) . toBe ( '["a","b","c"]' ) ;
413+ expect ( url . searchParams . get ( 'filter[mixed]' ) ) . toBe ( '["string",42,true]' ) ;
414+ } ) ;
415+
397416 it ( 'should handle nested objects with special types at runtime' , async ( ) => {
398417 // Test runtime serialization of nested non-JSON types
399418 const params = {
@@ -474,6 +493,24 @@ describe('RequestBuilder', () => {
474493 } ) ;
475494} ) ;
476495
496+ /**
497+ * Property-Based Tests for RequestBuilder
498+ *
499+ * These tests verify invariants that must hold for ANY valid input,
500+ * replacing/supplementing example-based tests:
501+ *
502+ * Parameter Key Validation (replaces "should validate parameter keys and reject invalid characters"):
503+ * - Valid: "user_id", "filter.name", "x-custom-field" => accepted
504+ * - Invalid: "invalid key with spaces", "key@special!" => throws "Invalid parameter key"
505+ *
506+ * Value Serialization (supplements "should handle arrays correctly within objects" - kept for clarity):
507+ * - { arrayField: [1, 2, 3] } => filter[arrayField]="[1,2,3]"
508+ * - { stringArray: ["a", "b"] } => filter[stringArray]='["a","b"]'
509+ *
510+ * Deep Object Nesting (replaces "should throw error when recursion depth limit is exceeded"):
511+ * - { nested: { nested: { value: "ok" } } } (depth 3) => accepted
512+ * - { nested: { nested: { ... 12 levels ... } } } => throws "Maximum nesting depth (10) exceeded"
513+ */
477514describe ( 'RequestBuilder - Property-Based Tests' , ( ) => {
478515 const baseConfig = {
479516 kind : 'http' ,
@@ -491,7 +528,14 @@ describe('RequestBuilder - Property-Based Tests', () => {
491528 . string ( { minLength : 1 , maxLength : 20 } )
492529 . filter ( ( s ) => / [ ^ a - z A - Z 0 - 9 _ . - ] / . test ( s ) && s . trim ( ) . length > 0 ) ;
493530
531+ /**
532+ * Parameter Key Validation
533+ *
534+ * Examples of valid keys: "user_id", "filter.name", "X-Custom-Header"
535+ * Examples of invalid keys: "invalid key", "special@char", "has spaces"
536+ */
494537 describe ( 'Parameter Key Validation' , ( ) => {
538+ // Example: { filter: { user_id: "123" } } => ?filter[user_id]=123 (no error)
495539 fcTest . prop ( [ validKeyArbitrary , fc . string ( ) ] , { numRuns : 100 } ) (
496540 'accepts valid parameter keys' ,
497541 async ( key , value ) => {
@@ -506,6 +550,7 @@ describe('RequestBuilder - Property-Based Tests', () => {
506550 } ,
507551 ) ;
508552
553+ // Example: { filter: { "invalid key with spaces": "test" } } => throws Error
509554 fcTest . prop ( [ invalidKeyArbitrary , fc . string ( ) ] , { numRuns : 100 } ) (
510555 'rejects invalid parameter keys' ,
511556 async ( key , value ) => {
@@ -521,6 +566,14 @@ describe('RequestBuilder - Property-Based Tests', () => {
521566 ) ;
522567 } ) ;
523568
569+ /**
570+ * Header Management
571+ *
572+ * Examples:
573+ * - new RequestBuilder(config, { "Auth": "token" }).setHeaders({ "X-Api": "key" })
574+ * => getHeaders() returns { "Auth": "token", "X-Api": "key" }
575+ * - prepareHeaders() always includes "User-Agent: stackone-ai-node"
576+ */
524577 describe ( 'Header Management' , ( ) => {
525578 // Arbitrary for header key-value pairs
526579 const headerArbitrary = fc . dictionary (
@@ -529,6 +582,7 @@ describe('RequestBuilder - Property-Based Tests', () => {
529582 { minKeys : 1 , maxKeys : 5 } ,
530583 ) ;
531584
585+ // Example: init with {"A": "1"}, setHeaders({"B": "2"}) => {"A": "1", "B": "2"}
532586 fcTest . prop ( [ headerArbitrary , headerArbitrary ] , { numRuns : 50 } ) (
533587 'setHeaders accumulates headers without losing existing ones' ,
534588 ( headers1 , headers2 ) => {
@@ -549,6 +603,7 @@ describe('RequestBuilder - Property-Based Tests', () => {
549603 } ,
550604 ) ;
551605
606+ // Example: prepareHeaders() => { "User-Agent": "stackone-ai-node", ...customHeaders }
552607 fcTest . prop ( [ headerArbitrary ] , { numRuns : 50 } ) (
553608 'prepareHeaders always includes User-Agent' ,
554609 ( headers ) => {
@@ -559,6 +614,7 @@ describe('RequestBuilder - Property-Based Tests', () => {
559614 } ,
560615 ) ;
561616
617+ // Example: const h = getHeaders(); h["X"] = "Y"; getHeaders()["X"] is still undefined
562618 fcTest . prop ( [ headerArbitrary ] , { numRuns : 50 } ) (
563619 'getHeaders returns a copy, not the original' ,
564620 ( headers ) => {
@@ -573,7 +629,18 @@ describe('RequestBuilder - Property-Based Tests', () => {
573629 ) ;
574630 } ) ;
575631
632+ /**
633+ * Value Serialization
634+ *
635+ * Examples:
636+ * - { key: "hello" } => ?filter[key]=hello
637+ * - { key: 42 } => ?filter[key]=42
638+ * - { key: true } => ?filter[key]=true
639+ * - { key: [1, 2, 3] } => ?filter[key]=[1,2,3]
640+ * - { key: ["a", "b"] } => ?filter[key]=["a","b"]
641+ */
576642 describe ( 'Value Serialization' , ( ) => {
643+ // Example: { filter: { key: "hello world" } } => ?filter[key]=hello%20world
577644 fcTest . prop ( [ fc . string ( ) ] , { numRuns : 100 } ) (
578645 'string values serialize to themselves' ,
579646 async ( str ) => {
@@ -587,6 +654,7 @@ describe('RequestBuilder - Property-Based Tests', () => {
587654 } ,
588655 ) ;
589656
657+ // Example: { filter: { key: 42 } } => ?filter[key]=42
590658 fcTest . prop ( [ fc . integer ( ) ] , { numRuns : 100 } ) (
591659 'integer values serialize to string' ,
592660 async ( num ) => {
@@ -600,6 +668,7 @@ describe('RequestBuilder - Property-Based Tests', () => {
600668 } ,
601669 ) ;
602670
671+ // Example: { filter: { key: true } } => ?filter[key]=true
603672 fcTest . prop ( [ fc . boolean ( ) ] , { numRuns : 10 } ) (
604673 'boolean values serialize to string' ,
605674 async ( bool ) => {
@@ -613,6 +682,8 @@ describe('RequestBuilder - Property-Based Tests', () => {
613682 } ,
614683 ) ;
615684
685+ // Example: { filter: { key: [1, 2, 3] } } => ?filter[key]=[1,2,3]
686+ // Example: { filter: { key: ["a", "b"] } } => ?filter[key]=["a","b"]
616687 fcTest . prop (
617688 [ fc . array ( fc . oneof ( fc . string ( ) , fc . integer ( ) , fc . boolean ( ) ) , { minLength : 1 , maxLength : 5 } ) ] ,
618689 {
@@ -629,7 +700,16 @@ describe('RequestBuilder - Property-Based Tests', () => {
629700 } ) ;
630701 } ) ;
631702
703+ /**
704+ * Deep Object Nesting
705+ *
706+ * Examples:
707+ * - { nested: { value: "ok" } } (depth 2) => accepted
708+ * - { a: { b: { c: { d: { e: { f: { g: { h: { i: { j: { k: "too deep" } } } } } } } } } } }
709+ * (depth 11) => throws "Maximum nesting depth (10) exceeded"
710+ */
632711 describe ( 'Deep Object Nesting' , ( ) => {
712+ // Example: depth 5 => { nested: { nested: { nested: { nested: { nested: { value: "test" } } } } } }
633713 fcTest . prop ( [ fc . integer ( { min : 1 , max : 9 } ) ] , { numRuns : 20 } ) (
634714 'accepts objects within depth limit' ,
635715 async ( depth ) => {
@@ -649,6 +729,7 @@ describe('RequestBuilder - Property-Based Tests', () => {
649729 } ,
650730 ) ;
651731
732+ // Example: 12 levels of nesting => throws error
652733 test ( 'rejects objects exceeding depth limit of 10' , async ( ) => {
653734 const builder = new RequestBuilder ( baseConfig ) ;
654735 let deepObject : Record < string , unknown > = { value : 'test' } ;
@@ -664,9 +745,18 @@ describe('RequestBuilder - Property-Based Tests', () => {
664745 } ) ;
665746 } ) ;
666747
748+ /**
749+ * Body Type Handling
750+ *
751+ * Examples:
752+ * - bodyType: "json" => Content-Type: application/json, body: '{"test":"value"}'
753+ * - bodyType: "form" => Content-Type: application/x-www-form-urlencoded, body: "test=value"
754+ * - bodyType: "multipart-form" => body is FormData instance
755+ */
667756 describe ( 'Body Type Handling' , ( ) => {
668757 const bodyTypes = [ 'json' , 'form' , 'multipart-form' ] as const ;
669758
759+ // Example: buildFetchOptions({ test: "value" }) with bodyType "json" => valid options
670760 fcTest . prop (
671761 [
672762 fc . constantFrom ( ...bodyTypes ) ,
0 commit comments