@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.
- package/LICENSE +190 -0
- package/README.md +391 -0
- package/dist/src/AbstractBase.d.ts +24 -0
- package/dist/src/AbstractBase.d.ts.map +1 -0
- package/dist/src/AbstractBase.js +63 -0
- package/dist/src/AbstractBase.js.map +1 -0
- package/dist/src/AbstractLeaf.d.ts +12 -0
- package/dist/src/AbstractLeaf.d.ts.map +1 -0
- package/dist/src/AbstractLeaf.js +32 -0
- package/dist/src/AbstractLeaf.js.map +1 -0
- package/dist/src/Annotations.d.ts +15 -0
- package/dist/src/Annotations.d.ts.map +1 -0
- package/dist/src/Annotations.js +29 -0
- package/dist/src/Annotations.js.map +1 -0
- package/dist/src/Group.d.ts +32 -0
- package/dist/src/Group.d.ts.map +1 -0
- package/dist/src/Group.js +131 -0
- package/dist/src/Group.js.map +1 -0
- package/dist/src/Icon.d.ts +19 -0
- package/dist/src/Icon.d.ts.map +1 -0
- package/dist/src/Icon.js +33 -0
- package/dist/src/Icon.js.map +1 -0
- package/dist/src/Prompt.d.ts +11 -0
- package/dist/src/Prompt.d.ts.map +1 -0
- package/dist/src/Prompt.js +28 -0
- package/dist/src/Prompt.js.map +1 -0
- package/dist/src/PromptArgument.d.ts +10 -0
- package/dist/src/PromptArgument.d.ts.map +1 -0
- package/dist/src/PromptArgument.js +20 -0
- package/dist/src/PromptArgument.js.map +1 -0
- package/dist/src/Resource.d.ts +19 -0
- package/dist/src/Resource.d.ts.map +1 -0
- package/dist/src/Resource.js +34 -0
- package/dist/src/Resource.js.map +1 -0
- package/dist/src/Role.d.ts +5 -0
- package/dist/src/Role.d.ts.map +1 -0
- package/dist/src/Role.js +6 -0
- package/dist/src/Role.js.map +1 -0
- package/dist/src/Tool.d.ts +16 -0
- package/dist/src/Tool.d.ts.map +1 -0
- package/dist/src/Tool.js +28 -0
- package/dist/src/Tool.js.map +1 -0
- package/dist/src/ToolAnnotations.d.ts +23 -0
- package/dist/src/ToolAnnotations.d.ts.map +1 -0
- package/dist/src/ToolAnnotations.js +44 -0
- package/dist/src/ToolAnnotations.js.map +1 -0
- package/dist/src/converters/GroupConverter.d.ts +14 -0
- package/dist/src/converters/GroupConverter.d.ts.map +1 -0
- package/dist/src/converters/GroupConverter.js +13 -0
- package/dist/src/converters/GroupConverter.js.map +1 -0
- package/dist/src/converters/PromptConverter.d.ts +14 -0
- package/dist/src/converters/PromptConverter.d.ts.map +1 -0
- package/dist/src/converters/PromptConverter.js +13 -0
- package/dist/src/converters/PromptConverter.js.map +1 -0
- package/dist/src/converters/ResourceConverter.d.ts +14 -0
- package/dist/src/converters/ResourceConverter.d.ts.map +1 -0
- package/dist/src/converters/ResourceConverter.js +13 -0
- package/dist/src/converters/ResourceConverter.js.map +1 -0
- package/dist/src/converters/ToolAnnotationsConverter.d.ts +16 -0
- package/dist/src/converters/ToolAnnotationsConverter.d.ts.map +1 -0
- package/dist/src/converters/ToolAnnotationsConverter.js +23 -0
- package/dist/src/converters/ToolAnnotationsConverter.js.map +1 -0
- package/dist/src/converters/ToolConverter.d.ts +14 -0
- package/dist/src/converters/ToolConverter.d.ts.map +1 -0
- package/dist/src/converters/ToolConverter.js +13 -0
- package/dist/src/converters/ToolConverter.js.map +1 -0
- package/dist/src/converters/index.d.ts +6 -0
- package/dist/src/converters/index.d.ts.map +1 -0
- package/dist/src/converters/index.js +6 -0
- package/dist/src/converters/index.js.map +1 -0
- package/dist/src/framework/GroupedToolBuilder.d.ts +137 -0
- package/dist/src/framework/GroupedToolBuilder.d.ts.map +1 -0
- package/dist/src/framework/GroupedToolBuilder.js +289 -0
- package/dist/src/framework/GroupedToolBuilder.js.map +1 -0
- package/dist/src/framework/ResponseHelper.d.ts +43 -0
- package/dist/src/framework/ResponseHelper.d.ts.map +1 -0
- package/dist/src/framework/ResponseHelper.js +49 -0
- package/dist/src/framework/ResponseHelper.js.map +1 -0
- package/dist/src/framework/ToolBuilder.d.ts +46 -0
- package/dist/src/framework/ToolBuilder.d.ts.map +1 -0
- package/dist/src/framework/ToolBuilder.js +2 -0
- package/dist/src/framework/ToolBuilder.js.map +1 -0
- package/dist/src/framework/ToolRegistry.d.ts +85 -0
- package/dist/src/framework/ToolRegistry.d.ts.map +1 -0
- package/dist/src/framework/ToolRegistry.js +153 -0
- package/dist/src/framework/ToolRegistry.js.map +1 -0
- package/dist/src/framework/index.d.ts +9 -0
- package/dist/src/framework/index.d.ts.map +1 -0
- package/dist/src/framework/index.js +8 -0
- package/dist/src/framework/index.js.map +1 -0
- package/dist/src/framework/strategies/AnnotationAggregator.d.ts +11 -0
- package/dist/src/framework/strategies/AnnotationAggregator.d.ts.map +1 -0
- package/dist/src/framework/strategies/AnnotationAggregator.js +25 -0
- package/dist/src/framework/strategies/AnnotationAggregator.js.map +1 -0
- package/dist/src/framework/strategies/DescriptionGenerator.d.ts +12 -0
- package/dist/src/framework/strategies/DescriptionGenerator.d.ts.map +1 -0
- package/dist/src/framework/strategies/DescriptionGenerator.js +70 -0
- package/dist/src/framework/strategies/DescriptionGenerator.js.map +1 -0
- package/dist/src/framework/strategies/MiddlewareCompiler.d.ts +13 -0
- package/dist/src/framework/strategies/MiddlewareCompiler.d.ts.map +1 -0
- package/dist/src/framework/strategies/MiddlewareCompiler.js +24 -0
- package/dist/src/framework/strategies/MiddlewareCompiler.js.map +1 -0
- package/dist/src/framework/strategies/SchemaGenerator.d.ts +15 -0
- package/dist/src/framework/strategies/SchemaGenerator.d.ts.map +1 -0
- package/dist/src/framework/strategies/SchemaGenerator.js +97 -0
- package/dist/src/framework/strategies/SchemaGenerator.js.map +1 -0
- package/dist/src/framework/strategies/SchemaUtils.d.ts +7 -0
- package/dist/src/framework/strategies/SchemaUtils.d.ts.map +1 -0
- package/dist/src/framework/strategies/SchemaUtils.js +17 -0
- package/dist/src/framework/strategies/SchemaUtils.js.map +1 -0
- package/dist/src/framework/strategies/ToonDescriptionGenerator.d.ts +3 -0
- package/dist/src/framework/strategies/ToonDescriptionGenerator.d.ts.map +1 -0
- package/dist/src/framework/strategies/ToonDescriptionGenerator.js +53 -0
- package/dist/src/framework/strategies/ToonDescriptionGenerator.js.map +1 -0
- package/dist/src/framework/strategies/Types.d.ts +34 -0
- package/dist/src/framework/strategies/Types.d.ts.map +1 -0
- package/dist/src/framework/strategies/Types.js +2 -0
- package/dist/src/framework/strategies/Types.js.map +1 -0
- package/dist/src/framework/strategies/index.d.ts +12 -0
- package/dist/src/framework/strategies/index.d.ts.map +1 -0
- package/dist/src/framework/strategies/index.js +11 -0
- package/dist/src/framework/strategies/index.js.map +1 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +15 -0
- package/dist/src/index.js.map +1 -0
- package/dist/tests/AbstractBase.test.d.ts +2 -0
- package/dist/tests/AbstractBase.test.d.ts.map +1 -0
- package/dist/tests/AbstractBase.test.js +130 -0
- package/dist/tests/AbstractBase.test.js.map +1 -0
- package/dist/tests/AbstractLeaf.test.d.ts +2 -0
- package/dist/tests/AbstractLeaf.test.d.ts.map +1 -0
- package/dist/tests/AbstractLeaf.test.js +65 -0
- package/dist/tests/AbstractLeaf.test.js.map +1 -0
- package/dist/tests/Annotations.test.d.ts +2 -0
- package/dist/tests/Annotations.test.d.ts.map +1 -0
- package/dist/tests/Annotations.test.js +34 -0
- package/dist/tests/Annotations.test.js.map +1 -0
- package/dist/tests/BarrelExport.test.d.ts +2 -0
- package/dist/tests/BarrelExport.test.d.ts.map +1 -0
- package/dist/tests/BarrelExport.test.js +42 -0
- package/dist/tests/BarrelExport.test.js.map +1 -0
- package/dist/tests/Converters.test.d.ts +2 -0
- package/dist/tests/Converters.test.d.ts.map +1 -0
- package/dist/tests/Converters.test.js +193 -0
- package/dist/tests/Converters.test.js.map +1 -0
- package/dist/tests/Group.test.d.ts +2 -0
- package/dist/tests/Group.test.d.ts.map +1 -0
- package/dist/tests/Group.test.js +257 -0
- package/dist/tests/Group.test.js.map +1 -0
- package/dist/tests/Icon.test.d.ts +2 -0
- package/dist/tests/Icon.test.d.ts.map +1 -0
- package/dist/tests/Icon.test.js +44 -0
- package/dist/tests/Icon.test.js.map +1 -0
- package/dist/tests/Prompt.test.d.ts +2 -0
- package/dist/tests/Prompt.test.d.ts.map +1 -0
- package/dist/tests/Prompt.test.js +63 -0
- package/dist/tests/Prompt.test.js.map +1 -0
- package/dist/tests/PromptArgument.test.d.ts +2 -0
- package/dist/tests/PromptArgument.test.d.ts.map +1 -0
- package/dist/tests/PromptArgument.test.js +37 -0
- package/dist/tests/PromptArgument.test.js.map +1 -0
- package/dist/tests/Resource.test.d.ts +2 -0
- package/dist/tests/Resource.test.d.ts.map +1 -0
- package/dist/tests/Resource.test.js +61 -0
- package/dist/tests/Resource.test.js.map +1 -0
- package/dist/tests/Role.test.d.ts +2 -0
- package/dist/tests/Role.test.d.ts.map +1 -0
- package/dist/tests/Role.test.js +17 -0
- package/dist/tests/Role.test.js.map +1 -0
- package/dist/tests/Tool.test.d.ts +2 -0
- package/dist/tests/Tool.test.d.ts.map +1 -0
- package/dist/tests/Tool.test.js +62 -0
- package/dist/tests/Tool.test.js.map +1 -0
- package/dist/tests/ToolAnnotations.test.d.ts +2 -0
- package/dist/tests/ToolAnnotations.test.d.ts.map +1 -0
- package/dist/tests/ToolAnnotations.test.js +55 -0
- package/dist/tests/ToolAnnotations.test.js.map +1 -0
- package/dist/tests/framework/AdversarialQA.test.d.ts +2 -0
- package/dist/tests/framework/AdversarialQA.test.d.ts.map +1 -0
- package/dist/tests/framework/AdversarialQA.test.js +906 -0
- package/dist/tests/framework/AdversarialQA.test.js.map +1 -0
- package/dist/tests/framework/GroupedToolBuilder.test.d.ts +2 -0
- package/dist/tests/framework/GroupedToolBuilder.test.d.ts.map +1 -0
- package/dist/tests/framework/GroupedToolBuilder.test.js +712 -0
- package/dist/tests/framework/GroupedToolBuilder.test.js.map +1 -0
- package/dist/tests/framework/LargeScaleScenarios.test.d.ts +2 -0
- package/dist/tests/framework/LargeScaleScenarios.test.d.ts.map +1 -0
- package/dist/tests/framework/LargeScaleScenarios.test.js +1043 -0
- package/dist/tests/framework/LargeScaleScenarios.test.js.map +1 -0
- package/dist/tests/framework/McpServerAdapter.test.d.ts +2 -0
- package/dist/tests/framework/McpServerAdapter.test.d.ts.map +1 -0
- package/dist/tests/framework/McpServerAdapter.test.js +380 -0
- package/dist/tests/framework/McpServerAdapter.test.js.map +1 -0
- package/dist/tests/framework/ResponseHelper.test.d.ts +2 -0
- package/dist/tests/framework/ResponseHelper.test.d.ts.map +1 -0
- package/dist/tests/framework/ResponseHelper.test.js +202 -0
- package/dist/tests/framework/ResponseHelper.test.js.map +1 -0
- package/dist/tests/framework/SecurityDeep.test.d.ts +2 -0
- package/dist/tests/framework/SecurityDeep.test.d.ts.map +1 -0
- package/dist/tests/framework/SecurityDeep.test.js +825 -0
- package/dist/tests/framework/SecurityDeep.test.js.map +1 -0
- package/dist/tests/framework/ToolRegistry.test.d.ts +2 -0
- package/dist/tests/framework/ToolRegistry.test.d.ts.map +1 -0
- package/dist/tests/framework/ToolRegistry.test.js +152 -0
- package/dist/tests/framework/ToolRegistry.test.js.map +1 -0
- package/dist/tests/framework/ToonDescription.test.d.ts +2 -0
- package/dist/tests/framework/ToonDescription.test.d.ts.map +1 -0
- package/dist/tests/framework/ToonDescription.test.js +287 -0
- package/dist/tests/framework/ToonDescription.test.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LargeScaleScenarios.test.ts
|
|
3
|
+
*
|
|
4
|
+
* Simulates thousands of MCP endpoints consolidated through the grouping
|
|
5
|
+
* framework, exercising tag-based selective exposure, mass registration,
|
|
6
|
+
* routing, and schema validation at enterprise scale.
|
|
7
|
+
*
|
|
8
|
+
* Domains modeled:
|
|
9
|
+
* - Project Management (tasks, sprints, boards, labels, epics)
|
|
10
|
+
* - CRM (contacts, deals, pipelines, activities, companies)
|
|
11
|
+
* - DevOps (deployments, pipelines, artifacts, environments, monitors)
|
|
12
|
+
* - Collaboration (channels, messages, threads, reactions, files)
|
|
13
|
+
* - Analytics (dashboards, reports, metrics, exports, schedules)
|
|
14
|
+
* - Finance (invoices, payments, subscriptions, refunds, taxes)
|
|
15
|
+
* - Identity (users, roles, permissions, tokens, sessions)
|
|
16
|
+
* - Storage (buckets, objects, versions, policies, lifecycles)
|
|
17
|
+
* - Notifications (templates, channels, preferences, logs, rules)
|
|
18
|
+
* - Integrations (webhooks, connections, transforms, mappings, syncs)
|
|
19
|
+
*/
|
|
20
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
import { GroupedToolBuilder } from '../../src/framework/GroupedToolBuilder.js';
|
|
23
|
+
import { ToolRegistry } from '../../src/framework/ToolRegistry.js';
|
|
24
|
+
import { success } from '../../src/framework/ResponseHelper.js';
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Helpers — Factory functions to generate realistic domain builders
|
|
27
|
+
// ============================================================================
|
|
28
|
+
/** Standard CRUD + 2 extra actions per entity = 7 actions each */
|
|
29
|
+
const CRUD_ACTIONS = ['list', 'get', 'create', 'update', 'delete', 'archive', 'export'];
|
|
30
|
+
const DOMAINS = [
|
|
31
|
+
{
|
|
32
|
+
domain: 'project_management',
|
|
33
|
+
tags: ['pm', 'core'],
|
|
34
|
+
entities: ['tasks', 'sprints', 'boards', 'labels', 'epics', 'milestones', 'comments', 'attachments', 'time_entries', 'checklists'],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
domain: 'crm',
|
|
38
|
+
tags: ['crm', 'sales'],
|
|
39
|
+
entities: ['contacts', 'deals', 'pipelines', 'activities', 'companies', 'emails', 'notes', 'tags', 'segments', 'campaigns'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
domain: 'devops',
|
|
43
|
+
tags: ['devops', 'infra'],
|
|
44
|
+
entities: ['deployments', 'ci_pipelines', 'artifacts', 'environments', 'monitors', 'alerts', 'logs', 'configs', 'secrets', 'clusters'],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
domain: 'collaboration',
|
|
48
|
+
tags: ['collab', 'core'],
|
|
49
|
+
entities: ['channels', 'messages', 'threads', 'reactions', 'files', 'mentions', 'bookmarks', 'pins', 'polls', 'events'],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
domain: 'analytics',
|
|
53
|
+
tags: ['analytics', 'reporting'],
|
|
54
|
+
entities: ['dashboards', 'reports', 'metrics', 'exports', 'schedules', 'widgets', 'filters', 'datasets', 'annotations', 'alerts'],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
domain: 'finance',
|
|
58
|
+
tags: ['finance', 'billing'],
|
|
59
|
+
entities: ['invoices', 'payments', 'subscriptions', 'refunds', 'taxes', 'credits', 'plans', 'coupons', 'receipts', 'ledger'],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
domain: 'identity',
|
|
63
|
+
tags: ['identity', 'security'],
|
|
64
|
+
entities: ['users', 'roles', 'permissions', 'tokens', 'sessions', 'groups', 'policies', 'audits', 'mfa', 'invitations'],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
domain: 'storage',
|
|
68
|
+
tags: ['storage', 'infra'],
|
|
69
|
+
entities: ['buckets', 'objects', 'versions', 'acl_policies', 'lifecycles', 'transfers', 'archives', 'quotas', 'replications', 'snapshots'],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
domain: 'notifications',
|
|
73
|
+
tags: ['notifications', 'core'],
|
|
74
|
+
entities: ['templates', 'channels', 'preferences', 'delivery_logs', 'rules', 'batches', 'schedules', 'providers', 'suppressions', 'digests'],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
domain: 'integrations',
|
|
78
|
+
tags: ['integrations', 'core'],
|
|
79
|
+
entities: ['webhooks', 'connections', 'transforms', 'mappings', 'syncs', 'oauth_apps', 'api_keys', 'rate_limits', 'event_bus', 'schemas'],
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
// Total endpoints = 10 domains × 10 entities × 7 actions = 700 actions
|
|
83
|
+
// Consolidated into 10 domains × 10 entities = 100 grouped tools
|
|
84
|
+
// Each tool consolidates 7 REST-equivalent endpoints into 1 MCP tool
|
|
85
|
+
/**
|
|
86
|
+
* Build a GroupedToolBuilder for a domain entity.
|
|
87
|
+
* Uses flat mode: entity name as tool name, actions as CRUD verbs.
|
|
88
|
+
*/
|
|
89
|
+
function buildEntityTool(domain, entity, domainTags) {
|
|
90
|
+
const builder = new GroupedToolBuilder(`${domain}_${entity}`)
|
|
91
|
+
.description(`Manage ${entity} within ${domain}`)
|
|
92
|
+
.tags(...domainTags, entity);
|
|
93
|
+
for (const action of CRUD_ACTIONS) {
|
|
94
|
+
const isWrite = ['create', 'update', 'delete', 'archive'].includes(action);
|
|
95
|
+
const isRead = ['list', 'get', 'export'].includes(action);
|
|
96
|
+
builder.action({
|
|
97
|
+
name: action,
|
|
98
|
+
description: `${action} ${entity}`,
|
|
99
|
+
readOnly: isRead,
|
|
100
|
+
schema: action === 'get'
|
|
101
|
+
? z.object({ id: z.string().describe(`ID of the ${entity.slice(0, -1)}`) })
|
|
102
|
+
: action === 'create'
|
|
103
|
+
? z.object({ name: z.string().describe('Name for the new record') })
|
|
104
|
+
: action === 'update'
|
|
105
|
+
? z.object({
|
|
106
|
+
id: z.string().describe('ID to update'),
|
|
107
|
+
data: z.string().describe('JSON payload'),
|
|
108
|
+
})
|
|
109
|
+
: action === 'delete' || action === 'archive'
|
|
110
|
+
? z.object({ id: z.string().describe('ID to process') })
|
|
111
|
+
: action === 'export'
|
|
112
|
+
? z.object({ format: z.enum(['csv', 'json', 'xlsx']).describe('Export format') })
|
|
113
|
+
: undefined, // 'list' has no extra params
|
|
114
|
+
handler: async (_ctx, args) => {
|
|
115
|
+
return success(`[${domain}/${entity}] ${action} executed ${JSON.stringify(args)}`);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return builder;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Build all entity tools for a domain and return them.
|
|
123
|
+
*/
|
|
124
|
+
function buildDomainTools(def) {
|
|
125
|
+
return def.entities.map(entity => buildEntityTool(def.domain, entity, def.tags));
|
|
126
|
+
}
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Test Suites
|
|
129
|
+
// ============================================================================
|
|
130
|
+
describe('Large-Scale Scenarios — Mass Endpoint Registration', () => {
|
|
131
|
+
const registry = new ToolRegistry();
|
|
132
|
+
const allBuilders = [];
|
|
133
|
+
// Register all domains
|
|
134
|
+
for (const domain of DOMAINS) {
|
|
135
|
+
const builders = buildDomainTools(domain);
|
|
136
|
+
allBuilders.push(...builders);
|
|
137
|
+
}
|
|
138
|
+
it('should register 100 grouped tools (700 consolidated endpoints)', () => {
|
|
139
|
+
for (const builder of allBuilders) {
|
|
140
|
+
registry.register(builder);
|
|
141
|
+
}
|
|
142
|
+
expect(registry.size).toBe(100); // 10 domains × 10 entities
|
|
143
|
+
});
|
|
144
|
+
it('should expose all 100 tool definitions via getAllTools()', () => {
|
|
145
|
+
const tools = registry.getAllTools();
|
|
146
|
+
expect(tools).toHaveLength(100);
|
|
147
|
+
// Every tool must have a valid JSON schema with the 'action' discriminator
|
|
148
|
+
for (const tool of tools) {
|
|
149
|
+
expect(tool.name).toBeTruthy();
|
|
150
|
+
expect(tool.inputSchema).toBeDefined();
|
|
151
|
+
expect(tool.inputSchema.properties).toHaveProperty('action');
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
it('each tool should enumerate exactly 7 actions in its enum', () => {
|
|
155
|
+
const tools = registry.getAllTools();
|
|
156
|
+
for (const tool of tools) {
|
|
157
|
+
const actionProp = tool.inputSchema.properties['action'];
|
|
158
|
+
const enumValues = actionProp['enum'];
|
|
159
|
+
expect(enumValues).toHaveLength(CRUD_ACTIONS.length);
|
|
160
|
+
expect(enumValues).toEqual(expect.arrayContaining([...CRUD_ACTIONS]));
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
it('should reject duplicate tool registrations', () => {
|
|
164
|
+
const duplicate = buildEntityTool('project_management', 'tasks', ['pm']);
|
|
165
|
+
expect(() => registry.register(duplicate)).toThrow('already registered');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
// ============================================================================
|
|
169
|
+
// Tag-Based Filtering at Scale
|
|
170
|
+
// ============================================================================
|
|
171
|
+
describe('Large-Scale Scenarios — Tag-Based Selective Exposure', () => {
|
|
172
|
+
let registry;
|
|
173
|
+
// Re-create registry for isolation
|
|
174
|
+
beforeAll(() => {
|
|
175
|
+
registry = new ToolRegistry();
|
|
176
|
+
for (const domain of DOMAINS) {
|
|
177
|
+
for (const builder of buildDomainTools(domain)) {
|
|
178
|
+
registry.register(builder);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
it('should filter by single domain tag → 10 tools', () => {
|
|
183
|
+
const pmTools = registry.getTools({ tags: ['pm'] });
|
|
184
|
+
expect(pmTools).toHaveLength(10);
|
|
185
|
+
for (const tool of pmTools) {
|
|
186
|
+
expect(tool.name).toContain('project_management');
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
it('should filter by "core" tag → 40 tools (pm + collab + notif + integrations)', () => {
|
|
190
|
+
const coreTools = registry.getTools({ tags: ['core'] });
|
|
191
|
+
// pm(10) + collab(10) + notifications(10) + integrations(10) = 40
|
|
192
|
+
expect(coreTools).toHaveLength(40);
|
|
193
|
+
});
|
|
194
|
+
it('should filter by "infra" tag → 20 tools (devops + storage)', () => {
|
|
195
|
+
const infraTools = registry.getTools({ tags: ['infra'] });
|
|
196
|
+
expect(infraTools).toHaveLength(20);
|
|
197
|
+
});
|
|
198
|
+
it('should intersect multiple tags → narrow selection', () => {
|
|
199
|
+
// "core" AND "tasks" entity tag → only project_management_tasks
|
|
200
|
+
const narrow = registry.getTools({ tags: ['core', 'tasks'] });
|
|
201
|
+
expect(narrow).toHaveLength(1);
|
|
202
|
+
expect(narrow[0].name).toBe('project_management_tasks');
|
|
203
|
+
});
|
|
204
|
+
it('should exclude specific tags', () => {
|
|
205
|
+
// All 100 tools minus finance(10) and identity(10) = 80
|
|
206
|
+
const filtered = registry.getTools({
|
|
207
|
+
exclude: ['finance', 'identity'],
|
|
208
|
+
});
|
|
209
|
+
expect(filtered).toHaveLength(80);
|
|
210
|
+
for (const tool of filtered) {
|
|
211
|
+
expect(tool.name).not.toContain('finance');
|
|
212
|
+
expect(tool.name).not.toContain('identity');
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
it('should combine include + exclude tags', () => {
|
|
216
|
+
// "core" tagged (40 tools) minus "notifications" (10) = 30
|
|
217
|
+
const filtered = registry.getTools({
|
|
218
|
+
tags: ['core'],
|
|
219
|
+
exclude: ['notifications'],
|
|
220
|
+
});
|
|
221
|
+
expect(filtered).toHaveLength(30);
|
|
222
|
+
});
|
|
223
|
+
it('should return empty set for non-existent tag', () => {
|
|
224
|
+
const empty = registry.getTools({ tags: ['nonexistent-tag-xyz'] });
|
|
225
|
+
expect(empty).toHaveLength(0);
|
|
226
|
+
});
|
|
227
|
+
it('should return all tools when filter is empty', () => {
|
|
228
|
+
const all = registry.getTools({});
|
|
229
|
+
expect(all).toHaveLength(100);
|
|
230
|
+
});
|
|
231
|
+
it('should handle overlapping domain tags correctly', () => {
|
|
232
|
+
// "infra" appears in both devops and storage
|
|
233
|
+
const devopsOnly = registry.getTools({ tags: ['infra', 'devops'] });
|
|
234
|
+
expect(devopsOnly).toHaveLength(10);
|
|
235
|
+
for (const tool of devopsOnly) {
|
|
236
|
+
expect(tool.name).toContain('devops');
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// Routing at Scale — Dispatch to Correct Tool + Action
|
|
242
|
+
// ============================================================================
|
|
243
|
+
describe('Large-Scale Scenarios — Routing & Execution', () => {
|
|
244
|
+
let registry;
|
|
245
|
+
beforeAll(() => {
|
|
246
|
+
registry = new ToolRegistry();
|
|
247
|
+
for (const domain of DOMAINS) {
|
|
248
|
+
for (const builder of buildDomainTools(domain)) {
|
|
249
|
+
registry.register(builder);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
it('should route to the correct tool and action', async () => {
|
|
254
|
+
const result = await registry.routeCall(undefined, 'crm_contacts', { action: 'get', id: 'contact-42' });
|
|
255
|
+
expect(result.isError).toBeUndefined();
|
|
256
|
+
const text = result.content[0].text;
|
|
257
|
+
expect(text).toContain('crm/contacts');
|
|
258
|
+
expect(text).toContain('get');
|
|
259
|
+
});
|
|
260
|
+
it('should validate schema — missing required `id` on get action', async () => {
|
|
261
|
+
const result = await registry.routeCall(undefined, 'devops_deployments', { action: 'get' } // missing 'id'
|
|
262
|
+
);
|
|
263
|
+
expect(result.isError).toBe(true);
|
|
264
|
+
const text = result.content[0].text;
|
|
265
|
+
expect(text).toContain('id');
|
|
266
|
+
});
|
|
267
|
+
it('should return error for unknown tool', async () => {
|
|
268
|
+
const result = await registry.routeCall(undefined, 'nonexistent_tool', { action: 'list' });
|
|
269
|
+
expect(result.isError).toBe(true);
|
|
270
|
+
const text = result.content[0].text;
|
|
271
|
+
expect(text).toContain('Unknown tool');
|
|
272
|
+
});
|
|
273
|
+
it('should return error for unknown action within valid tool', async () => {
|
|
274
|
+
const result = await registry.routeCall(undefined, 'finance_invoices', { action: 'teleport' });
|
|
275
|
+
expect(result.isError).toBe(true);
|
|
276
|
+
const text = result.content[0].text;
|
|
277
|
+
expect(text).toContain('teleport');
|
|
278
|
+
});
|
|
279
|
+
it('should successfully execute CRUD actions across different domains', async () => {
|
|
280
|
+
const scenarios = [
|
|
281
|
+
{ tool: 'project_management_tasks', action: 'list', args: {} },
|
|
282
|
+
{ tool: 'crm_deals', action: 'create', args: { name: 'Big Deal' } },
|
|
283
|
+
{ tool: 'devops_ci_pipelines', action: 'get', args: { id: 'pipe-7' } },
|
|
284
|
+
{ tool: 'collaboration_messages', action: 'delete', args: { id: 'msg-99' } },
|
|
285
|
+
{ tool: 'analytics_reports', action: 'export', args: { format: 'csv' } },
|
|
286
|
+
{ tool: 'finance_payments', action: 'archive', args: { id: 'pay-123' } },
|
|
287
|
+
{ tool: 'identity_users', action: 'update', args: { id: 'usr-1', data: '{"name":"Alice"}' } },
|
|
288
|
+
{ tool: 'storage_buckets', action: 'list', args: {} },
|
|
289
|
+
{ tool: 'notifications_templates', action: 'get', args: { id: 'tpl-42' } },
|
|
290
|
+
{ tool: 'integrations_webhooks', action: 'create', args: { name: 'My Hook' } },
|
|
291
|
+
];
|
|
292
|
+
for (const { tool, action, args } of scenarios) {
|
|
293
|
+
const result = await registry.routeCall(undefined, tool, { action, ...args });
|
|
294
|
+
expect(result.isError).toBeUndefined();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// Stress Test — Programmatic Generation of N Builders
|
|
300
|
+
// ============================================================================
|
|
301
|
+
describe('Large-Scale Scenarios — Stress: 500 Grouped Tools', () => {
|
|
302
|
+
it('should register and query 500 builders (3500 endpoints) efficiently', () => {
|
|
303
|
+
const stressRegistry = new ToolRegistry();
|
|
304
|
+
// Generate 50 synthetic domains × 10 entities each = 500 tools
|
|
305
|
+
for (let d = 0; d < 50; d++) {
|
|
306
|
+
const domainName = `domain_${String(d).padStart(3, '0')}`;
|
|
307
|
+
const domainTag = d < 25 ? 'tier_a' : 'tier_b';
|
|
308
|
+
for (let e = 0; e < 10; e++) {
|
|
309
|
+
const entityName = `entity_${String(e).padStart(2, '0')}`;
|
|
310
|
+
const builder = buildEntityTool(domainName, entityName, [domainTag, `d${d}`]);
|
|
311
|
+
stressRegistry.register(builder);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
expect(stressRegistry.size).toBe(500);
|
|
315
|
+
// All tools
|
|
316
|
+
const all = stressRegistry.getAllTools();
|
|
317
|
+
expect(all).toHaveLength(500);
|
|
318
|
+
// Tag filtering — tier_a has first 25 domains × 10 entities = 250
|
|
319
|
+
const tierA = stressRegistry.getTools({ tags: ['tier_a'] });
|
|
320
|
+
expect(tierA).toHaveLength(250);
|
|
321
|
+
const tierB = stressRegistry.getTools({ tags: ['tier_b'] });
|
|
322
|
+
expect(tierB).toHaveLength(250);
|
|
323
|
+
// Single-domain filter
|
|
324
|
+
const d7 = stressRegistry.getTools({ tags: ['d7'] });
|
|
325
|
+
expect(d7).toHaveLength(10);
|
|
326
|
+
// Exclude tier_b → only tier_a remains
|
|
327
|
+
const noTierB = stressRegistry.getTools({ exclude: ['tier_b'] });
|
|
328
|
+
expect(noTierB).toHaveLength(250);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
// ============================================================================
|
|
332
|
+
// Schema Introspection — Verify Generated Descriptions at Scale
|
|
333
|
+
// ============================================================================
|
|
334
|
+
describe('Large-Scale Scenarios — Schema Introspection', () => {
|
|
335
|
+
it('should generate meaningful descriptions for every tool', () => {
|
|
336
|
+
const registry = new ToolRegistry();
|
|
337
|
+
for (const domain of DOMAINS) {
|
|
338
|
+
for (const builder of buildDomainTools(domain)) {
|
|
339
|
+
registry.register(builder);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const tools = registry.getAllTools();
|
|
343
|
+
for (const tool of tools) {
|
|
344
|
+
// Description must mention the entity and domain
|
|
345
|
+
expect(tool.description).toBeTruthy();
|
|
346
|
+
expect(tool.description.length).toBeGreaterThan(10);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
it('should include workflow lines listing all CRUD actions', () => {
|
|
350
|
+
const builder = buildEntityTool('test', 'widgets', ['test']);
|
|
351
|
+
const def = builder.buildToolDefinition();
|
|
352
|
+
// Description should list all 7 action capabilities
|
|
353
|
+
for (const action of CRUD_ACTIONS) {
|
|
354
|
+
expect(def.description).toContain(action);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
it('should correctly aggregate readOnlyHint across mixed actions', () => {
|
|
358
|
+
var _a;
|
|
359
|
+
const builder = buildEntityTool('test', 'samples', ['test']);
|
|
360
|
+
const def = builder.buildToolDefinition();
|
|
361
|
+
// Since this tool has both read and write actions,
|
|
362
|
+
// readOnlyHint must be false (not all actions are read-only)
|
|
363
|
+
expect((_a = def.annotations) === null || _a === void 0 ? void 0 : _a.readOnlyHint).toBe(false);
|
|
364
|
+
});
|
|
365
|
+
it('should produce unique tool names across all domains', () => {
|
|
366
|
+
const registry = new ToolRegistry();
|
|
367
|
+
for (const domain of DOMAINS) {
|
|
368
|
+
for (const builder of buildDomainTools(domain)) {
|
|
369
|
+
registry.register(builder);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const names = registry.getAllTools().map(t => t.name);
|
|
373
|
+
const uniqueNames = new Set(names);
|
|
374
|
+
expect(uniqueNames.size).toBe(names.length);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
// ============================================================================
|
|
378
|
+
// Grouped Mode at Scale — Hierarchical Actions
|
|
379
|
+
// ============================================================================
|
|
380
|
+
describe('Large-Scale Scenarios — Grouped Mode Hierarchical', () => {
|
|
381
|
+
it('should support grouped tools with multiple subgroups', () => {
|
|
382
|
+
const builder = new GroupedToolBuilder('enterprise_api')
|
|
383
|
+
.description('Full enterprise API surface')
|
|
384
|
+
.tags('enterprise', 'all')
|
|
385
|
+
.group('users', 'User management', g => g
|
|
386
|
+
.action({ name: 'list', handler: async () => success('users listed') })
|
|
387
|
+
.action({ name: 'create', handler: async () => success('user created') })
|
|
388
|
+
.action({ name: 'delete', handler: async () => success('user deleted') }))
|
|
389
|
+
.group('projects', 'Project management', g => g
|
|
390
|
+
.action({ name: 'list', handler: async () => success('projects listed') })
|
|
391
|
+
.action({ name: 'create', handler: async () => success('project created') })
|
|
392
|
+
.action({ name: 'archive', handler: async () => success('project archived') }))
|
|
393
|
+
.group('billing', 'Billing operations', g => g
|
|
394
|
+
.action({ name: 'invoices', handler: async () => success('invoices listed') })
|
|
395
|
+
.action({ name: 'payments', handler: async () => success('payments listed') }));
|
|
396
|
+
const def = builder.buildToolDefinition();
|
|
397
|
+
// Should produce compound action enum: users.list, users.create, etc.
|
|
398
|
+
const actionProp = def.inputSchema.properties['action'];
|
|
399
|
+
const enumValues = actionProp['enum'];
|
|
400
|
+
expect(enumValues).toContain('users.list');
|
|
401
|
+
expect(enumValues).toContain('users.create');
|
|
402
|
+
expect(enumValues).toContain('users.delete');
|
|
403
|
+
expect(enumValues).toContain('projects.list');
|
|
404
|
+
expect(enumValues).toContain('projects.create');
|
|
405
|
+
expect(enumValues).toContain('projects.archive');
|
|
406
|
+
expect(enumValues).toContain('billing.invoices');
|
|
407
|
+
expect(enumValues).toContain('billing.payments');
|
|
408
|
+
expect(enumValues).toHaveLength(8);
|
|
409
|
+
});
|
|
410
|
+
it('should correctly route grouped hierarchical actions', async () => {
|
|
411
|
+
const builder = new GroupedToolBuilder('enterprise_api')
|
|
412
|
+
.description('API')
|
|
413
|
+
.group('users', 'Users', g => g
|
|
414
|
+
.action({
|
|
415
|
+
name: 'get',
|
|
416
|
+
schema: z.object({ id: z.string() }),
|
|
417
|
+
handler: async (_ctx, args) => success(`user ${args.id}`),
|
|
418
|
+
}))
|
|
419
|
+
.group('billing', 'Billing', g => g
|
|
420
|
+
.action({
|
|
421
|
+
name: 'charge',
|
|
422
|
+
schema: z.object({ amount: z.number() }),
|
|
423
|
+
handler: async (_ctx, args) => success(`charged ${args.amount}`),
|
|
424
|
+
}));
|
|
425
|
+
builder.buildToolDefinition();
|
|
426
|
+
const userResult = await builder.execute(undefined, {
|
|
427
|
+
action: 'users.get',
|
|
428
|
+
id: 'usr-42',
|
|
429
|
+
});
|
|
430
|
+
expect(userResult.isError).toBeUndefined();
|
|
431
|
+
expect(userResult.content[0].text).toContain('user usr-42');
|
|
432
|
+
const billingResult = await builder.execute(undefined, {
|
|
433
|
+
action: 'billing.charge',
|
|
434
|
+
amount: 99.99,
|
|
435
|
+
});
|
|
436
|
+
expect(billingResult.isError).toBeUndefined();
|
|
437
|
+
expect(billingResult.content[0].text).toContain('charged 99.99');
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
// ============================================================================
|
|
441
|
+
// Edge Cases in Large Registries
|
|
442
|
+
// ============================================================================
|
|
443
|
+
describe('Large-Scale Scenarios — Edge Cases', () => {
|
|
444
|
+
it('should handle registry clear and re-registration', () => {
|
|
445
|
+
const registry = new ToolRegistry();
|
|
446
|
+
// Register some tools
|
|
447
|
+
for (const builder of buildDomainTools(DOMAINS[0])) {
|
|
448
|
+
registry.register(builder);
|
|
449
|
+
}
|
|
450
|
+
expect(registry.size).toBe(10);
|
|
451
|
+
// Clear
|
|
452
|
+
registry.clear();
|
|
453
|
+
expect(registry.size).toBe(0);
|
|
454
|
+
expect(registry.getAllTools()).toHaveLength(0);
|
|
455
|
+
// Re-register (same names should work after clear)
|
|
456
|
+
for (const builder of buildDomainTools(DOMAINS[0])) {
|
|
457
|
+
registry.register(builder);
|
|
458
|
+
}
|
|
459
|
+
expect(registry.size).toBe(10);
|
|
460
|
+
});
|
|
461
|
+
it('should report has() correctly for all registered tools', () => {
|
|
462
|
+
const registry = new ToolRegistry();
|
|
463
|
+
for (const builder of buildDomainTools(DOMAINS[1])) {
|
|
464
|
+
registry.register(builder);
|
|
465
|
+
}
|
|
466
|
+
expect(registry.has('crm_contacts')).toBe(true);
|
|
467
|
+
expect(registry.has('crm_deals')).toBe(true);
|
|
468
|
+
expect(registry.has('nonexistent_tool')).toBe(false);
|
|
469
|
+
});
|
|
470
|
+
it('should exclude all tools when exclude tag matches all', () => {
|
|
471
|
+
const registry = new ToolRegistry();
|
|
472
|
+
// Register only PM tools (tagged 'pm', 'core')
|
|
473
|
+
for (const builder of buildDomainTools(DOMAINS[0])) {
|
|
474
|
+
registry.register(builder);
|
|
475
|
+
}
|
|
476
|
+
const filtered = registry.getTools({ exclude: ['pm'] });
|
|
477
|
+
expect(filtered).toHaveLength(0);
|
|
478
|
+
});
|
|
479
|
+
it('should handle empty registry gracefully', () => {
|
|
480
|
+
const registry = new ToolRegistry();
|
|
481
|
+
expect(registry.getAllTools()).toHaveLength(0);
|
|
482
|
+
expect(registry.getTools({ tags: ['any'] })).toHaveLength(0);
|
|
483
|
+
expect(registry.size).toBe(0);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
// ============================================================================
|
|
487
|
+
// ENTERPRISE CHAOS SCENARIOS
|
|
488
|
+
// These simulate real-world abuse patterns: LLMs sending garbage, malformed
|
|
489
|
+
// schemas, injection attempts, handler explosions, race-like registration,
|
|
490
|
+
// unicode madness, and middleware chains under pressure.
|
|
491
|
+
// ============================================================================
|
|
492
|
+
// ── Chaos 1: LLM Sends Garbage ─────────────────────────────────────────────
|
|
493
|
+
// Real scenario: LLM hallucinates action names, sends wrong types, injects
|
|
494
|
+
// extra fields, omits required params, or sends completely empty payloads.
|
|
495
|
+
describe('Enterprise Chaos — LLM Garbage Input', () => {
|
|
496
|
+
let registry;
|
|
497
|
+
beforeAll(() => {
|
|
498
|
+
registry = new ToolRegistry();
|
|
499
|
+
const builder = new GroupedToolBuilder('ticket_system')
|
|
500
|
+
.description('Manage support tickets')
|
|
501
|
+
.tags('support')
|
|
502
|
+
.commonSchema(z.object({
|
|
503
|
+
workspace_id: z.string().describe('Workspace identifier'),
|
|
504
|
+
}))
|
|
505
|
+
.action({
|
|
506
|
+
name: 'create',
|
|
507
|
+
description: 'Create a new ticket',
|
|
508
|
+
schema: z.object({
|
|
509
|
+
title: z.string().min(1).max(500),
|
|
510
|
+
priority: z.enum(['low', 'medium', 'high', 'critical']),
|
|
511
|
+
description: z.string().optional(),
|
|
512
|
+
}),
|
|
513
|
+
handler: async (_ctx, args) => success(`ticket created: ${args.title}`),
|
|
514
|
+
})
|
|
515
|
+
.action({
|
|
516
|
+
name: 'list',
|
|
517
|
+
description: 'List tickets',
|
|
518
|
+
readOnly: true,
|
|
519
|
+
handler: async (_ctx, args) => success(`listed for ${args.workspace_id}`),
|
|
520
|
+
})
|
|
521
|
+
.action({
|
|
522
|
+
name: 'close',
|
|
523
|
+
description: 'Close a ticket',
|
|
524
|
+
schema: z.object({
|
|
525
|
+
ticket_id: z.string(),
|
|
526
|
+
resolution: z.string().min(10),
|
|
527
|
+
}),
|
|
528
|
+
handler: async (_ctx, args) => success(`closed ${args.ticket_id}`),
|
|
529
|
+
});
|
|
530
|
+
registry.register(builder);
|
|
531
|
+
});
|
|
532
|
+
it('should reject completely empty payload', async () => {
|
|
533
|
+
const result = await registry.routeCall(undefined, 'ticket_system', {});
|
|
534
|
+
expect(result.isError).toBe(true);
|
|
535
|
+
});
|
|
536
|
+
it('should reject payload with no action field', async () => {
|
|
537
|
+
const result = await registry.routeCall(undefined, 'ticket_system', {
|
|
538
|
+
workspace_id: 'ws-1',
|
|
539
|
+
title: 'Bug report',
|
|
540
|
+
});
|
|
541
|
+
expect(result.isError).toBe(true);
|
|
542
|
+
});
|
|
543
|
+
it('should reject hallucinated action name', async () => {
|
|
544
|
+
const result = await registry.routeCall(undefined, 'ticket_system', {
|
|
545
|
+
action: 'reopen_and_escalate_to_manager',
|
|
546
|
+
workspace_id: 'ws-1',
|
|
547
|
+
});
|
|
548
|
+
expect(result.isError).toBe(true);
|
|
549
|
+
const text = result.content[0].text;
|
|
550
|
+
expect(text).toContain('reopen_and_escalate_to_manager');
|
|
551
|
+
});
|
|
552
|
+
it('should reject wrong types — number instead of string for workspace_id', async () => {
|
|
553
|
+
const result = await registry.routeCall(undefined, 'ticket_system', {
|
|
554
|
+
action: 'create',
|
|
555
|
+
workspace_id: 12345, // should be string
|
|
556
|
+
title: 'Test',
|
|
557
|
+
priority: 'high',
|
|
558
|
+
});
|
|
559
|
+
expect(result.isError).toBe(true);
|
|
560
|
+
});
|
|
561
|
+
it('should reject invalid enum value for priority', async () => {
|
|
562
|
+
const result = await registry.routeCall(undefined, 'ticket_system', {
|
|
563
|
+
action: 'create',
|
|
564
|
+
workspace_id: 'ws-1',
|
|
565
|
+
title: 'Test',
|
|
566
|
+
priority: 'ULTRA_MEGA_CRITICAL', // not in enum
|
|
567
|
+
});
|
|
568
|
+
expect(result.isError).toBe(true);
|
|
569
|
+
});
|
|
570
|
+
it('should strip unknown fields (LLM adds extra hallucinated params)', async () => {
|
|
571
|
+
const result = await registry.routeCall(undefined, 'ticket_system', {
|
|
572
|
+
action: 'list',
|
|
573
|
+
workspace_id: 'ws-1',
|
|
574
|
+
hallucinated_filter: 'open',
|
|
575
|
+
sort_by_moon_phase: true,
|
|
576
|
+
__internal_admin_override: true,
|
|
577
|
+
});
|
|
578
|
+
// Should succeed — extra fields are stripped by .strip()
|
|
579
|
+
expect(result.isError).toBeUndefined();
|
|
580
|
+
});
|
|
581
|
+
it('should reject too-short resolution on close', async () => {
|
|
582
|
+
const result = await registry.routeCall(undefined, 'ticket_system', {
|
|
583
|
+
action: 'close',
|
|
584
|
+
workspace_id: 'ws-1',
|
|
585
|
+
ticket_id: 'TKT-42',
|
|
586
|
+
resolution: 'done', // too short, min 10
|
|
587
|
+
});
|
|
588
|
+
expect(result.isError).toBe(true);
|
|
589
|
+
});
|
|
590
|
+
it('should reject empty string title', async () => {
|
|
591
|
+
const result = await registry.routeCall(undefined, 'ticket_system', {
|
|
592
|
+
action: 'create',
|
|
593
|
+
workspace_id: 'ws-1',
|
|
594
|
+
title: '', // min 1 char
|
|
595
|
+
priority: 'low',
|
|
596
|
+
});
|
|
597
|
+
expect(result.isError).toBe(true);
|
|
598
|
+
});
|
|
599
|
+
it('should accept valid input after multiple failures', async () => {
|
|
600
|
+
const result = await registry.routeCall(undefined, 'ticket_system', {
|
|
601
|
+
action: 'create',
|
|
602
|
+
workspace_id: 'ws-1',
|
|
603
|
+
title: 'Login page returns 500 after OAuth redirect',
|
|
604
|
+
priority: 'critical',
|
|
605
|
+
description: 'Steps to reproduce: ...',
|
|
606
|
+
});
|
|
607
|
+
expect(result.isError).toBeUndefined();
|
|
608
|
+
const text = result.content[0].text;
|
|
609
|
+
expect(text).toContain('Login page');
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
// ── Chaos 2: Handler Explosions ─────────────────────────────────────────────
|
|
613
|
+
// Real scenario: handlers throw sync errors, async rejections, return
|
|
614
|
+
// undefined, throw non-Error objects, or timeout.
|
|
615
|
+
describe('Enterprise Chaos — Handler Explosions', () => {
|
|
616
|
+
it('should catch sync throw in handler', async () => {
|
|
617
|
+
const builder = new GroupedToolBuilder('exploding_service')
|
|
618
|
+
.action({
|
|
619
|
+
name: 'boom',
|
|
620
|
+
handler: () => { throw new Error('DATABASE_CONNECTION_REFUSED'); },
|
|
621
|
+
});
|
|
622
|
+
builder.buildToolDefinition();
|
|
623
|
+
const result = await builder.execute(undefined, { action: 'boom' });
|
|
624
|
+
expect(result.isError).toBe(true);
|
|
625
|
+
expect(result.content[0].text).toContain('DATABASE_CONNECTION_REFUSED');
|
|
626
|
+
});
|
|
627
|
+
it('should catch async rejection in handler', async () => {
|
|
628
|
+
const builder = new GroupedToolBuilder('async_fail')
|
|
629
|
+
.action({
|
|
630
|
+
name: 'fetch',
|
|
631
|
+
handler: async () => { throw new Error('ECONNRESET: connection reset by peer'); },
|
|
632
|
+
});
|
|
633
|
+
builder.buildToolDefinition();
|
|
634
|
+
const result = await builder.execute(undefined, { action: 'fetch' });
|
|
635
|
+
expect(result.isError).toBe(true);
|
|
636
|
+
expect(result.content[0].text).toContain('ECONNRESET');
|
|
637
|
+
});
|
|
638
|
+
it('should handle throwing a string (non-Error throw)', async () => {
|
|
639
|
+
const builder = new GroupedToolBuilder('string_throw')
|
|
640
|
+
.action({
|
|
641
|
+
name: 'fail',
|
|
642
|
+
handler: async () => { throw 'RATE_LIMIT_EXCEEDED'; },
|
|
643
|
+
});
|
|
644
|
+
builder.buildToolDefinition();
|
|
645
|
+
const result = await builder.execute(undefined, { action: 'fail' });
|
|
646
|
+
expect(result.isError).toBe(true);
|
|
647
|
+
expect(result.content[0].text).toBe('[string_throw/fail] RATE_LIMIT_EXCEEDED');
|
|
648
|
+
});
|
|
649
|
+
it('should handle throwing an object (non-Error throw)', async () => {
|
|
650
|
+
const builder = new GroupedToolBuilder('object_throw')
|
|
651
|
+
.action({
|
|
652
|
+
name: 'process',
|
|
653
|
+
handler: async () => { throw { code: 503, message: 'Service Unavailable' }; },
|
|
654
|
+
});
|
|
655
|
+
builder.buildToolDefinition();
|
|
656
|
+
const result = await builder.execute(undefined, { action: 'process' });
|
|
657
|
+
expect(result.isError).toBe(true);
|
|
658
|
+
});
|
|
659
|
+
it('should handle throwing null', async () => {
|
|
660
|
+
const builder = new GroupedToolBuilder('null_throw')
|
|
661
|
+
.action({
|
|
662
|
+
name: 'crash',
|
|
663
|
+
handler: async () => { throw null; },
|
|
664
|
+
});
|
|
665
|
+
builder.buildToolDefinition();
|
|
666
|
+
const result = await builder.execute(undefined, { action: 'crash' });
|
|
667
|
+
expect(result.isError).toBe(true);
|
|
668
|
+
});
|
|
669
|
+
it('should handle throwing undefined', async () => {
|
|
670
|
+
const builder = new GroupedToolBuilder('undef_throw')
|
|
671
|
+
.action({
|
|
672
|
+
name: 'ghost',
|
|
673
|
+
handler: async () => { throw undefined; },
|
|
674
|
+
});
|
|
675
|
+
builder.buildToolDefinition();
|
|
676
|
+
const result = await builder.execute(undefined, { action: 'ghost' });
|
|
677
|
+
expect(result.isError).toBe(true);
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
// ── Chaos 3: Deeply Nested Schemas ──────────────────────────────────────────
|
|
681
|
+
// Real scenario: enterprise APIs have deeply nested config objects,
|
|
682
|
+
// arrays of objects, optional nested blocks, etc.
|
|
683
|
+
describe('Enterprise Chaos — Complex Nested Schemas', () => {
|
|
684
|
+
it('should validate deeply nested config objects', async () => {
|
|
685
|
+
const builder = new GroupedToolBuilder('deployment_manager')
|
|
686
|
+
.action({
|
|
687
|
+
name: 'deploy',
|
|
688
|
+
schema: z.object({
|
|
689
|
+
environment: z.enum(['staging', 'production']),
|
|
690
|
+
config: z.object({
|
|
691
|
+
replicas: z.number().int().min(1).max(100),
|
|
692
|
+
resources: z.object({
|
|
693
|
+
cpu: z.string().regex(/^\d+m$/),
|
|
694
|
+
memory: z.string().regex(/^\d+Mi$/),
|
|
695
|
+
}),
|
|
696
|
+
env_vars: z.array(z.object({
|
|
697
|
+
name: z.string(),
|
|
698
|
+
value: z.string(),
|
|
699
|
+
})).optional(),
|
|
700
|
+
}),
|
|
701
|
+
rollback_on_failure: z.boolean().default(true),
|
|
702
|
+
}),
|
|
703
|
+
handler: async (_ctx, args) => success(`deploying to ${args.environment} with ${args.config.replicas} replicas`),
|
|
704
|
+
});
|
|
705
|
+
builder.buildToolDefinition();
|
|
706
|
+
// Valid complex nested input
|
|
707
|
+
const result = await builder.execute(undefined, {
|
|
708
|
+
action: 'deploy',
|
|
709
|
+
environment: 'staging',
|
|
710
|
+
config: {
|
|
711
|
+
replicas: 3,
|
|
712
|
+
resources: { cpu: '500m', memory: '256Mi' },
|
|
713
|
+
env_vars: [
|
|
714
|
+
{ name: 'NODE_ENV', value: 'staging' },
|
|
715
|
+
{ name: 'LOG_LEVEL', value: 'debug' },
|
|
716
|
+
],
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
expect(result.isError).toBeUndefined();
|
|
720
|
+
expect(result.content[0].text).toContain('3 replicas');
|
|
721
|
+
});
|
|
722
|
+
it('should reject invalid nested resource format', async () => {
|
|
723
|
+
const builder = new GroupedToolBuilder('deploy_v2')
|
|
724
|
+
.action({
|
|
725
|
+
name: 'deploy',
|
|
726
|
+
schema: z.object({
|
|
727
|
+
config: z.object({
|
|
728
|
+
resources: z.object({
|
|
729
|
+
cpu: z.string().regex(/^\d+m$/),
|
|
730
|
+
}),
|
|
731
|
+
}),
|
|
732
|
+
}),
|
|
733
|
+
handler: async () => success('ok'),
|
|
734
|
+
});
|
|
735
|
+
builder.buildToolDefinition();
|
|
736
|
+
const result = await builder.execute(undefined, {
|
|
737
|
+
action: 'deploy',
|
|
738
|
+
config: {
|
|
739
|
+
resources: { cpu: '500cores' }, // wrong format
|
|
740
|
+
},
|
|
741
|
+
});
|
|
742
|
+
expect(result.isError).toBe(true);
|
|
743
|
+
});
|
|
744
|
+
it('should handle optional nested blocks as absent', async () => {
|
|
745
|
+
const builder = new GroupedToolBuilder('pipeline_config')
|
|
746
|
+
.action({
|
|
747
|
+
name: 'run',
|
|
748
|
+
schema: z.object({
|
|
749
|
+
pipeline: z.string(),
|
|
750
|
+
hooks: z.object({
|
|
751
|
+
before: z.string().optional(),
|
|
752
|
+
after: z.string().optional(),
|
|
753
|
+
}).optional(),
|
|
754
|
+
}),
|
|
755
|
+
handler: async (_ctx, args) => success(`running ${args.pipeline}`),
|
|
756
|
+
});
|
|
757
|
+
builder.buildToolDefinition();
|
|
758
|
+
// No hooks at all — should work
|
|
759
|
+
const result = await builder.execute(undefined, {
|
|
760
|
+
action: 'run',
|
|
761
|
+
pipeline: 'ci-main',
|
|
762
|
+
});
|
|
763
|
+
expect(result.isError).toBeUndefined();
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
// ── Chaos 4: Unicode & i18n ─────────────────────────────────────────────────
|
|
767
|
+
// Real scenario: international teams use unicode in names, descriptions,
|
|
768
|
+
// and field values. Emoji in identifiers. Multi-byte characters everywhere.
|
|
769
|
+
describe('Enterprise Chaos — Unicode & i18n', () => {
|
|
770
|
+
it('should handle unicode in tool descriptions and action values', async () => {
|
|
771
|
+
const builder = new GroupedToolBuilder('intl_service')
|
|
772
|
+
.description('Serviço de gerenciamento internacional 🌍')
|
|
773
|
+
.action({
|
|
774
|
+
name: 'create',
|
|
775
|
+
description: 'Criar novo registro — inclui suporte a múltiplos idiomas',
|
|
776
|
+
schema: z.object({
|
|
777
|
+
nome: z.string().describe('Nome do registro em qualquer idioma'),
|
|
778
|
+
descrição: z.string().optional().describe('Descrição detalhada'),
|
|
779
|
+
}),
|
|
780
|
+
handler: async (_ctx, args) => success(`criado: ${args.nome}`),
|
|
781
|
+
})
|
|
782
|
+
.action({
|
|
783
|
+
name: 'search',
|
|
784
|
+
description: '検索 — 日本語対応',
|
|
785
|
+
schema: z.object({
|
|
786
|
+
query: z.string(),
|
|
787
|
+
}),
|
|
788
|
+
handler: async (_ctx, args) => success(`results for: ${args.query}`),
|
|
789
|
+
});
|
|
790
|
+
builder.buildToolDefinition();
|
|
791
|
+
const result = await builder.execute(undefined, {
|
|
792
|
+
action: 'create',
|
|
793
|
+
nome: '项目管理工具 🚀',
|
|
794
|
+
descrição: 'Описание на русском языке с эмодзи 💼',
|
|
795
|
+
});
|
|
796
|
+
expect(result.isError).toBeUndefined();
|
|
797
|
+
expect(result.content[0].text).toContain('项目管理工具');
|
|
798
|
+
});
|
|
799
|
+
it('should handle emoji and special chars in search queries', async () => {
|
|
800
|
+
const builder = new GroupedToolBuilder('emoji_search')
|
|
801
|
+
.action({
|
|
802
|
+
name: 'find',
|
|
803
|
+
schema: z.object({
|
|
804
|
+
q: z.string(),
|
|
805
|
+
}),
|
|
806
|
+
handler: async (_ctx, args) => success(`found: ${args.q}`),
|
|
807
|
+
});
|
|
808
|
+
builder.buildToolDefinition();
|
|
809
|
+
const result = await builder.execute(undefined, {
|
|
810
|
+
action: 'find',
|
|
811
|
+
q: '🎯 café résumé naïve über straße 日本語テスト',
|
|
812
|
+
});
|
|
813
|
+
expect(result.isError).toBeUndefined();
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
// ── Chaos 5: Middleware Under Pressure ──────────────────────────────────────
|
|
817
|
+
// Real scenario: multiple middleware layers processing security, logging,
|
|
818
|
+
// rate limiting, audit trails. Middleware can short-circuit, modify context,
|
|
819
|
+
// or throw.
|
|
820
|
+
describe('Enterprise Chaos — Middleware Chains Under Pressure', () => {
|
|
821
|
+
it('should execute middleware chain in order with audit trail', async () => {
|
|
822
|
+
const auditLog = [];
|
|
823
|
+
const builder = new GroupedToolBuilder('audit_service')
|
|
824
|
+
.use(async (ctx, args, next) => {
|
|
825
|
+
auditLog.push(`auth:${ctx.userId}`);
|
|
826
|
+
return next();
|
|
827
|
+
})
|
|
828
|
+
.use(async (_ctx, args, next) => {
|
|
829
|
+
auditLog.push(`validate:${args.action}`);
|
|
830
|
+
return next();
|
|
831
|
+
})
|
|
832
|
+
.use(async (_ctx, _args, next) => {
|
|
833
|
+
auditLog.push('rate_limit:pass');
|
|
834
|
+
return next();
|
|
835
|
+
})
|
|
836
|
+
.action({
|
|
837
|
+
name: 'sensitive_operation',
|
|
838
|
+
handler: async (ctx) => {
|
|
839
|
+
auditLog.push(`execute:${ctx.userId}`);
|
|
840
|
+
return success('done');
|
|
841
|
+
},
|
|
842
|
+
});
|
|
843
|
+
builder.buildToolDefinition();
|
|
844
|
+
const result = await builder.execute({ userId: 'admin-42' }, { action: 'sensitive_operation' });
|
|
845
|
+
expect(result.isError).toBeUndefined();
|
|
846
|
+
expect(auditLog).toEqual([
|
|
847
|
+
'auth:admin-42',
|
|
848
|
+
'validate:sensitive_operation',
|
|
849
|
+
'rate_limit:pass',
|
|
850
|
+
'execute:admin-42',
|
|
851
|
+
]);
|
|
852
|
+
});
|
|
853
|
+
it('should short-circuit middleware when unauthorized', async () => {
|
|
854
|
+
const builder = new GroupedToolBuilder('admin_only')
|
|
855
|
+
.use(async (ctx, _args, next) => {
|
|
856
|
+
if (ctx.role !== 'admin') {
|
|
857
|
+
return { isError: true, content: [{ type: 'text', text: 'FORBIDDEN: admin role required' }] };
|
|
858
|
+
}
|
|
859
|
+
return next();
|
|
860
|
+
})
|
|
861
|
+
.action({
|
|
862
|
+
name: 'nuke_database',
|
|
863
|
+
description: 'Delete everything',
|
|
864
|
+
handler: async () => success('kaboom 💥'),
|
|
865
|
+
});
|
|
866
|
+
builder.buildToolDefinition();
|
|
867
|
+
// Non-admin should be blocked
|
|
868
|
+
const blocked = await builder.execute({ role: 'viewer' }, { action: 'nuke_database' });
|
|
869
|
+
expect(blocked.isError).toBe(true);
|
|
870
|
+
expect(blocked.content[0].text).toContain('FORBIDDEN');
|
|
871
|
+
// Admin should pass
|
|
872
|
+
const allowed = await builder.execute({ role: 'admin' }, { action: 'nuke_database' });
|
|
873
|
+
expect(allowed.isError).toBeUndefined();
|
|
874
|
+
});
|
|
875
|
+
it('should handle middleware that throws', async () => {
|
|
876
|
+
const builder = new GroupedToolBuilder('mw_crash')
|
|
877
|
+
.use(async () => {
|
|
878
|
+
throw new Error('MIDDLEWARE_PANIC: certificate expired');
|
|
879
|
+
})
|
|
880
|
+
.action({
|
|
881
|
+
name: 'any',
|
|
882
|
+
handler: async () => success('unreachable'),
|
|
883
|
+
});
|
|
884
|
+
builder.buildToolDefinition();
|
|
885
|
+
const result = await builder.execute(undefined, { action: 'any' });
|
|
886
|
+
expect(result.isError).toBe(true);
|
|
887
|
+
expect(result.content[0].text).toContain('certificate expired');
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
// ── Chaos 6: Multi-Tenant Context Routing ───────────────────────────────────
|
|
891
|
+
// Real scenario: SaaS multi-tenant system where context carries tenant info
|
|
892
|
+
// and different tenants have different configurations.
|
|
893
|
+
describe('Enterprise Chaos — Multi-Tenant Context', () => {
|
|
894
|
+
it('should route based on tenant context with plan enforcement', async () => {
|
|
895
|
+
const builder = new GroupedToolBuilder('billing_api')
|
|
896
|
+
.use(async (ctx, _args, next) => {
|
|
897
|
+
// Free plan can only list, not create
|
|
898
|
+
if (ctx.plan === 'free' && _args.action !== 'list') {
|
|
899
|
+
return {
|
|
900
|
+
isError: true,
|
|
901
|
+
content: [{ type: 'text', text: `UPGRADE_REQUIRED: ${_args.action} is not available on free plan` }],
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
return next();
|
|
905
|
+
})
|
|
906
|
+
.commonSchema(z.object({
|
|
907
|
+
currency: z.enum(['USD', 'EUR', 'BRL']).default('USD'),
|
|
908
|
+
}))
|
|
909
|
+
.action({
|
|
910
|
+
name: 'list',
|
|
911
|
+
readOnly: true,
|
|
912
|
+
handler: async (ctx) => success(`${ctx.tenantId}@${ctx.region}: invoices listed`),
|
|
913
|
+
})
|
|
914
|
+
.action({
|
|
915
|
+
name: 'create',
|
|
916
|
+
schema: z.object({
|
|
917
|
+
amount: z.number().positive(),
|
|
918
|
+
customer_email: z.string().email(),
|
|
919
|
+
}),
|
|
920
|
+
handler: async (ctx, args) => success(`${ctx.tenantId}: invoice $${args.amount} for ${args.customer_email}`),
|
|
921
|
+
});
|
|
922
|
+
builder.buildToolDefinition();
|
|
923
|
+
// Free tenant can list
|
|
924
|
+
const freeList = await builder.execute({ tenantId: 'acme', plan: 'free', region: 'us-east-1' }, { action: 'list', currency: 'USD' });
|
|
925
|
+
expect(freeList.isError).toBeUndefined();
|
|
926
|
+
// Free tenant cannot create
|
|
927
|
+
const freeCreate = await builder.execute({ tenantId: 'acme', plan: 'free', region: 'us-east-1' }, { action: 'create', currency: 'BRL', amount: 100, customer_email: 'test@example.com' });
|
|
928
|
+
expect(freeCreate.isError).toBe(true);
|
|
929
|
+
expect(freeCreate.content[0].text).toContain('UPGRADE_REQUIRED');
|
|
930
|
+
// Enterprise tenant can create
|
|
931
|
+
const entCreate = await builder.execute({ tenantId: 'megacorp', plan: 'enterprise', region: 'eu-west-1' }, { action: 'create', currency: 'EUR', amount: 50000, customer_email: 'cfo@megacorp.com' });
|
|
932
|
+
expect(entCreate.isError).toBeUndefined();
|
|
933
|
+
expect(entCreate.content[0].text).toContain('megacorp');
|
|
934
|
+
});
|
|
935
|
+
});
|
|
936
|
+
// ── Chaos 7: Rapid Re-registration & Hot Reload ────────────────────────────
|
|
937
|
+
// Real scenario: microservice hot-reloads during deployment, tools
|
|
938
|
+
// get cleared and re-registered rapidly. Tests state integrity.
|
|
939
|
+
describe('Enterprise Chaos — Hot Reload & Re-registration', () => {
|
|
940
|
+
it('should survive 100 clear+re-register cycles', () => {
|
|
941
|
+
const registry = new ToolRegistry();
|
|
942
|
+
for (let cycle = 0; cycle < 100; cycle++) {
|
|
943
|
+
registry.clear();
|
|
944
|
+
expect(registry.size).toBe(0);
|
|
945
|
+
const builder = new GroupedToolBuilder(`service_v${cycle}`)
|
|
946
|
+
.tags('versioned')
|
|
947
|
+
.action({
|
|
948
|
+
name: 'health',
|
|
949
|
+
handler: async () => success(`v${cycle} healthy`),
|
|
950
|
+
});
|
|
951
|
+
registry.register(builder);
|
|
952
|
+
expect(registry.size).toBe(1);
|
|
953
|
+
expect(registry.has(`service_v${cycle}`)).toBe(true);
|
|
954
|
+
}
|
|
955
|
+
// Final state should have only the last version
|
|
956
|
+
expect(registry.size).toBe(1);
|
|
957
|
+
expect(registry.has('service_v99')).toBe(true);
|
|
958
|
+
});
|
|
959
|
+
it('should maintain tag filtering correctness across multiple registrations', () => {
|
|
960
|
+
const registry = new ToolRegistry();
|
|
961
|
+
// Simulate microservices registering one by one
|
|
962
|
+
const services = [
|
|
963
|
+
{ name: 'auth_service', tags: ['auth', 'critical'] },
|
|
964
|
+
{ name: 'user_service', tags: ['users', 'critical'] },
|
|
965
|
+
{ name: 'payment_service', tags: ['payments', 'pci'] },
|
|
966
|
+
{ name: 'notification_service', tags: ['notifications'] },
|
|
967
|
+
{ name: 'analytics_service', tags: ['analytics'] },
|
|
968
|
+
{ name: 'search_service', tags: ['search', 'critical'] },
|
|
969
|
+
];
|
|
970
|
+
for (const svc of services) {
|
|
971
|
+
const builder = new GroupedToolBuilder(svc.name)
|
|
972
|
+
.tags(...svc.tags)
|
|
973
|
+
.action({ name: 'status', handler: async () => success(`${svc.name} ok`) });
|
|
974
|
+
registry.register(builder);
|
|
975
|
+
}
|
|
976
|
+
expect(registry.size).toBe(6);
|
|
977
|
+
// Critical services
|
|
978
|
+
const critical = registry.getTools({ tags: ['critical'] });
|
|
979
|
+
expect(critical).toHaveLength(3);
|
|
980
|
+
// PCI scope
|
|
981
|
+
const pci = registry.getTools({ tags: ['pci'] });
|
|
982
|
+
expect(pci).toHaveLength(1);
|
|
983
|
+
expect(pci[0].name).toBe('payment_service');
|
|
984
|
+
// Exclude analytics and search
|
|
985
|
+
const core = registry.getTools({ exclude: ['analytics', 'search'] });
|
|
986
|
+
expect(core).toHaveLength(4);
|
|
987
|
+
});
|
|
988
|
+
});
|
|
989
|
+
// ── Chaos 8: Schema Accumulation & Cross-Action Field Conflicts ─────────────
|
|
990
|
+
// Real scenario: different actions define the same field name with
|
|
991
|
+
// different types or constraints. The framework must handle this correctly.
|
|
992
|
+
describe('Enterprise Chaos — Schema Field Conflicts', () => {
|
|
993
|
+
it('should merge commonSchema + actionSchema correctly', async () => {
|
|
994
|
+
const builder = new GroupedToolBuilder('data_pipeline')
|
|
995
|
+
.commonSchema(z.object({
|
|
996
|
+
pipeline_id: z.string().uuid(),
|
|
997
|
+
dry_run: z.boolean().default(false),
|
|
998
|
+
}))
|
|
999
|
+
.action({
|
|
1000
|
+
name: 'trigger',
|
|
1001
|
+
schema: z.object({
|
|
1002
|
+
source: z.enum(['s3', 'gcs', 'azure_blob']),
|
|
1003
|
+
partition_key: z.string().optional(),
|
|
1004
|
+
}),
|
|
1005
|
+
handler: async (_ctx, args) => success(`triggered ${args.pipeline_id} from ${args.source}`),
|
|
1006
|
+
})
|
|
1007
|
+
.action({
|
|
1008
|
+
name: 'status',
|
|
1009
|
+
readOnly: true,
|
|
1010
|
+
handler: async (_ctx, args) => success(`pipeline ${args.pipeline_id} status: running`),
|
|
1011
|
+
});
|
|
1012
|
+
builder.buildToolDefinition();
|
|
1013
|
+
// Trigger with all fields
|
|
1014
|
+
const triggerResult = await builder.execute(undefined, {
|
|
1015
|
+
action: 'trigger',
|
|
1016
|
+
pipeline_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
1017
|
+
source: 's3',
|
|
1018
|
+
partition_key: '2024-01-15',
|
|
1019
|
+
});
|
|
1020
|
+
expect(triggerResult.isError).toBeUndefined();
|
|
1021
|
+
// Status with only common fields
|
|
1022
|
+
const statusResult = await builder.execute(undefined, {
|
|
1023
|
+
action: 'status',
|
|
1024
|
+
pipeline_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
1025
|
+
});
|
|
1026
|
+
expect(statusResult.isError).toBeUndefined();
|
|
1027
|
+
// Trigger with invalid UUID
|
|
1028
|
+
const badUuid = await builder.execute(undefined, {
|
|
1029
|
+
action: 'trigger',
|
|
1030
|
+
pipeline_id: 'not-a-uuid',
|
|
1031
|
+
source: 's3',
|
|
1032
|
+
});
|
|
1033
|
+
expect(badUuid.isError).toBe(true);
|
|
1034
|
+
// Trigger with invalid source enum
|
|
1035
|
+
const badSource = await builder.execute(undefined, {
|
|
1036
|
+
action: 'trigger',
|
|
1037
|
+
pipeline_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
1038
|
+
source: 'local_disk',
|
|
1039
|
+
});
|
|
1040
|
+
expect(badSource.isError).toBe(true);
|
|
1041
|
+
});
|
|
1042
|
+
});
|
|
1043
|
+
//# sourceMappingURL=LargeScaleScenarios.test.js.map
|