Skip to content

Commit be84ef7

Browse files
Claudehotlong
andauthored
Add tests for playground plugins and tool routes
Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/fdb1a79a-ee3d-49d8-ac23-7ef960bf001d Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent d16a96c commit be84ef7

File tree

2 files changed

+276
-0
lines changed

2 files changed

+276
-0
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// @vitest-environment happy-dom
2+
import { describe, it, expect } from 'vitest';
3+
import { agentPlaygroundPlugin, toolPlaygroundPlugin } from '../src/plugins/built-in';
4+
5+
describe('Playground Plugins', () => {
6+
describe('Agent Playground Plugin', () => {
7+
it('should have correct manifest structure', () => {
8+
expect(agentPlaygroundPlugin.manifest).toBeDefined();
9+
expect(agentPlaygroundPlugin.manifest.id).toBe('objectstack.agent-playground');
10+
expect(agentPlaygroundPlugin.manifest.name).toBe('Agent Playground');
11+
expect(agentPlaygroundPlugin.activate).toBeTypeOf('function');
12+
});
13+
14+
it('should contribute agent viewer for preview mode', () => {
15+
const viewers = agentPlaygroundPlugin.manifest.contributes?.metadataViewers || [];
16+
expect(viewers).toHaveLength(1);
17+
18+
const viewer = viewers[0];
19+
expect(viewer.id).toBe('agent-playground');
20+
expect(viewer.metadataTypes).toContain('agent');
21+
expect(viewer.label).toBe('Playground');
22+
expect(viewer.priority).toBe(10); // Higher than default inspector (-1)
23+
expect(viewer.modes).toContain('preview');
24+
});
25+
26+
it('should register viewer on activation', () => {
27+
const registeredViewers: string[] = [];
28+
const mockAPI = {
29+
registerViewer: (id: string) => registeredViewers.push(id),
30+
registerPanel: () => {},
31+
registerAction: () => {},
32+
registerCommand: () => {},
33+
registerMetadataIcon: () => {},
34+
};
35+
36+
agentPlaygroundPlugin.activate(mockAPI as any);
37+
expect(registeredViewers).toContain('agent-playground');
38+
});
39+
});
40+
41+
describe('Tool Playground Plugin', () => {
42+
it('should have correct manifest structure', () => {
43+
expect(toolPlaygroundPlugin.manifest).toBeDefined();
44+
expect(toolPlaygroundPlugin.manifest.id).toBe('objectstack.tool-playground');
45+
expect(toolPlaygroundPlugin.manifest.name).toBe('Tool Playground');
46+
expect(toolPlaygroundPlugin.activate).toBeTypeOf('function');
47+
});
48+
49+
it('should contribute tool viewer for preview mode', () => {
50+
const viewers = toolPlaygroundPlugin.manifest.contributes?.metadataViewers || [];
51+
expect(viewers).toHaveLength(1);
52+
53+
const viewer = viewers[0];
54+
expect(viewer.id).toBe('tool-playground');
55+
expect(viewer.metadataTypes).toContain('tool');
56+
expect(viewer.label).toBe('Playground');
57+
expect(viewer.priority).toBe(10); // Higher than default inspector (-1)
58+
expect(viewer.modes).toContain('preview');
59+
});
60+
61+
it('should register viewer on activation', () => {
62+
const registeredViewers: string[] = [];
63+
const mockAPI = {
64+
registerViewer: (id: string) => registeredViewers.push(id),
65+
registerPanel: () => {},
66+
registerAction: () => {},
67+
registerCommand: () => {},
68+
registerMetadataIcon: () => {},
69+
};
70+
71+
toolPlaygroundPlugin.activate(mockAPI as any);
72+
expect(registeredViewers).toContain('tool-playground');
73+
});
74+
});
75+
76+
describe('Plugin Priority', () => {
77+
it('both playground plugins should have priority higher than default inspector', () => {
78+
const agentViewers = agentPlaygroundPlugin.manifest.contributes?.metadataViewers || [];
79+
const toolViewers = toolPlaygroundPlugin.manifest.contributes?.metadataViewers || [];
80+
81+
const agentPriority = agentViewers[0]?.priority ?? 0;
82+
const toolPriority = toolViewers[0]?.priority ?? 0;
83+
84+
const defaultInspectorPriority = -1; // As defined in default-plugin.tsx
85+
86+
expect(agentPriority).toBeGreaterThan(defaultInspectorPriority);
87+
expect(toolPriority).toBeGreaterThan(defaultInspectorPriority);
88+
});
89+
});
90+
});
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, it, expect, beforeEach, vi } from 'vitest';
4+
import { buildToolRoutes } from '../routes/tool-routes.js';
5+
import { AIService } from '../ai-service.js';
6+
import { InMemoryConversationService } from '../conversation/in-memory-conversation-service.js';
7+
import { ToolRegistry } from '../tools/tool-registry.js';
8+
import type { Logger } from '@objectstack/spec/contracts';
9+
10+
const silentLogger: Logger = {
11+
debug: vi.fn(),
12+
info: vi.fn(),
13+
warn: vi.fn(),
14+
error: vi.fn(),
15+
fatal: vi.fn(),
16+
};
17+
18+
describe('Tool Routes', () => {
19+
let aiService: AIService;
20+
let routes: ReturnType<typeof buildToolRoutes>;
21+
22+
beforeEach(() => {
23+
const conversationService = new InMemoryConversationService();
24+
aiService = new AIService({
25+
adapter: 'memory',
26+
conversationService,
27+
});
28+
29+
// Register a test tool
30+
aiService.toolRegistry.register({
31+
name: 'test_tool',
32+
description: 'A test tool for playground',
33+
parameters: {
34+
type: 'object',
35+
properties: {
36+
message: { type: 'string' },
37+
},
38+
required: ['message'],
39+
},
40+
handler: async (params: any) => {
41+
return { echo: params.message };
42+
},
43+
});
44+
45+
routes = buildToolRoutes(aiService, silentLogger);
46+
});
47+
48+
describe('GET /api/v1/ai/tools', () => {
49+
it('should list all registered tools', async () => {
50+
const listRoute = routes.find(r => r.method === 'GET' && r.path === '/api/v1/ai/tools');
51+
expect(listRoute).toBeDefined();
52+
53+
const response = await listRoute!.handler({});
54+
expect(response.status).toBe(200);
55+
expect(response.body).toHaveProperty('tools');
56+
expect(Array.isArray((response.body as any).tools)).toBe(true);
57+
58+
const tools = (response.body as any).tools;
59+
expect(tools.length).toBeGreaterThan(0);
60+
expect(tools.some((t: any) => t.name === 'test_tool')).toBe(true);
61+
});
62+
63+
it('should require authentication', () => {
64+
const listRoute = routes.find(r => r.method === 'GET' && r.path === '/api/v1/ai/tools');
65+
expect(listRoute?.auth).toBe(true);
66+
expect(listRoute?.permissions).toContain('ai:tools');
67+
});
68+
});
69+
70+
describe('POST /api/v1/ai/tools/:toolName/execute', () => {
71+
it('should execute a tool with parameters', async () => {
72+
const executeRoute = routes.find(
73+
r => r.method === 'POST' && r.path === '/api/v1/ai/tools/:toolName/execute'
74+
);
75+
expect(executeRoute).toBeDefined();
76+
77+
const response = await executeRoute!.handler({
78+
params: { toolName: 'test_tool' },
79+
body: {
80+
parameters: { message: 'Hello, Playground!' },
81+
},
82+
});
83+
84+
expect(response.status).toBe(200);
85+
expect(response.body).toHaveProperty('result');
86+
expect((response.body as any).result).toEqual({ echo: 'Hello, Playground!' });
87+
expect((response.body as any).toolName).toBe('test_tool');
88+
expect((response.body as any).duration).toBeTypeOf('number');
89+
});
90+
91+
it('should return 404 for non-existent tool', async () => {
92+
const executeRoute = routes.find(
93+
r => r.method === 'POST' && r.path === '/api/v1/ai/tools/:toolName/execute'
94+
);
95+
96+
const response = await executeRoute!.handler({
97+
params: { toolName: 'non_existent_tool' },
98+
body: {
99+
parameters: {},
100+
},
101+
});
102+
103+
expect(response.status).toBe(404);
104+
expect((response.body as any).error).toContain('not found');
105+
});
106+
107+
it('should return 400 when toolName is missing', async () => {
108+
const executeRoute = routes.find(
109+
r => r.method === 'POST' && r.path === '/api/v1/ai/tools/:toolName/execute'
110+
);
111+
112+
const response = await executeRoute!.handler({
113+
body: {
114+
parameters: {},
115+
},
116+
});
117+
118+
expect(response.status).toBe(400);
119+
expect((response.body as any).error).toContain('toolName');
120+
});
121+
122+
it('should return 400 when parameters are missing', async () => {
123+
const executeRoute = routes.find(
124+
r => r.method === 'POST' && r.path === '/api/v1/ai/tools/:toolName/execute'
125+
);
126+
127+
const response = await executeRoute!.handler({
128+
params: { toolName: 'test_tool' },
129+
body: {},
130+
});
131+
132+
expect(response.status).toBe(400);
133+
expect((response.body as any).error).toContain('parameters');
134+
});
135+
136+
it('should handle tool execution errors', async () => {
137+
// Register a tool that throws an error
138+
aiService.toolRegistry.register({
139+
name: 'error_tool',
140+
description: 'A tool that throws an error',
141+
parameters: { type: 'object', properties: {} },
142+
handler: async () => {
143+
throw new Error('Tool execution failed');
144+
},
145+
});
146+
147+
const executeRoute = routes.find(
148+
r => r.method === 'POST' && r.path === '/api/v1/ai/tools/:toolName/execute'
149+
);
150+
151+
const response = await executeRoute!.handler({
152+
params: { toolName: 'error_tool' },
153+
body: {
154+
parameters: {},
155+
},
156+
});
157+
158+
expect(response.status).toBe(500);
159+
expect((response.body as any).error).toContain('Tool execution failed');
160+
expect((response.body as any).duration).toBeTypeOf('number');
161+
});
162+
163+
it('should require authentication and permissions', () => {
164+
const executeRoute = routes.find(
165+
r => r.method === 'POST' && r.path === '/api/v1/ai/tools/:toolName/execute'
166+
);
167+
168+
expect(executeRoute?.auth).toBe(true);
169+
expect(executeRoute?.permissions).toContain('ai:tools');
170+
expect(executeRoute?.permissions).toContain('ai:execute');
171+
});
172+
});
173+
174+
describe('Route Configuration', () => {
175+
it('should register exactly 2 routes', () => {
176+
expect(routes).toHaveLength(2);
177+
});
178+
179+
it('should have descriptive route descriptions', () => {
180+
routes.forEach(route => {
181+
expect(route.description).toBeTruthy();
182+
expect(route.description.length).toBeGreaterThan(10);
183+
});
184+
});
185+
});
186+
});

0 commit comments

Comments
 (0)