@optimizely-opal/opal-tool-ocp-sdk 0.0.0-devmg.13 → 1.0.0-OCP-1441.1

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 (62) hide show
  1. package/README.md +108 -15
  2. package/dist/auth/AuthUtils.d.ts +26 -0
  3. package/dist/auth/AuthUtils.d.ts.map +1 -0
  4. package/dist/auth/AuthUtils.js +109 -0
  5. package/dist/auth/AuthUtils.js.map +1 -0
  6. package/dist/auth/AuthUtils.test.d.ts +2 -0
  7. package/dist/auth/AuthUtils.test.d.ts.map +1 -0
  8. package/dist/auth/AuthUtils.test.js +601 -0
  9. package/dist/auth/AuthUtils.test.js.map +1 -0
  10. package/dist/auth/TokenVerifier.d.ts.map +1 -1
  11. package/dist/auth/TokenVerifier.js +0 -1
  12. package/dist/auth/TokenVerifier.js.map +1 -1
  13. package/dist/auth/TokenVerifier.test.js +9 -0
  14. package/dist/auth/TokenVerifier.test.js.map +1 -1
  15. package/dist/function/GlobalToolFunction.d.ts +27 -0
  16. package/dist/function/GlobalToolFunction.d.ts.map +1 -0
  17. package/dist/function/GlobalToolFunction.js +53 -0
  18. package/dist/function/GlobalToolFunction.js.map +1 -0
  19. package/dist/function/GlobalToolFunction.test.d.ts +2 -0
  20. package/dist/function/GlobalToolFunction.test.d.ts.map +1 -0
  21. package/dist/function/GlobalToolFunction.test.js +425 -0
  22. package/dist/function/GlobalToolFunction.test.js.map +1 -0
  23. package/dist/function/ToolFunction.d.ts +1 -2
  24. package/dist/function/ToolFunction.d.ts.map +1 -1
  25. package/dist/function/ToolFunction.js +2 -35
  26. package/dist/function/ToolFunction.js.map +1 -1
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/service/Service.d.ts +8 -7
  32. package/dist/service/Service.d.ts.map +1 -1
  33. package/dist/service/Service.js +13 -0
  34. package/dist/service/Service.js.map +1 -1
  35. package/dist/service/Service.test.js +86 -4
  36. package/dist/service/Service.test.js.map +1 -1
  37. package/dist/validation/ParameterValidator.d.ts +42 -0
  38. package/dist/validation/ParameterValidator.d.ts.map +1 -0
  39. package/dist/validation/ParameterValidator.js +122 -0
  40. package/dist/validation/ParameterValidator.js.map +1 -0
  41. package/dist/validation/ParameterValidator.test.d.ts +2 -0
  42. package/dist/validation/ParameterValidator.test.d.ts.map +1 -0
  43. package/dist/validation/ParameterValidator.test.js +282 -0
  44. package/dist/validation/ParameterValidator.test.js.map +1 -0
  45. package/package.json +3 -4
  46. package/src/auth/AuthUtils.test.ts +729 -0
  47. package/src/auth/AuthUtils.ts +117 -0
  48. package/src/auth/TokenVerifier.test.ts +11 -0
  49. package/src/auth/TokenVerifier.ts +0 -1
  50. package/src/function/GlobalToolFunction.test.ts +505 -0
  51. package/src/function/GlobalToolFunction.ts +56 -0
  52. package/src/function/ToolFunction.ts +3 -41
  53. package/src/index.ts +1 -0
  54. package/src/service/Service.test.ts +126 -12
  55. package/src/service/Service.ts +47 -9
  56. package/src/validation/ParameterValidator.test.ts +341 -0
  57. package/src/validation/ParameterValidator.ts +153 -0
  58. package/dist/function/ToolFunction.test.d.ts +0 -2
  59. package/dist/function/ToolFunction.test.d.ts.map +0 -1
  60. package/dist/function/ToolFunction.test.js +0 -314
  61. package/dist/function/ToolFunction.test.js.map +0 -1
  62. package/src/function/ToolFunction.test.ts +0 -374
@@ -1,7 +1,5 @@
1
- import { Function, Response, amendLogContext, getAppContext, logger } from '@zaiusinc/app-sdk';
1
+ import { Function, Response, amendLogContext } from '@zaiusinc/app-sdk';
2
2
  import { toolsService } from '../service/Service';
3
- import { getTokenVerifier } from '../auth/TokenVerifier';
4
- import { OptiIdAuthData } from '../types/Models';
5
3
 
6
4
  /**
7
5
  * Abstract base class for tool-based function execution
@@ -41,45 +39,9 @@ export abstract class ToolFunction extends Function {
41
39
  /**
42
40
  * Authenticate the incoming request by validating the OptiID token and organization ID
43
41
  *
44
- * @throws true if authentication succeeds
42
+ * @returns true if authentication succeeds
45
43
  */
46
44
  private async authorizeRequest(): Promise<boolean> {
47
- if (this.request.path === '/discovery' || this.request.path === '/ready') {
48
- return true;
49
- }
50
- const authData = this.request.bodyJSON?.auth as OptiIdAuthData;
51
- const accessToken = authData?.credentials?.access_token;
52
- if (!accessToken || authData?.provider?.toLowerCase() !== 'optiid') {
53
- logger.error('OptiID token is required but not provided');
54
- return false;
55
- }
56
-
57
- const customerId = authData.credentials?.customer_id;
58
- if (!customerId) {
59
- logger.error('Organisation ID is required but not provided');
60
- return false;
61
- }
62
-
63
- const appOrganisationId = getAppContext().account?.organizationId;
64
- if (customerId !== appOrganisationId) {
65
- logger.error(`Invalid organisation ID: expected ${appOrganisationId}, received ${customerId}`);
66
- return false;
67
- }
68
-
69
- return await this.validateAccessToken(accessToken);
45
+ return true;
70
46
  }
71
-
72
- private async validateAccessToken(accessToken: string | undefined): Promise<boolean> {
73
- try {
74
- if (!accessToken) {
75
- return false;
76
- }
77
- const tokenVerifier = await getTokenVerifier();
78
- return await tokenVerifier.verify(accessToken);
79
- } catch (error) {
80
- logger.error('OptiID token validation failed:', error);
81
- return false;
82
- }
83
- }
84
-
85
47
  }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './function/ToolFunction';
2
+ export * from './function/GlobalToolFunction';
2
3
  export * from './types/Models';
3
4
  export * from './decorator/Decorator';
4
5
  export * from './auth/TokenVerifier';
@@ -522,15 +522,25 @@ describe('ToolsService', () => {
522
522
 
523
523
  describe('edge cases', () => {
524
524
  it('should handle request with null bodyJSON', async () => {
525
+ // Create a tool without required parameters
526
+ const toolWithoutRequiredParams = {
527
+ name: 'no_required_params_tool',
528
+ description: 'Tool without required parameters',
529
+ handler: jest.fn().mockResolvedValue({ result: 'success' }),
530
+ parameters: [], // No parameters defined
531
+ endpoint: '/no-required-params-tool'
532
+ };
533
+
525
534
  toolsService.registerTool(
526
- mockTool.name,
527
- mockTool.description,
528
- mockTool.handler,
529
- mockTool.parameters,
530
- mockTool.endpoint
535
+ toolWithoutRequiredParams.name,
536
+ toolWithoutRequiredParams.description,
537
+ toolWithoutRequiredParams.handler,
538
+ toolWithoutRequiredParams.parameters,
539
+ toolWithoutRequiredParams.endpoint
531
540
  );
532
541
 
533
542
  const requestWithNullBody = createMockRequest({
543
+ path: '/no-required-params-tool',
534
544
  bodyJSON: null,
535
545
  body: null
536
546
  });
@@ -538,19 +548,29 @@ describe('ToolsService', () => {
538
548
  const response = await toolsService.processRequest(requestWithNullBody, mockToolFunction);
539
549
 
540
550
  expect(response.status).toBe(200);
541
- expect(mockTool.handler).toHaveBeenCalledWith(mockToolFunction, null, undefined);
551
+ expect(toolWithoutRequiredParams.handler).toHaveBeenCalledWith(mockToolFunction, null, undefined);
542
552
  });
543
553
 
544
554
  it('should handle request with undefined bodyJSON', async () => {
555
+ // Create a tool without required parameters
556
+ const toolWithoutRequiredParams = {
557
+ name: 'no_required_params_tool_2',
558
+ description: 'Tool without required parameters',
559
+ handler: jest.fn().mockResolvedValue({ result: 'success' }),
560
+ parameters: [], // No parameters defined
561
+ endpoint: '/no-required-params-tool-2'
562
+ };
563
+
545
564
  toolsService.registerTool(
546
- mockTool.name,
547
- mockTool.description,
548
- mockTool.handler,
549
- mockTool.parameters,
550
- mockTool.endpoint
565
+ toolWithoutRequiredParams.name,
566
+ toolWithoutRequiredParams.description,
567
+ toolWithoutRequiredParams.handler,
568
+ toolWithoutRequiredParams.parameters,
569
+ toolWithoutRequiredParams.endpoint
551
570
  );
552
571
 
553
572
  const requestWithUndefinedBody = createMockRequest({
573
+ path: '/no-required-params-tool-2',
554
574
  bodyJSON: undefined,
555
575
  body: undefined
556
576
  });
@@ -558,7 +578,7 @@ describe('ToolsService', () => {
558
578
  const response = await toolsService.processRequest(requestWithUndefinedBody, mockToolFunction);
559
579
 
560
580
  expect(response.status).toBe(200);
561
- expect(mockTool.handler).toHaveBeenCalledWith(mockToolFunction, undefined, undefined);
581
+ expect(toolWithoutRequiredParams.handler).toHaveBeenCalledWith(mockToolFunction, undefined, undefined);
562
582
  });
563
583
 
564
584
  it('should extract auth data from bodyJSON when body exists', async () => {
@@ -657,5 +677,99 @@ describe('ToolsService', () => {
657
677
  );
658
678
  });
659
679
  });
680
+
681
+ describe('parameter validation', () => {
682
+ beforeEach(() => {
683
+ jest.clearAllMocks();
684
+ });
685
+
686
+ it('should validate parameters and return 400 for invalid types', async () => {
687
+ // Register a tool with specific parameter types
688
+ const toolWithTypedParams = {
689
+ name: 'typed_tool',
690
+ description: 'Tool with typed parameters',
691
+ handler: jest.fn().mockResolvedValue({ result: 'success' }),
692
+ parameters: [
693
+ new Parameter('name', ParameterType.String, 'User name', true),
694
+ new Parameter('age', ParameterType.Integer, 'User age', true),
695
+ new Parameter('active', ParameterType.Boolean, 'Is active', false)
696
+ ],
697
+ endpoint: '/typed-tool'
698
+ };
699
+
700
+ toolsService.registerTool(
701
+ toolWithTypedParams.name,
702
+ toolWithTypedParams.description,
703
+ toolWithTypedParams.handler,
704
+ toolWithTypedParams.parameters,
705
+ toolWithTypedParams.endpoint
706
+ );
707
+
708
+ // Send invalid parameter types
709
+ const invalidRequest = createMockRequest({
710
+ path: '/typed-tool',
711
+ bodyJSON: {
712
+ parameters: {
713
+ name: 123, // should be string
714
+ age: '25', // should be integer
715
+ active: 'true' // should be boolean
716
+ }
717
+ }
718
+ });
719
+
720
+ const response = await toolsService.processRequest(invalidRequest, mockToolFunction);
721
+
722
+ expect(response.status).toBe(400);
723
+
724
+ // Expect simplified error format
725
+ expect(response.bodyJSON).toHaveProperty('errors');
726
+ expect(response.bodyJSON.errors).toHaveLength(3);
727
+
728
+ // Check error structure - only field and message
729
+ const errors = response.bodyJSON.errors;
730
+ expect(errors[0]).toHaveProperty('field', 'name');
731
+ expect(errors[0]).toHaveProperty('message', "Parameter 'name' must be a string, but received number");
732
+
733
+ // Verify the handler was not called
734
+ expect(toolWithTypedParams.handler).not.toHaveBeenCalled();
735
+ });
736
+
737
+ it('should skip validation for tools with no parameter definitions', async () => {
738
+ const toolWithoutParams = {
739
+ name: 'no_params_tool',
740
+ description: 'Tool without parameters',
741
+ handler: jest.fn().mockResolvedValue({ result: 'success' }),
742
+ parameters: [], // No parameters defined
743
+ endpoint: '/no-params-tool'
744
+ };
745
+
746
+ toolsService.registerTool(
747
+ toolWithoutParams.name,
748
+ toolWithoutParams.description,
749
+ toolWithoutParams.handler,
750
+ toolWithoutParams.parameters,
751
+ toolWithoutParams.endpoint
752
+ );
753
+
754
+ // Send request with any data (should be ignored)
755
+ const request = createMockRequest({
756
+ path: '/no-params-tool',
757
+ bodyJSON: {
758
+ parameters: {
759
+ unexpected: 'value'
760
+ }
761
+ }
762
+ });
763
+
764
+ const response = await toolsService.processRequest(request, mockToolFunction);
765
+
766
+ expect(response.status).toBe(200);
767
+ expect(toolWithoutParams.handler).toHaveBeenCalledWith(
768
+ mockToolFunction,
769
+ { unexpected: 'value' },
770
+ undefined
771
+ );
772
+ });
773
+ });
660
774
  });
661
775
  });
@@ -3,14 +3,14 @@ import { AuthRequirement, Parameter } from '../types/Models';
3
3
  import * as App from '@zaiusinc/app-sdk';
4
4
  import { logger } from '@zaiusinc/app-sdk';
5
5
  import { ToolFunction } from '../function/ToolFunction';
6
+ import { GlobalToolFunction } from '../function/GlobalToolFunction';
7
+ import { ParameterValidator } from '../validation/ParameterValidator';
6
8
 
7
9
  /**
8
10
  * Default OptiID authentication requirement that will be enforced for all tools
9
11
  */
10
12
  const DEFAULT_OPTIID_AUTH = new AuthRequirement('OptiID', 'default', true);
11
13
 
12
-
13
-
14
14
  /**
15
15
  * Result type for interaction handlers
16
16
  */
@@ -28,7 +28,11 @@ export class Interaction<TAuthData> {
28
28
  public constructor(
29
29
  public name: string,
30
30
  public endpoint: string,
31
- public handler: (functionContext: ToolFunction, data: unknown, authData?: TAuthData) => Promise<InteractionResult>
31
+ public handler: (
32
+ functionContext: ToolFunction | GlobalToolFunction,
33
+ data: unknown,
34
+ authData?: TAuthData
35
+ ) => Promise<InteractionResult>
32
36
  ) {}
33
37
  }
34
38
 
@@ -55,7 +59,11 @@ export class Tool<TAuthData> {
55
59
  public description: string,
56
60
  public parameters: Parameter[],
57
61
  public endpoint: string,
58
- public handler: (functionContext: ToolFunction, params: unknown, authData?: TAuthData) => Promise<unknown>,
62
+ public handler: (
63
+ functionContext: ToolFunction | GlobalToolFunction,
64
+ params: unknown,
65
+ authData?: TAuthData
66
+ ) => Promise<unknown>,
59
67
  public authRequirements: AuthRequirement[] = [DEFAULT_OPTIID_AUTH]
60
68
  ) {}
61
69
 
@@ -108,14 +116,25 @@ export class ToolsService {
108
116
  public registerTool<TAuthData>(
109
117
  name: string,
110
118
  description: string,
111
- handler: (functionContext: ToolFunction, params: unknown, authData?: TAuthData) => Promise<unknown>,
119
+ handler: (
120
+ functionContext: ToolFunction | GlobalToolFunction,
121
+ params: unknown,
122
+ authData?: TAuthData
123
+ ) => Promise<unknown>,
112
124
  parameters: Parameter[],
113
125
  endpoint: string,
114
126
  authRequirements?: AuthRequirement[]
115
127
  ): void {
116
128
  // Enforce OptiID authentication for all tools
117
129
  const enforcedAuthRequirements = this.enforceOptiIdAuth(authRequirements);
118
- const func = new Tool<TAuthData>(name, description, parameters, endpoint, handler, enforcedAuthRequirements);
130
+ const func = new Tool<TAuthData>(
131
+ name,
132
+ description,
133
+ parameters,
134
+ endpoint,
135
+ handler,
136
+ enforcedAuthRequirements
137
+ );
119
138
  this.functions.set(endpoint, func);
120
139
  }
121
140
 
@@ -127,15 +146,21 @@ export class ToolsService {
127
146
  */
128
147
  public registerInteraction<TAuthData>(
129
148
  name: string,
130
- handler: (functionContext: ToolFunction, data: unknown, authData?: TAuthData) => Promise<InteractionResult>,
149
+ handler: (
150
+ functionContext: ToolFunction | GlobalToolFunction,
151
+ data: unknown,
152
+ authData?: TAuthData
153
+ ) => Promise<InteractionResult>,
131
154
  endpoint: string
132
155
  ): void {
133
156
  const func = new Interaction<TAuthData>(name, endpoint, handler);
134
157
  this.interactions.set(endpoint, func);
135
158
  }
136
159
 
137
- public async processRequest(req: App.Request,
138
- functionContext: ToolFunction): Promise<App.Response> {
160
+ public async processRequest(
161
+ req: App.Request,
162
+ functionContext: ToolFunction | GlobalToolFunction
163
+ ): Promise<App.Response> {
139
164
  if (req.path === '/discovery') {
140
165
  return new App.Response(200, { functions: Array.from(this.functions.values()).map((f) => f.toJSON()) });
141
166
  } else {
@@ -149,6 +174,19 @@ export class ToolsService {
149
174
  params = req.bodyJSON;
150
175
  }
151
176
 
177
+ // Validate parameters before calling the handler (only if tool has parameter definitions)
178
+ if (func.parameters && func.parameters.length > 0) {
179
+ const validationResult = ParameterValidator.validate(params, func.parameters);
180
+ if (!validationResult.isValid) {
181
+ return new App.Response(400, {
182
+ errors: validationResult.errors.map((error) => ({
183
+ field: error.parameter,
184
+ message: error.message
185
+ }))
186
+ });
187
+ }
188
+ }
189
+
152
190
  // Extract auth data from body JSON
153
191
  const authData = req.bodyJSON ? req.bodyJSON.auth : undefined;
154
192
 
@@ -0,0 +1,341 @@
1
+ import { ParameterValidator } from './ParameterValidator';
2
+ import { Parameter, ParameterType } from '../types/Models';
3
+
4
+ describe('ParameterValidator', () => {
5
+ describe('validate', () => {
6
+ it('should pass validation for valid parameters', () => {
7
+ const paramDefs = [
8
+ new Parameter('name', ParameterType.String, 'User name', true),
9
+ new Parameter('age', ParameterType.Integer, 'User age', false),
10
+ new Parameter('score', ParameterType.Number, 'User score', false),
11
+ new Parameter('active', ParameterType.Boolean, 'Is active', false),
12
+ new Parameter('tags', ParameterType.List, 'User tags', false),
13
+ new Parameter('config', ParameterType.Dictionary, 'User config', false)
14
+ ];
15
+
16
+ const validParams = {
17
+ name: 'John Doe',
18
+ age: 25,
19
+ score: 85.5,
20
+ active: true,
21
+ tags: ['admin', 'user'],
22
+ config: { theme: 'dark', language: 'en' }
23
+ };
24
+
25
+ const result = ParameterValidator.validate(validParams, paramDefs);
26
+
27
+ expect(result.isValid).toBe(true);
28
+ expect(result.errors).toHaveLength(0);
29
+ });
30
+
31
+ it('should fail validation for missing required parameters', () => {
32
+ const paramDefs = [
33
+ new Parameter('name', ParameterType.String, 'User name', true),
34
+ new Parameter('email', ParameterType.String, 'User email', true)
35
+ ];
36
+
37
+ const params = {
38
+ name: 'John Doe'
39
+ // email is missing
40
+ };
41
+
42
+ const result = ParameterValidator.validate(params, paramDefs);
43
+
44
+ expect(result.isValid).toBe(false);
45
+ expect(result.errors).toHaveLength(1);
46
+ expect(result.errors[0]).toEqual({
47
+ parameter: 'email',
48
+ message: "Required parameter 'email' is missing"
49
+ });
50
+ });
51
+
52
+ it('should fail validation for wrong parameter types', () => {
53
+ const paramDefs = [
54
+ new Parameter('name', ParameterType.String, 'User name', true),
55
+ new Parameter('age', ParameterType.Integer, 'User age', true),
56
+ new Parameter('active', ParameterType.Boolean, 'Is active', true),
57
+ new Parameter('tags', ParameterType.List, 'User tags', true),
58
+ new Parameter('config', ParameterType.Dictionary, 'User config', true)
59
+ ];
60
+
61
+ const invalidParams = {
62
+ name: 123, // should be string
63
+ age: '25', // should be integer
64
+ active: 'true', // should be boolean
65
+ tags: 'tag1,tag2', // should be array
66
+ config: 'invalid' // should be object
67
+ };
68
+
69
+ const result = ParameterValidator.validate(invalidParams, paramDefs);
70
+
71
+ expect(result.isValid).toBe(false);
72
+ expect(result.errors).toHaveLength(5);
73
+
74
+ expect(result.errors[0].parameter).toBe('name');
75
+ expect(result.errors[0].message).toContain('must be a string');
76
+
77
+ expect(result.errors[1].parameter).toBe('age');
78
+ expect(result.errors[1].message).toContain('must be an integer');
79
+
80
+ expect(result.errors[2].parameter).toBe('active');
81
+ expect(result.errors[2].message).toContain('must be a boolean');
82
+
83
+ expect(result.errors[3].parameter).toBe('tags');
84
+ expect(result.errors[3].message).toContain('must be an array');
85
+
86
+ expect(result.errors[4].parameter).toBe('config');
87
+ expect(result.errors[4].message).toContain('must be an object');
88
+ });
89
+
90
+ it('should handle null/undefined parameters correctly', () => {
91
+ const paramDefs = [
92
+ new Parameter('required', ParameterType.String, 'Required param', true),
93
+ new Parameter('optional', ParameterType.String, 'Optional param', false)
94
+ ];
95
+
96
+ const result1 = ParameterValidator.validate(null, paramDefs);
97
+ expect(result1.isValid).toBe(false);
98
+ expect(result1.errors).toHaveLength(1);
99
+ expect(result1.errors[0].parameter).toBe('required');
100
+
101
+ const result2 = ParameterValidator.validate(undefined, paramDefs);
102
+ expect(result2.isValid).toBe(false);
103
+ expect(result2.errors).toHaveLength(1);
104
+ expect(result2.errors[0].parameter).toBe('required');
105
+ });
106
+
107
+ it('should allow optional parameters to be missing', () => {
108
+ const paramDefs = [
109
+ new Parameter('name', ParameterType.String, 'User name', true),
110
+ new Parameter('age', ParameterType.Integer, 'User age', false)
111
+ ];
112
+
113
+ const params = {
114
+ name: 'John Doe'
115
+ // age is optional and missing
116
+ };
117
+
118
+ const result = ParameterValidator.validate(params, paramDefs);
119
+
120
+ expect(result.isValid).toBe(true);
121
+ expect(result.errors).toHaveLength(0);
122
+ });
123
+
124
+ it('should distinguish between integer and number types', () => {
125
+ const paramDefs = [
126
+ new Parameter('count', ParameterType.Integer, 'Item count', true),
127
+ new Parameter('score', ParameterType.Number, 'Score value', true)
128
+ ];
129
+
130
+ const params1 = {
131
+ count: 25.5, // should be integer, not float
132
+ score: 85.5 // number is fine
133
+ };
134
+
135
+ const result1 = ParameterValidator.validate(params1, paramDefs);
136
+ expect(result1.isValid).toBe(false);
137
+ expect(result1.errors).toHaveLength(1);
138
+ expect(result1.errors[0].parameter).toBe('count');
139
+ expect(result1.errors[0].message).toContain('must be an integer');
140
+
141
+ const params2 = {
142
+ count: 25, // integer is fine
143
+ score: 85.5 // number is fine
144
+ };
145
+
146
+ const result2 = ParameterValidator.validate(params2, paramDefs);
147
+ expect(result2.isValid).toBe(true);
148
+ expect(result2.errors).toHaveLength(0);
149
+ });
150
+
151
+ it('should handle array vs object distinction', () => {
152
+ const paramDefs = [
153
+ new Parameter('tags', ParameterType.List, 'Tag list', true),
154
+ new Parameter('config', ParameterType.Dictionary, 'Config object', true)
155
+ ];
156
+
157
+ const params = {
158
+ tags: { 0: 'tag1', 1: 'tag2' }, // object that looks like array
159
+ config: ['key1', 'key2'] // array instead of object
160
+ };
161
+
162
+ const result = ParameterValidator.validate(params, paramDefs);
163
+ expect(result.isValid).toBe(false);
164
+ expect(result.errors).toHaveLength(2);
165
+
166
+ expect(result.errors[0].parameter).toBe('tags');
167
+ expect(result.errors[0].message).toContain('must be an array');
168
+
169
+ expect(result.errors[1].parameter).toBe('config');
170
+ expect(result.errors[1].message).toContain('must be an object');
171
+ });
172
+
173
+ it('should handle null values for optional parameters', () => {
174
+ const paramDefs = [
175
+ new Parameter('name', ParameterType.String, 'User name', true),
176
+ new Parameter('age', ParameterType.Integer, 'User age', false)
177
+ ];
178
+
179
+ const params = {
180
+ name: 'John Doe',
181
+ age: null // null for optional parameter should be allowed
182
+ };
183
+
184
+ const result = ParameterValidator.validate(params, paramDefs);
185
+ expect(result.isValid).toBe(true);
186
+ expect(result.errors).toHaveLength(0);
187
+ });
188
+
189
+ it('should handle edge cases for number validation', () => {
190
+ const paramDefs = [
191
+ new Parameter('score', ParameterType.Number, 'Score value', true)
192
+ ];
193
+
194
+ // Test NaN
195
+ const params1 = { score: NaN };
196
+ const result1 = ParameterValidator.validate(params1, paramDefs);
197
+ expect(result1.isValid).toBe(false);
198
+ expect(result1.errors[0].message).toContain('must be a number');
199
+
200
+ // Test Infinity
201
+ const params2 = { score: Infinity };
202
+ const result2 = ParameterValidator.validate(params2, paramDefs);
203
+ expect(result2.isValid).toBe(true);
204
+ expect(result2.errors).toHaveLength(0);
205
+
206
+ // Test negative numbers
207
+ const params3 = { score: -42.5 };
208
+ const result3 = ParameterValidator.validate(params3, paramDefs);
209
+ expect(result3.isValid).toBe(true);
210
+ expect(result3.errors).toHaveLength(0);
211
+ });
212
+
213
+ it('should handle edge cases for integer validation', () => {
214
+ const paramDefs = [
215
+ new Parameter('count', ParameterType.Integer, 'Item count', true)
216
+ ];
217
+
218
+ // Test negative integers
219
+ const params1 = { count: -5 };
220
+ const result1 = ParameterValidator.validate(params1, paramDefs);
221
+ expect(result1.isValid).toBe(true);
222
+ expect(result1.errors).toHaveLength(0);
223
+
224
+ // Test zero
225
+ const params2 = { count: 0 };
226
+ const result2 = ParameterValidator.validate(params2, paramDefs);
227
+ expect(result2.isValid).toBe(true);
228
+ expect(result2.errors).toHaveLength(0);
229
+
230
+ // Test very large integers
231
+ const params3 = { count: Number.MAX_SAFE_INTEGER };
232
+ const result3 = ParameterValidator.validate(params3, paramDefs);
233
+ expect(result3.isValid).toBe(true);
234
+ expect(result3.errors).toHaveLength(0);
235
+ });
236
+
237
+ it('should handle empty arrays and objects', () => {
238
+ const paramDefs = [
239
+ new Parameter('tags', ParameterType.List, 'Tag list', true),
240
+ new Parameter('config', ParameterType.Dictionary, 'Config object', true)
241
+ ];
242
+
243
+ const params = {
244
+ tags: [], // empty array should be valid
245
+ config: {} // empty object should be valid
246
+ };
247
+
248
+ const result = ParameterValidator.validate(params, paramDefs);
249
+ expect(result.isValid).toBe(true);
250
+ expect(result.errors).toHaveLength(0);
251
+ });
252
+
253
+ it('should handle special string values', () => {
254
+ const paramDefs = [
255
+ new Parameter('text', ParameterType.String, 'Text value', true)
256
+ ];
257
+
258
+ // Test empty string
259
+ const params1 = { text: '' };
260
+ const result1 = ParameterValidator.validate(params1, paramDefs);
261
+ expect(result1.isValid).toBe(true);
262
+ expect(result1.errors).toHaveLength(0);
263
+
264
+ // Test string with special characters
265
+ const params2 = { text: 'Hello\nWorld\t!' };
266
+ const result2 = ParameterValidator.validate(params2, paramDefs);
267
+ expect(result2.isValid).toBe(true);
268
+ expect(result2.errors).toHaveLength(0);
269
+ });
270
+
271
+ it('should handle multiple validation errors', () => {
272
+ const paramDefs = [
273
+ new Parameter('name', ParameterType.String, 'User name', true),
274
+ new Parameter('age', ParameterType.Integer, 'User age', true),
275
+ new Parameter('email', ParameterType.String, 'User email', true)
276
+ ];
277
+
278
+ const params = {
279
+ name: 123, // wrong type
280
+ age: '25' // wrong type
281
+ // email is missing
282
+ };
283
+
284
+ const result = ParameterValidator.validate(params, paramDefs);
285
+ expect(result.isValid).toBe(false);
286
+ expect(result.errors).toHaveLength(3);
287
+
288
+ expect(result.errors.some((e) => e.parameter === 'name')).toBe(true);
289
+ expect(result.errors.some((e) => e.parameter === 'age')).toBe(true);
290
+ expect(result.errors.some((e) => e.parameter === 'email')).toBe(true);
291
+ });
292
+
293
+ it('should handle tools with no parameter definitions', () => {
294
+ const result = ParameterValidator.validate({ someParam: 'value' }, []);
295
+ expect(result.isValid).toBe(true);
296
+ expect(result.errors).toHaveLength(0);
297
+ });
298
+
299
+ it('should handle extra parameters not in definition', () => {
300
+ const paramDefs = [
301
+ new Parameter('name', ParameterType.String, 'User name', true)
302
+ ];
303
+
304
+ const params = {
305
+ name: 'John Doe',
306
+ extraParam: 'should be ignored'
307
+ };
308
+
309
+ const result = ParameterValidator.validate(params, paramDefs);
310
+ expect(result.isValid).toBe(true);
311
+ expect(result.errors).toHaveLength(0);
312
+ });
313
+
314
+ it('should handle nested objects and arrays', () => {
315
+ const paramDefs = [
316
+ new Parameter('config', ParameterType.Dictionary, 'Config object', true),
317
+ new Parameter('matrix', ParameterType.List, 'Matrix data', true)
318
+ ];
319
+
320
+ const params = {
321
+ config: {
322
+ nested: {
323
+ deep: {
324
+ value: 'test'
325
+ }
326
+ },
327
+ array: [1, 2, 3]
328
+ },
329
+ matrix: [
330
+ [1, 2, 3],
331
+ [4, 5, 6],
332
+ { nested: 'object in array' }
333
+ ]
334
+ };
335
+
336
+ const result = ParameterValidator.validate(params, paramDefs);
337
+ expect(result.isValid).toBe(true);
338
+ expect(result.errors).toHaveLength(0);
339
+ });
340
+ });
341
+ });