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