@forgehive/task 0.2.0 → 0.2.2

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,28 +1,31 @@
1
- import { Task, type TaskRecord } from '../index'
2
- import { Schema, type InferSchema } from '@forgehive/schema'
3
-
4
- describe('Listener tests', () => {
5
- it('Should record one item', async () => {
6
- const tape: TaskRecord<{ value: number }, { value: number }>[] = []
7
- const task = new Task(function (_argv: { value: number }) {
8
- return _argv
1
+ import { Task, type ExecutionRecord } from '../index'
2
+
3
+ describe('Add listener', () => {
4
+ it('Should add a listener to the task', async () => {
5
+ const tape: ExecutionRecord[] = []
6
+ const task = new Task(async (argv: { value: number }) => {
7
+ return { value: argv.value }
9
8
  })
10
9
 
11
10
  task.addListener((record) => {
12
11
  tape.push(record)
13
12
  })
14
13
 
15
- await task.run({ value: 5 })
16
-
17
- expect(tape.length).toBe(1)
18
- expect(tape[0].input).toEqual({ value: 5 })
19
- expect(tape[0].output).toEqual({ value: 5 })
14
+ await task.run({ value: 1 })
15
+ expect(tape).toEqual([{
16
+ input: { value: 1 },
17
+ output: { value: 1 },
18
+ boundaries: {},
19
+ taskName: undefined,
20
+ metadata: {},
21
+ type: 'success'
22
+ }])
20
23
  })
21
24
 
22
- it('Should record execution error', async () => {
23
- const tape: TaskRecord<{ value: number }, never>[] = []
24
- const task = new Task(function (_argv: { value: number }) {
25
- throw new Error('This should happen')
25
+ it('Should add a listener to the task and catch error', async () => {
26
+ const tape: ExecutionRecord[] = []
27
+ const task = new Task(async (_argv: { value: number }) => {
28
+ throw new Error('Test error')
26
29
  })
27
30
 
28
31
  task.addListener((record) => {
@@ -30,83 +33,106 @@ describe('Listener tests', () => {
30
33
  })
31
34
 
32
35
  try {
33
- await task.run({ value: 5 })
34
- } catch (e) {
35
- // Error is expected
36
+ await task.run({ value: 1 })
37
+ } catch (error) {
38
+ // Expected error
36
39
  }
37
40
 
38
- expect(tape.length).toBe(1)
39
- expect(tape[0].input).toEqual({ value: 5 })
40
- expect(tape[0].error).toBe('This should happen')
41
+ expect(tape).toEqual([{
42
+ input: { value: 1 },
43
+ error: 'Test error',
44
+ boundaries: {},
45
+ taskName: undefined,
46
+ metadata: {},
47
+ type: 'error'
48
+ }])
41
49
  })
42
50
 
43
- it('Should record validation error', async () => {
44
- const tape: TaskRecord<{ value: number | null }, { value: number }>[] = []
45
- const schema = new Schema({
46
- value: Schema.number().min(5)
47
- })
48
-
49
- const task = new Task(function (_argv: InferSchema<typeof schema>) {
50
- return _argv
51
- }, {
52
- schema
51
+ it('Should add a listener to the task and catch error with null input', async () => {
52
+ const tape: ExecutionRecord[] = []
53
+ const task = new Task(async (argv: { value: number | null }) => {
54
+ if (argv.value === null) {
55
+ throw new Error('Value is null')
56
+ }
57
+ return { value: argv.value }
53
58
  })
54
59
 
55
- task.addListener<{ value: number | null }, { value: number }>((record) => {
60
+ task.addListener((record) => {
56
61
  tape.push(record)
57
62
  })
58
63
 
59
64
  try {
60
- await task.run({ value: 3 })
61
- } catch (e) {
62
- // Error is expected
65
+ await task.run({ value: null })
66
+ } catch (error) {
67
+ // Expected error
63
68
  }
64
69
 
65
- expect(tape.length).toBe(1)
66
- expect(tape[0].input).toEqual({ value: 3 })
67
- expect(tape[0].error).toContain('Invalid input on:')
68
- expect(tape[0].error).toContain('value:')
69
- expect(tape[0].error).toContain('Number must be greater than or equal to 5')
70
+ expect(tape).toEqual([{
71
+ input: { value: null },
72
+ error: 'Value is null',
73
+ boundaries: {},
74
+ taskName: undefined,
75
+ metadata: {},
76
+ type: 'error'
77
+ }])
70
78
  })
71
79
 
72
- it('Should record multiple records', async () => {
73
- const tape: TaskRecord<{ value: number }, { value: number }>[] = []
74
- const task = new Task(function (_argv: { value: number }) {
75
- return _argv
80
+ it('Should add a listener to the task and call twice', async () => {
81
+ const tape: ExecutionRecord[] = []
82
+ const task = new Task(async (argv: { value: number }) => {
83
+ return { value: argv.value }
76
84
  })
77
85
 
78
86
  task.addListener((record) => {
79
87
  tape.push(record)
80
88
  })
81
89
 
82
- await task.run({ value: 5 })
83
- await task.run({ value: 6 })
84
-
85
- expect(tape.length).toBe(2)
86
- expect(tape[0].input).toEqual({ value: 5 })
87
- expect(tape[0].output).toEqual({ value: 5 })
88
-
89
- expect(tape[1].input).toEqual({ value: 6 })
90
- expect(tape[1].output).toEqual({ value: 6 })
90
+ await task.run({ value: 1 })
91
+ await task.run({ value: 2 })
92
+
93
+ expect(tape).toEqual([
94
+ {
95
+ input: { value: 1 },
96
+ output: { value: 1 },
97
+ boundaries: {},
98
+ taskName: undefined,
99
+ metadata: {},
100
+ type: 'success'
101
+ },
102
+ {
103
+ input: { value: 2 },
104
+ output: { value: 2 },
105
+ boundaries: {},
106
+ taskName: undefined,
107
+ metadata: {},
108
+ type: 'success'
109
+ }
110
+ ])
91
111
  })
92
112
 
93
- it('Should stop recording', async () => {
94
- const tape: TaskRecord<{ value: number }, { value: number }>[] = []
95
- const task = new Task(function (_argv: { value: number }) {
96
- return _argv
113
+ it('Should add a listener and remove it', async () => {
114
+ const tape: ExecutionRecord[] = []
115
+ const task = new Task(async (argv: { value: number }) => {
116
+ return { value: argv.value }
97
117
  })
98
118
 
99
119
  task.addListener((record) => {
100
120
  tape.push(record)
101
121
  })
102
122
 
103
- await task.run({ value: 5 })
123
+ await task.run({ value: 1 })
104
124
 
105
125
  task.removeListener()
106
- await task.run({ value: 6 })
107
126
 
108
- expect(tape.length).toBe(1)
109
- expect(tape[0].input).toEqual({ value: 5 })
110
- expect(tape[0].output).toEqual({ value: 5 })
127
+ await task.run({ value: 2 })
128
+
129
+ expect(tape).toEqual([{
130
+ input: { value: 1 },
131
+ output: { value: 1 },
132
+ boundaries: {},
133
+ taskName: undefined,
134
+ metadata: {},
135
+ type: 'success'
136
+ }])
111
137
  })
112
138
  })
@@ -0,0 +1,266 @@
1
+ import { createTask, Schema } from '../index'
2
+
3
+ describe('execution-record-boundaries', () => {
4
+ describe('setMetadata boundary', () => {
5
+ it('should add metadata to execution record when setMetadata is called', async () => {
6
+ // Create a schema
7
+ const schema = new Schema({
8
+ value: Schema.number()
9
+ })
10
+
11
+ // Create a task that uses setMetadata
12
+ const task = createTask({
13
+ name: 'metadata-test-task',
14
+ schema,
15
+ boundaries: {
16
+ multiply: async (value: number) => value * 2
17
+ },
18
+ fn: async ({ value }, { multiply, setMetadata }) => {
19
+ await setMetadata('userId', '12345')
20
+ await setMetadata('executionType', 'background')
21
+
22
+ const result = await multiply(value)
23
+ return { result }
24
+ }
25
+ })
26
+
27
+ // Run the task with safeRun
28
+ const [result, error, record] = await task.safeRun({ value: 5 })
29
+
30
+ // Verify the execution was successful
31
+ expect(error).toBeNull()
32
+ expect(result).toEqual({ result: 10 })
33
+
34
+ // Verify the metadata is included in the record
35
+ expect(record.metadata).toEqual({
36
+ userId: '12345',
37
+ executionType: 'background'
38
+ })
39
+
40
+ // Verify the task name and other fields are correct
41
+ expect(record.taskName).toBe('metadata-test-task')
42
+ expect(record.input).toEqual({ value: 5 })
43
+ expect(record.output).toEqual({ result: 10 })
44
+ expect(record.type).toBe('success')
45
+ })
46
+
47
+ it('should not include setMetadata calls in boundary logs', async () => {
48
+ // Create a schema
49
+ const schema = new Schema({
50
+ value: Schema.number()
51
+ })
52
+
53
+ // Create a task that uses setMetadata and other boundaries
54
+ const task = createTask({
55
+ name: 'boundary-log-test',
56
+ schema,
57
+ boundaries: {
58
+ multiply: async (value: number) => value * 2,
59
+ fetchData: async (data: string) => `fetched-${data}`
60
+ },
61
+ fn: async ({ value }, { multiply, fetchData, setMetadata }) => {
62
+ await setMetadata('step', 'start')
63
+
64
+ const multiplied = await multiply(value)
65
+ await setMetadata('step', 'multiplied')
66
+
67
+ const fetched = await fetchData('test')
68
+ await setMetadata('step', 'completed')
69
+
70
+ return { multiplied, fetched }
71
+ }
72
+ })
73
+
74
+ // Run the task with safeRun
75
+ const [result, error, record] = await task.safeRun({ value: 3 })
76
+
77
+ // Verify the execution was successful
78
+ expect(error).toBeNull()
79
+ expect(result).toEqual({ multiplied: 6, fetched: 'fetched-test' })
80
+
81
+ // Verify the metadata is updated to the final value
82
+ expect(record.metadata).toEqual({
83
+ step: 'completed'
84
+ })
85
+
86
+ // Verify that only the actual boundaries appear in the logs
87
+ expect(record.boundaries).toHaveProperty('multiply')
88
+ expect(record.boundaries).toHaveProperty('fetchData')
89
+ expect(record.boundaries).not.toHaveProperty('setMetadata')
90
+
91
+ // Verify the boundary logs contain the expected calls
92
+ expect(record.boundaries.multiply).toHaveLength(1)
93
+ expect(record.boundaries.multiply[0]).toEqual({
94
+ input: [3],
95
+ output: 6
96
+ })
97
+
98
+ expect(record.boundaries.fetchData).toHaveLength(1)
99
+ expect(record.boundaries.fetchData[0]).toEqual({
100
+ input: ['test'],
101
+ output: 'fetched-test'
102
+ })
103
+ })
104
+
105
+ it('should preserve metadata in error scenarios', async () => {
106
+ // Create a schema
107
+ const schema = new Schema({
108
+ value: Schema.number()
109
+ })
110
+
111
+ // Create a task that sets metadata before throwing an error
112
+ const task = createTask({
113
+ name: 'error-metadata-test',
114
+ schema,
115
+ boundaries: {},
116
+ fn: async ({ value }, { setMetadata }) => {
117
+ await setMetadata('errorContext', 'validation')
118
+ await setMetadata('userId', '67890')
119
+
120
+ if (value < 0) {
121
+ throw new Error('Value cannot be negative')
122
+ }
123
+
124
+ return { result: value }
125
+ }
126
+ })
127
+
128
+ // Run the task with a value that will cause an error
129
+ const [result, error, record] = await task.safeRun({ value: -1 })
130
+
131
+ // Verify the execution failed as expected
132
+ expect(result).toBeNull()
133
+ expect(error).not.toBeNull()
134
+ expect(error?.message).toBe('Value cannot be negative')
135
+
136
+ // Verify the metadata is preserved in the error record
137
+ expect(record.metadata).toEqual({
138
+ errorContext: 'validation',
139
+ userId: '67890'
140
+ })
141
+
142
+ // Verify other record fields
143
+ expect(record.taskName).toBe('error-metadata-test')
144
+ expect(record.input).toEqual({ value: -1 })
145
+ expect(record.error).toBe('Value cannot be negative')
146
+ expect(record.type).toBe('error')
147
+ })
148
+
149
+ it('should preserve existing metadata when adding new metadata', async () => {
150
+ // Create a schema
151
+ const schema = new Schema({
152
+ value: Schema.number()
153
+ })
154
+
155
+ // Create a task that uses setMetadata
156
+ const task = createTask({
157
+ name: 'preserve-metadata-test',
158
+ schema,
159
+ boundaries: {},
160
+ fn: async ({ value }, { setMetadata }) => {
161
+ await setMetadata('step1', 'completed')
162
+ await setMetadata('step2', 'in-progress')
163
+ await setMetadata('step1', 'updated') // Overwrite step1
164
+
165
+ return { result: value * 2 }
166
+ }
167
+ })
168
+
169
+ // Run the task with initial metadata context
170
+ const [result, error, record] = await task.safeRun({ value: 4 })
171
+
172
+ // Verify the execution was successful
173
+ expect(error).toBeNull()
174
+ expect(result).toEqual({ result: 8 })
175
+
176
+ // Verify the metadata contains the final values
177
+ expect(record.metadata).toEqual({
178
+ step1: 'updated',
179
+ step2: 'in-progress'
180
+ })
181
+ })
182
+
183
+ it('should work with setMetadata in safeReplay', async () => {
184
+ // Create a schema
185
+ const schema = new Schema({
186
+ value: Schema.number()
187
+ })
188
+
189
+ // Use a counter to ensure different values between original and replay
190
+ let executionCounter = 0
191
+
192
+ // Create a task with boundaries and metadata
193
+ const task = createTask({
194
+ name: 'replay-metadata-test',
195
+ schema,
196
+ boundaries: {
197
+ fetchData: async (value: number) => value * 3
198
+ },
199
+ fn: async ({ value }, { fetchData, setMetadata }) => {
200
+ executionCounter++
201
+ await setMetadata('replayTest', 'true')
202
+ await setMetadata('executionNumber', executionCounter.toString())
203
+
204
+ const result = await fetchData(value)
205
+ return { result }
206
+ }
207
+ })
208
+
209
+ // First, run the task normally to get an execution record
210
+ const [, , originalRecord] = await task.safeRun({ value: 2 })
211
+
212
+ // Verify original metadata was set
213
+ expect(originalRecord.metadata).toHaveProperty('replayTest', 'true')
214
+ expect(originalRecord.metadata).toHaveProperty('executionNumber', '1')
215
+
216
+ // Now replay the task
217
+ const [replayResult, replayError, replayRecord] = await task.safeReplay(
218
+ originalRecord,
219
+ { boundaries: { fetchData: 'replay' } }
220
+ )
221
+
222
+ // Verify the replay was successful
223
+ expect(replayError).toBeNull()
224
+ expect(replayResult).toEqual({ result: 6 })
225
+
226
+ // Verify that the replay has its own metadata (setMetadata was called again)
227
+ expect(replayRecord.metadata).toHaveProperty('replayTest', 'true')
228
+ expect(replayRecord.metadata).toHaveProperty('executionNumber', '2')
229
+
230
+ // The execution numbers should be different since setMetadata was called again
231
+ expect(replayRecord.metadata?.executionNumber).not.toBe(originalRecord.metadata?.executionNumber)
232
+ expect(originalRecord.metadata?.executionNumber).toBe('1')
233
+ expect(replayRecord.metadata?.executionNumber).toBe('2')
234
+ })
235
+
236
+ it('should handle empty metadata gracefully', async () => {
237
+ // Create a schema
238
+ const schema = new Schema({
239
+ value: Schema.number()
240
+ })
241
+
242
+ // Create a task that doesn't use setMetadata
243
+ const task = createTask({
244
+ name: 'no-metadata-test',
245
+ schema,
246
+ boundaries: {
247
+ multiply: async (value: number) => value * 2
248
+ },
249
+ fn: async ({ value }, { multiply }) => {
250
+ const result = await multiply(value)
251
+ return { result }
252
+ }
253
+ })
254
+
255
+ // Run the task without using setMetadata
256
+ const [result, error, record] = await task.safeRun({ value: 7 })
257
+
258
+ // Verify the execution was successful
259
+ expect(error).toBeNull()
260
+ expect(result).toEqual({ result: 14 })
261
+
262
+ // Verify the metadata is empty but defined
263
+ expect(record.metadata).toEqual({})
264
+ })
265
+ })
266
+ })
@@ -1,4 +1,4 @@
1
- import { createTask, Schema, TaskRecord, BoundaryTapeData } from '../index'
1
+ import { createTask, Schema, ExecutionRecord, BoundaryTapeData } from '../index'
2
2
 
3
3
  // Need to add proxy cache mode to the boundaries
4
4
  describe('Boundaries tasks tests', () => {
@@ -148,7 +148,7 @@ describe('Boundaries tasks tests', () => {
148
148
  }
149
149
 
150
150
  // Use the correct type definition for records
151
- const records: TaskRecord<{value: number}, Promise<number>>[] = []
151
+ const records: ExecutionRecord[] = []
152
152
 
153
153
  // Create a schema for the task that accepts a number
154
154
  const schema = new Schema({
@@ -192,7 +192,7 @@ describe('Boundaries tasks tests', () => {
192
192
  expect(records.length).toBe(3)
193
193
 
194
194
  // Sort records by input value for consistent testing
195
- const sortedRecords = [...records].sort((a, b) => a.input.value - b.input.value)
195
+ const sortedRecords = [...records].sort((a, b) => (a.input as {value: number}).value - (b.input as {value: number}).value)
196
196
 
197
197
  // Check record for first task (value: 2)
198
198
  expect(sortedRecords[0].input).toEqual({ value: 2 })