@inferencesh/sdk 0.6.12 → 0.6.14
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/actions.test.js +329 -2
- package/dist/agent/api.test.js +39 -1
- package/dist/api/agents.test.js +405 -1
- package/dist/api/files.test.js +8 -0
- package/dist/api/knowledge.test.js +144 -0
- package/dist/api/mcp-servers.test.js +34 -0
- package/dist/api/projects.test.js +9 -0
- package/dist/api/search.test.js +11 -0
- package/dist/api/sessions.test.js +12 -0
- package/dist/api/tasks.test.js +58 -0
- package/dist/api/teams.test.js +60 -0
- package/dist/client.test.js +112 -1
- package/dist/http/poll.test.js +61 -0
- package/dist/http/streamable.test.js +100 -0
- package/dist/proxy/hono.test.d.ts +1 -0
- package/dist/proxy/hono.test.js +90 -0
- package/dist/proxy/index.test.js +17 -0
- package/dist/proxy/nextjs.test.d.ts +1 -0
- package/dist/proxy/nextjs.test.js +125 -0
- package/dist/proxy/remix.test.d.ts +1 -0
- package/dist/proxy/remix.test.js +66 -0
- package/dist/proxy/svelte.test.d.ts +1 -0
- package/dist/proxy/svelte.test.js +69 -0
- package/dist/stream.test.js +9 -0
- package/dist/types.d.ts +1 -0
- package/package.json +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ChatStatusBusy, ToolInvocationStatusAwaitingInput, ToolTypeClient, } from '../types';
|
|
2
|
-
import { createActions } from './actions';
|
|
1
|
+
import { ChatStatusBusy, ToolInvocationStatusAwaitingInput, ToolInvocationStatusInProgress, ToolTypeClient, } from '../types';
|
|
2
|
+
import { createActions, getClientToolHandlers } from './actions';
|
|
3
3
|
import * as agentApi from './api';
|
|
4
4
|
import { PollManager } from '../http/poll';
|
|
5
5
|
import { StreamableManager } from '../http/streamable';
|
|
@@ -94,6 +94,12 @@ describe('createActions', () => {
|
|
|
94
94
|
mockAgentApi.approveTool.mockResolvedValue(undefined);
|
|
95
95
|
mockAgentApi.rejectTool.mockResolvedValue(undefined);
|
|
96
96
|
mockAgentApi.alwaysAllowTool.mockResolvedValue(undefined);
|
|
97
|
+
mockAgentApi.uploadFile.mockResolvedValue({
|
|
98
|
+
id: 'file-1',
|
|
99
|
+
uri: 'inf://files/uploaded',
|
|
100
|
+
filename: 'notes.txt',
|
|
101
|
+
content_type: 'text/plain',
|
|
102
|
+
});
|
|
97
103
|
});
|
|
98
104
|
describe('updateMessage (via stream listeners)', () => {
|
|
99
105
|
it('should ignore messages for a different chat when IDs do not prefix-match', async () => {
|
|
@@ -141,6 +147,30 @@ describe('createActions', () => {
|
|
|
141
147
|
expect(handler).toHaveBeenCalledWith({ x: 1 });
|
|
142
148
|
expect(mockAgentApi.submitToolResult).toHaveBeenCalledWith(ctx.client, 'tool-inv-ok', 'tool ok');
|
|
143
149
|
});
|
|
150
|
+
it('should dispatch client tool handlers when status is in_progress', async () => {
|
|
151
|
+
const handler = jest.fn().mockResolvedValue('in progress ok');
|
|
152
|
+
const { ctx } = createTestContext({
|
|
153
|
+
getClientToolHandlers: () => new Map([['my_tool', handler]]),
|
|
154
|
+
});
|
|
155
|
+
const { internalActions } = createActions(ctx);
|
|
156
|
+
internalActions.streamChat('chat-full-id-123');
|
|
157
|
+
await Promise.resolve();
|
|
158
|
+
const onMessage = streamInstances[0].addEventListener.mock.calls.find(([event]) => event === 'chat_messages')?.[1];
|
|
159
|
+
onMessage(makeMessage({
|
|
160
|
+
chat_id: 'chat-short',
|
|
161
|
+
tool_invocations: [
|
|
162
|
+
{
|
|
163
|
+
id: 'tool-inv-progress',
|
|
164
|
+
type: ToolTypeClient,
|
|
165
|
+
status: ToolInvocationStatusInProgress,
|
|
166
|
+
function: { name: 'my_tool', arguments: { y: 2 } },
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
}));
|
|
170
|
+
await Promise.resolve();
|
|
171
|
+
expect(handler).toHaveBeenCalledWith({ y: 2 });
|
|
172
|
+
expect(mockAgentApi.submitToolResult).toHaveBeenCalledWith(ctx.client, 'tool-inv-progress', 'in progress ok');
|
|
173
|
+
});
|
|
144
174
|
it('should submit a JSON error when a client tool handler throws', async () => {
|
|
145
175
|
const handler = jest.fn().mockRejectedValue(new Error('handler boom'));
|
|
146
176
|
const { ctx } = createTestContext({
|
|
@@ -200,6 +230,35 @@ describe('createActions', () => {
|
|
|
200
230
|
expect(pollInstances[0].options.pollFunction).toBeDefined();
|
|
201
231
|
expect(pollInstances[0].start).toHaveBeenCalled();
|
|
202
232
|
});
|
|
233
|
+
it('should stop an existing stream manager before starting a new connection', async () => {
|
|
234
|
+
const { ctx, dispatch } = createTestContext();
|
|
235
|
+
const { internalActions } = createActions(ctx);
|
|
236
|
+
internalActions.streamChat('chat-full-id-123');
|
|
237
|
+
await Promise.resolve();
|
|
238
|
+
const firstManager = streamInstances[0];
|
|
239
|
+
internalActions.streamChat('chat-full-id-123');
|
|
240
|
+
await Promise.resolve();
|
|
241
|
+
expect(firstManager.stop).toHaveBeenCalled();
|
|
242
|
+
expect(streamInstances).toHaveLength(2);
|
|
243
|
+
expect(streamInstances[1].start).toHaveBeenCalled();
|
|
244
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
245
|
+
type: 'SET_CONNECTION_STATUS',
|
|
246
|
+
payload: 'connecting',
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
it('should dispatch streaming status when the stream manager starts', async () => {
|
|
250
|
+
const onStatusChange = jest.fn();
|
|
251
|
+
const { ctx, dispatch } = createTestContext({ callbacks: { onStatusChange } });
|
|
252
|
+
const { internalActions } = createActions(ctx);
|
|
253
|
+
internalActions.streamChat('chat-full-id-123');
|
|
254
|
+
await Promise.resolve();
|
|
255
|
+
streamInstances[0].options.onStart?.();
|
|
256
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
257
|
+
type: 'SET_CONNECTION_STATUS',
|
|
258
|
+
payload: 'streaming',
|
|
259
|
+
});
|
|
260
|
+
expect(onStatusChange).toHaveBeenCalledWith('streaming');
|
|
261
|
+
});
|
|
203
262
|
});
|
|
204
263
|
describe('stopStream', () => {
|
|
205
264
|
it('should clear the manager ref before stop so onEnd does not double-dispatch idle', async () => {
|
|
@@ -216,6 +275,78 @@ describe('createActions', () => {
|
|
|
216
275
|
// Only the explicit stopStream dispatch, not a second from onEnd
|
|
217
276
|
expect(idleDispatches).toHaveLength(1);
|
|
218
277
|
});
|
|
278
|
+
it('should reset to idle when the stream ends unexpectedly with manager still set', async () => {
|
|
279
|
+
const onStatusChange = jest.fn();
|
|
280
|
+
const { ctx, dispatch } = createTestContext({ callbacks: { onStatusChange } });
|
|
281
|
+
const { internalActions } = createActions(ctx);
|
|
282
|
+
internalActions.streamChat('chat-full-id-123');
|
|
283
|
+
await Promise.resolve();
|
|
284
|
+
streamInstances[0].options.onEnd?.();
|
|
285
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
286
|
+
type: 'SET_CONNECTION_STATUS',
|
|
287
|
+
payload: 'idle',
|
|
288
|
+
});
|
|
289
|
+
expect(onStatusChange).toHaveBeenCalledWith('idle');
|
|
290
|
+
});
|
|
291
|
+
it('should clear the manager ref before poll stop so onStop does not double-dispatch idle', async () => {
|
|
292
|
+
const onStatusChange = jest.fn();
|
|
293
|
+
const { ctx, dispatch, setStreamManager } = createTestContext({
|
|
294
|
+
getStreamEnabled: () => false,
|
|
295
|
+
callbacks: { onStatusChange },
|
|
296
|
+
});
|
|
297
|
+
const { internalActions } = createActions(ctx);
|
|
298
|
+
internalActions.streamChat('chat-full-id-123');
|
|
299
|
+
await Promise.resolve();
|
|
300
|
+
const manager = pollInstances[0];
|
|
301
|
+
internalActions.stopStream();
|
|
302
|
+
expect(setStreamManager).toHaveBeenCalledWith(undefined);
|
|
303
|
+
expect(manager.stop).toHaveBeenCalled();
|
|
304
|
+
manager.options.onStop?.();
|
|
305
|
+
const idleDispatches = dispatch.mock.calls.filter(([action]) => action.type === 'SET_CONNECTION_STATUS' && action.payload === 'idle');
|
|
306
|
+
expect(idleDispatches).toHaveLength(1);
|
|
307
|
+
expect(onStatusChange).toHaveBeenCalledWith('idle');
|
|
308
|
+
});
|
|
309
|
+
it('should reset to idle when poll transport stops unexpectedly with manager still set', async () => {
|
|
310
|
+
const onStatusChange = jest.fn();
|
|
311
|
+
const { ctx, dispatch } = createTestContext({
|
|
312
|
+
getStreamEnabled: () => false,
|
|
313
|
+
callbacks: { onStatusChange },
|
|
314
|
+
});
|
|
315
|
+
const { internalActions } = createActions(ctx);
|
|
316
|
+
internalActions.streamChat('chat-full-id-123');
|
|
317
|
+
await Promise.resolve();
|
|
318
|
+
pollInstances[0].options.onStop?.();
|
|
319
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
320
|
+
type: 'SET_CONNECTION_STATUS',
|
|
321
|
+
payload: 'idle',
|
|
322
|
+
});
|
|
323
|
+
expect(onStatusChange).toHaveBeenCalledWith('idle');
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
describe('stream and poll error callbacks', () => {
|
|
327
|
+
it('should forward stream errors to onError callback', async () => {
|
|
328
|
+
const onError = jest.fn();
|
|
329
|
+
const { ctx } = createTestContext({ callbacks: { onError } });
|
|
330
|
+
const { internalActions } = createActions(ctx);
|
|
331
|
+
internalActions.streamChat('chat-full-id-123');
|
|
332
|
+
await Promise.resolve();
|
|
333
|
+
const streamError = new Error('stream connection lost');
|
|
334
|
+
streamInstances[0].options.onError?.(streamError);
|
|
335
|
+
expect(onError).toHaveBeenCalledWith(streamError);
|
|
336
|
+
});
|
|
337
|
+
it('should forward poll manager errors to onError callback', async () => {
|
|
338
|
+
const onError = jest.fn();
|
|
339
|
+
const { ctx } = createTestContext({
|
|
340
|
+
getStreamEnabled: () => false,
|
|
341
|
+
callbacks: { onError },
|
|
342
|
+
});
|
|
343
|
+
const { internalActions } = createActions(ctx);
|
|
344
|
+
internalActions.streamChat('chat-full-id-123');
|
|
345
|
+
await Promise.resolve();
|
|
346
|
+
const pollError = new Error('poll transport failed');
|
|
347
|
+
pollInstances[0].options.onError?.(pollError);
|
|
348
|
+
expect(onError).toHaveBeenCalledWith(pollError);
|
|
349
|
+
});
|
|
219
350
|
});
|
|
220
351
|
describe('publicActions.sendMessage', () => {
|
|
221
352
|
it('should call onChatCreated and start streaming for a new chat', async () => {
|
|
@@ -265,6 +396,19 @@ describe('createActions', () => {
|
|
|
265
396
|
await publicActions.sendMessage(' ');
|
|
266
397
|
expect(mockAgentApi.sendMessage).not.toHaveBeenCalled();
|
|
267
398
|
});
|
|
399
|
+
it('should no-op when agent config is missing', async () => {
|
|
400
|
+
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
401
|
+
const { ctx, dispatch } = createTestContext({ getConfig: () => null });
|
|
402
|
+
const { publicActions } = createActions(ctx);
|
|
403
|
+
await publicActions.sendMessage('hello');
|
|
404
|
+
expect(mockAgentApi.sendMessage).not.toHaveBeenCalled();
|
|
405
|
+
expect(dispatch).not.toHaveBeenCalledWith({
|
|
406
|
+
type: 'SET_CONNECTION_STATUS',
|
|
407
|
+
payload: 'streaming',
|
|
408
|
+
});
|
|
409
|
+
expect(consoleError).toHaveBeenCalledWith('[AgentSDK] No agent config provided');
|
|
410
|
+
consoleError.mockRestore();
|
|
411
|
+
});
|
|
268
412
|
});
|
|
269
413
|
describe('streamChat error handling', () => {
|
|
270
414
|
it('should reset to idle when initial fetchChat fails', async () => {
|
|
@@ -296,6 +440,22 @@ describe('createActions', () => {
|
|
|
296
440
|
});
|
|
297
441
|
});
|
|
298
442
|
describe('pollChat', () => {
|
|
443
|
+
it('should dispatch streaming status when the poll manager starts', async () => {
|
|
444
|
+
const onStatusChange = jest.fn();
|
|
445
|
+
const { ctx, dispatch } = createTestContext({
|
|
446
|
+
getStreamEnabled: () => false,
|
|
447
|
+
callbacks: { onStatusChange },
|
|
448
|
+
});
|
|
449
|
+
const { internalActions } = createActions(ctx);
|
|
450
|
+
internalActions.streamChat('chat-full-id-123');
|
|
451
|
+
await Promise.resolve();
|
|
452
|
+
pollInstances[0].options.onStart?.();
|
|
453
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
454
|
+
type: 'SET_CONNECTION_STATUS',
|
|
455
|
+
payload: 'streaming',
|
|
456
|
+
});
|
|
457
|
+
expect(onStatusChange).toHaveBeenCalledWith('streaming');
|
|
458
|
+
});
|
|
299
459
|
it('should fetch full chat when poll status changes', async () => {
|
|
300
460
|
const { ctx: baseCtx } = createTestContext({ getStreamEnabled: () => false });
|
|
301
461
|
const { ctx } = createTestContext({
|
|
@@ -348,6 +508,77 @@ describe('createActions', () => {
|
|
|
348
508
|
await Promise.resolve();
|
|
349
509
|
expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'poll fetch failed' }));
|
|
350
510
|
});
|
|
511
|
+
it('should not refetch chat when poll status is unchanged', async () => {
|
|
512
|
+
const { ctx: baseCtx } = createTestContext({ getStreamEnabled: () => false });
|
|
513
|
+
const { ctx } = createTestContext({
|
|
514
|
+
getStreamEnabled: () => false,
|
|
515
|
+
client: {
|
|
516
|
+
...baseCtx.client,
|
|
517
|
+
http: { ...baseCtx.client.http, request: jest.fn().mockResolvedValue({ status: ChatStatusBusy }) },
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
const { internalActions } = createActions(ctx);
|
|
521
|
+
mockAgentApi.fetchChat.mockResolvedValue({
|
|
522
|
+
id: 'chat-full-id-123',
|
|
523
|
+
status: ChatStatusBusy,
|
|
524
|
+
chat_messages: [],
|
|
525
|
+
});
|
|
526
|
+
internalActions.streamChat('chat-full-id-123');
|
|
527
|
+
await Promise.resolve();
|
|
528
|
+
await pollInstances[0].options.onData?.({ status: ChatStatusBusy });
|
|
529
|
+
await Promise.resolve();
|
|
530
|
+
const fetchCountAfterFirst = mockAgentApi.fetchChat.mock.calls.length;
|
|
531
|
+
await pollInstances[0].options.onData?.({ status: ChatStatusBusy });
|
|
532
|
+
await Promise.resolve();
|
|
533
|
+
expect(mockAgentApi.fetchChat).toHaveBeenCalledTimes(fetchCountAfterFirst);
|
|
534
|
+
});
|
|
535
|
+
it('should dispatch client tools from poll path when status changes', async () => {
|
|
536
|
+
const handler = jest.fn().mockResolvedValue('poll ok');
|
|
537
|
+
const toolMessage = makeMessage({
|
|
538
|
+
chat_id: 'chat-short',
|
|
539
|
+
tool_invocations: [
|
|
540
|
+
{
|
|
541
|
+
id: 'tool-inv-poll',
|
|
542
|
+
type: ToolTypeClient,
|
|
543
|
+
status: ToolInvocationStatusInProgress,
|
|
544
|
+
function: { name: 'my_tool', arguments: { y: 2 } },
|
|
545
|
+
},
|
|
546
|
+
],
|
|
547
|
+
});
|
|
548
|
+
const { ctx: baseCtx } = createTestContext({
|
|
549
|
+
getStreamEnabled: () => false,
|
|
550
|
+
getClientToolHandlers: () => new Map([['my_tool', handler]]),
|
|
551
|
+
});
|
|
552
|
+
const { ctx } = createTestContext({
|
|
553
|
+
getStreamEnabled: () => false,
|
|
554
|
+
getClientToolHandlers: () => new Map([['my_tool', handler]]),
|
|
555
|
+
client: {
|
|
556
|
+
...baseCtx.client,
|
|
557
|
+
http: {
|
|
558
|
+
...baseCtx.client.http,
|
|
559
|
+
request: jest.fn().mockResolvedValue({ status: ChatStatusBusy }),
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
const { internalActions } = createActions(ctx);
|
|
564
|
+
mockAgentApi.fetchChat
|
|
565
|
+
.mockResolvedValueOnce({
|
|
566
|
+
id: 'chat-full-id-123',
|
|
567
|
+
status: ChatStatusBusy,
|
|
568
|
+
chat_messages: [],
|
|
569
|
+
})
|
|
570
|
+
.mockResolvedValueOnce({
|
|
571
|
+
id: 'chat-full-id-123',
|
|
572
|
+
status: ChatStatusBusy,
|
|
573
|
+
chat_messages: [toolMessage],
|
|
574
|
+
});
|
|
575
|
+
internalActions.streamChat('chat-full-id-123');
|
|
576
|
+
await Promise.resolve();
|
|
577
|
+
await pollInstances[0].options.onData?.({ status: 'idle' });
|
|
578
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
579
|
+
expect(handler).toHaveBeenCalledWith({ y: 2 });
|
|
580
|
+
expect(mockAgentApi.submitToolResult).toHaveBeenCalledWith(ctx.client, 'tool-inv-poll', 'poll ok');
|
|
581
|
+
});
|
|
351
582
|
});
|
|
352
583
|
describe('client tool deduplication', () => {
|
|
353
584
|
it('should not submit the same client tool invocation twice', async () => {
|
|
@@ -378,6 +609,14 @@ describe('createActions', () => {
|
|
|
378
609
|
});
|
|
379
610
|
});
|
|
380
611
|
describe('publicActions lifecycle', () => {
|
|
612
|
+
it('uploadFile should delegate to the API layer', async () => {
|
|
613
|
+
const file = new File(['hello'], 'notes.txt', { type: 'text/plain' });
|
|
614
|
+
const { ctx } = createTestContext();
|
|
615
|
+
const { publicActions } = createActions(ctx);
|
|
616
|
+
const result = await publicActions.uploadFile(file);
|
|
617
|
+
expect(mockAgentApi.uploadFile).toHaveBeenCalledWith(ctx.client, file);
|
|
618
|
+
expect(result).toEqual(expect.objectContaining({ uri: 'inf://files/uploaded', filename: 'notes.txt' }));
|
|
619
|
+
});
|
|
381
620
|
it('reset should stop stream and dispatch RESET', async () => {
|
|
382
621
|
const { ctx, dispatch } = createTestContext();
|
|
383
622
|
const { publicActions } = createActions(ctx);
|
|
@@ -422,6 +661,38 @@ describe('createActions', () => {
|
|
|
422
661
|
payload: 'idle',
|
|
423
662
|
});
|
|
424
663
|
});
|
|
664
|
+
it('reset should allow the same client tool invocation to dispatch again', async () => {
|
|
665
|
+
const handler = jest.fn().mockResolvedValue('ok');
|
|
666
|
+
const { ctx } = createTestContext({
|
|
667
|
+
getClientToolHandlers: () => new Map([['my_tool', handler]]),
|
|
668
|
+
});
|
|
669
|
+
const { publicActions, internalActions } = createActions(ctx);
|
|
670
|
+
internalActions.streamChat('chat-full-id-123');
|
|
671
|
+
await Promise.resolve();
|
|
672
|
+
const onMessage = streamInstances[0].addEventListener.mock.calls.find(([event]) => event === 'chat_messages')?.[1];
|
|
673
|
+
const toolMessage = makeMessage({
|
|
674
|
+
chat_id: 'chat-short',
|
|
675
|
+
tool_invocations: [
|
|
676
|
+
{
|
|
677
|
+
id: 'tool-inv-reset',
|
|
678
|
+
type: ToolTypeClient,
|
|
679
|
+
status: ToolInvocationStatusAwaitingInput,
|
|
680
|
+
function: { name: 'my_tool', arguments: {} },
|
|
681
|
+
},
|
|
682
|
+
],
|
|
683
|
+
});
|
|
684
|
+
onMessage(toolMessage);
|
|
685
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
686
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
687
|
+
publicActions.reset();
|
|
688
|
+
internalActions.streamChat('chat-full-id-123');
|
|
689
|
+
await Promise.resolve();
|
|
690
|
+
const onMessageAfterReset = streamInstances[1].addEventListener.mock.calls.find(([event]) => event === 'chat_messages')?.[1];
|
|
691
|
+
onMessageAfterReset(toolMessage);
|
|
692
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
693
|
+
expect(handler).toHaveBeenCalledTimes(2);
|
|
694
|
+
expect(mockAgentApi.submitToolResult).toHaveBeenCalledTimes(2);
|
|
695
|
+
});
|
|
425
696
|
});
|
|
426
697
|
describe('HIL tool actions', () => {
|
|
427
698
|
it('approveTool should delegate to the API', async () => {
|
|
@@ -460,6 +731,33 @@ describe('createActions', () => {
|
|
|
460
731
|
});
|
|
461
732
|
expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'approve failed' }));
|
|
462
733
|
});
|
|
734
|
+
it('rejectTool should set error state when API fails', async () => {
|
|
735
|
+
mockAgentApi.rejectTool.mockRejectedValueOnce(new Error('reject failed'));
|
|
736
|
+
const onError = jest.fn();
|
|
737
|
+
const { ctx, dispatch } = createTestContext({ callbacks: { onError } });
|
|
738
|
+
const { publicActions } = createActions(ctx);
|
|
739
|
+
await expect(publicActions.rejectTool('inv-1', 'unsafe')).rejects.toThrow('reject failed');
|
|
740
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
741
|
+
type: 'SET_CONNECTION_STATUS',
|
|
742
|
+
payload: 'error',
|
|
743
|
+
});
|
|
744
|
+
expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'reject failed' }));
|
|
745
|
+
});
|
|
746
|
+
it('alwaysAllowTool should set error state when API fails', async () => {
|
|
747
|
+
mockAgentApi.alwaysAllowTool.mockRejectedValueOnce(new Error('allow failed'));
|
|
748
|
+
const onError = jest.fn();
|
|
749
|
+
const { ctx, dispatch } = createTestContext({
|
|
750
|
+
getChatId: () => 'chat-short',
|
|
751
|
+
callbacks: { onError },
|
|
752
|
+
});
|
|
753
|
+
const { publicActions } = createActions(ctx);
|
|
754
|
+
await expect(publicActions.alwaysAllowTool('inv-1', 'my_tool')).rejects.toThrow('allow failed');
|
|
755
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
756
|
+
type: 'SET_CONNECTION_STATUS',
|
|
757
|
+
payload: 'error',
|
|
758
|
+
});
|
|
759
|
+
expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'allow failed' }));
|
|
760
|
+
});
|
|
463
761
|
});
|
|
464
762
|
describe('setChatId', () => {
|
|
465
763
|
it('should no-op when the chat id is unchanged', async () => {
|
|
@@ -491,3 +789,32 @@ describe('createActions', () => {
|
|
|
491
789
|
});
|
|
492
790
|
});
|
|
493
791
|
});
|
|
792
|
+
describe('getClientToolHandlers', () => {
|
|
793
|
+
it('returns an empty map when config is null', () => {
|
|
794
|
+
expect(getClientToolHandlers(null)).toEqual(new Map());
|
|
795
|
+
});
|
|
796
|
+
it('returns an empty map for template agent refs', () => {
|
|
797
|
+
expect(getClientToolHandlers({ agent: 'inference/my-agent' })).toEqual(new Map());
|
|
798
|
+
});
|
|
799
|
+
it('returns an empty map when ad-hoc config has no tools', () => {
|
|
800
|
+
expect(getClientToolHandlers({
|
|
801
|
+
core_app: { ref: 'openrouter/claude@abc' },
|
|
802
|
+
system_prompt: 'test',
|
|
803
|
+
})).toEqual(new Map());
|
|
804
|
+
});
|
|
805
|
+
it('extracts client tool handlers from ad-hoc config tools', () => {
|
|
806
|
+
const handler = jest.fn();
|
|
807
|
+
const map = getClientToolHandlers({
|
|
808
|
+
core_app: { ref: 'openrouter/claude@abc' },
|
|
809
|
+
system_prompt: 'test',
|
|
810
|
+
tools: [
|
|
811
|
+
{
|
|
812
|
+
schema: { name: 'browser', type: ToolTypeClient, description: 'browse' },
|
|
813
|
+
handler,
|
|
814
|
+
},
|
|
815
|
+
],
|
|
816
|
+
});
|
|
817
|
+
expect(map.size).toBe(1);
|
|
818
|
+
expect(map.get('browser')).toBe(handler);
|
|
819
|
+
});
|
|
820
|
+
});
|
package/dist/agent/api.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HttpClient } from '../http/client';
|
|
2
2
|
import { FilesAPI } from '../api/files';
|
|
3
|
-
import { sendAdHocMessage, sendTemplateMessage, sendMessage, submitToolResult, approveTool, rejectTool, alwaysAllowTool, fetchChat, stopChat, getChatStreamConfig, } from './api';
|
|
3
|
+
import { sendAdHocMessage, sendTemplateMessage, sendMessage, submitToolResult, approveTool, rejectTool, alwaysAllowTool, fetchChat, stopChat, getChatStreamConfig, uploadFile, } from './api';
|
|
4
4
|
import { ToolTypeClient } from '../types';
|
|
5
5
|
const mockFetch = jest.fn();
|
|
6
6
|
global.fetch = mockFetch;
|
|
@@ -125,6 +125,15 @@ describe('agent/api', () => {
|
|
|
125
125
|
const body = JSON.parse(String(runInit.body));
|
|
126
126
|
expect(body.input.attachments).toEqual([fileRecord]);
|
|
127
127
|
});
|
|
128
|
+
it('should route template agent configs to /agents/run without agent_config', async () => {
|
|
129
|
+
mockJsonResponse(runResponse);
|
|
130
|
+
await sendMessage(makeClient(), { agent: 'agent-template-1' }, 'chat-existing', 'hi');
|
|
131
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
132
|
+
const body = JSON.parse(String(init.body));
|
|
133
|
+
expect(body.agent).toBe('agent-template-1');
|
|
134
|
+
expect(body.chat_id).toBe('chat-existing');
|
|
135
|
+
expect(body).not.toHaveProperty('agent_config');
|
|
136
|
+
});
|
|
128
137
|
});
|
|
129
138
|
describe('submitToolResult', () => {
|
|
130
139
|
it('should wrap string results in { result }', async () => {
|
|
@@ -143,6 +152,10 @@ describe('agent/api', () => {
|
|
|
143
152
|
const [, init] = mockFetch.mock.calls[0];
|
|
144
153
|
expect(JSON.parse(String(init.body))).toEqual(payload);
|
|
145
154
|
});
|
|
155
|
+
it('should rethrow when the request fails', async () => {
|
|
156
|
+
mockFetch.mockRejectedValueOnce(new Error('submit failed'));
|
|
157
|
+
await expect(submitToolResult(makeClient(), 'inv-3', 'done')).rejects.toThrow('submit failed');
|
|
158
|
+
});
|
|
146
159
|
});
|
|
147
160
|
describe('fetchChat', () => {
|
|
148
161
|
it('should return chat data on success', async () => {
|
|
@@ -196,6 +209,14 @@ describe('agent/api', () => {
|
|
|
196
209
|
mockFetch.mockRejectedValueOnce(new Error('approve failed'));
|
|
197
210
|
await expect(approveTool(makeClient(), 'inv-1')).rejects.toThrow('approve failed');
|
|
198
211
|
});
|
|
212
|
+
it('should rethrow when rejectTool request fails', async () => {
|
|
213
|
+
mockFetch.mockRejectedValueOnce(new Error('reject failed'));
|
|
214
|
+
await expect(rejectTool(makeClient(), 'inv-1', 'unsafe')).rejects.toThrow('reject failed');
|
|
215
|
+
});
|
|
216
|
+
it('should rethrow when alwaysAllowTool request fails', async () => {
|
|
217
|
+
mockFetch.mockRejectedValueOnce(new Error('allow failed'));
|
|
218
|
+
await expect(alwaysAllowTool(makeClient(), 'chat-1', 'inv-1', 'browser_tool')).rejects.toThrow('allow failed');
|
|
219
|
+
});
|
|
199
220
|
});
|
|
200
221
|
describe('getChatStreamConfig', () => {
|
|
201
222
|
it('should delegate to HttpClient.getStreamableConfig for the chat stream path', () => {
|
|
@@ -205,4 +226,21 @@ describe('agent/api', () => {
|
|
|
205
226
|
expect(config.headers).toEqual(expect.objectContaining({ Authorization: expect.stringContaining('Bearer') }));
|
|
206
227
|
});
|
|
207
228
|
});
|
|
229
|
+
describe('uploadFile', () => {
|
|
230
|
+
it('should delegate to client.files.upload and return the uploaded file ref', async () => {
|
|
231
|
+
const client = makeClient();
|
|
232
|
+
const fileRecord = {
|
|
233
|
+
id: 'file-1',
|
|
234
|
+
uri: 'inf://files/uploaded',
|
|
235
|
+
filename: 'notes.txt',
|
|
236
|
+
content_type: 'text/plain',
|
|
237
|
+
};
|
|
238
|
+
const uploadSpy = jest.spyOn(client.files, 'upload').mockResolvedValue(fileRecord);
|
|
239
|
+
const file = new File(['hello'], 'notes.txt', { type: 'text/plain' });
|
|
240
|
+
const result = await uploadFile(client, file);
|
|
241
|
+
expect(uploadSpy).toHaveBeenCalledWith(file);
|
|
242
|
+
expect(result).toEqual(fileRecord);
|
|
243
|
+
uploadSpy.mockRestore();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
208
246
|
});
|