@forgehive/hive-sdk 0.1.6 → 0.2.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.
@@ -1,21 +1,60 @@
1
1
  import axios from 'axios'
2
+ import fs from 'fs'
2
3
  import { HiveLogClient } from '../index'
3
4
 
4
- // Mock axios
5
+ // Mock axios and fs
5
6
  jest.mock('axios')
7
+ jest.mock('fs')
6
8
  const mockedAxios = axios as jest.Mocked<typeof axios>
9
+ const mockedFs = fs as jest.Mocked<typeof fs>
7
10
 
8
11
  describe('HiveLogClient sendLog with ExecutionRecord', () => {
9
12
  let client: HiveLogClient
10
13
 
11
14
  const testConfig = {
12
15
  projectName: 'test-project',
16
+ projectUuid: '550e8400-e29b-41d4-a716-446655440000',
13
17
  apiKey: 'test-api-key',
14
18
  apiSecret: 'test-api-secret',
15
- host: 'https://test-host.com'
19
+ host: 'https://test-host.com',
20
+ forgeConfigPath: './forge.json'
21
+ }
22
+
23
+ const mockForgeConfig = {
24
+ project: {
25
+ name: 'test-project',
26
+ uuid: '550e8400-e29b-41d4-a716-446655440000'
27
+ },
28
+ tasks: {
29
+ 'test-task': {
30
+ path: 'src/tasks/test.ts',
31
+ handler: 'testTask',
32
+ uuid: 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
33
+ },
34
+ 'complex-task': {
35
+ path: 'src/tasks/complex.ts',
36
+ handler: 'complexTask',
37
+ uuid: 'a45aafe3-8b01-4b58-b15d-9a96274858ee'
38
+ },
39
+ 'error-task': {
40
+ path: 'src/tasks/error.ts',
41
+ handler: 'errorTask',
42
+ uuid: '8879271f-7e84-4748-bd11-4d81acf29fb6'
43
+ },
44
+ 'metadata-task': {
45
+ path: 'src/tasks/metadata.ts',
46
+ handler: 'metadataTask',
47
+ uuid: 'fad2f735-ca09-4b8b-9c44-597de3641d28'
48
+ }
49
+ }
16
50
  }
17
51
 
18
52
  beforeEach(() => {
53
+ // Mock fs.existsSync to return true
54
+ mockedFs.existsSync.mockReturnValue(true)
55
+ // Mock fs.readFileSync to return forge.json content
56
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockForgeConfig))
57
+
19
58
  // Create client instance with config
20
59
  client = new HiveLogClient(testConfig)
21
60
 
@@ -41,27 +80,24 @@ describe('HiveLogClient sendLog with ExecutionRecord', () => {
41
80
 
42
81
  expect(result).toBe('success')
43
82
  expect(mockedAxios.post).toHaveBeenCalledTimes(1)
44
- expect(mockedAxios.post).toHaveBeenCalledWith(
45
- 'https://test-host.com/api/tasks/log-ingest',
46
- {
47
- projectName: 'test-project',
48
- taskName: 'test-task',
49
- logItem: JSON.stringify({
50
- input: { value: 'test-input' },
51
- output: { result: 'test-output' },
52
- taskName: 'test-task',
53
- type: 'success',
54
- boundaries: {},
55
- metadata: {}
56
- })
57
- },
58
- {
59
- headers: {
60
- Authorization: 'Bearer test-api-key:test-api-secret',
61
- 'Content-Type': 'application/json'
62
- }
63
- }
64
- )
83
+
84
+ // Verify it uses the UUID endpoint
85
+ const callArgs = mockedAxios.post.mock.calls[0]
86
+ expect(callArgs[0]).toBe('https://test-host.com/api/log-ingest')
87
+
88
+ // Verify request body structure
89
+ const requestBody = callArgs[1] as { projectUuid: string; taskUuid: string; logItem: string }
90
+ expect(requestBody.projectUuid).toBe(testConfig.projectUuid)
91
+ expect(requestBody.taskUuid).toBe('f47ac10b-58cc-4372-a567-0e02b2c3d479')
92
+
93
+ // Verify logItem has UUID generated
94
+ const logItem = JSON.parse(requestBody.logItem)
95
+ expect(logItem.uuid).toBeDefined()
96
+ expect(typeof logItem.uuid).toBe('string')
97
+ expect(logItem.input).toEqual({ value: 'test-input' })
98
+ expect(logItem.output).toEqual({ result: 'test-output' })
99
+ expect(logItem.taskName).toBe('test-task')
100
+ expect(logItem.type).toBe('success')
65
101
  })
66
102
 
67
103
  it('should handle ExecutionRecord with complex boundaries', async () => {
@@ -90,38 +126,18 @@ describe('HiveLogClient sendLog with ExecutionRecord', () => {
90
126
  const result = await client.sendLog(executionRecord)
91
127
 
92
128
  expect(result).toBe('success')
93
- expect(mockedAxios.post).toHaveBeenCalledWith(
94
- 'https://test-host.com/api/tasks/log-ingest',
95
- {
96
- projectName: 'test-project',
97
- taskName: 'complex-task',
98
- logItem: JSON.stringify({
99
- input: { userId: 123, action: 'login' },
100
- output: { success: true, sessionId: 'abc123' },
101
- taskName: 'complex-task',
102
- type: 'success',
103
- boundaries: {
104
- database: [{
105
- input: ['SELECT * FROM users'],
106
- output: [{ id: 123 }],
107
- timing: { startTime: 1000, endTime: 1100, duration: 100 }
108
- }],
109
- api: [{
110
- input: [{ endpoint: '/auth' }],
111
- output: { token: 'jwt123' },
112
- timing: { startTime: 1200, endTime: 1250, duration: 50 }
113
- }]
114
- },
115
- metadata: { environment: 'test' }
116
- })
117
- },
118
- {
119
- headers: {
120
- Authorization: 'Bearer test-api-key:test-api-secret',
121
- 'Content-Type': 'application/json'
122
- }
123
- }
124
- )
129
+
130
+ // Verify request structure
131
+ const callArgs = mockedAxios.post.mock.calls[0]
132
+ expect(callArgs[0]).toBe('https://test-host.com/api/log-ingest')
133
+
134
+ const requestBody = callArgs[1] as { projectUuid: string; taskUuid: string; logItem: string }
135
+ expect(requestBody.projectUuid).toBe(testConfig.projectUuid)
136
+ expect(requestBody.taskUuid).toBe('a45aafe3-8b01-4b58-b15d-9a96274858ee')
137
+
138
+ const logItem = JSON.parse(requestBody.logItem)
139
+ expect(logItem.uuid).toBeDefined()
140
+ expect(logItem.boundaries).toEqual(executionRecord.boundaries)
125
141
  })
126
142
 
127
143
  it('should handle ExecutionRecord with error', async () => {
@@ -140,32 +156,25 @@ describe('HiveLogClient sendLog with ExecutionRecord', () => {
140
156
  const result = await client.sendLog(executionRecord)
141
157
 
142
158
  expect(result).toBe('success')
143
- expect(mockedAxios.post).toHaveBeenCalledWith(
144
- 'https://test-host.com/api/tasks/log-ingest',
145
- {
146
- projectName: 'test-project',
147
- taskName: 'error-task',
148
- logItem: JSON.stringify({
149
- input: { value: 'test-input' },
150
- output: undefined,
151
- error: 'Task execution failed',
152
- taskName: 'error-task',
153
- type: 'error',
154
- boundaries: {},
155
- metadata: {}
156
- })
157
- },
158
- expect.any(Object)
159
- )
159
+
160
+ const callArgs = mockedAxios.post.mock.calls[0]
161
+ const requestBody = callArgs[1] as { projectUuid: string; taskUuid: string; logItem: string }
162
+ expect(requestBody.taskUuid).toBe('8879271f-7e84-4748-bd11-4d81acf29fb6')
163
+
164
+ const logItem = JSON.parse(requestBody.logItem)
165
+ expect(logItem.error).toBe('Task execution failed')
166
+ expect(logItem.type).toBe('error')
160
167
  })
161
168
 
162
- it('should use "unknown-task" when taskName is missing', async () => {
169
+ it('should preserve existing UUID when execution record already has one', async () => {
163
170
  mockedAxios.post.mockResolvedValueOnce({ data: { success: true } })
164
171
 
172
+ const existingUuid = '01234567-89ab-7def-8123-456789abcdef'
165
173
  const executionRecord = {
174
+ uuid: existingUuid,
166
175
  input: { value: 'test-input' },
167
176
  output: { result: 'test-output' },
168
- // taskName is missing
177
+ taskName: 'test-task',
169
178
  type: 'success' as const,
170
179
  boundaries: {},
171
180
  metadata: {}
@@ -174,22 +183,38 @@ describe('HiveLogClient sendLog with ExecutionRecord', () => {
174
183
  const result = await client.sendLog(executionRecord)
175
184
 
176
185
  expect(result).toBe('success')
177
- expect(mockedAxios.post).toHaveBeenCalledWith(
178
- 'https://test-host.com/api/tasks/log-ingest',
179
- {
180
- projectName: 'test-project',
181
- taskName: 'unknown-task',
182
- logItem: JSON.stringify({
183
- input: { value: 'test-input' },
184
- output: { result: 'test-output' },
185
- type: 'success',
186
- boundaries: {},
187
- metadata: {},
188
- taskName: 'unknown-task'
189
- })
190
- },
191
- expect.any(Object)
192
- )
186
+
187
+ const callArgs = mockedAxios.post.mock.calls[0]
188
+ const requestBody = callArgs[1] as { projectUuid: string; taskUuid: string; logItem: string }
189
+ const logItem = JSON.parse(requestBody.logItem)
190
+
191
+ expect(logItem.uuid).toBe(existingUuid)
192
+ })
193
+
194
+ it('should generate UUID v7 when execution record has no UUID', async () => {
195
+ mockedAxios.post.mockResolvedValueOnce({ data: { success: true } })
196
+
197
+ const executionRecord = {
198
+ input: { value: 'test-input' },
199
+ output: { result: 'test-output' },
200
+ taskName: 'test-task',
201
+ type: 'success' as const,
202
+ boundaries: {},
203
+ metadata: {}
204
+ }
205
+
206
+ const result = await client.sendLog(executionRecord)
207
+
208
+ expect(result).toBe('success')
209
+
210
+ const callArgs = mockedAxios.post.mock.calls[0]
211
+ const requestBody = callArgs[1] as { projectUuid: string; taskUuid: string; logItem: string }
212
+ const logItem = JSON.parse(requestBody.logItem)
213
+
214
+ expect(logItem.uuid).toBeDefined()
215
+ expect(typeof logItem.uuid).toBe('string')
216
+ // UUID v7 pattern
217
+ expect(logItem.uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)
193
218
  })
194
219
  })
195
220
 
@@ -217,26 +242,16 @@ describe('HiveLogClient sendLog with ExecutionRecord', () => {
217
242
  const result = await client.sendLog(executionRecord, sendLogMetadata)
218
243
 
219
244
  expect(result).toBe('success')
220
- expect(mockedAxios.post).toHaveBeenCalledWith(
221
- 'https://test-host.com/api/tasks/log-ingest',
222
- {
223
- projectName: 'test-project',
224
- taskName: 'metadata-task',
225
- logItem: JSON.stringify({
226
- input: { value: 'test-input' },
227
- output: { result: 'test-output' },
228
- taskName: 'metadata-task',
229
- type: 'success',
230
- boundaries: {},
231
- metadata: {
232
- recordMeta: 'from-record',
233
- sharedKey: 'sendlog-value', // sendLog metadata takes priority
234
- sendLogMeta: 'from-sendlog'
235
- }
236
- })
237
- },
238
- expect.any(Object)
239
- )
245
+
246
+ const callArgs = mockedAxios.post.mock.calls[0]
247
+ const requestBody = callArgs[1] as { projectUuid: string; taskUuid: string; logItem: string }
248
+ const logItem = JSON.parse(requestBody.logItem)
249
+
250
+ expect(logItem.metadata).toEqual({
251
+ recordMeta: 'from-record',
252
+ sharedKey: 'sendlog-value', // sendLog metadata takes priority
253
+ sendLogMeta: 'from-sendlog'
254
+ })
240
255
  })
241
256
  })
242
257
 
@@ -275,6 +290,44 @@ describe('HiveLogClient sendLog with ExecutionRecord', () => {
275
290
 
276
291
  expect(result).toBe('error')
277
292
  })
293
+
294
+ it('should return error when task is not found in forge.json', async () => {
295
+ const executionRecord = {
296
+ input: { value: 'test-input' },
297
+ taskName: 'nonexistent-task',
298
+ type: 'success' as const,
299
+ boundaries: {},
300
+ metadata: {}
301
+ }
302
+
303
+ const result = await client.sendLog(executionRecord)
304
+
305
+ expect(result).toBe('error')
306
+ expect(mockedAxios.post).not.toHaveBeenCalled()
307
+ })
308
+
309
+ it('should return error when projectUuid is not set', async () => {
310
+ // Create client without projectUuid
311
+ const clientWithoutUuid = new HiveLogClient({
312
+ projectName: 'test-project',
313
+ apiKey: 'test-key',
314
+ apiSecret: 'test-secret',
315
+ host: 'https://test-host.com'
316
+ })
317
+
318
+ const executionRecord = {
319
+ input: { value: 'test-input' },
320
+ taskName: 'test-task',
321
+ type: 'success' as const,
322
+ boundaries: {},
323
+ metadata: {}
324
+ }
325
+
326
+ const result = await clientWithoutUuid.sendLog(executionRecord)
327
+
328
+ expect(result).toBe('error')
329
+ expect(mockedAxios.post).not.toHaveBeenCalled()
330
+ })
278
331
  })
279
332
 
280
333
  describe('sendLog in silent mode', () => {
@@ -298,6 +351,50 @@ describe('HiveLogClient sendLog with ExecutionRecord', () => {
298
351
  expect(mockedAxios.post).not.toHaveBeenCalled()
299
352
  })
300
353
  })
354
+
355
+ describe('sendLog with unknown task name', () => {
356
+ it('should return error when taskName is missing and defaults to unknown-task', async () => {
357
+ const executionRecord = {
358
+ input: { value: 'test-input' },
359
+ output: { result: 'test-output' },
360
+ // taskName is missing - will default to 'unknown-task'
361
+ type: 'success' as const,
362
+ boundaries: {},
363
+ metadata: {}
364
+ }
365
+
366
+ const result = await client.sendLog(executionRecord)
367
+
368
+ // Should return error because 'unknown-task' is not in forge.json
369
+ expect(result).toBe('error')
370
+ expect(mockedAxios.post).not.toHaveBeenCalled()
371
+ })
372
+ })
373
+
374
+ describe('sendLog response handling', () => {
375
+ it('should return response data when API returns object with uuid', async () => {
376
+ const responseData = {
377
+ uuid: 'log-uuid-123',
378
+ taskName: 'test-task',
379
+ projectName: 'test-project',
380
+ logItem: {},
381
+ createdAt: '2024-01-01T00:00:00Z'
382
+ }
383
+ mockedAxios.post.mockResolvedValueOnce({ data: responseData })
384
+
385
+ const executionRecord = {
386
+ input: { value: 'test' },
387
+ taskName: 'test-task',
388
+ type: 'success' as const,
389
+ boundaries: {},
390
+ metadata: {}
391
+ }
392
+
393
+ const result = await client.sendLog(executionRecord)
394
+
395
+ expect(result).toEqual(responseData)
396
+ })
397
+ })
301
398
  })
302
399
 
303
400
 
@@ -306,12 +403,32 @@ describe('HiveLogClient getListener', () => {
306
403
 
307
404
  const testConfig = {
308
405
  projectName: 'test-project',
406
+ projectUuid: '550e8400-e29b-41d4-a716-446655440000',
309
407
  apiKey: 'test-api-key',
310
408
  apiSecret: 'test-api-secret',
311
- host: 'https://test-host.com'
409
+ host: 'https://test-host.com',
410
+ forgeConfigPath: './forge.json'
411
+ }
412
+
413
+ const mockForgeConfig = {
414
+ project: {
415
+ name: 'test-project',
416
+ uuid: '550e8400-e29b-41d4-a716-446655440000'
417
+ },
418
+ tasks: {
419
+ 'test-task': {
420
+ path: 'src/tasks/test.ts',
421
+ handler: 'testTask',
422
+ uuid: 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
423
+ }
424
+ }
312
425
  }
313
426
 
314
427
  beforeEach(() => {
428
+ // Mock fs
429
+ mockedFs.existsSync.mockReturnValue(true)
430
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockForgeConfig))
431
+
315
432
  client = new HiveLogClient(testConfig)
316
433
  jest.clearAllMocks()
317
434
  })
@@ -336,22 +453,10 @@ describe('HiveLogClient getListener', () => {
336
453
  await listener(executionRecord)
337
454
 
338
455
  expect(mockedAxios.post).toHaveBeenCalledTimes(1)
339
- expect(mockedAxios.post).toHaveBeenCalledWith(
340
- 'https://test-host.com/api/tasks/log-ingest',
341
- {
342
- projectName: 'test-project',
343
- taskName: 'test-task',
344
- logItem: JSON.stringify({
345
- input: { value: 'test-input' },
346
- output: { result: 'test-output' },
347
- taskName: 'test-task',
348
- type: 'success',
349
- boundaries: {},
350
- metadata: {}
351
- })
352
- },
353
- expect.any(Object)
354
- )
456
+
457
+ // Verify it uses the UUID endpoint
458
+ const callArgs = mockedAxios.post.mock.calls[0]
459
+ expect(callArgs[0]).toBe('https://test-host.com/api/log-ingest')
355
460
  })
356
461
 
357
462
  it('should return a function that calls sendLog with provided metadata', async () => {
@@ -370,24 +475,11 @@ describe('HiveLogClient getListener', () => {
370
475
 
371
476
  await listener(executionRecord)
372
477
 
373
- expect(mockedAxios.post).toHaveBeenCalledWith(
374
- 'https://test-host.com/api/tasks/log-ingest',
375
- {
376
- projectName: 'test-project',
377
- taskName: 'test-task',
378
- logItem: JSON.stringify({
379
- input: { value: 'test-input' },
380
- output: { result: 'test-output' },
381
- taskName: 'test-task',
382
- type: 'success',
383
- boundaries: {},
384
- metadata: {
385
- recordMeta: 'from-record'
386
- }
387
- })
388
- },
389
- expect.any(Object)
390
- )
478
+ const callArgs = mockedAxios.post.mock.calls[0]
479
+ const requestBody = callArgs[1] as { projectUuid: string; taskUuid: string; logItem: string }
480
+ const logItem = JSON.parse(requestBody.logItem)
481
+
482
+ expect(logItem.metadata).toEqual({ recordMeta: 'from-record' })
391
483
  })
392
484
 
393
485
  it('should handle listener errors gracefully', async () => {