@inferencesh/sdk 0.6.4 → 0.6.6

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/README.md CHANGED
@@ -409,7 +409,7 @@ import type { Task, ApiTaskRequest, RunOptions } from '@inferencesh/sdk';
409
409
  - [documentation](https://inference.sh/docs) — getting started guides and api reference
410
410
  - [blog](https://inference.sh/blog) — tutorials on ai agents, image generation, and more
411
411
  - [app store](https://app.inference.sh) — browse 250+ ai models
412
- - [discord](https://discord.gg/RM77SWSbyT) — community support
412
+ - [discord](https://discord.gg/inference) — community support
413
413
  - [github](https://github.com/inference-sh) — open source projects
414
414
 
415
415
  ## license
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,127 @@
1
+ import { ChatStatusBusy, ChatStatusIdle } from '../types';
2
+ import { chatReducer, initialState } from './reducer';
3
+ function makeMessage(id, order, chatId = 'chat-1') {
4
+ return {
5
+ id,
6
+ short_id: id,
7
+ created_at: '2026-01-01T00:00:00Z',
8
+ updated_at: '2026-01-01T00:00:00Z',
9
+ user_id: 'user-1',
10
+ team_id: 'team-1',
11
+ visibility: 'private',
12
+ chat_id: chatId,
13
+ order,
14
+ status: 'completed',
15
+ role: 'user',
16
+ content: [{ type: 'text', text: `message ${id}` }],
17
+ };
18
+ }
19
+ function makeChat(overrides = {}) {
20
+ return {
21
+ id: 'chat-1',
22
+ short_id: 'c1',
23
+ created_at: '2026-01-01T00:00:00Z',
24
+ updated_at: '2026-01-01T00:00:00Z',
25
+ user_id: 'user-1',
26
+ team_id: 'team-1',
27
+ visibility: 'private',
28
+ status: ChatStatusBusy,
29
+ name: 'Test chat',
30
+ description: '',
31
+ children: [],
32
+ chat_messages: [makeMessage('msg-1', 1), makeMessage('msg-2', 2)],
33
+ agent_data: {},
34
+ ...overrides,
35
+ };
36
+ }
37
+ describe('chatReducer', () => {
38
+ it('SET_CHAT should sort messages by order', () => {
39
+ const chat = makeChat({
40
+ chat_messages: [makeMessage('msg-2', 2), makeMessage('msg-1', 1)],
41
+ });
42
+ const next = chatReducer(initialState, { type: 'SET_CHAT', payload: chat });
43
+ expect(next.messages.map((m) => m.id)).toEqual(['msg-1', 'msg-2']);
44
+ expect(next.chat?.status).toBe(ChatStatusBusy);
45
+ });
46
+ it('UPDATE_CHAT should update chat metadata without replacing messages', () => {
47
+ const chat = makeChat();
48
+ const withMessages = chatReducer(initialState, { type: 'SET_CHAT', payload: chat });
49
+ const idleChat = makeChat({ status: ChatStatusIdle, chat_messages: [] });
50
+ const next = chatReducer(withMessages, { type: 'UPDATE_CHAT', payload: idleChat });
51
+ expect(next.chat?.status).toBe(ChatStatusIdle);
52
+ expect(next.messages).toHaveLength(2);
53
+ expect(next.messages.map((m) => m.id)).toEqual(['msg-1', 'msg-2']);
54
+ });
55
+ it('UPDATE_CHAT with null payload should leave state unchanged', () => {
56
+ const chat = makeChat();
57
+ const withChat = chatReducer(initialState, { type: 'SET_CHAT', payload: chat });
58
+ const next = chatReducer(withChat, { type: 'UPDATE_CHAT', payload: null });
59
+ expect(next).toBe(withChat);
60
+ });
61
+ it('UPDATE_MESSAGE should replace an existing message by id', () => {
62
+ const chat = makeChat();
63
+ const state = chatReducer(initialState, { type: 'SET_CHAT', payload: chat });
64
+ const updated = makeMessage('msg-1', 1);
65
+ updated.content = [{ type: 'text', text: 'edited' }];
66
+ const next = chatReducer(state, { type: 'UPDATE_MESSAGE', payload: updated });
67
+ expect(next.messages.find((m) => m.id === 'msg-1')?.content[0]?.text).toBe('edited');
68
+ expect(next.messages).toHaveLength(2);
69
+ });
70
+ it('UPDATE_MESSAGE should append and sort new messages', () => {
71
+ const chat = makeChat({ chat_messages: [makeMessage('msg-1', 1)] });
72
+ const state = chatReducer(initialState, { type: 'SET_CHAT', payload: chat });
73
+ const next = chatReducer(state, { type: 'UPDATE_MESSAGE', payload: makeMessage('msg-2', 2) });
74
+ expect(next.messages.map((m) => m.id)).toEqual(['msg-1', 'msg-2']);
75
+ });
76
+ it('SET_CHAT with null should clear chat and messages', () => {
77
+ const chat = makeChat();
78
+ const state = chatReducer(initialState, { type: 'SET_CHAT', payload: chat });
79
+ const next = chatReducer(state, { type: 'SET_CHAT', payload: null });
80
+ expect(next.chat).toBeNull();
81
+ expect(next.messages).toEqual([]);
82
+ expect(next.connectionStatus).toBe('idle');
83
+ });
84
+ it('ADD_MESSAGE should append and sort by order', () => {
85
+ const chat = makeChat({ chat_messages: [makeMessage('msg-1', 1)] });
86
+ const state = chatReducer(initialState, { type: 'SET_CHAT', payload: chat });
87
+ const next = chatReducer(state, {
88
+ type: 'ADD_MESSAGE',
89
+ payload: makeMessage('msg-0', 0),
90
+ });
91
+ expect(next.messages.map((m) => m.id)).toEqual(['msg-0', 'msg-1']);
92
+ });
93
+ it('RESET should return initial state', () => {
94
+ const chat = makeChat();
95
+ const state = chatReducer(initialState, { type: 'SET_CHAT', payload: chat });
96
+ expect(chatReducer(state, { type: 'RESET' })).toEqual(initialState);
97
+ });
98
+ it('SET_CHAT_ID should update chatId', () => {
99
+ const next = chatReducer(initialState, { type: 'SET_CHAT_ID', payload: 'chat-99' });
100
+ expect(next.chatId).toBe('chat-99');
101
+ });
102
+ it('SET_MESSAGES should replace the message list', () => {
103
+ const messages = [makeMessage('msg-a', 1)];
104
+ const next = chatReducer(initialState, { type: 'SET_MESSAGES', payload: messages });
105
+ expect(next.messages).toEqual(messages);
106
+ });
107
+ it('ADD_MESSAGE after SET_MESSAGES should append and sort by order', () => {
108
+ const state = chatReducer(initialState, {
109
+ type: 'SET_MESSAGES',
110
+ payload: [makeMessage('msg-2', 2)],
111
+ });
112
+ const next = chatReducer(state, { type: 'ADD_MESSAGE', payload: makeMessage('msg-1', 1) });
113
+ expect(next.messages.map((m) => m.id)).toEqual(['msg-1', 'msg-2']);
114
+ });
115
+ it('SET_CONNECTION_STATUS and SET_ERROR should update connection fields', () => {
116
+ const streaming = chatReducer(initialState, {
117
+ type: 'SET_CONNECTION_STATUS',
118
+ payload: 'streaming',
119
+ });
120
+ expect(streaming.connectionStatus).toBe('streaming');
121
+ const errored = chatReducer(streaming, {
122
+ type: 'SET_ERROR',
123
+ payload: 'stream failed',
124
+ });
125
+ expect(errored.error).toBe('stream failed');
126
+ });
127
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,141 @@
1
+ import { HttpClient } from '../http/client';
2
+ import { ChatStatusBusy, ChatStatusIdle, ToolInvocationStatusAwaitingInput, ToolTypeClient, } from '../types';
3
+ import { FilesAPI } from './files';
4
+ import { AgentsAPI } from './agents';
5
+ const mockFetch = jest.fn();
6
+ global.fetch = mockFetch;
7
+ function mockJsonResponse(body) {
8
+ mockFetch.mockResolvedValueOnce({
9
+ ok: true,
10
+ status: 200,
11
+ text: () => Promise.resolve(JSON.stringify(body)),
12
+ });
13
+ }
14
+ function makeMessage(overrides = {}) {
15
+ return {
16
+ id: 'msg-1',
17
+ chat_id: 'chat-1',
18
+ role: 'assistant',
19
+ content: 'hello',
20
+ ...overrides,
21
+ };
22
+ }
23
+ describe('Agent.sendMessage (polling mode)', () => {
24
+ beforeEach(() => {
25
+ jest.clearAllMocks();
26
+ });
27
+ const agent = () => {
28
+ const http = new HttpClient({
29
+ apiKey: 'test-key',
30
+ stream: false,
31
+ pollIntervalMs: 20,
32
+ });
33
+ return new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
34
+ };
35
+ it('should wait until chat is idle when stream is false', async () => {
36
+ const userMessage = makeMessage({ id: 'user-1', role: 'user' });
37
+ const assistantMessage = makeMessage({ id: 'asst-1' });
38
+ mockJsonResponse({
39
+ success: true,
40
+ data: { user_message: userMessage, assistant_message: assistantMessage },
41
+ });
42
+ mockJsonResponse({ success: true, data: { status: ChatStatusBusy } });
43
+ mockJsonResponse({
44
+ success: true,
45
+ data: { id: 'chat-1', status: ChatStatusBusy, chat_messages: [] },
46
+ });
47
+ mockJsonResponse({ success: true, data: { status: ChatStatusIdle } });
48
+ mockJsonResponse({
49
+ success: true,
50
+ data: { id: 'chat-1', status: ChatStatusIdle, chat_messages: [] },
51
+ });
52
+ const onChat = jest.fn();
53
+ const result = await agent().sendMessage('hello', { stream: false, onChat });
54
+ expect(result.userMessage).toEqual(userMessage);
55
+ expect(result.assistantMessage).toEqual(assistantMessage);
56
+ expect(onChat).toHaveBeenCalledWith(expect.objectContaining({ id: 'chat-1', status: ChatStatusIdle }));
57
+ });
58
+ it('should dispatch onToolCall once per client tool invocation', async () => {
59
+ const toolInvocation = {
60
+ id: 'tool-inv-1',
61
+ type: ToolTypeClient,
62
+ status: ToolInvocationStatusAwaitingInput,
63
+ function: { name: 'my_tool', arguments: { x: 1 } },
64
+ };
65
+ const messageWithTool = makeMessage({ tool_invocations: [toolInvocation] });
66
+ mockJsonResponse({
67
+ success: true,
68
+ data: {
69
+ user_message: makeMessage({ id: 'user-1', role: 'user' }),
70
+ assistant_message: makeMessage(),
71
+ },
72
+ });
73
+ mockJsonResponse({ success: true, data: { status: ChatStatusBusy } });
74
+ mockJsonResponse({
75
+ success: true,
76
+ data: {
77
+ id: 'chat-1',
78
+ status: ChatStatusBusy,
79
+ chat_messages: [messageWithTool],
80
+ },
81
+ });
82
+ // Same status again — stub poll should not re-dispatch tool
83
+ mockJsonResponse({ success: true, data: { status: ChatStatusBusy } });
84
+ mockJsonResponse({ success: true, data: { status: ChatStatusIdle } });
85
+ mockJsonResponse({
86
+ success: true,
87
+ data: { id: 'chat-1', status: ChatStatusIdle, chat_messages: [messageWithTool] },
88
+ });
89
+ const onMessage = jest.fn();
90
+ const onToolCall = jest.fn();
91
+ await agent().sendMessage('run tool', { stream: false, onMessage, onToolCall });
92
+ expect(onToolCall).toHaveBeenCalledTimes(1);
93
+ expect(onToolCall).toHaveBeenCalledWith({
94
+ id: 'tool-inv-1',
95
+ name: 'my_tool',
96
+ args: { x: 1 },
97
+ });
98
+ });
99
+ it('should return chat output from run() after polling completes', async () => {
100
+ const userMessage = makeMessage({ id: 'user-1', role: 'user' });
101
+ const assistantMessage = makeMessage();
102
+ mockJsonResponse({
103
+ success: true,
104
+ data: { user_message: userMessage, assistant_message: assistantMessage },
105
+ });
106
+ mockJsonResponse({ success: true, data: { status: ChatStatusBusy } });
107
+ mockJsonResponse({
108
+ success: true,
109
+ data: { id: 'chat-1', status: ChatStatusBusy },
110
+ });
111
+ mockJsonResponse({ success: true, data: { status: ChatStatusIdle } });
112
+ mockJsonResponse({
113
+ success: true,
114
+ data: { id: 'chat-1', status: ChatStatusIdle },
115
+ });
116
+ mockJsonResponse({
117
+ success: true,
118
+ data: { id: 'chat-1', status: ChatStatusIdle, output: { answer: 42 } },
119
+ });
120
+ const output = await agent().run('compute');
121
+ expect(output).toEqual({ answer: 42 });
122
+ });
123
+ });
124
+ describe('Agent.submitToolResult', () => {
125
+ beforeEach(() => {
126
+ jest.clearAllMocks();
127
+ });
128
+ it('should JSON-stringify structured action results', async () => {
129
+ const http = new HttpClient({ apiKey: 'test-key' });
130
+ const agentInstance = new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
131
+ mockJsonResponse({ success: true, data: null });
132
+ const payload = {
133
+ action: { type: 'form_submit', payload: { field: 'value' } },
134
+ form_data: { field: 'value' },
135
+ };
136
+ await agentInstance.submitToolResult('inv-99', payload);
137
+ const [, init] = mockFetch.mock.calls[0];
138
+ const body = JSON.parse(String(init.body));
139
+ expect(body.result).toBe(JSON.stringify(payload));
140
+ });
141
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ import { HttpClient } from '../http/client';
2
+ import { FilesAPI } from './files';
3
+ const mockFetch = jest.fn();
4
+ global.fetch = mockFetch;
5
+ function mockJsonResponse(body) {
6
+ mockFetch.mockResolvedValueOnce({
7
+ ok: true,
8
+ status: 200,
9
+ text: () => Promise.resolve(JSON.stringify(body)),
10
+ });
11
+ }
12
+ describe('FilesAPI', () => {
13
+ beforeEach(() => {
14
+ jest.clearAllMocks();
15
+ });
16
+ const api = () => new FilesAPI(new HttpClient({ apiKey: 'test-key' }));
17
+ describe('processInput', () => {
18
+ it('should not treat short plain strings as base64 file uploads', async () => {
19
+ const result = await api().processInput({ key: 'key1', note: 'hello' });
20
+ expect(result).toEqual({ key: 'key1', note: 'hello' });
21
+ expect(mockFetch).not.toHaveBeenCalled();
22
+ });
23
+ it('should upload data URIs embedded in nested objects', async () => {
24
+ const fileRecord = {
25
+ id: 'file-1',
26
+ uri: 'inf://files/abc',
27
+ upload_url: 'https://upload.example.com/put',
28
+ content_type: 'image/png',
29
+ };
30
+ mockJsonResponse({ success: true, data: [fileRecord] });
31
+ mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
32
+ const input = {
33
+ prompt: 'draw',
34
+ image: 'data:image/png;base64,iVBORw0KGgo=',
35
+ };
36
+ const result = (await api().processInput(input));
37
+ expect(result.prompt).toBe('draw');
38
+ expect(result.image).toBe('inf://files/abc');
39
+ expect(mockFetch).toHaveBeenCalledTimes(2);
40
+ });
41
+ });
42
+ describe('upload', () => {
43
+ it('should reject invalid data URI format when uploading content', async () => {
44
+ mockJsonResponse({
45
+ success: true,
46
+ data: [{ id: 'file-x', uri: '', upload_url: 'https://upload.example.com/put' }],
47
+ });
48
+ await expect(api().upload('data:invalid')).rejects.toThrow('Invalid data URI format');
49
+ });
50
+ it('should decode URL-safe base64 in data URIs', async () => {
51
+ const fileRecord = {
52
+ id: 'file-2',
53
+ uri: 'inf://files/def',
54
+ upload_url: 'https://upload.example.com/put',
55
+ content_type: 'text/plain',
56
+ };
57
+ mockJsonResponse({ success: true, data: [fileRecord] });
58
+ mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
59
+ // "SGVsbG8" is "Hello" in standard base64; URL-safe variant uses '-' instead of '+'
60
+ const dataUri = 'data:text/plain;base64,SGVsbG8';
61
+ const result = await api().upload(dataUri);
62
+ expect(result.uri).toBe('inf://files/def');
63
+ const putCall = mockFetch.mock.calls[1];
64
+ expect(putCall[0]).toBe('https://upload.example.com/put');
65
+ expect(putCall[1]?.method).toBe('PUT');
66
+ });
67
+ });
68
+ });
@@ -7,12 +7,8 @@ export interface RunOptions {
7
7
  onPartialUpdate?: (update: Task, fields: string[]) => void;
8
8
  /** Wait for task completion (default: true) */
9
9
  wait?: boolean;
10
- /** Auto-reconnect on connection loss (default: true) */
11
- autoReconnect?: boolean;
12
- /** Maximum reconnection attempts (default: 5) */
10
+ /** Maximum retry attempts when using polling mode (stream: false). Default: 5 */
13
11
  maxReconnects?: number;
14
- /** Delay between reconnection attempts in ms (default: 1000) */
15
- reconnectDelayMs?: number;
16
12
  /** Use SSE streaming (true) or polling (false). Overrides client default. */
17
13
  stream?: boolean;
18
14
  /** Polling interval in ms when stream is false. Overrides client default. */
package/dist/api/tasks.js CHANGED
@@ -69,7 +69,7 @@ export class TasksAPI {
69
69
  * Run a task and optionally wait for completion
70
70
  */
71
71
  async run(params, processedInput, options = {}) {
72
- const { onUpdate, onPartialUpdate, wait = true, autoReconnect = true, maxReconnects = 5, reconnectDelayMs = 1000, } = options;
72
+ const { onUpdate, onPartialUpdate, wait = true, } = options;
73
73
  const task = await this.http.request('post', '/apps/run', {
74
74
  data: {
75
75
  ...params,
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,157 @@
1
+ import { HttpClient } from '../http/client';
2
+ import { TaskStatusCancelled, TaskStatusCompleted, TaskStatusFailed, TaskStatusRunning, } from '../types';
3
+ import { TasksAPI } from './tasks';
4
+ const mockFetch = jest.fn();
5
+ global.fetch = mockFetch;
6
+ function mockJsonResponse(body) {
7
+ mockFetch.mockResolvedValueOnce({
8
+ ok: true,
9
+ status: 200,
10
+ text: () => Promise.resolve(JSON.stringify(body)),
11
+ });
12
+ }
13
+ function makeTask(overrides = {}) {
14
+ return {
15
+ id: 'task-1',
16
+ status: TaskStatusRunning,
17
+ created_at: '2026-01-01T00:00:00Z',
18
+ updated_at: '2026-01-01T00:00:00Z',
19
+ input: { prompt: 'hi' },
20
+ output: null,
21
+ logs: [],
22
+ session_id: 'sess-1',
23
+ ...overrides,
24
+ };
25
+ }
26
+ describe('TasksAPI.run (polling mode)', () => {
27
+ beforeEach(() => {
28
+ jest.clearAllMocks();
29
+ });
30
+ const api = () => new TasksAPI(new HttpClient({
31
+ apiKey: 'test-key',
32
+ stream: false,
33
+ pollIntervalMs: 20,
34
+ }));
35
+ it('should resolve when status polling detects completion', async () => {
36
+ const runningTask = makeTask();
37
+ const completedTask = makeTask({ status: TaskStatusCompleted, output: { ok: true } });
38
+ mockJsonResponse({ success: true, data: runningTask });
39
+ mockJsonResponse({ success: true, data: { status: TaskStatusRunning } });
40
+ mockJsonResponse({ success: true, data: { status: TaskStatusCompleted } });
41
+ mockJsonResponse({ success: true, data: completedTask });
42
+ const onUpdate = jest.fn();
43
+ const result = await api().run({ app: 'test-app', input: {} }, { prompt: 'hi' }, { wait: true, stream: false, onUpdate });
44
+ expect(result.status).toBe(TaskStatusCompleted);
45
+ expect(onUpdate).toHaveBeenCalledWith(expect.objectContaining({ id: 'task-1', status: TaskStatusCompleted }));
46
+ });
47
+ it('should reject when polling detects a failed task', async () => {
48
+ const runningTask = makeTask();
49
+ const failedTask = makeTask({ status: TaskStatusFailed, error: 'model error' });
50
+ mockJsonResponse({ success: true, data: runningTask });
51
+ mockJsonResponse({ success: true, data: { status: TaskStatusRunning } });
52
+ mockJsonResponse({ success: true, data: { status: TaskStatusFailed } });
53
+ mockJsonResponse({ success: true, data: failedTask });
54
+ await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true, stream: false })).rejects.toThrow('model error');
55
+ });
56
+ it('should reject when polling detects a cancelled task', async () => {
57
+ const runningTask = makeTask();
58
+ const cancelledTask = makeTask({ status: TaskStatusCancelled });
59
+ mockJsonResponse({ success: true, data: runningTask });
60
+ mockJsonResponse({ success: true, data: { status: TaskStatusRunning } });
61
+ mockJsonResponse({ success: true, data: { status: TaskStatusCancelled } });
62
+ mockJsonResponse({ success: true, data: cancelledTask });
63
+ await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true, stream: false })).rejects.toThrow('task cancelled');
64
+ });
65
+ it('should parse string terminal statuses from the status endpoint', async () => {
66
+ const runningTask = makeTask();
67
+ const completedTask = makeTask({ status: TaskStatusCompleted });
68
+ mockJsonResponse({ success: true, data: runningTask });
69
+ mockJsonResponse({ success: true, data: { status: TaskStatusRunning } });
70
+ mockJsonResponse({ success: true, data: { status: 'completed' } });
71
+ mockJsonResponse({ success: true, data: completedTask });
72
+ const result = await api().run({ app: 'test-app', input: {} }, {}, { wait: true, stream: false });
73
+ expect(result.status).toBe(TaskStatusCompleted);
74
+ });
75
+ });
76
+ function mockNdjsonStream(chunks) {
77
+ let chunkIndex = 0;
78
+ const mockReader = {
79
+ read: jest.fn().mockImplementation(async () => {
80
+ if (chunkIndex >= chunks.length) {
81
+ return { done: true, value: undefined };
82
+ }
83
+ return { done: false, value: new TextEncoder().encode(chunks[chunkIndex++]) };
84
+ }),
85
+ releaseLock: jest.fn(),
86
+ };
87
+ return {
88
+ ok: true,
89
+ status: 200,
90
+ body: { getReader: () => mockReader },
91
+ };
92
+ }
93
+ describe('TasksAPI.run (general)', () => {
94
+ beforeEach(() => {
95
+ jest.clearAllMocks();
96
+ });
97
+ const streamingApi = () => new TasksAPI(new HttpClient({ apiKey: 'test-key', stream: true }));
98
+ it('should return immediately when wait is false', async () => {
99
+ const task = makeTask();
100
+ mockJsonResponse({ success: true, data: task });
101
+ const result = await streamingApi().run({ app: 'test-app', input: {} }, { prompt: 'hi' }, { wait: false });
102
+ expect(result.id).toBe('task-1');
103
+ expect(mockFetch).toHaveBeenCalledTimes(1);
104
+ });
105
+ });
106
+ describe('TasksAPI.run (streaming mode)', () => {
107
+ beforeEach(() => {
108
+ jest.clearAllMocks();
109
+ });
110
+ const api = () => new TasksAPI(new HttpClient({ apiKey: 'test-key', stream: true }));
111
+ function setupStreamMocks(ndjsonChunks, initialTask = makeTask()) {
112
+ mockFetch.mockImplementation((url) => {
113
+ if (url.includes('/apps/run')) {
114
+ return Promise.resolve({
115
+ ok: true,
116
+ status: 200,
117
+ text: () => Promise.resolve(JSON.stringify({ success: true, data: initialTask })),
118
+ });
119
+ }
120
+ return Promise.resolve(mockNdjsonStream(ndjsonChunks));
121
+ });
122
+ }
123
+ it('should resolve when NDJSON stream reports completion', async () => {
124
+ setupStreamMocks([
125
+ `${JSON.stringify({ status: TaskStatusRunning, id: 'task-1' })}\n`,
126
+ `${JSON.stringify({ status: TaskStatusCompleted, id: 'task-1', output: { ok: true } })}\n`,
127
+ ]);
128
+ const onUpdate = jest.fn();
129
+ const result = await api().run({ app: 'test-app', input: {} }, { prompt: 'hi' }, { wait: true, onUpdate });
130
+ expect(result.status).toBe(TaskStatusCompleted);
131
+ expect(onUpdate).toHaveBeenCalled();
132
+ });
133
+ it('should reject when NDJSON stream reports failure', async () => {
134
+ setupStreamMocks([
135
+ `${JSON.stringify({ status: TaskStatusFailed, id: 'task-1', error: 'gpu OOM' })}\n`,
136
+ ]);
137
+ await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true })).rejects.toThrow('gpu OOM');
138
+ });
139
+ it('should reject when NDJSON stream reports cancellation', async () => {
140
+ setupStreamMocks([
141
+ `${JSON.stringify({ status: TaskStatusCancelled, id: 'task-1' })}\n`,
142
+ ]);
143
+ await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true })).rejects.toThrow('task cancelled');
144
+ });
145
+ it('should handle partial updates via onPartialUpdate', async () => {
146
+ setupStreamMocks([
147
+ `${JSON.stringify({
148
+ data: { status: TaskStatusCompleted, id: 'task-1', session_id: 'sess-1' },
149
+ fields: ['status'],
150
+ })}\n`,
151
+ ]);
152
+ const onPartialUpdate = jest.fn();
153
+ const result = await api().run({ app: 'test-app', input: {} }, {}, { wait: true, onPartialUpdate });
154
+ expect(result.status).toBe(TaskStatusCompleted);
155
+ expect(onPartialUpdate).toHaveBeenCalledWith(expect.objectContaining({ id: 'task-1', status: TaskStatusCompleted }), ['status']);
156
+ });
157
+ });
@@ -0,0 +1 @@
1
+ export {};