@output.ai/core 0.3.0 → 0.3.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.
- package/README.md +3 -90
- package/package.json +1 -1
- package/src/index.d.ts +158 -41
- package/src/index.js +2 -0
- package/src/interface/evaluator.js +147 -65
- package/src/interface/evaluator.spec.js +463 -1
- package/src/interface/validations/static.js +16 -0
- package/src/interface/validations/static.spec.js +79 -1
- package/src/interface/workflow_utils.js +49 -0
- package/src/interface/workflow_utils.spec.js +190 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { executeInParallel } from './workflow_utils.js';
|
|
3
|
+
|
|
4
|
+
describe( 'executeInParallel', () => {
|
|
5
|
+
it( 'returns empty array for empty jobs', async () => {
|
|
6
|
+
const results = await executeInParallel( { jobs: [] } );
|
|
7
|
+
expect( results ).toEqual( [] );
|
|
8
|
+
} );
|
|
9
|
+
|
|
10
|
+
it( 'executes all jobs and returns results', async () => {
|
|
11
|
+
const jobs = [
|
|
12
|
+
() => Promise.resolve( 'a' ),
|
|
13
|
+
() => Promise.resolve( 'b' ),
|
|
14
|
+
() => Promise.resolve( 'c' )
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const results = await executeInParallel( { jobs } );
|
|
18
|
+
|
|
19
|
+
expect( results ).toHaveLength( 3 );
|
|
20
|
+
expect( results ).toContainEqual( { ok: true, result: 'a', index: 0 } );
|
|
21
|
+
expect( results ).toContainEqual( { ok: true, result: 'b', index: 1 } );
|
|
22
|
+
expect( results ).toContainEqual( { ok: true, result: 'c', index: 2 } );
|
|
23
|
+
} );
|
|
24
|
+
|
|
25
|
+
it( 'handles job failures without throwing', async () => {
|
|
26
|
+
const error = new Error( 'job failed' );
|
|
27
|
+
const jobs = [
|
|
28
|
+
() => Promise.resolve( 'ok' ),
|
|
29
|
+
() => Promise.reject( error )
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const results = await executeInParallel( { jobs } );
|
|
33
|
+
|
|
34
|
+
expect( results ).toHaveLength( 2 );
|
|
35
|
+
expect( results ).toContainEqual( { ok: true, result: 'ok', index: 0 } );
|
|
36
|
+
expect( results ).toContainEqual( { ok: false, error, index: 1 } );
|
|
37
|
+
} );
|
|
38
|
+
|
|
39
|
+
it( 'handles all jobs failing', async () => {
|
|
40
|
+
const errors = [ new Error( 'e1' ), new Error( 'e2' ) ];
|
|
41
|
+
const jobs = [
|
|
42
|
+
() => Promise.reject( errors[0] ),
|
|
43
|
+
() => Promise.reject( errors[1] )
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const results = await executeInParallel( { jobs } );
|
|
47
|
+
|
|
48
|
+
expect( results ).toHaveLength( 2 );
|
|
49
|
+
expect( results ).toContainEqual( { ok: false, error: errors[0], index: 0 } );
|
|
50
|
+
expect( results ).toContainEqual( { ok: false, error: errors[1], index: 1 } );
|
|
51
|
+
} );
|
|
52
|
+
|
|
53
|
+
it( 'respects concurrency limit', async () => {
|
|
54
|
+
const tracker = { active: 0, max: 0 };
|
|
55
|
+
|
|
56
|
+
const createJob = ( id, delay ) => async () => {
|
|
57
|
+
tracker.active++;
|
|
58
|
+
tracker.max = Math.max( tracker.max, tracker.active );
|
|
59
|
+
await new Promise( r => setTimeout( r, delay ) );
|
|
60
|
+
tracker.active--;
|
|
61
|
+
return id;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const jobs = [
|
|
65
|
+
createJob( 'a', 50 ),
|
|
66
|
+
createJob( 'b', 50 ),
|
|
67
|
+
createJob( 'c', 50 ),
|
|
68
|
+
createJob( 'd', 50 )
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
await executeInParallel( { jobs, concurrency: 2 } );
|
|
72
|
+
|
|
73
|
+
expect( tracker.max ).toBe( 2 );
|
|
74
|
+
} );
|
|
75
|
+
|
|
76
|
+
it( 'runs all jobs concurrently when concurrency is Infinity', async () => {
|
|
77
|
+
const tracker = { active: 0, max: 0 };
|
|
78
|
+
|
|
79
|
+
const createJob = delay => async () => {
|
|
80
|
+
tracker.active++;
|
|
81
|
+
tracker.max = Math.max( tracker.max, tracker.active );
|
|
82
|
+
await new Promise( r => setTimeout( r, delay ) );
|
|
83
|
+
tracker.active--;
|
|
84
|
+
return 'done';
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const jobs = [ createJob( 30 ), createJob( 30 ), createJob( 30 ), createJob( 30 ) ];
|
|
88
|
+
|
|
89
|
+
await executeInParallel( { jobs } );
|
|
90
|
+
|
|
91
|
+
expect( tracker.max ).toBe( 4 );
|
|
92
|
+
} );
|
|
93
|
+
|
|
94
|
+
it( 'calls onJobCompleted for each job', async () => {
|
|
95
|
+
const onJobCompleted = vi.fn();
|
|
96
|
+
const jobs = [
|
|
97
|
+
() => Promise.resolve( 'a' ),
|
|
98
|
+
() => Promise.resolve( 'b' )
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
await executeInParallel( { jobs, onJobCompleted } );
|
|
102
|
+
|
|
103
|
+
expect( onJobCompleted ).toHaveBeenCalledTimes( 2 );
|
|
104
|
+
expect( onJobCompleted ).toHaveBeenCalledWith( expect.objectContaining( { ok: true, result: 'a', index: 0 } ) );
|
|
105
|
+
expect( onJobCompleted ).toHaveBeenCalledWith( expect.objectContaining( { ok: true, result: 'b', index: 1 } ) );
|
|
106
|
+
} );
|
|
107
|
+
|
|
108
|
+
it( 'works without onJobCompleted callback', async () => {
|
|
109
|
+
const jobs = [ () => Promise.resolve( 'x' ) ];
|
|
110
|
+
|
|
111
|
+
const results = await executeInParallel( { jobs } );
|
|
112
|
+
|
|
113
|
+
expect( results ).toEqual( [ { ok: true, result: 'x', index: 0 } ] );
|
|
114
|
+
} );
|
|
115
|
+
|
|
116
|
+
it( 'handles synchronous job functions', async () => {
|
|
117
|
+
const jobs = [
|
|
118
|
+
() => 'sync-a',
|
|
119
|
+
() => 'sync-b'
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const results = await executeInParallel( { jobs } );
|
|
123
|
+
|
|
124
|
+
expect( results ).toContainEqual( { ok: true, result: 'sync-a', index: 0 } );
|
|
125
|
+
expect( results ).toContainEqual( { ok: true, result: 'sync-b', index: 1 } );
|
|
126
|
+
} );
|
|
127
|
+
|
|
128
|
+
it( 'handles synchronous throwing job functions', async () => {
|
|
129
|
+
const error = new Error( 'sync throw' );
|
|
130
|
+
const jobs = [
|
|
131
|
+
() => {
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const results = await executeInParallel( { jobs } );
|
|
137
|
+
|
|
138
|
+
expect( results ).toEqual( [ { ok: false, error, index: 0 } ] );
|
|
139
|
+
} );
|
|
140
|
+
|
|
141
|
+
it( 'handles concurrency greater than job count', async () => {
|
|
142
|
+
const jobs = [ () => 'only-one' ];
|
|
143
|
+
|
|
144
|
+
const results = await executeInParallel( { jobs, concurrency: 10 } );
|
|
145
|
+
|
|
146
|
+
expect( results ).toEqual( [ { ok: true, result: 'only-one', index: 0 } ] );
|
|
147
|
+
} );
|
|
148
|
+
|
|
149
|
+
it( 'calls onJobCompleted in completion order, not submission order', async () => {
|
|
150
|
+
const completionOrder = [];
|
|
151
|
+
const onJobCompleted = result => completionOrder.push( result.index );
|
|
152
|
+
|
|
153
|
+
const jobs = [
|
|
154
|
+
async () => {
|
|
155
|
+
await new Promise( r => setTimeout( r, 60 ) );
|
|
156
|
+
return 'slow';
|
|
157
|
+
},
|
|
158
|
+
async () => {
|
|
159
|
+
await new Promise( r => setTimeout( r, 10 ) );
|
|
160
|
+
return 'fast';
|
|
161
|
+
}
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
await executeInParallel( { jobs, onJobCompleted } );
|
|
165
|
+
|
|
166
|
+
expect( completionOrder ).toEqual( [ 1, 0 ] ); // fast (index 1) completes before slow (index 0)
|
|
167
|
+
} );
|
|
168
|
+
|
|
169
|
+
it( 'returns results sorted by job index for determinism', async () => {
|
|
170
|
+
const jobs = [
|
|
171
|
+
async () => {
|
|
172
|
+
await new Promise( r => setTimeout( r, 60 ) );
|
|
173
|
+
return 'slow';
|
|
174
|
+
},
|
|
175
|
+
async () => {
|
|
176
|
+
await new Promise( r => setTimeout( r, 10 ) );
|
|
177
|
+
return 'fast';
|
|
178
|
+
}
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
const results = await executeInParallel( { jobs } );
|
|
182
|
+
|
|
183
|
+
expect( results[0].index ).toBe( 0 );
|
|
184
|
+
expect( results[1].index ).toBe( 1 );
|
|
185
|
+
expect( results ).toEqual( [
|
|
186
|
+
{ ok: true, result: 'slow', index: 0 },
|
|
187
|
+
{ ok: true, result: 'fast', index: 1 }
|
|
188
|
+
] );
|
|
189
|
+
} );
|
|
190
|
+
} );
|