@inferencesh/sdk 0.4.26 → 0.5.1

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.
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Sessions integration tests for @inferencesh/sdk
3
+ *
4
+ * These tests hit the real API and require INFERENCE_API_KEY to be set.
5
+ * Run with: npm run test:integration
6
+ *
7
+ * Prerequisites:
8
+ * - API and scheduler must be running
9
+ * - Test app deployed: infsh/session-test
10
+ *
11
+ * @jest-environment node
12
+ */
13
+ import { Inference } from './index';
14
+ import { TaskStatusCompleted } from './types';
15
+ const API_KEY = process.env.INFERENCE_API_KEY;
16
+ const BASE_URL = process.env.INFERENCE_BASE_URL || 'https://api.inference.sh';
17
+ // Session test app with multi-function support
18
+ const SESSION_TEST_APP = 'infsh/session-test';
19
+ const describeIfApiKey = API_KEY ? describe : describe.skip;
20
+ describeIfApiKey('Sessions Integration Tests', () => {
21
+ let client;
22
+ beforeAll(() => {
23
+ client = new Inference({
24
+ apiKey: API_KEY,
25
+ baseUrl: BASE_URL,
26
+ });
27
+ });
28
+ // ==========================================================================
29
+ // Session Creation Tests
30
+ // ==========================================================================
31
+ describe('Session Creation', () => {
32
+ it('should create a new session and return real session ID', async () => {
33
+ const result = await client.run({
34
+ app: SESSION_TEST_APP,
35
+ function: 'set_value',
36
+ input: { key: 'test_create', value: 'hello' },
37
+ session: 'new',
38
+ });
39
+ expect(result.status).toBe(TaskStatusCompleted);
40
+ expect(result.session_id).toBeDefined();
41
+ expect(result.session_id).not.toBe('new');
42
+ expect(result.session_id).toMatch(/^sess_/);
43
+ }, 60000);
44
+ it('should return correct output data', async () => {
45
+ const result = await client.run({
46
+ app: SESSION_TEST_APP,
47
+ function: 'set_value',
48
+ input: { key: 'output_test', value: 'test_value' },
49
+ session: 'new',
50
+ });
51
+ expect(result.status).toBe(TaskStatusCompleted);
52
+ const output = result.output;
53
+ expect(output.success).toBe(true);
54
+ expect(output.key).toBe('output_test');
55
+ expect(output.value).toBe('test_value');
56
+ }, 60000);
57
+ });
58
+ // ==========================================================================
59
+ // Session Continuity Tests
60
+ // ==========================================================================
61
+ describe('Session Continuity', () => {
62
+ it('should persist state across calls in same session', async () => {
63
+ // Create session with initial value
64
+ const result1 = await client.run({
65
+ app: SESSION_TEST_APP,
66
+ function: 'set_value',
67
+ input: { key: 'persist_key', value: 'persist_value' },
68
+ session: 'new',
69
+ });
70
+ expect(result1.status).toBe(TaskStatusCompleted);
71
+ const sessionId = result1.session_id;
72
+ expect(sessionId).toMatch(/^sess_/);
73
+ // Retrieve value from same session
74
+ const result2 = await client.run({
75
+ app: SESSION_TEST_APP,
76
+ function: 'get_value',
77
+ input: { key: 'persist_key' },
78
+ session: sessionId,
79
+ });
80
+ expect(result2.status).toBe(TaskStatusCompleted);
81
+ expect(result2.session_id).toBe(sessionId);
82
+ const output = result2.output;
83
+ expect(output.found).toBe(true);
84
+ expect(output.value).toBe('persist_value');
85
+ }, 120000);
86
+ it('should accumulate multiple values in session', async () => {
87
+ // Create session
88
+ const result1 = await client.run({
89
+ app: SESSION_TEST_APP,
90
+ function: 'set_value',
91
+ input: { key: 'key1', value: 'value1' },
92
+ session: 'new',
93
+ });
94
+ expect(result1.status).toBe(TaskStatusCompleted);
95
+ const sessionId = result1.session_id;
96
+ // Add second value
97
+ const result2 = await client.run({
98
+ app: SESSION_TEST_APP,
99
+ function: 'set_value',
100
+ input: { key: 'key2', value: 'value2' },
101
+ session: sessionId,
102
+ });
103
+ expect(result2.status).toBe(TaskStatusCompleted);
104
+ // Get all state
105
+ const result3 = await client.run({
106
+ app: SESSION_TEST_APP,
107
+ function: 'get_all',
108
+ input: {},
109
+ session: sessionId,
110
+ });
111
+ expect(result3.status).toBe(TaskStatusCompleted);
112
+ const output = result3.output;
113
+ expect(output.state.key1).toBe('value1');
114
+ expect(output.state.key2).toBe('value2');
115
+ }, 120000);
116
+ });
117
+ // ==========================================================================
118
+ // Multi-Function Routing Tests
119
+ // ==========================================================================
120
+ describe('Multi-Function Routing', () => {
121
+ it('should route to increment function correctly', async () => {
122
+ const result = await client.run({
123
+ app: SESSION_TEST_APP,
124
+ function: 'increment',
125
+ input: { key: 'counter', amount: 5 },
126
+ session: 'new',
127
+ });
128
+ expect(result.status).toBe(TaskStatusCompleted);
129
+ const output = result.output;
130
+ expect(output.key).toBe('counter');
131
+ expect(output.previous).toBe(0);
132
+ expect(output.current).toBe(5);
133
+ }, 60000);
134
+ it('should handle different input schemas per function', async () => {
135
+ // Create session
136
+ const result1 = await client.run({
137
+ app: SESSION_TEST_APP,
138
+ function: 'set_value',
139
+ input: { key: 'schema_test', value: 'v1' },
140
+ session: 'new',
141
+ });
142
+ expect(result1.status).toBe(TaskStatusCompleted);
143
+ const sessionId = result1.session_id;
144
+ // get_value has different schema (no 'value' field)
145
+ const result2 = await client.run({
146
+ app: SESSION_TEST_APP,
147
+ function: 'get_value',
148
+ input: { key: 'schema_test' },
149
+ session: sessionId,
150
+ });
151
+ expect(result2.status).toBe(TaskStatusCompleted);
152
+ // increment has different schema (amount field)
153
+ const result3 = await client.run({
154
+ app: SESSION_TEST_APP,
155
+ function: 'increment',
156
+ input: { key: 'cnt', amount: 10 },
157
+ session: sessionId,
158
+ });
159
+ expect(result3.status).toBe(TaskStatusCompleted);
160
+ // get_all has empty schema
161
+ const result4 = await client.run({
162
+ app: SESSION_TEST_APP,
163
+ function: 'get_all',
164
+ input: {},
165
+ session: sessionId,
166
+ });
167
+ expect(result4.status).toBe(TaskStatusCompleted);
168
+ }, 120000);
169
+ });
170
+ // ==========================================================================
171
+ // Session Isolation Tests
172
+ // ==========================================================================
173
+ describe('Session Isolation', () => {
174
+ it('should create different sessions for different "new" requests', async () => {
175
+ // Create first session
176
+ const result1 = await client.run({
177
+ app: SESSION_TEST_APP,
178
+ function: 'set_value',
179
+ input: { key: 'isolated', value: 'session1' },
180
+ session: 'new',
181
+ });
182
+ expect(result1.status).toBe(TaskStatusCompleted);
183
+ const session1 = result1.session_id;
184
+ // Create second session
185
+ const result2 = await client.run({
186
+ app: SESSION_TEST_APP,
187
+ function: 'set_value',
188
+ input: { key: 'isolated', value: 'session2' },
189
+ session: 'new',
190
+ });
191
+ expect(result2.status).toBe(TaskStatusCompleted);
192
+ const session2 = result2.session_id;
193
+ // Sessions should be different
194
+ expect(session1).not.toBe(session2);
195
+ }, 120000);
196
+ });
197
+ // ==========================================================================
198
+ // Sessions API Tests
199
+ // ==========================================================================
200
+ describe('Sessions API', () => {
201
+ it('should get session info', async () => {
202
+ // Create a session
203
+ const result = await client.run({
204
+ app: SESSION_TEST_APP,
205
+ function: 'set_value',
206
+ input: { key: 'api_test', value: 'hello' },
207
+ session: 'new',
208
+ });
209
+ expect(result.status).toBe(TaskStatusCompleted);
210
+ const sessionId = result.session_id;
211
+ // Get session info via API
212
+ const sessionInfo = await client.sessions.get(sessionId);
213
+ // API returns flattened structure (Go embedded structs)
214
+ expect(sessionInfo.id).toBe(sessionId);
215
+ expect(sessionInfo.status).toBe('active');
216
+ expect(sessionInfo.call_count).toBeGreaterThanOrEqual(1);
217
+ }, 60000);
218
+ it('should list sessions', async () => {
219
+ const sessions = await client.sessions.list();
220
+ expect(Array.isArray(sessions)).toBe(true);
221
+ // Should have at least one session from previous tests
222
+ }, 30000);
223
+ it('should keepalive a session', async () => {
224
+ // Create a session
225
+ const result = await client.run({
226
+ app: SESSION_TEST_APP,
227
+ function: 'set_value',
228
+ input: { key: 'keepalive_test', value: 'hello' },
229
+ session: 'new',
230
+ });
231
+ expect(result.status).toBe(TaskStatusCompleted);
232
+ const sessionId = result.session_id;
233
+ // Keepalive
234
+ const updated = await client.sessions.keepalive(sessionId);
235
+ // API returns flattened structure (Go embedded structs)
236
+ expect(updated.id).toBe(sessionId);
237
+ expect(updated.status).toBe('active');
238
+ }, 60000);
239
+ it('should end a session', async () => {
240
+ // Create a session
241
+ const result = await client.run({
242
+ app: SESSION_TEST_APP,
243
+ function: 'set_value',
244
+ input: { key: 'end_test', value: 'goodbye' },
245
+ session: 'new',
246
+ });
247
+ expect(result.status).toBe(TaskStatusCompleted);
248
+ const sessionId = result.session_id;
249
+ // End the session
250
+ await client.sessions.end(sessionId);
251
+ // Trying to use ended session should fail
252
+ await expect(client.run({
253
+ app: SESSION_TEST_APP,
254
+ function: 'get_value',
255
+ input: { key: 'end_test' },
256
+ session: sessionId,
257
+ })).rejects.toThrow();
258
+ }, 60000);
259
+ });
260
+ // ==========================================================================
261
+ // Error Cases
262
+ // ==========================================================================
263
+ describe('Error Cases', () => {
264
+ it('should fail with invalid session ID', async () => {
265
+ await expect(client.run({
266
+ app: SESSION_TEST_APP,
267
+ function: 'get_value',
268
+ input: { key: 'foo' },
269
+ session: 'sess_invalid_does_not_exist_12345',
270
+ })).rejects.toThrow();
271
+ }, 30000);
272
+ });
273
+ // ==========================================================================
274
+ // Performance Tests
275
+ // ==========================================================================
276
+ describe('Performance', () => {
277
+ it('should be faster on warm worker', async () => {
278
+ // Create session (cold start)
279
+ const result1 = await client.run({
280
+ app: SESSION_TEST_APP,
281
+ function: 'set_value',
282
+ input: { key: 'perf_test', value: '1' },
283
+ session: 'new',
284
+ });
285
+ expect(result1.status).toBe(TaskStatusCompleted);
286
+ const sessionId = result1.session_id;
287
+ // Second call should be fast (warm worker)
288
+ const start = Date.now();
289
+ const result2 = await client.run({
290
+ app: SESSION_TEST_APP,
291
+ function: 'set_value',
292
+ input: { key: 'perf_test', value: '2' },
293
+ session: sessionId,
294
+ });
295
+ const elapsed = Date.now() - start;
296
+ expect(result2.status).toBe(TaskStatusCompleted);
297
+ // Log for visibility
298
+ console.log(`Warm worker call took: ${elapsed}ms`);
299
+ // Should be under 2 seconds for warm worker
300
+ expect(elapsed).toBeLessThan(2000);
301
+ }, 120000);
302
+ });
303
+ });
304
+ // Placeholder test that always runs
305
+ describe('Sessions Integration Test Setup', () => {
306
+ it('should have API key check', () => {
307
+ if (!API_KEY) {
308
+ console.log('⚠️ Skipping sessions integration tests - INFERENCE_API_KEY not set');
309
+ }
310
+ expect(true).toBe(true);
311
+ });
312
+ });
@@ -54,11 +54,17 @@ declare class AppToolBuilder extends ToolBuilder {
54
54
  private appRef;
55
55
  private setupValues?;
56
56
  private inputValues?;
57
+ private functionName?;
58
+ private sessionEnabledValue;
57
59
  constructor(name: string, appRef: string);
58
60
  /** Set one-time setup values (hidden from agent, passed on every call) */
59
61
  setup(values: Record<string, unknown>): this;
60
62
  /** Set default input values (agent can override these) */
61
63
  input(values: Record<string, unknown>): this;
64
+ /** Set which function to call on multi-function apps */
65
+ function(name: string): this;
66
+ /** Enable session support (agent can pass session parameter and sees session_id in output) */
67
+ sessionEnabled(): this;
62
68
  build(): AgentTool;
63
69
  }
64
70
  declare class AgentToolBuilder extends ToolBuilder {
@@ -94,6 +94,7 @@ class ClientToolBuilder extends ToolBuilder {
94
94
  class AppToolBuilder extends ToolBuilder {
95
95
  constructor(name, appRef) {
96
96
  super(name);
97
+ this.sessionEnabledValue = false;
97
98
  this.appRef = appRef;
98
99
  }
99
100
  /** Set one-time setup values (hidden from agent, passed on every call) */
@@ -106,6 +107,16 @@ class AppToolBuilder extends ToolBuilder {
106
107
  this.inputValues = values;
107
108
  return this;
108
109
  }
110
+ /** Set which function to call on multi-function apps */
111
+ function(name) {
112
+ this.functionName = name;
113
+ return this;
114
+ }
115
+ /** Enable session support (agent can pass session parameter and sees session_id in output) */
116
+ sessionEnabled() {
117
+ this.sessionEnabledValue = true;
118
+ return this;
119
+ }
109
120
  build() {
110
121
  return {
111
122
  name: this.name,
@@ -115,6 +126,8 @@ class AppToolBuilder extends ToolBuilder {
115
126
  require_approval: this.approval || undefined,
116
127
  app: {
117
128
  ref: this.appRef,
129
+ function: this.functionName,
130
+ session_enabled: this.sessionEnabledValue || undefined,
118
131
  setup: this.setupValues,
119
132
  input: this.inputValues,
120
133
  },
@@ -181,6 +181,36 @@ describe('AppToolBuilder (appTool)', () => {
181
181
  expect(t.name).toBe('fetch');
182
182
  // Note: App tools don't use client input_schema, params are for documentation
183
183
  });
184
+ it('supports function selection for multi-function apps', () => {
185
+ const t = appTool('upscale', 'infsh/sdxl@v1')
186
+ .function('upscale')
187
+ .describe('Upscale an image')
188
+ .build();
189
+ expect(t.app?.function).toBe('upscale');
190
+ });
191
+ it('supports session enabled for stateful apps', () => {
192
+ const t = appTool('browser', 'infsh/browser-use@v1')
193
+ .sessionEnabled()
194
+ .describe('Control a browser')
195
+ .build();
196
+ expect(t.app?.session_enabled).toBe(true);
197
+ });
198
+ it('supports full configuration with function and session', () => {
199
+ const t = appTool('chat', 'infsh/chatbot@v1')
200
+ .describe('Chat with context')
201
+ .function('stream')
202
+ .sessionEnabled()
203
+ .setup({ api_key: 'secret' })
204
+ .input({ temperature: 0.7 })
205
+ .build();
206
+ expect(t.app).toEqual({
207
+ ref: 'infsh/chatbot@v1',
208
+ function: 'stream',
209
+ session_enabled: true,
210
+ setup: { api_key: 'secret' },
211
+ input: { temperature: 0.7 },
212
+ });
213
+ });
184
214
  });
185
215
  describe('AgentToolBuilder (agentTool)', () => {
186
216
  it('creates agent tool with reference', () => {