@inferencesh/sdk 0.6.12 → 0.6.13

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.
@@ -50,6 +50,55 @@ describe('KnowledgeAPI', () => {
50
50
  expect(url).toContain('/knowledge/know-1/transfer');
51
51
  expect(JSON.parse(init.body)).toEqual({ team_id: 'team-5' });
52
52
  });
53
+ it('should GET /knowledge/{id} for get()', async () => {
54
+ const entry = { id: 'know-1', name: 'docs' };
55
+ mockJsonResponse(entry);
56
+ const result = await api().get('know-1');
57
+ expect(result).toEqual(entry);
58
+ const [url, init] = mockFetch.mock.calls[0];
59
+ expect(url).toContain('/knowledge/know-1');
60
+ expect(init.method).toBe('GET');
61
+ });
62
+ it('should POST /knowledge/{id} for update()', async () => {
63
+ const entry = { id: 'know-1', name: 'updated' };
64
+ mockJsonResponse(entry);
65
+ const result = await api().update('know-1', { name: 'updated' });
66
+ expect(result).toEqual(entry);
67
+ const [, init] = mockFetch.mock.calls[0];
68
+ expect(JSON.parse(init.body)).toEqual({ name: 'updated' });
69
+ });
70
+ it('should DELETE /knowledge/{id} for delete()', async () => {
71
+ mockJsonResponse(null);
72
+ await api().delete('know-1');
73
+ const [url, init] = mockFetch.mock.calls[0];
74
+ expect(url).toContain('/knowledge/know-1');
75
+ expect(init.method).toBe('DELETE');
76
+ });
77
+ it('should POST /knowledge/{id}/versions/list for listVersions()', async () => {
78
+ const page = { items: [{ id: 'ver-1' }], next_cursor: null };
79
+ mockJsonResponse(page);
80
+ const result = await api().listVersions('know-1', { limit: 10 });
81
+ expect(result).toEqual(page);
82
+ const [url, init] = mockFetch.mock.calls[0];
83
+ expect(url).toContain('/knowledge/know-1/versions/list');
84
+ expect(JSON.parse(init.body)).toEqual({ limit: 10 });
85
+ });
86
+ it('should GET /knowledge/{id}/versions/{versionId} for getVersion()', async () => {
87
+ const version = { id: 'ver-1', knowledge_id: 'know-1' };
88
+ mockJsonResponse(version);
89
+ const result = await api().getVersion('know-1', 'ver-1');
90
+ expect(result).toEqual(version);
91
+ const [url, init] = mockFetch.mock.calls[0];
92
+ expect(url).toContain('/knowledge/know-1/versions/ver-1');
93
+ expect(init.method).toBe('GET');
94
+ });
95
+ it('should POST visibility for updateVisibility()', async () => {
96
+ const entry = { id: 'know-1', visibility: 'team' };
97
+ mockJsonResponse(entry);
98
+ await api().updateVisibility('know-1', 'team');
99
+ const [, init] = mockFetch.mock.calls[0];
100
+ expect(JSON.parse(init.body)).toEqual({ visibility: 'team' });
101
+ });
53
102
  });
54
103
  describe('SkillsAPI', () => {
55
104
  beforeEach(() => {
@@ -91,4 +140,99 @@ describe('SkillsAPI', () => {
91
140
  expect(url).toContain('/store/skills/list');
92
141
  expect(init.method).toBe('POST');
93
142
  });
143
+ it('should GET /skills/resolve with ref only when skill is omitted', async () => {
144
+ const resolved = { ref: 'acme/skill@v1' };
145
+ mockJsonResponse(resolved);
146
+ const result = await api().resolve('acme/skill@v1');
147
+ expect(result).toEqual(resolved);
148
+ const [url] = mockFetch.mock.calls[0];
149
+ expect(url).toContain('/skills/resolve');
150
+ expect(url).toContain('ref=acme');
151
+ expect(url).not.toContain('skill=');
152
+ });
153
+ it('should GET /skills/{namespace}/{name}/content for getContent()', async () => {
154
+ mockJsonResponse({ body: '# Skill instructions' });
155
+ const result = await api().getContent('acme', 'research');
156
+ expect(result).toEqual({ body: '# Skill instructions' });
157
+ const [url, init] = mockFetch.mock.calls[0];
158
+ expect(url).toContain('/skills/acme/research/content');
159
+ expect(init.method).toBe('GET');
160
+ });
161
+ it('should POST team_id for transferOwnership()', async () => {
162
+ const skill = { id: 'skill-1' };
163
+ mockJsonResponse(skill);
164
+ await api().transferOwnership('skill-1', 'team-8');
165
+ const [url, init] = mockFetch.mock.calls[0];
166
+ expect(url).toContain('/skills/skill-1/transfer');
167
+ expect(JSON.parse(init.body)).toEqual({ team_id: 'team-8' });
168
+ });
169
+ it('should POST /skills/list for list()', async () => {
170
+ const page = { items: [{ id: 'skill-1' }], next_cursor: null };
171
+ mockJsonResponse(page);
172
+ const result = await api().list({ limit: 10 });
173
+ expect(result).toEqual(page);
174
+ const [url, init] = mockFetch.mock.calls[0];
175
+ expect(url).toContain('/skills/list');
176
+ expect(init.method).toBe('POST');
177
+ });
178
+ it('should GET /skills/{id} for get()', async () => {
179
+ const skill = { id: 'skill-1', name: 'research' };
180
+ mockJsonResponse(skill);
181
+ const result = await api().get('skill-1');
182
+ expect(result).toEqual(skill);
183
+ const [url, init] = mockFetch.mock.calls[0];
184
+ expect(url).toContain('/skills/skill-1');
185
+ expect(init.method).toBe('GET');
186
+ });
187
+ it('should POST /skills for create()', async () => {
188
+ const payload = { namespace: 'acme', name: 'research' };
189
+ const skill = { id: 'skill-new', ...payload };
190
+ mockJsonResponse(skill);
191
+ const result = await api().create(payload);
192
+ expect(result).toEqual(skill);
193
+ const [url, init] = mockFetch.mock.calls[0];
194
+ expect(url).toContain('/skills');
195
+ expect(init.method).toBe('POST');
196
+ expect(JSON.parse(init.body)).toEqual(payload);
197
+ });
198
+ it('should POST /skills/{id} for update()', async () => {
199
+ const skill = { id: 'skill-1', name: 'updated' };
200
+ mockJsonResponse(skill);
201
+ const result = await api().update('skill-1', { name: 'updated' });
202
+ expect(result).toEqual(skill);
203
+ const [, init] = mockFetch.mock.calls[0];
204
+ expect(JSON.parse(init.body)).toEqual({ name: 'updated' });
205
+ });
206
+ it('should DELETE /skills/{id} for delete()', async () => {
207
+ mockJsonResponse(null);
208
+ await api().delete('skill-1');
209
+ const [url, init] = mockFetch.mock.calls[0];
210
+ expect(url).toContain('/skills/skill-1');
211
+ expect(init.method).toBe('DELETE');
212
+ });
213
+ it('should POST /skills/{id}/versions/list for listVersions()', async () => {
214
+ const page = { items: [{ id: 'ver-1' }], next_cursor: null };
215
+ mockJsonResponse(page);
216
+ const result = await api().listVersions('skill-1', { limit: 5 });
217
+ expect(result).toEqual(page);
218
+ const [url, init] = mockFetch.mock.calls[0];
219
+ expect(url).toContain('/skills/skill-1/versions/list');
220
+ expect(JSON.parse(init.body)).toEqual({ limit: 5 });
221
+ });
222
+ it('should GET /skills/{id}/versions/{versionId} for getVersion()', async () => {
223
+ const version = { id: 'ver-1', skill_id: 'skill-1' };
224
+ mockJsonResponse(version);
225
+ const result = await api().getVersion('skill-1', 'ver-1');
226
+ expect(result).toEqual(version);
227
+ const [url, init] = mockFetch.mock.calls[0];
228
+ expect(url).toContain('/skills/skill-1/versions/ver-1');
229
+ expect(init.method).toBe('GET');
230
+ });
231
+ it('should POST visibility for updateVisibility()', async () => {
232
+ const skill = { id: 'skill-1', visibility: 'team' };
233
+ mockJsonResponse(skill);
234
+ await api().updateVisibility('skill-1', 'team');
235
+ const [, init] = mockFetch.mock.calls[0];
236
+ expect(JSON.parse(init.body)).toEqual({ visibility: 'team' });
237
+ });
94
238
  });
@@ -61,4 +61,38 @@ describe('MCPServersAPI', () => {
61
61
  expect(url).toContain('/mcp-servers/mcp-1');
62
62
  expect(init.method).toBe('PUT');
63
63
  });
64
+ it('should GET /mcps/{slug} for get()', async () => {
65
+ const server = { slug: 'filesystem', name: 'Filesystem MCP' };
66
+ mockJsonResponse(server);
67
+ const result = await api().get('filesystem');
68
+ expect(result).toEqual(server);
69
+ const [url, init] = mockFetch.mock.calls[0];
70
+ expect(url).toContain('/mcps/filesystem');
71
+ expect(init.method).toBe('GET');
72
+ });
73
+ it('should POST /mcp-servers/list for listOwned()', async () => {
74
+ const page = { items: [{ id: 'mcp-1' }], next_cursor: null };
75
+ mockJsonResponse(page);
76
+ const result = await api().listOwned({ limit: 5 });
77
+ expect(result).toEqual(page);
78
+ const [url, init] = mockFetch.mock.calls[0];
79
+ expect(url).toContain('/mcp-servers/list');
80
+ expect(init.method).toBe('POST');
81
+ });
82
+ it('should GET /mcp-servers/{id} for getOwned()', async () => {
83
+ const server = { id: 'mcp-1', name: 'My MCP' };
84
+ mockJsonResponse(server);
85
+ const result = await api().getOwned('mcp-1');
86
+ expect(result).toEqual(server);
87
+ const [url, init] = mockFetch.mock.calls[0];
88
+ expect(url).toContain('/mcp-servers/mcp-1');
89
+ expect(init.method).toBe('GET');
90
+ });
91
+ it('should DELETE /mcp-servers/{id} for delete()', async () => {
92
+ mockJsonResponse(null);
93
+ await api().delete('mcp-1');
94
+ const [url, init] = mockFetch.mock.calls[0];
95
+ expect(url).toContain('/mcp-servers/mcp-1');
96
+ expect(init.method).toBe('DELETE');
97
+ });
64
98
  });
@@ -49,4 +49,13 @@ describe('ProjectsAPI', () => {
49
49
  expect(url).toContain('/projects/proj-1');
50
50
  expect(init.method).toBe('DELETE');
51
51
  });
52
+ it('should POST /projects/{id} for update()', async () => {
53
+ const project = { id: 'proj-1', name: 'Renamed' };
54
+ mockJsonResponse(project);
55
+ const result = await api().update('proj-1', { name: 'Renamed' });
56
+ expect(result).toEqual(project);
57
+ const [url, init] = mockFetch.mock.calls[0];
58
+ expect(url).toContain('/projects/proj-1');
59
+ expect(JSON.parse(init.body)).toEqual({ name: 'Renamed' });
60
+ });
52
61
  });
@@ -23,6 +23,18 @@ describe('SessionsAPI', () => {
23
23
  expect(url).toContain('/sessions/sess_1');
24
24
  expect(init.method).toBe('GET');
25
25
  });
26
+ it('should GET /sessions and return session list', async () => {
27
+ const sessions = [
28
+ { id: 'sess_1', status: 'active' },
29
+ { id: 'sess_2', status: 'active' },
30
+ ];
31
+ mockJsonResponse(sessions);
32
+ const result = await api().list();
33
+ expect(result).toEqual(sessions);
34
+ const [url, init] = mockFetch.mock.calls[0];
35
+ expect(url).toContain('/sessions');
36
+ expect(init.method).toBe('GET');
37
+ });
26
38
  it('should return an empty array when list() response is null', async () => {
27
39
  mockJsonResponse(null);
28
40
  const result = await api().list();
@@ -188,6 +188,23 @@ describe('TasksAPI.run (streaming mode)', () => {
188
188
  ]);
189
189
  await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true })).rejects.toThrow('task cancelled');
190
190
  });
191
+ it('should reject when the NDJSON stream connection fails', async () => {
192
+ mockFetch.mockImplementation((url) => {
193
+ if (url.includes('/apps/run')) {
194
+ return Promise.resolve({
195
+ ok: true,
196
+ status: 200,
197
+ text: () => Promise.resolve(JSON.stringify(makeTask())),
198
+ });
199
+ }
200
+ return Promise.resolve({
201
+ ok: false,
202
+ status: 503,
203
+ text: () => Promise.resolve('service unavailable'),
204
+ });
205
+ });
206
+ await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true })).rejects.toThrow('HTTP 503');
207
+ });
191
208
  it('should handle partial updates via onPartialUpdate', async () => {
192
209
  setupStreamMocks([
193
210
  `${JSON.stringify({
@@ -201,6 +218,26 @@ describe('TasksAPI.run (streaming mode)', () => {
201
218
  expect(onPartialUpdate).toHaveBeenCalledWith(expect.objectContaining({ id: 'task-1', status: TaskStatusCompleted }), ['status']);
202
219
  });
203
220
  });
221
+ describe('TasksAPI.run (HTTP contract)', () => {
222
+ beforeEach(() => {
223
+ jest.clearAllMocks();
224
+ });
225
+ const api = () => new TasksAPI(new HttpClient({ apiKey: 'test-key' }));
226
+ it('should POST /apps/run with merged params and processedInput', async () => {
227
+ const task = makeTask();
228
+ mockJsonResponse(task);
229
+ const result = await api().run({ app: 'image-gen', version_id: 'ver-2', input: {} }, { prompt: 'sunset', width: 512 }, { wait: false });
230
+ expect(result).toEqual(task);
231
+ const [url, init] = mockFetch.mock.calls[0];
232
+ expect(url).toContain('/apps/run');
233
+ expect(init.method).toBe('POST');
234
+ expect(JSON.parse(init.body)).toEqual({
235
+ app: 'image-gen',
236
+ version_id: 'ver-2',
237
+ input: { prompt: 'sunset', width: 512 },
238
+ });
239
+ });
240
+ });
204
241
  describe('TasksAPI.create', () => {
205
242
  beforeEach(() => {
206
243
  jest.clearAllMocks();
@@ -76,4 +76,64 @@ describe('TeamsAPI', () => {
76
76
  expect(url).toContain('/teams/team-1/invites');
77
77
  expect(JSON.parse(init.body)).toEqual(payload);
78
78
  });
79
+ it('should GET /teams/{id} for get()', async () => {
80
+ const team = { id: 'team-1', name: 'Acme' };
81
+ mockJsonResponse(team);
82
+ const result = await api().get('team-1');
83
+ expect(result).toEqual(team);
84
+ const [url, init] = mockFetch.mock.calls[0];
85
+ expect(url).toContain('/teams/team-1');
86
+ expect(init.method).toBe('GET');
87
+ });
88
+ it('should POST /teams/{id} for update()', async () => {
89
+ const team = { id: 'team-1', name: 'Acme Updated' };
90
+ mockJsonResponse(team);
91
+ const result = await api().update('team-1', { name: 'Acme Updated' });
92
+ expect(result).toEqual(team);
93
+ const [url, init] = mockFetch.mock.calls[0];
94
+ expect(url).toContain('/teams/team-1');
95
+ expect(JSON.parse(init.body)).toEqual({ name: 'Acme Updated' });
96
+ });
97
+ it('should DELETE /teams/{id} for delete()', async () => {
98
+ mockJsonResponse(null);
99
+ await api().delete('team-1');
100
+ const [url, init] = mockFetch.mock.calls[0];
101
+ expect(url).toContain('/teams/team-1');
102
+ expect(init.method).toBe('DELETE');
103
+ });
104
+ it('should GET /teams/{id}/members for getMembers()', async () => {
105
+ const members = [{ user_id: 'user-1', role: 'owner' }];
106
+ mockJsonResponse(members);
107
+ const result = await api().getMembers('team-1');
108
+ expect(result).toEqual(members);
109
+ const [url, init] = mockFetch.mock.calls[0];
110
+ expect(url).toContain('/teams/team-1/members');
111
+ expect(init.method).toBe('GET');
112
+ });
113
+ it('should POST /teams/{id}/members for addMember()', async () => {
114
+ const payload = { user_id: 'user-4', role: 'member' };
115
+ const member = { ...payload };
116
+ mockJsonResponse(member);
117
+ const result = await api().addMember('team-1', payload);
118
+ expect(result).toEqual(member);
119
+ const [url, init] = mockFetch.mock.calls[0];
120
+ expect(url).toContain('/teams/team-1/members');
121
+ expect(JSON.parse(init.body)).toEqual(payload);
122
+ });
123
+ it('should GET /teams/{id}/invites for listInvites()', async () => {
124
+ const invites = [{ id: 'inv-1', email: 'dev@example.com' }];
125
+ mockJsonResponse(invites);
126
+ const result = await api().listInvites('team-1');
127
+ expect(result).toEqual(invites);
128
+ const [url, init] = mockFetch.mock.calls[0];
129
+ expect(url).toContain('/teams/team-1/invites');
130
+ expect(init.method).toBe('GET');
131
+ });
132
+ it('should DELETE /teams/{id}/invites/{inviteId} for revokeInvite()', async () => {
133
+ mockJsonResponse(null);
134
+ await api().revokeInvite('team-1', 'inv-9');
135
+ const [url, init] = mockFetch.mock.calls[0];
136
+ expect(url).toContain('/teams/team-1/invites/inv-9');
137
+ expect(init.method).toBe('DELETE');
138
+ });
79
139
  });
@@ -1,5 +1,7 @@
1
- import { Inference, inference } from './index';
1
+ import { Inference, inference, createClient } from './index';
2
2
  import { RequirementsNotMetException } from './http/errors';
3
+ import { HttpClient } from './http/client';
4
+ import { ChatStatusBusy, ChatStatusIdle } from './types';
3
5
  // Mock fetch globally
4
6
  const mockFetch = jest.fn();
5
7
  global.fetch = mockFetch;
@@ -138,6 +140,45 @@ describe('Inference', () => {
138
140
  expect(exception.errors[0].action?.scopes).toEqual(['calendar.readonly', 'calendar.events']);
139
141
  }
140
142
  });
143
+ it('should process Blob inputs via processInput before calling /apps/run', async () => {
144
+ const fileRecord = {
145
+ id: 'file-blob',
146
+ uri: 'inf://files/blob',
147
+ upload_url: 'https://upload.example.com/put',
148
+ content_type: 'image/png',
149
+ };
150
+ const mockTask = {
151
+ id: 'task-456',
152
+ status: 9,
153
+ created_at: new Date().toISOString(),
154
+ updated_at: new Date().toISOString(),
155
+ input: {},
156
+ output: { result: 'done' },
157
+ };
158
+ mockFetch
159
+ .mockResolvedValueOnce({
160
+ ok: true,
161
+ status: 200,
162
+ text: () => Promise.resolve(JSON.stringify([fileRecord])),
163
+ json: () => Promise.resolve([fileRecord]),
164
+ })
165
+ .mockResolvedValueOnce({ ok: true, status: 200 })
166
+ .mockResolvedValueOnce({
167
+ ok: true,
168
+ status: 200,
169
+ text: () => Promise.resolve(JSON.stringify(mockTask)),
170
+ json: () => Promise.resolve(mockTask),
171
+ });
172
+ const client = new Inference({ apiKey: 'test-api-key' });
173
+ const blob = new Blob(['png-bytes'], { type: 'image/png' });
174
+ const result = await client.run({ app: 'test-app', input: { image: blob } }, { wait: false });
175
+ expect(result.id).toBe('task-456');
176
+ expect(mockFetch).toHaveBeenCalledTimes(3);
177
+ const runCall = mockFetch.mock.calls.find((call) => String(call[0]).includes('/apps/run'));
178
+ expect(runCall).toBeDefined();
179
+ const runBody = JSON.parse(runCall[1].body);
180
+ expect(runBody.input.image).toBe('inf://files/blob');
181
+ });
141
182
  });
142
183
  describe('cancel', () => {
143
184
  it('should make a POST request to cancel endpoint', async () => {
@@ -153,6 +194,76 @@ describe('Inference', () => {
153
194
  expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/tasks/task-123/cancel'), expect.objectContaining({ method: 'POST' }));
154
195
  });
155
196
  });
197
+ describe('legacy task helpers', () => {
198
+ it('should delegate getTask() to tasks.get', async () => {
199
+ const mockTask = { id: 'task-legacy', status: 9 };
200
+ mockFetch.mockResolvedValueOnce({
201
+ ok: true,
202
+ status: 200,
203
+ text: () => Promise.resolve(JSON.stringify(mockTask)),
204
+ json: () => Promise.resolve(mockTask),
205
+ });
206
+ const client = new Inference({ apiKey: 'test-api-key' });
207
+ const result = await client.getTask('task-legacy');
208
+ expect(result.id).toBe('task-legacy');
209
+ expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/tasks/task-legacy'), expect.objectContaining({ method: 'GET' }));
210
+ });
211
+ it('should delegate streamTask() to tasks.stream', async () => {
212
+ const client = new Inference({ apiKey: 'test-api-key' });
213
+ const createEventSource = jest
214
+ .spyOn(HttpClient.prototype, 'createEventSource')
215
+ .mockResolvedValue(null);
216
+ await client.streamTask('task-stream');
217
+ expect(createEventSource).toHaveBeenCalledWith('/tasks/task-stream/stream');
218
+ createEventSource.mockRestore();
219
+ });
220
+ });
221
+ describe('agent()', () => {
222
+ it('should return an Agent whose sendMessage hits POST /agents/run', async () => {
223
+ mockFetch
224
+ .mockResolvedValueOnce({
225
+ ok: true,
226
+ status: 200,
227
+ text: () => Promise.resolve(JSON.stringify({
228
+ user_message: { id: 'user-1', chat_id: 'chat-1', role: 'user', content: 'hi' },
229
+ assistant_message: { id: 'asst-1', chat_id: 'chat-1', role: 'assistant', content: 'hello' },
230
+ })),
231
+ })
232
+ .mockResolvedValueOnce({
233
+ ok: true,
234
+ status: 200,
235
+ text: () => Promise.resolve(JSON.stringify({ status: ChatStatusBusy })),
236
+ })
237
+ .mockResolvedValueOnce({
238
+ ok: true,
239
+ status: 200,
240
+ text: () => Promise.resolve(JSON.stringify({ id: 'chat-1', status: ChatStatusBusy, chat_messages: [] })),
241
+ })
242
+ .mockResolvedValueOnce({
243
+ ok: true,
244
+ status: 200,
245
+ text: () => Promise.resolve(JSON.stringify({ status: ChatStatusIdle })),
246
+ })
247
+ .mockResolvedValueOnce({
248
+ ok: true,
249
+ status: 200,
250
+ text: () => Promise.resolve(JSON.stringify({ id: 'chat-1', status: ChatStatusIdle, chat_messages: [] })),
251
+ });
252
+ const client = new Inference({ apiKey: 'test-api-key', stream: false, pollIntervalMs: 20 });
253
+ const agentInstance = client.agent('my-agent');
254
+ await agentInstance.sendMessage('hi', { stream: false });
255
+ const runCall = mockFetch.mock.calls.find(([url]) => String(url).includes('/agents/run'));
256
+ expect(runCall).toBeDefined();
257
+ const body = JSON.parse(runCall[1].body);
258
+ expect(body.agent).toBe('my-agent');
259
+ });
260
+ });
261
+ describe('createClient', () => {
262
+ it('should create an Inference instance with extended HttpClient config', () => {
263
+ const client = createClient({ apiKey: 'extended-key', baseUrl: 'https://api.example.com' });
264
+ expect(client).toBeInstanceOf(Inference);
265
+ });
266
+ });
156
267
  describe('lowercase factory', () => {
157
268
  it('should export lowercase inference factory', () => {
158
269
  expect(typeof inference).toBe('function');
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,90 @@
1
+ import { INF_TARGET_HEADER } from './index';
2
+ import { createHandler } from './hono';
3
+ function createMockContext(overrides = {}) {
4
+ const headers = new Headers(overrides.headers ?? {});
5
+ const url = overrides.url ?? 'http://localhost/api/inference/proxy';
6
+ return {
7
+ req: {
8
+ method: overrides.method ?? 'POST',
9
+ url,
10
+ raw: { headers },
11
+ header: (name) => headers.get(name) ?? undefined,
12
+ text: () => Promise.resolve(overrides.body ?? '{"ok":true}'),
13
+ },
14
+ };
15
+ }
16
+ describe('hono createHandler', () => {
17
+ const originalFetch = global.fetch;
18
+ beforeEach(() => {
19
+ jest.clearAllMocks();
20
+ process.env.INFERENCE_API_KEY = 'hono-test-key';
21
+ });
22
+ afterEach(() => {
23
+ global.fetch = originalFetch;
24
+ });
25
+ it('should return 400 when target URL is missing', async () => {
26
+ const handler = createHandler();
27
+ const response = await handler(createMockContext());
28
+ expect(response.status).toBe(400);
29
+ expect(await response.json()).toEqual({
30
+ error: `Missing ${INF_TARGET_HEADER} header or __inf_target query param`,
31
+ });
32
+ });
33
+ it('should proxy JSON responses', async () => {
34
+ global.fetch = jest.fn().mockResolvedValue(new Response(JSON.stringify({ ok: true }), {
35
+ status: 200,
36
+ headers: { 'content-type': 'application/json' },
37
+ }));
38
+ const handler = createHandler();
39
+ const target = 'https://api.inference.sh/v1/run';
40
+ const response = await handler(createMockContext({
41
+ headers: { [INF_TARGET_HEADER]: target },
42
+ }));
43
+ expect(response.status).toBe(200);
44
+ expect(await response.json()).toEqual({ ok: true });
45
+ expect(global.fetch).toHaveBeenCalledWith(target, expect.objectContaining({
46
+ headers: expect.objectContaining({
47
+ authorization: 'Bearer hono-test-key',
48
+ }),
49
+ }));
50
+ });
51
+ it('should passthrough SSE streaming responses', async () => {
52
+ const encoder = new TextEncoder();
53
+ const stream = new ReadableStream({
54
+ start(controller) {
55
+ controller.enqueue(encoder.encode('data: {"x":1}\n\n'));
56
+ controller.close();
57
+ },
58
+ });
59
+ global.fetch = jest.fn().mockResolvedValue(new Response(stream, {
60
+ status: 200,
61
+ headers: { 'content-type': 'text/event-stream' },
62
+ }));
63
+ const handler = createHandler();
64
+ const target = 'https://api.inference.sh/v1/stream';
65
+ const response = await handler(createMockContext({
66
+ headers: { [INF_TARGET_HEADER]: target },
67
+ }));
68
+ expect(response.status).toBe(200);
69
+ expect(response.headers.get('content-type')).toContain('text/event-stream');
70
+ expect(await response.text()).toContain('data:');
71
+ });
72
+ it('should resolve API key via resolveApiKey option', async () => {
73
+ global.fetch = jest.fn().mockResolvedValue(new Response(JSON.stringify({ ok: true }), {
74
+ status: 200,
75
+ headers: { 'content-type': 'application/json' },
76
+ }));
77
+ const handler = createHandler({
78
+ resolveApiKey: async () => 'custom-hono-key',
79
+ });
80
+ const target = 'https://api.inference.sh/v1/run';
81
+ await handler(createMockContext({
82
+ headers: { [INF_TARGET_HEADER]: target },
83
+ }));
84
+ expect(global.fetch).toHaveBeenCalledWith(target, expect.objectContaining({
85
+ headers: expect.objectContaining({
86
+ authorization: 'Bearer custom-hono-key',
87
+ }),
88
+ }));
89
+ });
90
+ });
@@ -0,0 +1 @@
1
+ export {};