@inferencesh/sdk 0.6.8 → 0.6.12

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/README.md +70 -1
  3. package/dist/agent/actions.js +3 -3
  4. package/dist/agent/actions.test.js +89 -0
  5. package/dist/agent/api.test.js +86 -8
  6. package/dist/api/agents.d.ts +10 -0
  7. package/dist/api/agents.js +17 -3
  8. package/dist/api/agents.test.js +303 -92
  9. package/dist/api/api-keys.d.ts +24 -0
  10. package/dist/api/api-keys.js +26 -0
  11. package/dist/api/api-keys.test.d.ts +1 -0
  12. package/dist/api/api-keys.test.js +44 -0
  13. package/dist/api/apps.js +1 -1
  14. package/dist/api/apps.test.d.ts +1 -0
  15. package/dist/api/apps.test.js +135 -0
  16. package/dist/api/chats.d.ts +14 -0
  17. package/dist/api/chats.js +18 -0
  18. package/dist/api/chats.test.d.ts +1 -0
  19. package/dist/api/chats.test.js +85 -0
  20. package/dist/api/engines.d.ts +12 -0
  21. package/dist/api/engines.js +18 -0
  22. package/dist/api/engines.test.d.ts +1 -0
  23. package/dist/api/engines.test.js +133 -0
  24. package/dist/api/files.test.js +186 -6
  25. package/dist/api/flow-runs.test.d.ts +1 -0
  26. package/dist/api/flow-runs.test.js +97 -0
  27. package/dist/api/flows.d.ts +4 -0
  28. package/dist/api/flows.js +6 -0
  29. package/dist/api/flows.test.d.ts +1 -0
  30. package/dist/api/flows.test.js +118 -0
  31. package/dist/api/integrations.d.ts +41 -0
  32. package/dist/api/integrations.js +56 -0
  33. package/dist/api/integrations.test.d.ts +1 -0
  34. package/dist/api/integrations.test.js +109 -0
  35. package/dist/api/knowledge.d.ts +112 -0
  36. package/dist/api/knowledge.js +163 -0
  37. package/dist/api/knowledge.test.d.ts +1 -0
  38. package/dist/api/knowledge.test.js +94 -0
  39. package/dist/api/mcp-servers.d.ts +45 -0
  40. package/dist/api/mcp-servers.js +62 -0
  41. package/dist/api/mcp-servers.test.d.ts +1 -0
  42. package/dist/api/mcp-servers.test.js +64 -0
  43. package/dist/api/projects.d.ts +29 -0
  44. package/dist/api/projects.js +38 -0
  45. package/dist/api/projects.test.d.ts +1 -0
  46. package/dist/api/projects.test.js +52 -0
  47. package/dist/api/search.d.ts +21 -0
  48. package/dist/api/search.js +20 -0
  49. package/dist/api/search.test.d.ts +1 -0
  50. package/dist/api/search.test.js +39 -0
  51. package/dist/api/secrets.d.ts +29 -0
  52. package/dist/api/secrets.js +38 -0
  53. package/dist/api/secrets.test.d.ts +1 -0
  54. package/dist/api/secrets.test.js +61 -0
  55. package/dist/api/sessions.test.js +4 -4
  56. package/dist/api/tasks.d.ts +13 -1
  57. package/dist/api/tasks.js +18 -0
  58. package/dist/api/tasks.test.js +174 -22
  59. package/dist/api/teams.d.ts +71 -0
  60. package/dist/api/teams.js +92 -0
  61. package/dist/api/teams.test.d.ts +1 -0
  62. package/dist/api/teams.test.js +79 -0
  63. package/dist/client.test.js +8 -8
  64. package/dist/http/client.d.ts +5 -0
  65. package/dist/http/client.js +46 -48
  66. package/dist/http/client.test.js +296 -13
  67. package/dist/http/errors.test.js +13 -0
  68. package/dist/index.d.ts +25 -0
  69. package/dist/index.js +25 -0
  70. package/dist/proxy/express.test.d.ts +1 -0
  71. package/dist/proxy/express.test.js +106 -0
  72. package/dist/proxy/index.test.js +10 -1
  73. package/dist/tool-builder.test.js +11 -1
  74. package/dist/types.d.ts +793 -23
  75. package/dist/types.js +180 -8
  76. package/package.json +4 -4
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Projects API
3
+ */
4
+ export class ProjectsAPI {
5
+ constructor(http) {
6
+ this.http = http;
7
+ }
8
+ /**
9
+ * List projects with cursor-based pagination
10
+ */
11
+ async list(params) {
12
+ return this.http.request('post', '/projects/list', { data: params });
13
+ }
14
+ /**
15
+ * Get a project by ID
16
+ */
17
+ async get(id) {
18
+ return this.http.request('get', `/projects/${id}`);
19
+ }
20
+ /**
21
+ * Create a project
22
+ */
23
+ async create(data) {
24
+ return this.http.request('post', '/projects', { data });
25
+ }
26
+ /**
27
+ * Update a project
28
+ */
29
+ async update(id, data) {
30
+ return this.http.request('post', `/projects/${id}`, { data });
31
+ }
32
+ /**
33
+ * Delete a project
34
+ */
35
+ async delete(id) {
36
+ return this.http.request('delete', `/projects/${id}`);
37
+ }
38
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ import { HttpClient } from '../http/client';
2
+ import { ProjectsAPI } from './projects';
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('ProjectsAPI', () => {
13
+ beforeEach(() => {
14
+ jest.clearAllMocks();
15
+ });
16
+ const api = () => new ProjectsAPI(new HttpClient({ apiKey: 'test-key' }));
17
+ it('should POST /projects/list for list()', async () => {
18
+ const page = { items: [{ id: 'proj-1' }], next_cursor: null };
19
+ mockJsonResponse(page);
20
+ const result = await api().list({ limit: 20 });
21
+ expect(result).toEqual(page);
22
+ const [url, init] = mockFetch.mock.calls[0];
23
+ expect(url).toContain('/projects/list');
24
+ expect(init.method).toBe('POST');
25
+ });
26
+ it('should GET /projects/{id} for get()', async () => {
27
+ const project = { id: 'proj-1', name: 'Demo' };
28
+ mockJsonResponse(project);
29
+ const result = await api().get('proj-1');
30
+ expect(result).toEqual(project);
31
+ const [url, init] = mockFetch.mock.calls[0];
32
+ expect(url).toContain('/projects/proj-1');
33
+ expect(init.method).toBe('GET');
34
+ });
35
+ it('should POST /projects for create()', async () => {
36
+ const payload = { name: 'New Project' };
37
+ const project = { id: 'proj-new', ...payload };
38
+ mockJsonResponse(project);
39
+ const result = await api().create(payload);
40
+ expect(result).toEqual(project);
41
+ const [url, init] = mockFetch.mock.calls[0];
42
+ expect(url).toContain('/projects');
43
+ expect(init.method).toBe('POST');
44
+ });
45
+ it('should DELETE /projects/{id} for delete()', async () => {
46
+ mockJsonResponse(null);
47
+ await api().delete('proj-1');
48
+ const [url, init] = mockFetch.mock.calls[0];
49
+ expect(url).toContain('/projects/proj-1');
50
+ expect(init.method).toBe('DELETE');
51
+ });
52
+ });
@@ -0,0 +1,21 @@
1
+ import { HttpClient } from '../http/client';
2
+ import { SuggestRequest, SuggestResponse } from '../types';
3
+ /**
4
+ * Search API
5
+ */
6
+ export declare class SearchAPI {
7
+ private readonly http;
8
+ constructor(http: HttpClient);
9
+ /**
10
+ * Unified search across skills, knowledge, and apps
11
+ */
12
+ suggest(params: Partial<SuggestRequest>): Promise<SuggestResponse>;
13
+ /**
14
+ * Full-text search via Meilisearch
15
+ */
16
+ search(params: {
17
+ q: string;
18
+ type?: string;
19
+ limit?: number;
20
+ }): Promise<unknown>;
21
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Search API
3
+ */
4
+ export class SearchAPI {
5
+ constructor(http) {
6
+ this.http = http;
7
+ }
8
+ /**
9
+ * Unified search across skills, knowledge, and apps
10
+ */
11
+ async suggest(params) {
12
+ return this.http.request('post', '/suggest', { data: params });
13
+ }
14
+ /**
15
+ * Full-text search via Meilisearch
16
+ */
17
+ async search(params) {
18
+ return this.http.request('post', '/search', { data: params });
19
+ }
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ import { HttpClient } from '../http/client';
2
+ import { SearchAPI } from './search';
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('SearchAPI', () => {
13
+ beforeEach(() => {
14
+ jest.clearAllMocks();
15
+ });
16
+ const api = () => new SearchAPI(new HttpClient({ apiKey: 'test-key' }));
17
+ it('should POST /suggest for suggest()', async () => {
18
+ const payload = { query: 'image gen', types: ['apps', 'skills'] };
19
+ const response = { results: [{ type: 'app', id: 'app-1' }] };
20
+ mockJsonResponse(response);
21
+ const result = await api().suggest(payload);
22
+ expect(result).toEqual(response);
23
+ const [url, init] = mockFetch.mock.calls[0];
24
+ expect(url).toContain('/suggest');
25
+ expect(init.method).toBe('POST');
26
+ expect(JSON.parse(init.body)).toEqual(payload);
27
+ });
28
+ it('should POST /search for search()', async () => {
29
+ const payload = { q: 'claude', type: 'apps', limit: 5 };
30
+ const response = { hits: [{ id: 'app-1' }] };
31
+ mockJsonResponse(response);
32
+ const result = await api().search(payload);
33
+ expect(result).toEqual(response);
34
+ const [url, init] = mockFetch.mock.calls[0];
35
+ expect(url).toContain('/search');
36
+ expect(init.method).toBe('POST');
37
+ expect(JSON.parse(init.body)).toEqual(payload);
38
+ });
39
+ });
@@ -0,0 +1,29 @@
1
+ import { HttpClient } from '../http/client';
2
+ import { SecretDTO, SecretCreateRequest, CursorListRequest, CursorListResponse } from '../types';
3
+ /**
4
+ * Secrets API
5
+ */
6
+ export declare class SecretsAPI {
7
+ private readonly http;
8
+ constructor(http: HttpClient);
9
+ /**
10
+ * List secrets with cursor-based pagination
11
+ */
12
+ list(params?: Partial<CursorListRequest>): Promise<CursorListResponse<SecretDTO>>;
13
+ /**
14
+ * Create a secret
15
+ */
16
+ create(data: SecretCreateRequest): Promise<SecretDTO>;
17
+ /**
18
+ * Update a secret
19
+ */
20
+ update(key: string, data: Partial<SecretCreateRequest>): Promise<SecretDTO>;
21
+ /**
22
+ * Delete a secret
23
+ */
24
+ delete(key: string): Promise<void>;
25
+ /**
26
+ * Reveal a secret value
27
+ */
28
+ reveal(key: string): Promise<SecretDTO>;
29
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Secrets API
3
+ */
4
+ export class SecretsAPI {
5
+ constructor(http) {
6
+ this.http = http;
7
+ }
8
+ /**
9
+ * List secrets with cursor-based pagination
10
+ */
11
+ async list(params) {
12
+ return this.http.request('post', '/secrets/list', { data: params });
13
+ }
14
+ /**
15
+ * Create a secret
16
+ */
17
+ async create(data) {
18
+ return this.http.request('post', '/secrets', { data });
19
+ }
20
+ /**
21
+ * Update a secret
22
+ */
23
+ async update(key, data) {
24
+ return this.http.request('put', `/secrets/${key}`, { data });
25
+ }
26
+ /**
27
+ * Delete a secret
28
+ */
29
+ async delete(key) {
30
+ return this.http.request('delete', `/secrets/${key}`);
31
+ }
32
+ /**
33
+ * Reveal a secret value
34
+ */
35
+ async reveal(key) {
36
+ return this.http.request('get', `/secrets/reveal/${key}`);
37
+ }
38
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ import { HttpClient } from '../http/client';
2
+ import { SecretsAPI } from './secrets';
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('SecretsAPI', () => {
13
+ beforeEach(() => {
14
+ jest.clearAllMocks();
15
+ });
16
+ const api = () => new SecretsAPI(new HttpClient({ apiKey: 'test-key' }));
17
+ it('should POST /secrets/list for list()', async () => {
18
+ const page = { items: [{ key: 'API_KEY' }], next_cursor: null };
19
+ mockJsonResponse(page);
20
+ const result = await api().list({ limit: 10 });
21
+ expect(result).toEqual(page);
22
+ const [url, init] = mockFetch.mock.calls[0];
23
+ expect(url).toContain('/secrets/list');
24
+ expect(init.method).toBe('POST');
25
+ });
26
+ it('should POST /secrets for create()', async () => {
27
+ const payload = { key: 'DB_PASSWORD', value: 'secret' };
28
+ const secret = { key: 'DB_PASSWORD' };
29
+ mockJsonResponse(secret);
30
+ const result = await api().create(payload);
31
+ expect(result).toEqual(secret);
32
+ const [url, init] = mockFetch.mock.calls[0];
33
+ expect(url).toContain('/secrets');
34
+ expect(init.method).toBe('POST');
35
+ expect(JSON.parse(init.body)).toEqual(payload);
36
+ });
37
+ it('should PUT /secrets/{key} for update()', async () => {
38
+ const secret = { key: 'DB_PASSWORD' };
39
+ mockJsonResponse(secret);
40
+ await api().update('DB_PASSWORD', { value: 'new-secret' });
41
+ const [url, init] = mockFetch.mock.calls[0];
42
+ expect(url).toContain('/secrets/DB_PASSWORD');
43
+ expect(init.method).toBe('PUT');
44
+ });
45
+ it('should GET /secrets/reveal/{key} for reveal()', async () => {
46
+ const secret = { key: 'API_KEY', value: 'sk-live-abc' };
47
+ mockJsonResponse(secret);
48
+ const result = await api().reveal('API_KEY');
49
+ expect(result).toEqual(secret);
50
+ const [url, init] = mockFetch.mock.calls[0];
51
+ expect(url).toContain('/secrets/reveal/API_KEY');
52
+ expect(init.method).toBe('GET');
53
+ });
54
+ it('should DELETE /secrets/{key} for delete()', async () => {
55
+ mockJsonResponse(null);
56
+ await api().delete('OLD_KEY');
57
+ const [url, init] = mockFetch.mock.calls[0];
58
+ expect(url).toContain('/secrets/OLD_KEY');
59
+ expect(init.method).toBe('DELETE');
60
+ });
61
+ });
@@ -16,7 +16,7 @@ describe('SessionsAPI', () => {
16
16
  const api = () => new SessionsAPI(new HttpClient({ apiKey: 'test-key' }));
17
17
  it('should GET /sessions/{id} for get()', async () => {
18
18
  const session = { id: 'sess_1', status: 'active' };
19
- mockJsonResponse({ success: true, data: session });
19
+ mockJsonResponse(session);
20
20
  const result = await api().get('sess_1');
21
21
  expect(result).toEqual(session);
22
22
  const [url, init] = mockFetch.mock.calls[0];
@@ -24,7 +24,7 @@ describe('SessionsAPI', () => {
24
24
  expect(init.method).toBe('GET');
25
25
  });
26
26
  it('should return an empty array when list() response is null', async () => {
27
- mockJsonResponse({ success: true, data: null });
27
+ mockJsonResponse(null);
28
28
  const result = await api().list();
29
29
  expect(result).toEqual([]);
30
30
  const [url] = mockFetch.mock.calls[0];
@@ -32,7 +32,7 @@ describe('SessionsAPI', () => {
32
32
  });
33
33
  it('should POST /sessions/{id}/keepalive for keepalive()', async () => {
34
34
  const session = { id: 'sess_2', status: 'active' };
35
- mockJsonResponse({ success: true, data: session });
35
+ mockJsonResponse(session);
36
36
  const result = await api().keepalive('sess_2');
37
37
  expect(result).toEqual(session);
38
38
  const [url, init] = mockFetch.mock.calls[0];
@@ -40,7 +40,7 @@ describe('SessionsAPI', () => {
40
40
  expect(init.method).toBe('POST');
41
41
  });
42
42
  it('should DELETE /sessions/{id} for end()', async () => {
43
- mockJsonResponse({ success: true, data: null });
43
+ mockJsonResponse(null);
44
44
  await api().end('sess_3');
45
45
  const [url, init] = mockFetch.mock.calls[0];
46
46
  expect(url).toContain('/sessions/sess_3');
@@ -1,5 +1,5 @@
1
1
  import { HttpClient } from '../http/client';
2
- import { TaskDTO as Task, ApiAppRunRequest, CursorListRequest, CursorListResponse } from '../types';
2
+ import { TaskDTO as Task, TaskLogsDTO, TaskTimingsDTO, ApiAppRunRequest, CursorListRequest, CursorListResponse } from '../types';
3
3
  export interface RunOptions {
4
4
  /** Callback for real-time status updates */
5
5
  onUpdate?: (update: Task) => void;
@@ -62,5 +62,17 @@ export declare class TasksAPI {
62
62
  * Feature/unfeature a task
63
63
  */
64
64
  feature(taskId: string, featured: boolean): Promise<Task>;
65
+ /**
66
+ * Get task logs
67
+ */
68
+ getLogs(taskId: string): Promise<TaskLogsDTO>;
69
+ /**
70
+ * Get task timings
71
+ */
72
+ getTimings(taskId: string): Promise<TaskTimingsDTO>;
73
+ /**
74
+ * Get task telemetry
75
+ */
76
+ getTelemetry(taskId: string): Promise<Record<string, unknown>[]>;
65
77
  }
66
78
  export declare function createTasksAPI(http: HttpClient): TasksAPI;
package/dist/api/tasks.js CHANGED
@@ -193,6 +193,24 @@ export class TasksAPI {
193
193
  async feature(taskId, featured) {
194
194
  return this.http.request('post', `/tasks/${taskId}/featured`, { data: { is_featured: featured } });
195
195
  }
196
+ /**
197
+ * Get task logs
198
+ */
199
+ async getLogs(taskId) {
200
+ return this.http.request('get', `/tasks/${taskId}/logs`);
201
+ }
202
+ /**
203
+ * Get task timings
204
+ */
205
+ async getTimings(taskId) {
206
+ return this.http.request('get', `/tasks/${taskId}/timings`);
207
+ }
208
+ /**
209
+ * Get task telemetry
210
+ */
211
+ async getTelemetry(taskId) {
212
+ return this.http.request('get', `/tasks/${taskId}/telemetry`);
213
+ }
196
214
  }
197
215
  export function createTasksAPI(http) {
198
216
  return new TasksAPI(http);
@@ -35,10 +35,10 @@ describe('TasksAPI.run (polling mode)', () => {
35
35
  it('should resolve when status polling detects completion', async () => {
36
36
  const runningTask = makeTask();
37
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 });
38
+ mockJsonResponse(runningTask);
39
+ mockJsonResponse({ status: TaskStatusRunning });
40
+ mockJsonResponse({ status: TaskStatusCompleted });
41
+ mockJsonResponse(completedTask);
42
42
  const onUpdate = jest.fn();
43
43
  const result = await api().run({ app: 'test-app', input: {} }, { prompt: 'hi' }, { wait: true, stream: false, onUpdate });
44
44
  expect(result.status).toBe(TaskStatusCompleted);
@@ -47,42 +47,56 @@ describe('TasksAPI.run (polling mode)', () => {
47
47
  it('should reject when polling detects a failed task', async () => {
48
48
  const runningTask = makeTask();
49
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 });
50
+ mockJsonResponse(runningTask);
51
+ mockJsonResponse({ status: TaskStatusRunning });
52
+ mockJsonResponse({ status: TaskStatusFailed });
53
+ mockJsonResponse(failedTask);
54
54
  await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true, stream: false })).rejects.toThrow('model error');
55
55
  });
56
56
  it('should reject when polling detects a cancelled task', async () => {
57
57
  const runningTask = makeTask();
58
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 });
59
+ mockJsonResponse(runningTask);
60
+ mockJsonResponse({ status: TaskStatusRunning });
61
+ mockJsonResponse({ status: TaskStatusCancelled });
62
+ mockJsonResponse(cancelledTask);
63
63
  await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true, stream: false })).rejects.toThrow('task cancelled');
64
64
  });
65
65
  it('should reject when full task fetch fails after status change', async () => {
66
66
  const runningTask = makeTask();
67
- mockJsonResponse({ success: true, data: runningTask });
68
- mockJsonResponse({ success: true, data: { status: TaskStatusRunning } });
69
- mockJsonResponse({ success: true, data: { status: TaskStatusCompleted } });
67
+ mockJsonResponse(runningTask);
68
+ mockJsonResponse({ status: TaskStatusRunning });
69
+ mockJsonResponse({ status: TaskStatusCompleted });
70
70
  mockFetch.mockRejectedValueOnce(new Error('network down'));
71
71
  await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true, stream: false })).rejects.toThrow('network down');
72
72
  });
73
73
  it('should reject when status polling fails', async () => {
74
74
  const runningTask = makeTask();
75
- mockJsonResponse({ success: true, data: runningTask });
75
+ mockJsonResponse(runningTask);
76
76
  mockFetch.mockRejectedValueOnce(new Error('status endpoint down'));
77
77
  await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true, stream: false })).rejects.toThrow('status endpoint down');
78
78
  });
79
+ it('should not refetch full task when poll status is unchanged', async () => {
80
+ const runningTask = makeTask();
81
+ const completedTask = makeTask({ status: TaskStatusCompleted, output: { ok: true } });
82
+ mockJsonResponse(runningTask);
83
+ mockJsonResponse({ status: TaskStatusRunning });
84
+ mockJsonResponse({ status: TaskStatusRunning });
85
+ mockJsonResponse({ status: TaskStatusCompleted });
86
+ mockJsonResponse(completedTask);
87
+ await api().run({ app: 'test-app', input: {} }, {}, { wait: true, stream: false });
88
+ const fullTaskGets = mockFetch.mock.calls.filter(([url, init]) => String(url).includes('/tasks/task-1') &&
89
+ !String(url).includes('/status') &&
90
+ init.method === 'GET');
91
+ expect(fullTaskGets).toHaveLength(1);
92
+ });
79
93
  it('should parse string terminal statuses from the status endpoint', async () => {
80
94
  const runningTask = makeTask();
81
95
  const completedTask = makeTask({ status: TaskStatusCompleted });
82
- mockJsonResponse({ success: true, data: runningTask });
83
- mockJsonResponse({ success: true, data: { status: TaskStatusRunning } });
84
- mockJsonResponse({ success: true, data: { status: 'completed' } });
85
- mockJsonResponse({ success: true, data: completedTask });
96
+ mockJsonResponse(runningTask);
97
+ mockJsonResponse({ status: TaskStatusRunning });
98
+ mockJsonResponse({ status: 'completed' });
99
+ mockJsonResponse(completedTask);
86
100
  const result = await api().run({ app: 'test-app', input: {} }, {}, { wait: true, stream: false });
87
101
  expect(result.status).toBe(TaskStatusCompleted);
88
102
  });
@@ -111,7 +125,7 @@ describe('TasksAPI.run (general)', () => {
111
125
  const streamingApi = () => new TasksAPI(new HttpClient({ apiKey: 'test-key', stream: true }));
112
126
  it('should return immediately when wait is false', async () => {
113
127
  const task = makeTask();
114
- mockJsonResponse({ success: true, data: task });
128
+ mockJsonResponse(task);
115
129
  const result = await streamingApi().run({ app: 'test-app', input: {} }, { prompt: 'hi' }, { wait: false });
116
130
  expect(result.id).toBe('task-1');
117
131
  expect(mockFetch).toHaveBeenCalledTimes(1);
@@ -128,7 +142,7 @@ describe('TasksAPI.run (streaming mode)', () => {
128
142
  return Promise.resolve({
129
143
  ok: true,
130
144
  status: 200,
131
- text: () => Promise.resolve(JSON.stringify({ success: true, data: initialTask })),
145
+ text: () => Promise.resolve(JSON.stringify(initialTask)),
132
146
  });
133
147
  }
134
148
  return Promise.resolve(mockNdjsonStream(ndjsonChunks));
@@ -156,6 +170,24 @@ describe('TasksAPI.run (streaming mode)', () => {
156
170
  ]);
157
171
  await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true })).rejects.toThrow('task cancelled');
158
172
  });
173
+ it('should reject when partial stream reports failure', async () => {
174
+ setupStreamMocks([
175
+ `${JSON.stringify({
176
+ data: { status: TaskStatusFailed, id: 'task-1', error: 'partial fail' },
177
+ fields: ['status'],
178
+ })}\n`,
179
+ ]);
180
+ await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true })).rejects.toThrow('partial fail');
181
+ });
182
+ it('should reject when partial stream reports cancellation', async () => {
183
+ setupStreamMocks([
184
+ `${JSON.stringify({
185
+ data: { status: TaskStatusCancelled, id: 'task-1' },
186
+ fields: ['status'],
187
+ })}\n`,
188
+ ]);
189
+ await expect(api().run({ app: 'test-app', input: {} }, {}, { wait: true })).rejects.toThrow('task cancelled');
190
+ });
159
191
  it('should handle partial updates via onPartialUpdate', async () => {
160
192
  setupStreamMocks([
161
193
  `${JSON.stringify({
@@ -169,3 +201,123 @@ describe('TasksAPI.run (streaming mode)', () => {
169
201
  expect(onPartialUpdate).toHaveBeenCalledWith(expect.objectContaining({ id: 'task-1', status: TaskStatusCompleted }), ['status']);
170
202
  });
171
203
  });
204
+ describe('TasksAPI.create', () => {
205
+ beforeEach(() => {
206
+ jest.clearAllMocks();
207
+ });
208
+ const api = () => new TasksAPI(new HttpClient({ apiKey: 'test-key' }));
209
+ it('should POST /apps/run for create()', async () => {
210
+ const task = makeTask();
211
+ mockJsonResponse(task);
212
+ const result = await api().create({ app: 'test-app', input: { prompt: 'hi' } });
213
+ expect(result).toEqual(task);
214
+ const [url, init] = mockFetch.mock.calls[0];
215
+ expect(url).toContain('/apps/run');
216
+ expect(init.method).toBe('POST');
217
+ expect(JSON.parse(init.body)).toEqual({
218
+ app: 'test-app',
219
+ input: { prompt: 'hi' },
220
+ });
221
+ });
222
+ });
223
+ describe('TasksAPI (CRUD and admin)', () => {
224
+ beforeEach(() => {
225
+ jest.clearAllMocks();
226
+ });
227
+ const api = () => new TasksAPI(new HttpClient({ apiKey: 'test-key' }));
228
+ it('should POST /tasks/list for list()', async () => {
229
+ const page = { items: [{ id: 'task-1' }], next_cursor: null };
230
+ mockJsonResponse(page);
231
+ const result = await api().list({ limit: 5 });
232
+ expect(result).toEqual(page);
233
+ const [url, init] = mockFetch.mock.calls[0];
234
+ expect(url).toContain('/tasks/list');
235
+ expect(init.method).toBe('POST');
236
+ expect(JSON.parse(init.body)).toEqual({ limit: 5 });
237
+ });
238
+ it('should GET /tasks/featured for listFeatured()', async () => {
239
+ const page = { items: [{ id: 'task-f' }], next_cursor: null };
240
+ mockJsonResponse(page);
241
+ const result = await api().listFeatured({ cursor: 'abc' });
242
+ expect(result).toEqual(page);
243
+ const [url, init] = mockFetch.mock.calls[0];
244
+ expect(url).toContain('/tasks/featured');
245
+ expect(init.method).toBe('GET');
246
+ });
247
+ it('should GET /tasks/{id} for get()', async () => {
248
+ const task = makeTask();
249
+ mockJsonResponse(task);
250
+ const result = await api().get('task-1');
251
+ expect(result).toEqual(task);
252
+ const [url, init] = mockFetch.mock.calls[0];
253
+ expect(url).toContain('/tasks/task-1');
254
+ expect(init.method).toBe('GET');
255
+ });
256
+ it('should DELETE /tasks/{id} for delete()', async () => {
257
+ mockJsonResponse(null);
258
+ await api().delete('task-1');
259
+ const [url, init] = mockFetch.mock.calls[0];
260
+ expect(url).toContain('/tasks/task-1');
261
+ expect(init.method).toBe('DELETE');
262
+ });
263
+ it('should POST /tasks/{id}/cancel for cancel()', async () => {
264
+ mockJsonResponse(null);
265
+ await api().cancel('task-1');
266
+ const [url, init] = mockFetch.mock.calls[0];
267
+ expect(url).toContain('/tasks/task-1/cancel');
268
+ expect(init.method).toBe('POST');
269
+ });
270
+ it('should POST visibility for updateVisibility()', async () => {
271
+ const task = makeTask({ visibility: 'public' });
272
+ mockJsonResponse(task);
273
+ const result = await api().updateVisibility('task-1', 'public');
274
+ expect(result).toEqual(task);
275
+ const [, init] = mockFetch.mock.calls[0];
276
+ expect(JSON.parse(init.body)).toEqual({ visibility: 'public' });
277
+ });
278
+ it('should POST is_featured for feature()', async () => {
279
+ const task = makeTask({ is_featured: true });
280
+ mockJsonResponse(task);
281
+ await api().feature('task-1', true);
282
+ const [url, init] = mockFetch.mock.calls[0];
283
+ expect(url).toContain('/tasks/task-1/featured');
284
+ expect(JSON.parse(init.body)).toEqual({ is_featured: true });
285
+ });
286
+ it('should open SSE on /tasks/{id}/stream for stream()', async () => {
287
+ const http = new HttpClient({ apiKey: 'test-key' });
288
+ const createEventSource = jest
289
+ .spyOn(http, 'createEventSource')
290
+ .mockResolvedValue(null);
291
+ const tasks = new TasksAPI(http);
292
+ await tasks.stream('task-42');
293
+ expect(createEventSource).toHaveBeenCalledWith('/tasks/task-42/stream');
294
+ createEventSource.mockRestore();
295
+ });
296
+ it('should GET /tasks/{id}/logs for getLogs()', async () => {
297
+ const logs = { logs: [{ message: 'started', timestamp: '2026-01-01T00:00:00Z' }] };
298
+ mockJsonResponse(logs);
299
+ const result = await api().getLogs('task-1');
300
+ expect(result).toEqual(logs);
301
+ const [url, init] = mockFetch.mock.calls[0];
302
+ expect(url).toContain('/tasks/task-1/logs');
303
+ expect(init.method).toBe('GET');
304
+ });
305
+ it('should GET /tasks/{id}/timings for getTimings()', async () => {
306
+ const timings = { total_ms: 1200, stages: [{ name: 'inference', ms: 900 }] };
307
+ mockJsonResponse(timings);
308
+ const result = await api().getTimings('task-1');
309
+ expect(result).toEqual(timings);
310
+ const [url, init] = mockFetch.mock.calls[0];
311
+ expect(url).toContain('/tasks/task-1/timings');
312
+ expect(init.method).toBe('GET');
313
+ });
314
+ it('should GET /tasks/{id}/telemetry for getTelemetry()', async () => {
315
+ const telemetry = [{ metric: 'gpu_util', value: 0.82 }];
316
+ mockJsonResponse(telemetry);
317
+ const result = await api().getTelemetry('task-1');
318
+ expect(result).toEqual(telemetry);
319
+ const [url, init] = mockFetch.mock.calls[0];
320
+ expect(url).toContain('/tasks/task-1/telemetry');
321
+ expect(init.method).toBe('GET');
322
+ });
323
+ });