@inferencesh/sdk 0.6.15 → 0.6.20
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/dist/agent/provider.d.ts +2 -1
- package/dist/api/apps.d.ts +4 -0
- package/dist/api/apps.js +6 -0
- package/dist/api/index.d.ts +3 -3
- package/dist/http/index.d.ts +3 -3
- package/dist/index.d.ts +9 -9
- package/dist/proxy/index.js +11 -3
- package/dist/types.d.ts +30 -4
- package/dist/types.js +4 -0
- package/package.json +8 -5
- package/dist/agent/actions.test.d.ts +0 -1
- package/dist/agent/actions.test.js +0 -820
- package/dist/agent/api.test.d.ts +0 -1
- package/dist/agent/api.test.js +0 -246
- package/dist/agent/reducer.test.d.ts +0 -1
- package/dist/agent/reducer.test.js +0 -131
- package/dist/agent/types.test.d.ts +0 -1
- package/dist/agent/types.test.js +0 -75
- package/dist/api/agents.test.d.ts +0 -1
- package/dist/api/agents.test.js +0 -1014
- package/dist/api/api-keys.test.d.ts +0 -1
- package/dist/api/api-keys.test.js +0 -44
- package/dist/api/apps.test.d.ts +0 -1
- package/dist/api/apps.test.js +0 -135
- package/dist/api/chats.test.d.ts +0 -1
- package/dist/api/chats.test.js +0 -85
- package/dist/api/engines.test.d.ts +0 -1
- package/dist/api/engines.test.js +0 -133
- package/dist/api/files.test.d.ts +0 -1
- package/dist/api/files.test.js +0 -256
- package/dist/api/flow-runs.test.d.ts +0 -1
- package/dist/api/flow-runs.test.js +0 -97
- package/dist/api/flows.test.d.ts +0 -1
- package/dist/api/flows.test.js +0 -118
- package/dist/api/integrations.test.d.ts +0 -1
- package/dist/api/integrations.test.js +0 -109
- package/dist/api/knowledge.test.d.ts +0 -1
- package/dist/api/knowledge.test.js +0 -238
- package/dist/api/mcp-servers.test.d.ts +0 -1
- package/dist/api/mcp-servers.test.js +0 -98
- package/dist/api/projects.test.d.ts +0 -1
- package/dist/api/projects.test.js +0 -61
- package/dist/api/search.test.d.ts +0 -1
- package/dist/api/search.test.js +0 -50
- package/dist/api/secrets.test.d.ts +0 -1
- package/dist/api/secrets.test.js +0 -61
- package/dist/api/sessions.test.d.ts +0 -1
- package/dist/api/sessions.test.js +0 -61
- package/dist/api/tasks.test.d.ts +0 -1
- package/dist/api/tasks.test.js +0 -381
- package/dist/api/teams.test.d.ts +0 -1
- package/dist/api/teams.test.js +0 -139
- package/dist/client.test.d.ts +0 -1
- package/dist/client.test.js +0 -446
- package/dist/http/client.test.d.ts +0 -1
- package/dist/http/client.test.js +0 -435
- package/dist/http/errors.test.d.ts +0 -1
- package/dist/http/errors.test.js +0 -66
- package/dist/http/poll.test.d.ts +0 -1
- package/dist/http/poll.test.js +0 -146
- package/dist/http/streamable.test.d.ts +0 -4
- package/dist/http/streamable.test.js +0 -510
- package/dist/integration.test.d.ts +0 -9
- package/dist/integration.test.js +0 -107
- package/dist/proxy/express.test.d.ts +0 -1
- package/dist/proxy/express.test.js +0 -106
- package/dist/proxy/hono.test.d.ts +0 -1
- package/dist/proxy/hono.test.js +0 -90
- package/dist/proxy/index.test.d.ts +0 -1
- package/dist/proxy/index.test.js +0 -173
- package/dist/proxy/nextjs.test.d.ts +0 -1
- package/dist/proxy/nextjs.test.js +0 -125
- package/dist/proxy/remix.test.d.ts +0 -1
- package/dist/proxy/remix.test.js +0 -66
- package/dist/proxy/svelte.test.d.ts +0 -1
- package/dist/proxy/svelte.test.js +0 -69
- package/dist/sessions.integration.test.d.ts +0 -13
- package/dist/sessions.integration.test.js +0 -310
- package/dist/stream.test.d.ts +0 -1
- package/dist/stream.test.js +0 -363
- package/dist/streamable.integration.test.d.ts +0 -11
- package/dist/streamable.integration.test.js +0 -150
- package/dist/tool-builder.test.d.ts +0 -1
- package/dist/tool-builder.test.js +0 -407
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -43
package/dist/http/client.test.js
DELETED
|
@@ -1,435 +0,0 @@
|
|
|
1
|
-
import { HttpClient, createHttpClient } from './client';
|
|
2
|
-
import { InferenceError, RequirementsNotMetException } from './errors';
|
|
3
|
-
import { EventSource } from 'eventsource';
|
|
4
|
-
jest.mock('eventsource');
|
|
5
|
-
const mockFetch = jest.fn();
|
|
6
|
-
global.fetch = mockFetch;
|
|
7
|
-
const MockEventSource = EventSource;
|
|
8
|
-
function mockJsonResponse(body, status = 200, ok = true) {
|
|
9
|
-
mockFetch.mockResolvedValueOnce({
|
|
10
|
-
ok,
|
|
11
|
-
status,
|
|
12
|
-
text: () => Promise.resolve(JSON.stringify(body)),
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
describe('HttpClient', () => {
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
jest.clearAllMocks();
|
|
18
|
-
});
|
|
19
|
-
describe('constructor', () => {
|
|
20
|
-
it('should throw when no apiKey, getToken, or proxyUrl', () => {
|
|
21
|
-
expect(() => new HttpClient({})).toThrow('Either apiKey, getToken, or proxyUrl is required');
|
|
22
|
-
});
|
|
23
|
-
it('should allow proxyUrl without apiKey', () => {
|
|
24
|
-
const client = new HttpClient({ proxyUrl: 'https://proxy.example.com' });
|
|
25
|
-
expect(client.isProxyMode()).toBe(true);
|
|
26
|
-
});
|
|
27
|
-
it('should expose stream and poll interval config', () => {
|
|
28
|
-
const client = new HttpClient({
|
|
29
|
-
apiKey: 'key',
|
|
30
|
-
stream: false,
|
|
31
|
-
pollIntervalMs: 5000,
|
|
32
|
-
});
|
|
33
|
-
expect(client.getStreamDefault()).toBe(false);
|
|
34
|
-
expect(client.getPollIntervalMs()).toBe(5000);
|
|
35
|
-
});
|
|
36
|
-
it('createHttpClient should return an HttpClient instance', () => {
|
|
37
|
-
const client = createHttpClient({ apiKey: 'key' });
|
|
38
|
-
expect(client).toBeInstanceOf(HttpClient);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
describe('request', () => {
|
|
42
|
-
const client = () => new HttpClient({ apiKey: 'test-key' });
|
|
43
|
-
it('should return parsed data on success', async () => {
|
|
44
|
-
mockJsonResponse({ id: 'task-1' });
|
|
45
|
-
const result = await client().request('get', '/tasks/task-1');
|
|
46
|
-
expect(result).toEqual({ id: 'task-1' });
|
|
47
|
-
});
|
|
48
|
-
it('should return null for null response body', async () => {
|
|
49
|
-
mockJsonResponse(null);
|
|
50
|
-
const result = await client().request('post', '/tasks/task-1/cancel');
|
|
51
|
-
expect(result).toBeNull();
|
|
52
|
-
});
|
|
53
|
-
it('should return undefined for 204 No Content', async () => {
|
|
54
|
-
mockFetch.mockResolvedValueOnce({
|
|
55
|
-
ok: true,
|
|
56
|
-
status: 204,
|
|
57
|
-
text: () => Promise.resolve(''),
|
|
58
|
-
});
|
|
59
|
-
const result = await client().request('delete', '/tasks/task-1');
|
|
60
|
-
expect(result).toBeUndefined();
|
|
61
|
-
});
|
|
62
|
-
it('should throw InferenceError on non-ok response', async () => {
|
|
63
|
-
mockJsonResponse({ message: 'Invalid request' }, 400, false);
|
|
64
|
-
const err = await client().request('get', '/tasks/1').catch((e) => e);
|
|
65
|
-
expect(err).toBeInstanceOf(InferenceError);
|
|
66
|
-
expect(err.message).toContain('Invalid request');
|
|
67
|
-
});
|
|
68
|
-
it('should throw RequirementsNotMetException on HTTP 412', async () => {
|
|
69
|
-
mockFetch.mockResolvedValueOnce({
|
|
70
|
-
ok: false,
|
|
71
|
-
status: 412,
|
|
72
|
-
text: () => Promise.resolve(JSON.stringify({
|
|
73
|
-
errors: [{ type: 'secret', key: 'API_KEY', message: 'Missing secret' }],
|
|
74
|
-
})),
|
|
75
|
-
});
|
|
76
|
-
await expect(client().request('post', '/apps/run')).rejects.toBeInstanceOf(RequirementsNotMetException);
|
|
77
|
-
});
|
|
78
|
-
it('should retry when onError handler calls retry', async () => {
|
|
79
|
-
const httpClient = new HttpClient({
|
|
80
|
-
apiKey: 'key',
|
|
81
|
-
onError: async (_error, retry) => retry(),
|
|
82
|
-
});
|
|
83
|
-
mockFetch
|
|
84
|
-
.mockResolvedValueOnce({
|
|
85
|
-
ok: false,
|
|
86
|
-
status: 500,
|
|
87
|
-
text: () => Promise.resolve('server error'),
|
|
88
|
-
})
|
|
89
|
-
.mockResolvedValueOnce({
|
|
90
|
-
ok: true,
|
|
91
|
-
status: 200,
|
|
92
|
-
text: () => Promise.resolve(JSON.stringify({ ok: true })),
|
|
93
|
-
});
|
|
94
|
-
const result = await httpClient.request('get', '/tasks/1');
|
|
95
|
-
expect(result).toEqual({ ok: true });
|
|
96
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
97
|
-
});
|
|
98
|
-
it('should route through proxy with x-inf-target-url header', async () => {
|
|
99
|
-
const proxyClient = new HttpClient({ proxyUrl: 'https://proxy.example.com' });
|
|
100
|
-
mockJsonResponse({ id: '1' });
|
|
101
|
-
await proxyClient.request('get', '/tasks/1');
|
|
102
|
-
expect(mockFetch).toHaveBeenCalledWith('https://proxy.example.com', expect.objectContaining({
|
|
103
|
-
headers: expect.objectContaining({
|
|
104
|
-
'x-inf-target-url': 'https://api.inference.sh/tasks/1',
|
|
105
|
-
}),
|
|
106
|
-
}));
|
|
107
|
-
});
|
|
108
|
-
it('should serialize array query params as JSON', async () => {
|
|
109
|
-
mockJsonResponse([]);
|
|
110
|
-
await client().request('get', '/tasks', {
|
|
111
|
-
params: { ids: ['a', 'b'] },
|
|
112
|
-
});
|
|
113
|
-
const calledUrl = mockFetch.mock.calls[0][0];
|
|
114
|
-
expect(calledUrl).toContain('ids=');
|
|
115
|
-
expect(decodeURIComponent(calledUrl)).toContain('["a","b"]');
|
|
116
|
-
});
|
|
117
|
-
it('should serialize object query params as JSON', async () => {
|
|
118
|
-
mockJsonResponse([]);
|
|
119
|
-
await client().request('get', '/tasks', {
|
|
120
|
-
params: { filter: { status: 'active', team_id: 'team-1' } },
|
|
121
|
-
});
|
|
122
|
-
const calledUrl = mockFetch.mock.calls[0][0];
|
|
123
|
-
expect(calledUrl).toContain('filter=');
|
|
124
|
-
expect(decodeURIComponent(calledUrl)).toContain('"status":"active"');
|
|
125
|
-
expect(decodeURIComponent(calledUrl)).toContain('"team_id":"team-1"');
|
|
126
|
-
});
|
|
127
|
-
it('should omit Authorization when getToken returns null', async () => {
|
|
128
|
-
mockJsonResponse({ id: 'task-1' });
|
|
129
|
-
const tokenClient = new HttpClient({ getToken: () => null });
|
|
130
|
-
await tokenClient.request('get', '/tasks/task-1');
|
|
131
|
-
const [, init] = mockFetch.mock.calls[0];
|
|
132
|
-
const headers = init.headers;
|
|
133
|
-
expect(headers.Authorization).toBeUndefined();
|
|
134
|
-
});
|
|
135
|
-
it('should omit Authorization when getToken returns undefined', async () => {
|
|
136
|
-
mockJsonResponse({ id: 'task-1' });
|
|
137
|
-
const tokenClient = new HttpClient({ getToken: () => undefined });
|
|
138
|
-
await tokenClient.request('get', '/tasks/task-1');
|
|
139
|
-
const [, init] = mockFetch.mock.calls[0];
|
|
140
|
-
const headers = init.headers;
|
|
141
|
-
expect(headers.Authorization).toBeUndefined();
|
|
142
|
-
});
|
|
143
|
-
it('should use top-level message field in HTTP error responses', async () => {
|
|
144
|
-
mockFetch.mockResolvedValueOnce({
|
|
145
|
-
ok: false,
|
|
146
|
-
status: 503,
|
|
147
|
-
text: () => Promise.resolve(JSON.stringify({ message: 'service unavailable' })),
|
|
148
|
-
});
|
|
149
|
-
await expect(client().request('get', '/tasks/1')).rejects.toMatchObject({
|
|
150
|
-
name: 'InferenceError',
|
|
151
|
-
message: expect.stringContaining('service unavailable'),
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
it('should use getToken for Authorization on regular requests', async () => {
|
|
155
|
-
mockJsonResponse({ id: 'task-1' });
|
|
156
|
-
const tokenClient = new HttpClient({ getToken: () => 'dynamic-key' });
|
|
157
|
-
await tokenClient.request('get', '/tasks/task-1');
|
|
158
|
-
const [, init] = mockFetch.mock.calls[0];
|
|
159
|
-
const headers = init.headers;
|
|
160
|
-
expect(headers.Authorization).toBe('Bearer dynamic-key');
|
|
161
|
-
});
|
|
162
|
-
it('should resolve function-valued headers on each request', async () => {
|
|
163
|
-
mockJsonResponse({ id: 'task-1' });
|
|
164
|
-
mockJsonResponse({ id: 'task-2' });
|
|
165
|
-
let teamId = 'team-a';
|
|
166
|
-
const client = new HttpClient({
|
|
167
|
-
apiKey: 'test-key',
|
|
168
|
-
headers: {
|
|
169
|
-
'X-Team-ID': () => teamId,
|
|
170
|
-
'X-Request-Id': () => 'req-1',
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
await client.request('get', '/tasks/task-1');
|
|
174
|
-
teamId = 'team-b';
|
|
175
|
-
await client.request('get', '/tasks/task-2');
|
|
176
|
-
const firstHeaders = mockFetch.mock.calls[0][1]?.headers;
|
|
177
|
-
const secondHeaders = mockFetch.mock.calls[1][1]?.headers;
|
|
178
|
-
expect(firstHeaders['X-Team-ID']).toBe('team-a');
|
|
179
|
-
expect(secondHeaders['X-Team-ID']).toBe('team-b');
|
|
180
|
-
expect(firstHeaders['X-Request-Id']).toBe('req-1');
|
|
181
|
-
});
|
|
182
|
-
it('should omit headers when a resolver returns undefined', async () => {
|
|
183
|
-
mockJsonResponse({ id: 'task-1' });
|
|
184
|
-
const client = new HttpClient({
|
|
185
|
-
apiKey: 'test-key',
|
|
186
|
-
headers: {
|
|
187
|
-
'X-Team-ID': () => undefined,
|
|
188
|
-
'X-Custom': 'static',
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
await client.request('get', '/tasks/task-1');
|
|
192
|
-
const headers = mockFetch.mock.calls[0][1]?.headers;
|
|
193
|
-
expect(headers['X-Team-ID']).toBeUndefined();
|
|
194
|
-
expect(headers['X-Custom']).toBe('static');
|
|
195
|
-
});
|
|
196
|
-
it('should send X-API-Version 2 and X-Client-Source on every request', async () => {
|
|
197
|
-
mockJsonResponse({ id: 'task-1' });
|
|
198
|
-
await client().request('get', '/tasks/task-1');
|
|
199
|
-
const [, init] = mockFetch.mock.calls[0];
|
|
200
|
-
const headers = init.headers;
|
|
201
|
-
expect(headers['X-API-Version']).toBe('2');
|
|
202
|
-
expect(headers['X-Client-Source']).toMatch(/inference-sdk-js\//);
|
|
203
|
-
});
|
|
204
|
-
it('should prefer RFC 9457 detail over title in error responses', async () => {
|
|
205
|
-
mockFetch.mockResolvedValueOnce({
|
|
206
|
-
ok: false,
|
|
207
|
-
status: 422,
|
|
208
|
-
text: () => Promise.resolve(JSON.stringify({
|
|
209
|
-
type: 'about:blank',
|
|
210
|
-
title: 'Validation failed',
|
|
211
|
-
detail: 'app field is required',
|
|
212
|
-
})),
|
|
213
|
-
});
|
|
214
|
-
await expect(client().request('post', '/apps')).rejects.toMatchObject({
|
|
215
|
-
name: 'InferenceError',
|
|
216
|
-
message: expect.stringContaining('app field is required'),
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
it('should fall back to RFC 9457 title when detail is absent', async () => {
|
|
220
|
-
mockFetch.mockResolvedValueOnce({
|
|
221
|
-
ok: false,
|
|
222
|
-
status: 403,
|
|
223
|
-
text: () => Promise.resolve(JSON.stringify({ type: 'about:blank', title: 'Forbidden' })),
|
|
224
|
-
});
|
|
225
|
-
await expect(client().request('get', '/tasks/1')).rejects.toMatchObject({
|
|
226
|
-
name: 'InferenceError',
|
|
227
|
-
message: expect.stringContaining('Forbidden'),
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
it('should use raw response text when error body is not JSON', async () => {
|
|
231
|
-
mockFetch.mockResolvedValueOnce({
|
|
232
|
-
ok: false,
|
|
233
|
-
status: 502,
|
|
234
|
-
text: () => Promise.resolve('Bad Gateway from upstream'),
|
|
235
|
-
});
|
|
236
|
-
await expect(client().request('get', '/tasks/1')).rejects.toMatchObject({
|
|
237
|
-
name: 'InferenceError',
|
|
238
|
-
message: expect.stringContaining('Bad Gateway from upstream'),
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
it('should not unwrap legacy v1 APIResponse envelopes', async () => {
|
|
242
|
-
const v1Envelope = { success: true, data: { id: 'task-legacy' } };
|
|
243
|
-
mockJsonResponse(v1Envelope);
|
|
244
|
-
const result = await client().request('get', '/tasks/legacy');
|
|
245
|
-
expect(result).toEqual(v1Envelope);
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
describe('getStreamableConfig', () => {
|
|
249
|
-
it('should include bearer token in direct mode', () => {
|
|
250
|
-
const config = new HttpClient({ apiKey: 'secret-key' }).getStreamableConfig('/tasks/task-1/stream');
|
|
251
|
-
expect(config.url).toBe('https://api.inference.sh/tasks/task-1/stream');
|
|
252
|
-
expect(config.headers.Authorization).toBe('Bearer secret-key');
|
|
253
|
-
});
|
|
254
|
-
it('should route through proxy with target URL header', () => {
|
|
255
|
-
const config = new HttpClient({
|
|
256
|
-
proxyUrl: 'https://proxy.example.com/api',
|
|
257
|
-
}).getStreamableConfig('/tasks/task-1/stream');
|
|
258
|
-
expect(config.url).toContain('https://proxy.example.com/api');
|
|
259
|
-
expect(config.url).toContain('__inf_target=');
|
|
260
|
-
expect(config.headers['x-inf-target-url']).toBe('https://api.inference.sh/tasks/task-1/stream');
|
|
261
|
-
});
|
|
262
|
-
it('should use getToken when apiKey is not set', () => {
|
|
263
|
-
const config = new HttpClient({
|
|
264
|
-
getToken: () => 'dynamic-token',
|
|
265
|
-
}).getStreamableConfig('/tasks/task-1/stream');
|
|
266
|
-
expect(config.headers.Authorization).toBe('Bearer dynamic-token');
|
|
267
|
-
});
|
|
268
|
-
it('should include resolved dynamic headers for streaming requests', () => {
|
|
269
|
-
const config = new HttpClient({
|
|
270
|
-
apiKey: 'secret-key',
|
|
271
|
-
headers: { 'X-Team-ID': () => 'team-stream' },
|
|
272
|
-
}).getStreamableConfig('/tasks/task-1/stream');
|
|
273
|
-
expect(config.headers['X-Team-ID']).toBe('team-stream');
|
|
274
|
-
expect(config.headers.Authorization).toBe('Bearer secret-key');
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
describe('createEventSource', () => {
|
|
278
|
-
beforeEach(() => {
|
|
279
|
-
MockEventSource.mockReset();
|
|
280
|
-
});
|
|
281
|
-
it('should attach Bearer token in direct mode via custom fetch', async () => {
|
|
282
|
-
let capturedFetch;
|
|
283
|
-
MockEventSource.mockImplementation((_url, options) => {
|
|
284
|
-
capturedFetch = options?.fetch;
|
|
285
|
-
return { close: jest.fn(), onmessage: null, onerror: null };
|
|
286
|
-
});
|
|
287
|
-
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
288
|
-
const client = new HttpClient({ apiKey: 'sse-key' });
|
|
289
|
-
await client.createEventSource('/tasks/task-1/stream');
|
|
290
|
-
expect(capturedFetch).toBeDefined();
|
|
291
|
-
await capturedFetch('https://api.inference.sh/tasks/task-1/stream', { headers: {} });
|
|
292
|
-
expect(mockFetch).toHaveBeenCalledWith('https://api.inference.sh/tasks/task-1/stream', expect.objectContaining({
|
|
293
|
-
headers: expect.objectContaining({
|
|
294
|
-
Authorization: 'Bearer sse-key',
|
|
295
|
-
}),
|
|
296
|
-
}));
|
|
297
|
-
});
|
|
298
|
-
it('should route through proxy with target URL header on custom fetch', async () => {
|
|
299
|
-
let capturedFetch;
|
|
300
|
-
MockEventSource.mockImplementation((url, options) => {
|
|
301
|
-
capturedFetch = options?.fetch;
|
|
302
|
-
expect(url).toContain('https://proxy.example.com');
|
|
303
|
-
expect(url).toContain('__inf_target=');
|
|
304
|
-
return { close: jest.fn(), onmessage: null, onerror: null };
|
|
305
|
-
});
|
|
306
|
-
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
307
|
-
const client = new HttpClient({ proxyUrl: 'https://proxy.example.com' });
|
|
308
|
-
await client.createEventSource('/tasks/task-1/stream');
|
|
309
|
-
await capturedFetch('https://proxy.example.com', { headers: {} });
|
|
310
|
-
expect(mockFetch).toHaveBeenCalledWith('https://proxy.example.com', expect.objectContaining({
|
|
311
|
-
headers: expect.objectContaining({
|
|
312
|
-
'x-inf-target-url': 'https://api.inference.sh/tasks/task-1/stream',
|
|
313
|
-
}),
|
|
314
|
-
}));
|
|
315
|
-
});
|
|
316
|
-
it('should attach resolved dynamic headers on SSE custom fetch', async () => {
|
|
317
|
-
let capturedFetch;
|
|
318
|
-
MockEventSource.mockImplementation((_url, options) => {
|
|
319
|
-
capturedFetch = options?.fetch;
|
|
320
|
-
return { close: jest.fn(), onmessage: null, onerror: null };
|
|
321
|
-
});
|
|
322
|
-
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
323
|
-
const client = new HttpClient({
|
|
324
|
-
apiKey: 'sse-key',
|
|
325
|
-
headers: { 'X-Team-ID': () => 'team-sse' },
|
|
326
|
-
});
|
|
327
|
-
await client.createEventSource('/tasks/task-1/stream');
|
|
328
|
-
await capturedFetch('https://api.inference.sh/tasks/task-1/stream', { headers: {} });
|
|
329
|
-
expect(mockFetch).toHaveBeenCalledWith('https://api.inference.sh/tasks/task-1/stream', expect.objectContaining({
|
|
330
|
-
headers: expect.objectContaining({
|
|
331
|
-
'X-Team-ID': 'team-sse',
|
|
332
|
-
Authorization: 'Bearer sse-key',
|
|
333
|
-
}),
|
|
334
|
-
}));
|
|
335
|
-
});
|
|
336
|
-
it('should use getToken for Authorization on SSE custom fetch', async () => {
|
|
337
|
-
let capturedFetch;
|
|
338
|
-
MockEventSource.mockImplementation((_url, options) => {
|
|
339
|
-
capturedFetch = options?.fetch;
|
|
340
|
-
return { close: jest.fn(), onmessage: null, onerror: null };
|
|
341
|
-
});
|
|
342
|
-
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
343
|
-
const client = new HttpClient({ getToken: () => 'session-token' });
|
|
344
|
-
await client.createEventSource('/tasks/task-1/stream');
|
|
345
|
-
await capturedFetch('https://api.inference.sh/tasks/task-1/stream', { headers: {} });
|
|
346
|
-
expect(mockFetch).toHaveBeenCalledWith('https://api.inference.sh/tasks/task-1/stream', expect.objectContaining({
|
|
347
|
-
headers: expect.objectContaining({
|
|
348
|
-
Authorization: 'Bearer session-token',
|
|
349
|
-
}),
|
|
350
|
-
}));
|
|
351
|
-
});
|
|
352
|
-
it('should omit Authorization on SSE fetch when getToken returns null', async () => {
|
|
353
|
-
let capturedFetch;
|
|
354
|
-
MockEventSource.mockImplementation((_url, options) => {
|
|
355
|
-
capturedFetch = options?.fetch;
|
|
356
|
-
return { close: jest.fn(), onmessage: null, onerror: null };
|
|
357
|
-
});
|
|
358
|
-
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
359
|
-
const client = new HttpClient({ getToken: () => null });
|
|
360
|
-
await client.createEventSource('/tasks/task-1/stream');
|
|
361
|
-
await capturedFetch('https://api.inference.sh/tasks/task-1/stream', { headers: {} });
|
|
362
|
-
const [, init] = mockFetch.mock.calls[0];
|
|
363
|
-
const headers = init.headers;
|
|
364
|
-
expect(headers.Authorization).toBeUndefined();
|
|
365
|
-
});
|
|
366
|
-
it('should return EventSource without awaiting the initial fetch', async () => {
|
|
367
|
-
MockEventSource.mockImplementation(() => ({ close: jest.fn() }));
|
|
368
|
-
const client = new HttpClient({ apiKey: 'sse-key' });
|
|
369
|
-
const eventSource = await client.createEventSource('/tasks/task-1/stream');
|
|
370
|
-
expect(eventSource).toBeDefined();
|
|
371
|
-
expect(MockEventSource).toHaveBeenCalled();
|
|
372
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
373
|
-
});
|
|
374
|
-
function mockFailedResponse(status, body) {
|
|
375
|
-
return {
|
|
376
|
-
ok: false,
|
|
377
|
-
status,
|
|
378
|
-
clone: () => ({
|
|
379
|
-
text: () => Promise.resolve(body),
|
|
380
|
-
}),
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
it('should route failed initial SSE response through onError and return retried response', async () => {
|
|
384
|
-
let capturedFetch;
|
|
385
|
-
MockEventSource.mockImplementation((_url, options) => {
|
|
386
|
-
capturedFetch = options?.fetch;
|
|
387
|
-
return { close: jest.fn(), onmessage: null, onerror: null };
|
|
388
|
-
});
|
|
389
|
-
const onError = jest.fn(async (_error, retry) => retry());
|
|
390
|
-
const client = new HttpClient({ apiKey: 'sse-key', onError });
|
|
391
|
-
mockFetch
|
|
392
|
-
.mockResolvedValueOnce(mockFailedResponse(403, JSON.stringify({ detail: 'otp_required' })))
|
|
393
|
-
.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
394
|
-
await client.createEventSource('/tasks/task-1/stream');
|
|
395
|
-
const response = await capturedFetch('https://api.inference.sh/tasks/task-1/stream', {});
|
|
396
|
-
expect(onError).toHaveBeenCalledTimes(1);
|
|
397
|
-
const [error] = onError.mock.calls[0];
|
|
398
|
-
expect(error).toBeInstanceOf(InferenceError);
|
|
399
|
-
expect(error.statusCode).toBe(403);
|
|
400
|
-
expect(error.message).toContain('otp_required');
|
|
401
|
-
expect(response).toEqual({ ok: true, status: 200 });
|
|
402
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
403
|
-
});
|
|
404
|
-
it('should return original failed response when onError does not retry', async () => {
|
|
405
|
-
let capturedFetch;
|
|
406
|
-
MockEventSource.mockImplementation((_url, options) => {
|
|
407
|
-
capturedFetch = options?.fetch;
|
|
408
|
-
return { close: jest.fn(), onmessage: null, onerror: null };
|
|
409
|
-
});
|
|
410
|
-
const failed = mockFailedResponse(401, JSON.stringify({ title: 'Unauthorized' }));
|
|
411
|
-
const onError = jest.fn(async () => undefined);
|
|
412
|
-
const client = new HttpClient({ apiKey: 'sse-key', onError });
|
|
413
|
-
mockFetch.mockResolvedValueOnce(failed);
|
|
414
|
-
await client.createEventSource('/tasks/task-1/stream');
|
|
415
|
-
const response = await capturedFetch('https://api.inference.sh/tasks/task-1/stream', {});
|
|
416
|
-
expect(onError).toHaveBeenCalledTimes(1);
|
|
417
|
-
expect(response).toBe(failed);
|
|
418
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
419
|
-
});
|
|
420
|
-
it('should return failed response when no onError handler is configured', async () => {
|
|
421
|
-
let capturedFetch;
|
|
422
|
-
MockEventSource.mockImplementation((_url, options) => {
|
|
423
|
-
capturedFetch = options?.fetch;
|
|
424
|
-
return { close: jest.fn(), onmessage: null, onerror: null };
|
|
425
|
-
});
|
|
426
|
-
const failed = mockFailedResponse(503, 'service unavailable');
|
|
427
|
-
const client = new HttpClient({ apiKey: 'sse-key' });
|
|
428
|
-
mockFetch.mockResolvedValueOnce(failed);
|
|
429
|
-
await client.createEventSource('/tasks/task-1/stream');
|
|
430
|
-
const response = await capturedFetch('https://api.inference.sh/tasks/task-1/stream', {});
|
|
431
|
-
expect(response).toBe(failed);
|
|
432
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/http/errors.test.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { InferenceError, RequirementsNotMetException, SessionEndedError, SessionExpiredError, SessionNotFoundError, WorkerLostError, isInferenceError, isRequirementsNotMetException, isSessionError, } from './errors';
|
|
2
|
-
import { RequirementTypeIntegration, RequirementTypeScope, RequirementTypeSecret, } from '../types';
|
|
3
|
-
describe('error type guards', () => {
|
|
4
|
-
it('isRequirementsNotMetException should match instances and plain objects', () => {
|
|
5
|
-
const instance = new RequirementsNotMetException([
|
|
6
|
-
{ type: 'secret', key: 'K', message: 'missing' },
|
|
7
|
-
]);
|
|
8
|
-
expect(isRequirementsNotMetException(instance)).toBe(true);
|
|
9
|
-
const plain = {
|
|
10
|
-
name: 'RequirementsNotMetException',
|
|
11
|
-
statusCode: 412,
|
|
12
|
-
errors: [{ type: 'secret', key: 'K', message: 'missing' }],
|
|
13
|
-
};
|
|
14
|
-
expect(isRequirementsNotMetException(plain)).toBe(true);
|
|
15
|
-
expect(isRequirementsNotMetException(new Error('other'))).toBe(false);
|
|
16
|
-
});
|
|
17
|
-
it('isInferenceError should match instances and plain objects', () => {
|
|
18
|
-
const instance = new InferenceError(500, 'server error');
|
|
19
|
-
expect(isInferenceError(instance)).toBe(true);
|
|
20
|
-
expect(isInferenceError({ name: 'InferenceError', statusCode: 404 })).toBe(true);
|
|
21
|
-
expect(isInferenceError({ name: 'InferenceError' })).toBe(false);
|
|
22
|
-
});
|
|
23
|
-
it('isSessionError should match session error subclasses and plain objects', () => {
|
|
24
|
-
const instance = new SessionNotFoundError('sess-1');
|
|
25
|
-
expect(isSessionError(instance)).toBe(true);
|
|
26
|
-
expect(isSessionError({
|
|
27
|
-
name: 'SessionNotFoundError',
|
|
28
|
-
sessionId: 'sess-1',
|
|
29
|
-
statusCode: 404,
|
|
30
|
-
})).toBe(true);
|
|
31
|
-
expect(isSessionError({ name: 'InferenceError', statusCode: 500 })).toBe(false);
|
|
32
|
-
});
|
|
33
|
-
it('session error subclasses should expose sessionId', () => {
|
|
34
|
-
expect(new SessionNotFoundError('sess-1').sessionId).toBe('sess-1');
|
|
35
|
-
expect(new SessionExpiredError('sess-2').statusCode).toBe(410);
|
|
36
|
-
expect(new SessionEndedError('sess-3').name).toBe('SessionEndedError');
|
|
37
|
-
expect(new WorkerLostError('sess-4').statusCode).toBe(500);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
describe('error classes', () => {
|
|
41
|
-
it('RequirementsNotMetException.fromResponse should default empty errors', () => {
|
|
42
|
-
const err = RequirementsNotMetException.fromResponse({}, 412);
|
|
43
|
-
expect(err.errors).toEqual([]);
|
|
44
|
-
expect(err.statusCode).toBe(412);
|
|
45
|
-
expect(err.message).toBe('requirements not met');
|
|
46
|
-
});
|
|
47
|
-
it('RequirementsNotMetException should preserve typed RequirementType values', () => {
|
|
48
|
-
const errors = [
|
|
49
|
-
{ type: RequirementTypeSecret, key: 'API_KEY', message: 'Missing secret' },
|
|
50
|
-
{ type: RequirementTypeIntegration, key: 'google', message: 'Not connected' },
|
|
51
|
-
{ type: RequirementTypeScope, key: 'calendar.readonly', message: 'Missing scope' },
|
|
52
|
-
];
|
|
53
|
-
const err = new RequirementsNotMetException(errors);
|
|
54
|
-
expect(err.errors[0].type).toBe('secret');
|
|
55
|
-
expect(err.errors[1].type).toBe('integration');
|
|
56
|
-
expect(err.errors[2].type).toBe('scope');
|
|
57
|
-
expect(isRequirementsNotMetException(err)).toBe(true);
|
|
58
|
-
});
|
|
59
|
-
it('session error subclasses should expose sessionId and statusCode', () => {
|
|
60
|
-
expect(new SessionNotFoundError('sess-a').sessionId).toBe('sess-a');
|
|
61
|
-
expect(new SessionNotFoundError('sess-a').statusCode).toBe(404);
|
|
62
|
-
expect(new SessionExpiredError('sess-b').statusCode).toBe(410);
|
|
63
|
-
expect(new SessionEndedError('sess-c').message).toContain('sess-c');
|
|
64
|
-
expect(new WorkerLostError('sess-d').name).toBe('WorkerLostError');
|
|
65
|
-
});
|
|
66
|
-
});
|
package/dist/http/poll.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/http/poll.test.js
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { PollManager } from './poll';
|
|
2
|
-
describe('PollManager', () => {
|
|
3
|
-
beforeEach(() => {
|
|
4
|
-
jest.useFakeTimers();
|
|
5
|
-
});
|
|
6
|
-
afterEach(() => {
|
|
7
|
-
jest.useRealTimers();
|
|
8
|
-
});
|
|
9
|
-
it('should invoke onData after the immediate first poll', async () => {
|
|
10
|
-
const onData = jest.fn();
|
|
11
|
-
const pollFunction = jest.fn().mockResolvedValue({ status: 'running' });
|
|
12
|
-
const manager = new PollManager({
|
|
13
|
-
pollFunction,
|
|
14
|
-
intervalMs: 1000,
|
|
15
|
-
onData,
|
|
16
|
-
});
|
|
17
|
-
manager.start();
|
|
18
|
-
await Promise.resolve();
|
|
19
|
-
expect(pollFunction).toHaveBeenCalledTimes(1);
|
|
20
|
-
expect(onData).toHaveBeenCalledWith({ status: 'running' });
|
|
21
|
-
manager.stop();
|
|
22
|
-
});
|
|
23
|
-
it('should poll repeatedly at intervalMs', async () => {
|
|
24
|
-
const pollFunction = jest.fn().mockResolvedValue({ status: 'running' });
|
|
25
|
-
const manager = new PollManager({
|
|
26
|
-
pollFunction,
|
|
27
|
-
intervalMs: 1000,
|
|
28
|
-
});
|
|
29
|
-
manager.start();
|
|
30
|
-
await Promise.resolve();
|
|
31
|
-
expect(pollFunction).toHaveBeenCalledTimes(1);
|
|
32
|
-
jest.advanceTimersByTime(1000);
|
|
33
|
-
await Promise.resolve();
|
|
34
|
-
expect(pollFunction).toHaveBeenCalledTimes(2);
|
|
35
|
-
manager.stop();
|
|
36
|
-
});
|
|
37
|
-
it('should call onStart when started and onStop when stopped', async () => {
|
|
38
|
-
const onStart = jest.fn();
|
|
39
|
-
const onStop = jest.fn();
|
|
40
|
-
const pollFunction = jest.fn().mockResolvedValue({});
|
|
41
|
-
const manager = new PollManager({ pollFunction, onStart, onStop });
|
|
42
|
-
manager.start();
|
|
43
|
-
expect(onStart).toHaveBeenCalled();
|
|
44
|
-
manager.stop();
|
|
45
|
-
expect(onStop).toHaveBeenCalled();
|
|
46
|
-
});
|
|
47
|
-
it('should stop after maxRetries consecutive poll errors', async () => {
|
|
48
|
-
const onError = jest.fn();
|
|
49
|
-
const onStop = jest.fn();
|
|
50
|
-
const pollFunction = jest.fn().mockRejectedValue(new Error('network error'));
|
|
51
|
-
const manager = new PollManager({
|
|
52
|
-
pollFunction,
|
|
53
|
-
intervalMs: 100,
|
|
54
|
-
maxRetries: 3,
|
|
55
|
-
onError,
|
|
56
|
-
onStop,
|
|
57
|
-
});
|
|
58
|
-
manager.start();
|
|
59
|
-
// Immediate first poll
|
|
60
|
-
await Promise.resolve();
|
|
61
|
-
jest.advanceTimersByTime(100);
|
|
62
|
-
await Promise.resolve();
|
|
63
|
-
jest.advanceTimersByTime(100);
|
|
64
|
-
await Promise.resolve();
|
|
65
|
-
expect(onError).toHaveBeenCalledTimes(3);
|
|
66
|
-
expect(onStop).toHaveBeenCalled();
|
|
67
|
-
expect(pollFunction.mock.calls.length).toBeGreaterThanOrEqual(3);
|
|
68
|
-
});
|
|
69
|
-
it('should not invoke onData after stop', async () => {
|
|
70
|
-
const onData = jest.fn();
|
|
71
|
-
const pollFunction = jest.fn().mockResolvedValue({ status: 'running' });
|
|
72
|
-
const manager = new PollManager({
|
|
73
|
-
pollFunction,
|
|
74
|
-
intervalMs: 100,
|
|
75
|
-
onData,
|
|
76
|
-
});
|
|
77
|
-
manager.start();
|
|
78
|
-
await Promise.resolve();
|
|
79
|
-
manager.stop();
|
|
80
|
-
const callsBefore = onData.mock.calls.length;
|
|
81
|
-
jest.advanceTimersByTime(500);
|
|
82
|
-
await Promise.resolve();
|
|
83
|
-
expect(onData.mock.calls.length).toBe(callsBefore);
|
|
84
|
-
});
|
|
85
|
-
it('should not run overlapping polls when pollFunction is still in flight', async () => {
|
|
86
|
-
let resolvePoll = () => { };
|
|
87
|
-
const pollFunction = jest.fn().mockImplementation(() => new Promise((resolve) => {
|
|
88
|
-
resolvePoll = resolve;
|
|
89
|
-
}));
|
|
90
|
-
const manager = new PollManager({
|
|
91
|
-
pollFunction,
|
|
92
|
-
intervalMs: 100,
|
|
93
|
-
});
|
|
94
|
-
manager.start();
|
|
95
|
-
await Promise.resolve();
|
|
96
|
-
expect(pollFunction).toHaveBeenCalledTimes(1);
|
|
97
|
-
jest.advanceTimersByTime(100);
|
|
98
|
-
await Promise.resolve();
|
|
99
|
-
expect(pollFunction).toHaveBeenCalledTimes(1);
|
|
100
|
-
resolvePoll({ status: 'ok' });
|
|
101
|
-
await Promise.resolve();
|
|
102
|
-
jest.advanceTimersByTime(100);
|
|
103
|
-
await Promise.resolve();
|
|
104
|
-
expect(pollFunction).toHaveBeenCalledTimes(2);
|
|
105
|
-
manager.stop();
|
|
106
|
-
});
|
|
107
|
-
it('should ignore start() when polling is already active', async () => {
|
|
108
|
-
const onStart = jest.fn();
|
|
109
|
-
const pollFunction = jest.fn().mockResolvedValue({});
|
|
110
|
-
const manager = new PollManager({ pollFunction, onStart });
|
|
111
|
-
manager.start();
|
|
112
|
-
manager.start();
|
|
113
|
-
expect(onStart).toHaveBeenCalledTimes(1);
|
|
114
|
-
manager.stop();
|
|
115
|
-
});
|
|
116
|
-
it('should reset consecutive error count after a successful poll', async () => {
|
|
117
|
-
const onError = jest.fn();
|
|
118
|
-
const onStop = jest.fn();
|
|
119
|
-
const pollFunction = jest
|
|
120
|
-
.fn()
|
|
121
|
-
.mockRejectedValueOnce(new Error('fail 1'))
|
|
122
|
-
.mockResolvedValueOnce({ status: 'ok' })
|
|
123
|
-
.mockRejectedValueOnce(new Error('fail 2'))
|
|
124
|
-
.mockRejectedValueOnce(new Error('fail 3'));
|
|
125
|
-
const manager = new PollManager({
|
|
126
|
-
pollFunction,
|
|
127
|
-
intervalMs: 100,
|
|
128
|
-
maxRetries: 2,
|
|
129
|
-
onError,
|
|
130
|
-
onStop,
|
|
131
|
-
});
|
|
132
|
-
manager.start();
|
|
133
|
-
await Promise.resolve();
|
|
134
|
-
jest.advanceTimersByTime(100);
|
|
135
|
-
await Promise.resolve();
|
|
136
|
-
expect(onStop).not.toHaveBeenCalled();
|
|
137
|
-
expect(onError).toHaveBeenCalledTimes(1);
|
|
138
|
-
jest.advanceTimersByTime(100);
|
|
139
|
-
await Promise.resolve();
|
|
140
|
-
jest.advanceTimersByTime(100);
|
|
141
|
-
await Promise.resolve();
|
|
142
|
-
expect(onStop).toHaveBeenCalled();
|
|
143
|
-
expect(onError).toHaveBeenCalledTimes(3);
|
|
144
|
-
manager.stop();
|
|
145
|
-
});
|
|
146
|
-
});
|