@forgehive/hive-sdk 0.0.4 → 0.1.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.
@@ -8,7 +8,7 @@ const index_1 = require("../index");
8
8
  // Mock axios
9
9
  jest.mock('axios');
10
10
  const mockedAxios = axios_1.default;
11
- describe('HiveLogClient sendLog', () => {
11
+ describe('HiveLogClient sendLog with ExecutionRecord', () => {
12
12
  let client;
13
13
  const testConfig = {
14
14
  projectName: 'test-project',
@@ -22,18 +22,32 @@ describe('HiveLogClient sendLog', () => {
22
22
  // Clear all mocks
23
23
  jest.clearAllMocks();
24
24
  });
25
- describe('successful sendLog', () => {
26
- it('should send log successfully and return true', async () => {
25
+ describe('successful sendLog with ExecutionRecord', () => {
26
+ it('should send log successfully with ExecutionRecord and return success', async () => {
27
27
  // Mock successful axios response
28
28
  mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
29
- const logItem = { input: 'test-input', output: 'test-output' };
30
- const result = await client.sendLog('test-task', logItem);
29
+ const executionRecord = {
30
+ input: { value: 'test-input' },
31
+ output: { result: 'test-output' },
32
+ taskName: 'test-task',
33
+ type: 'success',
34
+ boundaries: {},
35
+ metadata: {}
36
+ };
37
+ const result = await client.sendLog(executionRecord);
31
38
  expect(result).toBe('success');
32
39
  expect(mockedAxios.post).toHaveBeenCalledTimes(1);
33
40
  expect(mockedAxios.post).toHaveBeenCalledWith('https://test-host.com/api/tasks/log-ingest', {
34
41
  projectName: 'test-project',
35
42
  taskName: 'test-task',
36
- logItem: JSON.stringify({ ...logItem, metadata: {} })
43
+ logItem: JSON.stringify({
44
+ input: { value: 'test-input' },
45
+ output: { result: 'test-output' },
46
+ taskName: 'test-task',
47
+ type: 'success',
48
+ boundaries: {},
49
+ metadata: {}
50
+ })
37
51
  }, {
38
52
  headers: {
39
53
  Authorization: 'Bearer test-api-key:test-api-secret',
@@ -41,22 +55,51 @@ describe('HiveLogClient sendLog', () => {
41
55
  }
42
56
  });
43
57
  });
44
- it('should handle complex log items', async () => {
58
+ it('should handle ExecutionRecord with complex boundaries', async () => {
45
59
  mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
46
- const complexLogItem = {
60
+ const executionRecord = {
47
61
  input: { userId: 123, action: 'login' },
48
62
  output: { success: true, sessionId: 'abc123' },
49
- error: null,
63
+ taskName: 'complex-task',
64
+ type: 'success',
50
65
  boundaries: {
51
- database: [{ input: 'SELECT * FROM users', output: [{ id: 123 }], error: null }]
52
- }
66
+ database: [{
67
+ input: ['SELECT * FROM users'],
68
+ output: [{ id: 123 }],
69
+ timing: { startTime: 1000, endTime: 1100, duration: 100 }
70
+ }],
71
+ api: [{
72
+ input: [{ endpoint: '/auth' }],
73
+ output: { token: 'jwt123' },
74
+ timing: { startTime: 1200, endTime: 1250, duration: 50 }
75
+ }]
76
+ },
77
+ metadata: { environment: 'test' }
53
78
  };
54
- const result = await client.sendLog('complex-task', complexLogItem);
79
+ const result = await client.sendLog(executionRecord);
55
80
  expect(result).toBe('success');
56
81
  expect(mockedAxios.post).toHaveBeenCalledWith('https://test-host.com/api/tasks/log-ingest', {
57
82
  projectName: 'test-project',
58
83
  taskName: 'complex-task',
59
- logItem: JSON.stringify({ ...complexLogItem, metadata: {} })
84
+ logItem: JSON.stringify({
85
+ input: { userId: 123, action: 'login' },
86
+ output: { success: true, sessionId: 'abc123' },
87
+ taskName: 'complex-task',
88
+ type: 'success',
89
+ boundaries: {
90
+ database: [{
91
+ input: ['SELECT * FROM users'],
92
+ output: [{ id: 123 }],
93
+ timing: { startTime: 1000, endTime: 1100, duration: 100 }
94
+ }],
95
+ api: [{
96
+ input: [{ endpoint: '/auth' }],
97
+ output: { token: 'jwt123' },
98
+ timing: { startTime: 1200, endTime: 1250, duration: 50 }
99
+ }]
100
+ },
101
+ metadata: { environment: 'test' }
102
+ })
60
103
  }, {
61
104
  headers: {
62
105
  Authorization: 'Bearer test-api-key:test-api-secret',
@@ -64,44 +107,224 @@ describe('HiveLogClient sendLog', () => {
64
107
  }
65
108
  });
66
109
  });
110
+ it('should handle ExecutionRecord with error', async () => {
111
+ mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
112
+ const executionRecord = {
113
+ input: { value: 'test-input' },
114
+ output: undefined,
115
+ error: 'Task execution failed',
116
+ taskName: 'error-task',
117
+ type: 'error',
118
+ boundaries: {},
119
+ metadata: {}
120
+ };
121
+ const result = await client.sendLog(executionRecord);
122
+ expect(result).toBe('success');
123
+ expect(mockedAxios.post).toHaveBeenCalledWith('https://test-host.com/api/tasks/log-ingest', {
124
+ projectName: 'test-project',
125
+ taskName: 'error-task',
126
+ logItem: JSON.stringify({
127
+ input: { value: 'test-input' },
128
+ output: undefined,
129
+ error: 'Task execution failed',
130
+ taskName: 'error-task',
131
+ type: 'error',
132
+ boundaries: {},
133
+ metadata: {}
134
+ })
135
+ }, expect.any(Object));
136
+ });
137
+ it('should use "unknown-task" when taskName is missing', async () => {
138
+ mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
139
+ const executionRecord = {
140
+ input: { value: 'test-input' },
141
+ output: { result: 'test-output' },
142
+ // taskName is missing
143
+ type: 'success',
144
+ boundaries: {},
145
+ metadata: {}
146
+ };
147
+ const result = await client.sendLog(executionRecord);
148
+ expect(result).toBe('success');
149
+ expect(mockedAxios.post).toHaveBeenCalledWith('https://test-host.com/api/tasks/log-ingest', {
150
+ projectName: 'test-project',
151
+ taskName: 'unknown-task',
152
+ logItem: JSON.stringify({
153
+ input: { value: 'test-input' },
154
+ output: { result: 'test-output' },
155
+ type: 'success',
156
+ boundaries: {},
157
+ metadata: {},
158
+ taskName: 'unknown-task'
159
+ })
160
+ }, expect.any(Object));
161
+ });
162
+ });
163
+ describe('sendLog with additional metadata', () => {
164
+ it('should merge metadata from ExecutionRecord and sendLog parameter', async () => {
165
+ mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
166
+ const executionRecord = {
167
+ input: { value: 'test-input' },
168
+ output: { result: 'test-output' },
169
+ taskName: 'metadata-task',
170
+ type: 'success',
171
+ boundaries: {},
172
+ metadata: {
173
+ recordMeta: 'from-record',
174
+ sharedKey: 'record-value'
175
+ }
176
+ };
177
+ const sendLogMetadata = {
178
+ sendLogMeta: 'from-sendlog',
179
+ sharedKey: 'sendlog-value' // This should override record value
180
+ };
181
+ const result = await client.sendLog(executionRecord, sendLogMetadata);
182
+ expect(result).toBe('success');
183
+ expect(mockedAxios.post).toHaveBeenCalledWith('https://test-host.com/api/tasks/log-ingest', {
184
+ projectName: 'test-project',
185
+ taskName: 'metadata-task',
186
+ logItem: JSON.stringify({
187
+ input: { value: 'test-input' },
188
+ output: { result: 'test-output' },
189
+ taskName: 'metadata-task',
190
+ type: 'success',
191
+ boundaries: {},
192
+ metadata: {
193
+ recordMeta: 'from-record',
194
+ sharedKey: 'sendlog-value', // sendLog metadata takes priority
195
+ sendLogMeta: 'from-sendlog'
196
+ }
197
+ })
198
+ }, expect.any(Object));
199
+ });
67
200
  });
68
201
  describe('failed sendLog', () => {
69
- it('should return false when axios throws an error', async () => {
202
+ it('should return error when axios throws an error', async () => {
70
203
  // Mock axios to throw an error
71
204
  mockedAxios.post.mockRejectedValueOnce(new Error('Network error'));
72
- const logItem = { input: 'test-input' };
73
- const result = await client.sendLog('test-task', logItem);
205
+ const executionRecord = {
206
+ input: { value: 'test-input' },
207
+ taskName: 'test-task',
208
+ type: 'success',
209
+ boundaries: {},
210
+ metadata: {}
211
+ };
212
+ const result = await client.sendLog(executionRecord);
74
213
  expect(result).toBe('error');
75
214
  });
76
- it('should return false when server returns 500', async () => {
215
+ it('should return error when server returns 500', async () => {
77
216
  // Mock axios to throw a server error
78
217
  const serverError = new Error('Server Error');
79
218
  mockedAxios.post.mockRejectedValueOnce(serverError);
80
- const result = await client.sendLog('test-task', { input: 'test' });
219
+ const executionRecord = {
220
+ input: { value: 'test' },
221
+ taskName: 'test-task',
222
+ type: 'success',
223
+ boundaries: {},
224
+ metadata: {}
225
+ };
226
+ const result = await client.sendLog(executionRecord);
81
227
  expect(result).toBe('error');
82
228
  });
83
229
  });
84
- describe('sendLog parameters', () => {
85
- it('should handle log items with minimal input', async () => {
230
+ describe('sendLog in silent mode', () => {
231
+ it('should return silent when client is not initialized', async () => {
232
+ const uninitializedClient = new index_1.HiveLogClient({
233
+ projectName: 'test-project'
234
+ // No API credentials
235
+ });
236
+ const executionRecord = {
237
+ input: { value: 'test-input' },
238
+ taskName: 'test-task',
239
+ type: 'success',
240
+ boundaries: {},
241
+ metadata: {}
242
+ };
243
+ const result = await uninitializedClient.sendLog(executionRecord);
244
+ expect(result).toBe('silent');
245
+ expect(mockedAxios.post).not.toHaveBeenCalled();
246
+ });
247
+ });
248
+ });
249
+ describe('HiveLogClient getListener', () => {
250
+ let client;
251
+ const testConfig = {
252
+ projectName: 'test-project',
253
+ apiKey: 'test-api-key',
254
+ apiSecret: 'test-api-secret',
255
+ host: 'https://test-host.com'
256
+ };
257
+ beforeEach(() => {
258
+ client = new index_1.HiveLogClient(testConfig);
259
+ jest.clearAllMocks();
260
+ });
261
+ describe('getListener method', () => {
262
+ it('should return a function that calls sendLog', async () => {
86
263
  mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
87
- const result = await client.sendLog('minimal-task', { input: 'minimal input' });
88
- expect(result).toBe('success');
264
+ const listener = client.getListener();
265
+ expect(typeof listener).toBe('function');
266
+ const executionRecord = {
267
+ input: { value: 'test-input' },
268
+ output: { result: 'test-output' },
269
+ taskName: 'test-task',
270
+ type: 'success',
271
+ boundaries: {},
272
+ metadata: {}
273
+ };
274
+ await listener(executionRecord);
275
+ expect(mockedAxios.post).toHaveBeenCalledTimes(1);
89
276
  expect(mockedAxios.post).toHaveBeenCalledWith('https://test-host.com/api/tasks/log-ingest', {
90
277
  projectName: 'test-project',
91
- taskName: 'minimal-task',
92
- logItem: JSON.stringify({ input: 'minimal input', metadata: {} })
278
+ taskName: 'test-task',
279
+ logItem: JSON.stringify({
280
+ input: { value: 'test-input' },
281
+ output: { result: 'test-output' },
282
+ taskName: 'test-task',
283
+ type: 'success',
284
+ boundaries: {},
285
+ metadata: {}
286
+ })
93
287
  }, expect.any(Object));
94
288
  });
95
- it('should handle null/undefined values in log items', async () => {
289
+ it('should return a function that calls sendLog with provided metadata', async () => {
96
290
  mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
97
- const logItem = { input: null, output: undefined, error: 'some error' };
98
- const result = await client.sendLog('null-task', logItem);
99
- expect(result).toBe('success');
291
+ const listener = client.getListener();
292
+ const executionRecord = {
293
+ input: { value: 'test-input' },
294
+ output: { result: 'test-output' },
295
+ taskName: 'test-task',
296
+ type: 'success',
297
+ boundaries: {},
298
+ metadata: { recordMeta: 'from-record' }
299
+ };
300
+ await listener(executionRecord);
100
301
  expect(mockedAxios.post).toHaveBeenCalledWith('https://test-host.com/api/tasks/log-ingest', {
101
302
  projectName: 'test-project',
102
- taskName: 'null-task',
103
- logItem: JSON.stringify({ ...logItem, metadata: {} })
303
+ taskName: 'test-task',
304
+ logItem: JSON.stringify({
305
+ input: { value: 'test-input' },
306
+ output: { result: 'test-output' },
307
+ taskName: 'test-task',
308
+ type: 'success',
309
+ boundaries: {},
310
+ metadata: {
311
+ recordMeta: 'from-record'
312
+ }
313
+ })
104
314
  }, expect.any(Object));
105
315
  });
316
+ it('should handle listener errors gracefully', async () => {
317
+ mockedAxios.post.mockRejectedValueOnce(new Error('Network error'));
318
+ const listener = client.getListener();
319
+ const executionRecord = {
320
+ input: { value: 'test-input' },
321
+ taskName: 'test-task',
322
+ type: 'success',
323
+ boundaries: {},
324
+ metadata: {}
325
+ };
326
+ // Should not throw, even if sendLog fails
327
+ await expect(listener(executionRecord)).resolves.toBeUndefined();
328
+ });
106
329
  });
107
330
  });
package/package.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "name": "@forgehive/hive-sdk",
3
- "version": "0.0.4",
3
+ "version": "0.1.1",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "dependencies": {
10
+ "@forgehive/task": "^0.2.4"
11
+ }
12
+ },
7
13
  "devDependencies": {
8
14
  "@types/jest": "^29.5.14",
9
15
  "@types/node": "^24.0.3",
@@ -15,7 +21,8 @@
15
21
  "license": "ISC",
16
22
  "dependencies": {
17
23
  "axios": "^1.8.4",
18
- "debug": "^4.4.1"
24
+ "debug": "^4.4.1",
25
+ "@forgehive/task": "0.2.4"
19
26
  },
20
27
  "scripts": {
21
28
  "build": "tsc",
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import axios from 'axios'
2
2
  import debug from 'debug'
3
+ import type { ExecutionRecord } from '@forgehive/task'
3
4
 
4
5
  const log = debug('hive-sdk')
5
6
 
@@ -8,17 +9,8 @@ export interface Metadata {
8
9
  [key: string]: string
9
10
  }
10
11
 
11
- // Log item interface for sendLog method - flexible to accept task execution records
12
- export interface LogItemInput {
13
- input: unknown
14
- output?: unknown
15
- error?: unknown
16
- boundaries?: unknown // Allow any boundary structure (task records have different format)
17
- metadata?: Metadata
18
- }
19
-
20
- // Backward compatibility alias
21
- export type LogItem = LogItemInput
12
+ // Re-export ExecutionRecord type from task package for convenience
13
+ export type { ExecutionRecord } from '@forgehive/task'
22
14
 
23
15
  // Configuration interface for HiveLogClient
24
16
  export interface HiveLogClientConfig {
@@ -34,13 +26,7 @@ export interface LogApiResponse {
34
26
  uuid: string
35
27
  taskName: string
36
28
  projectName: string
37
- logItem: {
38
- input: unknown
39
- output?: unknown
40
- error?: unknown
41
- boundaries?: Record<string, Array<{ input: unknown; output: unknown, error: unknown }>>
42
- metadata?: Metadata
43
- }
29
+ logItem: ExecutionRecord
44
30
  replayFrom?: string
45
31
  createdAt: string
46
32
  }
@@ -100,13 +86,13 @@ export class HiveLogClient {
100
86
  return this.isInitialized
101
87
  }
102
88
 
103
- private mergeMetadata<T extends { input: unknown; metadata?: Metadata }>(logItem: T, sendLogMetadata?: Metadata): Metadata {
89
+ private mergeMetadata(record: ExecutionRecord, sendLogMetadata?: Metadata): Metadata {
104
90
  // Start with base metadata from client
105
91
  let finalMetadata = { ...this.baseMetadata }
106
92
 
107
- // Merge with logItem metadata if it exists
108
- if (logItem.metadata) {
109
- finalMetadata = { ...finalMetadata, ...logItem.metadata }
93
+ // Merge with record metadata if it exists
94
+ if (record.metadata) {
95
+ finalMetadata = { ...finalMetadata, ...record.metadata }
110
96
  }
111
97
 
112
98
  // Merge with sendLog metadata (highest priority)
@@ -117,7 +103,10 @@ export class HiveLogClient {
117
103
  return finalMetadata
118
104
  }
119
105
 
120
- async sendLog<T extends { input: unknown; metadata?: Metadata }>(taskName: string, logItem: T, metadata?: Metadata): Promise<'success' | 'error' | 'silent'> {
106
+ async sendLog(record: ExecutionRecord, metadata?: Metadata): Promise<'success' | 'error' | 'silent'> {
107
+ // Extract taskName from record
108
+ const taskName = record.taskName || 'unknown-task'
109
+
121
110
  if (!this.isInitialized) {
122
111
  log('Silent mode: Skipping sendLog for task "%s" - client not initialized', taskName)
123
112
  return 'silent'
@@ -129,19 +118,20 @@ export class HiveLogClient {
129
118
 
130
119
  const authToken = `${this.apiKey}:${this.apiSecret}`
131
120
 
132
- // Merge metadata with priority: sendLog > logItem > client
133
- const finalMetadata = this.mergeMetadata(logItem, metadata)
121
+ // Merge metadata with priority: sendLog > record.metadata > client
122
+ const finalMetadata = this.mergeMetadata(record, metadata)
134
123
 
135
- // Create enhanced logItem with merged metadata
136
- const enhancedLogItem = {
137
- ...logItem,
124
+ // Create logItem with merged metadata
125
+ const logItem = {
126
+ ...record,
127
+ taskName,
138
128
  metadata: finalMetadata
139
129
  }
140
130
 
141
131
  await axios.post(logsUrl, {
142
132
  projectName: this.projectName,
143
133
  taskName,
144
- logItem: JSON.stringify(enhancedLogItem)
134
+ logItem: JSON.stringify(logItem)
145
135
  }, {
146
136
  headers: {
147
137
  Authorization: `Bearer ${authToken}`,
@@ -158,6 +148,12 @@ export class HiveLogClient {
158
148
  }
159
149
  }
160
150
 
151
+ getListener(): (record: ExecutionRecord) => Promise<void> {
152
+ return async (record: ExecutionRecord) => {
153
+ await this.sendLog(record)
154
+ }
155
+ }
156
+
161
157
  async getLog(taskName: string, uuid: string): Promise<LogApiResult | null> {
162
158
  if (!this.isInitialized) {
163
159
  log('Error: getLog for task "%s" with uuid "%s" - missing credentials', taskName, uuid)
@@ -32,10 +32,15 @@ describe('HiveLogClient getLog', () => {
32
32
  logItem: {
33
33
  input: { userId: 123, action: 'login' },
34
34
  output: { success: true, sessionId: 'abc123' },
35
- error: null,
35
+ error: undefined,
36
36
  boundaries: {
37
- database: [{ input: 'SELECT * FROM users', output: [{ id: 123 }], error: null }]
38
- }
37
+ database: [{
38
+ input: ['SELECT * FROM users'],
39
+ output: [{ id: 123 }],
40
+ timing: { startTime: 1000, endTime: 1100, duration: 100 }
41
+ }]
42
+ },
43
+ type: 'success' as const
39
44
  },
40
45
  replayFrom: 'some-replay-id',
41
46
  createdAt: '2023-12-01T10:00:00Z'
@@ -78,7 +83,9 @@ describe('HiveLogClient getLog', () => {
78
83
  taskName: 'minimal-task',
79
84
  projectName: 'test-project',
80
85
  logItem: {
81
- input: 'simple input'
86
+ input: 'simple input',
87
+ boundaries: {},
88
+ type: 'pending' as const
82
89
  },
83
90
  createdAt: '2023-12-01T10:00:00Z'
84
91
  }
@@ -129,7 +136,11 @@ describe('HiveLogClient getLog', () => {
129
136
  uuid: 'uuid-with-special-chars-!@#',
130
137
  taskName: 'task-with-special-chars-!@#',
131
138
  projectName: 'test-project',
132
- logItem: { input: 'test' },
139
+ logItem: {
140
+ input: 'test',
141
+ boundaries: {},
142
+ type: 'pending' as const
143
+ },
133
144
  createdAt: '2023-12-01T10:00:00Z'
134
145
  }
135
146
 
@@ -157,7 +168,11 @@ describe('isApiError type guard', () => {
157
168
  uuid: 'test-uuid',
158
169
  taskName: 'test-task',
159
170
  projectName: 'test-project',
160
- logItem: { input: 'test' },
171
+ logItem: {
172
+ input: 'test',
173
+ boundaries: {},
174
+ type: 'pending' as const
175
+ },
161
176
  createdAt: '2023-12-01T10:00:00Z'
162
177
  }
163
178
  expect(isApiError(logResponse)).toBe(false)
@@ -106,7 +106,14 @@ describe('Hive SDK', () => {
106
106
  })
107
107
 
108
108
  it('should return "silent" for sendLog in silent mode', async () => {
109
- const result = await silentClient.sendLog('test-task', { input: 'test' })
109
+ const executionRecord = {
110
+ input: { test: 'value' },
111
+ taskName: 'test-task',
112
+ type: 'success' as const,
113
+ boundaries: {},
114
+ metadata: {}
115
+ }
116
+ const result = await silentClient.sendLog(executionRecord)
110
117
  expect(result).toBe('silent')
111
118
  })
112
119