@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.
- package/dist/index.d.ts +33 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +173 -16
- package/dist/index.js.map +1 -1
- package/dist/test/add-listener-with-boundaries.test.js +56 -26
- package/dist/test/add-listener-with-boundaries.test.js.map +1 -1
- package/dist/test/safe-replay-complex-boundary.test.js +36 -27
- package/dist/test/safe-replay-complex-boundary.test.js.map +1 -1
- package/dist/test/safe-replay.test.js +23 -14
- package/dist/test/safe-replay.test.js.map +1 -1
- package/dist/test/safe-run.test.js +41 -16
- package/dist/test/safe-run.test.js.map +1 -1
- package/dist/test/task-boundary-mocking.test.js +17 -7
- package/dist/test/task-boundary-mocking.test.js.map +1 -1
- package/dist/test/task-execution-log.test.d.ts +2 -0
- package/dist/test/task-execution-log.test.d.ts.map +1 -0
- package/dist/test/task-execution-log.test.js +207 -0
- package/dist/test/task-execution-log.test.js.map +1 -0
- package/dist/test/task-with-boundaries.test.js +56 -24
- package/dist/test/task-with-boundaries.test.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +236 -23
- package/src/test/add-listener-with-boundaries.test.ts +24 -18
- package/src/test/safe-replay-complex-boundary.test.ts +18 -10
- package/src/test/safe-replay.test.ts +21 -12
- package/src/test/safe-run.test.ts +20 -15
- package/src/test/task-boundary-mocking.test.ts +8 -6
- package/src/test/task-execution-log.test.ts +246 -0
- package/src/test/task-with-boundaries.test.ts +42 -41
package/src/index.ts
CHANGED
|
@@ -35,6 +35,8 @@ export type {
|
|
|
35
35
|
export { Schema }
|
|
36
36
|
|
|
37
37
|
export interface TaskConfig<B extends Boundaries = Boundaries> {
|
|
38
|
+
name?: string
|
|
39
|
+
description?: string
|
|
38
40
|
schema?: Schema<Record<string, SchemaType>>
|
|
39
41
|
mode?: Mode
|
|
40
42
|
boundaries?: B
|
|
@@ -85,11 +87,19 @@ export interface ExecutionRecord<InputType = unknown, OutputType = unknown, B ex
|
|
|
85
87
|
error?: string
|
|
86
88
|
/** Boundary execution data */
|
|
87
89
|
boundaries: BoundaryLogsFor<B>
|
|
90
|
+
/** The name of the task (if set) */
|
|
91
|
+
taskName?: string
|
|
92
|
+
/** Additional context metadata */
|
|
93
|
+
metadata?: Record<string, string>
|
|
94
|
+
/** The type of execution record - computed from output/error state */
|
|
95
|
+
type: 'success' | 'error' | 'pending'
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
export interface TaskInstanceType<Func extends BaseFunction = BaseFunction, B extends Boundaries = Boundaries> {
|
|
91
99
|
version: string
|
|
92
100
|
|
|
101
|
+
getName: () => string | undefined
|
|
102
|
+
setName: (name: string) => void
|
|
93
103
|
getMode: () => Mode
|
|
94
104
|
setMode: (mode: Mode) => void
|
|
95
105
|
setSchema: (base: Schema<Record<string, SchemaType>>) => void
|
|
@@ -119,7 +129,7 @@ export interface TaskInstanceType<Func extends BaseFunction = BaseFunction, B ex
|
|
|
119
129
|
resetMocks: () => void
|
|
120
130
|
|
|
121
131
|
run: (argv?: Parameters<Func>[0]) => Promise<ReturnType<Func>>
|
|
122
|
-
safeRun: (argv?: Parameters<Func>[0]) => Promise<[Awaited<ReturnType<Func>> | null, Error | null, ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B>]>
|
|
132
|
+
safeRun: (argv?: Parameters<Func>[0], context?: Record<string, string>) => Promise<[Awaited<ReturnType<Func>> | null, Error | null, ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B>]>
|
|
123
133
|
|
|
124
134
|
// Method for replaying task execution
|
|
125
135
|
safeReplay: (
|
|
@@ -146,6 +156,21 @@ export type TaskFunction<S, B extends Boundaries> =
|
|
|
146
156
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
147
157
|
(argv: InferSchemaType<S>, boundaries: WrappedBoundaries<B>) => Promise<any>;
|
|
148
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Utility function to compute the execution record type based on output and error state
|
|
161
|
+
*/
|
|
162
|
+
export function getExecutionRecordType<InputType = unknown, OutputType = unknown, B extends Boundaries = Boundaries>(
|
|
163
|
+
record: Omit<ExecutionRecord<InputType, OutputType, B>, 'type'>
|
|
164
|
+
): 'success' | 'error' | 'pending' {
|
|
165
|
+
if (record.error !== undefined && record.error !== null) {
|
|
166
|
+
return 'error'
|
|
167
|
+
}
|
|
168
|
+
if (record.output !== undefined && record.output !== null) {
|
|
169
|
+
return 'success'
|
|
170
|
+
}
|
|
171
|
+
return 'pending'
|
|
172
|
+
}
|
|
173
|
+
|
|
149
174
|
export const Task = class Task<
|
|
150
175
|
B extends Boundaries = Boundaries,
|
|
151
176
|
Func extends BaseFunction = BaseFunction
|
|
@@ -155,6 +180,7 @@ export const Task = class Task<
|
|
|
155
180
|
_fn: Func
|
|
156
181
|
_mode: Mode
|
|
157
182
|
_coolDown: number
|
|
183
|
+
_name?: string
|
|
158
184
|
_description?: string
|
|
159
185
|
|
|
160
186
|
_boundariesDefinition: B
|
|
@@ -168,6 +194,8 @@ export const Task = class Task<
|
|
|
168
194
|
_listener?: ((record: TaskRecord<Parameters<Func>[0], ReturnType<Func>>) => void) | undefined
|
|
169
195
|
|
|
170
196
|
constructor (fn: Func, conf: TaskConfig<B> = {
|
|
197
|
+
name: undefined,
|
|
198
|
+
description: undefined,
|
|
171
199
|
schema: undefined,
|
|
172
200
|
mode: 'proxy',
|
|
173
201
|
boundaries: undefined,
|
|
@@ -182,6 +210,10 @@ export const Task = class Task<
|
|
|
182
210
|
this._mode = conf.mode ?? 'proxy'
|
|
183
211
|
this._boundariesDefinition = conf.boundaries ?? {} as B
|
|
184
212
|
|
|
213
|
+
// Set name and description from config
|
|
214
|
+
this._name = conf.name
|
|
215
|
+
this._description = conf.description
|
|
216
|
+
|
|
185
217
|
this._listener = undefined
|
|
186
218
|
|
|
187
219
|
// Cool down time before killing the process on cli runner
|
|
@@ -208,6 +240,14 @@ export const Task = class Task<
|
|
|
208
240
|
}
|
|
209
241
|
}
|
|
210
242
|
|
|
243
|
+
getName(): string | undefined {
|
|
244
|
+
return this._name
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
setName(name: string): void {
|
|
248
|
+
this._name = name
|
|
249
|
+
}
|
|
250
|
+
|
|
211
251
|
getMode (): Mode {
|
|
212
252
|
return this._mode
|
|
213
253
|
}
|
|
@@ -378,10 +418,22 @@ export const Task = class Task<
|
|
|
378
418
|
Error | null,
|
|
379
419
|
ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B>
|
|
380
420
|
]> {
|
|
381
|
-
//
|
|
382
|
-
|
|
421
|
+
// Metadata is empty at start. Then will be populated on the task execution
|
|
422
|
+
// Need to implement that task have a ctx and setMetadata({key, value}) boundary
|
|
423
|
+
const metadata = {} as Record<string, string>
|
|
424
|
+
|
|
425
|
+
// Initialize log item (without type initially)
|
|
426
|
+
const logItemBase = {
|
|
383
427
|
input: argv as Parameters<Func>[0],
|
|
384
|
-
boundaries: {} as BoundaryLogsFor<B
|
|
428
|
+
boundaries: {} as BoundaryLogsFor<B>,
|
|
429
|
+
taskName: this._name,
|
|
430
|
+
metadata: metadata || {}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Create the log item with computed type
|
|
434
|
+
const logItem: ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B> = {
|
|
435
|
+
...logItemBase,
|
|
436
|
+
type: getExecutionRecordType(logItemBase)
|
|
385
437
|
}
|
|
386
438
|
|
|
387
439
|
// Create fresh boundaries for this execution
|
|
@@ -410,6 +462,7 @@ export const Task = class Task<
|
|
|
410
462
|
: 'Invalid input'
|
|
411
463
|
|
|
412
464
|
logItem.error = errorMessage
|
|
465
|
+
logItem.type = 'error'
|
|
413
466
|
logItem.boundaries = {} as BoundaryLogsFor<B>
|
|
414
467
|
|
|
415
468
|
// Add boundary elements empty
|
|
@@ -433,9 +486,11 @@ export const Task = class Task<
|
|
|
433
486
|
)
|
|
434
487
|
|
|
435
488
|
logItem.output = output
|
|
489
|
+
logItem.type = 'success'
|
|
436
490
|
} catch (caughtError) {
|
|
437
491
|
const errorMessage = caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
438
492
|
logItem.error = errorMessage
|
|
493
|
+
logItem.type = 'error'
|
|
439
494
|
error = new Error(errorMessage)
|
|
440
495
|
}
|
|
441
496
|
|
|
@@ -487,10 +542,18 @@ export const Task = class Task<
|
|
|
487
542
|
// Extract the input from the execution log
|
|
488
543
|
const argv = executionLog.input
|
|
489
544
|
|
|
490
|
-
// Initialize log item for this replay
|
|
491
|
-
const
|
|
545
|
+
// Initialize log item for this replay (without type initially)
|
|
546
|
+
const logItemBase = {
|
|
492
547
|
input: argv,
|
|
493
|
-
boundaries: {} as BoundaryLogsFor<B
|
|
548
|
+
boundaries: {} as BoundaryLogsFor<B>,
|
|
549
|
+
taskName: this._name,
|
|
550
|
+
metadata: executionLog.metadata || {}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Create the log item with computed type
|
|
554
|
+
const logItem: ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B> = {
|
|
555
|
+
...logItemBase,
|
|
556
|
+
type: getExecutionRecordType(logItemBase)
|
|
494
557
|
}
|
|
495
558
|
|
|
496
559
|
// Create boundaries for this replay execution with custom modes based on config
|
|
@@ -534,6 +597,7 @@ export const Task = class Task<
|
|
|
534
597
|
: 'Invalid input'
|
|
535
598
|
|
|
536
599
|
logItem.error = errorMessage
|
|
600
|
+
logItem.type = 'error'
|
|
537
601
|
logItem.output = executionLog.output // Keep the original output
|
|
538
602
|
|
|
539
603
|
// Copy the boundary data from the execution log
|
|
@@ -555,9 +619,11 @@ export const Task = class Task<
|
|
|
555
619
|
)
|
|
556
620
|
|
|
557
621
|
logItem.output = output
|
|
622
|
+
logItem.type = 'success'
|
|
558
623
|
} catch (caughtError) {
|
|
559
624
|
const errorMessage = caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
560
625
|
logItem.error = errorMessage
|
|
626
|
+
logItem.type = 'error'
|
|
561
627
|
error = new Error(errorMessage)
|
|
562
628
|
}
|
|
563
629
|
|
|
@@ -602,14 +668,50 @@ export const Task = class Task<
|
|
|
602
668
|
statusCode: number
|
|
603
669
|
body: string
|
|
604
670
|
}> => {
|
|
671
|
+
const eventArgs = (event && typeof event === 'object' && 'args' in event) ? (event).args : {}
|
|
672
|
+
|
|
673
|
+
// Check validation first
|
|
674
|
+
if (this._schema) {
|
|
675
|
+
const validation = this._schema.safeParse(eventArgs)
|
|
676
|
+
if (!validation.success) {
|
|
677
|
+
const errorDetails = validation.error?.errors.map(err =>
|
|
678
|
+
`${err.path.join('.')}: ${err.message}`
|
|
679
|
+
).join(', ')
|
|
680
|
+
|
|
681
|
+
const errorMessage = errorDetails
|
|
682
|
+
? `Invalid input on: ${errorDetails}`
|
|
683
|
+
: 'Invalid input'
|
|
684
|
+
|
|
685
|
+
return {
|
|
686
|
+
statusCode: 422,
|
|
687
|
+
body: JSON.stringify({
|
|
688
|
+
error: errorMessage,
|
|
689
|
+
details: validation.error?.errors
|
|
690
|
+
})
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
605
695
|
try {
|
|
606
696
|
// Call the task's safeRun method
|
|
607
|
-
const
|
|
608
|
-
|
|
697
|
+
const [outcome, error, log] = await this.safeRun(eventArgs)
|
|
698
|
+
|
|
699
|
+
// Send log to Hive if environment variables are present
|
|
700
|
+
await this._sendToHive(log)
|
|
701
|
+
|
|
702
|
+
if (error) {
|
|
703
|
+
return {
|
|
704
|
+
statusCode: 500,
|
|
705
|
+
body: JSON.stringify({
|
|
706
|
+
error: error.message,
|
|
707
|
+
stack: error.stack
|
|
708
|
+
})
|
|
709
|
+
}
|
|
710
|
+
}
|
|
609
711
|
|
|
610
712
|
return {
|
|
611
713
|
statusCode: 200,
|
|
612
|
-
body: JSON.stringify(
|
|
714
|
+
body: JSON.stringify(outcome)
|
|
613
715
|
}
|
|
614
716
|
} catch (e: unknown) {
|
|
615
717
|
const error = e as Error
|
|
@@ -623,14 +725,124 @@ export const Task = class Task<
|
|
|
623
725
|
}
|
|
624
726
|
}
|
|
625
727
|
}
|
|
728
|
+
|
|
729
|
+
async _sendToHive(log: ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B>): Promise<void> {
|
|
730
|
+
const apiKey = process.env.HIVE_API_KEY
|
|
731
|
+
const apiSecret = process.env.HIVE_API_SECRET
|
|
732
|
+
const host = process.env.HIVE_HOST
|
|
733
|
+
const projectName = process.env.HIVE_PROJECT_NAME
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
// If any required env vars are missing, do nothing
|
|
737
|
+
if (!apiKey || !apiSecret || !host || !projectName) {
|
|
738
|
+
// eslint-disable-next-line no-console
|
|
739
|
+
console.log('Missing required env vars for sending log to Hive:', { apiKey, apiSecret, host, projectName })
|
|
740
|
+
return
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// eslint-disable-next-line no-console
|
|
744
|
+
console.log('Sending log to Hive:', log)
|
|
745
|
+
|
|
746
|
+
return new Promise<void>((resolve) => {
|
|
747
|
+
try {
|
|
748
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
749
|
+
const https = require('https')
|
|
750
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
751
|
+
const http = require('http')
|
|
752
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
753
|
+
const url = require('url')
|
|
754
|
+
|
|
755
|
+
const logsUrl = `${host}/api/tasks/log-ingest`
|
|
756
|
+
// eslint-disable-next-line no-console
|
|
757
|
+
console.log('logsUrl', logsUrl)
|
|
758
|
+
const parsedUrl = url.parse(logsUrl)
|
|
759
|
+
const authToken = `${apiKey}:${apiSecret}`
|
|
760
|
+
|
|
761
|
+
const postData = JSON.stringify({
|
|
762
|
+
projectName,
|
|
763
|
+
taskName: process.env.HIVE_TASK_NAME || this._fn.name || 'unnamed-task',
|
|
764
|
+
logItem: JSON.stringify(log)
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
const options = {
|
|
768
|
+
hostname: parsedUrl.hostname,
|
|
769
|
+
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
|
770
|
+
path: parsedUrl.path,
|
|
771
|
+
method: 'POST',
|
|
772
|
+
headers: {
|
|
773
|
+
'Authorization': `Bearer ${authToken}`,
|
|
774
|
+
'Content-Type': 'application/json',
|
|
775
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const client = parsedUrl.protocol === 'https:' ? https : http
|
|
780
|
+
|
|
781
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
782
|
+
const req = client.request(options, (res: any) => {
|
|
783
|
+
// eslint-disable-next-line no-console
|
|
784
|
+
console.log('Hive API response status:', res.statusCode)
|
|
785
|
+
// eslint-disable-next-line no-console
|
|
786
|
+
console.log('Hive API response headers:', res.headers)
|
|
787
|
+
|
|
788
|
+
let responseData = ''
|
|
789
|
+
|
|
790
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
791
|
+
res.on('data', (chunk: any) => {
|
|
792
|
+
responseData += chunk
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
res.on('end', () => {
|
|
796
|
+
// eslint-disable-next-line no-console
|
|
797
|
+
console.log('Hive API response body:', responseData)
|
|
798
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
799
|
+
// eslint-disable-next-line no-console
|
|
800
|
+
console.log('Successfully sent log to Hive')
|
|
801
|
+
} else {
|
|
802
|
+
// eslint-disable-next-line no-console
|
|
803
|
+
console.error('Hive API error - Status:', res.statusCode, 'Body:', responseData)
|
|
804
|
+
}
|
|
805
|
+
resolve() // Resolve the promise when request completes
|
|
806
|
+
})
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
req.on('error', (error: Error) => {
|
|
810
|
+
// eslint-disable-next-line no-console
|
|
811
|
+
console.error('Failed to send log to Hive - Request error:', error.message)
|
|
812
|
+
resolve() // Resolve even on error to not block the handler
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
req.write(postData)
|
|
816
|
+
req.end()
|
|
817
|
+
} catch (error) {
|
|
818
|
+
// eslint-disable-next-line no-console
|
|
819
|
+
console.error('Failed to send log to Hive:', error instanceof Error ? error.message : 'Unknown error')
|
|
820
|
+
resolve() // Resolve even on error to not block the handler
|
|
821
|
+
}
|
|
822
|
+
})
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Configuration object for creating a task
|
|
828
|
+
*/
|
|
829
|
+
export interface CreateTaskConfig<
|
|
830
|
+
S extends Schema<Record<string, SchemaType>>,
|
|
831
|
+
B extends Boundaries,
|
|
832
|
+
R
|
|
833
|
+
> {
|
|
834
|
+
name?: string
|
|
835
|
+
description?: string
|
|
836
|
+
schema: S
|
|
837
|
+
boundaries: B
|
|
838
|
+
fn: (argv: InferSchemaType<S>, boundaries: WrappedBoundaries<B>) => Promise<R>
|
|
839
|
+
mode?: Mode
|
|
840
|
+
boundariesData?: BoundaryTapeData
|
|
626
841
|
}
|
|
627
842
|
|
|
628
843
|
/**
|
|
629
844
|
* Helper function to create a task with proper type inference
|
|
630
|
-
* @param
|
|
631
|
-
* @param boundaries The boundaries to use
|
|
632
|
-
* @param fn The task function
|
|
633
|
-
* @param config Additional task configuration
|
|
845
|
+
* @param config Configuration object containing schema, boundaries, and function
|
|
634
846
|
* @returns A new Task instance with proper type inference
|
|
635
847
|
*/
|
|
636
848
|
export function createTask<
|
|
@@ -638,17 +850,18 @@ export function createTask<
|
|
|
638
850
|
B extends Boundaries,
|
|
639
851
|
R
|
|
640
852
|
>(
|
|
641
|
-
|
|
642
|
-
boundaries: B,
|
|
643
|
-
fn: (argv: InferSchemaType<S>, boundaries: WrappedBoundaries<B>) => Promise<R>,
|
|
644
|
-
config?: Omit<TaskConfig<B>, 'schema' | 'boundaries'>
|
|
853
|
+
config: CreateTaskConfig<S, B, R>
|
|
645
854
|
): TaskInstanceType<(argv: InferSchemaType<S>, boundaries: WrappedBoundaries<B>) => Promise<R>, B> {
|
|
646
|
-
|
|
647
|
-
fn,
|
|
855
|
+
const task = new Task(
|
|
856
|
+
config.fn,
|
|
648
857
|
{
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
858
|
+
name: config.name,
|
|
859
|
+
description: config.description,
|
|
860
|
+
schema: config.schema,
|
|
861
|
+
boundaries: config.boundaries,
|
|
862
|
+
mode: config.mode,
|
|
863
|
+
boundariesData: config.boundariesData
|
|
652
864
|
}
|
|
653
865
|
)
|
|
866
|
+
return task
|
|
654
867
|
}
|
|
@@ -17,14 +17,15 @@ describe('Listener with boundaries tests', () => {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// Create the task using createTask
|
|
20
|
-
const task = createTask(
|
|
20
|
+
const task = createTask({
|
|
21
|
+
name: 'task',
|
|
21
22
|
schema,
|
|
22
23
|
boundaries,
|
|
23
|
-
async (argv, boundaries) => {
|
|
24
|
+
fn: async (argv, boundaries) => {
|
|
24
25
|
const externalData = await boundaries.fetchExternalData()
|
|
25
26
|
return { ...externalData, ...argv }
|
|
26
27
|
}
|
|
27
|
-
)
|
|
28
|
+
})
|
|
28
29
|
|
|
29
30
|
task.addListener<{ value: number }, { value: number, foo: boolean }>((record) => {
|
|
30
31
|
tape.push(record)
|
|
@@ -58,14 +59,15 @@ describe('Listener with boundaries tests', () => {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
// Create the task using createTask
|
|
61
|
-
const task = createTask(
|
|
62
|
+
const task = createTask({
|
|
63
|
+
name: 'task',
|
|
62
64
|
schema,
|
|
63
65
|
boundaries,
|
|
64
|
-
async (argv, boundaries) => {
|
|
66
|
+
fn: async (argv, boundaries) => {
|
|
65
67
|
const externalData = await boundaries.fetchExternalData()
|
|
66
68
|
return { ...externalData, ...argv }
|
|
67
69
|
}
|
|
68
|
-
)
|
|
70
|
+
})
|
|
69
71
|
|
|
70
72
|
task.addListener<{ value: number }, { value: number, foo: boolean }>((record) => {
|
|
71
73
|
tape.push(record)
|
|
@@ -109,10 +111,11 @@ describe('Listener with boundaries tests', () => {
|
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
// Create the task using createTask
|
|
112
|
-
const task = createTask(
|
|
114
|
+
const task = createTask({
|
|
115
|
+
name: 'task',
|
|
113
116
|
schema,
|
|
114
117
|
boundaries,
|
|
115
|
-
async (argv, boundaries) => {
|
|
118
|
+
fn: async (argv, boundaries) => {
|
|
116
119
|
const externalData = await boundaries.fetchExternalData()
|
|
117
120
|
if (typeof argv.value === 'undefined') {
|
|
118
121
|
throw new Error('Value is required')
|
|
@@ -120,7 +123,7 @@ describe('Listener with boundaries tests', () => {
|
|
|
120
123
|
|
|
121
124
|
return { ...externalData, ...argv as { value: number } }
|
|
122
125
|
}
|
|
123
|
-
)
|
|
126
|
+
})
|
|
124
127
|
|
|
125
128
|
task.addListener<Record<string, unknown>, { value: number, foo: boolean }>((record) => {
|
|
126
129
|
tape.push(record)
|
|
@@ -158,10 +161,11 @@ describe('Listener with boundaries tests', () => {
|
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
// Create the task using createTask
|
|
161
|
-
const task = createTask(
|
|
164
|
+
const task = createTask({
|
|
165
|
+
name: 'task',
|
|
162
166
|
schema,
|
|
163
167
|
boundaries,
|
|
164
|
-
async (argv, boundaries) => {
|
|
168
|
+
fn: async (argv, boundaries) => {
|
|
165
169
|
const externalData = await boundaries.fetchExternalData()
|
|
166
170
|
if (typeof argv.value === 'undefined') {
|
|
167
171
|
throw new Error('Value is required')
|
|
@@ -169,7 +173,7 @@ describe('Listener with boundaries tests', () => {
|
|
|
169
173
|
|
|
170
174
|
return { ...externalData, ...argv as { value: number } }
|
|
171
175
|
}
|
|
172
|
-
)
|
|
176
|
+
})
|
|
173
177
|
|
|
174
178
|
task.addListener<{ value?: number }, { value: number, foo: boolean }>((record) => {
|
|
175
179
|
tape.push(record)
|
|
@@ -216,16 +220,17 @@ describe('Listener with boundaries tests', () => {
|
|
|
216
220
|
}
|
|
217
221
|
|
|
218
222
|
// Create the task using createTask
|
|
219
|
-
const task = createTask(
|
|
223
|
+
const task = createTask({
|
|
224
|
+
name: 'task',
|
|
220
225
|
schema,
|
|
221
226
|
boundaries,
|
|
222
|
-
async (argv, boundaries) => {
|
|
227
|
+
fn: async (argv, boundaries) => {
|
|
223
228
|
await boundaries.fetchExternalData()
|
|
224
229
|
await boundaries.fetchExternalData()
|
|
225
230
|
|
|
226
231
|
return { foo: true }
|
|
227
232
|
}
|
|
228
|
-
)
|
|
233
|
+
})
|
|
229
234
|
|
|
230
235
|
task.addListener<{ value: number }, { foo: boolean }>((record) => {
|
|
231
236
|
tape.push(record)
|
|
@@ -263,10 +268,11 @@ describe('Listener with boundaries tests', () => {
|
|
|
263
268
|
}
|
|
264
269
|
|
|
265
270
|
// Create the task using createTask
|
|
266
|
-
const task = createTask(
|
|
271
|
+
const task = createTask({
|
|
272
|
+
name: 'task',
|
|
267
273
|
schema,
|
|
268
274
|
boundaries,
|
|
269
|
-
async (argv, boundaries) => {
|
|
275
|
+
fn: async (argv, boundaries) => {
|
|
270
276
|
let counter = argv.value
|
|
271
277
|
|
|
272
278
|
counter = await boundaries.add(counter)
|
|
@@ -275,7 +281,7 @@ describe('Listener with boundaries tests', () => {
|
|
|
275
281
|
|
|
276
282
|
return counter
|
|
277
283
|
}
|
|
278
|
-
)
|
|
284
|
+
})
|
|
279
285
|
|
|
280
286
|
task.addListener<{ value: number }, number>((record) => {
|
|
281
287
|
tape.push(record)
|
|
@@ -1,7 +1,14 @@
|
|
|
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('Complex boundary replay 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
|
+
}
|
|
5
12
|
// Define types for our portfolio data
|
|
6
13
|
type Stock = {
|
|
7
14
|
ticker: string;
|
|
@@ -81,10 +88,11 @@ describe('Complex boundary replay tests', () => {
|
|
|
81
88
|
})
|
|
82
89
|
|
|
83
90
|
// Create the portfolio value calculation task
|
|
84
|
-
calculatePortfolioValue = createTask(
|
|
91
|
+
calculatePortfolioValue = createTask({
|
|
92
|
+
name: 'calculatePortfolioValue',
|
|
85
93
|
schema,
|
|
86
94
|
boundaries,
|
|
87
|
-
async ({ userId }, { fetchPortfolio, fetchPrice }) => {
|
|
95
|
+
fn: async ({ userId }, { fetchPortfolio, fetchPrice }) => {
|
|
88
96
|
// First fetch the portfolio for the user
|
|
89
97
|
const portfolio = await fetchPortfolio(userId)
|
|
90
98
|
|
|
@@ -114,7 +122,7 @@ describe('Complex boundary replay tests', () => {
|
|
|
114
122
|
stocks: stocksWithValue
|
|
115
123
|
}
|
|
116
124
|
}
|
|
117
|
-
)
|
|
125
|
+
})
|
|
118
126
|
})
|
|
119
127
|
|
|
120
128
|
it('Should calculate portfolio value using multiple boundaries', async () => {
|
|
@@ -158,7 +166,7 @@ describe('Complex boundary replay tests', () => {
|
|
|
158
166
|
|
|
159
167
|
it('Should replay portfolio calculation from an execution log', async () => {
|
|
160
168
|
// First create a portfolio value calculation execution log
|
|
161
|
-
const executionLog: ExecutionRecord = {
|
|
169
|
+
const executionLog: ExecutionRecord = createExecutionRecord({
|
|
162
170
|
input: { userId: 'user1' },
|
|
163
171
|
output: {
|
|
164
172
|
id: 'portfolio1',
|
|
@@ -200,7 +208,7 @@ describe('Complex boundary replay tests', () => {
|
|
|
200
208
|
}
|
|
201
209
|
]
|
|
202
210
|
}
|
|
203
|
-
}
|
|
211
|
+
})
|
|
204
212
|
|
|
205
213
|
// Use replay mode for all boundaries
|
|
206
214
|
const [replayResult, replayError, replayLog] = await calculatePortfolioValue.safeReplay(
|
|
@@ -235,7 +243,7 @@ describe('Complex boundary replay tests', () => {
|
|
|
235
243
|
|
|
236
244
|
it('Should handle errors during replay', async () => {
|
|
237
245
|
// Create an execution log with an error in one of the price fetches
|
|
238
|
-
const executionLog: ExecutionRecord = {
|
|
246
|
+
const executionLog: ExecutionRecord = createExecutionRecord({
|
|
239
247
|
input: { userId: 'user1' },
|
|
240
248
|
error: 'Price data not available for ticker: GOOGL',
|
|
241
249
|
boundaries: {
|
|
@@ -268,7 +276,7 @@ describe('Complex boundary replay tests', () => {
|
|
|
268
276
|
}
|
|
269
277
|
]
|
|
270
278
|
}
|
|
271
|
-
}
|
|
279
|
+
})
|
|
272
280
|
|
|
273
281
|
// Use replay mode for all boundaries
|
|
274
282
|
const [replayResult, replayError, replayLog] = await calculatePortfolioValue.safeReplay(
|
|
@@ -303,7 +311,7 @@ describe('Complex boundary replay tests', () => {
|
|
|
303
311
|
priceData['AAPL'] = 195.00 // Different from replay data
|
|
304
312
|
|
|
305
313
|
// Create an execution log with historical data
|
|
306
|
-
const executionLog: ExecutionRecord = {
|
|
314
|
+
const executionLog: ExecutionRecord = createExecutionRecord({
|
|
307
315
|
input: { userId: 'user1' },
|
|
308
316
|
output: {
|
|
309
317
|
id: 'portfolio1',
|
|
@@ -345,7 +353,7 @@ describe('Complex boundary replay tests', () => {
|
|
|
345
353
|
}
|
|
346
354
|
]
|
|
347
355
|
}
|
|
348
|
-
}
|
|
356
|
+
})
|
|
349
357
|
|
|
350
358
|
// Use replay mode for portfolio but proxy mode for prices
|
|
351
359
|
const [replayResult, replayError, replayLog] = await calculatePortfolioValue.safeReplay(
|