@forgehive/task 0.1.12 → 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,7 +1,15 @@
1
1
  import { Schema } from '@forgehive/schema'
2
- import { createTask, ExecutionRecord } from '../index'
2
+ import { createTask, ExecutionRecord, getExecutionRecordType } from '../index'
3
3
 
4
4
  describe('safeReplay functionality tests', () => {
5
+ // Helper function to create ExecutionRecord with computed type
6
+ function createExecutionRecord<T, U>(partial: Omit<ExecutionRecord<T, U>, 'type'>): ExecutionRecord<T, U> {
7
+ return {
8
+ ...partial,
9
+ type: getExecutionRecordType(partial)
10
+ }
11
+ }
12
+
5
13
  // Common variables
6
14
  let prices: Record<string, number>
7
15
  let boundaries: {
@@ -38,22 +46,23 @@ describe('safeReplay functionality tests', () => {
38
46
  }
39
47
 
40
48
  // Create the task using createTask
41
- getTickerPrice = createTask(
49
+ getTickerPrice = createTask({
50
+ name: 'getTickerPrice',
42
51
  schema,
43
52
  boundaries,
44
- async ({ ticker }, { fetchData }) => {
53
+ fn: async ({ ticker }, { fetchData }) => {
45
54
  const price = await fetchData(ticker)
46
55
  return {
47
56
  ticker,
48
57
  price
49
58
  }
50
59
  }
51
- )
60
+ })
52
61
  })
53
62
 
54
63
  it('Should replay a previous execution using the execution log and replay the fetchData boundary', async () => {
55
64
  // Create a manual execution log
56
- const executionLog: ExecutionRecord = {
65
+ const executionLog: ExecutionRecord = createExecutionRecord({
57
66
  input: { ticker: 'AAPL' },
58
67
  output: {
59
68
  ticker: 'AAPL',
@@ -67,7 +76,7 @@ describe('safeReplay functionality tests', () => {
67
76
  }
68
77
  ]
69
78
  }
70
- }
79
+ })
71
80
 
72
81
  // No safeReplay method yet, this will be implemented later
73
82
  // This will be our test for that functionality
@@ -101,7 +110,7 @@ describe('safeReplay functionality tests', () => {
101
110
 
102
111
  it('Should execute with mixed boundaries modes', async () => {
103
112
  // Create a manual execution log for testing
104
- const executionLog: ExecutionRecord = {
113
+ const executionLog: ExecutionRecord = createExecutionRecord({
105
114
  input: { ticker: 'AAPL' },
106
115
  output: {
107
116
  ticker: 'AAPL',
@@ -115,7 +124,7 @@ describe('safeReplay functionality tests', () => {
115
124
  }
116
125
  ]
117
126
  }
118
- }
127
+ })
119
128
 
120
129
  // Use mixed mode - replay for fetchData but execute logAccess
121
130
  const [replayResult, replayError, replayLog] = await getTickerPrice.safeReplay(
@@ -153,7 +162,7 @@ describe('safeReplay functionality tests', () => {
153
162
 
154
163
  it('Should properly handle errors in boundary replay mode', async () => {
155
164
  // Create a manual execution log with an error in the boundary
156
- const executionLog: ExecutionRecord = {
165
+ const executionLog: ExecutionRecord = createExecutionRecord({
157
166
  input: { ticker: 'AAPL' },
158
167
  output: null,
159
168
  error: 'API error: Rate limit exceeded',
@@ -165,7 +174,7 @@ describe('safeReplay functionality tests', () => {
165
174
  }
166
175
  ]
167
176
  }
168
- }
177
+ })
169
178
 
170
179
  // Use replay mode for fetchData
171
180
  const [replayResult, replayError, replayLog] = await getTickerPrice.safeReplay(
@@ -189,7 +198,7 @@ describe('safeReplay functionality tests', () => {
189
198
 
190
199
  it('Should handle boundaries with both output and error as null', async () => {
191
200
  // Create a manual execution log with null output and error in the boundary
192
- const executionLog: ExecutionRecord = {
201
+ const executionLog: ExecutionRecord = createExecutionRecord({
193
202
  input: { ticker: 'AAPL' },
194
203
  output: { ticker: 'AAPL', price: 160.23 },
195
204
  boundaries: {
@@ -201,7 +210,7 @@ describe('safeReplay functionality tests', () => {
201
210
  }
202
211
  ]
203
212
  }
204
- }
213
+ })
205
214
 
206
215
  // Use replay mode for fetchData
207
216
  const [replayResult, replayError, replayLog] = await getTickerPrice.safeReplay(
@@ -15,14 +15,15 @@ describe('Task safeRun tests', () => {
15
15
  }
16
16
 
17
17
  // Create the task
18
- const successTask = createTask(
18
+ const successTask = createTask({
19
+ name: 'successTask',
19
20
  schema,
20
21
  boundaries,
21
- async function ({ value }, { fetchData }) {
22
+ fn: async function ({ value }, { fetchData }) {
22
23
  const result = await fetchData(value)
23
24
  return { result, success: true }
24
25
  }
25
- )
26
+ })
26
27
 
27
28
  // Call safeRun with valid input
28
29
  const [result, error, record] = await successTask.safeRun({ value: 5 })
@@ -58,14 +59,15 @@ describe('Task safeRun tests', () => {
58
59
  }
59
60
 
60
61
  // Create the task
61
- const errorTask = createTask(
62
+ const errorTask = createTask({
63
+ name: 'errorTask',
62
64
  schema,
63
65
  boundaries,
64
- async function ({ value }, { fetchData }) {
66
+ fn: async function ({ value }, { fetchData }) {
65
67
  const result = await fetchData(value)
66
68
  return { result, success: true }
67
69
  }
68
- )
70
+ })
69
71
 
70
72
  // Call safeRun with problematic input that will cause an error
71
73
  const [result, error, record] = await errorTask.safeRun({ value: -5 })
@@ -101,14 +103,15 @@ describe('Task safeRun tests', () => {
101
103
  }
102
104
 
103
105
  // Create the task
104
- const validationTask = createTask(
106
+ const validationTask = createTask({
107
+ name: 'validationTask',
105
108
  schema,
106
109
  boundaries,
107
- async function ({ value }, { fetchData }) {
110
+ fn: async function ({ value }, { fetchData }) {
108
111
  const result = await fetchData(value)
109
112
  return { result, success: true }
110
113
  }
111
- )
114
+ })
112
115
 
113
116
  // Call safeRun with invalid input that will fail schema validation
114
117
  const [result, error, record] = await validationTask.safeRun({ value: 0 })
@@ -142,14 +145,15 @@ describe('Task safeRun tests', () => {
142
145
  }
143
146
 
144
147
  // Create the task
145
- const listenerTask = createTask(
148
+ const listenerTask = createTask({
149
+ name: 'listenerTask',
146
150
  schema,
147
151
  boundaries,
148
- async function ({ value }, { fetchData }) {
152
+ fn: async function ({ value }, { fetchData }) {
149
153
  const result = await fetchData(value)
150
154
  return result
151
155
  }
152
- )
156
+ })
153
157
 
154
158
  // Create a mock listener
155
159
  const originalListener = jest.fn()
@@ -206,15 +210,16 @@ describe('Task safeRun tests', () => {
206
210
  }
207
211
 
208
212
  // Create a task that uses multiple boundaries
209
- const multiBoundaryTask = createTask(
213
+ const multiBoundaryTask = createTask({
214
+ name: 'multiBoundaryTask',
210
215
  schema,
211
216
  boundaries,
212
- async function ({ values }, { doubleValue, sumValues }) {
217
+ fn: async function ({ values }, { doubleValue, sumValues }) {
213
218
  const doubled = await Promise.all(values.map(value => doubleValue(value)))
214
219
  const total = await sumValues(doubled)
215
220
  return { doubled, total }
216
221
  }
217
- )
222
+ })
218
223
 
219
224
  // Call safeRun
220
225
  const [result, error, record] = await multiBoundaryTask.safeRun({ values: [1, 2, 3] })
@@ -17,14 +17,15 @@ describe('Task boundary mocking', () => {
17
17
  }
18
18
 
19
19
  // Create the task using createTask
20
- const multiplyTask = createTask(
20
+ const multiplyTask = createTask({
21
+ name: 'multiplyTask',
21
22
  schema,
22
23
  boundaries,
23
- async function ({ value }, { fetchExternalData }) {
24
+ fn: async function ({ value }, { fetchExternalData }) {
24
25
  const result = value * await fetchExternalData(value)
25
26
  return result
26
27
  }
27
- )
28
+ })
28
29
 
29
30
  // Create mock for fetchExternalData boundary that returns a specific value
30
31
  const mockFetchData = jest.fn().mockResolvedValue(5)
@@ -71,15 +72,16 @@ describe('Task boundary mocking', () => {
71
72
  }
72
73
 
73
74
  // Create the task
74
- const calculateTask = createTask(
75
+ const calculateTask = createTask({
76
+ name: 'calculateTask',
75
77
  schema,
76
78
  boundaries,
77
- async function ({ value }, { doubleValue, tripleValue }) {
79
+ fn: async function ({ value }, { doubleValue, tripleValue }) {
78
80
  const doubled = await doubleValue(value)
79
81
  const tripled = await tripleValue(value)
80
82
  return doubled + tripled
81
83
  }
82
- )
84
+ })
83
85
 
84
86
  // Create wrapped mock functions
85
87
  const mockDoubleValue = jest.fn().mockResolvedValue(10)
@@ -0,0 +1,246 @@
1
+ import { createTask, Schema, getExecutionRecordType } from '../index'
2
+
3
+ describe('Task execution log record', () => {
4
+ describe('Task name in execution record', () => {
5
+ it('should include task name in safeRun execution record when task has a name', async () => {
6
+ // Create a schema
7
+ const schema = new Schema({
8
+ value: Schema.number()
9
+ })
10
+
11
+ // Create a task with a name
12
+ const task = createTask({
13
+ name: 'test-multiplication-task',
14
+ description: 'A task that multiplies input by 2',
15
+ schema,
16
+ boundaries: {
17
+ multiply: async (value: number) => value * 2
18
+ },
19
+ fn: async ({ value }, { multiply }) => {
20
+ const result = await multiply(value)
21
+ return { result }
22
+ }
23
+ })
24
+
25
+ // Run the task with safeRun
26
+ const [result, error, record] = await task.safeRun({ value: 5 })
27
+
28
+ // Verify the execution was successful
29
+ expect(error).toBeNull()
30
+ expect(result).toEqual({ result: 10 })
31
+
32
+ // Verify the task name is included in the record
33
+ expect(record.taskName).toBe('test-multiplication-task')
34
+ expect(record.input).toEqual({ value: 5 })
35
+ expect(record.output).toEqual({ result: 10 })
36
+ })
37
+
38
+ it('should include undefined task name when task has no name', async () => {
39
+ // Create a schema
40
+ const schema = new Schema({
41
+ value: Schema.number()
42
+ })
43
+
44
+ // Create a task without a name
45
+ const task = createTask({
46
+ schema,
47
+ boundaries: {},
48
+ fn: async ({ value }) => {
49
+ return { result: value * 3 }
50
+ }
51
+ })
52
+
53
+ // Run the task with safeRun
54
+ const [result, error, record] = await task.safeRun({ value: 4 })
55
+
56
+ // Verify the execution was successful
57
+ expect(error).toBeNull()
58
+ expect(result).toEqual({ result: 12 })
59
+
60
+ // Verify the task name is undefined in the record
61
+ expect(record.taskName).toBeUndefined()
62
+ expect(record.input).toEqual({ value: 4 })
63
+ expect(record.output).toEqual({ result: 12 })
64
+ })
65
+
66
+ it('should include task name in error execution records', async () => {
67
+ // Create a schema
68
+ const schema = new Schema({
69
+ value: Schema.number()
70
+ })
71
+
72
+ // Create a task that will throw an error
73
+ const task = createTask({
74
+ name: 'error-task',
75
+ schema,
76
+ boundaries: {},
77
+ fn: async ({ value }) => {
78
+ if (value < 0) {
79
+ throw new Error('Value cannot be negative')
80
+ }
81
+ return { result: value }
82
+ }
83
+ })
84
+
85
+ // Run the task with a value that will cause an error
86
+ const [result, error, record] = await task.safeRun({ value: -1 })
87
+
88
+ // Verify the execution failed as expected
89
+ expect(result).toBeNull()
90
+ expect(error).not.toBeNull()
91
+ expect(error?.message).toBe('Value cannot be negative')
92
+
93
+ // Verify the task name is included in the error record
94
+ expect(record.taskName).toBe('error-task')
95
+ expect(record.input).toEqual({ value: -1 })
96
+ expect(record.error).toBe('Value cannot be negative')
97
+ expect(record.output).toBeUndefined()
98
+ })
99
+
100
+ it('should include task name in safeReplay execution record', async () => {
101
+ // Create a schema
102
+ const schema = new Schema({
103
+ value: Schema.number()
104
+ })
105
+
106
+ // Create a task with boundaries
107
+ const task = createTask({
108
+ name: 'replay-test-task',
109
+ schema,
110
+ boundaries: {
111
+ fetchData: async (value: number) => value * 2
112
+ },
113
+ fn: async ({ value }, { fetchData }) => {
114
+ const result = await fetchData(value)
115
+ return { result }
116
+ }
117
+ })
118
+
119
+ // First, run the task normally to get an execution record
120
+ const [, , originalRecord] = await task.safeRun({ value: 3 })
121
+
122
+ // Now replay the task
123
+ const [replayResult, replayError, replayRecord] = await task.safeReplay(
124
+ originalRecord,
125
+ { boundaries: { fetchData: 'replay' } }
126
+ )
127
+
128
+ // Verify the replay was successful
129
+ expect(replayError).toBeNull()
130
+ expect(replayResult).toEqual({ result: 6 })
131
+
132
+ // Verify the task name is included in the replay record
133
+ expect(replayRecord.taskName).toBe('replay-test-task')
134
+ expect(replayRecord.input).toEqual({ value: 3 })
135
+ expect(replayRecord.output).toEqual({ result: 6 })
136
+ })
137
+ })
138
+
139
+ describe('Type computation in execution record', () => {
140
+ it('should have type "success" when task completes successfully', async () => {
141
+ const schema = new Schema({
142
+ value: Schema.number()
143
+ })
144
+
145
+ const task = createTask({
146
+ name: 'success-task',
147
+ schema,
148
+ boundaries: {},
149
+ fn: async ({ value }) => {
150
+ return { result: value * 2 }
151
+ }
152
+ })
153
+
154
+ const [result, error, record] = await task.safeRun({ value: 5 })
155
+
156
+ expect(error).toBeNull()
157
+ expect(result).toEqual({ result: 10 })
158
+ expect(record.type).toBe('success')
159
+ expect(record.output).toEqual({ result: 10 })
160
+ expect(record.error).toBeUndefined()
161
+ })
162
+
163
+ it('should have type "error" when task throws an error', async () => {
164
+ const schema = new Schema({
165
+ value: Schema.number()
166
+ })
167
+
168
+ const task = createTask({
169
+ name: 'error-task',
170
+ schema,
171
+ boundaries: {},
172
+ fn: async ({ value }) => {
173
+ if (value < 0) {
174
+ throw new Error('Value cannot be negative')
175
+ }
176
+ return { result: value }
177
+ }
178
+ })
179
+
180
+ const [result, error, record] = await task.safeRun({ value: -1 })
181
+
182
+ expect(result).toBeNull()
183
+ expect(error).not.toBeNull()
184
+ expect(record.type).toBe('error')
185
+ expect(record.error).toBe('Value cannot be negative')
186
+ expect(record.output).toBeUndefined()
187
+ })
188
+
189
+ it('should have type "pending" when neither output nor error is set', async () => {
190
+ // This tests the getExecutionRecordType utility function directly
191
+ const partialRecord = {
192
+ input: { value: 1 },
193
+ boundaries: {},
194
+ taskName: 'test'
195
+ }
196
+
197
+ const type = getExecutionRecordType(partialRecord)
198
+ expect(type).toBe('pending')
199
+ })
200
+ })
201
+
202
+ describe('Context in execution record', () => {
203
+ it('should include context when provided to safeRun', async () => {
204
+ const schema = new Schema({
205
+ value: Schema.number()
206
+ })
207
+
208
+ const task = createTask({
209
+ name: 'context-task',
210
+ schema,
211
+ boundaries: {},
212
+ fn: async ({ value }) => {
213
+ return { result: value * 2 }
214
+ }
215
+ })
216
+
217
+ const [result, error, record] = await task.safeRun({ value: 5 })
218
+
219
+ expect(error).toBeNull()
220
+ expect(result).toEqual({ result: 10 })
221
+ expect(record.metadata).toEqual({})
222
+ expect(record.taskName).toBe('context-task')
223
+ })
224
+
225
+ it('should handle undefined context gracefully', async () => {
226
+ const schema = new Schema({
227
+ value: Schema.number()
228
+ })
229
+
230
+ const task = createTask({
231
+ name: 'no-context-task',
232
+ schema,
233
+ boundaries: {},
234
+ fn: async ({ value }) => {
235
+ return { result: value * 2 }
236
+ }
237
+ })
238
+
239
+ const [result, error, record] = await task.safeRun({ value: 5 })
240
+
241
+ expect(error).toBeNull()
242
+ expect(result).toEqual({ result: 10 })
243
+ expect(record.metadata).toEqual({})
244
+ })
245
+ })
246
+ })
@@ -14,14 +14,15 @@ describe('Boundaries tasks tests', () => {
14
14
  }
15
15
 
16
16
  // Create the task using createTask
17
- const indentity = createTask(
17
+ const indentity = createTask({
18
+ name: 'indentity',
18
19
  schema,
19
20
  boundaries,
20
- async (argv, boundaries) => {
21
+ fn: async (argv, boundaries) => {
21
22
  const externalData = await boundaries.fetchExternalData()
22
23
  return { ...externalData, ...argv }
23
24
  }
24
- )
25
+ })
25
26
 
26
27
  const object = await indentity.run({ bar: true })
27
28
  const { foo } = await indentity.run({ foo: true })
@@ -43,22 +44,21 @@ describe('Boundaries tasks tests', () => {
43
44
  }
44
45
 
45
46
  // Create the task using createTask with boundariesData
46
- const indentity = createTask(
47
+ const indentity = createTask({
48
+ name: 'indentity',
47
49
  schema,
48
50
  boundaries,
49
- async (argv, boundaries) => {
51
+ fn: async (argv, boundaries) => {
50
52
  const externalData = await boundaries.fetchExternalData()
51
53
  return { ...externalData, ...argv }
52
54
  },
53
- {
54
- boundariesData: {
55
- fetchExternalData: [
56
- { input: [], output: { foo: false } }
57
- ]
58
- },
59
- mode: 'proxy-pass'
60
- }
61
- )
55
+ boundariesData: {
56
+ fetchExternalData: [
57
+ { input: [], output: { foo: false } }
58
+ ]
59
+ },
60
+ mode: 'proxy-pass'
61
+ })
62
62
 
63
63
  const object = await indentity.run({ bar: true })
64
64
  const { foo } = await indentity.run({ foo: true })
@@ -81,14 +81,15 @@ describe('Boundaries tasks tests', () => {
81
81
  }
82
82
 
83
83
  // Create the task using createTask
84
- const add = createTask(
84
+ const add = createTask({
85
+ name: 'add',
85
86
  schema,
86
87
  boundaries,
87
- async function (argv, boundaries) {
88
+ fn: async function (argv, boundaries) {
88
89
  const externalData: number = await boundaries.fetchExternalData(1)
89
90
  return argv.value + externalData
90
91
  }
91
- )
92
+ })
92
93
 
93
94
  const six = await add.run({ value: 4 })
94
95
  const seven = await add.run({ value: 5 })
@@ -111,22 +112,21 @@ describe('Boundaries tasks tests', () => {
111
112
  }
112
113
 
113
114
  // Create the task using createTask with boundariesData
114
- const add = createTask(
115
+ const add = createTask({
116
+ name: 'add',
115
117
  schema,
116
118
  boundaries,
117
- async function (argv, boundaries) {
119
+ fn: async function (argv, boundaries) {
118
120
  const externalData: number = await boundaries.fetchExternalData(argv.value)
119
121
  return argv.value + externalData
120
122
  },
121
- {
122
- boundariesData: {
123
- fetchExternalData: [
124
- { input: [4], output: 2 }
125
- ]
126
- },
127
- mode: 'proxy-pass'
128
- }
129
- )
123
+ boundariesData: {
124
+ fetchExternalData: [
125
+ { input: [4], output: 2 }
126
+ ]
127
+ },
128
+ mode: 'proxy-pass'
129
+ })
130
130
 
131
131
  const six = await add.run({ value: 4 }) // From tape data
132
132
  const fifteen = await add.run({ value: 5 })
@@ -163,14 +163,15 @@ describe('Boundaries tasks tests', () => {
163
163
  }
164
164
 
165
165
  // Create the task using createTask
166
- const multiplyTask = createTask(
166
+ const multiplyTask = createTask({
167
+ name: 'multiplyTask',
167
168
  schema,
168
169
  boundaries,
169
- async function ({ value }, { fetchExternalData}) {
170
+ fn: async function ({ value }, { fetchExternalData}) {
170
171
  const externalData: number = await fetchExternalData(value)
171
172
  return value * externalData
172
173
  }
173
- )
174
+ })
174
175
 
175
176
  multiplyTask.addListener((record) => {
176
177
  records.push(record)
@@ -238,14 +239,15 @@ describe('Boundaries tasks tests', () => {
238
239
  }
239
240
 
240
241
  // Create the task using createTask
241
- const multiplyTask = createTask(
242
+ const multiplyTask = createTask({
243
+ name: 'multiplyTask',
242
244
  schema,
243
245
  boundaries,
244
- async function ({ value }, { fetchExternalData }) {
246
+ fn: async function ({ value }, { fetchExternalData }) {
245
247
  const externalData: number = await fetchExternalData(value)
246
248
  return value * externalData
247
249
  }
248
- )
250
+ })
249
251
 
250
252
  // Run task with value 2
251
253
  await multiplyTask.run({ value: 2 })
@@ -308,18 +310,17 @@ describe('Boundaries tasks tests', () => {
308
310
  expect(finalSortedTape[2].output).toBe(8)
309
311
 
310
312
  // Verify tape can be used for replay in proxy-pass mode
311
- const replayTask = createTask(
313
+ const replayTask = createTask({
314
+ name: 'replayTask',
312
315
  schema,
313
316
  boundaries,
314
- async function ({ value }, { fetchExternalData }) {
317
+ fn: async function ({ value }, { fetchExternalData }) {
315
318
  const externalData: number = await fetchExternalData(value)
316
319
  return value * externalData
317
320
  },
318
- {
319
- boundariesData: boundariesData3 as BoundaryTapeData,
320
- mode: 'proxy-pass'
321
- }
322
- )
321
+ boundariesData: boundariesData3 as BoundaryTapeData,
322
+ mode: 'proxy-pass'
323
+ })
323
324
 
324
325
  // Run task with all three values from the tape
325
326
  const result2 = await replayTask.run({ value: 2 })