@forgehive/task 0.1.5 → 0.1.6
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.
- package/dist/index.d.ts +27 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +149 -69
- package/dist/index.js.map +1 -1
- package/dist/test/safe-run.test.d.ts +2 -0
- package/dist/test/safe-run.test.d.ts.map +1 -0
- package/dist/test/safe-run.test.js +159 -0
- package/dist/test/safe-run.test.js.map +1 -0
- package/dist/test/task-boundary-mocking.test.d.ts +2 -0
- package/dist/test/task-boundary-mocking.test.d.ts.map +1 -0
- package/dist/test/task-boundary-mocking.test.js +87 -0
- package/dist/test/task-boundary-mocking.test.js.map +1 -0
- package/dist/test/task-with-boundaries.test.js +135 -0
- package/dist/test/task-with-boundaries.test.js.map +1 -1
- package/dist/utils/mock.d.ts +17 -0
- package/dist/utils/mock.d.ts.map +1 -0
- package/dist/utils/mock.js +34 -0
- package/dist/utils/mock.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +183 -93
- package/src/test/safe-run.test.ts +214 -0
- package/src/test/task-boundary-mocking.test.ts +116 -0
- package/src/test/task-with-boundaries.test.ts +198 -1
- package/src/utils/mock.ts +45 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createTask, Schema } from '../index'
|
|
2
|
+
import { createMockBoundary } from '../utils/mock'
|
|
3
|
+
|
|
4
|
+
describe('Task boundary mocking', () => {
|
|
5
|
+
it('can mock specific boundaries for testing', async () => {
|
|
6
|
+
// Create a schema for the task
|
|
7
|
+
const schema = new Schema({
|
|
8
|
+
value: Schema.number()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
// Define the boundaries
|
|
12
|
+
const boundaries = {
|
|
13
|
+
fetchExternalData: async (int: number): Promise<number> => {
|
|
14
|
+
// This would normally fetch data from an external source
|
|
15
|
+
return int * 2
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Create the task using createTask
|
|
20
|
+
const multiplyTask = createTask(
|
|
21
|
+
schema,
|
|
22
|
+
boundaries,
|
|
23
|
+
async function ({ value }, { fetchExternalData }) {
|
|
24
|
+
const result = value * await fetchExternalData(value)
|
|
25
|
+
return result
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
// Create mock for fetchExternalData boundary that returns a specific value
|
|
30
|
+
const mockFetchData = jest.fn().mockResolvedValue(5)
|
|
31
|
+
const wrappedMockFetchData = createMockBoundary(mockFetchData)
|
|
32
|
+
|
|
33
|
+
// Mock only the fetchExternalData boundary, leaving logData boundary as is
|
|
34
|
+
multiplyTask.mockBoundary('fetchExternalData', wrappedMockFetchData)
|
|
35
|
+
|
|
36
|
+
// Run the task with mocked boundary
|
|
37
|
+
const result = await multiplyTask.run({ value: 3 })
|
|
38
|
+
|
|
39
|
+
// Verify the correct result was returned
|
|
40
|
+
// Since fetchExternalData is mocked to always return 5, result should be 3 * 5 = 15
|
|
41
|
+
expect(result).toBe(15)
|
|
42
|
+
|
|
43
|
+
// Verify the mock was called with correct arguments
|
|
44
|
+
expect(mockFetchData).toHaveBeenCalledWith(3)
|
|
45
|
+
expect(mockFetchData).toHaveBeenCalledTimes(1)
|
|
46
|
+
|
|
47
|
+
// Reset the mocks
|
|
48
|
+
multiplyTask.resetMocks()
|
|
49
|
+
|
|
50
|
+
// Run the task again, now with original boundaries
|
|
51
|
+
const result2 = await multiplyTask.run({ value: 3 })
|
|
52
|
+
|
|
53
|
+
// With original boundaries, result should be 3 * (3 * 2) = 18
|
|
54
|
+
expect(result2).toBe(18)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('can mock multiple boundaries at once', async () => {
|
|
58
|
+
// Create a schema for the task
|
|
59
|
+
const schema = new Schema({
|
|
60
|
+
value: Schema.number()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Define the boundaries
|
|
64
|
+
const boundaries = {
|
|
65
|
+
doubleValue: async (int: number): Promise<number> => {
|
|
66
|
+
return int * 2
|
|
67
|
+
},
|
|
68
|
+
tripleValue: async (int: number): Promise<number> => {
|
|
69
|
+
return int * 3
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Create the task
|
|
74
|
+
const calculateTask = createTask(
|
|
75
|
+
schema,
|
|
76
|
+
boundaries,
|
|
77
|
+
async function ({ value }, { doubleValue, tripleValue }) {
|
|
78
|
+
const doubled = await doubleValue(value)
|
|
79
|
+
const tripled = await tripleValue(value)
|
|
80
|
+
return doubled + tripled
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
// Create wrapped mock functions
|
|
85
|
+
const mockDoubleValue = jest.fn().mockResolvedValue(10)
|
|
86
|
+
const mockTripleValue = jest.fn().mockResolvedValue(20)
|
|
87
|
+
|
|
88
|
+
// Mock both boundaries
|
|
89
|
+
calculateTask.mockBoundary('doubleValue', createMockBoundary(mockDoubleValue))
|
|
90
|
+
calculateTask.mockBoundary('tripleValue', createMockBoundary(mockTripleValue))
|
|
91
|
+
|
|
92
|
+
// Run the task with both mocked boundaries
|
|
93
|
+
const result = await calculateTask.run({ value: 5 })
|
|
94
|
+
|
|
95
|
+
// Result should be 10 + 20 = 30
|
|
96
|
+
expect(result).toBe(30)
|
|
97
|
+
|
|
98
|
+
// Reset only the doubleValue mock
|
|
99
|
+
calculateTask.resetMock('doubleValue')
|
|
100
|
+
|
|
101
|
+
// Run the task with only tripleValue mocked
|
|
102
|
+
const result2 = await calculateTask.run({ value: 5 })
|
|
103
|
+
|
|
104
|
+
// Result should be (5 * 2) + 20 = 30
|
|
105
|
+
expect(result2).toBe(30)
|
|
106
|
+
|
|
107
|
+
// Reset all mocks
|
|
108
|
+
calculateTask.resetMocks()
|
|
109
|
+
|
|
110
|
+
// Run the task with original boundaries
|
|
111
|
+
const result3 = await calculateTask.run({ value: 5 })
|
|
112
|
+
|
|
113
|
+
// Result should be (5 * 2) + (5 * 3) = 25
|
|
114
|
+
expect(result3).toBe(25)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createTask, Schema } from '../index'
|
|
1
|
+
import { createTask, Schema, TaskRecord } from '../index'
|
|
2
2
|
|
|
3
3
|
// Need to add proxy cache mode to the boundaries
|
|
4
4
|
describe('Boundaries tasks tests', () => {
|
|
@@ -134,4 +134,201 @@ describe('Boundaries tasks tests', () => {
|
|
|
134
134
|
expect(six).toBe(6)
|
|
135
135
|
expect(fifteen).toBe(15)
|
|
136
136
|
})
|
|
137
|
+
|
|
138
|
+
it('Multiple parallel task runs with boundaries', async () => {
|
|
139
|
+
// Define a type for the boundary data structure we expect
|
|
140
|
+
type BoundaryData = {
|
|
141
|
+
input: unknown[];
|
|
142
|
+
output?: unknown;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Define a type for the record boundaries
|
|
146
|
+
interface RecordBoundaries {
|
|
147
|
+
fetchExternalData: BoundaryData[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Use the correct type definition for records
|
|
151
|
+
const records: TaskRecord<{value: number}, Promise<number>>[] = []
|
|
152
|
+
|
|
153
|
+
// Create a schema for the task that accepts a number
|
|
154
|
+
const schema = new Schema({
|
|
155
|
+
value: Schema.number()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Define the boundaries with a function that returns different values based on input
|
|
159
|
+
const boundaries = {
|
|
160
|
+
fetchExternalData: async (int: number): Promise<number> => {
|
|
161
|
+
return int * 2
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Create the task using createTask
|
|
166
|
+
const multiplyTask = createTask(
|
|
167
|
+
schema,
|
|
168
|
+
boundaries,
|
|
169
|
+
async function ({ value }, { fetchExternalData}) {
|
|
170
|
+
const externalData: number = await fetchExternalData(value)
|
|
171
|
+
return value * externalData
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
multiplyTask.addListener((record) => {
|
|
176
|
+
records.push(record)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// Run multiple tasks in parallel
|
|
180
|
+
const results = await Promise.all([
|
|
181
|
+
multiplyTask.run({ value: 2 }),
|
|
182
|
+
multiplyTask.run({ value: 3 }),
|
|
183
|
+
multiplyTask.run({ value: 4 })
|
|
184
|
+
])
|
|
185
|
+
|
|
186
|
+
// Check the results
|
|
187
|
+
expect(results).toEqual([8, 18, 32])
|
|
188
|
+
|
|
189
|
+
// Test records array
|
|
190
|
+
// Should have exactly 3 elements (one for each task run)
|
|
191
|
+
expect(records.length).toBe(3)
|
|
192
|
+
|
|
193
|
+
// Sort records by input value for consistent testing
|
|
194
|
+
const sortedRecords = [...records].sort((a, b) => a.input.value - b.input.value)
|
|
195
|
+
|
|
196
|
+
// Check record for first task (value: 2)
|
|
197
|
+
expect(sortedRecords[0].input).toEqual({ value: 2 })
|
|
198
|
+
expect(sortedRecords[0].output).toBe(8)
|
|
199
|
+
|
|
200
|
+
// Use type assertion to access the boundary data safely
|
|
201
|
+
const boundaries0 = sortedRecords[0].boundaries as unknown as RecordBoundaries
|
|
202
|
+
expect(boundaries0.fetchExternalData).toHaveLength(1)
|
|
203
|
+
expect(boundaries0.fetchExternalData[0].input).toEqual([2])
|
|
204
|
+
expect(boundaries0.fetchExternalData[0].output).toBe(4)
|
|
205
|
+
|
|
206
|
+
// Check record for second task (value: 3)
|
|
207
|
+
expect(sortedRecords[1].input).toEqual({ value: 3 })
|
|
208
|
+
expect(sortedRecords[1].output).toBe(18)
|
|
209
|
+
|
|
210
|
+
// Use type assertion to access the boundary data safely
|
|
211
|
+
const boundaries1 = sortedRecords[1].boundaries as unknown as RecordBoundaries
|
|
212
|
+
expect(boundaries1.fetchExternalData).toHaveLength(1)
|
|
213
|
+
expect(boundaries1.fetchExternalData[0].input).toEqual([3])
|
|
214
|
+
expect(boundaries1.fetchExternalData[0].output).toBe(6)
|
|
215
|
+
|
|
216
|
+
// Check record for third task (value: 4)
|
|
217
|
+
expect(sortedRecords[2].input).toEqual({ value: 4 })
|
|
218
|
+
expect(sortedRecords[2].output).toBe(32)
|
|
219
|
+
|
|
220
|
+
// Use type assertion to access the boundary data safely
|
|
221
|
+
const boundaries2 = sortedRecords[2].boundaries as unknown as RecordBoundaries
|
|
222
|
+
expect(boundaries2.fetchExternalData).toHaveLength(1)
|
|
223
|
+
expect(boundaries2.fetchExternalData[0].input).toEqual([4])
|
|
224
|
+
expect(boundaries2.fetchExternalData[0].output).toBe(8)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('Boundary data accumulates run data correctly', async () => {
|
|
228
|
+
// Create a schema for the task that accepts a number
|
|
229
|
+
const schema = new Schema({
|
|
230
|
+
value: Schema.number()
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// Define the boundaries
|
|
234
|
+
const boundaries = {
|
|
235
|
+
fetchExternalData: async (int: number): Promise<number> => {
|
|
236
|
+
return int * 2
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Create the task using createTask
|
|
241
|
+
const multiplyTask = createTask(
|
|
242
|
+
schema,
|
|
243
|
+
boundaries,
|
|
244
|
+
async function ({ value }, { fetchExternalData }) {
|
|
245
|
+
const externalData: number = await fetchExternalData(value)
|
|
246
|
+
return value * externalData
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
// Run task with value 2
|
|
251
|
+
await multiplyTask.run({ value: 2 })
|
|
252
|
+
|
|
253
|
+
// Get boundary data after first run
|
|
254
|
+
const boundariesData1 = multiplyTask.getBondariesData()
|
|
255
|
+
|
|
256
|
+
// Verify data structure
|
|
257
|
+
expect(boundariesData1).toHaveProperty('fetchExternalData')
|
|
258
|
+
expect(Array.isArray(boundariesData1.fetchExternalData)).toBe(true)
|
|
259
|
+
expect(boundariesData1.fetchExternalData).toHaveLength(1)
|
|
260
|
+
|
|
261
|
+
// Verify the tape entry for first run
|
|
262
|
+
const firstRunTape = boundariesData1.fetchExternalData as Array<{input: unknown[], output: unknown}>
|
|
263
|
+
expect(firstRunTape[0].input).toEqual([2])
|
|
264
|
+
expect(firstRunTape[0].output).toBe(4)
|
|
265
|
+
|
|
266
|
+
// Run task with value 3
|
|
267
|
+
await multiplyTask.run({ value: 3 })
|
|
268
|
+
|
|
269
|
+
// Get boundary data after second run
|
|
270
|
+
const boundariesData2 = multiplyTask.getBondariesData()
|
|
271
|
+
|
|
272
|
+
// Tape should now have 2 entries
|
|
273
|
+
expect(boundariesData2.fetchExternalData).toHaveLength(2)
|
|
274
|
+
|
|
275
|
+
// Sort the tape by input value for consistent testing
|
|
276
|
+
const secondRunTape = boundariesData2.fetchExternalData as Array<{input: unknown[], output: unknown}>
|
|
277
|
+
const sortedTape = [...secondRunTape].sort((a, b) => (a.input[0] as number) - (b.input[0] as number))
|
|
278
|
+
|
|
279
|
+
// First entry should still be the same
|
|
280
|
+
expect(sortedTape[0].input).toEqual([2])
|
|
281
|
+
expect(sortedTape[0].output).toBe(4)
|
|
282
|
+
|
|
283
|
+
// Second entry should be from the second run
|
|
284
|
+
expect(sortedTape[1].input).toEqual([3])
|
|
285
|
+
expect(sortedTape[1].output).toBe(6)
|
|
286
|
+
|
|
287
|
+
// Run task with value 4
|
|
288
|
+
await multiplyTask.run({ value: 4 })
|
|
289
|
+
|
|
290
|
+
// Get boundary data after third run
|
|
291
|
+
const boundariesData3 = multiplyTask.getBondariesData()
|
|
292
|
+
|
|
293
|
+
// Tape should now have 3 entries
|
|
294
|
+
expect(boundariesData3.fetchExternalData).toHaveLength(3)
|
|
295
|
+
|
|
296
|
+
// Sort the tape again
|
|
297
|
+
const thirdRunTape = boundariesData3.fetchExternalData as Array<{input: unknown[], output: unknown}>
|
|
298
|
+
const finalSortedTape = [...thirdRunTape].sort((a, b) => (a.input[0] as number) - (b.input[0] as number))
|
|
299
|
+
|
|
300
|
+
// Verify all three entries
|
|
301
|
+
expect(finalSortedTape[0].input).toEqual([2])
|
|
302
|
+
expect(finalSortedTape[0].output).toBe(4)
|
|
303
|
+
|
|
304
|
+
expect(finalSortedTape[1].input).toEqual([3])
|
|
305
|
+
expect(finalSortedTape[1].output).toBe(6)
|
|
306
|
+
|
|
307
|
+
expect(finalSortedTape[2].input).toEqual([4])
|
|
308
|
+
expect(finalSortedTape[2].output).toBe(8)
|
|
309
|
+
|
|
310
|
+
// Verify tape can be used for replay in proxy-pass mode
|
|
311
|
+
const replayTask = createTask(
|
|
312
|
+
schema,
|
|
313
|
+
boundaries,
|
|
314
|
+
async function ({ value }, { fetchExternalData }) {
|
|
315
|
+
const externalData: number = await fetchExternalData(value)
|
|
316
|
+
return value * externalData
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
boundariesData: boundariesData3,
|
|
320
|
+
mode: 'proxy-pass'
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
// Run task with all three values from the tape
|
|
325
|
+
const result2 = await replayTask.run({ value: 2 })
|
|
326
|
+
const result3 = await replayTask.run({ value: 3 })
|
|
327
|
+
const result4 = await replayTask.run({ value: 4 })
|
|
328
|
+
|
|
329
|
+
// Results should match original runs
|
|
330
|
+
expect(result2).toBe(8)
|
|
331
|
+
expect(result3).toBe(18)
|
|
332
|
+
expect(result4).toBe(32)
|
|
333
|
+
})
|
|
137
334
|
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type WrappedBoundaryFunction, type Mode, type BoundaryRecord } from './boundary'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a wrapped boundary function from any function
|
|
5
|
+
* This is framework-agnostic and can be used with or without testing libraries like Jest
|
|
6
|
+
*
|
|
7
|
+
* @param fn The function to wrap as a boundary
|
|
8
|
+
* @param options Optional configuration for the mock boundary
|
|
9
|
+
* @returns A function wrapped as a WrappedBoundaryFunction
|
|
10
|
+
*/
|
|
11
|
+
export function createMockBoundary<T extends (...args: unknown[]) => unknown>(
|
|
12
|
+
fn: T,
|
|
13
|
+
options: {
|
|
14
|
+
getTape?: () => Array<BoundaryRecord>,
|
|
15
|
+
setTape?: (tape: Array<BoundaryRecord>) => void,
|
|
16
|
+
getMode?: () => Mode,
|
|
17
|
+
setMode?: (mode: Mode) => void,
|
|
18
|
+
getRunData?: () => Array<BoundaryRecord>
|
|
19
|
+
} = {}
|
|
20
|
+
): WrappedBoundaryFunction {
|
|
21
|
+
// Use provided functions or create simple implementations
|
|
22
|
+
const mockGetTape = options.getTape || ((): BoundaryRecord[] => [])
|
|
23
|
+
const mockSetTape = options.setTape || ((_tape: BoundaryRecord[]): void => {})
|
|
24
|
+
const mockGetMode = options.getMode || ((): Mode => 'proxy')
|
|
25
|
+
const mockSetMode = options.setMode || ((_mode: Mode): void => {})
|
|
26
|
+
const mockGetRunData = options.getRunData || ((): BoundaryRecord[] => [])
|
|
27
|
+
|
|
28
|
+
// Empty functions for start/stop run
|
|
29
|
+
const mockStartRun = (): void => {}
|
|
30
|
+
const mockStopRun = (): void => {}
|
|
31
|
+
|
|
32
|
+
// Cast the function to a WrappedBoundaryFunction
|
|
33
|
+
const wrappedFn = fn as unknown as WrappedBoundaryFunction
|
|
34
|
+
|
|
35
|
+
// Add required methods to satisfy the interface
|
|
36
|
+
wrappedFn.getTape = mockGetTape
|
|
37
|
+
wrappedFn.setTape = mockSetTape
|
|
38
|
+
wrappedFn.getMode = mockGetMode
|
|
39
|
+
wrappedFn.setMode = mockSetMode
|
|
40
|
+
wrappedFn.startRun = mockStartRun
|
|
41
|
+
wrappedFn.stopRun = mockStopRun
|
|
42
|
+
wrappedFn.getRunData = mockGetRunData
|
|
43
|
+
|
|
44
|
+
return wrappedFn
|
|
45
|
+
}
|