@inferencesh/sdk 0.6.7 → 0.6.10
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/CHANGELOG.md +32 -1
- package/README.md +305 -36
- package/dist/agent/actions.test.d.ts +1 -0
- package/dist/agent/actions.test.js +487 -0
- package/dist/agent/api.test.d.ts +1 -0
- package/dist/agent/api.test.js +208 -0
- package/dist/agent/reducer.test.js +4 -0
- package/dist/agent/types.test.d.ts +1 -0
- package/dist/agent/types.test.js +75 -0
- package/dist/api/agents.test.js +289 -35
- package/dist/api/apps.test.d.ts +1 -0
- package/dist/api/apps.test.js +67 -0
- package/dist/api/chats.test.d.ts +1 -0
- package/dist/api/chats.test.js +33 -0
- package/dist/api/engines.test.d.ts +1 -0
- package/dist/api/engines.test.js +55 -0
- package/dist/api/files.test.js +3 -6
- package/dist/api/flow-runs.test.d.ts +1 -0
- package/dist/api/flow-runs.test.js +55 -0
- package/dist/api/flows.test.d.ts +1 -0
- package/dist/api/flows.test.js +43 -0
- package/dist/api/sessions.d.ts +2 -1
- package/dist/api/sessions.js +2 -1
- package/dist/api/sessions.test.d.ts +1 -0
- package/dist/api/sessions.test.js +49 -0
- package/dist/api/tasks.test.js +50 -18
- package/dist/client.test.js +8 -8
- package/dist/http/client.js +5 -26
- package/dist/http/client.test.js +51 -13
- package/dist/proxy/express.test.d.ts +1 -0
- package/dist/proxy/express.test.js +106 -0
- package/dist/proxy/index.test.js +10 -1
- package/dist/stream.test.js +139 -0
- package/dist/tool-builder.test.js +69 -2
- package/dist/types.d.ts +731 -30
- package/dist/types.js +154 -14
- package/package.json +11 -4
package/dist/http/client.test.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { HttpClient, createHttpClient } from './client';
|
|
2
2
|
import { InferenceError, RequirementsNotMetException } from './errors';
|
|
3
|
+
import { EventSource } from 'eventsource';
|
|
4
|
+
jest.mock('eventsource');
|
|
3
5
|
const mockFetch = jest.fn();
|
|
4
6
|
global.fetch = mockFetch;
|
|
7
|
+
const MockEventSource = EventSource;
|
|
5
8
|
function mockJsonResponse(body, status = 200, ok = true) {
|
|
6
9
|
mockFetch.mockResolvedValueOnce({
|
|
7
10
|
ok,
|
|
@@ -38,17 +41,12 @@ describe('HttpClient', () => {
|
|
|
38
41
|
describe('request', () => {
|
|
39
42
|
const client = () => new HttpClient({ apiKey: 'test-key' });
|
|
40
43
|
it('should return parsed data on success', async () => {
|
|
41
|
-
mockJsonResponse({
|
|
44
|
+
mockJsonResponse({ id: 'task-1' });
|
|
42
45
|
const result = await client().request('get', '/tasks/task-1');
|
|
43
46
|
expect(result).toEqual({ id: 'task-1' });
|
|
44
47
|
});
|
|
45
|
-
it('should return null
|
|
46
|
-
mockJsonResponse(
|
|
47
|
-
const result = await client().request('post', '/tasks/task-1/cancel');
|
|
48
|
-
expect(result).toBeNull();
|
|
49
|
-
});
|
|
50
|
-
it('should return null when success is true with explicit data: null', async () => {
|
|
51
|
-
mockJsonResponse({ success: true, data: null });
|
|
48
|
+
it('should return null for null response body', async () => {
|
|
49
|
+
mockJsonResponse(null);
|
|
52
50
|
const result = await client().request('post', '/tasks/task-1/cancel');
|
|
53
51
|
expect(result).toBeNull();
|
|
54
52
|
});
|
|
@@ -61,8 +59,8 @@ describe('HttpClient', () => {
|
|
|
61
59
|
const result = await client().request('delete', '/tasks/task-1');
|
|
62
60
|
expect(result).toBeUndefined();
|
|
63
61
|
});
|
|
64
|
-
it('should throw InferenceError
|
|
65
|
-
mockJsonResponse({
|
|
62
|
+
it('should throw InferenceError on non-ok response', async () => {
|
|
63
|
+
mockJsonResponse({ message: 'Invalid request' }, 400, false);
|
|
66
64
|
const err = await client().request('get', '/tasks/1').catch((e) => e);
|
|
67
65
|
expect(err).toBeInstanceOf(InferenceError);
|
|
68
66
|
expect(err.message).toContain('Invalid request');
|
|
@@ -91,7 +89,7 @@ describe('HttpClient', () => {
|
|
|
91
89
|
.mockResolvedValueOnce({
|
|
92
90
|
ok: true,
|
|
93
91
|
status: 200,
|
|
94
|
-
text: () => Promise.resolve(JSON.stringify({
|
|
92
|
+
text: () => Promise.resolve(JSON.stringify({ ok: true })),
|
|
95
93
|
});
|
|
96
94
|
const result = await httpClient.request('get', '/tasks/1');
|
|
97
95
|
expect(result).toEqual({ ok: true });
|
|
@@ -99,7 +97,7 @@ describe('HttpClient', () => {
|
|
|
99
97
|
});
|
|
100
98
|
it('should route through proxy with x-inf-target-url header', async () => {
|
|
101
99
|
const proxyClient = new HttpClient({ proxyUrl: 'https://proxy.example.com' });
|
|
102
|
-
mockJsonResponse({
|
|
100
|
+
mockJsonResponse({ id: '1' });
|
|
103
101
|
await proxyClient.request('get', '/tasks/1');
|
|
104
102
|
expect(mockFetch).toHaveBeenCalledWith('https://proxy.example.com', expect.objectContaining({
|
|
105
103
|
headers: expect.objectContaining({
|
|
@@ -108,7 +106,7 @@ describe('HttpClient', () => {
|
|
|
108
106
|
}));
|
|
109
107
|
});
|
|
110
108
|
it('should serialize array query params as JSON', async () => {
|
|
111
|
-
mockJsonResponse(
|
|
109
|
+
mockJsonResponse([]);
|
|
112
110
|
await client().request('get', '/tasks', {
|
|
113
111
|
params: { ids: ['a', 'b'] },
|
|
114
112
|
});
|
|
@@ -149,4 +147,44 @@ describe('HttpClient', () => {
|
|
|
149
147
|
expect(config.headers.Authorization).toBe('Bearer dynamic-token');
|
|
150
148
|
});
|
|
151
149
|
});
|
|
150
|
+
describe('createEventSource', () => {
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
MockEventSource.mockReset();
|
|
153
|
+
});
|
|
154
|
+
it('should attach Bearer token in direct mode via custom fetch', async () => {
|
|
155
|
+
let capturedFetch;
|
|
156
|
+
MockEventSource.mockImplementation((_url, options) => {
|
|
157
|
+
capturedFetch = options?.fetch;
|
|
158
|
+
return { close: jest.fn(), onmessage: null, onerror: null };
|
|
159
|
+
});
|
|
160
|
+
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
161
|
+
const client = new HttpClient({ apiKey: 'sse-key' });
|
|
162
|
+
await client.createEventSource('/tasks/task-1/stream');
|
|
163
|
+
expect(capturedFetch).toBeDefined();
|
|
164
|
+
await capturedFetch('https://api.inference.sh/tasks/task-1/stream', { headers: {} });
|
|
165
|
+
expect(mockFetch).toHaveBeenCalledWith('https://api.inference.sh/tasks/task-1/stream', expect.objectContaining({
|
|
166
|
+
headers: expect.objectContaining({
|
|
167
|
+
Authorization: 'Bearer sse-key',
|
|
168
|
+
}),
|
|
169
|
+
}));
|
|
170
|
+
});
|
|
171
|
+
it('should route through proxy with target URL header on custom fetch', async () => {
|
|
172
|
+
let capturedFetch;
|
|
173
|
+
MockEventSource.mockImplementation((url, options) => {
|
|
174
|
+
capturedFetch = options?.fetch;
|
|
175
|
+
expect(url).toContain('https://proxy.example.com');
|
|
176
|
+
expect(url).toContain('__inf_target=');
|
|
177
|
+
return { close: jest.fn(), onmessage: null, onerror: null };
|
|
178
|
+
});
|
|
179
|
+
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
180
|
+
const client = new HttpClient({ proxyUrl: 'https://proxy.example.com' });
|
|
181
|
+
await client.createEventSource('/tasks/task-1/stream');
|
|
182
|
+
await capturedFetch('https://proxy.example.com', { headers: {} });
|
|
183
|
+
expect(mockFetch).toHaveBeenCalledWith('https://proxy.example.com', expect.objectContaining({
|
|
184
|
+
headers: expect.objectContaining({
|
|
185
|
+
'x-inf-target-url': 'https://api.inference.sh/tasks/task-1/stream',
|
|
186
|
+
}),
|
|
187
|
+
}));
|
|
188
|
+
});
|
|
189
|
+
});
|
|
152
190
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { INF_TARGET_HEADER } from './index';
|
|
2
|
+
import { createHandler } from './express';
|
|
3
|
+
function createMockResponse() {
|
|
4
|
+
const res = {
|
|
5
|
+
statusCode: 200,
|
|
6
|
+
headers: {},
|
|
7
|
+
body: undefined,
|
|
8
|
+
chunks: [],
|
|
9
|
+
status(code) {
|
|
10
|
+
this.statusCode = code;
|
|
11
|
+
return this;
|
|
12
|
+
},
|
|
13
|
+
setHeader(name, value) {
|
|
14
|
+
this.headers[name] = value;
|
|
15
|
+
},
|
|
16
|
+
json(data) {
|
|
17
|
+
this.body = data;
|
|
18
|
+
return this;
|
|
19
|
+
},
|
|
20
|
+
send(data) {
|
|
21
|
+
this.body = data;
|
|
22
|
+
return this;
|
|
23
|
+
},
|
|
24
|
+
write(chunk) {
|
|
25
|
+
this.chunks.push(chunk);
|
|
26
|
+
},
|
|
27
|
+
end() {
|
|
28
|
+
return;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
return res;
|
|
32
|
+
}
|
|
33
|
+
describe('express createHandler', () => {
|
|
34
|
+
const originalFetch = global.fetch;
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
jest.clearAllMocks();
|
|
37
|
+
process.env.INFERENCE_API_KEY = 'express-test-key';
|
|
38
|
+
});
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
global.fetch = originalFetch;
|
|
41
|
+
});
|
|
42
|
+
it('should return 400 when target URL is missing', async () => {
|
|
43
|
+
const handler = createHandler();
|
|
44
|
+
const req = {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: {},
|
|
47
|
+
headers: {},
|
|
48
|
+
query: {},
|
|
49
|
+
};
|
|
50
|
+
const res = createMockResponse();
|
|
51
|
+
await handler(req, res, jest.fn());
|
|
52
|
+
expect(res.statusCode).toBe(400);
|
|
53
|
+
expect(res.body).toEqual({
|
|
54
|
+
error: `Missing ${INF_TARGET_HEADER} header or __inf_target query param`,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
it('should proxy JSON responses through res.json()', async () => {
|
|
58
|
+
global.fetch = jest.fn().mockResolvedValue(new Response(JSON.stringify({ ok: true }), {
|
|
59
|
+
status: 200,
|
|
60
|
+
headers: { 'content-type': 'application/json' },
|
|
61
|
+
}));
|
|
62
|
+
const handler = createHandler();
|
|
63
|
+
const target = 'https://api.inference.sh/v1/run';
|
|
64
|
+
const req = {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
body: { prompt: 'hi' },
|
|
67
|
+
headers: { [INF_TARGET_HEADER]: target },
|
|
68
|
+
query: {},
|
|
69
|
+
};
|
|
70
|
+
const res = createMockResponse();
|
|
71
|
+
await handler(req, res, jest.fn());
|
|
72
|
+
expect(res.statusCode).toBe(200);
|
|
73
|
+
expect(res.body).toEqual({ ok: true });
|
|
74
|
+
expect(global.fetch).toHaveBeenCalledWith(target, expect.objectContaining({
|
|
75
|
+
headers: expect.objectContaining({
|
|
76
|
+
authorization: 'Bearer express-test-key',
|
|
77
|
+
}),
|
|
78
|
+
}));
|
|
79
|
+
});
|
|
80
|
+
it('should stream SSE responses with res.write()', async () => {
|
|
81
|
+
const encoder = new TextEncoder();
|
|
82
|
+
const stream = new ReadableStream({
|
|
83
|
+
start(controller) {
|
|
84
|
+
controller.enqueue(encoder.encode('data: {"x":1}\n\n'));
|
|
85
|
+
controller.close();
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
global.fetch = jest.fn().mockResolvedValue(new Response(stream, {
|
|
89
|
+
status: 200,
|
|
90
|
+
headers: { 'content-type': 'text/event-stream' },
|
|
91
|
+
}));
|
|
92
|
+
const handler = createHandler();
|
|
93
|
+
const target = 'https://api.inference.sh/v1/stream';
|
|
94
|
+
const req = {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
body: {},
|
|
97
|
+
headers: { [INF_TARGET_HEADER]: target },
|
|
98
|
+
query: {},
|
|
99
|
+
};
|
|
100
|
+
const res = createMockResponse();
|
|
101
|
+
await handler(req, res, jest.fn());
|
|
102
|
+
expect(res.statusCode).toBe(200);
|
|
103
|
+
expect(res.chunks.length).toBeGreaterThan(0);
|
|
104
|
+
expect(res.body).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
});
|
package/dist/proxy/index.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { INF_TARGET_HEADER, INF_TARGET_PARAM, processProxyRequest, } from './index';
|
|
1
|
+
import { INF_TARGET_HEADER, INF_TARGET_PARAM, headersToRecord, processProxyRequest, } from './index';
|
|
2
2
|
function createTestAdapter(overrides = {}) {
|
|
3
3
|
const state = { status: 0 };
|
|
4
4
|
const adapter = {
|
|
@@ -26,6 +26,15 @@ function createTestAdapter(overrides = {}) {
|
|
|
26
26
|
};
|
|
27
27
|
return adapter;
|
|
28
28
|
}
|
|
29
|
+
describe('headersToRecord', () => {
|
|
30
|
+
it('should convert Headers to a plain record', () => {
|
|
31
|
+
const headers = new Headers({ 'x-test': 'value', 'content-type': 'application/json' });
|
|
32
|
+
expect(headersToRecord(headers)).toEqual({
|
|
33
|
+
'x-test': 'value',
|
|
34
|
+
'content-type': 'application/json',
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
29
38
|
describe('processProxyRequest', () => {
|
|
30
39
|
const originalFetch = global.fetch;
|
|
31
40
|
const originalApiKey = process.env.INFERENCE_API_KEY;
|
package/dist/stream.test.js
CHANGED
|
@@ -211,5 +211,144 @@ describe('StreamManager', () => {
|
|
|
211
211
|
await new Promise((r) => setTimeout(r, 50));
|
|
212
212
|
expect(createEventSource).toHaveBeenCalledTimes(1);
|
|
213
213
|
});
|
|
214
|
+
it('should stop reconnecting after maxReconnects initial failures', async () => {
|
|
215
|
+
jest.useFakeTimers();
|
|
216
|
+
const onError = jest.fn();
|
|
217
|
+
const createEventSource = jest
|
|
218
|
+
.fn()
|
|
219
|
+
.mockResolvedValue(mockEventSource);
|
|
220
|
+
const manager = new StreamManager({
|
|
221
|
+
createEventSource,
|
|
222
|
+
autoReconnect: true,
|
|
223
|
+
maxReconnects: 2,
|
|
224
|
+
reconnectDelayMs: 100,
|
|
225
|
+
onError,
|
|
226
|
+
});
|
|
227
|
+
await manager.connect();
|
|
228
|
+
mockEventSource.onerror?.({});
|
|
229
|
+
jest.advanceTimersByTime(100);
|
|
230
|
+
await Promise.resolve();
|
|
231
|
+
expect(createEventSource).toHaveBeenCalledTimes(2);
|
|
232
|
+
mockEventSource.onerror?.({});
|
|
233
|
+
jest.advanceTimersByTime(100);
|
|
234
|
+
await Promise.resolve();
|
|
235
|
+
expect(createEventSource).toHaveBeenCalledTimes(3);
|
|
236
|
+
mockEventSource.onerror?.({});
|
|
237
|
+
jest.advanceTimersByTime(100);
|
|
238
|
+
await Promise.resolve();
|
|
239
|
+
expect(createEventSource).toHaveBeenCalledTimes(3);
|
|
240
|
+
jest.useRealTimers();
|
|
241
|
+
});
|
|
242
|
+
it('should call onError and stop when createEventSource throws', async () => {
|
|
243
|
+
jest.useFakeTimers();
|
|
244
|
+
const onError = jest.fn();
|
|
245
|
+
const createEventSource = jest
|
|
246
|
+
.fn()
|
|
247
|
+
.mockRejectedValue(new Error('connection refused'));
|
|
248
|
+
const manager = new StreamManager({
|
|
249
|
+
createEventSource,
|
|
250
|
+
autoReconnect: false,
|
|
251
|
+
onError,
|
|
252
|
+
});
|
|
253
|
+
await manager.connect();
|
|
254
|
+
expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'connection refused' }));
|
|
255
|
+
jest.useRealTimers();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
describe('stopAfter and clearStopTimeout', () => {
|
|
259
|
+
it('should stop after the configured delay', async () => {
|
|
260
|
+
jest.useFakeTimers();
|
|
261
|
+
const onStop = jest.fn();
|
|
262
|
+
const manager = new StreamManager({
|
|
263
|
+
createEventSource: async () => mockEventSource,
|
|
264
|
+
onStop,
|
|
265
|
+
});
|
|
266
|
+
await manager.connect();
|
|
267
|
+
manager.stopAfter(5000);
|
|
268
|
+
jest.advanceTimersByTime(5000);
|
|
269
|
+
expect(mockEventSource.close).toHaveBeenCalled();
|
|
270
|
+
expect(onStop).toHaveBeenCalled();
|
|
271
|
+
jest.useRealTimers();
|
|
272
|
+
});
|
|
273
|
+
it('should cancel a pending stop when clearStopTimeout is called', async () => {
|
|
274
|
+
jest.useFakeTimers();
|
|
275
|
+
const manager = new StreamManager({
|
|
276
|
+
createEventSource: async () => mockEventSource,
|
|
277
|
+
});
|
|
278
|
+
await manager.connect();
|
|
279
|
+
manager.stopAfter(5000);
|
|
280
|
+
manager.clearStopTimeout();
|
|
281
|
+
jest.advanceTimersByTime(5000);
|
|
282
|
+
expect(mockEventSource.close).not.toHaveBeenCalled();
|
|
283
|
+
jest.useRealTimers();
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
describe('addEventListener lifecycle', () => {
|
|
287
|
+
it('should register listeners on an existing connection when added after connect', async () => {
|
|
288
|
+
const typedListeners = {};
|
|
289
|
+
const eventSource = {
|
|
290
|
+
onmessage: null,
|
|
291
|
+
onerror: null,
|
|
292
|
+
close: jest.fn(),
|
|
293
|
+
addEventListener: (eventName, handler) => {
|
|
294
|
+
const listeners = typedListeners[eventName] || new Set();
|
|
295
|
+
listeners.add(handler);
|
|
296
|
+
typedListeners[eventName] = listeners;
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
const manager = new StreamManager({
|
|
300
|
+
createEventSource: async () => eventSource,
|
|
301
|
+
});
|
|
302
|
+
await manager.connect();
|
|
303
|
+
const onChat = jest.fn();
|
|
304
|
+
manager.addEventListener('chats', onChat);
|
|
305
|
+
const payload = { id: 'chat-2' };
|
|
306
|
+
typedListeners.chats?.forEach((handler) => handler({ data: JSON.stringify(payload) }));
|
|
307
|
+
expect(onChat).toHaveBeenCalledWith(payload);
|
|
308
|
+
});
|
|
309
|
+
it('should remove a listener when the cleanup function is called', async () => {
|
|
310
|
+
const typedListeners = {};
|
|
311
|
+
const eventSource = {
|
|
312
|
+
onmessage: null,
|
|
313
|
+
onerror: null,
|
|
314
|
+
close: jest.fn(),
|
|
315
|
+
addEventListener: (eventName, handler) => {
|
|
316
|
+
const listeners = typedListeners[eventName] || new Set();
|
|
317
|
+
listeners.add(handler);
|
|
318
|
+
typedListeners[eventName] = listeners;
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
const manager = new StreamManager({
|
|
322
|
+
createEventSource: async () => eventSource,
|
|
323
|
+
});
|
|
324
|
+
const onChat = jest.fn();
|
|
325
|
+
const unsubscribe = manager.addEventListener('chats', onChat);
|
|
326
|
+
await manager.connect();
|
|
327
|
+
unsubscribe();
|
|
328
|
+
typedListeners.chats?.forEach((handler) => handler({ data: '{"id":"chat-3"}' }));
|
|
329
|
+
expect(onChat).not.toHaveBeenCalled();
|
|
330
|
+
});
|
|
331
|
+
it('should call onError when typed event payload is invalid JSON', async () => {
|
|
332
|
+
const typedListeners = {};
|
|
333
|
+
const eventSource = {
|
|
334
|
+
onmessage: null,
|
|
335
|
+
onerror: null,
|
|
336
|
+
close: jest.fn(),
|
|
337
|
+
addEventListener: (eventName, handler) => {
|
|
338
|
+
const listeners = typedListeners[eventName] || new Set();
|
|
339
|
+
listeners.add(handler);
|
|
340
|
+
typedListeners[eventName] = listeners;
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
const onError = jest.fn();
|
|
344
|
+
const manager = new StreamManager({
|
|
345
|
+
createEventSource: async () => eventSource,
|
|
346
|
+
onError,
|
|
347
|
+
});
|
|
348
|
+
manager.addEventListener('chats', jest.fn());
|
|
349
|
+
await manager.connect();
|
|
350
|
+
typedListeners.chats?.forEach((handler) => handler({ data: 'not-json' }));
|
|
351
|
+
expect(onError).toHaveBeenCalled();
|
|
352
|
+
});
|
|
214
353
|
});
|
|
215
354
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { tool, appTool, agentTool, webhookTool, internalTools, string, number, integer, boolean, enumOf, object, array, optional, } from './tool-builder';
|
|
2
|
-
import { ToolTypeClient, ToolTypeApp, ToolTypeAgent, ToolTypeHook } from './types';
|
|
1
|
+
import { tool, appTool, agentTool, webhookTool, httpTool, callTool, mcpTool, internalTools, string, number, integer, boolean, enumOf, object, array, optional, } from './tool-builder';
|
|
2
|
+
import { ToolTypeClient, ToolTypeApp, ToolTypeAgent, ToolTypeHook, ToolTypeHTTP, ToolTypeMCP, IntegrationProviderGoogle, } from './types';
|
|
3
3
|
describe('Schema Helpers', () => {
|
|
4
4
|
describe('string', () => {
|
|
5
5
|
it('creates string schema without description', () => {
|
|
@@ -109,6 +109,18 @@ describe('ClientToolBuilder (tool)', () => {
|
|
|
109
109
|
const t = tool('get_data').display('Get Data').build();
|
|
110
110
|
expect(t.display_name).toBe('Get Data');
|
|
111
111
|
});
|
|
112
|
+
it('creates tool with displayName alias', () => {
|
|
113
|
+
const t = tool('get_data').displayName('Get Data').build();
|
|
114
|
+
expect(t.display_name).toBe('Get Data');
|
|
115
|
+
});
|
|
116
|
+
it('returns schema and handler from handler()', async () => {
|
|
117
|
+
const clientTool = tool('greet')
|
|
118
|
+
.describe('Say hi')
|
|
119
|
+
.param('name', string('Name'))
|
|
120
|
+
.handler(async (args) => `Hello, ${args.name}`);
|
|
121
|
+
expect(clientTool.schema.name).toBe('greet');
|
|
122
|
+
expect(await clientTool.handler({ name: 'Ada' })).toBe('Hello, Ada');
|
|
123
|
+
});
|
|
112
124
|
it('creates tool with parameters', () => {
|
|
113
125
|
const t = tool('add')
|
|
114
126
|
.param('a', number('First number'))
|
|
@@ -240,6 +252,61 @@ describe('AgentToolBuilder (agentTool)', () => {
|
|
|
240
252
|
expect(t.require_approval).toBe(true);
|
|
241
253
|
});
|
|
242
254
|
});
|
|
255
|
+
describe('HTTPToolBuilder (httpTool)', () => {
|
|
256
|
+
it('includes custom method and headers in the built tool', () => {
|
|
257
|
+
const t = httpTool('fetch', 'https://api.example.com/data')
|
|
258
|
+
.method('GET')
|
|
259
|
+
.header('X-Custom', '1')
|
|
260
|
+
.build();
|
|
261
|
+
expect(t.http?.method).toBe('GET');
|
|
262
|
+
expect(t.http?.headers).toEqual({ 'X-Custom': '1' });
|
|
263
|
+
});
|
|
264
|
+
it('omits method when POST is the default', () => {
|
|
265
|
+
const t = httpTool('post', 'https://api.example.com').build();
|
|
266
|
+
expect(t.http?.method).toBeUndefined();
|
|
267
|
+
});
|
|
268
|
+
it('should attach integration auth with provider and integration id', () => {
|
|
269
|
+
const t = httpTool('gmail_send', 'https://api.example.com/send')
|
|
270
|
+
.auth({ integration: IntegrationProviderGoogle, integrationId: 'int-123' })
|
|
271
|
+
.build();
|
|
272
|
+
expect(t.type).toBe(ToolTypeHTTP);
|
|
273
|
+
expect(t.http?.auth).toEqual({
|
|
274
|
+
type: 'integration',
|
|
275
|
+
provider: IntegrationProviderGoogle,
|
|
276
|
+
integration_id: 'int-123',
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
it('should attach api key auth with default header', () => {
|
|
280
|
+
const t = httpTool('fetch', 'https://api.example.com').auth({ apiKey: 'KEY' }).build();
|
|
281
|
+
expect(t.http?.auth).toEqual({
|
|
282
|
+
type: 'api_key',
|
|
283
|
+
secret: 'KEY',
|
|
284
|
+
header: 'X-API-Key',
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
it('should attach bearer auth', () => {
|
|
288
|
+
const t = httpTool('fetch', 'https://api.example.com')
|
|
289
|
+
.auth({ bearer: 'token-abc' })
|
|
290
|
+
.build();
|
|
291
|
+
expect(t.http?.auth).toEqual({
|
|
292
|
+
type: 'bearer',
|
|
293
|
+
secret: 'token-abc',
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
it('callTool should be an alias for httpTool', () => {
|
|
297
|
+
const t = callTool('ping', 'https://api.example.com/ping').method('GET').build();
|
|
298
|
+
expect(t.type).toBe(ToolTypeHTTP);
|
|
299
|
+
expect(t.http?.method).toBe('GET');
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
describe('MCPToolBuilder (mcpTool)', () => {
|
|
303
|
+
it('creates MCP tool with integration and tool name', () => {
|
|
304
|
+
const t = mcpTool('search_docs', 'int-mcp-1', 'search').describe('Search docs').build();
|
|
305
|
+
expect(t.type).toBe(ToolTypeMCP);
|
|
306
|
+
expect(t.mcp).toEqual({ integration_id: 'int-mcp-1', tool_name: 'search' });
|
|
307
|
+
expect(t.description).toBe('Search docs');
|
|
308
|
+
});
|
|
309
|
+
});
|
|
243
310
|
describe('WebhookToolBuilder (webhookTool)', () => {
|
|
244
311
|
it('creates webhook tool with URL', () => {
|
|
245
312
|
const t = webhookTool('notify', 'https://api.example.com/webhook')
|