@outputai/core 0.3.3-next.b4a190e.0 → 0.3.3-next.cb14409.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/package.json +7 -2
- package/src/interface/workflow.d.ts +2 -2
- package/src/worker/bundler_options.js +33 -4
- package/src/worker/bundler_options.spec.js +62 -0
- package/src/worker/loader.js +62 -50
- package/src/worker/loader.spec.js +285 -82
- package/src/worker/loader_tools.js +232 -60
- package/src/worker/loader_tools.spec.js +496 -25
- package/src/worker/webpack_loaders/npm_workflow_export_resolve.js +474 -0
- package/src/worker/webpack_loaders/npm_workflow_export_resolve.spec.js +374 -0
- package/src/worker/webpack_loaders/tools.js +9 -0
- package/src/worker/webpack_loaders/tools.spec.js +7 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +80 -11
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +67 -0
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +2 -1
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +73 -1
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +66 -1
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +62 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
|
+
import { staticMatchers } from './loader_tools.js';
|
|
4
5
|
|
|
5
6
|
vi.mock( '#consts', () => ( {
|
|
6
7
|
ACTIVITY_SEND_HTTP_REQUEST: '__internal#sendHttpRequest',
|
|
@@ -18,10 +19,22 @@ vi.mock( '#internal_activities', () => ( {
|
|
|
18
19
|
getTraceDestinations: getTraceDestinationsMock
|
|
19
20
|
} ) );
|
|
20
21
|
|
|
21
|
-
const importComponentsMock = vi.
|
|
22
|
+
const { importComponentsMock, findSharedActivitiesFromWorkflowsMock, findWorkflowsInNodeModulesMock, matchFilesMock } = vi.hoisted( () => ( {
|
|
23
|
+
importComponentsMock: vi.fn(),
|
|
24
|
+
findSharedActivitiesFromWorkflowsMock: vi.fn(),
|
|
25
|
+
findWorkflowsInNodeModulesMock: vi.fn(),
|
|
26
|
+
matchFilesMock: vi.fn()
|
|
27
|
+
} ) );
|
|
28
|
+
|
|
22
29
|
vi.mock( './loader_tools.js', async importOriginal => {
|
|
23
30
|
const actual = await importOriginal();
|
|
24
|
-
return {
|
|
31
|
+
return {
|
|
32
|
+
...actual,
|
|
33
|
+
importComponents: importComponentsMock,
|
|
34
|
+
findSharedActivitiesFromWorkflows: findSharedActivitiesFromWorkflowsMock,
|
|
35
|
+
findWorkflowsInNodeModules: findWorkflowsInNodeModulesMock,
|
|
36
|
+
matchFiles: matchFilesMock
|
|
37
|
+
};
|
|
25
38
|
} );
|
|
26
39
|
|
|
27
40
|
const fsMocks = vi.hoisted( () => ( {
|
|
@@ -38,12 +51,19 @@ vi.mock( 'node:fs', () => ( {
|
|
|
38
51
|
describe( 'worker/loader', () => {
|
|
39
52
|
beforeEach( () => {
|
|
40
53
|
vi.clearAllMocks();
|
|
54
|
+
importComponentsMock.mockReset();
|
|
55
|
+
importComponentsMock.mockImplementation( async function *() {} );
|
|
56
|
+
findSharedActivitiesFromWorkflowsMock.mockReset();
|
|
57
|
+
findSharedActivitiesFromWorkflowsMock.mockReturnValue( [] );
|
|
58
|
+
findWorkflowsInNodeModulesMock.mockReset();
|
|
59
|
+
findWorkflowsInNodeModulesMock.mockReturnValue( [] );
|
|
60
|
+
matchFilesMock.mockReset();
|
|
61
|
+
matchFilesMock.mockReturnValue( [] );
|
|
41
62
|
} );
|
|
42
63
|
|
|
43
64
|
it( 'loadActivities returns map including system activity and writes options file', async () => {
|
|
44
65
|
const { loadActivities } = await import( './loader.js' );
|
|
45
66
|
|
|
46
|
-
// First call: workflow directory scan (options.activityOptions propagated to activity options file)
|
|
47
67
|
importComponentsMock.mockImplementationOnce( async function *() {
|
|
48
68
|
yield {
|
|
49
69
|
fn: () => {},
|
|
@@ -51,7 +71,6 @@ describe( 'worker/loader', () => {
|
|
|
51
71
|
path: '/a/steps.js'
|
|
52
72
|
};
|
|
53
73
|
} );
|
|
54
|
-
// Second call: shared activities scan (no results)
|
|
55
74
|
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
56
75
|
|
|
57
76
|
const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
|
|
@@ -59,7 +78,6 @@ describe( 'worker/loader', () => {
|
|
|
59
78
|
expect( activities['A#Act1'] ).toBeTypeOf( 'function' );
|
|
60
79
|
expect( activities['__internal#sendHttpRequest'] ).toBe( sendHttpRequestMock );
|
|
61
80
|
|
|
62
|
-
// options file written with the collected activityOptions map
|
|
63
81
|
expect( fsMocks.writeFileSync ).toHaveBeenCalledTimes( 1 );
|
|
64
82
|
const [ writtenPath, contents ] = fsMocks.writeFileSync.mock.calls[0];
|
|
65
83
|
expect( writtenPath ).toMatch( /temp\/__activity_options\.js$/ );
|
|
@@ -86,15 +104,212 @@ describe( 'worker/loader', () => {
|
|
|
86
104
|
expect( written['A#EmptyOptions'] ).toBeUndefined();
|
|
87
105
|
} );
|
|
88
106
|
|
|
89
|
-
it( '
|
|
90
|
-
const {
|
|
107
|
+
it( 'loadActivities throws when two activities in the same workflow share a name', async () => {
|
|
108
|
+
const { loadActivities } = await import( './loader.js' );
|
|
109
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
110
|
+
yield { fn: () => {}, metadata: { name: 'DuplicateActivity' }, path: '/a/steps.js' };
|
|
111
|
+
yield { fn: () => {}, metadata: { name: 'DuplicateActivity' }, path: '/a/evaluators.js' };
|
|
112
|
+
} );
|
|
91
113
|
|
|
114
|
+
await expect( loadActivities( '/root', [ { name: 'A', path: '/a/workflow.js' } ] ) ).rejects.toThrow(
|
|
115
|
+
'Activity "DuplicateActivity" in workflow "A" conflicts with another activity in the same workflow. \
|
|
116
|
+
Activity names must be unique within a workflow.'
|
|
117
|
+
);
|
|
118
|
+
} );
|
|
119
|
+
|
|
120
|
+
it( 'loadActivities throws when two shared activities share a name', async () => {
|
|
121
|
+
const { loadActivities } = await import( './loader.js' );
|
|
122
|
+
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
92
123
|
importComponentsMock.mockImplementationOnce( async function *() {
|
|
93
|
-
yield {
|
|
124
|
+
yield { fn: () => {}, metadata: { name: 'DuplicateShared' }, path: '/root/shared/steps/a.js' };
|
|
125
|
+
yield { fn: () => {}, metadata: { name: 'DuplicateShared' }, path: '/root/shared/evaluators/a.js' };
|
|
126
|
+
} );
|
|
127
|
+
|
|
128
|
+
await expect( loadActivities( '/root', [ { name: 'A', path: '/a/workflow.js' } ] ) ).rejects.toThrow(
|
|
129
|
+
'Shared activity "DuplicateShared" conflicts with another shared activity. Shared activity names must be unique.'
|
|
130
|
+
);
|
|
131
|
+
} );
|
|
132
|
+
|
|
133
|
+
describe( 'loadWorkflows', () => {
|
|
134
|
+
it( 'returns local workflows from importComponents with metadata spread onto each entry', async () => {
|
|
135
|
+
const { loadWorkflows } = await import( './loader.js' );
|
|
136
|
+
const localFiles = [ { path: '/b/workflow.js', url: 'file:///b/workflow.js' } ];
|
|
137
|
+
matchFilesMock.mockReturnValueOnce( localFiles );
|
|
138
|
+
|
|
139
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
140
|
+
yield { metadata: { name: 'Flow1', description: 'd' }, path: '/b/workflow.js' };
|
|
141
|
+
} );
|
|
142
|
+
|
|
143
|
+
const workflows = await loadWorkflows( '/root' );
|
|
144
|
+
expect( workflows ).toEqual( [ { name: 'Flow1', description: 'd', path: '/b/workflow.js', external: false } ] );
|
|
145
|
+
expect( importComponentsMock ).toHaveBeenNthCalledWith( 1, localFiles );
|
|
146
|
+
expect( findWorkflowsInNodeModulesMock ).toHaveBeenCalledOnce();
|
|
147
|
+
expect( findWorkflowsInNodeModulesMock ).toHaveBeenCalledWith( '/root' );
|
|
148
|
+
} );
|
|
149
|
+
|
|
150
|
+
it( 'calls matchFiles with rootDir and workflowFile matcher', async () => {
|
|
151
|
+
const { loadWorkflows } = await import( './loader.js' );
|
|
152
|
+
const localFiles = [ { path: '/my/app/workflow.js', url: 'file:///my/app/workflow.js' } ];
|
|
153
|
+
const externalFiles = [ { path: '/my/app/node_modules/pkg/workflow.js', url: 'file:///my/app/node_modules/pkg/workflow.js' } ];
|
|
154
|
+
matchFilesMock.mockReturnValueOnce( localFiles );
|
|
155
|
+
findWorkflowsInNodeModulesMock.mockReturnValue( externalFiles );
|
|
156
|
+
|
|
157
|
+
await loadWorkflows( '/my/app' );
|
|
158
|
+
|
|
159
|
+
expect( matchFilesMock ).toHaveBeenCalledOnce();
|
|
160
|
+
expect( matchFilesMock ).toHaveBeenCalledWith( '/my/app', [ staticMatchers.workflowFile ] );
|
|
161
|
+
expect( importComponentsMock ).toHaveBeenCalledTimes( 1 );
|
|
162
|
+
expect( importComponentsMock ).toHaveBeenNthCalledWith( 1, [ ...localFiles, ...externalFiles ] );
|
|
163
|
+
expect( findWorkflowsInNodeModulesMock ).toHaveBeenCalledOnce();
|
|
164
|
+
expect( findWorkflowsInNodeModulesMock ).toHaveBeenCalledWith( '/my/app' );
|
|
165
|
+
} );
|
|
166
|
+
|
|
167
|
+
it( 'appends node_modules workflows after local ones and sets external: true', async () => {
|
|
168
|
+
const { loadWorkflows } = await import( './loader.js' );
|
|
169
|
+
|
|
170
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
171
|
+
yield { metadata: { name: 'LocalFlow', description: 'local' }, path: '/my/app/workflows/wf/workflow.js' };
|
|
172
|
+
yield {
|
|
173
|
+
metadata: { name: '__sum_numbers', description: 'from catalog' },
|
|
174
|
+
path: '/my/app/node_modules/catalog_pkg/src/w/workflow.js'
|
|
175
|
+
};
|
|
176
|
+
} );
|
|
177
|
+
findWorkflowsInNodeModulesMock.mockReturnValue( [ { path: '/my/app/node_modules/catalog_pkg/src/w/workflow.js' } ] );
|
|
178
|
+
|
|
179
|
+
const workflows = await loadWorkflows( '/my/app' );
|
|
180
|
+
expect( workflows ).toEqual( [
|
|
181
|
+
{ name: 'LocalFlow', description: 'local', path: '/my/app/workflows/wf/workflow.js', external: false },
|
|
182
|
+
{
|
|
183
|
+
name: '__sum_numbers',
|
|
184
|
+
description: 'from catalog',
|
|
185
|
+
path: '/my/app/node_modules/catalog_pkg/src/w/workflow.js',
|
|
186
|
+
external: true
|
|
187
|
+
}
|
|
188
|
+
] );
|
|
189
|
+
} );
|
|
190
|
+
|
|
191
|
+
it( 'returns only external workflows when the project root has none', async () => {
|
|
192
|
+
const { loadWorkflows } = await import( './loader.js' );
|
|
193
|
+
|
|
194
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
195
|
+
yield { metadata: { name: 'PkgFlow', description: 'pkg' }, path: '/proj/node_modules/a/w/workflow.js' };
|
|
196
|
+
} );
|
|
197
|
+
findWorkflowsInNodeModulesMock.mockReturnValue( [ { path: '/proj/node_modules/a/w/workflow.js' } ] );
|
|
198
|
+
|
|
199
|
+
const workflows = await loadWorkflows( '/proj' );
|
|
200
|
+
expect( workflows ).toEqual( [
|
|
201
|
+
{
|
|
202
|
+
name: 'PkgFlow',
|
|
203
|
+
description: 'pkg',
|
|
204
|
+
path: '/proj/node_modules/a/w/workflow.js',
|
|
205
|
+
external: true
|
|
206
|
+
}
|
|
207
|
+
] );
|
|
208
|
+
} );
|
|
209
|
+
|
|
210
|
+
it( 'throws when a local workflow path is under a shared directory', async () => {
|
|
211
|
+
const { loadWorkflows } = await import( './loader.js' );
|
|
212
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
213
|
+
yield { metadata: { name: 'Invalid' }, path: '/root/shared/workflow.js' };
|
|
214
|
+
} );
|
|
215
|
+
|
|
216
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow( 'Workflow directory can\'t be named "shared"' );
|
|
217
|
+
expect( findWorkflowsInNodeModulesMock ).toHaveBeenCalledOnce();
|
|
218
|
+
} );
|
|
219
|
+
|
|
220
|
+
it( 'throws when a workflow name conflicts with an earlier workflow name', async () => {
|
|
221
|
+
const { loadWorkflows } = await import( './loader.js' );
|
|
222
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
223
|
+
yield { metadata: { name: 'duplicate' }, path: '/root/a/workflow.js' };
|
|
224
|
+
yield { metadata: { name: 'duplicate' }, path: '/root/b/workflow.js' };
|
|
225
|
+
} );
|
|
226
|
+
|
|
227
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow(
|
|
228
|
+
'Workflow name "duplicate" conflicts with another workflow or alias. Workflow names and aliases must be unique.'
|
|
229
|
+
);
|
|
230
|
+
} );
|
|
231
|
+
|
|
232
|
+
it( 'throws when a workflow name conflicts with an earlier alias', async () => {
|
|
233
|
+
const { loadWorkflows } = await import( './loader.js' );
|
|
234
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
235
|
+
yield { metadata: { name: 'alpha', aliases: [ 'legacy' ] }, path: '/root/a/workflow.js' };
|
|
236
|
+
yield { metadata: { name: 'legacy' }, path: '/root/b/workflow.js' };
|
|
237
|
+
} );
|
|
238
|
+
|
|
239
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow(
|
|
240
|
+
'Workflow name "legacy" conflicts with another workflow or alias. Workflow names and aliases must be unique.'
|
|
241
|
+
);
|
|
242
|
+
} );
|
|
243
|
+
|
|
244
|
+
it( 'throws when an alias conflicts with an earlier workflow name', async () => {
|
|
245
|
+
const { loadWorkflows } = await import( './loader.js' );
|
|
246
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
247
|
+
yield { metadata: { name: 'alpha' }, path: '/root/a/workflow.js' };
|
|
248
|
+
yield { metadata: { name: 'beta', aliases: [ 'alpha' ] }, path: '/root/b/workflow.js' };
|
|
249
|
+
} );
|
|
250
|
+
|
|
251
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow(
|
|
252
|
+
'Workflow "beta" alias "alpha" conflicts with another workflow or alias. Workflow names and aliases must be unique.'
|
|
253
|
+
);
|
|
254
|
+
} );
|
|
255
|
+
|
|
256
|
+
it( 'throws when an alias conflicts with an earlier alias', async () => {
|
|
257
|
+
const { loadWorkflows } = await import( './loader.js' );
|
|
258
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
259
|
+
yield { metadata: { name: 'alpha', aliases: [ 'shared_alias' ] }, path: '/root/a/workflow.js' };
|
|
260
|
+
yield { metadata: { name: 'beta', aliases: [ 'shared_alias' ] }, path: '/root/b/workflow.js' };
|
|
261
|
+
} );
|
|
262
|
+
|
|
263
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow(
|
|
264
|
+
'Workflow "beta" alias "shared_alias" conflicts with another workflow or alias. Workflow names and aliases must be unique.'
|
|
265
|
+
);
|
|
266
|
+
} );
|
|
267
|
+
|
|
268
|
+
it( 'throws when an alias is identical to its workflow name', async () => {
|
|
269
|
+
const { loadWorkflows } = await import( './loader.js' );
|
|
270
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
271
|
+
yield { metadata: { name: 'alpha', aliases: [ 'alpha' ] }, path: '/root/a/workflow.js' };
|
|
272
|
+
} );
|
|
273
|
+
|
|
274
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow(
|
|
275
|
+
'Workflow "alpha" alias "alpha" conflicts with another workflow or alias. Workflow names and aliases must be unique.'
|
|
276
|
+
);
|
|
94
277
|
} );
|
|
95
278
|
|
|
96
|
-
|
|
97
|
-
|
|
279
|
+
it( 'allows workflow names and aliases that only differ by case', async () => {
|
|
280
|
+
const { loadWorkflows } = await import( './loader.js' );
|
|
281
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
282
|
+
yield { metadata: { name: 'Alpha', aliases: [ 'Legacy' ] }, path: '/root/a/workflow.js' };
|
|
283
|
+
yield { metadata: { name: 'alpha', aliases: [ 'legacy' ] }, path: '/root/b/workflow.js' };
|
|
284
|
+
} );
|
|
285
|
+
|
|
286
|
+
await expect( loadWorkflows( '/root' ) ).resolves.toEqual( [
|
|
287
|
+
{ name: 'Alpha', aliases: [ 'Legacy' ], path: '/root/a/workflow.js', external: false },
|
|
288
|
+
{ name: 'alpha', aliases: [ 'legacy' ], path: '/root/b/workflow.js', external: false }
|
|
289
|
+
] );
|
|
290
|
+
} );
|
|
291
|
+
|
|
292
|
+
it( 'throws when a workflow name is reserved for the internal catalog', async () => {
|
|
293
|
+
const { loadWorkflows } = await import( './loader.js' );
|
|
294
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
295
|
+
yield { metadata: { name: 'catalog' }, path: '/root/catalog/workflow.js' };
|
|
296
|
+
} );
|
|
297
|
+
|
|
298
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow(
|
|
299
|
+
'Workflow name "catalog" is reserved for the internal catalog workflow.'
|
|
300
|
+
);
|
|
301
|
+
} );
|
|
302
|
+
|
|
303
|
+
it( 'throws when a workflow alias is reserved for the internal catalog', async () => {
|
|
304
|
+
const { loadWorkflows } = await import( './loader.js' );
|
|
305
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
306
|
+
yield { metadata: { name: 'alpha', aliases: [ 'catalog' ] }, path: '/root/a/workflow.js' };
|
|
307
|
+
} );
|
|
308
|
+
|
|
309
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow(
|
|
310
|
+
'Workflow "alpha" alias "catalog" is reserved for the internal catalog workflow.'
|
|
311
|
+
);
|
|
312
|
+
} );
|
|
98
313
|
} );
|
|
99
314
|
|
|
100
315
|
it( 'createWorkflowsEntryPoint writes index and returns its path', async () => {
|
|
@@ -123,89 +338,69 @@ describe( 'worker/loader', () => {
|
|
|
123
338
|
expect( contents ).toContain( 'export { default as W_legacy } from \'/abs/wf.js\';' );
|
|
124
339
|
} );
|
|
125
340
|
|
|
126
|
-
it( 'createWorkflowsEntryPoint throws on alias conflicting with primary name', async () => {
|
|
127
|
-
const { createWorkflowsEntryPoint } = await import( './loader.js' );
|
|
128
|
-
|
|
129
|
-
const workflows = [
|
|
130
|
-
{ name: 'alpha', path: '/a.js', aliases: [] },
|
|
131
|
-
{ name: 'beta', path: '/b.js', aliases: [ 'alpha' ] }
|
|
132
|
-
];
|
|
133
|
-
expect( () => createWorkflowsEntryPoint( workflows ) ).toThrow( /Alias "alpha" on workflow "beta" conflicts with workflow "alpha"/ );
|
|
134
|
-
} );
|
|
135
|
-
|
|
136
|
-
it( 'createWorkflowsEntryPoint throws on alias conflicting with another alias', async () => {
|
|
137
|
-
const { createWorkflowsEntryPoint } = await import( './loader.js' );
|
|
138
|
-
|
|
139
|
-
const workflows = [
|
|
140
|
-
{ name: 'alpha', path: '/a.js', aliases: [ 'shared_alias' ] },
|
|
141
|
-
{ name: 'beta', path: '/b.js', aliases: [ 'shared_alias' ] }
|
|
142
|
-
];
|
|
143
|
-
expect( () => createWorkflowsEntryPoint( workflows ) ).toThrow( /Alias "shared_alias" on workflow "beta" conflicts with/ );
|
|
144
|
-
} );
|
|
145
|
-
|
|
146
|
-
it( 'createWorkflowsEntryPoint throws on alias identical to own name', async () => {
|
|
147
|
-
const { createWorkflowsEntryPoint } = await import( './loader.js' );
|
|
148
|
-
|
|
149
|
-
const workflows = [ { name: 'alpha', path: '/a.js', aliases: [ 'alpha' ] } ];
|
|
150
|
-
expect( () => createWorkflowsEntryPoint( workflows ) ).toThrow( /Workflow "alpha" has an alias identical to its own name/ );
|
|
151
|
-
} );
|
|
152
|
-
|
|
153
|
-
it( 'createWorkflowsEntryPoint catches case-insensitive alias collision with primary name', async () => {
|
|
154
|
-
const { createWorkflowsEntryPoint } = await import( './loader.js' );
|
|
155
|
-
|
|
156
|
-
const workflows = [
|
|
157
|
-
{ name: 'Alpha', path: '/a.js', aliases: [] },
|
|
158
|
-
{ name: 'beta', path: '/b.js', aliases: [ 'alpha' ] }
|
|
159
|
-
];
|
|
160
|
-
expect( () => createWorkflowsEntryPoint( workflows ) ).toThrow( /Alias "alpha" on workflow "beta" conflicts with workflow "Alpha"/ );
|
|
161
|
-
} );
|
|
162
|
-
|
|
163
|
-
it( 'createWorkflowsEntryPoint catches case-insensitive alias-to-alias collision', async () => {
|
|
164
|
-
const { createWorkflowsEntryPoint } = await import( './loader.js' );
|
|
165
|
-
|
|
166
|
-
const workflows = [
|
|
167
|
-
{ name: 'alpha', path: '/a.js', aliases: [ 'Legacy' ] },
|
|
168
|
-
{ name: 'beta', path: '/b.js', aliases: [ 'legacy' ] }
|
|
169
|
-
];
|
|
170
|
-
expect( () => createWorkflowsEntryPoint( workflows ) ).toThrow( /Alias "legacy" on workflow "beta" conflicts with/ );
|
|
171
|
-
} );
|
|
172
|
-
|
|
173
341
|
it( 'loadActivities uses folder-based matchers for steps/evaluators and shared', async () => {
|
|
174
342
|
const { loadActivities } = await import( './loader.js' );
|
|
175
|
-
|
|
343
|
+
const workflowFiles = [ { path: '/a/steps/foo.js' } ];
|
|
344
|
+
const sharedFiles = [ { path: '/root/shared/steps/baz.js' } ];
|
|
345
|
+
matchFilesMock.mockReturnValueOnce( workflowFiles );
|
|
346
|
+
matchFilesMock.mockReturnValueOnce( sharedFiles );
|
|
176
347
|
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
177
|
-
// Second call (shared): no results
|
|
178
348
|
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
179
349
|
|
|
180
350
|
const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
|
|
181
351
|
await loadActivities( '/root', workflows );
|
|
182
352
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const [ firstDir, firstMatchers ] = importComponentsMock.mock.calls[0];
|
|
353
|
+
expect( matchFilesMock ).toHaveBeenCalledTimes( 2 );
|
|
354
|
+
const [ firstDir, firstMatchers ] = matchFilesMock.mock.calls[0];
|
|
186
355
|
expect( firstDir ).toBe( '/a' );
|
|
187
356
|
expect( Array.isArray( firstMatchers ) ).toBe( true );
|
|
188
|
-
// Should match folder-based steps and evaluators files
|
|
189
357
|
expect( firstMatchers.some( fn => fn( '/a/steps/foo.js' ) ) ).toBe( true );
|
|
190
358
|
expect( firstMatchers.some( fn => fn( '/a/evaluators/bar.js' ) ) ).toBe( true );
|
|
191
|
-
// And also direct file names
|
|
192
359
|
expect( firstMatchers.some( fn => fn( '/a/steps.js' ) ) ).toBe( true );
|
|
193
360
|
expect( firstMatchers.some( fn => fn( '/a/evaluators.js' ) ) ).toBe( true );
|
|
194
361
|
|
|
195
|
-
|
|
196
|
-
const [ secondDir, secondMatchers ] = importComponentsMock.mock.calls[1];
|
|
362
|
+
const [ secondDir, secondMatchers ] = matchFilesMock.mock.calls[1];
|
|
197
363
|
expect( secondDir ).toBe( '/root' );
|
|
198
364
|
expect( secondMatchers.some( fn => fn( '/root/shared/steps/baz.js' ) ) ).toBe( true );
|
|
199
365
|
expect( secondMatchers.some( fn => fn( '/root/shared/evaluators/qux.js' ) ) ).toBe( true );
|
|
366
|
+
|
|
367
|
+
expect( importComponentsMock ).toHaveBeenCalledTimes( 2 );
|
|
368
|
+
expect( importComponentsMock ).toHaveBeenNthCalledWith( 1, workflowFiles );
|
|
369
|
+
expect( importComponentsMock ).toHaveBeenNthCalledWith( 2, sharedFiles );
|
|
370
|
+
} );
|
|
371
|
+
|
|
372
|
+
it( 'loads shared activities from external workflow packages', async () => {
|
|
373
|
+
const { loadActivities } = await import( './loader.js' );
|
|
374
|
+
const externalSharedFiles = [ { path: '/root/node_modules/pkg/shared/steps/prepare.js' } ];
|
|
375
|
+
const localWorkflow = { name: 'Local', path: '/root/workflows/local/workflow.js' };
|
|
376
|
+
const externalWorkflow = { name: 'External', path: '/root/node_modules/pkg/workflows/a/workflow.js', external: true };
|
|
377
|
+
findSharedActivitiesFromWorkflowsMock.mockReturnValue( externalSharedFiles );
|
|
378
|
+
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
379
|
+
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
380
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
381
|
+
yield {
|
|
382
|
+
fn: () => {},
|
|
383
|
+
metadata: { name: 'ExternalShared', options: { activityOptions: { retry: { maximumAttempts: 2 } } } },
|
|
384
|
+
path: '/root/node_modules/pkg/shared/steps/prepare.js'
|
|
385
|
+
};
|
|
386
|
+
} );
|
|
387
|
+
|
|
388
|
+
const activities = await loadActivities( '/root', [ localWorkflow, externalWorkflow ] );
|
|
389
|
+
|
|
390
|
+
expect( findSharedActivitiesFromWorkflowsMock ).toHaveBeenCalledWith( [ externalWorkflow ] );
|
|
391
|
+
expect( importComponentsMock ).toHaveBeenNthCalledWith( 3, externalSharedFiles );
|
|
392
|
+
expect( activities['$shared#ExternalShared'] ).toBeTypeOf( 'function' );
|
|
393
|
+
const written = JSON.parse(
|
|
394
|
+
fsMocks.writeFileSync.mock.calls[0][1].replace( /^export default\s*/, '' ).replace( /;\s*$/, '' )
|
|
395
|
+
);
|
|
396
|
+
expect( written['$shared#ExternalShared'] ).toEqual( { retry: { maximumAttempts: 2 } } );
|
|
200
397
|
} );
|
|
201
398
|
|
|
202
399
|
it( 'loadActivities includes nested workflow steps and shared evaluators', async () => {
|
|
203
400
|
const { loadActivities } = await import( './loader.js' );
|
|
204
|
-
// Workflow dir scan returns a nested step
|
|
205
401
|
importComponentsMock.mockImplementationOnce( async function *() {
|
|
206
402
|
yield { fn: () => {}, metadata: { name: 'ActNested' }, path: '/a/steps/foo.js' };
|
|
207
403
|
} );
|
|
208
|
-
// Shared scan returns a shared evaluator
|
|
209
404
|
importComponentsMock.mockImplementationOnce( async function *() {
|
|
210
405
|
yield { fn: () => {}, metadata: { name: 'SharedEval' }, path: '/root/shared/evaluators/bar.js' };
|
|
211
406
|
} );
|
|
@@ -216,24 +411,14 @@ describe( 'worker/loader', () => {
|
|
|
216
411
|
expect( activities['$shared#SharedEval'] ).toBeTypeOf( 'function' );
|
|
217
412
|
} );
|
|
218
413
|
|
|
219
|
-
it( 'loadWorkflows throws when workflow is under shared directory', async () => {
|
|
220
|
-
const { loadWorkflows } = await import( './loader.js' );
|
|
221
|
-
importComponentsMock.mockImplementationOnce( async function *() {
|
|
222
|
-
yield { metadata: { name: 'Invalid' }, path: '/root/shared/workflow.js' };
|
|
223
|
-
} );
|
|
224
|
-
await expect( loadWorkflows( '/root' ) ).rejects.toThrow( 'Workflow directory can\'t be named \"shared\"' );
|
|
225
|
-
} );
|
|
226
|
-
|
|
227
414
|
it( 'collects workflow nested steps and evaluators across multiple subfolders', async () => {
|
|
228
415
|
const { loadActivities } = await import( './loader.js' );
|
|
229
|
-
// Workflow dir scan returns nested steps and evaluators
|
|
230
416
|
importComponentsMock.mockImplementationOnce( async function *() {
|
|
231
417
|
yield { fn: () => {}, metadata: { name: 'StepPrimary' }, path: '/a/steps/primary/foo.js' };
|
|
232
418
|
yield { fn: () => {}, metadata: { name: 'StepSecondary' }, path: '/a/steps/secondary/bar.js' };
|
|
233
419
|
yield { fn: () => {}, metadata: { name: 'EvalPrimary' }, path: '/a/evaluators/primary/baz.js' };
|
|
234
420
|
yield { fn: () => {}, metadata: { name: 'EvalSecondary' }, path: '/a/evaluators/secondary/qux.js' };
|
|
235
421
|
} );
|
|
236
|
-
// Shared scan returns nothing for this test
|
|
237
422
|
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
238
423
|
|
|
239
424
|
const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
|
|
@@ -246,9 +431,7 @@ describe( 'worker/loader', () => {
|
|
|
246
431
|
|
|
247
432
|
it( 'collects shared nested steps and evaluators across multiple subfolders', async () => {
|
|
248
433
|
const { loadActivities } = await import( './loader.js' );
|
|
249
|
-
// Workflow dir scan returns nothing for this test
|
|
250
434
|
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
251
|
-
// Shared scan returns nested steps and evaluators
|
|
252
435
|
importComponentsMock.mockImplementationOnce( async function *() {
|
|
253
436
|
yield { fn: () => {}, metadata: { name: 'SharedStepPrimary' }, path: '/root/shared/steps/primary/a.js' };
|
|
254
437
|
yield { fn: () => {}, metadata: { name: 'SharedStepSecondary' }, path: '/root/shared/steps/secondary/b.js' };
|
|
@@ -272,14 +455,14 @@ describe( 'worker/loader', () => {
|
|
|
272
455
|
expect( fsMocks.existsSync ).toHaveBeenCalledWith( join( '/root', 'package.json' ) );
|
|
273
456
|
} );
|
|
274
457
|
|
|
275
|
-
it( 'imports hook files listed in package.json
|
|
458
|
+
it( 'imports hook files listed in package.json outputai.hookFiles', async () => {
|
|
276
459
|
vi.doUnmock( 'node:fs' );
|
|
277
460
|
vi.resetModules();
|
|
278
461
|
const fs = await import( 'node:fs' );
|
|
279
462
|
const tmpDir = fs.mkdtempSync( join( tmpdir(), 'loader-spec-' ) );
|
|
280
463
|
try {
|
|
281
464
|
fs.writeFileSync( join( tmpDir, 'package.json' ), JSON.stringify( {
|
|
282
|
-
|
|
465
|
+
outputai: { hookFiles: [ 'hook.js' ] }
|
|
283
466
|
} ) );
|
|
284
467
|
fs.writeFileSync( join( tmpDir, 'hook.js' ), 'globalThis.__loadHooksTestLoaded = true;' );
|
|
285
468
|
|
|
@@ -291,5 +474,25 @@ describe( 'worker/loader', () => {
|
|
|
291
474
|
fs.rmSync( tmpDir, { recursive: true, force: true } );
|
|
292
475
|
}
|
|
293
476
|
} );
|
|
477
|
+
|
|
478
|
+
it( 'imports hook files from legacy package.json output.hookFiles', async () => {
|
|
479
|
+
vi.doUnmock( 'node:fs' );
|
|
480
|
+
vi.resetModules();
|
|
481
|
+
const fs = await import( 'node:fs' );
|
|
482
|
+
const tmpDir = fs.mkdtempSync( join( tmpdir(), 'loader-spec-' ) );
|
|
483
|
+
try {
|
|
484
|
+
fs.writeFileSync( join( tmpDir, 'package.json' ), JSON.stringify( {
|
|
485
|
+
output: { hookFiles: [ 'legacy_hook.js' ] }
|
|
486
|
+
} ) );
|
|
487
|
+
fs.writeFileSync( join( tmpDir, 'legacy_hook.js' ), 'globalThis.__loadHooksLegacyTestLoaded = true;' );
|
|
488
|
+
|
|
489
|
+
const { loadHooks } = await import( './loader.js' );
|
|
490
|
+
await loadHooks( tmpDir );
|
|
491
|
+
expect( globalThis.__loadHooksLegacyTestLoaded ).toBe( true );
|
|
492
|
+
} finally {
|
|
493
|
+
delete globalThis.__loadHooksLegacyTestLoaded;
|
|
494
|
+
fs.rmSync( tmpDir, { recursive: true, force: true } );
|
|
495
|
+
}
|
|
496
|
+
} );
|
|
294
497
|
} );
|
|
295
498
|
} );
|