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