valenceai 0.4.0

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,331 @@
1
+ import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals';
2
+ import nock from 'nock';
3
+ import fs from 'fs';
4
+ import { ValenceClient } from '../src/valenceClient.js';
5
+
6
+ describe('AsyncAudio', () => {
7
+ const originalEnv = process.env;
8
+ let fsMock, statMock;
9
+
10
+ beforeEach(() => {
11
+ process.env = { ...originalEnv };
12
+ process.env.VALENCE_API_KEY = 'test-api-key';
13
+ process.env.VALENCE_ASYNC_URL = 'https://test-api.com';
14
+
15
+ fsMock = jest.spyOn(fs, 'existsSync');
16
+ statMock = jest.spyOn(fs, 'statSync');
17
+
18
+ // Mock createReadStream to simulate file chunks
19
+ jest.spyOn(fs, 'createReadStream').mockReturnValue({
20
+ [Symbol.asyncIterator]: async function* () {
21
+ yield Buffer.from('chunk1');
22
+ yield Buffer.from('chunk2');
23
+ }
24
+ });
25
+ });
26
+
27
+ afterEach(() => {
28
+ process.env = originalEnv;
29
+ jest.restoreAllMocks();
30
+ nock.cleanAll();
31
+ });
32
+
33
+ describe('uploadAsyncAudio', () => {
34
+ test('should successfully upload async audio', async () => {
35
+ fsMock.mockReturnValue(true);
36
+ statMock.mockReturnValue({ size: 10485760 }); // 10MB
37
+
38
+ const mockInitiateResponse = {
39
+ request_id: 'req-123',
40
+ upload_id: 'upload-123',
41
+ presigned_urls: [
42
+ { part_number: 1, url: 'https://s3.test.com/part1' },
43
+ { part_number: 2, url: 'https://s3.test.com/part2' }
44
+ ]
45
+ };
46
+
47
+ // Mock initiate request
48
+ nock('https://test-api.com')
49
+ .get('/upload/initiate')
50
+ .query({ file_name: 'test.wav', part_count: 2 })
51
+ .reply(200, mockInitiateResponse);
52
+
53
+ // Mock S3 uploads
54
+ nock('https://s3.test.com')
55
+ .put('/part1')
56
+ .reply(200, '', { etag: '"etag1"' });
57
+
58
+ nock('https://s3.test.com')
59
+ .put('/part2')
60
+ .reply(200, '', { etag: '"etag2"' });
61
+
62
+ // Mock complete request
63
+ nock('https://test-api.com')
64
+ .post('/upload/complete', {
65
+ request_id: 'req-123',
66
+ upload_id: 'upload-123',
67
+ parts: [
68
+ { ETag: '"etag1"', PartNumber: 1 },
69
+ { ETag: '"etag2"', PartNumber: 2 }
70
+ ]
71
+ })
72
+ .reply(200);
73
+
74
+ const client = new ValenceClient();
75
+ const result = await client.asynch.upload('test.wav');
76
+
77
+ expect(result).toBe('req-123');
78
+ });
79
+
80
+ test('should throw error when filePath is missing', async () => {
81
+ const client = new ValenceClient();
82
+ await expect(client.asynch.upload()).rejects.toThrow(
83
+ 'filePath is required and must be a string'
84
+ );
85
+ });
86
+
87
+ test('should throw error when filePath is not a string', async () => {
88
+ const client = new ValenceClient();
89
+ await expect(client.asynch.upload(123)).rejects.toThrow(
90
+ 'filePath is required and must be a string'
91
+ );
92
+ });
93
+
94
+ test('should throw error when API key is missing', async () => {
95
+ delete process.env.VALENCE_API_KEY;
96
+
97
+ const client = new ValenceClient();
98
+ await expect(client.asynch.upload('test.wav')).rejects.toThrow(
99
+ 'VALENCE_API_KEY is required'
100
+ );
101
+ });
102
+
103
+ test('should throw error when file does not exist', async () => {
104
+ fsMock.mockReturnValue(false);
105
+
106
+ const client = new ValenceClient();
107
+ await expect(client.asynch.upload('nonexistent.wav')).rejects.toThrow(
108
+ 'File not found: nonexistent.wav'
109
+ );
110
+ });
111
+
112
+ test('should throw error with invalid partSize', async () => {
113
+ fsMock.mockReturnValue(true);
114
+
115
+ const client = new ValenceClient();
116
+ await expect(client.asynch.upload('test.wav', 500000)).rejects.toThrow(
117
+ 'partSize must be between 1MB and 100MB'
118
+ );
119
+
120
+ await expect(client.asynch.upload('test.wav', 200 * 1024 * 1024)).rejects.toThrow(
121
+ 'partSize must be between 1MB and 100MB'
122
+ );
123
+ });
124
+
125
+ test('should handle missing presigned URLs', async () => {
126
+ fsMock.mockReturnValue(true);
127
+ statMock.mockReturnValue({ size: 5242880 }); // 5MB
128
+
129
+ nock('https://test-api.com')
130
+ .get('/upload/initiate')
131
+ .reply(200, {
132
+ request_id: 'req-123',
133
+ upload_id: 'upload-123',
134
+ presigned_urls: [] // Empty array
135
+ });
136
+
137
+ const client = new ValenceClient();
138
+ await expect(client.asynch.upload('test.wav')).rejects.toThrow(
139
+ 'Missing presigned URL for part 1'
140
+ );
141
+ });
142
+
143
+ test('should handle missing ETag in response', async () => {
144
+ fsMock.mockReturnValue(true);
145
+ statMock.mockReturnValue({ size: 5242880 }); // 5MB
146
+
147
+ nock('https://test-api.com')
148
+ .get('/upload/initiate')
149
+ .reply(200, {
150
+ request_id: 'req-123',
151
+ upload_id: 'upload-123',
152
+ presigned_urls: [
153
+ { part_number: 1, url: 'https://s3.test.com/part1' }
154
+ ]
155
+ });
156
+
157
+ nock('https://s3.test.com')
158
+ .put('/part1')
159
+ .reply(200, ''); // No ETag header
160
+
161
+ const client = new ValenceClient();
162
+ await expect(client.asynch.upload('test.wav')).rejects.toThrow(
163
+ 'Failed to get ETag for part 1'
164
+ );
165
+ });
166
+
167
+ test('should handle API errors', async () => {
168
+ fsMock.mockReturnValue(true);
169
+ statMock.mockReturnValue({ size: 5242880 });
170
+
171
+ nock('https://test-api.com')
172
+ .get('/upload/initiate')
173
+ .reply(400, { message: 'Invalid request' });
174
+
175
+ const client = new ValenceClient();
176
+ await expect(client.asynch.upload('test.wav')).rejects.toThrow(
177
+ 'API error (400): Invalid request'
178
+ );
179
+ });
180
+ });
181
+
182
+ describe('getEmotions', () => {
183
+ test('should successfully get emotions', async () => {
184
+ const mockResponse = {
185
+ emotions: {
186
+ happy: 0.7,
187
+ sad: 0.2,
188
+ angry: 0.1
189
+ }
190
+ };
191
+
192
+ nock('https://test-api.com')
193
+ .get('/prediction')
194
+ .query({ request_id: 'req-123' })
195
+ .reply(200, mockResponse);
196
+
197
+ const client = new ValenceClient();
198
+ const result = await client.asynch.emotions('req-123');
199
+
200
+ expect(result).toEqual(mockResponse);
201
+ });
202
+
203
+ test('should poll until success', async () => {
204
+ const mockResponse = { emotions: { happy: 1.0 } };
205
+
206
+ nock('https://test-api.com')
207
+ .get('/prediction')
208
+ .query({ request_id: 'req-123' })
209
+ .reply(202) // Processing
210
+ .get('/prediction')
211
+ .query({ request_id: 'req-123' })
212
+ .reply(200, mockResponse); // Success
213
+
214
+ const client = new ValenceClient();
215
+ const result = await client.asynch.emotions('req-123', 5, 100); // Short interval for testing
216
+
217
+ expect(result).toEqual(mockResponse);
218
+ });
219
+
220
+ test('should throw error when requestId is missing', async () => {
221
+ const client = new ValenceClient();
222
+ await expect(client.asynch.emotions()).rejects.toThrow(
223
+ 'requestId is required and must be a string'
224
+ );
225
+ });
226
+
227
+ test('should throw error when requestId is not a string', async () => {
228
+ const client = new ValenceClient();
229
+ await expect(client.asynch.emotions(123)).rejects.toThrow(
230
+ 'requestId is required and must be a string'
231
+ );
232
+ });
233
+
234
+ test('should throw error when API key is missing', async () => {
235
+ delete process.env.VALENCE_API_KEY;
236
+
237
+ const client = new ValenceClient();
238
+ await expect(client.asynch.emotions('req-123')).rejects.toThrow(
239
+ 'VALENCE_API_KEY is required'
240
+ );
241
+ });
242
+
243
+ test('should throw error with invalid maxTries', async () => {
244
+ const client = new ValenceClient();
245
+ await expect(client.asynch.emotions('req-123', 0)).rejects.toThrow(
246
+ 'maxTries must be between 1 and 100'
247
+ );
248
+
249
+ await expect(client.asynch.emotions('req-123', 101)).rejects.toThrow(
250
+ 'maxTries must be between 1 and 100'
251
+ );
252
+ });
253
+
254
+ test('should throw error with invalid intervalMs', async () => {
255
+ const client = new ValenceClient();
256
+ await expect(client.asynch.emotions('req-123', 5, 500)).rejects.toThrow(
257
+ 'intervalMs must be between 1000 and 60000'
258
+ );
259
+
260
+ await expect(client.asynch.emotions('req-123', 5, 70000)).rejects.toThrow(
261
+ 'intervalMs must be between 1000 and 60000'
262
+ );
263
+ });
264
+
265
+ test('should handle 404 errors', async () => {
266
+ nock('https://test-api.com')
267
+ .get('/prediction')
268
+ .query({ request_id: 'invalid-id' })
269
+ .reply(404);
270
+
271
+ const client = new ValenceClient();
272
+ await expect(client.asynch.emotions('invalid-id')).rejects.toThrow(
273
+ 'Request ID not found: invalid-id'
274
+ );
275
+ });
276
+
277
+ test('should handle client errors', async () => {
278
+ nock('https://test-api.com')
279
+ .get('/prediction')
280
+ .query({ request_id: 'req-123' })
281
+ .reply(400, { message: 'Bad request' });
282
+
283
+ const client = new ValenceClient();
284
+ await expect(client.asynch.emotions('req-123')).rejects.toThrow(
285
+ 'Client error (400): Bad request'
286
+ );
287
+ });
288
+
289
+ test('should timeout after max attempts', async () => {
290
+ nock('https://test-api.com')
291
+ .get('/prediction')
292
+ .query({ request_id: 'req-123' })
293
+ .times(3)
294
+ .reply(202); // Always processing
295
+
296
+ const client = new ValenceClient();
297
+ await expect(client.asynch.emotions('req-123', 3, 100)).rejects.toThrow(
298
+ 'Prediction not available after 3 attempts. The request may still be processing.'
299
+ );
300
+ });
301
+
302
+ test('should handle server errors gracefully', async () => {
303
+ nock('https://test-api.com')
304
+ .get('/prediction')
305
+ .query({ request_id: 'req-123' })
306
+ .reply(500)
307
+ .get('/prediction')
308
+ .query({ request_id: 'req-123' })
309
+ .reply(200, { emotions: { happy: 1.0 } });
310
+
311
+ const client = new ValenceClient();
312
+ const result = await client.asynch.emotions('req-123', 5, 100);
313
+
314
+ expect(result).toEqual({ emotions: { happy: 1.0 } });
315
+ });
316
+
317
+ test('should include correct headers', async () => {
318
+ const scope = nock('https://test-api.com')
319
+ .get('/prediction')
320
+ .matchHeader('x-api-key', 'test-api-key')
321
+ .matchHeader('User-Agent', 'valenceai/0.4.0')
322
+ .query({ request_id: 'req-123' })
323
+ .reply(200, { emotions: {} });
324
+
325
+ const client = new ValenceClient();
326
+ await client.asynch.emotions('req-123');
327
+
328
+ expect(scope.isDone()).toBe(true);
329
+ });
330
+ });
331
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
2
+ import { getHeaders } from '../src/client.js';
3
+
4
+ describe('Client', () => {
5
+ const originalEnv = process.env;
6
+
7
+ beforeEach(() => {
8
+ process.env = { ...originalEnv };
9
+ });
10
+
11
+ afterEach(() => {
12
+ process.env = originalEnv;
13
+ });
14
+
15
+ describe('getHeaders', () => {
16
+ test('should return headers with API key', async () => {
17
+ process.env.VALENCE_API_KEY = 'test-api-key';
18
+
19
+ // Re-import to get fresh config
20
+ const { getHeaders: freshGetHeaders } = await import('../src/client.js?' + Math.random());
21
+
22
+ const headers = freshGetHeaders();
23
+
24
+ expect(headers).toEqual({
25
+ 'x-api-key': 'test-api-key',
26
+ 'User-Agent': 'valenceai/0.4.0'
27
+ });
28
+ });
29
+
30
+ test('should throw error when API key is missing', () => {
31
+ delete process.env.VALENCE_API_KEY;
32
+
33
+ expect(() => getHeaders()).toThrow('VALENCE_API_KEY is required but not configured');
34
+ });
35
+
36
+ test('should throw error when API key is empty string', () => {
37
+ process.env.VALENCE_API_KEY = '';
38
+
39
+ expect(() => getHeaders()).toThrow('VALENCE_API_KEY is required but not configured');
40
+ });
41
+ });
42
+ });
@@ -0,0 +1,90 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
2
+ import { config, validateConfig } from '../src/config.js';
3
+
4
+ describe('Config', () => {
5
+ const originalEnv = process.env;
6
+
7
+ beforeEach(() => {
8
+ process.env = { ...originalEnv };
9
+ });
10
+
11
+ afterEach(() => {
12
+ process.env = originalEnv;
13
+ });
14
+
15
+ describe('config object', () => {
16
+ test('should load from environment variables', async () => {
17
+ process.env.VALENCE_API_KEY = 'test-key';
18
+ process.env.VALENCE_DISCRETE_URL = 'https://test-short.com';
19
+ process.env.VALENCE_ASYNC_URL = 'https://test-long.com';
20
+ process.env.VALENCE_LOG_LEVEL = 'debug';
21
+
22
+ // Re-import to get fresh config
23
+ const { config: freshConfig } = await import('../src/config.js?' + Math.random());
24
+
25
+ expect(freshConfig.apiKey).toBe('test-key');
26
+ expect(freshConfig.discreteAudioUrl).toBe('https://test-short.com');
27
+ expect(freshConfig.asyncAudioUrl).toBe('https://test-long.com');
28
+ expect(freshConfig.logLevel).toBe('debug');
29
+ });
30
+
31
+ test('should use default URLs when not provided', async () => {
32
+ process.env.VALENCE_API_KEY = 'test-key';
33
+ delete process.env.VALENCE_DISCRETE_URL;
34
+ delete process.env.VALENCE_ASYNC_URL;
35
+
36
+ // Re-import to get fresh config
37
+ const { config: freshConfig } = await import('../src/config.js?' + Math.random());
38
+
39
+ expect(freshConfig.discreteAudioUrl).toBe('https://xc8n2bo4f0.execute-api.us-west-2.amazonaws.com/emotionprediction');
40
+ expect(freshConfig.asyncAudioUrl).toBe('https://wsgol61783.execute-api.us-west-2.amazonaws.com/prod');
41
+ });
42
+
43
+ test('should use default log level when not provided', async () => {
44
+ delete process.env.VALENCE_LOG_LEVEL;
45
+
46
+ // Re-import to get fresh config
47
+ const { config: freshConfig } = await import('../src/config.js?' + Math.random());
48
+
49
+ expect(freshConfig.logLevel).toBe('info');
50
+ });
51
+ });
52
+
53
+ describe('validateConfig', () => {
54
+ test('should pass with valid configuration', () => {
55
+ process.env.VALENCE_API_KEY = 'test-key';
56
+ process.env.VALENCE_DISCRETE_URL = 'https://test-short.com';
57
+ process.env.VALENCE_ASYNC_URL = 'https://test-long.com';
58
+ process.env.VALENCE_LOG_LEVEL = 'info';
59
+
60
+ expect(() => validateConfig()).not.toThrow();
61
+ });
62
+
63
+ test('should throw when API key is missing', async () => {
64
+ delete process.env.VALENCE_API_KEY;
65
+ const { validateConfig: freshValidateConfig } = await import('../src/config.js?' + Math.random());
66
+ expect(() => freshValidateConfig()).toThrow('VALENCE_API_KEY environment variable is required');
67
+ });
68
+
69
+ test('should throw when short URL is not HTTPS', async () => {
70
+ process.env.VALENCE_API_KEY = 'test-key';
71
+ process.env.VALENCE_DISCRETE_URL = 'http://test-short.com';
72
+ const { validateConfig: freshValidateConfig } = await import('../src/config.js?' + Math.random());
73
+ expect(() => freshValidateConfig()).toThrow('VALENCE_DISCRETE_URL must be a valid HTTPS URL');
74
+ });
75
+
76
+ test('should throw when long URL is not HTTPS', async () => {
77
+ process.env.VALENCE_API_KEY = 'test-key';
78
+ process.env.VALENCE_ASYNC_URL = 'http://test-long.com';
79
+ const { validateConfig: freshValidateConfig } = await import('../src/config.js?' + Math.random());
80
+ expect(() => freshValidateConfig()).toThrow('VALENCE_ASYNC_URL must be a valid HTTPS URL');
81
+ });
82
+
83
+ test('should throw with invalid log level', async () => {
84
+ process.env.VALENCE_API_KEY = 'test-key';
85
+ process.env.VALENCE_LOG_LEVEL = 'invalid';
86
+ const { validateConfig: freshValidateConfig } = await import('../src/config.js?' + Math.random());
87
+ expect(() => freshValidateConfig()).toThrow('VALENCE_LOG_LEVEL must be one of: debug, info, warn, error');
88
+ });
89
+ });
90
+ });
@@ -0,0 +1,168 @@
1
+ import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals';
2
+ import nock from 'nock';
3
+ import fs from 'fs';
4
+ import { ValenceClient } from '../src/valenceClient.js';
5
+
6
+ describe('DiscreteAudio', () => {
7
+ const originalEnv = process.env;
8
+ let fsMock;
9
+
10
+ beforeEach(() => {
11
+ process.env = { ...originalEnv };
12
+ process.env.VALENCE_API_KEY = 'test-api-key';
13
+ process.env.VALENCE_DISCRETE_URL = 'https://test-api.com/predict';
14
+
15
+ fsMock = jest.spyOn(fs, 'existsSync');
16
+ jest.spyOn(fs, 'createReadStream').mockReturnValue({
17
+ pipe: jest.fn(),
18
+ on: jest.fn()
19
+ });
20
+ });
21
+
22
+ afterEach(() => {
23
+ process.env = originalEnv;
24
+ jest.restoreAllMocks();
25
+ nock.cleanAll();
26
+ });
27
+
28
+ describe('predictDiscreteAudioEmotion', () => {
29
+ test('should successfully predict emotions', async () => {
30
+ fsMock.mockReturnValue(true);
31
+
32
+ const mockResponse = {
33
+ emotions: {
34
+ happy: 0.8,
35
+ sad: 0.1,
36
+ angry: 0.1
37
+ }
38
+ };
39
+
40
+ nock('https://test-api.com')
41
+ .post('/predict?model=7emotions')
42
+ .reply(200, mockResponse);
43
+
44
+ const client = new ValenceClient();
45
+ const result = await client.discrete.emotions('test.wav', '7emotions');
46
+
47
+ expect(result).toEqual(mockResponse);
48
+ });
49
+
50
+ test('should use default model when not specified', async () => {
51
+ fsMock.mockReturnValue(true);
52
+
53
+ nock('https://test-api.com')
54
+ .post('/predict?model=4emotions')
55
+ .reply(200, { emotions: {} });
56
+
57
+ const client = new ValenceClient();
58
+ await client.discrete.emotions('test.wav');
59
+
60
+ expect(nock.isDone()).toBe(true);
61
+ });
62
+
63
+ test('should throw error when filePath is missing', async () => {
64
+ const client = new ValenceClient();
65
+ await expect(client.discrete.emotions()).rejects.toThrow(
66
+ 'filePath is required and must be a string'
67
+ );
68
+ });
69
+
70
+ test('should throw error when filePath is not a string', async () => {
71
+ const client = new ValenceClient();
72
+ await expect(client.discrete.emotions(123)).rejects.toThrow(
73
+ 'filePath is required and must be a string'
74
+ );
75
+ });
76
+
77
+ test('should throw error with invalid model', async () => {
78
+ const client = new ValenceClient();
79
+ await expect(client.discrete.emotions('test.wav', 'invalid')).rejects.toThrow(
80
+ 'model must be either "4emotions" or "7emotions"'
81
+ );
82
+ });
83
+
84
+ test('should throw error when API key is missing', async () => {
85
+ delete process.env.VALENCE_API_KEY;
86
+
87
+ const client = new ValenceClient();
88
+ await expect(client.discrete.emotions('test.wav')).rejects.toThrow(
89
+ 'VALENCE_API_KEY is required'
90
+ );
91
+ });
92
+
93
+ test('should throw error when file does not exist', async () => {
94
+ fsMock.mockReturnValue(false);
95
+
96
+ const client = new ValenceClient();
97
+ await expect(client.discrete.emotions('nonexistent.wav')).rejects.toThrow(
98
+ 'File not found: nonexistent.wav'
99
+ );
100
+ });
101
+
102
+ test('should handle API error responses', async () => {
103
+ fsMock.mockReturnValue(true);
104
+
105
+ nock('https://test-api.com')
106
+ .post('/predict?model=4emotions')
107
+ .reply(400, { message: 'Invalid file format' });
108
+
109
+ const client = new ValenceClient();
110
+ await expect(client.discrete.emotions('test.wav')).rejects.toThrow(
111
+ 'API error (400): Invalid file format'
112
+ );
113
+ });
114
+
115
+ test('should handle API error without message', async () => {
116
+ fsMock.mockReturnValue(true);
117
+
118
+ nock('https://test-api.com')
119
+ .post('/predict?model=4emotions')
120
+ .reply(500, 'Internal Server Error');
121
+
122
+ const client = new ValenceClient();
123
+ await expect(client.discrete.emotions('test.wav')).rejects.toThrow(
124
+ 'API error (500): Internal Server Error'
125
+ );
126
+ });
127
+
128
+ test('should handle network errors', async () => {
129
+ fsMock.mockReturnValue(true);
130
+
131
+ nock('https://test-api.com')
132
+ .post('/predict?model=4emotions')
133
+ .replyWithError('Network error');
134
+
135
+ const client = new ValenceClient();
136
+ await expect(client.discrete.emotions('test.wav')).rejects.toThrow(
137
+ 'Network error: Unable to reach the API'
138
+ );
139
+ });
140
+
141
+ test('should handle timeout errors', async () => {
142
+ fsMock.mockReturnValue(true);
143
+
144
+ nock('https://test-api.com')
145
+ .post('/predict?model=4emotions')
146
+ .delay(31000)
147
+ .reply(200, {});
148
+
149
+ const client = new ValenceClient();
150
+ await expect(client.discrete.emotions('test.wav')).rejects.toThrow();
151
+ });
152
+
153
+ test('should include correct headers', async () => {
154
+ fsMock.mockReturnValue(true);
155
+
156
+ const scope = nock('https://test-api.com')
157
+ .post('/predict?model=4emotions')
158
+ .matchHeader('x-api-key', 'test-api-key')
159
+ .matchHeader('User-Agent', 'valenceai/0.4.0')
160
+ .reply(200, { emotions: {} });
161
+
162
+ const client = new ValenceClient();
163
+ await client.discrete.emotions('test.wav');
164
+
165
+ expect(scope.isDone()).toBe(true);
166
+ });
167
+ });
168
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, test, expect } from '@jest/globals';
2
+ import { ValenceClient, validateConfig } from '../src/index.js';
3
+
4
+ describe('Index exports', () => {
5
+ test('should export ValenceClient', () => {
6
+ expect(typeof ValenceClient).toBe('function');
7
+ });
8
+
9
+ test('should export validateConfig', () => {
10
+ expect(typeof validateConfig).toBe('function');
11
+ });
12
+
13
+ test('should export all expected functions', async () => {
14
+ const index = await import('../src/index.js');
15
+ const expectedExports = ['ValenceClient', 'validateConfig'];
16
+
17
+ expectedExports.forEach(exportName => {
18
+ expect(index).toHaveProperty(exportName);
19
+ expect(typeof index[exportName]).toBe('function');
20
+ });
21
+ });
22
+
23
+ test('should create ValenceClient with nested APIs', () => {
24
+ process.env.VALENCE_API_KEY = 'test-key';
25
+ const client = new ValenceClient();
26
+
27
+ expect(client).toHaveProperty('discrete');
28
+ expect(client).toHaveProperty('asynch');
29
+ expect(typeof client.discrete.emotions).toBe('function');
30
+ expect(typeof client.asynch.upload).toBe('function');
31
+ expect(typeof client.asynch.emotions).toBe('function');
32
+ });
33
+ });