Skip to content

Commit 690d052

Browse files
committed
docs(test): add documentation comments to PBT explaining replaced tests
- Add JSDoc comments explaining what example-based tests were replaced - Add inline comments with concrete examples for each property test - Restore 'should handle arrays correctly within objects' test for clarity - Keep concrete examples as documentation alongside PBT
1 parent 12884b0 commit 690d052

File tree

3 files changed

+125
-0
lines changed

3 files changed

+125
-0
lines changed

src/requestBuilder.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
*/
477514
describe('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-zA-Z0-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),

src/utils/array.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,29 @@
11
import { fc, test as fcTest } from '@fast-check/vitest';
22
import { toArray } from './array';
33

4+
/**
5+
* Property-Based Tests for toArray utility
6+
*
7+
* These tests verify the function's behavior for ANY valid input,
8+
* replacing example-based tests like:
9+
*
10+
* - toArray([1, 2, 3]) === [1, 2, 3] (same reference)
11+
* - toArray("hello") === ["hello"]
12+
* - toArray(42) === [42]
13+
* - toArray({ key: "value" }) === [{ key: "value" }]
14+
* - toArray(null) === []
15+
* - toArray(undefined) === []
16+
*/
417
describe('toArray - Property-Based Tests', () => {
18+
// Example: toArray([1, 2, 3]) returns the exact same array instance, not a copy
519
fcTest.prop([fc.array(fc.anything())], { numRuns: 100 })(
620
'array input returns the same array reference',
721
(arr) => {
822
expect(toArray(arr)).toBe(arr);
923
},
1024
);
1125

26+
// Example: toArray("hello") => ["hello"], toArray(42) => [42], toArray({a:1}) => [{a:1}]
1227
fcTest.prop([fc.anything().filter((x) => !Array.isArray(x) && x != null)], { numRuns: 100 })(
1328
'non-array non-nullish input returns single-element array',
1429
(value) => {
@@ -18,13 +33,15 @@ describe('toArray - Property-Based Tests', () => {
1833
},
1934
);
2035

36+
// Example: toArray(null) => [], toArray(undefined) => []
2137
fcTest.prop([fc.constantFrom(null, undefined)], { numRuns: 10 })(
2238
'null or undefined returns empty array',
2339
(value) => {
2440
expect(toArray(value)).toEqual([]);
2541
},
2642
);
2743

44+
// Invariant: no matter what input, output is always an array
2845
fcTest.prop([fc.anything()], { numRuns: 100 })('result is always an array', (value) => {
2946
expect(Array.isArray(toArray(value))).toBe(true);
3047
});

src/utils/tfidf-index.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ describe('TF-IDF Index - Tool Name Scenarios', () => {
8181
});
8282
});
8383

84+
/**
85+
* Property-Based Tests for TfidfIndex
86+
*
87+
* These tests verify invariants that must hold for ANY valid input,
88+
* replacing the following example-based tests:
89+
*
90+
* - Score Validation: scores like 0.7071, 0.5, 1.0 are always in [0, 1]
91+
* - Edge Cases: empty query "" returns [], query with no matches returns []
92+
* - Case Sensitivity: "Alpha" and "ALPHA" and "alpha" return same results
93+
* - Search Limits: search("term", 5) returns at most 5 results
94+
*/
8495
describe('TF-IDF Index - Property-Based Tests', () => {
8596
const documentArbitrary = fc.record({
8697
id: fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
@@ -93,6 +104,7 @@ describe('TF-IDF Index - Property-Based Tests', () => {
93104
.array(fc.stringMatching(/^[a-zA-Z][a-zA-Z0-9]*$/), { minLength: 1, maxLength: 5 })
94105
.map((words) => words.join(' '));
95106

107+
// Example: search("alpha") on any corpus returns scores like 0.0, 0.5, 1.0 - never 1.5 or -0.1
96108
fcTest.prop([corpusArbitrary, queryArbitrary], { numRuns: 100 })(
97109
'scores are always within [0, 1] range',
98110
(corpus, query) => {
@@ -107,6 +119,7 @@ describe('TF-IDF Index - Property-Based Tests', () => {
107119
},
108120
);
109121

122+
// Example: [{ score: 0.9 }, { score: 0.7 }, { score: 0.3 }] - always descending
110123
fcTest.prop([corpusArbitrary, queryArbitrary], { numRuns: 100 })(
111124
'results are always sorted by score in descending order',
112125
(corpus, query) => {
@@ -120,6 +133,7 @@ describe('TF-IDF Index - Property-Based Tests', () => {
120133
},
121134
);
122135

136+
// Example: search("term", 3) with 10 matching docs returns only 3 results
123137
fcTest.prop([corpusArbitrary, queryArbitrary, fc.integer({ min: 1, max: 50 })], { numRuns: 100 })(
124138
'search returns at most k results',
125139
(corpus, query, k) => {
@@ -131,6 +145,7 @@ describe('TF-IDF Index - Property-Based Tests', () => {
131145
},
132146
);
133147

148+
// Example: search("Alpha"), search("ALPHA"), search("alpha") all return identical results
134149
fcTest.prop([corpusArbitrary, queryArbitrary], { numRuns: 100 })(
135150
'search is case-insensitive',
136151
(corpus, query) => {
@@ -148,6 +163,7 @@ describe('TF-IDF Index - Property-Based Tests', () => {
148163
},
149164
);
150165

166+
// Example: index.build([]) then search("anything") returns []
151167
fcTest.prop([queryArbitrary], { numRuns: 50 })('empty corpus returns empty results', (query) => {
152168
const index = new TfidfIndex();
153169
index.build([]);
@@ -156,6 +172,7 @@ describe('TF-IDF Index - Property-Based Tests', () => {
156172
expect(results).toHaveLength(0);
157173
});
158174

175+
// Example: corpus has ids ["doc1", "doc2"], results only contain "doc1" or "doc2"
159176
fcTest.prop([corpusArbitrary, queryArbitrary], { numRuns: 100 })(
160177
'result IDs are from the indexed corpus',
161178
(corpus, query) => {
@@ -170,6 +187,7 @@ describe('TF-IDF Index - Property-Based Tests', () => {
170187
},
171188
);
172189

190+
// Example: same corpus + same query always produces identical results
173191
fcTest.prop([corpusArbitrary, queryArbitrary], { numRuns: 50 })(
174192
'search is deterministic',
175193
(corpus, query) => {

0 commit comments

Comments
 (0)