@objectstack/service-ai 4.0.3 → 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.
Files changed (52) hide show
  1. package/README.md +293 -0
  2. package/dist/index.cjs +1176 -135
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1225 -430
  5. package/dist/index.d.ts +1225 -430
  6. package/dist/index.js +1160 -128
  7. package/dist/index.js.map +1 -1
  8. package/package.json +35 -8
  9. package/.turbo/turbo-build.log +0 -22
  10. package/CHANGELOG.md +0 -53
  11. package/src/__tests__/ai-service.test.ts +0 -964
  12. package/src/__tests__/auth-and-toolcalling.test.ts +0 -677
  13. package/src/__tests__/chatbot-features.test.ts +0 -1116
  14. package/src/__tests__/metadata-tools.test.ts +0 -970
  15. package/src/__tests__/objectql-conversation-service.test.ts +0 -382
  16. package/src/__tests__/tool-routes.test.ts +0 -191
  17. package/src/__tests__/vercel-stream-encoder.test.ts +0 -310
  18. package/src/adapters/index.ts +0 -6
  19. package/src/adapters/memory-adapter.ts +0 -72
  20. package/src/adapters/types.ts +0 -3
  21. package/src/adapters/vercel-adapter.ts +0 -148
  22. package/src/agent-runtime.ts +0 -154
  23. package/src/agents/data-chat-agent.ts +0 -79
  24. package/src/agents/index.ts +0 -4
  25. package/src/agents/metadata-assistant-agent.ts +0 -87
  26. package/src/ai-service.ts +0 -364
  27. package/src/conversation/in-memory-conversation-service.ts +0 -103
  28. package/src/conversation/index.ts +0 -4
  29. package/src/conversation/objectql-conversation-service.ts +0 -301
  30. package/src/index.ts +0 -60
  31. package/src/objects/ai-conversation.object.ts +0 -86
  32. package/src/objects/ai-message.object.ts +0 -86
  33. package/src/objects/index.ts +0 -10
  34. package/src/plugin.ts +0 -391
  35. package/src/routes/agent-routes.ts +0 -190
  36. package/src/routes/ai-routes.ts +0 -439
  37. package/src/routes/index.ts +0 -5
  38. package/src/routes/message-utils.ts +0 -90
  39. package/src/routes/tool-routes.ts +0 -142
  40. package/src/stream/index.ts +0 -3
  41. package/src/stream/vercel-stream-encoder.ts +0 -153
  42. package/src/tools/add-field.tool.ts +0 -70
  43. package/src/tools/create-object.tool.ts +0 -66
  44. package/src/tools/data-tools.ts +0 -293
  45. package/src/tools/delete-field.tool.ts +0 -38
  46. package/src/tools/describe-object.tool.ts +0 -31
  47. package/src/tools/index.ts +0 -18
  48. package/src/tools/list-objects.tool.ts +0 -34
  49. package/src/tools/metadata-tools.ts +0 -430
  50. package/src/tools/modify-field.tool.ts +0 -44
  51. package/src/tools/tool-registry.ts +0 -132
  52. package/tsconfig.json +0 -17
@@ -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
- });