@forgehive/task 0.1.5 → 0.1.7
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 +50 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +142 -80
- 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 +185 -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 +206 -100
- package/src/test/safe-run.test.ts +241 -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
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface TaskConfig<B extends Boundaries = Boundaries> {
|
|
|
23
23
|
boundariesData?: Record<string, unknown>
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// ToDo: Add a type for the boundaries data
|
|
26
27
|
/**
|
|
27
28
|
* Represents the record passed to task listeners
|
|
28
29
|
*/
|
|
@@ -37,7 +38,37 @@ export interface TaskRecord<InputType = unknown, OutputType = unknown> {
|
|
|
37
38
|
boundaries?: Record<string, unknown>;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
// Make BoundaryLog generic
|
|
42
|
+
export type BoundaryLog<I extends unknown[] = unknown[], O = unknown> = {
|
|
43
|
+
input: I
|
|
44
|
+
output: O | null
|
|
45
|
+
error: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Mapped type for boundaries
|
|
49
|
+
export type BoundaryLogsFor<B extends Boundaries> = {
|
|
50
|
+
[K in keyof B]: B[K] extends (...args: infer I) => Promise<infer O>
|
|
51
|
+
? BoundaryLog<I, O>[]
|
|
52
|
+
: BoundaryLog[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Represents the execution record of a task, including input, output, error, and boundary data
|
|
57
|
+
*/
|
|
58
|
+
export interface ExecutionRecord<InputType = unknown, OutputType = unknown, B extends Boundaries = Boundaries> {
|
|
59
|
+
/** The input arguments passed to the task */
|
|
60
|
+
input: InputType
|
|
61
|
+
/** The output returned by the task (if successful) */
|
|
62
|
+
output?: OutputType | null
|
|
63
|
+
/** The error message if the task failed */
|
|
64
|
+
error?: string
|
|
65
|
+
/** Boundary execution data */
|
|
66
|
+
boundaries: BoundaryLogsFor<B>
|
|
67
|
+
}
|
|
68
|
+
|
|
40
69
|
export interface TaskInstanceType<Func extends BaseFunction = BaseFunction, B extends Boundaries = Boundaries> {
|
|
70
|
+
version: string
|
|
71
|
+
|
|
41
72
|
getMode: () => Mode
|
|
42
73
|
setMode: (mode: Mode) => void
|
|
43
74
|
setSchema: (base: Schema<Record<string, SchemaType>>) => void
|
|
@@ -60,9 +91,14 @@ export interface TaskInstanceType<Func extends BaseFunction = BaseFunction, B ex
|
|
|
60
91
|
getBoundaries: () => WrappedBoundaries<B>
|
|
61
92
|
setBoundariesData: (boundariesData: Record<string, unknown>) => void
|
|
62
93
|
getBondariesData: () => Record<string, unknown>
|
|
63
|
-
|
|
64
|
-
|
|
94
|
+
|
|
95
|
+
// Mocking methods for testing
|
|
96
|
+
mockBoundary: <K extends keyof B>(name: K, mockFn: WrappedBoundaryFunction) => void
|
|
97
|
+
resetMock: <K extends keyof B>(name: K) => void
|
|
98
|
+
resetMocks: () => void
|
|
99
|
+
|
|
65
100
|
run: (argv?: Parameters<Func>[0]) => Promise<ReturnType<Func>>
|
|
101
|
+
safeRun: (argv?: Parameters<Func>[0]) => Promise<[ReturnType<Func> | null, Error | null, ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B>]>
|
|
66
102
|
}
|
|
67
103
|
|
|
68
104
|
// Helper type to infer schema type
|
|
@@ -74,18 +110,26 @@ export type TaskFunction<S, B extends Boundaries> =
|
|
|
74
110
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
111
|
(argv: InferSchemaType<S>, boundaries: WrappedBoundaries<B>) => Promise<any>;
|
|
76
112
|
|
|
113
|
+
// Define a type for the accumulated boundary data
|
|
114
|
+
type BoundaryData = Array<{input: unknown[], output?: unknown}>
|
|
115
|
+
|
|
77
116
|
export const Task = class Task<
|
|
78
117
|
B extends Boundaries = Boundaries,
|
|
79
118
|
Func extends BaseFunction = BaseFunction
|
|
80
119
|
> implements TaskInstanceType<Func, B> {
|
|
120
|
+
public version: string = '0.1.7'
|
|
121
|
+
|
|
81
122
|
_fn: Func
|
|
82
123
|
_mode: Mode
|
|
83
124
|
_coolDown: number
|
|
84
125
|
_description?: string
|
|
85
126
|
|
|
86
127
|
_boundariesDefinition: B
|
|
87
|
-
_boundaries: WrappedBoundaries<B>
|
|
88
128
|
_boundariesData: Record<string, unknown> | null
|
|
129
|
+
_accumulatedBoundariesData: Record<string, BoundaryData> = {}
|
|
130
|
+
|
|
131
|
+
// For storing mocks
|
|
132
|
+
_boundaryMocks: Record<string, WrappedBoundaryFunction> = {}
|
|
89
133
|
|
|
90
134
|
_schema: Schema<Record<string, SchemaType>> | undefined
|
|
91
135
|
_listener?: ((record: TaskRecord<Parameters<Func>[0], ReturnType<Func>>) => void) | undefined
|
|
@@ -110,13 +154,25 @@ export const Task = class Task<
|
|
|
110
154
|
// Cool down time before killing the process on cli runner
|
|
111
155
|
this._coolDown = 1000
|
|
112
156
|
|
|
113
|
-
//
|
|
157
|
+
// Initialize boundaries data
|
|
114
158
|
this._boundariesData = conf.boundariesData ?? null
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
159
|
+
|
|
160
|
+
// Initialize empty accumulated boundaries data structure
|
|
161
|
+
for (const name in this._boundariesDefinition) {
|
|
162
|
+
this._accumulatedBoundariesData[name] = []
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Initialize accumulated boundaries data from initial boundaries data
|
|
166
|
+
if (this._boundariesData) {
|
|
167
|
+
// Type assertion to handle initial data safely
|
|
168
|
+
for (const name in this._boundariesData) {
|
|
169
|
+
if (Array.isArray(this._boundariesData[name])) {
|
|
170
|
+
this._accumulatedBoundariesData[name] = this._boundariesData[name] as BoundaryData
|
|
171
|
+
} else {
|
|
172
|
+
this._accumulatedBoundariesData[name] = []
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
120
176
|
}
|
|
121
177
|
|
|
122
178
|
getMode (): Mode {
|
|
@@ -124,12 +180,6 @@ export const Task = class Task<
|
|
|
124
180
|
}
|
|
125
181
|
|
|
126
182
|
setMode (mode: Mode): void {
|
|
127
|
-
for (const name in this._boundaries) {
|
|
128
|
-
const boundary = this._boundaries[name]
|
|
129
|
-
|
|
130
|
-
boundary.setMode(mode)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
183
|
this._mode = mode
|
|
134
184
|
}
|
|
135
185
|
|
|
@@ -188,45 +238,60 @@ export const Task = class Task<
|
|
|
188
238
|
emit (data: Partial<TaskRecord>): void {
|
|
189
239
|
if (typeof this._listener === 'undefined') { return }
|
|
190
240
|
|
|
191
|
-
|
|
192
|
-
...data,
|
|
193
|
-
boundaries: this.getBondariesRunLog()
|
|
194
|
-
} as TaskRecord<Parameters<Func>[0], ReturnType<Func>>
|
|
195
|
-
|
|
196
|
-
this._listener(event)
|
|
241
|
+
this._listener(data as TaskRecord<Parameters<Func>[0], ReturnType<Func>>)
|
|
197
242
|
}
|
|
198
243
|
|
|
199
244
|
getBoundaries (): WrappedBoundaries<B> {
|
|
200
|
-
|
|
245
|
+
// Create fresh boundaries when requested
|
|
246
|
+
return this._createBounderies({
|
|
247
|
+
definition: this._boundariesDefinition,
|
|
248
|
+
baseData: this._boundariesData,
|
|
249
|
+
mode: this._mode
|
|
250
|
+
})
|
|
201
251
|
}
|
|
202
252
|
|
|
203
253
|
setBoundariesData (boundariesData: Record<string, unknown>): void {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
214
|
-
boundary.setTape(tape as any)
|
|
254
|
+
this._boundariesData = boundariesData
|
|
255
|
+
|
|
256
|
+
// Update accumulated data as well
|
|
257
|
+
// Type assertion to handle provided data safely
|
|
258
|
+
for (const name in boundariesData) {
|
|
259
|
+
if (Array.isArray(boundariesData[name])) {
|
|
260
|
+
this._accumulatedBoundariesData[name] = boundariesData[name] as BoundaryData
|
|
261
|
+
} else {
|
|
262
|
+
this._accumulatedBoundariesData[name] = []
|
|
215
263
|
}
|
|
216
264
|
}
|
|
217
265
|
}
|
|
218
266
|
|
|
219
267
|
getBondariesData (): Record<string, unknown> {
|
|
220
|
-
|
|
221
|
-
|
|
268
|
+
return this._accumulatedBoundariesData
|
|
269
|
+
}
|
|
222
270
|
|
|
223
|
-
|
|
224
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Mocks a specific boundary function for testing
|
|
273
|
+
* @param name The name of the boundary to mock
|
|
274
|
+
* @param mockFn The mock function to use
|
|
275
|
+
*/
|
|
276
|
+
mockBoundary<K extends keyof B>(name: K, mockFn: WrappedBoundaryFunction): void {
|
|
277
|
+
this._boundaryMocks[name as string] = mockFn
|
|
278
|
+
}
|
|
225
279
|
|
|
226
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Resets a specific mocked boundary back to its original function
|
|
282
|
+
* @param name The name of the boundary to reset
|
|
283
|
+
*/
|
|
284
|
+
resetMock<K extends keyof B>(name: K): void {
|
|
285
|
+
if (this._boundaryMocks[name as string]) {
|
|
286
|
+
delete this._boundaryMocks[name as string]
|
|
227
287
|
}
|
|
288
|
+
}
|
|
228
289
|
|
|
229
|
-
|
|
290
|
+
/**
|
|
291
|
+
* Resets all mocked boundaries back to their original functions
|
|
292
|
+
*/
|
|
293
|
+
resetMocks(): void {
|
|
294
|
+
this._boundaryMocks = {}
|
|
230
295
|
}
|
|
231
296
|
|
|
232
297
|
_createBounderies ({
|
|
@@ -241,6 +306,13 @@ export const Task = class Task<
|
|
|
241
306
|
const boundariesFns: Record<string, WrappedBoundaryFunction> = {}
|
|
242
307
|
|
|
243
308
|
for (const name in definition) {
|
|
309
|
+
// Check if we have a mock for this boundary
|
|
310
|
+
if (this._boundaryMocks[name]) {
|
|
311
|
+
boundariesFns[name] = this._boundaryMocks[name]
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Otherwise create the normal boundary
|
|
244
316
|
const boundary = createBoundary(definition[name])
|
|
245
317
|
|
|
246
318
|
if (baseData !== null && typeof baseData[name] !== 'undefined') {
|
|
@@ -257,87 +329,121 @@ export const Task = class Task<
|
|
|
257
329
|
return boundariesFns as WrappedBoundaries<B>
|
|
258
330
|
}
|
|
259
331
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
332
|
+
asBoundary (): (args: Parameters<Func>[0]) => Promise<ReturnType<Func>> {
|
|
333
|
+
return async (args: Parameters<Func>[0]): Promise<ReturnType<Func>> => {
|
|
334
|
+
return await this.run(args)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async safeRun (argv?: Parameters<Func>[0]): Promise<[
|
|
339
|
+
ReturnType<Func> | null,
|
|
340
|
+
Error | null,
|
|
341
|
+
ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B>
|
|
342
|
+
]> {
|
|
343
|
+
// Initialize log item
|
|
344
|
+
const logItem: ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B> = {
|
|
345
|
+
input: argv as Parameters<Func>[0],
|
|
346
|
+
boundaries: {} as BoundaryLogsFor<B>
|
|
347
|
+
}
|
|
263
348
|
|
|
264
|
-
|
|
265
|
-
|
|
349
|
+
// Create fresh boundaries for this execution
|
|
350
|
+
const executionBoundaries = this._createBounderies({
|
|
351
|
+
definition: this._boundariesDefinition,
|
|
352
|
+
baseData: this._boundariesData,
|
|
353
|
+
mode: this._mode
|
|
354
|
+
})
|
|
266
355
|
|
|
267
|
-
|
|
356
|
+
// Start run for each boundary
|
|
357
|
+
for (const name in executionBoundaries) {
|
|
358
|
+
const boundary = executionBoundaries[name]
|
|
359
|
+
boundary.startRun()
|
|
268
360
|
}
|
|
269
361
|
|
|
270
|
-
|
|
271
|
-
|
|
362
|
+
// Handle schema validation
|
|
363
|
+
if (this._schema) {
|
|
364
|
+
const validation = this._schema.safeParse(argv)
|
|
365
|
+
if (!validation.success) {
|
|
366
|
+
const errorDetails = validation.error?.errors.map(err =>
|
|
367
|
+
`${err.path.join('.')}: ${err.message}`
|
|
368
|
+
).join(', ')
|
|
272
369
|
|
|
273
|
-
|
|
274
|
-
|
|
370
|
+
const errorMessage = errorDetails
|
|
371
|
+
? `Invalid input on: ${errorDetails}`
|
|
372
|
+
: 'Invalid input'
|
|
275
373
|
|
|
276
|
-
|
|
277
|
-
|
|
374
|
+
logItem.error = errorMessage
|
|
375
|
+
logItem.boundaries = {} as BoundaryLogsFor<B>
|
|
278
376
|
|
|
279
|
-
|
|
377
|
+
// Add boundary elements empty
|
|
378
|
+
for (const name in executionBoundaries) {
|
|
379
|
+
logItem.boundaries[name as keyof B] = [] as unknown as BoundaryLogsFor<B>[typeof name]
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
this.emit(logItem)
|
|
383
|
+
return [null, new Error(errorMessage), logItem]
|
|
384
|
+
}
|
|
280
385
|
}
|
|
281
|
-
}
|
|
282
386
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
387
|
+
let output: ReturnType<Func> | null = null
|
|
388
|
+
let error: Error | null = null
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
// Execute the task function
|
|
392
|
+
output = await this._fn(
|
|
393
|
+
argv as Parameters<Func>[0],
|
|
394
|
+
executionBoundaries as unknown as Parameters<Func>[1]
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
logItem.output = output
|
|
398
|
+
} catch (caughtError) {
|
|
399
|
+
const errorMessage = caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
400
|
+
logItem.error = errorMessage
|
|
401
|
+
error = new Error(errorMessage)
|
|
286
402
|
}
|
|
287
|
-
}
|
|
288
403
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
this.startRunLog()
|
|
292
|
-
const boundaries = this._boundaries
|
|
404
|
+
// Process boundary data after execution (both success and error cases)
|
|
405
|
+
const boundariesRunLog: BoundaryLogsFor<B> = {} as BoundaryLogsFor<B>
|
|
293
406
|
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
407
|
+
for (const name in executionBoundaries) {
|
|
408
|
+
const boundary = executionBoundaries[name]
|
|
409
|
+
const runData = boundary.getRunData()
|
|
297
410
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
`${err.path.join('.')}: ${err.message}`
|
|
301
|
-
).join(', ')
|
|
411
|
+
// Add to the run log
|
|
412
|
+
boundariesRunLog[name as keyof B] = runData as unknown as BoundaryLogsFor<B>[typeof name]
|
|
302
413
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
414
|
+
// Accumulate in the task's total boundaries data
|
|
415
|
+
if (!this._accumulatedBoundariesData[name]) {
|
|
416
|
+
this._accumulatedBoundariesData[name] = []
|
|
417
|
+
}
|
|
306
418
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
error: errorMessage
|
|
310
|
-
})
|
|
419
|
+
// Get the current accumulated data for this boundary
|
|
420
|
+
const currentData = this._accumulatedBoundariesData[name]
|
|
311
421
|
|
|
312
|
-
|
|
313
|
-
|
|
422
|
+
// Add the new run data
|
|
423
|
+
if (Array.isArray(runData) && runData.length > 0) {
|
|
424
|
+
// Cast the run data to the correct type
|
|
425
|
+
this._accumulatedBoundariesData[name] = [...currentData, ...(runData as BoundaryData)]
|
|
314
426
|
}
|
|
427
|
+
}
|
|
315
428
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const output = await this._fn(argv as Parameters<Func>[0], boundaries as unknown as Parameters<Func>[1])
|
|
319
|
-
|
|
320
|
-
return output
|
|
321
|
-
})().then((output) => {
|
|
322
|
-
this.emit({
|
|
323
|
-
input: argv,
|
|
324
|
-
output
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
resolve(output)
|
|
328
|
-
}).catch((error) => {
|
|
329
|
-
this.emit({
|
|
330
|
-
input: argv,
|
|
331
|
-
error: error.message
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
reject(error)
|
|
335
|
-
})
|
|
336
|
-
})
|
|
429
|
+
// Set boundaries in log item before emitting
|
|
430
|
+
logItem.boundaries = boundariesRunLog
|
|
337
431
|
|
|
338
|
-
|
|
432
|
+
// Emit the log item
|
|
433
|
+
this.emit(logItem)
|
|
339
434
|
|
|
340
|
-
|
|
435
|
+
// Return the error, output and log item
|
|
436
|
+
return [output, error, logItem]
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async run (argv?: Parameters<Func>[0]): Promise<ReturnType<Func>> {
|
|
440
|
+
const [result, error] = await this.safeRun(argv)
|
|
441
|
+
|
|
442
|
+
if (error) {
|
|
443
|
+
throw error
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return result as ReturnType<Func>
|
|
341
447
|
}
|
|
342
448
|
}
|
|
343
449
|
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { createTask, Schema } from '../index'
|
|
2
|
+
|
|
3
|
+
describe('Task safeRun tests', () => {
|
|
4
|
+
it('returns [result, null, record] on successful execution', async () => {
|
|
5
|
+
// Create a simple schema
|
|
6
|
+
const schema = new Schema({
|
|
7
|
+
value: Schema.number()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
// Define the boundaries
|
|
11
|
+
const boundaries = {
|
|
12
|
+
fetchData: async (value: number): Promise<number> => {
|
|
13
|
+
return value * 2
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Create the task
|
|
18
|
+
const successTask = createTask(
|
|
19
|
+
schema,
|
|
20
|
+
boundaries,
|
|
21
|
+
async function ({ value }, { fetchData }) {
|
|
22
|
+
const result = await fetchData(value)
|
|
23
|
+
return { result, success: true }
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// Call safeRun with valid input
|
|
28
|
+
const [result, error, record] = await successTask.safeRun({ value: 5 })
|
|
29
|
+
|
|
30
|
+
// Verify success case
|
|
31
|
+
expect(error).toBeNull()
|
|
32
|
+
expect(result).toEqual({ result: 10, success: true })
|
|
33
|
+
expect(record).not.toBeNull()
|
|
34
|
+
expect(record).toHaveProperty('boundaries.fetchData')
|
|
35
|
+
expect(record.boundaries.fetchData).toHaveLength(1)
|
|
36
|
+
|
|
37
|
+
// useful to check types on record
|
|
38
|
+
const data = record.boundaries.fetchData[0]
|
|
39
|
+
expect(data.input).toEqual([5])
|
|
40
|
+
expect(data.output).toEqual(10)
|
|
41
|
+
expect(data.error).toBeUndefined()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('returns [null, error, record] on failed execution', async () => {
|
|
45
|
+
// Create a simple schema
|
|
46
|
+
const schema = new Schema({
|
|
47
|
+
value: Schema.number()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Define the boundaries with a function that will throw an error
|
|
51
|
+
const boundaries = {
|
|
52
|
+
fetchData: async (value: number): Promise<number> => {
|
|
53
|
+
if (value < 0) {
|
|
54
|
+
throw new Error('Value cannot be negative')
|
|
55
|
+
}
|
|
56
|
+
return value * 2
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create the task
|
|
61
|
+
const errorTask = createTask(
|
|
62
|
+
schema,
|
|
63
|
+
boundaries,
|
|
64
|
+
async function ({ value }, { fetchData }) {
|
|
65
|
+
const result = await fetchData(value)
|
|
66
|
+
return { result, success: true }
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
// Call safeRun with problematic input that will cause an error
|
|
71
|
+
const [result, error, record] = await errorTask.safeRun({ value: -5 })
|
|
72
|
+
|
|
73
|
+
// Verify error case
|
|
74
|
+
expect(error).not.toBeNull()
|
|
75
|
+
expect(error instanceof Error).toBe(true)
|
|
76
|
+
if (error instanceof Error) {
|
|
77
|
+
expect(error.message).toContain('Value cannot be negative')
|
|
78
|
+
}
|
|
79
|
+
expect(result).toBeNull()
|
|
80
|
+
expect(record).not.toBeNull()
|
|
81
|
+
expect(record).toHaveProperty('boundaries.fetchData')
|
|
82
|
+
expect(record.boundaries.fetchData).toHaveLength(1)
|
|
83
|
+
|
|
84
|
+
const data = record.boundaries.fetchData[0]
|
|
85
|
+
expect(data.input).toEqual([-5])
|
|
86
|
+
expect(data.error).toContain('Value cannot be negative')
|
|
87
|
+
expect(data.output).toBeUndefined()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('returns [null, error, record] on schema validation failure', async () => {
|
|
91
|
+
// Create a schema that requires a positive number
|
|
92
|
+
const schema = new Schema({
|
|
93
|
+
value: Schema.number().min(1, 'Value must be positive')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// Define the boundaries
|
|
97
|
+
const boundaries = {
|
|
98
|
+
fetchData: async (value: number): Promise<number> => {
|
|
99
|
+
return value * 2
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Create the task
|
|
104
|
+
const validationTask = createTask(
|
|
105
|
+
schema,
|
|
106
|
+
boundaries,
|
|
107
|
+
async function ({ value }, { fetchData }) {
|
|
108
|
+
const result = await fetchData(value)
|
|
109
|
+
return { result, success: true }
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
// Call safeRun with invalid input that will fail schema validation
|
|
114
|
+
const [result, error, record] = await validationTask.safeRun({ value: 0 })
|
|
115
|
+
|
|
116
|
+
// Verify validation error case
|
|
117
|
+
expect(error).toBeInstanceOf(Error)
|
|
118
|
+
expect(error instanceof Error).toBe(true)
|
|
119
|
+
if (error instanceof Error) {
|
|
120
|
+
expect(error.message).toContain('Value must be positive')
|
|
121
|
+
}
|
|
122
|
+
expect(result).toBeNull()
|
|
123
|
+
expect(record).not.toBeNull()
|
|
124
|
+
expect(record.input).toEqual({ value: 0 })
|
|
125
|
+
expect(record.error).toContain('Value must be positive')
|
|
126
|
+
expect(record.boundaries).toEqual({
|
|
127
|
+
fetchData: []
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('properly calls the listener with safeRun and run', async () => {
|
|
132
|
+
// Create a schema
|
|
133
|
+
const schema = new Schema({
|
|
134
|
+
value: Schema.number()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Define the boundaries
|
|
138
|
+
const boundaries = {
|
|
139
|
+
fetchData: async (value: number): Promise<number> => {
|
|
140
|
+
return value * 2
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Create the task
|
|
145
|
+
const listenerTask = createTask(
|
|
146
|
+
schema,
|
|
147
|
+
boundaries,
|
|
148
|
+
async function ({ value }, { fetchData }) {
|
|
149
|
+
const result = await fetchData(value)
|
|
150
|
+
return result
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
// Create a mock listener
|
|
155
|
+
const originalListener = jest.fn()
|
|
156
|
+
listenerTask.addListener(originalListener)
|
|
157
|
+
|
|
158
|
+
// Call safeRun - this should call the listener once
|
|
159
|
+
await listenerTask.safeRun({ value: 10 })
|
|
160
|
+
|
|
161
|
+
// Run the task normally - this should call the listener again through safeRun
|
|
162
|
+
await listenerTask.run({ value: 20 })
|
|
163
|
+
|
|
164
|
+
// The original listener should have been called for both runs
|
|
165
|
+
expect(originalListener).toHaveBeenCalledTimes(2)
|
|
166
|
+
|
|
167
|
+
// First call should be for safeRun with value 10
|
|
168
|
+
expect(originalListener).toHaveBeenNthCalledWith(
|
|
169
|
+
1,
|
|
170
|
+
expect.objectContaining({
|
|
171
|
+
input: { value: 10 },
|
|
172
|
+
output: 20,
|
|
173
|
+
boundaries: {
|
|
174
|
+
fetchData: expect.any(Array)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
// Second call should be for run with value 20
|
|
180
|
+
expect(originalListener).toHaveBeenNthCalledWith(
|
|
181
|
+
2,
|
|
182
|
+
expect.objectContaining({
|
|
183
|
+
input: { value: 20 },
|
|
184
|
+
output: 40,
|
|
185
|
+
boundaries: {
|
|
186
|
+
fetchData: expect.any(Array)
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('handles multiple boundary calls correctly', async () => {
|
|
193
|
+
// Create a schema
|
|
194
|
+
const schema = new Schema({
|
|
195
|
+
values: Schema.array(Schema.number())
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// Define multiple boundaries
|
|
199
|
+
const boundaries = {
|
|
200
|
+
doubleValue: async (value: number): Promise<number> => {
|
|
201
|
+
return value * 2
|
|
202
|
+
},
|
|
203
|
+
sumValues: async (values: number[]): Promise<number> => {
|
|
204
|
+
return values.reduce((sum, val) => sum + val, 0)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Create a task that uses multiple boundaries
|
|
209
|
+
const multiBoundaryTask = createTask(
|
|
210
|
+
schema,
|
|
211
|
+
boundaries,
|
|
212
|
+
async function ({ values }, { doubleValue, sumValues }) {
|
|
213
|
+
const doubled = await Promise.all(values.map(value => doubleValue(value)))
|
|
214
|
+
const total = await sumValues(doubled)
|
|
215
|
+
return { doubled, total }
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
// Call safeRun
|
|
220
|
+
const [result, error, record] = await multiBoundaryTask.safeRun({ values: [1, 2, 3] })
|
|
221
|
+
|
|
222
|
+
// Verify success
|
|
223
|
+
expect(error).toBeNull()
|
|
224
|
+
expect(result).toEqual({
|
|
225
|
+
doubled: [2, 4, 6],
|
|
226
|
+
total: 12
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
// Verify record structure
|
|
230
|
+
expect(record).not.toBeNull()
|
|
231
|
+
expect(record).toHaveProperty('boundaries.doubleValue')
|
|
232
|
+
expect(record).toHaveProperty('boundaries.sumValues')
|
|
233
|
+
|
|
234
|
+
expect(record.boundaries.doubleValue).toHaveLength(3)
|
|
235
|
+
expect(record.boundaries.sumValues).toHaveLength(1)
|
|
236
|
+
expect(record.boundaries.doubleValue[0]).toEqual({ input: [1], output: 2 })
|
|
237
|
+
expect(record.boundaries.doubleValue[1]).toEqual({ input: [2], output: 4 })
|
|
238
|
+
expect(record.boundaries.doubleValue[2]).toEqual({ input: [3], output: 6 })
|
|
239
|
+
expect(record.boundaries.sumValues[0]).toEqual({ input: [[2, 4, 6]], output: 12 })
|
|
240
|
+
})
|
|
241
|
+
})
|