Skip to content

Commit 3c58325

Browse files
Copilothotlong
andauthored
fix: address all 9 review comments — rename tools, fix reference type, add validations and tests
Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/96854b99-76e5-4225-8b77-ec8a660bc4ea Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 1583ed4 commit 3c58325

3 files changed

Lines changed: 271 additions & 43 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
### Added
1818
- **Metadata Management Tools (`service-ai`)** — Added 6 built-in AI tools for metadata
1919
CRUD operations in `packages/services/service-ai/src/tools/metadata-tools.ts`:
20-
`create_object`, `add_field`, `modify_field`, `delete_field`, `list_objects`,
21-
`describe_object`. Tool definitions (`AIToolDefinition`) and `ToolHandler` implementations
20+
`create_object`, `add_field`, `modify_field`, `delete_field`, `list_metadata_objects`,
21+
`describe_metadata_object`. Tool definitions (`AIToolDefinition`) and `ToolHandler` implementations
2222
live together in a single file (matching the `data-tools.ts` pattern). Handlers call
2323
`IMetadataService` CRUD methods (`register`, `getObject`, `listObjects`).
2424
Registered via `registerMetadataTools(registry, { metadataService })`.
25-
29 unit tests covering handler execution, input validation, error handling, and a full
26-
create→add→modify→delete→describe lifecycle.
25+
37 unit tests covering handler execution, input validation, error handling, dual registration
26+
with data tools (no name collisions), and a full create→add→modify→delete→describe lifecycle.
2727
- **Agent Skills — `skills/` directory (agentskills.io)** — Created `skills/` folder at
2828
repository root following the [agentskills.io specification](https://agentskills.io/specification).
2929
Five expert-knowledge skills with hand-written `SKILL.md` files and `references/` quick-lookup

packages/services/service-ai/src/__tests__/metadata-tools.test.ts

Lines changed: 177 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import {
77
registerMetadataTools,
88
METADATA_TOOL_DEFINITIONS,
99
} from '../tools/metadata-tools.js';
10+
import {
11+
registerDataTools,
12+
} from '../tools/data-tools.js';
1013
import type { MetadataToolContext } from '../tools/metadata-tools.js';
1114

1215
// ── Helpers ────────────────────────────────────────────────────────
@@ -52,8 +55,8 @@ describe('Metadata Tool Definitions', () => {
5255
'add_field',
5356
'modify_field',
5457
'delete_field',
55-
'list_objects',
56-
'describe_object',
58+
'list_metadata_objects',
59+
'describe_metadata_object',
5760
]);
5861
});
5962

@@ -85,8 +88,45 @@ describe('registerMetadataTools', () => {
8588
expect(registry.has('add_field')).toBe(true);
8689
expect(registry.has('modify_field')).toBe(true);
8790
expect(registry.has('delete_field')).toBe(true);
91+
expect(registry.has('list_metadata_objects')).toBe(true);
92+
expect(registry.has('describe_metadata_object')).toBe(true);
93+
});
94+
});
95+
96+
// ═══════════════════════════════════════════════════════════════════
97+
// Dual registration (data tools + metadata tools)
98+
// ═══════════════════════════════════════════════════════════════════
99+
100+
describe('registerDataTools + registerMetadataTools — no collision', () => {
101+
it('should register both tool sets on the same registry without overwriting', () => {
102+
const registry = new ToolRegistry();
103+
const metadataService = createMockMetadataService();
104+
const dataEngine = {
105+
find: vi.fn(),
106+
findOne: vi.fn(),
107+
aggregate: vi.fn(),
108+
} as any;
109+
110+
registerDataTools(registry, { dataEngine, metadataService });
111+
const sizeAfterData = registry.size;
112+
113+
registerMetadataTools(registry, { metadataService });
114+
const sizeAfterBoth = registry.size;
115+
116+
// Data tools define: list_objects, describe_object, query_records, get_record, aggregate_data
117+
// Metadata tools define: create_object, add_field, modify_field, delete_field, list_metadata_objects, describe_metadata_object
118+
// No overlap — total should be sum of both
119+
expect(sizeAfterBoth).toBe(sizeAfterData + 6);
120+
121+
// Data tools should still be present
88122
expect(registry.has('list_objects')).toBe(true);
89123
expect(registry.has('describe_object')).toBe(true);
124+
expect(registry.has('query_records')).toBe(true);
125+
126+
// Metadata tools should also be present with distinct names
127+
expect(registry.has('list_metadata_objects')).toBe(true);
128+
expect(registry.has('describe_metadata_object')).toBe(true);
129+
expect(registry.has('create_object')).toBe(true);
90130
});
91131
});
92132

@@ -217,6 +257,64 @@ describe('create_object handler', () => {
217257
const parsed = JSON.parse((result.output as any).value);
218258
expect(parsed.error).toContain('required');
219259
});
260+
261+
it('should reject fields with invalid snake_case names', async () => {
262+
const result = await registry.execute({
263+
type: 'tool-call' as const,
264+
toolCallId: 'c7',
265+
toolName: 'create_object',
266+
input: {
267+
name: 'project',
268+
label: 'Project',
269+
fields: [
270+
{ name: 'ValidField', type: 'text' },
271+
],
272+
},
273+
});
274+
275+
const parsed = JSON.parse((result.output as any).value);
276+
expect(parsed.error).toContain('snake_case');
277+
expect(metadataService.register).not.toHaveBeenCalled();
278+
});
279+
280+
it('should reject fields with duplicate names', async () => {
281+
const result = await registry.execute({
282+
type: 'tool-call' as const,
283+
toolCallId: 'c8',
284+
toolName: 'create_object',
285+
input: {
286+
name: 'project',
287+
label: 'Project',
288+
fields: [
289+
{ name: 'status', type: 'text' },
290+
{ name: 'status', type: 'select' },
291+
],
292+
},
293+
});
294+
295+
const parsed = JSON.parse((result.output as any).value);
296+
expect(parsed.error).toContain('Duplicate');
297+
expect(metadataService.register).not.toHaveBeenCalled();
298+
});
299+
300+
it('should reject fields with missing name', async () => {
301+
const result = await registry.execute({
302+
type: 'tool-call' as const,
303+
toolCallId: 'c9',
304+
toolName: 'create_object',
305+
input: {
306+
name: 'project',
307+
label: 'Project',
308+
fields: [
309+
{ type: 'text' },
310+
],
311+
},
312+
});
313+
314+
const parsed = JSON.parse((result.output as any).value);
315+
expect(parsed.error).toBeTruthy();
316+
expect(metadataService.register).not.toHaveBeenCalled();
317+
});
220318
});
221319

222320
// ═══════════════════════════════════════════════════════════════════
@@ -332,6 +430,70 @@ describe('add_field handler', () => {
332430
const parsed = JSON.parse((result.output as any).value);
333431
expect(parsed.error).toContain('snake_case');
334432
});
433+
434+
it('should reject invalid objectName (not snake_case)', async () => {
435+
const result = await registry.execute({
436+
type: 'tool-call' as const,
437+
toolCallId: 'c6',
438+
toolName: 'add_field',
439+
input: { objectName: 'MyProject', name: 'status', type: 'text' },
440+
});
441+
442+
const parsed = JSON.parse((result.output as any).value);
443+
expect(parsed.error).toContain('snake_case');
444+
});
445+
446+
it('should accept reference as a string (not object)', async () => {
447+
const result = await registry.execute({
448+
type: 'tool-call' as const,
449+
toolCallId: 'c7',
450+
toolName: 'add_field',
451+
input: { objectName: 'project', name: 'account_id', type: 'lookup', reference: 'account' },
452+
});
453+
454+
const parsed = JSON.parse((result.output as any).value);
455+
expect(parsed.fieldName).toBe('account_id');
456+
expect(metadataService.register).toHaveBeenCalledWith(
457+
'object',
458+
'project',
459+
expect.objectContaining({
460+
fields: expect.objectContaining({
461+
account_id: expect.objectContaining({ type: 'lookup', reference: 'account' }),
462+
}),
463+
}),
464+
);
465+
});
466+
467+
it('should reject invalid reference (not snake_case)', async () => {
468+
const result = await registry.execute({
469+
type: 'tool-call' as const,
470+
toolCallId: 'c8',
471+
toolName: 'add_field',
472+
input: { objectName: 'project', name: 'account_id', type: 'lookup', reference: 'MyAccount' },
473+
});
474+
475+
const parsed = JSON.parse((result.output as any).value);
476+
expect(parsed.error).toContain('snake_case');
477+
});
478+
479+
it('should reject invalid select option values (not snake_case)', async () => {
480+
const result = await registry.execute({
481+
type: 'tool-call' as const,
482+
toolCallId: 'c9',
483+
toolName: 'add_field',
484+
input: {
485+
objectName: 'project',
486+
name: 'priority',
487+
type: 'select',
488+
options: [
489+
{ label: 'High Priority', value: 'HighPriority' },
490+
],
491+
},
492+
});
493+
494+
const parsed = JSON.parse((result.output as any).value);
495+
expect(parsed.error).toContain('snake_case');
496+
});
335497
});
336498

337499
// ═══════════════════════════════════════════════════════════════════
@@ -496,10 +658,10 @@ describe('delete_field handler', () => {
496658
});
497659

498660
// ═══════════════════════════════════════════════════════════════════
499-
// list_objects handler
661+
// list_metadata_objects handler
500662
// ═══════════════════════════════════════════════════════════════════
501663

502-
describe('list_objects handler', () => {
664+
describe('list_metadata_objects handler', () => {
503665
let registry: ToolRegistry;
504666
let metadataService: IMetadataService;
505667

@@ -516,7 +678,7 @@ describe('list_objects handler', () => {
516678
const result = await registry.execute({
517679
type: 'tool-call' as const,
518680
toolCallId: 'c1',
519-
toolName: 'list_objects',
681+
toolName: 'list_metadata_objects',
520682
input: {},
521683
});
522684

@@ -531,7 +693,7 @@ describe('list_objects handler', () => {
531693
const result = await registry.execute({
532694
type: 'tool-call' as const,
533695
toolCallId: 'c2',
534-
toolName: 'list_objects',
696+
toolName: 'list_metadata_objects',
535697
input: { filter: 'account' },
536698
});
537699

@@ -544,7 +706,7 @@ describe('list_objects handler', () => {
544706
const result = await registry.execute({
545707
type: 'tool-call' as const,
546708
toolCallId: 'c3',
547-
toolName: 'list_objects',
709+
toolName: 'list_metadata_objects',
548710
input: { includeFields: true },
549711
});
550712

@@ -561,7 +723,7 @@ describe('list_objects handler', () => {
561723
const result = await registry.execute({
562724
type: 'tool-call' as const,
563725
toolCallId: 'c4',
564-
toolName: 'list_objects',
726+
toolName: 'list_metadata_objects',
565727
input: {},
566728
});
567729

@@ -572,10 +734,10 @@ describe('list_objects handler', () => {
572734
});
573735

574736
// ═══════════════════════════════════════════════════════════════════
575-
// describe_object handler
737+
// describe_metadata_object handler
576738
// ═══════════════════════════════════════════════════════════════════
577739

578-
describe('describe_object handler', () => {
740+
describe('describe_metadata_object handler', () => {
579741
let registry: ToolRegistry;
580742
let metadataService: IMetadataService;
581743

@@ -600,7 +762,7 @@ describe('describe_object handler', () => {
600762
const result = await registry.execute({
601763
type: 'tool-call' as const,
602764
toolCallId: 'c1',
603-
toolName: 'describe_object',
765+
toolName: 'describe_metadata_object',
604766
input: { objectName: 'account' },
605767
});
606768

@@ -622,7 +784,7 @@ describe('describe_object handler', () => {
622784
const result = await registry.execute({
623785
type: 'tool-call' as const,
624786
toolCallId: 'c2',
625-
toolName: 'describe_object',
787+
toolName: 'describe_metadata_object',
626788
input: { objectName: 'nonexistent' },
627789
});
628790

@@ -673,7 +835,7 @@ describe('Metadata Tools — full lifecycle', () => {
673835
const descResult = await registry.execute({
674836
type: 'tool-call' as const,
675837
toolCallId: 's4',
676-
toolName: 'describe_object',
838+
toolName: 'describe_metadata_object',
677839
input: { objectName: 'invoice' },
678840
});
679841
const desc = JSON.parse((descResult.output as any).value);
@@ -705,7 +867,7 @@ describe('Metadata Tools — full lifecycle', () => {
705867
const descResult2 = await registry.execute({
706868
type: 'tool-call' as const,
707869
toolCallId: 's7',
708-
toolName: 'describe_object',
870+
toolName: 'describe_metadata_object',
709871
input: { objectName: 'invoice' },
710872
});
711873
const desc2 = JSON.parse((descResult2.output as any).value);
@@ -718,7 +880,7 @@ describe('Metadata Tools — full lifecycle', () => {
718880
const listResult = await registry.execute({
719881
type: 'tool-call' as const,
720882
toolCallId: 's8',
721-
toolName: 'list_objects',
883+
toolName: 'list_metadata_objects',
722884
input: {},
723885
});
724886
const list = JSON.parse((listResult.output as any).value);

0 commit comments

Comments
 (0)