@lantos1618/better-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/lib/aui/README.md +136 -0
- package/lib/aui/__tests__/aui-complete.test.ts +251 -0
- package/lib/aui/__tests__/aui-comprehensive.test.ts +376 -0
- package/lib/aui/__tests__/aui-concise.test.ts +278 -0
- package/lib/aui/__tests__/aui-integration.test.ts +309 -0
- package/lib/aui/__tests__/aui-simple.test.ts +116 -0
- package/lib/aui/__tests__/aui.test.ts +269 -0
- package/lib/aui/__tests__/concise-api.test.ts +165 -0
- package/lib/aui/__tests__/core.test.ts +265 -0
- package/lib/aui/__tests__/simple-api.test.ts +200 -0
- package/lib/aui/ai-assistant.ts +408 -0
- package/lib/aui/ai-control.ts +353 -0
- package/lib/aui/client/use-aui.ts +55 -0
- package/lib/aui/client-control.ts +551 -0
- package/lib/aui/client-executor.ts +417 -0
- package/lib/aui/components/ToolRenderer.tsx +22 -0
- package/lib/aui/core.ts +137 -0
- package/lib/aui/demo.tsx +89 -0
- package/lib/aui/examples/ai-complete-demo.tsx +359 -0
- package/lib/aui/examples/ai-control-demo.tsx +356 -0
- package/lib/aui/examples/ai-control-tools.ts +308 -0
- package/lib/aui/examples/concise-api.tsx +153 -0
- package/lib/aui/examples/index.tsx +163 -0
- package/lib/aui/examples/quick-demo.tsx +91 -0
- package/lib/aui/examples/simple-demo.tsx +71 -0
- package/lib/aui/examples/simple-tools.tsx +160 -0
- package/lib/aui/examples/user-api.tsx +208 -0
- package/lib/aui/examples/user-requested.tsx +174 -0
- package/lib/aui/examples/weather-search-tools.tsx +119 -0
- package/lib/aui/examples.tsx +367 -0
- package/lib/aui/hooks/useAUITool.ts +142 -0
- package/lib/aui/hooks/useAUIToolEnhanced.ts +343 -0
- package/lib/aui/hooks/useAUITools.ts +195 -0
- package/lib/aui/index.ts +156 -0
- package/lib/aui/provider.tsx +45 -0
- package/lib/aui/server-control.ts +386 -0
- package/lib/aui/server-executor.ts +165 -0
- package/lib/aui/server.ts +167 -0
- package/lib/aui/tool-registry.ts +380 -0
- package/lib/aui/tools/advanced-examples.tsx +86 -0
- package/lib/aui/tools/ai-complete.ts +375 -0
- package/lib/aui/tools/api-tools.tsx +230 -0
- package/lib/aui/tools/data-tools.tsx +232 -0
- package/lib/aui/tools/dom-tools.tsx +202 -0
- package/lib/aui/tools/examples.ts +43 -0
- package/lib/aui/tools/file-tools.tsx +202 -0
- package/lib/aui/tools/form-tools.tsx +233 -0
- package/lib/aui/tools/index.ts +8 -0
- package/lib/aui/tools/navigation-tools.tsx +172 -0
- package/lib/aui/tools/notification-tools.ts +213 -0
- package/lib/aui/tools/state-tools.tsx +209 -0
- package/lib/aui/types.ts +47 -0
- package/lib/aui/vercel-ai.ts +100 -0
- package/package.json +51 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import aui from '../index';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
describe('AUI Concise API Tests', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
aui.clear();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe('Simple Tool Pattern', () => {
|
|
10
|
+
it('should create tool with just 2 methods (input + execute)', async () => {
|
|
11
|
+
const simpleTool = aui
|
|
12
|
+
.tool('weather')
|
|
13
|
+
.input(z.object({ city: z.string() }))
|
|
14
|
+
.execute(async ({ input }) => ({ temp: 72, city: input.city }));
|
|
15
|
+
|
|
16
|
+
const result = await simpleTool.run({ city: 'SF' });
|
|
17
|
+
expect(result).toEqual({ temp: 72, city: 'SF' });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should work with render method', async () => {
|
|
21
|
+
const weatherTool = aui
|
|
22
|
+
.tool('weather')
|
|
23
|
+
.input(z.object({ city: z.string() }))
|
|
24
|
+
.execute(async ({ input }) => ({ temp: 72, city: input.city }))
|
|
25
|
+
.render(({ data }) => ({ type: 'div', props: { children: `${data.city}: ${data.temp}°` } }) as any);
|
|
26
|
+
|
|
27
|
+
const result = await weatherTool.run({ city: 'NYC' });
|
|
28
|
+
expect(result).toEqual({ temp: 72, city: 'NYC' });
|
|
29
|
+
expect(weatherTool.renderer).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('Complex Tool Pattern', () => {
|
|
34
|
+
it('should support client optimization', async () => {
|
|
35
|
+
const searchTool = aui
|
|
36
|
+
.tool('search')
|
|
37
|
+
.input(z.object({ query: z.string() }))
|
|
38
|
+
.execute(async ({ input }) => ({ results: [`Server: ${input.query}`] }))
|
|
39
|
+
.clientExecute(async ({ input, ctx }) => {
|
|
40
|
+
const cached = ctx.cache.get(input.query);
|
|
41
|
+
if (cached) return cached;
|
|
42
|
+
|
|
43
|
+
const result = { results: [`Client: ${input.query}`] };
|
|
44
|
+
ctx.cache.set(input.query, result);
|
|
45
|
+
return result;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Server context (isServer: true)
|
|
49
|
+
const serverResult = await searchTool.run(
|
|
50
|
+
{ query: 'test' },
|
|
51
|
+
{ isServer: true, cache: new Map(), fetch: global.fetch }
|
|
52
|
+
);
|
|
53
|
+
expect(serverResult.results[0]).toBe('Server: test');
|
|
54
|
+
|
|
55
|
+
// Client context (isServer: false)
|
|
56
|
+
const clientCache = new Map();
|
|
57
|
+
const clientResult = await searchTool.run(
|
|
58
|
+
{ query: 'test' },
|
|
59
|
+
{ isServer: false, cache: clientCache, fetch: global.fetch }
|
|
60
|
+
);
|
|
61
|
+
expect(clientResult.results[0]).toBe('Client: test');
|
|
62
|
+
|
|
63
|
+
// Check caching works
|
|
64
|
+
const cachedResult = await searchTool.run(
|
|
65
|
+
{ query: 'test' },
|
|
66
|
+
{ isServer: false, cache: clientCache, fetch: global.fetch }
|
|
67
|
+
);
|
|
68
|
+
expect(cachedResult).toBe(clientResult); // Same reference = cached
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('Frontend/Backend Control', () => {
|
|
73
|
+
it('should allow database queries (backend)', async () => {
|
|
74
|
+
const dbTool = aui
|
|
75
|
+
.tool('queryDB')
|
|
76
|
+
.input(z.object({ table: z.string(), limit: z.number() }))
|
|
77
|
+
.execute(async ({ input }) => ({
|
|
78
|
+
rows: Array(input.limit).fill(null).map((_, i) => ({
|
|
79
|
+
id: i + 1,
|
|
80
|
+
table: input.table
|
|
81
|
+
})),
|
|
82
|
+
count: input.limit
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
const result = await dbTool.run({ table: 'users', limit: 3 });
|
|
86
|
+
expect(result.count).toBe(3);
|
|
87
|
+
expect(result.rows).toHaveLength(3);
|
|
88
|
+
expect(result.rows[0].table).toBe('users');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should allow DOM manipulation (frontend)', async () => {
|
|
92
|
+
const domTool = aui
|
|
93
|
+
.tool('updateDOM')
|
|
94
|
+
.input(z.object({ selector: z.string(), text: z.string() }))
|
|
95
|
+
.clientExecute(async ({ input }) => {
|
|
96
|
+
// Mock DOM operation
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
selector: input.selector,
|
|
100
|
+
text: input.text,
|
|
101
|
+
isClient: true
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const result = await domTool.run(
|
|
106
|
+
{ selector: '#title', text: 'New Title' },
|
|
107
|
+
{ isServer: false, cache: new Map(), fetch: global.fetch }
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect(result.success).toBe(true);
|
|
111
|
+
expect(result.isClient).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should handle API calls', async () => {
|
|
115
|
+
const apiTool = aui
|
|
116
|
+
.tool('callAPI')
|
|
117
|
+
.input(z.object({ endpoint: z.string(), method: z.string() }))
|
|
118
|
+
.execute(async ({ input }) => ({
|
|
119
|
+
status: 200,
|
|
120
|
+
endpoint: input.endpoint,
|
|
121
|
+
method: input.method,
|
|
122
|
+
data: { message: 'Success' }
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
const result = await apiTool.run({ endpoint: '/api/users', method: 'GET' });
|
|
126
|
+
expect(result.status).toBe(200);
|
|
127
|
+
expect(result.data.message).toBe('Success');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('Chaining and Fluent Interface', () => {
|
|
132
|
+
it('should support method chaining without build()', async () => {
|
|
133
|
+
const tool = aui
|
|
134
|
+
.tool('chain-test')
|
|
135
|
+
.input(z.object({ value: z.number() }))
|
|
136
|
+
.execute(async ({ input }) => ({ doubled: input.value * 2 }))
|
|
137
|
+
.describe('Doubles a number')
|
|
138
|
+
.tag('math', 'simple')
|
|
139
|
+
.middleware(async ({ input, next }) => {
|
|
140
|
+
// Log before execution
|
|
141
|
+
const result = await next();
|
|
142
|
+
// Log after execution
|
|
143
|
+
return result;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const result = await tool.run({ value: 5 });
|
|
147
|
+
expect(result.doubled).toBe(10);
|
|
148
|
+
expect(tool.description).toBe('Doubles a number');
|
|
149
|
+
expect(tool.tags).toContain('math');
|
|
150
|
+
expect(tool.tags).toContain('simple');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should return tool directly without build step', () => {
|
|
154
|
+
const tool = aui
|
|
155
|
+
.tool('direct')
|
|
156
|
+
.input(z.object({ test: z.string() }))
|
|
157
|
+
.execute(async ({ input }) => input);
|
|
158
|
+
|
|
159
|
+
// Tool is immediately usable
|
|
160
|
+
expect(tool.name).toBe('direct');
|
|
161
|
+
expect(tool.run).toBeDefined();
|
|
162
|
+
expect(typeof tool.run).toBe('function');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import aui, { AUI, z } from '../index';
|
|
3
|
+
|
|
4
|
+
describe('AUI Core System', () => {
|
|
5
|
+
let testAUI: AUI;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
testAUI = new AUI();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('Simple Tool Pattern', () => {
|
|
12
|
+
it('should create a simple tool with just execute and render', () => {
|
|
13
|
+
const weatherTool = testAUI
|
|
14
|
+
.tool('weather')
|
|
15
|
+
.input(z.object({ city: z.string() }))
|
|
16
|
+
.execute(async ({ input }) => ({ temp: 72, city: input.city }));
|
|
17
|
+
|
|
18
|
+
expect(weatherTool.name).toBe('weather');
|
|
19
|
+
expect(weatherTool.schema).toBeDefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should execute simple tool correctly', async () => {
|
|
23
|
+
const weatherTool = testAUI
|
|
24
|
+
.tool('weather')
|
|
25
|
+
.input(z.object({ city: z.string() }))
|
|
26
|
+
.execute(async ({ input }) => ({ temp: 72, city: input.city }));
|
|
27
|
+
|
|
28
|
+
const result = await weatherTool.run({ city: 'San Francisco' });
|
|
29
|
+
expect(result).toEqual({ temp: 72, city: 'San Francisco' });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should validate input with zod schema', async () => {
|
|
33
|
+
const tool = testAUI
|
|
34
|
+
.tool('validated')
|
|
35
|
+
.input(z.object({
|
|
36
|
+
email: z.string().email(),
|
|
37
|
+
age: z.number().min(18)
|
|
38
|
+
}))
|
|
39
|
+
.execute(async ({ input }) => ({ valid: true, ...input }));
|
|
40
|
+
|
|
41
|
+
await expect(tool.run({ email: 'invalid', age: 17 }))
|
|
42
|
+
.rejects.toThrow();
|
|
43
|
+
|
|
44
|
+
const result = await tool.run({ email: 'test@example.com', age: 25 });
|
|
45
|
+
expect(result).toEqual({ valid: true, email: 'test@example.com', age: 25 });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('Complex Tool Pattern', () => {
|
|
50
|
+
it('should create tool with client execution', () => {
|
|
51
|
+
const searchTool = testAUI
|
|
52
|
+
.tool('search')
|
|
53
|
+
.input(z.object({ query: z.string() }))
|
|
54
|
+
.execute(async ({ input }) => [{ id: 1, title: input.query }])
|
|
55
|
+
.clientExecute(async ({ input, ctx }) => {
|
|
56
|
+
const cached = ctx.cache.get(input.query);
|
|
57
|
+
return cached || [{ id: 2, title: `Client: ${input.query}` }];
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(searchTool.name).toBe('search');
|
|
61
|
+
const config = searchTool.getConfig();
|
|
62
|
+
expect(config.clientHandler).toBeDefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should use client handler when not on server', async () => {
|
|
66
|
+
const tool = testAUI
|
|
67
|
+
.tool('hybrid')
|
|
68
|
+
.input(z.object({ value: z.string() }))
|
|
69
|
+
.execute(async ({ input }) => ({ source: 'server', value: input.value }))
|
|
70
|
+
.clientExecute(async ({ input }) => ({ source: 'client', value: input.value }));
|
|
71
|
+
|
|
72
|
+
// Test with client context
|
|
73
|
+
const clientResult = await tool.run(
|
|
74
|
+
{ value: 'test' },
|
|
75
|
+
{ cache: new Map(), fetch, isServer: false }
|
|
76
|
+
);
|
|
77
|
+
expect(clientResult).toEqual({ source: 'client', value: 'test' });
|
|
78
|
+
|
|
79
|
+
// Test with server context
|
|
80
|
+
const serverResult = await tool.run(
|
|
81
|
+
{ value: 'test' },
|
|
82
|
+
{ cache: new Map(), fetch, isServer: true }
|
|
83
|
+
);
|
|
84
|
+
expect(serverResult).toEqual({ source: 'server', value: 'test' });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should use cache in client execution', async () => {
|
|
88
|
+
const cache = new Map();
|
|
89
|
+
cache.set('cached-query', [{ id: 99, title: 'Cached Result' }]);
|
|
90
|
+
|
|
91
|
+
const tool = testAUI
|
|
92
|
+
.tool('search')
|
|
93
|
+
.input(z.object({ query: z.string() }))
|
|
94
|
+
.execute(async ({ input }) => [{ id: 1, title: input.query }])
|
|
95
|
+
.clientExecute(async ({ input, ctx }) => {
|
|
96
|
+
const cached = ctx.cache.get(input.query);
|
|
97
|
+
if (cached) return cached;
|
|
98
|
+
return [{ id: 2, title: `Fresh: ${input.query}` }];
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const result = await tool.run(
|
|
102
|
+
{ query: 'cached-query' },
|
|
103
|
+
{ cache, fetch, isServer: false }
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
expect(result).toEqual([{ id: 99, title: 'Cached Result' }]);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('Middleware Support', () => {
|
|
111
|
+
it('should apply middleware in order', async () => {
|
|
112
|
+
const logs: string[] = [];
|
|
113
|
+
|
|
114
|
+
const tool = testAUI
|
|
115
|
+
.tool('middleware-test')
|
|
116
|
+
.input(z.object({ value: z.number() }))
|
|
117
|
+
.execute(async ({ input }) => ({ result: input.value * 2 }))
|
|
118
|
+
.middleware(async ({ input, next }) => {
|
|
119
|
+
logs.push('middleware-1-before');
|
|
120
|
+
const result = await next();
|
|
121
|
+
logs.push('middleware-1-after');
|
|
122
|
+
return { ...result, middleware1: true };
|
|
123
|
+
})
|
|
124
|
+
.middleware(async ({ input, next }) => {
|
|
125
|
+
logs.push('middleware-2-before');
|
|
126
|
+
const result = await next();
|
|
127
|
+
logs.push('middleware-2-after');
|
|
128
|
+
return { ...result, middleware2: true };
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const result = await tool.run({ value: 5 });
|
|
132
|
+
|
|
133
|
+
expect(logs).toEqual([
|
|
134
|
+
'middleware-1-before',
|
|
135
|
+
'middleware-2-before',
|
|
136
|
+
'middleware-2-after',
|
|
137
|
+
'middleware-1-after'
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
expect(result).toEqual({
|
|
141
|
+
result: 10,
|
|
142
|
+
middleware1: true,
|
|
143
|
+
middleware2: true
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('Tool Management', () => {
|
|
149
|
+
it('should register and retrieve tools', () => {
|
|
150
|
+
const tool1 = testAUI.tool('tool1').execute(async () => 'result1');
|
|
151
|
+
const tool2 = testAUI.tool('tool2').execute(async () => 'result2');
|
|
152
|
+
|
|
153
|
+
expect(testAUI.has('tool1')).toBe(true);
|
|
154
|
+
expect(testAUI.has('tool2')).toBe(true);
|
|
155
|
+
expect(testAUI.get('tool1')).toBe(tool1);
|
|
156
|
+
expect(testAUI.get('tool2')).toBe(tool2);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should list all tools', () => {
|
|
160
|
+
testAUI.tool('a').execute(async () => 'a');
|
|
161
|
+
testAUI.tool('b').execute(async () => 'b');
|
|
162
|
+
testAUI.tool('c').execute(async () => 'c');
|
|
163
|
+
|
|
164
|
+
const tools = testAUI.list();
|
|
165
|
+
expect(tools).toHaveLength(3);
|
|
166
|
+
expect(testAUI.getToolNames()).toEqual(['a', 'b', 'c']);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should execute tool by name', async () => {
|
|
170
|
+
testAUI
|
|
171
|
+
.tool('calculator')
|
|
172
|
+
.input(z.object({ a: z.number(), b: z.number() }))
|
|
173
|
+
.execute(async ({ input }) => input.a + input.b);
|
|
174
|
+
|
|
175
|
+
const result = await testAUI.execute('calculator', { a: 5, b: 3 });
|
|
176
|
+
expect(result).toBe(8);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should find tools by tags', () => {
|
|
180
|
+
testAUI.tool('t1').tag('ai', 'search').execute(async () => 't1');
|
|
181
|
+
testAUI.tool('t2').tag('ai', 'database').execute(async () => 't2');
|
|
182
|
+
testAUI.tool('t3').tag('search').execute(async () => 't3');
|
|
183
|
+
|
|
184
|
+
const aiTools = testAUI.findByTag('ai');
|
|
185
|
+
expect(aiTools).toHaveLength(2);
|
|
186
|
+
|
|
187
|
+
const searchTools = testAUI.findByTag('search');
|
|
188
|
+
expect(searchTools).toHaveLength(2);
|
|
189
|
+
|
|
190
|
+
const aiSearchTools = testAUI.findByTags('ai', 'search');
|
|
191
|
+
expect(aiSearchTools).toHaveLength(1);
|
|
192
|
+
expect(aiSearchTools[0].name).toBe('t1');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('Tool Metadata', () => {
|
|
197
|
+
it('should support descriptions', () => {
|
|
198
|
+
const tool = testAUI
|
|
199
|
+
.tool('described')
|
|
200
|
+
.describe('This tool does something important')
|
|
201
|
+
.execute(async () => 'result');
|
|
202
|
+
|
|
203
|
+
expect(tool.description).toBe('This tool does something important');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should serialize to JSON', () => {
|
|
207
|
+
const tool = testAUI
|
|
208
|
+
.tool('serializable')
|
|
209
|
+
.describe('Test tool')
|
|
210
|
+
.tag('test', 'example')
|
|
211
|
+
.input(z.object({ x: z.number() }))
|
|
212
|
+
.execute(async ({ input }) => input.x * 2)
|
|
213
|
+
.clientExecute(async ({ input }) => input.x * 3)
|
|
214
|
+
.middleware(async ({ next }) => next());
|
|
215
|
+
|
|
216
|
+
const json = tool.toJSON();
|
|
217
|
+
expect(json).toEqual({
|
|
218
|
+
name: 'serializable',
|
|
219
|
+
description: 'Test tool',
|
|
220
|
+
tags: ['test', 'example'],
|
|
221
|
+
hasInput: true,
|
|
222
|
+
hasExecute: true,
|
|
223
|
+
hasClientExecute: true,
|
|
224
|
+
hasRender: false,
|
|
225
|
+
hasMiddleware: true
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('Error Handling', () => {
|
|
231
|
+
it('should throw error for tool without execute handler', async () => {
|
|
232
|
+
const tool = testAUI
|
|
233
|
+
.tool('incomplete')
|
|
234
|
+
.input(z.object({ x: z.number() }));
|
|
235
|
+
|
|
236
|
+
await expect(tool.run({ x: 5 }))
|
|
237
|
+
.rejects.toThrow('Tool incomplete has no execute handler');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should throw error for unknown tool', async () => {
|
|
241
|
+
await expect(testAUI.execute('nonexistent', {}))
|
|
242
|
+
.rejects.toThrow('Tool "nonexistent" not found');
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('Context Creation', () => {
|
|
247
|
+
it('should create default context', () => {
|
|
248
|
+
const ctx = testAUI.createContext();
|
|
249
|
+
expect(ctx.cache).toBeInstanceOf(Map);
|
|
250
|
+
expect(ctx.fetch).toBeDefined();
|
|
251
|
+
expect(typeof ctx.isServer).toBe('boolean');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should merge additional context', () => {
|
|
255
|
+
const ctx = testAUI.createContext({
|
|
256
|
+
user: { id: 1, name: 'Test User' },
|
|
257
|
+
session: { token: 'abc123' }
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(ctx.user).toEqual({ id: 1, name: 'Test User' });
|
|
261
|
+
expect(ctx.session).toEqual({ token: 'abc123' });
|
|
262
|
+
expect(ctx.cache).toBeInstanceOf(Map);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { AUI } from '../index';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
describe('AUI Simple API', () => {
|
|
6
|
+
let aui: AUI;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
aui = new AUI();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('Basic Tool Creation', () => {
|
|
13
|
+
it('should create a simple tool with just execute', async () => {
|
|
14
|
+
const tool = aui
|
|
15
|
+
.tool('simple')
|
|
16
|
+
.execute(async () => ({ result: 'success' }));
|
|
17
|
+
|
|
18
|
+
const result = await tool.run({});
|
|
19
|
+
expect(result).toEqual({ result: 'success' });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should create a tool with input validation', async () => {
|
|
23
|
+
const tool = aui
|
|
24
|
+
.tool('validated')
|
|
25
|
+
.input(z.object({ name: z.string() }))
|
|
26
|
+
.execute(async ({ input }) => ({ greeting: `Hello, ${input.name}!` }));
|
|
27
|
+
|
|
28
|
+
const result = await tool.run({ name: 'World' });
|
|
29
|
+
expect(result).toEqual({ greeting: 'Hello, World!' });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should throw on invalid input', async () => {
|
|
33
|
+
const tool = aui
|
|
34
|
+
.tool('strict')
|
|
35
|
+
.input(z.object({ age: z.number().min(0) }))
|
|
36
|
+
.execute(async ({ input }) => ({ valid: true, age: input.age }));
|
|
37
|
+
|
|
38
|
+
await expect(tool.run({ age: -1 } as any)).rejects.toThrow();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('Client/Server Execution', () => {
|
|
43
|
+
it('should use server execute when on server', async () => {
|
|
44
|
+
const tool = aui
|
|
45
|
+
.tool('dual')
|
|
46
|
+
.execute(async () => ({ from: 'server' }))
|
|
47
|
+
.clientExecute(async () => ({ from: 'client' }));
|
|
48
|
+
|
|
49
|
+
const serverCtx = aui.createContext({ isServer: true });
|
|
50
|
+
const result = await tool.run({}, serverCtx);
|
|
51
|
+
expect(result).toEqual({ from: 'server' });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should use client execute when on client', async () => {
|
|
55
|
+
const tool = aui
|
|
56
|
+
.tool('dual')
|
|
57
|
+
.execute(async () => ({ from: 'server' }))
|
|
58
|
+
.clientExecute(async () => ({ from: 'client' }));
|
|
59
|
+
|
|
60
|
+
const clientCtx = aui.createContext({ isServer: false });
|
|
61
|
+
const result = await tool.run({}, clientCtx);
|
|
62
|
+
expect(result).toEqual({ from: 'client' });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('Middleware', () => {
|
|
67
|
+
it('should apply middleware in order', async () => {
|
|
68
|
+
const log: string[] = [];
|
|
69
|
+
|
|
70
|
+
const tool = aui
|
|
71
|
+
.tool('middleware')
|
|
72
|
+
.middleware(async ({ next }) => {
|
|
73
|
+
log.push('before-1');
|
|
74
|
+
const result = await next();
|
|
75
|
+
log.push('after-1');
|
|
76
|
+
return result;
|
|
77
|
+
})
|
|
78
|
+
.middleware(async ({ next }) => {
|
|
79
|
+
log.push('before-2');
|
|
80
|
+
const result = await next();
|
|
81
|
+
log.push('after-2');
|
|
82
|
+
return result;
|
|
83
|
+
})
|
|
84
|
+
.execute(async () => {
|
|
85
|
+
log.push('execute');
|
|
86
|
+
return { done: true };
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await tool.run({});
|
|
90
|
+
expect(log).toEqual(['before-1', 'before-2', 'execute', 'after-2', 'after-1']);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should pass context through middleware', async () => {
|
|
94
|
+
const tool = aui
|
|
95
|
+
.tool('context-test')
|
|
96
|
+
.input(z.object({ value: z.number() }))
|
|
97
|
+
.middleware(async ({ input, ctx, next }) => {
|
|
98
|
+
ctx.cache.set('multiplier', 2);
|
|
99
|
+
return next();
|
|
100
|
+
})
|
|
101
|
+
.execute(async ({ input, ctx }) => {
|
|
102
|
+
const multiplier = ctx?.cache.get('multiplier') || 1;
|
|
103
|
+
return { result: input.value * multiplier };
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = await tool.run({ value: 5 });
|
|
107
|
+
expect(result).toEqual({ result: 10 });
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('Tool Registry', () => {
|
|
112
|
+
it('should register and retrieve tools', () => {
|
|
113
|
+
const tool1 = aui.tool('tool1').execute(async () => ({}));
|
|
114
|
+
const tool2 = aui.tool('tool2').execute(async () => ({}));
|
|
115
|
+
|
|
116
|
+
expect(aui.has('tool1')).toBe(true);
|
|
117
|
+
expect(aui.has('tool2')).toBe(true);
|
|
118
|
+
expect(aui.getToolNames()).toContain('tool1');
|
|
119
|
+
expect(aui.getToolNames()).toContain('tool2');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should support tags', () => {
|
|
123
|
+
const tool1 = aui.tool('tagged1').tag('ai', 'control').execute(async () => ({}));
|
|
124
|
+
const tool2 = aui.tool('tagged2').tag('ai', 'navigation').execute(async () => ({}));
|
|
125
|
+
const tool3 = aui.tool('tagged3').tag('control').execute(async () => ({}));
|
|
126
|
+
|
|
127
|
+
const aiTools = aui.findByTag('ai');
|
|
128
|
+
expect(aiTools).toHaveLength(2);
|
|
129
|
+
|
|
130
|
+
const controlTools = aui.findByTags('ai', 'control');
|
|
131
|
+
expect(controlTools).toHaveLength(1);
|
|
132
|
+
expect(controlTools[0].name).toBe('tagged1');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('Tool Metadata', () => {
|
|
137
|
+
it('should store and retrieve metadata', () => {
|
|
138
|
+
const tool = aui
|
|
139
|
+
.tool('documented')
|
|
140
|
+
.describe('This tool does something important')
|
|
141
|
+
.tag('important', 'documented')
|
|
142
|
+
.input(z.object({ data: z.string() }))
|
|
143
|
+
.execute(async ({ input }) => ({ processed: input.data }));
|
|
144
|
+
|
|
145
|
+
expect(tool.name).toBe('documented');
|
|
146
|
+
expect(tool.description).toBe('This tool does something important');
|
|
147
|
+
expect(tool.tags).toEqual(['important', 'documented']);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should serialize to JSON', () => {
|
|
151
|
+
const tool = aui
|
|
152
|
+
.tool('json-tool')
|
|
153
|
+
.describe('JSON serializable tool')
|
|
154
|
+
.input(z.object({ x: z.number() }))
|
|
155
|
+
.execute(async () => ({}))
|
|
156
|
+
.clientExecute(async () => ({}))
|
|
157
|
+
.middleware(async ({ next }) => next());
|
|
158
|
+
|
|
159
|
+
const json = tool.toJSON();
|
|
160
|
+
expect(json).toEqual({
|
|
161
|
+
name: 'json-tool',
|
|
162
|
+
description: 'JSON serializable tool',
|
|
163
|
+
tags: [],
|
|
164
|
+
hasInput: true,
|
|
165
|
+
hasExecute: true,
|
|
166
|
+
hasClientExecute: true,
|
|
167
|
+
hasRender: false,
|
|
168
|
+
hasMiddleware: true
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('Caching Context', () => {
|
|
174
|
+
it('should cache results in context', async () => {
|
|
175
|
+
let executionCount = 0;
|
|
176
|
+
|
|
177
|
+
const tool = aui
|
|
178
|
+
.tool('cacheable')
|
|
179
|
+
.input(z.object({ key: z.string() }))
|
|
180
|
+
.clientExecute(async ({ input, ctx }) => {
|
|
181
|
+
const cached = ctx.cache.get(input.key);
|
|
182
|
+
if (cached) return cached;
|
|
183
|
+
|
|
184
|
+
executionCount++;
|
|
185
|
+
const result = { value: `computed-${input.key}`, count: executionCount };
|
|
186
|
+
ctx.cache.set(input.key, result);
|
|
187
|
+
return result;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const ctx = aui.createContext({ isServer: false });
|
|
191
|
+
|
|
192
|
+
const result1 = await tool.run({ key: 'test' }, ctx);
|
|
193
|
+
const result2 = await tool.run({ key: 'test' }, ctx);
|
|
194
|
+
const result3 = await tool.run({ key: 'other' }, ctx);
|
|
195
|
+
|
|
196
|
+
expect(result1).toEqual(result2);
|
|
197
|
+
expect(executionCount).toBe(2); // Once for 'test', once for 'other'
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|