@objectstack/service-ai 4.0.4 → 4.0.5
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/dist/index.cjs +1176 -135
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1225 -430
- package/dist/index.d.ts +1225 -430
- package/dist/index.js +1160 -128
- package/dist/index.js.map +1 -1
- package/package.json +35 -8
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -61
- package/src/__tests__/ai-service.test.ts +0 -981
- package/src/__tests__/auth-and-toolcalling.test.ts +0 -677
- package/src/__tests__/chatbot-features.test.ts +0 -1116
- package/src/__tests__/metadata-tools.test.ts +0 -970
- package/src/__tests__/objectql-conversation-service.test.ts +0 -382
- package/src/__tests__/tool-routes.test.ts +0 -191
- package/src/__tests__/vercel-stream-encoder.test.ts +0 -310
- package/src/adapters/index.ts +0 -6
- package/src/adapters/memory-adapter.ts +0 -72
- package/src/adapters/types.ts +0 -3
- package/src/adapters/vercel-adapter.ts +0 -148
- package/src/agent-runtime.ts +0 -154
- package/src/agents/data-chat-agent.ts +0 -79
- package/src/agents/index.ts +0 -4
- package/src/agents/metadata-assistant-agent.ts +0 -87
- package/src/ai-service.ts +0 -364
- package/src/conversation/in-memory-conversation-service.ts +0 -103
- package/src/conversation/index.ts +0 -4
- package/src/conversation/objectql-conversation-service.ts +0 -301
- package/src/index.ts +0 -60
- package/src/objects/ai-conversation.object.ts +0 -86
- package/src/objects/ai-message.object.ts +0 -86
- package/src/objects/index.ts +0 -10
- package/src/plugin.ts +0 -391
- package/src/routes/agent-routes.ts +0 -190
- package/src/routes/ai-routes.ts +0 -439
- package/src/routes/index.ts +0 -5
- package/src/routes/message-utils.ts +0 -90
- package/src/routes/tool-routes.ts +0 -142
- package/src/stream/index.ts +0 -3
- package/src/stream/vercel-stream-encoder.ts +0 -153
- package/src/tools/add-field.tool.ts +0 -70
- package/src/tools/create-object.tool.ts +0 -66
- package/src/tools/data-tools.ts +0 -293
- package/src/tools/delete-field.tool.ts +0 -38
- package/src/tools/describe-object.tool.ts +0 -31
- package/src/tools/index.ts +0 -18
- package/src/tools/list-objects.tool.ts +0 -34
- package/src/tools/metadata-tools.ts +0 -430
- package/src/tools/modify-field.tool.ts +0 -44
- package/src/tools/tool-registry.ts +0 -132
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -23
|
@@ -1,970 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
-
import type { IMetadataService } from '@objectstack/spec/contracts';
|
|
5
|
-
import { ToolRegistry } from '../tools/tool-registry.js';
|
|
6
|
-
import {
|
|
7
|
-
registerMetadataTools,
|
|
8
|
-
METADATA_TOOL_DEFINITIONS,
|
|
9
|
-
} from '../tools/metadata-tools.js';
|
|
10
|
-
import {
|
|
11
|
-
registerDataTools,
|
|
12
|
-
} from '../tools/data-tools.js';
|
|
13
|
-
import type { MetadataToolContext } from '../tools/metadata-tools.js';
|
|
14
|
-
|
|
15
|
-
// Individual tool metadata imports
|
|
16
|
-
import { createObjectTool } from '../tools/create-object.tool.js';
|
|
17
|
-
import { addFieldTool } from '../tools/add-field.tool.js';
|
|
18
|
-
import { modifyFieldTool } from '../tools/modify-field.tool.js';
|
|
19
|
-
import { deleteFieldTool } from '../tools/delete-field.tool.js';
|
|
20
|
-
import { listObjectsTool } from '../tools/list-objects.tool.js';
|
|
21
|
-
import { describeObjectTool } from '../tools/describe-object.tool.js';
|
|
22
|
-
|
|
23
|
-
// ── Helpers ────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
/** Build a mock IMetadataService with optionally pre-loaded objects. */
|
|
26
|
-
function createMockMetadataService(
|
|
27
|
-
objects: Record<string, any> = {},
|
|
28
|
-
overrides: Partial<IMetadataService> = {},
|
|
29
|
-
): IMetadataService {
|
|
30
|
-
// Keep a mutable store so handlers can modify it
|
|
31
|
-
const store: Record<string, any> = { ...objects };
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
register: vi.fn(async (_type: string, name: string, data: unknown) => {
|
|
35
|
-
store[name] = data;
|
|
36
|
-
}),
|
|
37
|
-
get: vi.fn(async (_type: string, name: string) => store[name] ?? undefined),
|
|
38
|
-
list: vi.fn(async () => Object.values(store)),
|
|
39
|
-
unregister: vi.fn(async (_type: string, name: string) => {
|
|
40
|
-
delete store[name];
|
|
41
|
-
}),
|
|
42
|
-
exists: vi.fn(async (_type: string, name: string) => name in store),
|
|
43
|
-
listNames: vi.fn(async () => Object.keys(store)),
|
|
44
|
-
getObject: vi.fn(async (name: string) => store[name] ?? undefined),
|
|
45
|
-
listObjects: vi.fn(async () => Object.values(store)),
|
|
46
|
-
...overrides,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
51
|
-
// Metadata Tool Definitions
|
|
52
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
53
|
-
|
|
54
|
-
describe('Metadata Tool Definitions', () => {
|
|
55
|
-
it('should define exactly 6 tools', () => {
|
|
56
|
-
expect(METADATA_TOOL_DEFINITIONS).toHaveLength(6);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should include all expected tool names', () => {
|
|
60
|
-
const names = METADATA_TOOL_DEFINITIONS.map(t => t.name);
|
|
61
|
-
expect(names).toEqual([
|
|
62
|
-
'create_object',
|
|
63
|
-
'add_field',
|
|
64
|
-
'modify_field',
|
|
65
|
-
'delete_field',
|
|
66
|
-
'list_objects',
|
|
67
|
-
'describe_object',
|
|
68
|
-
]);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('should have descriptions and parameters for each tool', () => {
|
|
72
|
-
for (const def of METADATA_TOOL_DEFINITIONS) {
|
|
73
|
-
expect(def.description).toBeTruthy();
|
|
74
|
-
expect(def.parameters).toBeDefined();
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
80
|
-
// Individual Tool Metadata Files (.tool.ts)
|
|
81
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
82
|
-
|
|
83
|
-
describe('Individual Tool Metadata (.tool.ts)', () => {
|
|
84
|
-
const tools = [
|
|
85
|
-
{ tool: createObjectTool, expectedName: 'create_object', expectedLabel: 'Create Object' },
|
|
86
|
-
{ tool: addFieldTool, expectedName: 'add_field', expectedLabel: 'Add Field' },
|
|
87
|
-
{ tool: modifyFieldTool, expectedName: 'modify_field', expectedLabel: 'Modify Field' },
|
|
88
|
-
{ tool: deleteFieldTool, expectedName: 'delete_field', expectedLabel: 'Delete Field' },
|
|
89
|
-
{ tool: listObjectsTool, expectedName: 'list_objects', expectedLabel: 'List Objects' },
|
|
90
|
-
{ tool: describeObjectTool, expectedName: 'describe_object', expectedLabel: 'Describe Object' },
|
|
91
|
-
];
|
|
92
|
-
|
|
93
|
-
for (const { tool, expectedName, expectedLabel } of tools) {
|
|
94
|
-
describe(expectedName, () => {
|
|
95
|
-
it('should have correct name', () => {
|
|
96
|
-
expect(tool.name).toBe(expectedName);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('should have a label', () => {
|
|
100
|
-
expect(tool.label).toBe(expectedLabel);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should be categorized as data', () => {
|
|
104
|
-
expect(tool.category).toBe('data');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should be marked as built-in', () => {
|
|
108
|
-
expect(tool.builtIn).toBe(true);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should have a description', () => {
|
|
112
|
-
expect(tool.description).toBeTruthy();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should have parameters schema', () => {
|
|
116
|
-
expect(tool.parameters).toBeDefined();
|
|
117
|
-
expect(tool.parameters.type).toBe('object');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should be included in METADATA_TOOL_DEFINITIONS', () => {
|
|
121
|
-
expect(METADATA_TOOL_DEFINITIONS).toContain(tool);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
it('should not set requiresConfirmation on create_object (server-side enforcement not yet implemented)', () => {
|
|
127
|
-
expect(createObjectTool.requiresConfirmation).toBe(false);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('should not set requiresConfirmation on delete_field (server-side enforcement not yet implemented)', () => {
|
|
131
|
-
expect(deleteFieldTool.requiresConfirmation).toBe(false);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('should not mark read-only tools as requiresConfirmation', () => {
|
|
135
|
-
expect(listObjectsTool.requiresConfirmation).toBe(false);
|
|
136
|
-
expect(describeObjectTool.requiresConfirmation).toBe(false);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should not mark add_field and modify_field as requiresConfirmation', () => {
|
|
140
|
-
expect(addFieldTool.requiresConfirmation).toBe(false);
|
|
141
|
-
expect(modifyFieldTool.requiresConfirmation).toBe(false);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
146
|
-
// registerMetadataTools + Handlers
|
|
147
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
148
|
-
|
|
149
|
-
describe('registerMetadataTools', () => {
|
|
150
|
-
let registry: ToolRegistry;
|
|
151
|
-
let metadataService: IMetadataService;
|
|
152
|
-
|
|
153
|
-
beforeEach(() => {
|
|
154
|
-
registry = new ToolRegistry();
|
|
155
|
-
metadataService = createMockMetadataService();
|
|
156
|
-
registerMetadataTools(registry, { metadataService });
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('should register all 6 tools', () => {
|
|
160
|
-
expect(registry.size).toBe(6);
|
|
161
|
-
expect(registry.has('create_object')).toBe(true);
|
|
162
|
-
expect(registry.has('add_field')).toBe(true);
|
|
163
|
-
expect(registry.has('modify_field')).toBe(true);
|
|
164
|
-
expect(registry.has('delete_field')).toBe(true);
|
|
165
|
-
expect(registry.has('list_objects')).toBe(true);
|
|
166
|
-
expect(registry.has('describe_object')).toBe(true);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
171
|
-
// Dual registration (data tools + metadata tools)
|
|
172
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
173
|
-
|
|
174
|
-
describe('registerDataTools + registerMetadataTools — unified list/describe', () => {
|
|
175
|
-
it('should register both tool sets on the same registry with shared list_objects and describe_object', () => {
|
|
176
|
-
const registry = new ToolRegistry();
|
|
177
|
-
const metadataService = createMockMetadataService();
|
|
178
|
-
const dataEngine = {
|
|
179
|
-
find: vi.fn(),
|
|
180
|
-
findOne: vi.fn(),
|
|
181
|
-
aggregate: vi.fn(),
|
|
182
|
-
} as any;
|
|
183
|
-
|
|
184
|
-
registerDataTools(registry, { dataEngine });
|
|
185
|
-
const sizeAfterData = registry.size;
|
|
186
|
-
|
|
187
|
-
registerMetadataTools(registry, { metadataService });
|
|
188
|
-
const sizeAfterBoth = registry.size;
|
|
189
|
-
|
|
190
|
-
// Data tools define: query_records, get_record, aggregate_data (3)
|
|
191
|
-
// Metadata tools define: create_object, add_field, modify_field, delete_field, list_objects, describe_object (6)
|
|
192
|
-
// Total should be 3 + 6 = 9
|
|
193
|
-
expect(sizeAfterData).toBe(3);
|
|
194
|
-
expect(sizeAfterBoth).toBe(sizeAfterData + 6);
|
|
195
|
-
|
|
196
|
-
// Unified list/describe should be present (from metadata tools)
|
|
197
|
-
expect(registry.has('list_objects')).toBe(true);
|
|
198
|
-
expect(registry.has('describe_object')).toBe(true);
|
|
199
|
-
|
|
200
|
-
// Data-only tools should be present
|
|
201
|
-
expect(registry.has('query_records')).toBe(true);
|
|
202
|
-
expect(registry.has('get_record')).toBe(true);
|
|
203
|
-
expect(registry.has('aggregate_data')).toBe(true);
|
|
204
|
-
|
|
205
|
-
// Metadata-only tools should be present
|
|
206
|
-
expect(registry.has('create_object')).toBe(true);
|
|
207
|
-
expect(registry.has('add_field')).toBe(true);
|
|
208
|
-
expect(registry.has('modify_field')).toBe(true);
|
|
209
|
-
expect(registry.has('delete_field')).toBe(true);
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
214
|
-
// create_object handler
|
|
215
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
216
|
-
|
|
217
|
-
describe('create_object handler', () => {
|
|
218
|
-
let registry: ToolRegistry;
|
|
219
|
-
let metadataService: IMetadataService;
|
|
220
|
-
|
|
221
|
-
beforeEach(() => {
|
|
222
|
-
registry = new ToolRegistry();
|
|
223
|
-
metadataService = createMockMetadataService();
|
|
224
|
-
registerMetadataTools(registry, { metadataService });
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('should create object with name and label', async () => {
|
|
228
|
-
const result = await registry.execute({
|
|
229
|
-
type: 'tool-call' as const,
|
|
230
|
-
toolCallId: 'c1',
|
|
231
|
-
toolName: 'create_object',
|
|
232
|
-
input: { name: 'project', label: 'Project' },
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
236
|
-
expect(parsed.name).toBe('project');
|
|
237
|
-
expect(parsed.label).toBe('Project');
|
|
238
|
-
expect(parsed.fieldCount).toBe(0);
|
|
239
|
-
expect(metadataService.register).toHaveBeenCalledWith(
|
|
240
|
-
'object',
|
|
241
|
-
'project',
|
|
242
|
-
expect.objectContaining({ name: 'project', label: 'Project' }),
|
|
243
|
-
);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('should create object with initial fields', async () => {
|
|
247
|
-
const result = await registry.execute({
|
|
248
|
-
type: 'tool-call' as const,
|
|
249
|
-
toolCallId: 'c2',
|
|
250
|
-
toolName: 'create_object',
|
|
251
|
-
input: {
|
|
252
|
-
name: 'task',
|
|
253
|
-
label: 'Task',
|
|
254
|
-
fields: [
|
|
255
|
-
{ name: 'title', type: 'text', label: 'Title', required: true },
|
|
256
|
-
{ name: 'status', type: 'select' },
|
|
257
|
-
],
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
262
|
-
expect(parsed.name).toBe('task');
|
|
263
|
-
expect(parsed.fieldCount).toBe(2);
|
|
264
|
-
expect(metadataService.register).toHaveBeenCalledWith(
|
|
265
|
-
'object',
|
|
266
|
-
'task',
|
|
267
|
-
expect.objectContaining({
|
|
268
|
-
fields: {
|
|
269
|
-
title: { type: 'text', label: 'Title', required: true },
|
|
270
|
-
status: { type: 'select' },
|
|
271
|
-
},
|
|
272
|
-
}),
|
|
273
|
-
);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it('should create object with enableFeatures', async () => {
|
|
277
|
-
await registry.execute({
|
|
278
|
-
type: 'tool-call' as const,
|
|
279
|
-
toolCallId: 'c3',
|
|
280
|
-
toolName: 'create_object',
|
|
281
|
-
input: {
|
|
282
|
-
name: 'account',
|
|
283
|
-
label: 'Account',
|
|
284
|
-
enableFeatures: { trackHistory: true, apiEnabled: true },
|
|
285
|
-
},
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
expect(metadataService.register).toHaveBeenCalledWith(
|
|
289
|
-
'object',
|
|
290
|
-
'account',
|
|
291
|
-
expect.objectContaining({
|
|
292
|
-
enable: { trackHistory: true, apiEnabled: true },
|
|
293
|
-
}),
|
|
294
|
-
);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it('should reject invalid snake_case name', async () => {
|
|
298
|
-
const result = await registry.execute({
|
|
299
|
-
type: 'tool-call' as const,
|
|
300
|
-
toolCallId: 'c4',
|
|
301
|
-
toolName: 'create_object',
|
|
302
|
-
input: { name: 'MyProject', label: 'My Project' },
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
306
|
-
expect(parsed.error).toContain('snake_case');
|
|
307
|
-
expect(metadataService.register).not.toHaveBeenCalled();
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it('should reject duplicate object names', async () => {
|
|
311
|
-
// Pre-populate the store
|
|
312
|
-
metadataService = createMockMetadataService({
|
|
313
|
-
project: { name: 'project', label: 'Project' },
|
|
314
|
-
});
|
|
315
|
-
registry = new ToolRegistry();
|
|
316
|
-
registerMetadataTools(registry, { metadataService });
|
|
317
|
-
|
|
318
|
-
const result = await registry.execute({
|
|
319
|
-
type: 'tool-call' as const,
|
|
320
|
-
toolCallId: 'c5',
|
|
321
|
-
toolName: 'create_object',
|
|
322
|
-
input: { name: 'project', label: 'Project v2' },
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
326
|
-
expect(parsed.error).toContain('already exists');
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it('should return error when name or label is missing', async () => {
|
|
330
|
-
const result = await registry.execute({
|
|
331
|
-
type: 'tool-call' as const,
|
|
332
|
-
toolCallId: 'c6',
|
|
333
|
-
toolName: 'create_object',
|
|
334
|
-
input: { name: 'project' },
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
338
|
-
expect(parsed.error).toContain('required');
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it('should reject fields with invalid snake_case names', async () => {
|
|
342
|
-
const result = await registry.execute({
|
|
343
|
-
type: 'tool-call' as const,
|
|
344
|
-
toolCallId: 'c7',
|
|
345
|
-
toolName: 'create_object',
|
|
346
|
-
input: {
|
|
347
|
-
name: 'project',
|
|
348
|
-
label: 'Project',
|
|
349
|
-
fields: [
|
|
350
|
-
{ name: 'ValidField', type: 'text' },
|
|
351
|
-
],
|
|
352
|
-
},
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
356
|
-
expect(parsed.error).toContain('snake_case');
|
|
357
|
-
expect(metadataService.register).not.toHaveBeenCalled();
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it('should reject fields with duplicate names', async () => {
|
|
361
|
-
const result = await registry.execute({
|
|
362
|
-
type: 'tool-call' as const,
|
|
363
|
-
toolCallId: 'c8',
|
|
364
|
-
toolName: 'create_object',
|
|
365
|
-
input: {
|
|
366
|
-
name: 'project',
|
|
367
|
-
label: 'Project',
|
|
368
|
-
fields: [
|
|
369
|
-
{ name: 'status', type: 'text' },
|
|
370
|
-
{ name: 'status', type: 'select' },
|
|
371
|
-
],
|
|
372
|
-
},
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
376
|
-
expect(parsed.error).toContain('Duplicate');
|
|
377
|
-
expect(metadataService.register).not.toHaveBeenCalled();
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
it('should reject fields with missing name', async () => {
|
|
381
|
-
const result = await registry.execute({
|
|
382
|
-
type: 'tool-call' as const,
|
|
383
|
-
toolCallId: 'c9',
|
|
384
|
-
toolName: 'create_object',
|
|
385
|
-
input: {
|
|
386
|
-
name: 'project',
|
|
387
|
-
label: 'Project',
|
|
388
|
-
fields: [
|
|
389
|
-
{ type: 'text' },
|
|
390
|
-
],
|
|
391
|
-
},
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
395
|
-
expect(parsed.error).toBeTruthy();
|
|
396
|
-
expect(metadataService.register).not.toHaveBeenCalled();
|
|
397
|
-
});
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
401
|
-
// add_field handler
|
|
402
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
403
|
-
|
|
404
|
-
describe('add_field handler', () => {
|
|
405
|
-
let registry: ToolRegistry;
|
|
406
|
-
let metadataService: IMetadataService;
|
|
407
|
-
|
|
408
|
-
beforeEach(() => {
|
|
409
|
-
metadataService = createMockMetadataService({
|
|
410
|
-
project: { name: 'project', label: 'Project', fields: {} },
|
|
411
|
-
});
|
|
412
|
-
registry = new ToolRegistry();
|
|
413
|
-
registerMetadataTools(registry, { metadataService });
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
it('should add a field to an existing object', async () => {
|
|
417
|
-
const result = await registry.execute({
|
|
418
|
-
type: 'tool-call' as const,
|
|
419
|
-
toolCallId: 'c1',
|
|
420
|
-
toolName: 'add_field',
|
|
421
|
-
input: { objectName: 'project', name: 'due_date', type: 'date', label: 'Due Date' },
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
425
|
-
expect(parsed.objectName).toBe('project');
|
|
426
|
-
expect(parsed.fieldName).toBe('due_date');
|
|
427
|
-
expect(parsed.fieldType).toBe('date');
|
|
428
|
-
expect(metadataService.register).toHaveBeenCalledWith(
|
|
429
|
-
'object',
|
|
430
|
-
'project',
|
|
431
|
-
expect.objectContaining({
|
|
432
|
-
fields: expect.objectContaining({
|
|
433
|
-
due_date: expect.objectContaining({ type: 'date', label: 'Due Date' }),
|
|
434
|
-
}),
|
|
435
|
-
}),
|
|
436
|
-
);
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
it('should add a field with options (select type)', async () => {
|
|
440
|
-
await registry.execute({
|
|
441
|
-
type: 'tool-call' as const,
|
|
442
|
-
toolCallId: 'c2',
|
|
443
|
-
toolName: 'add_field',
|
|
444
|
-
input: {
|
|
445
|
-
objectName: 'project',
|
|
446
|
-
name: 'priority',
|
|
447
|
-
type: 'select',
|
|
448
|
-
options: [
|
|
449
|
-
{ label: 'Low', value: 'low' },
|
|
450
|
-
{ label: 'High', value: 'high' },
|
|
451
|
-
],
|
|
452
|
-
},
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
expect(metadataService.register).toHaveBeenCalledWith(
|
|
456
|
-
'object',
|
|
457
|
-
'project',
|
|
458
|
-
expect.objectContaining({
|
|
459
|
-
fields: expect.objectContaining({
|
|
460
|
-
priority: expect.objectContaining({
|
|
461
|
-
type: 'select',
|
|
462
|
-
options: [{ label: 'Low', value: 'low' }, { label: 'High', value: 'high' }],
|
|
463
|
-
}),
|
|
464
|
-
}),
|
|
465
|
-
}),
|
|
466
|
-
);
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
it('should reject adding field to non-existent object', async () => {
|
|
470
|
-
const result = await registry.execute({
|
|
471
|
-
type: 'tool-call' as const,
|
|
472
|
-
toolCallId: 'c3',
|
|
473
|
-
toolName: 'add_field',
|
|
474
|
-
input: { objectName: 'nonexistent', name: 'field_a', type: 'text' },
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
478
|
-
expect(parsed.error).toContain('not found');
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
it('should reject duplicate field name', async () => {
|
|
482
|
-
// Add the field first
|
|
483
|
-
await registry.execute({
|
|
484
|
-
type: 'tool-call' as const,
|
|
485
|
-
toolCallId: 'c4a',
|
|
486
|
-
toolName: 'add_field',
|
|
487
|
-
input: { objectName: 'project', name: 'status', type: 'text' },
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
// Try to add the same field again
|
|
491
|
-
const result = await registry.execute({
|
|
492
|
-
type: 'tool-call' as const,
|
|
493
|
-
toolCallId: 'c4b',
|
|
494
|
-
toolName: 'add_field',
|
|
495
|
-
input: { objectName: 'project', name: 'status', type: 'select' },
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
499
|
-
expect(parsed.error).toContain('already exists');
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
it('should reject invalid field name', async () => {
|
|
503
|
-
const result = await registry.execute({
|
|
504
|
-
type: 'tool-call' as const,
|
|
505
|
-
toolCallId: 'c5',
|
|
506
|
-
toolName: 'add_field',
|
|
507
|
-
input: { objectName: 'project', name: 'MyField', type: 'text' },
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
511
|
-
expect(parsed.error).toContain('snake_case');
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
it('should reject invalid objectName (not snake_case)', async () => {
|
|
515
|
-
const result = await registry.execute({
|
|
516
|
-
type: 'tool-call' as const,
|
|
517
|
-
toolCallId: 'c6',
|
|
518
|
-
toolName: 'add_field',
|
|
519
|
-
input: { objectName: 'MyProject', name: 'status', type: 'text' },
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
523
|
-
expect(parsed.error).toContain('snake_case');
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
it('should accept reference as a string (not object)', async () => {
|
|
527
|
-
const result = await registry.execute({
|
|
528
|
-
type: 'tool-call' as const,
|
|
529
|
-
toolCallId: 'c7',
|
|
530
|
-
toolName: 'add_field',
|
|
531
|
-
input: { objectName: 'project', name: 'account_id', type: 'lookup', reference: 'account' },
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
535
|
-
expect(parsed.fieldName).toBe('account_id');
|
|
536
|
-
expect(metadataService.register).toHaveBeenCalledWith(
|
|
537
|
-
'object',
|
|
538
|
-
'project',
|
|
539
|
-
expect.objectContaining({
|
|
540
|
-
fields: expect.objectContaining({
|
|
541
|
-
account_id: expect.objectContaining({ type: 'lookup', reference: 'account' }),
|
|
542
|
-
}),
|
|
543
|
-
}),
|
|
544
|
-
);
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
it('should reject invalid reference (not snake_case)', async () => {
|
|
548
|
-
const result = await registry.execute({
|
|
549
|
-
type: 'tool-call' as const,
|
|
550
|
-
toolCallId: 'c8',
|
|
551
|
-
toolName: 'add_field',
|
|
552
|
-
input: { objectName: 'project', name: 'account_id', type: 'lookup', reference: 'MyAccount' },
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
556
|
-
expect(parsed.error).toContain('snake_case');
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
it('should reject invalid select option values (not snake_case)', async () => {
|
|
560
|
-
const result = await registry.execute({
|
|
561
|
-
type: 'tool-call' as const,
|
|
562
|
-
toolCallId: 'c9',
|
|
563
|
-
toolName: 'add_field',
|
|
564
|
-
input: {
|
|
565
|
-
objectName: 'project',
|
|
566
|
-
name: 'priority',
|
|
567
|
-
type: 'select',
|
|
568
|
-
options: [
|
|
569
|
-
{ label: 'High Priority', value: 'HighPriority' },
|
|
570
|
-
],
|
|
571
|
-
},
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
575
|
-
expect(parsed.error).toContain('snake_case');
|
|
576
|
-
});
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
580
|
-
// modify_field handler
|
|
581
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
582
|
-
|
|
583
|
-
describe('modify_field handler', () => {
|
|
584
|
-
let registry: ToolRegistry;
|
|
585
|
-
let metadataService: IMetadataService;
|
|
586
|
-
|
|
587
|
-
beforeEach(() => {
|
|
588
|
-
metadataService = createMockMetadataService({
|
|
589
|
-
project: {
|
|
590
|
-
name: 'project',
|
|
591
|
-
label: 'Project',
|
|
592
|
-
fields: {
|
|
593
|
-
status: { type: 'text', label: 'Status', required: false },
|
|
594
|
-
budget: { type: 'number', label: 'Budget' },
|
|
595
|
-
},
|
|
596
|
-
},
|
|
597
|
-
});
|
|
598
|
-
registry = new ToolRegistry();
|
|
599
|
-
registerMetadataTools(registry, { metadataService });
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
it('should modify field label', async () => {
|
|
603
|
-
const result = await registry.execute({
|
|
604
|
-
type: 'tool-call' as const,
|
|
605
|
-
toolCallId: 'c1',
|
|
606
|
-
toolName: 'modify_field',
|
|
607
|
-
input: {
|
|
608
|
-
objectName: 'project',
|
|
609
|
-
fieldName: 'status',
|
|
610
|
-
changes: { label: 'Project Status' },
|
|
611
|
-
},
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
615
|
-
expect(parsed.objectName).toBe('project');
|
|
616
|
-
expect(parsed.fieldName).toBe('status');
|
|
617
|
-
expect(parsed.updatedProperties).toEqual(['label']);
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
it('should modify multiple field properties', async () => {
|
|
621
|
-
const result = await registry.execute({
|
|
622
|
-
type: 'tool-call' as const,
|
|
623
|
-
toolCallId: 'c2',
|
|
624
|
-
toolName: 'modify_field',
|
|
625
|
-
input: {
|
|
626
|
-
objectName: 'project',
|
|
627
|
-
fieldName: 'status',
|
|
628
|
-
changes: { label: 'Project Status', required: true },
|
|
629
|
-
},
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
633
|
-
expect(parsed.updatedProperties).toEqual(expect.arrayContaining(['label', 'required']));
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
it('should return error for non-existent object', async () => {
|
|
637
|
-
const result = await registry.execute({
|
|
638
|
-
type: 'tool-call' as const,
|
|
639
|
-
toolCallId: 'c3',
|
|
640
|
-
toolName: 'modify_field',
|
|
641
|
-
input: {
|
|
642
|
-
objectName: 'nonexistent',
|
|
643
|
-
fieldName: 'status',
|
|
644
|
-
changes: { label: 'New' },
|
|
645
|
-
},
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
649
|
-
expect(parsed.error).toContain('not found');
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
it('should return error for non-existent field', async () => {
|
|
653
|
-
const result = await registry.execute({
|
|
654
|
-
type: 'tool-call' as const,
|
|
655
|
-
toolCallId: 'c4',
|
|
656
|
-
toolName: 'modify_field',
|
|
657
|
-
input: {
|
|
658
|
-
objectName: 'project',
|
|
659
|
-
fieldName: 'nonexistent_field',
|
|
660
|
-
changes: { label: 'New' },
|
|
661
|
-
},
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
665
|
-
expect(parsed.error).toContain('not found');
|
|
666
|
-
});
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
670
|
-
// delete_field handler
|
|
671
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
672
|
-
|
|
673
|
-
describe('delete_field handler', () => {
|
|
674
|
-
let registry: ToolRegistry;
|
|
675
|
-
let metadataService: IMetadataService;
|
|
676
|
-
|
|
677
|
-
beforeEach(() => {
|
|
678
|
-
metadataService = createMockMetadataService({
|
|
679
|
-
project: {
|
|
680
|
-
name: 'project',
|
|
681
|
-
label: 'Project',
|
|
682
|
-
fields: {
|
|
683
|
-
status: { type: 'text', label: 'Status' },
|
|
684
|
-
budget: { type: 'number', label: 'Budget' },
|
|
685
|
-
},
|
|
686
|
-
},
|
|
687
|
-
});
|
|
688
|
-
registry = new ToolRegistry();
|
|
689
|
-
registerMetadataTools(registry, { metadataService });
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
it('should delete a field from an object', async () => {
|
|
693
|
-
const result = await registry.execute({
|
|
694
|
-
type: 'tool-call' as const,
|
|
695
|
-
toolCallId: 'c1',
|
|
696
|
-
toolName: 'delete_field',
|
|
697
|
-
input: { objectName: 'project', fieldName: 'budget' },
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
701
|
-
expect(parsed.objectName).toBe('project');
|
|
702
|
-
expect(parsed.fieldName).toBe('budget');
|
|
703
|
-
expect(parsed.success).toBe(true);
|
|
704
|
-
|
|
705
|
-
// Verify the field was removed from the re-registered object
|
|
706
|
-
expect(metadataService.register).toHaveBeenCalledWith(
|
|
707
|
-
'object',
|
|
708
|
-
'project',
|
|
709
|
-
expect.objectContaining({
|
|
710
|
-
fields: expect.not.objectContaining({ budget: expect.anything() }),
|
|
711
|
-
}),
|
|
712
|
-
);
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
it('should return error for non-existent object', async () => {
|
|
716
|
-
const result = await registry.execute({
|
|
717
|
-
type: 'tool-call' as const,
|
|
718
|
-
toolCallId: 'c2',
|
|
719
|
-
toolName: 'delete_field',
|
|
720
|
-
input: { objectName: 'nonexistent', fieldName: 'status' },
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
724
|
-
expect(parsed.error).toContain('not found');
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
it('should return error for non-existent field', async () => {
|
|
728
|
-
const result = await registry.execute({
|
|
729
|
-
type: 'tool-call' as const,
|
|
730
|
-
toolCallId: 'c3',
|
|
731
|
-
toolName: 'delete_field',
|
|
732
|
-
input: { objectName: 'project', fieldName: 'nonexistent_field' },
|
|
733
|
-
});
|
|
734
|
-
|
|
735
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
736
|
-
expect(parsed.error).toContain('not found');
|
|
737
|
-
});
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
741
|
-
// list_metadata_objects handler
|
|
742
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
743
|
-
|
|
744
|
-
describe('list_metadata_objects handler', () => {
|
|
745
|
-
let registry: ToolRegistry;
|
|
746
|
-
let metadataService: IMetadataService;
|
|
747
|
-
|
|
748
|
-
beforeEach(() => {
|
|
749
|
-
metadataService = createMockMetadataService({
|
|
750
|
-
account: { name: 'account', label: 'Account', fields: { name: { type: 'text' } } },
|
|
751
|
-
contact: { name: 'contact', label: 'Contact', fields: { email: { type: 'text' }, phone: { type: 'text' } } },
|
|
752
|
-
});
|
|
753
|
-
registry = new ToolRegistry();
|
|
754
|
-
registerMetadataTools(registry, { metadataService });
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
it('should list all objects with name, label, and field count', async () => {
|
|
758
|
-
const result = await registry.execute({
|
|
759
|
-
type: 'tool-call' as const,
|
|
760
|
-
toolCallId: 'c1',
|
|
761
|
-
toolName: 'list_objects',
|
|
762
|
-
input: {},
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
766
|
-
expect(parsed.totalCount).toBe(2);
|
|
767
|
-
expect(parsed.objects).toHaveLength(2);
|
|
768
|
-
expect(parsed.objects[0]).toEqual(expect.objectContaining({ name: 'account', label: 'Account', fieldCount: 1 }));
|
|
769
|
-
expect(parsed.objects[1]).toEqual(expect.objectContaining({ name: 'contact', label: 'Contact', fieldCount: 2 }));
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
it('should filter objects by name/label substring', async () => {
|
|
773
|
-
const result = await registry.execute({
|
|
774
|
-
type: 'tool-call' as const,
|
|
775
|
-
toolCallId: 'c2',
|
|
776
|
-
toolName: 'list_objects',
|
|
777
|
-
input: { filter: 'account' },
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
781
|
-
expect(parsed.totalCount).toBe(1);
|
|
782
|
-
expect(parsed.objects[0].name).toBe('account');
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
it('should include field summaries when includeFields is true', async () => {
|
|
786
|
-
const result = await registry.execute({
|
|
787
|
-
type: 'tool-call' as const,
|
|
788
|
-
toolCallId: 'c3',
|
|
789
|
-
toolName: 'list_objects',
|
|
790
|
-
input: { includeFields: true },
|
|
791
|
-
});
|
|
792
|
-
|
|
793
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
794
|
-
expect(parsed.objects[0].fields).toBeDefined();
|
|
795
|
-
expect(parsed.objects[0].fields).toHaveLength(1);
|
|
796
|
-
});
|
|
797
|
-
|
|
798
|
-
it('should return empty list when no objects exist', async () => {
|
|
799
|
-
metadataService = createMockMetadataService({});
|
|
800
|
-
registry = new ToolRegistry();
|
|
801
|
-
registerMetadataTools(registry, { metadataService });
|
|
802
|
-
|
|
803
|
-
const result = await registry.execute({
|
|
804
|
-
type: 'tool-call' as const,
|
|
805
|
-
toolCallId: 'c4',
|
|
806
|
-
toolName: 'list_objects',
|
|
807
|
-
input: {},
|
|
808
|
-
});
|
|
809
|
-
|
|
810
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
811
|
-
expect(parsed.totalCount).toBe(0);
|
|
812
|
-
expect(parsed.objects).toHaveLength(0);
|
|
813
|
-
});
|
|
814
|
-
});
|
|
815
|
-
|
|
816
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
817
|
-
// describe_metadata_object handler
|
|
818
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
819
|
-
|
|
820
|
-
describe('describe_metadata_object handler', () => {
|
|
821
|
-
let registry: ToolRegistry;
|
|
822
|
-
let metadataService: IMetadataService;
|
|
823
|
-
|
|
824
|
-
beforeEach(() => {
|
|
825
|
-
metadataService = createMockMetadataService({
|
|
826
|
-
account: {
|
|
827
|
-
name: 'account',
|
|
828
|
-
label: 'Account',
|
|
829
|
-
fields: {
|
|
830
|
-
name: { type: 'text', label: 'Account Name', required: true },
|
|
831
|
-
revenue: { type: 'number', label: 'Revenue' },
|
|
832
|
-
industry: { type: 'select', label: 'Industry', options: ['Tech', 'Finance'] },
|
|
833
|
-
},
|
|
834
|
-
enable: { trackHistory: true, apiEnabled: true },
|
|
835
|
-
},
|
|
836
|
-
});
|
|
837
|
-
registry = new ToolRegistry();
|
|
838
|
-
registerMetadataTools(registry, { metadataService });
|
|
839
|
-
});
|
|
840
|
-
|
|
841
|
-
it('should return full schema details with field array', async () => {
|
|
842
|
-
const result = await registry.execute({
|
|
843
|
-
type: 'tool-call' as const,
|
|
844
|
-
toolCallId: 'c1',
|
|
845
|
-
toolName: 'describe_object',
|
|
846
|
-
input: { objectName: 'account' },
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
850
|
-
expect(parsed.name).toBe('account');
|
|
851
|
-
expect(parsed.label).toBe('Account');
|
|
852
|
-
expect(parsed.fields).toHaveLength(3);
|
|
853
|
-
expect(parsed.enableFeatures).toEqual({ trackHistory: true, apiEnabled: true });
|
|
854
|
-
|
|
855
|
-
const nameField = parsed.fields.find((f: any) => f.name === 'name');
|
|
856
|
-
expect(nameField.type).toBe('text');
|
|
857
|
-
expect(nameField.required).toBe(true);
|
|
858
|
-
|
|
859
|
-
const industryField = parsed.fields.find((f: any) => f.name === 'industry');
|
|
860
|
-
expect(industryField.options).toEqual(['Tech', 'Finance']);
|
|
861
|
-
});
|
|
862
|
-
|
|
863
|
-
it('should return error for unknown object', async () => {
|
|
864
|
-
const result = await registry.execute({
|
|
865
|
-
type: 'tool-call' as const,
|
|
866
|
-
toolCallId: 'c2',
|
|
867
|
-
toolName: 'describe_object',
|
|
868
|
-
input: { objectName: 'nonexistent' },
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
const parsed = JSON.parse((result.output as any).value);
|
|
872
|
-
expect(parsed.error).toContain('not found');
|
|
873
|
-
});
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
877
|
-
// End-to-End: full lifecycle
|
|
878
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
879
|
-
|
|
880
|
-
describe('Metadata Tools — full lifecycle', () => {
|
|
881
|
-
let registry: ToolRegistry;
|
|
882
|
-
let metadataService: IMetadataService;
|
|
883
|
-
|
|
884
|
-
beforeEach(() => {
|
|
885
|
-
metadataService = createMockMetadataService();
|
|
886
|
-
registry = new ToolRegistry();
|
|
887
|
-
registerMetadataTools(registry, { metadataService });
|
|
888
|
-
});
|
|
889
|
-
|
|
890
|
-
it('should support create → add_field → describe → modify → delete lifecycle', async () => {
|
|
891
|
-
// 1. Create object
|
|
892
|
-
await registry.execute({
|
|
893
|
-
type: 'tool-call' as const,
|
|
894
|
-
toolCallId: 's1',
|
|
895
|
-
toolName: 'create_object',
|
|
896
|
-
input: { name: 'invoice', label: 'Invoice' },
|
|
897
|
-
});
|
|
898
|
-
|
|
899
|
-
// 2. Add fields
|
|
900
|
-
await registry.execute({
|
|
901
|
-
type: 'tool-call' as const,
|
|
902
|
-
toolCallId: 's2',
|
|
903
|
-
toolName: 'add_field',
|
|
904
|
-
input: { objectName: 'invoice', name: 'amount', type: 'number', label: 'Amount' },
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
await registry.execute({
|
|
908
|
-
type: 'tool-call' as const,
|
|
909
|
-
toolCallId: 's3',
|
|
910
|
-
toolName: 'add_field',
|
|
911
|
-
input: { objectName: 'invoice', name: 'status', type: 'text', label: 'Status' },
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
// 3. Describe — should show both fields
|
|
915
|
-
const descResult = await registry.execute({
|
|
916
|
-
type: 'tool-call' as const,
|
|
917
|
-
toolCallId: 's4',
|
|
918
|
-
toolName: 'describe_object',
|
|
919
|
-
input: { objectName: 'invoice' },
|
|
920
|
-
});
|
|
921
|
-
const desc = JSON.parse((descResult.output as any).value);
|
|
922
|
-
expect(desc.fields).toHaveLength(2);
|
|
923
|
-
|
|
924
|
-
// 4. Modify field
|
|
925
|
-
await registry.execute({
|
|
926
|
-
type: 'tool-call' as const,
|
|
927
|
-
toolCallId: 's5',
|
|
928
|
-
toolName: 'modify_field',
|
|
929
|
-
input: {
|
|
930
|
-
objectName: 'invoice',
|
|
931
|
-
fieldName: 'status',
|
|
932
|
-
changes: { type: 'select', label: 'Invoice Status' },
|
|
933
|
-
},
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
// 5. Delete field
|
|
937
|
-
const delResult = await registry.execute({
|
|
938
|
-
type: 'tool-call' as const,
|
|
939
|
-
toolCallId: 's6',
|
|
940
|
-
toolName: 'delete_field',
|
|
941
|
-
input: { objectName: 'invoice', fieldName: 'amount' },
|
|
942
|
-
});
|
|
943
|
-
const del = JSON.parse((delResult.output as any).value);
|
|
944
|
-
expect(del.success).toBe(true);
|
|
945
|
-
|
|
946
|
-
// 6. Describe again — should show only 1 field (status, modified)
|
|
947
|
-
const descResult2 = await registry.execute({
|
|
948
|
-
type: 'tool-call' as const,
|
|
949
|
-
toolCallId: 's7',
|
|
950
|
-
toolName: 'describe_object',
|
|
951
|
-
input: { objectName: 'invoice' },
|
|
952
|
-
});
|
|
953
|
-
const desc2 = JSON.parse((descResult2.output as any).value);
|
|
954
|
-
expect(desc2.fields).toHaveLength(1);
|
|
955
|
-
expect(desc2.fields[0].name).toBe('status');
|
|
956
|
-
expect(desc2.fields[0].type).toBe('select');
|
|
957
|
-
expect(desc2.fields[0].label).toBe('Invoice Status');
|
|
958
|
-
|
|
959
|
-
// 7. List objects — should show the invoice
|
|
960
|
-
const listResult = await registry.execute({
|
|
961
|
-
type: 'tool-call' as const,
|
|
962
|
-
toolCallId: 's8',
|
|
963
|
-
toolName: 'list_objects',
|
|
964
|
-
input: {},
|
|
965
|
-
});
|
|
966
|
-
const list = JSON.parse((listResult.output as any).value);
|
|
967
|
-
expect(list.totalCount).toBe(1);
|
|
968
|
-
expect(list.objects[0].name).toBe('invoice');
|
|
969
|
-
});
|
|
970
|
-
});
|