@vinkius-core/mcp-fusion 0.1.0

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 (211) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +391 -0
  3. package/dist/src/AbstractBase.d.ts +24 -0
  4. package/dist/src/AbstractBase.d.ts.map +1 -0
  5. package/dist/src/AbstractBase.js +63 -0
  6. package/dist/src/AbstractBase.js.map +1 -0
  7. package/dist/src/AbstractLeaf.d.ts +12 -0
  8. package/dist/src/AbstractLeaf.d.ts.map +1 -0
  9. package/dist/src/AbstractLeaf.js +32 -0
  10. package/dist/src/AbstractLeaf.js.map +1 -0
  11. package/dist/src/Annotations.d.ts +15 -0
  12. package/dist/src/Annotations.d.ts.map +1 -0
  13. package/dist/src/Annotations.js +29 -0
  14. package/dist/src/Annotations.js.map +1 -0
  15. package/dist/src/Group.d.ts +32 -0
  16. package/dist/src/Group.d.ts.map +1 -0
  17. package/dist/src/Group.js +131 -0
  18. package/dist/src/Group.js.map +1 -0
  19. package/dist/src/Icon.d.ts +19 -0
  20. package/dist/src/Icon.d.ts.map +1 -0
  21. package/dist/src/Icon.js +33 -0
  22. package/dist/src/Icon.js.map +1 -0
  23. package/dist/src/Prompt.d.ts +11 -0
  24. package/dist/src/Prompt.d.ts.map +1 -0
  25. package/dist/src/Prompt.js +28 -0
  26. package/dist/src/Prompt.js.map +1 -0
  27. package/dist/src/PromptArgument.d.ts +10 -0
  28. package/dist/src/PromptArgument.d.ts.map +1 -0
  29. package/dist/src/PromptArgument.js +20 -0
  30. package/dist/src/PromptArgument.js.map +1 -0
  31. package/dist/src/Resource.d.ts +19 -0
  32. package/dist/src/Resource.d.ts.map +1 -0
  33. package/dist/src/Resource.js +34 -0
  34. package/dist/src/Resource.js.map +1 -0
  35. package/dist/src/Role.d.ts +5 -0
  36. package/dist/src/Role.d.ts.map +1 -0
  37. package/dist/src/Role.js +6 -0
  38. package/dist/src/Role.js.map +1 -0
  39. package/dist/src/Tool.d.ts +16 -0
  40. package/dist/src/Tool.d.ts.map +1 -0
  41. package/dist/src/Tool.js +28 -0
  42. package/dist/src/Tool.js.map +1 -0
  43. package/dist/src/ToolAnnotations.d.ts +23 -0
  44. package/dist/src/ToolAnnotations.d.ts.map +1 -0
  45. package/dist/src/ToolAnnotations.js +44 -0
  46. package/dist/src/ToolAnnotations.js.map +1 -0
  47. package/dist/src/converters/GroupConverter.d.ts +14 -0
  48. package/dist/src/converters/GroupConverter.d.ts.map +1 -0
  49. package/dist/src/converters/GroupConverter.js +13 -0
  50. package/dist/src/converters/GroupConverter.js.map +1 -0
  51. package/dist/src/converters/PromptConverter.d.ts +14 -0
  52. package/dist/src/converters/PromptConverter.d.ts.map +1 -0
  53. package/dist/src/converters/PromptConverter.js +13 -0
  54. package/dist/src/converters/PromptConverter.js.map +1 -0
  55. package/dist/src/converters/ResourceConverter.d.ts +14 -0
  56. package/dist/src/converters/ResourceConverter.d.ts.map +1 -0
  57. package/dist/src/converters/ResourceConverter.js +13 -0
  58. package/dist/src/converters/ResourceConverter.js.map +1 -0
  59. package/dist/src/converters/ToolAnnotationsConverter.d.ts +16 -0
  60. package/dist/src/converters/ToolAnnotationsConverter.d.ts.map +1 -0
  61. package/dist/src/converters/ToolAnnotationsConverter.js +23 -0
  62. package/dist/src/converters/ToolAnnotationsConverter.js.map +1 -0
  63. package/dist/src/converters/ToolConverter.d.ts +14 -0
  64. package/dist/src/converters/ToolConverter.d.ts.map +1 -0
  65. package/dist/src/converters/ToolConverter.js +13 -0
  66. package/dist/src/converters/ToolConverter.js.map +1 -0
  67. package/dist/src/converters/index.d.ts +6 -0
  68. package/dist/src/converters/index.d.ts.map +1 -0
  69. package/dist/src/converters/index.js +6 -0
  70. package/dist/src/converters/index.js.map +1 -0
  71. package/dist/src/framework/GroupedToolBuilder.d.ts +137 -0
  72. package/dist/src/framework/GroupedToolBuilder.d.ts.map +1 -0
  73. package/dist/src/framework/GroupedToolBuilder.js +289 -0
  74. package/dist/src/framework/GroupedToolBuilder.js.map +1 -0
  75. package/dist/src/framework/ResponseHelper.d.ts +43 -0
  76. package/dist/src/framework/ResponseHelper.d.ts.map +1 -0
  77. package/dist/src/framework/ResponseHelper.js +49 -0
  78. package/dist/src/framework/ResponseHelper.js.map +1 -0
  79. package/dist/src/framework/ToolBuilder.d.ts +46 -0
  80. package/dist/src/framework/ToolBuilder.d.ts.map +1 -0
  81. package/dist/src/framework/ToolBuilder.js +2 -0
  82. package/dist/src/framework/ToolBuilder.js.map +1 -0
  83. package/dist/src/framework/ToolRegistry.d.ts +85 -0
  84. package/dist/src/framework/ToolRegistry.d.ts.map +1 -0
  85. package/dist/src/framework/ToolRegistry.js +153 -0
  86. package/dist/src/framework/ToolRegistry.js.map +1 -0
  87. package/dist/src/framework/index.d.ts +9 -0
  88. package/dist/src/framework/index.d.ts.map +1 -0
  89. package/dist/src/framework/index.js +8 -0
  90. package/dist/src/framework/index.js.map +1 -0
  91. package/dist/src/framework/strategies/AnnotationAggregator.d.ts +11 -0
  92. package/dist/src/framework/strategies/AnnotationAggregator.d.ts.map +1 -0
  93. package/dist/src/framework/strategies/AnnotationAggregator.js +25 -0
  94. package/dist/src/framework/strategies/AnnotationAggregator.js.map +1 -0
  95. package/dist/src/framework/strategies/DescriptionGenerator.d.ts +12 -0
  96. package/dist/src/framework/strategies/DescriptionGenerator.d.ts.map +1 -0
  97. package/dist/src/framework/strategies/DescriptionGenerator.js +70 -0
  98. package/dist/src/framework/strategies/DescriptionGenerator.js.map +1 -0
  99. package/dist/src/framework/strategies/MiddlewareCompiler.d.ts +13 -0
  100. package/dist/src/framework/strategies/MiddlewareCompiler.d.ts.map +1 -0
  101. package/dist/src/framework/strategies/MiddlewareCompiler.js +24 -0
  102. package/dist/src/framework/strategies/MiddlewareCompiler.js.map +1 -0
  103. package/dist/src/framework/strategies/SchemaGenerator.d.ts +15 -0
  104. package/dist/src/framework/strategies/SchemaGenerator.d.ts.map +1 -0
  105. package/dist/src/framework/strategies/SchemaGenerator.js +97 -0
  106. package/dist/src/framework/strategies/SchemaGenerator.js.map +1 -0
  107. package/dist/src/framework/strategies/SchemaUtils.d.ts +7 -0
  108. package/dist/src/framework/strategies/SchemaUtils.d.ts.map +1 -0
  109. package/dist/src/framework/strategies/SchemaUtils.js +17 -0
  110. package/dist/src/framework/strategies/SchemaUtils.js.map +1 -0
  111. package/dist/src/framework/strategies/ToonDescriptionGenerator.d.ts +3 -0
  112. package/dist/src/framework/strategies/ToonDescriptionGenerator.d.ts.map +1 -0
  113. package/dist/src/framework/strategies/ToonDescriptionGenerator.js +53 -0
  114. package/dist/src/framework/strategies/ToonDescriptionGenerator.js.map +1 -0
  115. package/dist/src/framework/strategies/Types.d.ts +34 -0
  116. package/dist/src/framework/strategies/Types.d.ts.map +1 -0
  117. package/dist/src/framework/strategies/Types.js +2 -0
  118. package/dist/src/framework/strategies/Types.js.map +1 -0
  119. package/dist/src/framework/strategies/index.d.ts +12 -0
  120. package/dist/src/framework/strategies/index.d.ts.map +1 -0
  121. package/dist/src/framework/strategies/index.js +11 -0
  122. package/dist/src/framework/strategies/index.js.map +1 -0
  123. package/dist/src/index.d.ts +15 -0
  124. package/dist/src/index.d.ts.map +1 -0
  125. package/dist/src/index.js +15 -0
  126. package/dist/src/index.js.map +1 -0
  127. package/dist/tests/AbstractBase.test.d.ts +2 -0
  128. package/dist/tests/AbstractBase.test.d.ts.map +1 -0
  129. package/dist/tests/AbstractBase.test.js +130 -0
  130. package/dist/tests/AbstractBase.test.js.map +1 -0
  131. package/dist/tests/AbstractLeaf.test.d.ts +2 -0
  132. package/dist/tests/AbstractLeaf.test.d.ts.map +1 -0
  133. package/dist/tests/AbstractLeaf.test.js +65 -0
  134. package/dist/tests/AbstractLeaf.test.js.map +1 -0
  135. package/dist/tests/Annotations.test.d.ts +2 -0
  136. package/dist/tests/Annotations.test.d.ts.map +1 -0
  137. package/dist/tests/Annotations.test.js +34 -0
  138. package/dist/tests/Annotations.test.js.map +1 -0
  139. package/dist/tests/BarrelExport.test.d.ts +2 -0
  140. package/dist/tests/BarrelExport.test.d.ts.map +1 -0
  141. package/dist/tests/BarrelExport.test.js +42 -0
  142. package/dist/tests/BarrelExport.test.js.map +1 -0
  143. package/dist/tests/Converters.test.d.ts +2 -0
  144. package/dist/tests/Converters.test.d.ts.map +1 -0
  145. package/dist/tests/Converters.test.js +193 -0
  146. package/dist/tests/Converters.test.js.map +1 -0
  147. package/dist/tests/Group.test.d.ts +2 -0
  148. package/dist/tests/Group.test.d.ts.map +1 -0
  149. package/dist/tests/Group.test.js +257 -0
  150. package/dist/tests/Group.test.js.map +1 -0
  151. package/dist/tests/Icon.test.d.ts +2 -0
  152. package/dist/tests/Icon.test.d.ts.map +1 -0
  153. package/dist/tests/Icon.test.js +44 -0
  154. package/dist/tests/Icon.test.js.map +1 -0
  155. package/dist/tests/Prompt.test.d.ts +2 -0
  156. package/dist/tests/Prompt.test.d.ts.map +1 -0
  157. package/dist/tests/Prompt.test.js +63 -0
  158. package/dist/tests/Prompt.test.js.map +1 -0
  159. package/dist/tests/PromptArgument.test.d.ts +2 -0
  160. package/dist/tests/PromptArgument.test.d.ts.map +1 -0
  161. package/dist/tests/PromptArgument.test.js +37 -0
  162. package/dist/tests/PromptArgument.test.js.map +1 -0
  163. package/dist/tests/Resource.test.d.ts +2 -0
  164. package/dist/tests/Resource.test.d.ts.map +1 -0
  165. package/dist/tests/Resource.test.js +61 -0
  166. package/dist/tests/Resource.test.js.map +1 -0
  167. package/dist/tests/Role.test.d.ts +2 -0
  168. package/dist/tests/Role.test.d.ts.map +1 -0
  169. package/dist/tests/Role.test.js +17 -0
  170. package/dist/tests/Role.test.js.map +1 -0
  171. package/dist/tests/Tool.test.d.ts +2 -0
  172. package/dist/tests/Tool.test.d.ts.map +1 -0
  173. package/dist/tests/Tool.test.js +62 -0
  174. package/dist/tests/Tool.test.js.map +1 -0
  175. package/dist/tests/ToolAnnotations.test.d.ts +2 -0
  176. package/dist/tests/ToolAnnotations.test.d.ts.map +1 -0
  177. package/dist/tests/ToolAnnotations.test.js +55 -0
  178. package/dist/tests/ToolAnnotations.test.js.map +1 -0
  179. package/dist/tests/framework/AdversarialQA.test.d.ts +2 -0
  180. package/dist/tests/framework/AdversarialQA.test.d.ts.map +1 -0
  181. package/dist/tests/framework/AdversarialQA.test.js +906 -0
  182. package/dist/tests/framework/AdversarialQA.test.js.map +1 -0
  183. package/dist/tests/framework/GroupedToolBuilder.test.d.ts +2 -0
  184. package/dist/tests/framework/GroupedToolBuilder.test.d.ts.map +1 -0
  185. package/dist/tests/framework/GroupedToolBuilder.test.js +712 -0
  186. package/dist/tests/framework/GroupedToolBuilder.test.js.map +1 -0
  187. package/dist/tests/framework/LargeScaleScenarios.test.d.ts +2 -0
  188. package/dist/tests/framework/LargeScaleScenarios.test.d.ts.map +1 -0
  189. package/dist/tests/framework/LargeScaleScenarios.test.js +1043 -0
  190. package/dist/tests/framework/LargeScaleScenarios.test.js.map +1 -0
  191. package/dist/tests/framework/McpServerAdapter.test.d.ts +2 -0
  192. package/dist/tests/framework/McpServerAdapter.test.d.ts.map +1 -0
  193. package/dist/tests/framework/McpServerAdapter.test.js +380 -0
  194. package/dist/tests/framework/McpServerAdapter.test.js.map +1 -0
  195. package/dist/tests/framework/ResponseHelper.test.d.ts +2 -0
  196. package/dist/tests/framework/ResponseHelper.test.d.ts.map +1 -0
  197. package/dist/tests/framework/ResponseHelper.test.js +202 -0
  198. package/dist/tests/framework/ResponseHelper.test.js.map +1 -0
  199. package/dist/tests/framework/SecurityDeep.test.d.ts +2 -0
  200. package/dist/tests/framework/SecurityDeep.test.d.ts.map +1 -0
  201. package/dist/tests/framework/SecurityDeep.test.js +825 -0
  202. package/dist/tests/framework/SecurityDeep.test.js.map +1 -0
  203. package/dist/tests/framework/ToolRegistry.test.d.ts +2 -0
  204. package/dist/tests/framework/ToolRegistry.test.d.ts.map +1 -0
  205. package/dist/tests/framework/ToolRegistry.test.js +152 -0
  206. package/dist/tests/framework/ToolRegistry.test.js.map +1 -0
  207. package/dist/tests/framework/ToonDescription.test.d.ts +2 -0
  208. package/dist/tests/framework/ToonDescription.test.d.ts.map +1 -0
  209. package/dist/tests/framework/ToonDescription.test.js +287 -0
  210. package/dist/tests/framework/ToonDescription.test.js.map +1 -0
  211. package/package.json +64 -0
@@ -0,0 +1,906 @@
1
+ /**
2
+ * AdversarialQA.test.ts
3
+ *
4
+ * Anthropic-level Quality Assurance tests for the MCP Tool Consolidation
5
+ * Framework. These tests are designed to probe invariants, contracts,
6
+ * security boundaries, state machine integrity, and protocol compliance
7
+ * at a level expected of production AI infrastructure.
8
+ *
9
+ * Categories:
10
+ * 1. Builder State Machine — frozen/unfrozen transitions, caching
11
+ * 2. MCP Protocol Contract — response shape compliance for all paths
12
+ * 3. Zod Defense Chain — boundary attacks on validation
13
+ * 4. Prototype Pollution & Injection — __proto__, constructor abuse
14
+ * 5. Discriminator Abuse — missing, null, numeric, object, array
15
+ * 6. Build Idempotency — multiple builds must return same cached ref
16
+ * 7. Middleware Ordering Guarantees — LIFO wrapping, correct ctx passing
17
+ * 8. Annotation Aggregation Invariants — all boolean combos
18
+ * 9. Description Generation Contract — 3-layer structure verification
19
+ * 10. Schema Field Annotation Accuracy — per-field annotation correctness
20
+ * 11. Custom Discriminator — non-default discriminator field support
21
+ * 12. ToolRegistry Contract — routing, registration, edge cases
22
+ * 13. ResponseHelper Contract — shape compliance for all helpers
23
+ */
24
+ import { describe, it, expect, beforeAll } from 'vitest';
25
+ import { z } from 'zod';
26
+ import { GroupedToolBuilder } from '../../src/framework/GroupedToolBuilder.js';
27
+ import { ToolRegistry } from '../../src/framework/ToolRegistry.js';
28
+ import { success, error, required } from '../../src/framework/ResponseHelper.js';
29
+ // ============================================================================
30
+ // 1. Builder State Machine Invariants
31
+ // ============================================================================
32
+ describe('QA: Builder State Machine', () => {
33
+ it('should prevent .action() after build', () => {
34
+ const b = new GroupedToolBuilder('sm_test')
35
+ .action({ name: 'a', handler: async () => success('ok') });
36
+ b.buildToolDefinition();
37
+ expect(() => b.action({ name: 'b', handler: async () => success('ok') }))
38
+ .toThrow(/frozen/i);
39
+ });
40
+ it('should prevent .group() after build', () => {
41
+ const b = new GroupedToolBuilder('sm_group')
42
+ .action({ name: 'a', handler: async () => success('ok') });
43
+ b.buildToolDefinition();
44
+ expect(() => b.group('g', 'desc', g => g.action({ name: 'x', handler: async () => success('ok') })))
45
+ .toThrow(/frozen/i);
46
+ });
47
+ it('should prevent .description() after build', () => {
48
+ const b = new GroupedToolBuilder('sm_desc')
49
+ .action({ name: 'a', handler: async () => success('ok') });
50
+ b.buildToolDefinition();
51
+ expect(() => b.description('new desc')).toThrow(/frozen/i);
52
+ });
53
+ it('should prevent .commonSchema() after build', () => {
54
+ const b = new GroupedToolBuilder('sm_schema')
55
+ .action({ name: 'a', handler: async () => success('ok') });
56
+ b.buildToolDefinition();
57
+ expect(() => b.commonSchema(z.object({ x: z.string() }))).toThrow(/frozen/i);
58
+ });
59
+ it('should prevent .use() after build', () => {
60
+ const b = new GroupedToolBuilder('sm_mw')
61
+ .action({ name: 'a', handler: async () => success('ok') });
62
+ b.buildToolDefinition();
63
+ expect(() => b.use(async (_ctx, _args, next) => next())).toThrow(/frozen/i);
64
+ });
65
+ it('should prevent .tags() after build', () => {
66
+ const b = new GroupedToolBuilder('sm_tags')
67
+ .action({ name: 'a', handler: async () => success('ok') });
68
+ b.buildToolDefinition();
69
+ expect(() => b.tags('tag')).toThrow(/frozen/i);
70
+ });
71
+ it('should prevent .annotations() after build', () => {
72
+ const b = new GroupedToolBuilder('sm_annot')
73
+ .action({ name: 'a', handler: async () => success('ok') });
74
+ b.buildToolDefinition();
75
+ expect(() => b.annotations({ readOnlyHint: true })).toThrow(/frozen/i);
76
+ });
77
+ it('should prevent .discriminator() after build', () => {
78
+ const b = new GroupedToolBuilder('sm_disc')
79
+ .action({ name: 'a', handler: async () => success('ok') });
80
+ b.buildToolDefinition();
81
+ expect(() => b.discriminator('op')).toThrow(/frozen/i);
82
+ });
83
+ it('should throw on build with zero actions', () => {
84
+ const b = new GroupedToolBuilder('empty_builder');
85
+ expect(() => b.buildToolDefinition()).toThrow(/no actions/i);
86
+ });
87
+ it('should auto-build on first execute if not yet built', async () => {
88
+ const b = new GroupedToolBuilder('auto_build')
89
+ .action({ name: 'ping', handler: async () => success('pong') });
90
+ // No buildToolDefinition() — execute should trigger build
91
+ const result = await b.execute(undefined, { action: 'ping' });
92
+ expect(result.isError).toBeUndefined();
93
+ expect(result.content[0].text).toBe('pong');
94
+ });
95
+ });
96
+ // ============================================================================
97
+ // 2. MCP Protocol Contract — Response Shape
98
+ // ============================================================================
99
+ describe('QA: MCP Response Contract', () => {
100
+ let builder;
101
+ beforeAll(() => {
102
+ builder = new GroupedToolBuilder('contract_test')
103
+ .commonSchema(z.object({ org: z.string() }))
104
+ .action({
105
+ name: 'ok',
106
+ schema: z.object({ id: z.string() }),
107
+ handler: async () => success('result'),
108
+ })
109
+ .action({
110
+ name: 'fail',
111
+ handler: async () => { throw new Error('BOOM'); },
112
+ });
113
+ builder.buildToolDefinition();
114
+ });
115
+ function assertMcpResponse(result) {
116
+ expect(result).toHaveProperty('content');
117
+ expect(Array.isArray(result.content)).toBe(true);
118
+ expect(result.content.length).toBeGreaterThan(0);
119
+ expect(result.content[0]).toHaveProperty('type', 'text');
120
+ expect(typeof result.content[0].text).toBe('string');
121
+ }
122
+ it('success response must comply with MCP shape', async () => {
123
+ const result = await builder.execute(undefined, {
124
+ action: 'ok', org: 'acme', id: 'x',
125
+ });
126
+ assertMcpResponse(result);
127
+ expect(result.isError).toBeUndefined();
128
+ });
129
+ it('handler error response must comply with MCP shape', async () => {
130
+ const result = await builder.execute(undefined, {
131
+ action: 'fail', org: 'acme',
132
+ });
133
+ assertMcpResponse(result);
134
+ expect(result.isError).toBe(true);
135
+ });
136
+ it('validation error response must comply with MCP shape', async () => {
137
+ const result = await builder.execute(undefined, {
138
+ action: 'ok', org: 123, // wrong type
139
+ });
140
+ assertMcpResponse(result);
141
+ expect(result.isError).toBe(true);
142
+ });
143
+ it('unknown action response must comply with MCP shape', async () => {
144
+ const result = await builder.execute(undefined, {
145
+ action: 'nonexistent', org: 'x',
146
+ });
147
+ assertMcpResponse(result);
148
+ expect(result.isError).toBe(true);
149
+ });
150
+ it('missing discriminator response must comply with MCP shape', async () => {
151
+ const result = await builder.execute(undefined, { org: 'x' });
152
+ assertMcpResponse(result);
153
+ expect(result.isError).toBe(true);
154
+ });
155
+ it('tool definition must comply with MCP Tool shape', () => {
156
+ const def = builder.buildToolDefinition();
157
+ expect(def).toHaveProperty('name');
158
+ expect(typeof def.name).toBe('string');
159
+ expect(def).toHaveProperty('description');
160
+ expect(typeof def.description).toBe('string');
161
+ expect(def).toHaveProperty('inputSchema');
162
+ expect(def.inputSchema).toHaveProperty('type', 'object');
163
+ expect(def.inputSchema).toHaveProperty('properties');
164
+ expect(def.inputSchema).toHaveProperty('required');
165
+ expect(Array.isArray(def.inputSchema.required)).toBe(true);
166
+ });
167
+ });
168
+ // ============================================================================
169
+ // 3. Zod Defense Chain — Boundary Attacks
170
+ // ============================================================================
171
+ describe('QA: Zod Defense Chain', () => {
172
+ let builder;
173
+ beforeAll(() => {
174
+ builder = new GroupedToolBuilder('zod_defense')
175
+ .commonSchema(z.object({
176
+ tenant: z.string().min(1).max(100),
177
+ }))
178
+ .action({
179
+ name: 'process',
180
+ schema: z.object({
181
+ count: z.number().int().min(0).max(1000000),
182
+ email: z.string().email(),
183
+ tags: z.array(z.string()).max(50).optional(),
184
+ }),
185
+ handler: async (_ctx, args) => success(`processed ${args.count}`),
186
+ });
187
+ builder.buildToolDefinition();
188
+ });
189
+ it('should reject negative count', async () => {
190
+ const r = await builder.execute(undefined, {
191
+ action: 'process', tenant: 'a', count: -1, email: 'x@y.com',
192
+ });
193
+ expect(r.isError).toBe(true);
194
+ });
195
+ it('should reject fractional count when int expected', async () => {
196
+ const r = await builder.execute(undefined, {
197
+ action: 'process', tenant: 'a', count: 3.14, email: 'x@y.com',
198
+ });
199
+ expect(r.isError).toBe(true);
200
+ });
201
+ it('should reject count exceeding max', async () => {
202
+ const r = await builder.execute(undefined, {
203
+ action: 'process', tenant: 'a', count: 1000001, email: 'x@y.com',
204
+ });
205
+ expect(r.isError).toBe(true);
206
+ });
207
+ it('should reject invalid email format', async () => {
208
+ const r = await builder.execute(undefined, {
209
+ action: 'process', tenant: 'a', count: 1, email: 'not-an-email',
210
+ });
211
+ expect(r.isError).toBe(true);
212
+ });
213
+ it('should reject empty tenant (min 1)', async () => {
214
+ const r = await builder.execute(undefined, {
215
+ action: 'process', tenant: '', count: 1, email: 'x@y.com',
216
+ });
217
+ expect(r.isError).toBe(true);
218
+ });
219
+ it('should reject oversized tenant (max 100)', async () => {
220
+ const r = await builder.execute(undefined, {
221
+ action: 'process', tenant: 'x'.repeat(101), count: 1, email: 'x@y.com',
222
+ });
223
+ expect(r.isError).toBe(true);
224
+ });
225
+ it('should accept valid input at exact boundaries', async () => {
226
+ const r = await builder.execute(undefined, {
227
+ action: 'process',
228
+ tenant: 'x', // min 1 ✓
229
+ count: 0, // min 0 ✓
230
+ email: 'a@b.co',
231
+ });
232
+ expect(r.isError).toBeUndefined();
233
+ });
234
+ it('should accept valid input at max boundaries', async () => {
235
+ const r = await builder.execute(undefined, {
236
+ action: 'process',
237
+ tenant: 'x'.repeat(100), // max 100 ✓
238
+ count: 1000000, // max 1000000 ✓
239
+ email: 'test@example.com',
240
+ });
241
+ expect(r.isError).toBeUndefined();
242
+ });
243
+ it('should strip extra fields injected by LLM', async () => {
244
+ const r = await builder.execute(undefined, {
245
+ action: 'process',
246
+ tenant: 'ok',
247
+ count: 1,
248
+ email: 'a@b.com',
249
+ __proto__: { admin: true },
250
+ constructor: 'hack',
251
+ malicious_field: 'rm -rf /',
252
+ });
253
+ // Should still succeed — extra fields stripped
254
+ expect(r.isError).toBeUndefined();
255
+ });
256
+ it('should reject when required field is null', async () => {
257
+ const r = await builder.execute(undefined, {
258
+ action: 'process', tenant: 'ok', count: null, email: 'a@b.com',
259
+ });
260
+ expect(r.isError).toBe(true);
261
+ });
262
+ it('should reject when required field is undefined', async () => {
263
+ const r = await builder.execute(undefined, {
264
+ action: 'process', tenant: 'ok', count: undefined, email: 'a@b.com',
265
+ });
266
+ expect(r.isError).toBe(true);
267
+ });
268
+ it('should handle array at max size', async () => {
269
+ const r = await builder.execute(undefined, {
270
+ action: 'process',
271
+ tenant: 'ok',
272
+ count: 1,
273
+ email: 'a@b.com',
274
+ tags: Array.from({ length: 50 }, (_, i) => `tag-${i}`),
275
+ });
276
+ expect(r.isError).toBeUndefined();
277
+ });
278
+ it('should reject array exceeding max size', async () => {
279
+ const r = await builder.execute(undefined, {
280
+ action: 'process',
281
+ tenant: 'ok',
282
+ count: 1,
283
+ email: 'a@b.com',
284
+ tags: Array.from({ length: 51 }, (_, i) => `tag-${i}`),
285
+ });
286
+ expect(r.isError).toBe(true);
287
+ });
288
+ });
289
+ // ============================================================================
290
+ // 4. Prototype Pollution & Injection via Field Names
291
+ // ============================================================================
292
+ describe('QA: Prototype Pollution & Injection', () => {
293
+ it('should not corrupt object prototype via args', async () => {
294
+ const builder = new GroupedToolBuilder('proto_test')
295
+ .action({
296
+ name: 'test',
297
+ schema: z.object({ data: z.string() }),
298
+ handler: async () => success('safe'),
299
+ });
300
+ builder.buildToolDefinition();
301
+ // Attempt prototype pollution
302
+ const maliciousArgs = JSON.parse('{"action":"test","data":"ok","__proto__":{"polluted":true}}');
303
+ const result = await builder.execute(undefined, maliciousArgs);
304
+ expect(result.isError).toBeUndefined();
305
+ // Verify Object.prototype is not polluted
306
+ expect(({}).polluted).toBeUndefined();
307
+ });
308
+ it('should handle constructor-named field safely', async () => {
309
+ const builder = new GroupedToolBuilder('constructor_test')
310
+ .action({
311
+ name: 'test',
312
+ handler: async () => success('safe'),
313
+ });
314
+ builder.buildToolDefinition();
315
+ const result = await builder.execute(undefined, {
316
+ action: 'test',
317
+ constructor: { prototype: { hacked: true } },
318
+ });
319
+ expect(result.isError).toBeUndefined();
320
+ });
321
+ it('should handle toString/valueOf override attempts', async () => {
322
+ const builder = new GroupedToolBuilder('override_test')
323
+ .action({
324
+ name: 'test',
325
+ schema: z.object({ value: z.string() }),
326
+ handler: async (_ctx, args) => success(`got: ${args.value}`),
327
+ });
328
+ builder.buildToolDefinition();
329
+ const result = await builder.execute(undefined, {
330
+ action: 'test',
331
+ value: 'normal',
332
+ toString: () => 'hacked',
333
+ valueOf: () => 999,
334
+ });
335
+ expect(result.isError).toBeUndefined();
336
+ });
337
+ });
338
+ // ============================================================================
339
+ // 5. Discriminator Abuse — Every Invalid Type
340
+ // ============================================================================
341
+ describe('QA: Discriminator Abuse', () => {
342
+ let builder;
343
+ beforeAll(() => {
344
+ builder = new GroupedToolBuilder('disc_abuse')
345
+ .action({ name: 'valid', handler: async () => success('ok') });
346
+ builder.buildToolDefinition();
347
+ });
348
+ it('should reject null action', async () => {
349
+ const r = await builder.execute(undefined, { action: null });
350
+ expect(r.isError).toBe(true);
351
+ });
352
+ it('should reject numeric action', async () => {
353
+ const r = await builder.execute(undefined, { action: 42 });
354
+ expect(r.isError).toBe(true);
355
+ });
356
+ it('should reject boolean action', async () => {
357
+ const r = await builder.execute(undefined, { action: true });
358
+ expect(r.isError).toBe(true);
359
+ });
360
+ it('should reject empty string action', async () => {
361
+ const r = await builder.execute(undefined, { action: '' });
362
+ expect(r.isError).toBe(true);
363
+ });
364
+ it('should reject object action', async () => {
365
+ const r = await builder.execute(undefined, { action: { name: 'valid' } });
366
+ expect(r.isError).toBe(true);
367
+ });
368
+ it('should reject array action', async () => {
369
+ const r = await builder.execute(undefined, { action: ['valid'] });
370
+ expect(r.isError).toBe(true);
371
+ });
372
+ it('should reject action with only whitespace', async () => {
373
+ const r = await builder.execute(undefined, { action: ' ' });
374
+ expect(r.isError).toBe(true);
375
+ });
376
+ it('should reject action with SQL injection attempt', async () => {
377
+ const r = await builder.execute(undefined, {
378
+ action: "valid'; DROP TABLE users; --",
379
+ });
380
+ expect(r.isError).toBe(true);
381
+ });
382
+ it('should list available actions on unknown action error', async () => {
383
+ const r = await builder.execute(undefined, { action: 'unknown' });
384
+ expect(r.isError).toBe(true);
385
+ expect(r.content[0].text).toContain('valid');
386
+ });
387
+ });
388
+ // ============================================================================
389
+ // 6. Build Idempotency — Cache Integrity
390
+ // ============================================================================
391
+ describe('QA: Build Idempotency', () => {
392
+ it('should return same cached reference on multiple builds', () => {
393
+ const b = new GroupedToolBuilder('idempotent')
394
+ .action({ name: 'a', handler: async () => success('ok') });
395
+ const first = b.buildToolDefinition();
396
+ const second = b.buildToolDefinition();
397
+ const third = b.buildToolDefinition();
398
+ expect(first).toBe(second);
399
+ expect(second).toBe(third);
400
+ });
401
+ it('should produce structurally identical definitions', () => {
402
+ const b = new GroupedToolBuilder('structural')
403
+ .description('Test tool')
404
+ .commonSchema(z.object({ org: z.string() }))
405
+ .action({
406
+ name: 'get',
407
+ schema: z.object({ id: z.string() }),
408
+ readOnly: true,
409
+ handler: async () => success('ok'),
410
+ });
411
+ const def = b.buildToolDefinition();
412
+ expect(def.name).toBe('structural');
413
+ expect(def.inputSchema.properties).toHaveProperty('action');
414
+ expect(def.inputSchema.properties).toHaveProperty('org');
415
+ expect(def.inputSchema.properties).toHaveProperty('id');
416
+ });
417
+ });
418
+ // ============================================================================
419
+ // 7. Middleware Ordering — LIFO Wrapping Guarantee
420
+ // ============================================================================
421
+ describe('QA: Middleware Ordering', () => {
422
+ it('should execute middlewares in registration order (left to right)', async () => {
423
+ const order = [];
424
+ const b = new GroupedToolBuilder('mw_order')
425
+ .use(async (_ctx, _args, next) => { order.push(1); return next(); })
426
+ .use(async (_ctx, _args, next) => { order.push(2); return next(); })
427
+ .use(async (_ctx, _args, next) => { order.push(3); return next(); })
428
+ .action({
429
+ name: 'run',
430
+ handler: async () => { order.push(4); return success('done'); },
431
+ });
432
+ b.buildToolDefinition();
433
+ await b.execute(undefined, { action: 'run' });
434
+ expect(order).toEqual([1, 2, 3, 4]);
435
+ });
436
+ it('should short-circuit correctly — later middleware should not run', async () => {
437
+ const order = [];
438
+ const b = new GroupedToolBuilder('mw_short')
439
+ .use(async (_ctx, _args, next) => { order.push('first'); return next(); })
440
+ .use(async () => { order.push('guard'); return error('BLOCKED'); })
441
+ .use(async (_ctx, _args, next) => { order.push('never'); return next(); })
442
+ .action({
443
+ name: 'run',
444
+ handler: async () => { order.push('handler'); return success('done'); },
445
+ });
446
+ b.buildToolDefinition();
447
+ const result = await b.execute(undefined, { action: 'run' });
448
+ expect(order).toEqual(['first', 'guard']);
449
+ expect(result.isError).toBe(true);
450
+ });
451
+ it('middleware should receive validated args, not raw input', async () => {
452
+ let capturedArgs = {};
453
+ const b = new GroupedToolBuilder('mw_validated')
454
+ .commonSchema(z.object({ org: z.string() }))
455
+ .use(async (_ctx, args, next) => {
456
+ capturedArgs = args;
457
+ return next();
458
+ })
459
+ .action({
460
+ name: 'check',
461
+ schema: z.object({ id: z.string() }),
462
+ handler: async () => success('ok'),
463
+ });
464
+ b.buildToolDefinition();
465
+ await b.execute(undefined, {
466
+ action: 'check',
467
+ org: 'acme',
468
+ id: 'x',
469
+ extra_garbage: 'should_be_stripped',
470
+ });
471
+ // Middleware sees stripped args (no extra_garbage)
472
+ expect(capturedArgs).not.toHaveProperty('extra_garbage');
473
+ expect(capturedArgs).toHaveProperty('org', 'acme');
474
+ expect(capturedArgs).toHaveProperty('id', 'x');
475
+ });
476
+ });
477
+ // ============================================================================
478
+ // 8. Annotation Aggregation Invariants — All Boolean Combos
479
+ // ============================================================================
480
+ describe('QA: Annotation Aggregation', () => {
481
+ it('should report readOnly=true only when ALL actions are readOnly', () => {
482
+ const b = new GroupedToolBuilder('all_read')
483
+ .action({ name: 'list', readOnly: true, handler: async () => success('ok') })
484
+ .action({ name: 'get', readOnly: true, handler: async () => success('ok') });
485
+ const def = b.buildToolDefinition();
486
+ expect(def.annotations.readOnlyHint).toBe(true);
487
+ });
488
+ it('should report readOnly=false when ANY action is not readOnly', () => {
489
+ const b = new GroupedToolBuilder('mixed_read')
490
+ .action({ name: 'list', readOnly: true, handler: async () => success('ok') })
491
+ .action({ name: 'create', handler: async () => success('ok') });
492
+ const def = b.buildToolDefinition();
493
+ expect(def.annotations.readOnlyHint).toBe(false);
494
+ });
495
+ it('should report destructive=true when ANY action is destructive', () => {
496
+ const b = new GroupedToolBuilder('has_destructive')
497
+ .action({ name: 'list', readOnly: true, handler: async () => success('ok') })
498
+ .action({ name: 'delete', destructive: true, handler: async () => success('ok') });
499
+ const def = b.buildToolDefinition();
500
+ expect(def.annotations.destructiveHint).toBe(true);
501
+ });
502
+ it('should report destructive=false when NO action is destructive', () => {
503
+ const b = new GroupedToolBuilder('no_destructive')
504
+ .action({ name: 'list', handler: async () => success('ok') })
505
+ .action({ name: 'get', handler: async () => success('ok') });
506
+ const def = b.buildToolDefinition();
507
+ expect(def.annotations.destructiveHint).toBe(false);
508
+ });
509
+ it('should report idempotent=true only when ALL actions are idempotent', () => {
510
+ const b = new GroupedToolBuilder('all_idempotent')
511
+ .action({ name: 'put', idempotent: true, handler: async () => success('ok') })
512
+ .action({ name: 'delete', idempotent: true, handler: async () => success('ok') });
513
+ const def = b.buildToolDefinition();
514
+ expect(def.annotations.idempotentHint).toBe(true);
515
+ });
516
+ it('should respect explicit annotation override even when actions disagree', () => {
517
+ const b = new GroupedToolBuilder('override')
518
+ .annotations({ readOnlyHint: true }) // explicit override
519
+ .action({ name: 'create', handler: async () => success('ok') }) // not read-only
520
+ .action({ name: 'delete', destructive: true, handler: async () => success('ok') });
521
+ const def = b.buildToolDefinition();
522
+ // Explicit override wins
523
+ expect(def.annotations.readOnlyHint).toBe(true);
524
+ // destructiveHint still auto-aggregated since not explicitly set
525
+ expect(def.annotations.destructiveHint).toBe(true);
526
+ });
527
+ });
528
+ // ============================================================================
529
+ // 9. Description Generation Contract
530
+ // ============================================================================
531
+ describe('QA: Description Generation', () => {
532
+ it('flat mode description should list action names', () => {
533
+ const b = new GroupedToolBuilder('desc_flat')
534
+ .description('My API')
535
+ .action({ name: 'list', handler: async () => success('ok') })
536
+ .action({ name: 'create', handler: async () => success('ok') })
537
+ .action({ name: 'delete', handler: async () => success('ok') });
538
+ const def = b.buildToolDefinition();
539
+ expect(def.description).toContain('Actions:');
540
+ expect(def.description).toContain('list');
541
+ expect(def.description).toContain('create');
542
+ expect(def.description).toContain('delete');
543
+ });
544
+ it('grouped mode description should list modules', () => {
545
+ const b = new GroupedToolBuilder('desc_grouped')
546
+ .description('Enterprise API')
547
+ .group('users', 'User mgmt', g => g
548
+ .action({ name: 'list', handler: async () => success('ok') })
549
+ .action({ name: 'create', handler: async () => success('ok') }))
550
+ .group('billing', 'Billing', g => g
551
+ .action({ name: 'charge', handler: async () => success('ok') }));
552
+ const def = b.buildToolDefinition();
553
+ expect(def.description).toContain('Modules:');
554
+ expect(def.description).toContain('users');
555
+ expect(def.description).toContain('billing');
556
+ });
557
+ it('workflow lines should show required fields and descriptions', () => {
558
+ const b = new GroupedToolBuilder('desc_workflow')
559
+ .action({
560
+ name: 'create',
561
+ description: 'Create a new record',
562
+ schema: z.object({ name: z.string(), priority: z.number() }),
563
+ handler: async () => success('ok'),
564
+ });
565
+ const def = b.buildToolDefinition();
566
+ expect(def.description).toContain('Workflow:');
567
+ expect(def.description).toContain('Create a new record');
568
+ expect(def.description).toContain('name');
569
+ expect(def.description).toContain('priority');
570
+ });
571
+ it('destructive actions should show ⚠️ DESTRUCTIVE in workflow', () => {
572
+ const b = new GroupedToolBuilder('desc_destructive')
573
+ .action({
574
+ name: 'nuke',
575
+ description: 'Delete everything',
576
+ destructive: true,
577
+ handler: async () => success('ok'),
578
+ });
579
+ const def = b.buildToolDefinition();
580
+ expect(def.description).toContain('⚠️ DESTRUCTIVE');
581
+ });
582
+ it('should use tool name as fallback when no description set', () => {
583
+ const b = new GroupedToolBuilder('fallback_name')
584
+ .action({ name: 'ping', handler: async () => success('ok') });
585
+ const def = b.buildToolDefinition();
586
+ expect(def.description).toContain('fallback_name');
587
+ });
588
+ });
589
+ // ============================================================================
590
+ // 10. Schema Field Annotation Accuracy
591
+ // ============================================================================
592
+ describe('QA: Schema Field Annotations', () => {
593
+ it('common schema required fields should be marked "always required"', () => {
594
+ const b = new GroupedToolBuilder('field_annot')
595
+ .commonSchema(z.object({
596
+ workspace_id: z.string().describe('The workspace'),
597
+ }))
598
+ .action({
599
+ name: 'list',
600
+ handler: async () => success('ok'),
601
+ });
602
+ const def = b.buildToolDefinition();
603
+ const wsField = def.inputSchema.properties.workspace_id;
604
+ expect(wsField.description).toContain('always required');
605
+ });
606
+ it('action-specific required fields should show "Required for" annotation', () => {
607
+ const b = new GroupedToolBuilder('field_req')
608
+ .action({
609
+ name: 'create',
610
+ schema: z.object({
611
+ name: z.string().describe('Record name'),
612
+ }),
613
+ handler: async () => success('ok'),
614
+ })
615
+ .action({
616
+ name: 'list',
617
+ handler: async () => success('ok'),
618
+ });
619
+ const def = b.buildToolDefinition();
620
+ const nameField = def.inputSchema.properties.name;
621
+ expect(nameField.description).toContain('Required for');
622
+ expect(nameField.description).toContain('create');
623
+ });
624
+ it('optional fields should show "For" annotation', () => {
625
+ const b = new GroupedToolBuilder('field_opt')
626
+ .action({
627
+ name: 'search',
628
+ schema: z.object({
629
+ filter: z.string().optional().describe('Search filter'),
630
+ }),
631
+ handler: async () => success('ok'),
632
+ });
633
+ const def = b.buildToolDefinition();
634
+ const filterField = def.inputSchema.properties.filter;
635
+ expect(filterField.description).toContain('For');
636
+ });
637
+ });
638
+ // ============================================================================
639
+ // 11. Custom Discriminator
640
+ // ============================================================================
641
+ describe('QA: Custom Discriminator', () => {
642
+ it('should use custom discriminator field name', async () => {
643
+ const b = new GroupedToolBuilder('custom_disc')
644
+ .discriminator('operation')
645
+ .action({ name: 'ping', handler: async () => success('pong') });
646
+ const def = b.buildToolDefinition();
647
+ expect(def.inputSchema.properties).toHaveProperty('operation');
648
+ expect(def.inputSchema.required).toContain('operation');
649
+ // Execute with custom discriminator
650
+ const result = await b.execute(undefined, { operation: 'ping' });
651
+ expect(result.isError).toBeUndefined();
652
+ expect(result.content[0].text).toBe('pong');
653
+ });
654
+ it('should reject when custom discriminator is missing', async () => {
655
+ const b = new GroupedToolBuilder('disc_missing')
656
+ .discriminator('cmd')
657
+ .action({ name: 'run', handler: async () => success('ok') });
658
+ b.buildToolDefinition();
659
+ const result = await b.execute(undefined, { action: 'run' }); // 'action' not 'cmd'
660
+ expect(result.isError).toBe(true);
661
+ expect(result.content[0].text).toContain('cmd');
662
+ });
663
+ });
664
+ // ============================================================================
665
+ // 12. ToolRegistry Contract
666
+ // ============================================================================
667
+ describe('QA: ToolRegistry Contract', () => {
668
+ it('should throw on duplicate registration', () => {
669
+ const registry = new ToolRegistry();
670
+ const b1 = new GroupedToolBuilder('dupe')
671
+ .action({ name: 'a', handler: async () => success('ok') });
672
+ const b2 = new GroupedToolBuilder('dupe')
673
+ .action({ name: 'b', handler: async () => success('ok') });
674
+ registry.register(b1);
675
+ expect(() => registry.register(b2)).toThrow(/already registered/i);
676
+ });
677
+ it('should route to correct tool in multi-tool registry', async () => {
678
+ const registry = new ToolRegistry();
679
+ registry.registerAll(new GroupedToolBuilder('tool_a')
680
+ .action({ name: 'ping', handler: async () => success('A:pong') }), new GroupedToolBuilder('tool_b')
681
+ .action({ name: 'ping', handler: async () => success('B:pong') }));
682
+ const rA = await registry.routeCall(undefined, 'tool_a', { action: 'ping' });
683
+ expect(rA.content[0].text).toBe('A:pong');
684
+ const rB = await registry.routeCall(undefined, 'tool_b', { action: 'ping' });
685
+ expect(rB.content[0].text).toBe('B:pong');
686
+ });
687
+ it('should return error for unknown tool (not throw)', async () => {
688
+ const registry = new ToolRegistry();
689
+ registry.register(new GroupedToolBuilder('only_one')
690
+ .action({ name: 'a', handler: async () => success('ok') }));
691
+ const result = await registry.routeCall(undefined, 'ghost', { action: 'a' });
692
+ expect(result.isError).toBe(true);
693
+ expect(result.content[0].text).toContain('Unknown tool');
694
+ expect(result.content[0].text).toContain('only_one');
695
+ });
696
+ it('registerAll should register multiple builders at once', () => {
697
+ const registry = new ToolRegistry();
698
+ registry.registerAll(new GroupedToolBuilder('batch_1')
699
+ .action({ name: 'a', handler: async () => success('ok') }), new GroupedToolBuilder('batch_2')
700
+ .action({ name: 'a', handler: async () => success('ok') }), new GroupedToolBuilder('batch_3')
701
+ .action({ name: 'a', handler: async () => success('ok') }));
702
+ expect(registry.size).toBe(3);
703
+ });
704
+ it('getTools with empty tags array should return all tools', () => {
705
+ const registry = new ToolRegistry();
706
+ registry.register(new GroupedToolBuilder('no_tags')
707
+ .action({ name: 'a', handler: async () => success('ok') }));
708
+ const tools = registry.getTools({ tags: [] });
709
+ expect(tools).toHaveLength(1);
710
+ });
711
+ it('getTools with empty exclude array should return all tools', () => {
712
+ const registry = new ToolRegistry();
713
+ registry.register(new GroupedToolBuilder('no_exclude')
714
+ .tags('x')
715
+ .action({ name: 'a', handler: async () => success('ok') }));
716
+ const tools = registry.getTools({ exclude: [] });
717
+ expect(tools).toHaveLength(1);
718
+ });
719
+ });
720
+ // ============================================================================
721
+ // 13. ResponseHelper Contract
722
+ // ============================================================================
723
+ describe('QA: ResponseHelper Contract', () => {
724
+ it('success() should produce valid MCP response', () => {
725
+ const r = success('hello');
726
+ expect(r.content).toEqual([{ type: 'text', text: 'hello' }]);
727
+ expect(r.isError).toBeUndefined();
728
+ });
729
+ it('error() should produce valid MCP error response', () => {
730
+ const r = error('bad');
731
+ expect(r.content).toEqual([{ type: 'text', text: 'bad' }]);
732
+ expect(r.isError).toBe(true);
733
+ });
734
+ it('required() should produce validation error with field name', () => {
735
+ const r = required('email');
736
+ expect(r.content[0].text).toContain('email');
737
+ expect(r.content[0].text).toContain('required');
738
+ expect(r.isError).toBe(true);
739
+ });
740
+ it('all helpers should return frozen-safe responses (no mutation risk)', () => {
741
+ const s = success('x');
742
+ const e = error('x');
743
+ const r = required('x');
744
+ // Responses should be plain objects, not class instances
745
+ expect(s.constructor).toBe(Object);
746
+ expect(e.constructor).toBe(Object);
747
+ expect(r.constructor).toBe(Object);
748
+ });
749
+ });
750
+ // ============================================================================
751
+ // 14. Group Mode Invariants
752
+ // ============================================================================
753
+ describe('QA: Group Mode Invariants', () => {
754
+ it('should reject dot in group name', () => {
755
+ const b = new GroupedToolBuilder('dot_group');
756
+ expect(() => b.group('users.admin', 'bad', g => g
757
+ .action({ name: 'list', handler: async () => success('ok') }))).toThrow(/dots/i);
758
+ });
759
+ it('should reject dot in action name within group', () => {
760
+ const b = new GroupedToolBuilder('dot_action');
761
+ expect(() => b.group('users', 'Users', g => g
762
+ .action({ name: 'list.all', handler: async () => success('ok') }))).toThrow(/dots/i);
763
+ });
764
+ it('should reject mixing .action() then .group()', () => {
765
+ const b = new GroupedToolBuilder('mix_ag')
766
+ .action({ name: 'flat', handler: async () => success('ok') });
767
+ expect(() => b.group('grp', 'Grp', g => g
768
+ .action({ name: 'a', handler: async () => success('ok') }))).toThrow(/Cannot use/i);
769
+ });
770
+ it('should reject mixing .group() then .action()', () => {
771
+ const b = new GroupedToolBuilder('mix_ga')
772
+ .group('grp', 'Grp', g => g
773
+ .action({ name: 'a', handler: async () => success('ok') }));
774
+ expect(() => b.action({ name: 'flat', handler: async () => success('ok') }))
775
+ .toThrow(/Cannot use/i);
776
+ });
777
+ it('grouped mode should produce compound action keys', () => {
778
+ const b = new GroupedToolBuilder('compound')
779
+ .group('users', 'Users', g => g
780
+ .action({ name: 'list', handler: async () => success('ok') })
781
+ .action({ name: 'create', handler: async () => success('ok') }));
782
+ const names = b.getActionNames();
783
+ // Before build, action names aren't populated, so build first
784
+ b.buildToolDefinition();
785
+ const postBuildNames = b.getActionNames();
786
+ expect(postBuildNames).toContain('users.list');
787
+ expect(postBuildNames).toContain('users.create');
788
+ });
789
+ it('should route correctly to grouped action via compound key', async () => {
790
+ const b = new GroupedToolBuilder('route_compound')
791
+ .group('billing', 'Billing', g => g
792
+ .action({
793
+ name: 'charge',
794
+ schema: z.object({ amount: z.number() }),
795
+ handler: async (_ctx, args) => success(`charged ${args.amount}`),
796
+ }));
797
+ b.buildToolDefinition();
798
+ const r = await b.execute(undefined, {
799
+ action: 'billing.charge',
800
+ amount: 42,
801
+ });
802
+ expect(r.isError).toBeUndefined();
803
+ expect(r.content[0].text).toContain('charged 42');
804
+ });
805
+ });
806
+ // ============================================================================
807
+ // 15. Extreme Edge Cases — The Outer Boundaries
808
+ // ============================================================================
809
+ describe('QA: Extreme Edge Cases', () => {
810
+ it('should handle single-action tool (simplest possible case)', async () => {
811
+ const b = new GroupedToolBuilder('minimal')
812
+ .action({ name: 'do', handler: async () => success('done') });
813
+ b.buildToolDefinition();
814
+ const r = await b.execute(undefined, { action: 'do' });
815
+ expect(r.isError).toBeUndefined();
816
+ });
817
+ it('should handle tool with many actions (50+)', () => {
818
+ const b = new GroupedToolBuilder('fifty_actions');
819
+ for (let i = 0; i < 50; i++) {
820
+ b.action({
821
+ name: `action_${i}`,
822
+ handler: async () => success(`result_${i}`),
823
+ });
824
+ }
825
+ const def = b.buildToolDefinition();
826
+ const actionProp = def.inputSchema.properties.action;
827
+ expect(actionProp.enum).toHaveLength(50);
828
+ });
829
+ it('should handle very long tool name', () => {
830
+ const longName = 'a'.repeat(200);
831
+ const b = new GroupedToolBuilder(longName)
832
+ .action({ name: 'x', handler: async () => success('ok') });
833
+ const def = b.buildToolDefinition();
834
+ expect(def.name).toBe(longName);
835
+ });
836
+ it('should handle tool with no schema at all (no validation)', async () => {
837
+ const b = new GroupedToolBuilder('no_schema')
838
+ .action({
839
+ name: 'run',
840
+ handler: async (_ctx, args) => success(`args: ${JSON.stringify(args)}`),
841
+ });
842
+ b.buildToolDefinition();
843
+ // Any garbage args should pass through (no validation)
844
+ const r = await b.execute(undefined, {
845
+ action: 'run',
846
+ anything: true,
847
+ nested: { deep: 'value' },
848
+ });
849
+ expect(r.isError).toBeUndefined();
850
+ });
851
+ it('should handle concurrent execute calls on same builder', async () => {
852
+ let callCount = 0;
853
+ const b = new GroupedToolBuilder('concurrent')
854
+ .action({
855
+ name: 'count',
856
+ handler: async () => {
857
+ callCount++;
858
+ // Simulate async work
859
+ await new Promise(resolve => setTimeout(resolve, 10));
860
+ return success(`call ${callCount}`);
861
+ },
862
+ });
863
+ b.buildToolDefinition();
864
+ // Fire 10 concurrent calls
865
+ const promises = Array.from({ length: 10 }, () => b.execute(undefined, { action: 'count' }));
866
+ const results = await Promise.all(promises);
867
+ // All should succeed
868
+ for (const r of results) {
869
+ expect(r.isError).toBeUndefined();
870
+ }
871
+ expect(callCount).toBe(10);
872
+ });
873
+ it('should handle handler that returns success with empty string', async () => {
874
+ const b = new GroupedToolBuilder('empty_success')
875
+ .action({ name: 'empty', handler: async () => success('') });
876
+ b.buildToolDefinition();
877
+ const r = await b.execute(undefined, { action: 'empty' });
878
+ expect(r.isError).toBeUndefined();
879
+ expect(r.content[0].text).toBe('');
880
+ });
881
+ it('should handle action names that are JavaScript keywords', async () => {
882
+ const b = new GroupedToolBuilder('js_keywords')
883
+ .action({ name: 'delete', handler: async () => success('deleted') })
884
+ .action({ name: 'return', handler: async () => success('returned') })
885
+ .action({ name: 'class', handler: async () => success('classed') })
886
+ .action({ name: 'export', handler: async () => success('exported') });
887
+ b.buildToolDefinition();
888
+ const r = await b.execute(undefined, { action: 'delete' });
889
+ expect(r.isError).toBeUndefined();
890
+ const r2 = await b.execute(undefined, { action: 'return' });
891
+ expect(r2.isError).toBeUndefined();
892
+ });
893
+ it('getTags should return a copy, not a reference', () => {
894
+ const b = new GroupedToolBuilder('tag_copy')
895
+ .tags('a', 'b', 'c')
896
+ .action({ name: 'x', handler: async () => success('ok') });
897
+ const tags1 = b.getTags();
898
+ const tags2 = b.getTags();
899
+ expect(tags1).toEqual(tags2);
900
+ expect(tags1).not.toBe(tags2); // Different array references
901
+ // Mutation of returned array should not affect builder
902
+ tags1.push('hacked');
903
+ expect(b.getTags()).not.toContain('hacked');
904
+ });
905
+ });
906
+ //# sourceMappingURL=AdversarialQA.test.js.map