@output.ai/core 0.3.0-dev.pr263-a59dd0e → 0.3.0-dev.pr263-7879ff1

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/bin/worker.sh CHANGED
@@ -23,4 +23,4 @@ sdk_dir="$(dirname "$script_dir")"
23
23
 
24
24
  cd ${sdk_dir}
25
25
 
26
- exec npm run worker -- ${invocation_dir} "${@:2}" # Pass remaining args
26
+ exec node "${sdk_dir}/src/worker/index.js" "${invocation_dir}" "${@:2}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@output.ai/core",
3
- "version": "0.3.0-dev.pr263-a59dd0e",
3
+ "version": "0.3.0-dev.pr263-7879ff1",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
package/src/consts.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const ACTIVITY_SEND_HTTP_REQUEST = '__internal#sendHttpRequest';
2
2
  export const ACTIVITY_GET_TRACE_DESTINATIONS = '__internal#getTraceDestinations';
3
3
  export const METADATA_ACCESS_SYMBOL = Symbol( '__metadata' );
4
- export const SHARED_STEP_PREFIX = '__shared#';
4
+ export const SHARED_STEP_PREFIX = '$shared';
5
5
  export const WORKFLOWS_INDEX_FILENAME = '__workflows_entrypoint.js';
6
6
  export const ACTIVITY_OPTIONS_FILENAME = '__activity_options.js';
7
7
  export const WORKFLOW_CATALOG = '$catalog';
@@ -81,6 +81,7 @@ export class EvaluationStringResult extends EvaluationResult {
81
81
  * @param {number} args.confidence - The confidence on the evaluation
82
82
  * @param {string} [args.reasoning] - The reasoning behind the result
83
83
  */
84
+ // eslint-disable-next-line no-useless-constructor
84
85
  constructor( args ) {
85
86
  super( args );
86
87
  }
@@ -103,6 +104,7 @@ export class EvaluationBooleanResult extends EvaluationResult {
103
104
  * @param {number} args.confidence - The confidence on the evaluation
104
105
  * @param {string} [args.reasoning] - The reasoning behind the result
105
106
  */
107
+ // eslint-disable-next-line no-useless-constructor
106
108
  constructor( args ) {
107
109
  super( args );
108
110
  }
@@ -125,6 +127,7 @@ export class EvaluationNumberResult extends EvaluationResult {
125
127
  * @param {number} args.confidence - The confidence on the evaluation
126
128
  * @param {string} [args.reasoning] - The reasoning behind the result
127
129
  */
130
+ // eslint-disable-next-line no-useless-constructor
128
131
  constructor( args ) {
129
132
  super( args );
130
133
  }
@@ -3,7 +3,7 @@ import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, uuid4,
3
3
  import { validateWorkflow } from './validations/static.js';
4
4
  import { validateWithSchema } from './validations/runtime.js';
5
5
  import { SHARED_STEP_PREFIX, ACTIVITY_GET_TRACE_DESTINATIONS } from '#consts';
6
- import { deepMerge, mergeActivityOptions, resolveInvocationDir, setMetadata } from '#utils';
6
+ import { deepMerge, mergeActivityOptions, setMetadata } from '#utils';
7
7
  import { FatalError, ValidationError } from '#errors';
8
8
 
9
9
  /**
@@ -51,7 +51,6 @@ const defaultActivityOptions = {
51
51
 
52
52
  export function workflow( { name, description, inputSchema, outputSchema, fn, options } ) {
53
53
  validateWorkflow( { name, description, inputSchema, outputSchema, fn, options } );
54
- const workflowPath = resolveInvocationDir();
55
54
 
56
55
  const activityOptions = mergeActivityOptions( defaultActivityOptions, options );
57
56
  const steps = proxyActivities( activityOptions );
@@ -99,9 +98,9 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
99
98
 
100
99
  // binds the methods called in the code that Webpack loader will add, they will exposed via "this"
101
100
  const output = await fn.call( {
102
- invokeStep: async ( stepName, input, options ) => steps[`${workflowPath}#${stepName}`]( input, options ),
101
+ invokeStep: async ( stepName, input, options ) => steps[`${name}#${stepName}`]( input, options ),
103
102
  invokeSharedStep: async ( stepName, input, options ) => steps[`${SHARED_STEP_PREFIX}#${stepName}`]( input, options ),
104
- invokeEvaluator: async ( evaluatorName, input, options ) => steps[`${workflowPath}#${evaluatorName}`]( input, options ),
103
+ invokeEvaluator: async ( evaluatorName, input, options ) => steps[`${name}#${evaluatorName}`]( input, options ),
105
104
 
106
105
  /**
107
106
  * Start a child workflow
@@ -1,4 +1,4 @@
1
- import { defineQuery, setHandler } from '@temporalio/workflow';
1
+ import { defineQuery, setHandler, condition } from '@temporalio/workflow';
2
2
 
3
3
  /**
4
4
  * This is a special workflow, unique to each worker, which holds the meta information of all other workflows in that worker.
@@ -9,5 +9,6 @@ import { defineQuery, setHandler } from '@temporalio/workflow';
9
9
  */
10
10
  export default async function catalogWorkflow( catalog ) {
11
11
  setHandler( defineQuery( 'get' ), () => catalog );
12
- await new Promise( () => {} ); // stay alive
12
+ // Wait indefinitely but remain responsive to workflow cancellation
13
+ await condition( () => false );
13
14
  };
@@ -18,7 +18,7 @@ const callerDir = process.argv[2];
18
18
  const workflows = await loadWorkflows( callerDir );
19
19
 
20
20
  console.log( '[Core]', 'Loading activities...', { callerDir } );
21
- const activities = await loadActivities( callerDir );
21
+ const activities = await loadActivities( callerDir, workflows );
22
22
 
23
23
  console.log( '[Core]', 'Creating worker entry point...' );
24
24
  const workflowsPath = createWorkflowsEntryPoint( workflows );
@@ -56,5 +56,39 @@ const callerDir = process.argv[2];
56
56
  } );
57
57
 
58
58
  console.log( '[Core]', 'Running worker...' );
59
- worker.run();
60
- } )();
59
+
60
+ // FORCE_QUIT_GRACE_MS delays the second instance of a shutdown command.
61
+ // If running output-worker directly with npx, 2 signals are recieved in
62
+ // rapid succession, and users see the force quit message.
63
+ const FORCE_QUIT_GRACE_MS = 1000;
64
+ const state = { isShuttingDown: false, shutdownStartedAt: null };
65
+
66
+ const shutdown = signal => {
67
+ if ( state.isShuttingDown ) {
68
+ const elapsed = Date.now() - state.shutdownStartedAt;
69
+ if ( elapsed < FORCE_QUIT_GRACE_MS ) {
70
+ return; // ignore rapid duplicate signals
71
+ }
72
+ process.stderr.write( '[Core] Force quitting...\n' );
73
+ process.exit( 1 );
74
+ }
75
+ state.isShuttingDown = true;
76
+ state.shutdownStartedAt = Date.now();
77
+ process.stderr.write( `[Core] Received ${signal}, shutting down...\n` );
78
+ worker.shutdown();
79
+ };
80
+
81
+ process.on( 'SIGTERM', () => shutdown( 'SIGTERM' ) );
82
+ process.on( 'SIGINT', () => shutdown( 'SIGINT' ) );
83
+
84
+ await worker.run();
85
+ process.stderr.write( '[Core] Worker stopped.\n' );
86
+
87
+ await connection.close();
88
+ process.stderr.write( '[Core] Connection closed.\n' );
89
+
90
+ process.exit( 0 );
91
+ } )().catch( error => {
92
+ process.stderr.write( `[Core] Fatal error: ${error.message}\n` );
93
+ process.exit( 1 );
94
+ } );
@@ -3,7 +3,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { EOL } from 'node:os';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { getTraceDestinations, sendHttpRequest } from '#internal_activities';
6
- import { importComponents } from './loader_tools.js';
6
+ import { importComponents, staticMatchers, activityMatchersBuilder } from './loader_tools.js';
7
7
  import {
8
8
  ACTIVITY_SEND_HTTP_REQUEST,
9
9
  ACTIVITY_OPTIONS_FILENAME,
@@ -27,24 +27,61 @@ const writeActivityOptionsFile = map => {
27
27
  };
28
28
 
29
29
  /**
30
- * Builds a map of activities, where the key is their path and name and the value is the function
30
+ * Creates the activity key that will identify it on Temporal.
31
31
  *
32
- * @param {string} target
32
+ * It composes it using a namespace and the name of the activity.
33
+ *
34
+ * No two activities with the same name can exist on the same namespace.
35
+ *
36
+ * @param {object} options
37
+ * @param {string} namespace
38
+ * @param {string} activityName
39
+ * @returns {string} key
40
+ */
41
+ const generateActivityKey = ( { namespace, activityName } ) => `${namespace}#${activityName}`;
42
+
43
+ /**
44
+ * Load activities:
45
+ *
46
+ * - Scans activities based on workflows, using each workflow folder as a point to lookup for steps, evaluators files;
47
+ * - Scans shared activities in the rootDir;
48
+ * - Loads internal activities as well;
49
+ *
50
+ * Builds a map of activities, where they is generated according to the type of activity and the value is the function itself and return it.
51
+ * - Shared activity keys have a common prefix followed by the activity name;
52
+ * - Internal activities are registered with a fixed key;
53
+ * - Workflow activities keys are composed using the workflow name and the activity name;
54
+ *
55
+ * @param {string} rootDir
56
+ * @param {object[]} workflows
33
57
  * @returns {object}
34
58
  */
35
- export async function loadActivities( target ) {
59
+ export async function loadActivities( rootDir, workflows ) {
36
60
  const activities = {};
37
61
  const activityOptionsMap = {};
38
- for await ( const { fn, metadata, path, isShared } of importComponents( target, [ 'steps.js', 'evaluators.js' ] ) ) {
39
- const prefix = isShared ? SHARED_STEP_PREFIX : dirname( path );
40
62
 
41
- console.log( '[Core.Scanner]', 'Component loaded:', metadata.type, metadata.name, 'at', path );
42
- activities[`${prefix}#${metadata.name}`] = fn;
43
- if ( metadata.options ) {
44
- activityOptionsMap[`${prefix}#${metadata.name}`] = metadata.options;
63
+ // Load workflow based activities
64
+ for ( const { path: workflowPath, name: workflowName } of workflows ) {
65
+ const dir = dirname( workflowPath );
66
+ for await ( const { fn, metadata, path } of importComponents( dir, Object.values( activityMatchersBuilder( dir ) ) ) ) {
67
+ console.log( '[Core.Scanner]', 'Component loaded:', metadata.type, metadata.name, 'at', path );
68
+ // Activities loaded from a workflow path will use the workflow name as a namespace, which is unique across the platform, avoiding collision
69
+ const activityKey = generateActivityKey( { namespace: workflowName, activityName: metadata.name } );
70
+ activities[activityKey] = fn;
71
+ // propagate the custom options set on the step()/evaluator() constructor
72
+ activityOptionsMap[activityKey] = metadata.options ?? undefined;
45
73
  }
46
74
  }
47
75
 
76
+ // Load shared activities/evaluators
77
+ for await ( const { fn, metadata, path } of importComponents( rootDir, [ staticMatchers.sharedStepsDir, staticMatchers.sharedEvaluatorsDir ] ) ) {
78
+ console.log( '[Core.Scanner]', 'Component loaded: shared ', metadata.type, metadata.name, 'at', path );
79
+ // The namespace for shared activities is fixed
80
+ const activityKey = generateActivityKey( { namespace: SHARED_STEP_PREFIX, activityName: metadata.name } );
81
+ activities[activityKey] = fn;
82
+ activityOptionsMap[activityKey] = metadata.options ?? undefined;
83
+ }
84
+
48
85
  // writes down the activity option overrides
49
86
  writeActivityOptionsFile( activityOptionsMap );
50
87
 
@@ -55,14 +92,19 @@ export async function loadActivities( target ) {
55
92
  };
56
93
 
57
94
  /**
58
- * Builds an array of workflow objects
95
+ * Scan and find workflow.js files and import them.
96
+ *
97
+ * Creates an array containing their metadata and path and return it.
59
98
  *
60
- * @param {string} target
99
+ * @param {string} rootDir
61
100
  * @returns {object[]}
62
101
  */
63
- export async function loadWorkflows( target ) {
102
+ export async function loadWorkflows( rootDir ) {
64
103
  const workflows = [];
65
- for await ( const { metadata, path } of importComponents( target, [ 'workflow.js' ] ) ) {
104
+ for await ( const { metadata, path } of importComponents( rootDir, [ staticMatchers.workflowFile ] ) ) {
105
+ if ( staticMatchers.workflowPathHasShared( path ) ) {
106
+ throw new Error( 'Workflow directory can\'t be named "shared"' );
107
+ }
66
108
  console.log( '[Core.Scanner]', 'Workflow loaded:', metadata.name, 'at', path );
67
109
  workflows.push( { ...metadata, path } );
68
110
  }
@@ -70,7 +112,7 @@ export async function loadWorkflows( target ) {
70
112
  };
71
113
 
72
114
  /**
73
- * Creates a temporary index file importing all workflows
115
+ * Creates a temporary index file importing all workflows for Temporal.
74
116
  *
75
117
  * @param {object[]} workflows
76
118
  * @returns
@@ -6,7 +6,7 @@ vi.mock( '#consts', () => ( {
6
6
  WORKFLOWS_INDEX_FILENAME: '__workflows_entrypoint.js',
7
7
  WORKFLOW_CATALOG: 'catalog',
8
8
  ACTIVITY_OPTIONS_FILENAME: '__activity_options.js',
9
- SHARED_STEP_PREFIX: '/shared'
9
+ SHARED_STEP_PREFIX: '$shared'
10
10
  } ) );
11
11
 
12
12
  const sendHttpRequestMock = vi.fn();
@@ -17,7 +17,10 @@ vi.mock( '#internal_activities', () => ( {
17
17
  } ) );
18
18
 
19
19
  const importComponentsMock = vi.fn();
20
- vi.mock( './loader_tools.js', () => ( { importComponents: importComponentsMock } ) );
20
+ vi.mock( './loader_tools.js', async importOriginal => {
21
+ const actual = await importOriginal();
22
+ return { ...actual, importComponents: importComponentsMock };
23
+ } );
21
24
 
22
25
  const mkdirSyncMock = vi.fn();
23
26
  const writeFileSyncMock = vi.fn();
@@ -34,12 +37,16 @@ describe( 'worker/loader', () => {
34
37
  it( 'loadActivities returns map including system activity and writes options file', async () => {
35
38
  const { loadActivities } = await import( './loader.js' );
36
39
 
40
+ // First call: workflow directory scan
37
41
  importComponentsMock.mockImplementationOnce( async function *() {
38
42
  yield { fn: () => {}, metadata: { name: 'Act1', options: { retry: { maximumAttempts: 3 } } }, path: '/a/steps.js' };
39
43
  } );
44
+ // Second call: shared activities scan (no results)
45
+ importComponentsMock.mockImplementationOnce( async function *() {} );
40
46
 
41
- const activities = await loadActivities( '/root' );
42
- expect( activities['/a#Act1'] ).toBeTypeOf( 'function' );
47
+ const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
48
+ const activities = await loadActivities( '/root', workflows );
49
+ expect( activities['A#Act1'] ).toBeTypeOf( 'function' );
43
50
  expect( activities['__internal#sendHttpRequest'] ).toBe( sendHttpRequestMock );
44
51
 
45
52
  // options file written with the collected map
@@ -48,32 +55,11 @@ describe( 'worker/loader', () => {
48
55
  expect( writtenPath ).toMatch( /temp\/__activity_options\.js$/ );
49
56
  expect( contents ).toContain( 'export default' );
50
57
  expect( JSON.parse( contents.replace( /^export default\s*/, '' ).replace( /;\s*$/, '' ) ) ).toEqual( {
51
- '/a#Act1': { retry: { maximumAttempts: 3 } }
58
+ 'A#Act1': { retry: { maximumAttempts: 3 } }
52
59
  } );
53
60
  expect( mkdirSyncMock ).toHaveBeenCalled();
54
61
  } );
55
62
 
56
- it( 'loadActivities uses SHARED_STEP_PREFIX for components with isShared flag', async () => {
57
- const { loadActivities } = await import( './loader.js' );
58
-
59
- importComponentsMock.mockImplementationOnce( async function *() {
60
- yield { fn: () => 'local', metadata: { name: 'LocalStep' }, path: '/workflows/example/steps.js', isShared: false };
61
- yield { fn: () => 'shared', metadata: { name: 'SharedStep' }, path: '/shared/steps/tools.js', isShared: true };
62
- } );
63
-
64
- const activities = await loadActivities( '/root' );
65
-
66
- // Local step uses dirname as prefix
67
- expect( activities['/workflows/example#LocalStep'] ).toBeTypeOf( 'function' );
68
-
69
- // Shared step uses SHARED_STEP_PREFIX ('/shared' from mock)
70
- expect( activities['/shared#SharedStep'] ).toBeTypeOf( 'function' );
71
-
72
- // Verify they're different functions
73
- expect( activities['/workflows/example#LocalStep']() ).toBe( 'local' );
74
- expect( activities['/shared#SharedStep']() ).toBe( 'shared' );
75
- } );
76
-
77
63
  it( 'loadWorkflows returns array of workflows with metadata', async () => {
78
64
  const { loadWorkflows } = await import( './loader.js' );
79
65
 
@@ -98,4 +84,98 @@ describe( 'worker/loader', () => {
98
84
  expect( contents ).toContain( 'export { default as catalog }' );
99
85
  expect( mkdirSyncMock ).toHaveBeenCalledTimes( 1 );
100
86
  } );
87
+
88
+ it( 'loadActivities uses folder-based matchers for steps/evaluators and shared', async () => {
89
+ const { loadActivities } = await import( './loader.js' );
90
+ // First call (workflow dir): no results
91
+ importComponentsMock.mockImplementationOnce( async function *() {} );
92
+ // Second call (shared): no results
93
+ importComponentsMock.mockImplementationOnce( async function *() {} );
94
+
95
+ const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
96
+ await loadActivities( '/root', workflows );
97
+
98
+ // First invocation should target the workflow directory with folder/file matchers
99
+ expect( importComponentsMock ).toHaveBeenCalledTimes( 2 );
100
+ const [ firstDir, firstMatchers ] = importComponentsMock.mock.calls[0];
101
+ expect( firstDir ).toBe( '/a' );
102
+ expect( Array.isArray( firstMatchers ) ).toBe( true );
103
+ // Should match folder-based steps and evaluators files
104
+ expect( firstMatchers.some( fn => fn( '/a/steps/foo.js' ) ) ).toBe( true );
105
+ expect( firstMatchers.some( fn => fn( '/a/evaluators/bar.js' ) ) ).toBe( true );
106
+ // And also direct file names
107
+ expect( firstMatchers.some( fn => fn( '/a/steps.js' ) ) ).toBe( true );
108
+ expect( firstMatchers.some( fn => fn( '/a/evaluators.js' ) ) ).toBe( true );
109
+
110
+ // Second invocation should target root with shared matchers
111
+ const [ secondDir, secondMatchers ] = importComponentsMock.mock.calls[1];
112
+ expect( secondDir ).toBe( '/root' );
113
+ expect( secondMatchers.some( fn => fn( '/root/shared/steps/baz.js' ) ) ).toBe( true );
114
+ expect( secondMatchers.some( fn => fn( '/root/shared/evaluators/qux.js' ) ) ).toBe( true );
115
+ } );
116
+
117
+ it( 'loadActivities includes nested workflow steps and shared evaluators', async () => {
118
+ const { loadActivities } = await import( './loader.js' );
119
+ // Workflow dir scan returns a nested step
120
+ importComponentsMock.mockImplementationOnce( async function *() {
121
+ yield { fn: () => {}, metadata: { name: 'ActNested' }, path: '/a/steps/foo.js' };
122
+ } );
123
+ // Shared scan returns a shared evaluator
124
+ importComponentsMock.mockImplementationOnce( async function *() {
125
+ yield { fn: () => {}, metadata: { name: 'SharedEval' }, path: '/root/shared/evaluators/bar.js' };
126
+ } );
127
+
128
+ const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
129
+ const activities = await loadActivities( '/root', workflows );
130
+ expect( activities['A#ActNested'] ).toBeTypeOf( 'function' );
131
+ expect( activities['$shared#SharedEval'] ).toBeTypeOf( 'function' );
132
+ } );
133
+
134
+ it( 'loadWorkflows throws when workflow is under shared directory', async () => {
135
+ const { loadWorkflows } = await import( './loader.js' );
136
+ importComponentsMock.mockImplementationOnce( async function *() {
137
+ yield { metadata: { name: 'Invalid' }, path: '/root/shared/workflow.js' };
138
+ } );
139
+ await expect( loadWorkflows( '/root' ) ).rejects.toThrow( 'Workflow directory can\'t be named \"shared\"' );
140
+ } );
141
+
142
+ it( 'collects workflow nested steps and evaluators across multiple subfolders', async () => {
143
+ const { loadActivities } = await import( './loader.js' );
144
+ // Workflow dir scan returns nested steps and evaluators
145
+ importComponentsMock.mockImplementationOnce( async function *() {
146
+ yield { fn: () => {}, metadata: { name: 'StepPrimary' }, path: '/a/steps/primary/foo.js' };
147
+ yield { fn: () => {}, metadata: { name: 'StepSecondary' }, path: '/a/steps/secondary/bar.js' };
148
+ yield { fn: () => {}, metadata: { name: 'EvalPrimary' }, path: '/a/evaluators/primary/baz.js' };
149
+ yield { fn: () => {}, metadata: { name: 'EvalSecondary' }, path: '/a/evaluators/secondary/qux.js' };
150
+ } );
151
+ // Shared scan returns nothing for this test
152
+ importComponentsMock.mockImplementationOnce( async function *() {} );
153
+
154
+ const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
155
+ const activities = await loadActivities( '/root', workflows );
156
+ expect( activities['A#StepPrimary'] ).toBeTypeOf( 'function' );
157
+ expect( activities['A#StepSecondary'] ).toBeTypeOf( 'function' );
158
+ expect( activities['A#EvalPrimary'] ).toBeTypeOf( 'function' );
159
+ expect( activities['A#EvalSecondary'] ).toBeTypeOf( 'function' );
160
+ } );
161
+
162
+ it( 'collects shared nested steps and evaluators across multiple subfolders', async () => {
163
+ const { loadActivities } = await import( './loader.js' );
164
+ // Workflow dir scan returns nothing for this test
165
+ importComponentsMock.mockImplementationOnce( async function *() {} );
166
+ // Shared scan returns nested steps and evaluators
167
+ importComponentsMock.mockImplementationOnce( async function *() {
168
+ yield { fn: () => {}, metadata: { name: 'SharedStepPrimary' }, path: '/root/shared/steps/primary/a.js' };
169
+ yield { fn: () => {}, metadata: { name: 'SharedStepSecondary' }, path: '/root/shared/steps/secondary/b.js' };
170
+ yield { fn: () => {}, metadata: { name: 'SharedEvalPrimary' }, path: '/root/shared/evaluators/primary/c.js' };
171
+ yield { fn: () => {}, metadata: { name: 'SharedEvalSecondary' }, path: '/root/shared/evaluators/secondary/d.js' };
172
+ } );
173
+
174
+ const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
175
+ const activities = await loadActivities( '/root', workflows );
176
+ expect( activities['$shared#SharedStepPrimary'] ).toBeTypeOf( 'function' );
177
+ expect( activities['$shared#SharedStepSecondary'] ).toBeTypeOf( 'function' );
178
+ expect( activities['$shared#SharedEvalPrimary'] ).toBeTypeOf( 'function' );
179
+ expect( activities['$shared#SharedEvalSecondary'] ).toBeTypeOf( 'function' );
180
+ } );
101
181
  } );
@@ -1,4 +1,4 @@
1
- import { resolve } from 'path';
1
+ import { resolve, sep } from 'path';
2
2
  import { pathToFileURL } from 'url';
3
3
  import { METADATA_ACCESS_SYMBOL } from '#consts';
4
4
  import { readdirSync } from 'fs';
@@ -7,59 +7,23 @@ import { readdirSync } from 'fs';
7
7
  * @typedef {object} CollectedFile
8
8
  * @property {string} path - The file path
9
9
  * @property {string} url - The resolved url of the file, ready to be imported
10
- * @property {boolean} [isShared] - Whether this file is in a shared/steps/ directory
11
10
  */
12
11
  /**
13
12
  * @typedef {object} Component
14
13
  * @property {Function} fn - The loaded component function
15
14
  * @property {object} metadata - Associated metadata with the component
16
- * @property {string} path - The file path of the component
17
- * @property {boolean} [isShared] - Whether this component is from a shared/steps/ directory
15
+ * @property {string} path - Associated metadata with the component
18
16
  */
19
17
 
20
18
  /**
21
- * Check if the search is targeting step or evaluator files.
22
- * @param {string[]} filenames - The filenames being searched for.
23
- * @returns {boolean} True if searching for steps.js or evaluators.js.
24
- */
25
- const isSearchingForStepFiles = filenames =>
26
- filenames.includes( 'steps.js' ) || filenames.includes( 'evaluators.js' );
27
-
28
- /**
29
- * Check if a directory is a shared steps directory.
30
- * @param {string} dirName - The directory name.
31
- * @param {string} parentPath - The parent path.
32
- * @returns {boolean} True if this is a shared/steps directory.
33
- */
34
- const isSharedStepsDirectory = ( dirName, parentPath ) =>
35
- dirName === 'steps' && parentPath.endsWith( '/shared' );
36
-
37
- /**
38
- * Collect all .js files from a shared steps directory.
39
- * @param {string} dirPath - The directory path.
40
- * @param {CollectedFile[]} collection - The collection to add files to.
41
- */
42
- const collectSharedStepsFiles = ( dirPath, collection ) => {
43
- for ( const entry of readdirSync( dirPath, { withFileTypes: true } ) ) {
44
- if ( entry.isFile() && entry.name.endsWith( '.js' ) ) {
45
- const filePath = resolve( dirPath, entry.name );
46
- collection.push( {
47
- path: filePath,
48
- url: pathToFileURL( filePath ).href,
49
- isShared: true
50
- } );
51
- }
52
- }
53
- };
54
-
55
- /**
56
- * Recursive traverse directories looking for files with given name.
19
+ * Recursive traverse directories collection files with paths that match one of the given matches.
57
20
  *
58
21
  * @param {string} path - The path to scan
59
- * @param {string[]} filenames - The filenames to look for
22
+ * @param {function[]} matchers - Boolean functions to match files to add to collection
60
23
  * @returns {CollectedFile[]} An array containing the collected files
61
- * */
62
- const findByNameRecursively = ( parentPath, filenames, collection = [], ignoreDirNames = [ 'vendor', 'node_modules' ] ) => {
24
+ */
25
+ const findByNameRecursively = ( parentPath, matchers, ignoreDirNames = [ 'vendor', 'node_modules' ] ) => {
26
+ const collection = [];
63
27
  for ( const entry of readdirSync( parentPath, { withFileTypes: true } ) ) {
64
28
  if ( ignoreDirNames.includes( entry.name ) ) {
65
29
  continue;
@@ -67,12 +31,8 @@ const findByNameRecursively = ( parentPath, filenames, collection = [], ignoreDi
67
31
 
68
32
  const path = resolve( parentPath, entry.name );
69
33
  if ( entry.isDirectory() ) {
70
- if ( isSharedStepsDirectory( entry.name, parentPath ) && isSearchingForStepFiles( filenames ) ) {
71
- collectSharedStepsFiles( path, collection );
72
- } else {
73
- findByNameRecursively( path, filenames, collection, ignoreDirNames );
74
- }
75
- } else if ( filenames.includes( entry.name ) ) {
34
+ collection.push( ...findByNameRecursively( path, matchers ) );
35
+ } else if ( matchers.some( m => m( path ) ) ) {
76
36
  collection.push( { path, url: pathToFileURL( path ).href } );
77
37
  }
78
38
  }
@@ -81,24 +41,92 @@ const findByNameRecursively = ( parentPath, filenames, collection = [], ignoreDi
81
41
  };
82
42
 
83
43
  /**
84
- * For each path, dynamic import it, and for each exported component with metadata (step, workflow), yields it.
44
+ * Scan a path for files testing each path against a matching function.
45
+ *
46
+ * For each file found, dynamic import it and for each exports on that file, yields it.
47
+ *
48
+ * @remarks
49
+ * - Only yields exports that have the METADATA_ACCESS_SYMBOL, as they are output components (steps, evaluators, etc).
85
50
  *
86
51
  * @generator
87
52
  * @async
88
53
  * @function importComponents
89
54
  * @param {string} target - Place to look for files
90
- * @param {string[]} filenames - File names to load recursively from target
55
+ * @param {function[]} matchers - Boolean functions to match files
91
56
  * @yields {Component}
92
57
  */
93
- export async function *importComponents( target, filenames ) {
94
- for ( const { url, path, isShared } of findByNameRecursively( target, filenames ) ) {
58
+ export async function *importComponents( target, matchers ) {
59
+ for ( const { url, path } of findByNameRecursively( target, matchers ) ) {
95
60
  const imported = await import( url );
96
61
  for ( const fn of Object.values( imported ) ) {
97
62
  const metadata = fn[METADATA_ACCESS_SYMBOL];
98
63
  if ( !metadata ) {
99
64
  continue;
100
65
  }
101
- yield { fn, metadata, path, isShared };
66
+ yield { fn, metadata, path };
102
67
  }
103
68
  }
104
69
  };
70
+
71
+ /**
72
+ * Returns matchers that need to be built using a relative path
73
+ *
74
+ * @param {string} path
75
+ * @returns {object} The object containing the matchers
76
+ */
77
+ export const activityMatchersBuilder = path => ( {
78
+ /**
79
+ * Matches a file called steps.js, located at the path
80
+ * @param {string} path - Path to test
81
+ * @returns {boolean}
82
+ */
83
+ stepsFile: v => v === `${path}${sep}steps.js`,
84
+ /**
85
+ * Matches a file called evaluators.js, located at the path
86
+ * @param {string} path - Path to test
87
+ * @returns {boolean}
88
+ */
89
+ evaluatorsFile: v => v === `${path}${sep}evaluators.js`,
90
+ /**
91
+ * Matches all files on any levels inside a folder called steps/, located at the path
92
+ * @param {string} path - Path to test
93
+ * @returns {boolean}
94
+ */
95
+ stepsDir: v => v.startsWith( `${path}${sep}steps${sep}` ),
96
+ /**
97
+ * Matches all files on any levels inside a folder called evaluators/, located at the path
98
+ * @param {string} path - Path to test
99
+ * @returns {boolean}
100
+ */
101
+ evaluatorsDir: v => v.startsWith( `${path}${sep}evaluators${sep}` )
102
+ } );
103
+
104
+ /**
105
+ * Matchers that can be used to access conditions without initializing them
106
+ */
107
+ export const staticMatchers = {
108
+ /**
109
+ * Matches a workflow.js file
110
+ * @param {string} path - Path to test
111
+ * @returns {boolean}
112
+ */
113
+ workflowFile: v => v.endsWith( `${sep}workflow.js` ),
114
+ /**
115
+ * Matches a workflow.js that is inside a shared folder: eg foo/shared/workflow.js
116
+ * @param {string} path - Path to test
117
+ * @returns {boolean}
118
+ */
119
+ workflowPathHasShared: v => v.endsWith( `${sep}shared${sep}workflow.js` ),
120
+ /**
121
+ * Matches the shared folder for steps src/shared/steps/../step_file.js
122
+ * @param {string} path - Path to test
123
+ * @returns {boolean}
124
+ */
125
+ sharedStepsDir: v => v.includes( `${sep}shared${sep}steps${sep}` ),
126
+ /**
127
+ * Matches the shared folder for evaluators src/shared/evaluators/../evaluator_file.js
128
+ * @param {string} path - Path to test
129
+ * @returns {boolean}
130
+ */
131
+ sharedEvaluatorsDir: v => v.includes( `${sep}shared${sep}evaluators${sep}` )
132
+ };
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, afterEach } from 'vitest';
2
2
  import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
3
- import { join } from 'node:path';
3
+ import { join, sep } from 'node:path';
4
4
  import { importComponents } from './loader_tools.js';
5
5
 
6
6
  describe( '.importComponents', () => {
@@ -21,7 +21,7 @@ describe( '.importComponents', () => {
21
21
  ].join( '\n' ) );
22
22
 
23
23
  const collected = [];
24
- for await ( const m of importComponents( root, [ 'meta.module.js' ] ) ) {
24
+ for await ( const m of importComponents( root, [ v => v.endsWith( 'meta.module.js' ) ] ) ) {
25
25
  collected.push( m );
26
26
  }
27
27
 
@@ -46,7 +46,7 @@ describe( '.importComponents', () => {
46
46
  ].join( '\n' ) );
47
47
 
48
48
  const collected = [];
49
- for await ( const m of importComponents( root, [ 'meta.module.js' ] ) ) {
49
+ for await ( const m of importComponents( root, [ v => v.endsWith( 'meta.module.js' ) ] ) ) {
50
50
  collected.push( m );
51
51
  }
52
52
 
@@ -77,7 +77,7 @@ describe( '.importComponents', () => {
77
77
  writeFileSync( vendorFile, fileContents );
78
78
 
79
79
  const collected = [];
80
- for await ( const m of importComponents( root, [ 'meta.module.js' ] ) ) {
80
+ for await ( const m of importComponents( root, [ v => v.endsWith( 'meta.module.js' ) ] ) ) {
81
81
  collected.push( m );
82
82
  }
83
83
 
@@ -87,82 +87,31 @@ describe( '.importComponents', () => {
87
87
  rmSync( root, { recursive: true, force: true } );
88
88
  } );
89
89
 
90
- it( 'collects all .js files from shared/steps/ directory with isShared flag', async () => {
91
- const root = join( process.cwd(), 'sdk/core/temp_test_modules', `meta-${Date.now()}-shared` );
92
- const sharedStepsDir = join( root, 'shared', 'steps' );
93
- const workflowDir = join( root, 'workflows', 'example' );
94
- mkdirSync( sharedStepsDir, { recursive: true } );
95
- mkdirSync( workflowDir, { recursive: true } );
96
-
97
- const sharedToolsFile = join( sharedStepsDir, 'tools.js' );
98
- const sharedUtilsFile = join( sharedStepsDir, 'utils.js' );
99
- const workflowStepsFile = join( workflowDir, 'steps.js' );
100
-
101
- const sharedFileContents = [
102
- 'import { METADATA_ACCESS_SYMBOL } from "#consts";',
103
- 'export const SharedStep = () => {};',
104
- 'SharedStep[METADATA_ACCESS_SYMBOL] = { kind: "step", name: "sharedStep" };'
105
- ].join( '\n' );
106
- const workflowFileContents = [
107
- 'import { METADATA_ACCESS_SYMBOL } from "#consts";',
108
- 'export const LocalStep = () => {};',
109
- 'LocalStep[METADATA_ACCESS_SYMBOL] = { kind: "step", name: "localStep" };'
110
- ].join( '\n' );
111
- writeFileSync( sharedToolsFile, sharedFileContents );
112
- writeFileSync( sharedUtilsFile, sharedFileContents );
113
- writeFileSync( workflowStepsFile, workflowFileContents );
114
-
115
- const collected = [];
116
- for await ( const m of importComponents( root, [ 'steps.js' ] ) ) {
117
- collected.push( m );
118
- }
119
-
120
- expect( collected.length ).toBe( 3 );
121
-
122
- const sharedComponents = collected.filter( m => m.isShared === true );
123
- const localComponents = collected.filter( m => !m.isShared );
124
-
125
- expect( sharedComponents.length ).toBe( 2 );
126
- expect( localComponents.length ).toBe( 1 );
127
-
128
- expect( sharedComponents.map( m => m.path ).sort() ).toEqual( [ sharedToolsFile, sharedUtilsFile ].sort() );
129
- expect( localComponents[0].path ).toBe( workflowStepsFile );
130
-
131
- rmSync( root, { recursive: true, force: true } );
132
- } );
133
-
134
- it( 'does not collect shared/steps/ files when searching for workflow.js', async () => {
135
- const root = join( process.cwd(), 'sdk/core/temp_test_modules', `meta-${Date.now()}-workflow-exclude` );
136
- const sharedStepsDir = join( root, 'shared', 'steps' );
137
- const workflowDir = join( root, 'workflows', 'example' );
138
- mkdirSync( sharedStepsDir, { recursive: true } );
139
- mkdirSync( workflowDir, { recursive: true } );
140
-
141
- const sharedToolsFile = join( sharedStepsDir, 'tools.js' );
142
- const workflowFile = join( workflowDir, 'workflow.js' );
90
+ it( 'supports partial matching by folder name', async () => {
91
+ const root = join( process.cwd(), 'sdk/core/temp_test_modules', `meta-${Date.now()}-foldermatch` );
92
+ const okDir = join( root, 'features', 'ok' );
93
+ const otherDir = join( root, 'features', 'other' );
94
+ mkdirSync( okDir, { recursive: true } );
95
+ mkdirSync( otherDir, { recursive: true } );
143
96
 
144
- const sharedFileContents = [
145
- 'import { METADATA_ACCESS_SYMBOL } from "#consts";',
146
- 'export const SharedStep = () => {};',
147
- 'SharedStep[METADATA_ACCESS_SYMBOL] = { kind: "step", name: "sharedStep" };'
148
- ].join( '\n' );
149
- const workflowFileContents = [
97
+ const okFile = join( okDir, 'alpha.js' );
98
+ const otherFile = join( otherDir, 'beta.js' );
99
+ const src = [
150
100
  'import { METADATA_ACCESS_SYMBOL } from "#consts";',
151
- 'export default function myWorkflow() {};',
152
- 'myWorkflow[METADATA_ACCESS_SYMBOL] = { kind: "workflow", name: "myWorkflow" };'
101
+ 'export const X = () => {};',
102
+ 'X[METADATA_ACCESS_SYMBOL] = { kind: "step", name: "x" };'
153
103
  ].join( '\n' );
154
- writeFileSync( sharedToolsFile, sharedFileContents );
155
- writeFileSync( workflowFile, workflowFileContents );
104
+ writeFileSync( okFile, src );
105
+ writeFileSync( otherFile, src );
156
106
 
107
+ // Match any JS under a folder named "ok"
108
+ const matcher = v => v.includes( `${join( 'features', 'ok' )}${sep}` );
157
109
  const collected = [];
158
- for await ( const m of importComponents( root, [ 'workflow.js' ] ) ) {
110
+ for await ( const m of importComponents( root, [ matcher ] ) ) {
159
111
  collected.push( m );
160
112
  }
161
-
162
113
  expect( collected.length ).toBe( 1 );
163
- expect( collected[0].path ).toBe( workflowFile );
164
- expect( collected[0].metadata.kind ).toBe( 'workflow' );
165
- expect( collected[0].isShared ).toBeUndefined();
114
+ expect( collected[0].path ).toBe( okFile );
166
115
 
167
116
  rmSync( root, { recursive: true, force: true } );
168
117
  } );
@@ -43,15 +43,15 @@ const getFileKindLabel = filename => {
43
43
  * - Core modules (@output.ai/core, local_core)
44
44
  * - ANY file that is NOT a component file (flexible utility imports)
45
45
  */
46
- const validateWorkflowImports = ( { specifier, filename } ) => {
46
+ const validateWorkflowImports = ( { specifier, filename, emitWarning } ) => {
47
47
  const isCore = Object.values( CoreModule ).includes( specifier );
48
48
  const fileKind = getFileKind( specifier );
49
49
  const isComponent = Object.values( ComponentFile ).includes( fileKind );
50
50
  const isNonComponentFile = fileKind === null;
51
51
 
52
52
  if ( !isCore && !isComponent && !isNonComponentFile ) {
53
- throw new Error( `Invalid dependency in workflow.js: '${specifier}'. \
54
- Only components (${Object.values( ComponentFile ) } ), @output.ai/core, or non-component files are allowed in ${filename}` );
53
+ emitWarning( new Error( `Invalid dependency in workflow.js: '${specifier}'. \
54
+ Only components (${Object.values( ComponentFile ) } ), @output.ai/core, or non-component files are allowed in ${filename}` ) );
55
55
  }
56
56
  };
57
57
 
@@ -65,25 +65,25 @@ Only components (${Object.values( ComponentFile ) } ), @output.ai/core, or non-c
65
65
  * Steps and evaluators CAN import:
66
66
  * - ANY file that is NOT a component file (flexible utility imports)
67
67
  */
68
- const validateStepEvaluatorImports = ( { specifier, filename } ) => {
68
+ const validateStepEvaluatorImports = ( { specifier, filename, emitWarning } ) => {
69
69
  const importedFileKind = getFileKind( specifier );
70
70
 
71
71
  // Activity isolation: steps/evaluators cannot import other steps, evaluators, or workflows
72
72
  if ( Object.values( ComponentFile ).includes( importedFileKind ) ) {
73
73
  const fileLabel = getFileKindLabel( filename );
74
- throw new Error( `Invalid dependency in ${fileLabel}: '${specifier}'. \
75
- Steps, evaluators or workflows are not allowed dependencies in ${filename}` );
74
+ emitWarning( new Error( `Invalid dependency in ${fileLabel}: '${specifier}'. \
75
+ Steps, evaluators or workflows are not allowed dependencies in ${filename}` ) );
76
76
  }
77
77
  };
78
78
 
79
79
  /**
80
80
  * Validate import for evaluators, steps, workflow
81
81
  */
82
- const executeImportValidations = ( { fileKind, specifier, filename } ) => {
82
+ const executeImportValidations = ( { fileKind, specifier, filename, emitWarning } ) => {
83
83
  if ( fileKind === ComponentFile.WORKFLOW ) {
84
- validateWorkflowImports( { specifier, filename } );
84
+ validateWorkflowImports( { specifier, filename, emitWarning } );
85
85
  } else if ( Object.values( ComponentFile ).includes( fileKind ) ) {
86
- validateStepEvaluatorImports( { specifier, filename } );
86
+ validateStepEvaluatorImports( { specifier, filename, emitWarning } );
87
87
  }
88
88
  };
89
89
 
@@ -127,6 +127,7 @@ const validateInstantiationLocation = ( calleeName, filename ) => {
127
127
  export default function workflowValidatorLoader( source, inputMap ) {
128
128
  this.cacheable?.( true );
129
129
  const callback = this.async?.() ?? this.callback;
130
+ const emitWarning = this.emitWarning?.bind( this ) ?? ( () => {} );
130
131
 
131
132
  try {
132
133
  const filename = this.resourcePath;
@@ -147,7 +148,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
147
148
  ImportDeclaration: path => {
148
149
  const specifier = path.node.source.value;
149
150
 
150
- executeImportValidations( { fileKind, specifier, filename } );
151
+ executeImportValidations( { fileKind, specifier, filename, emitWarning } );
151
152
 
152
153
  // Collect imported identifiers for later call checks
153
154
  const importedKind = getFileKind( specifier );
@@ -194,7 +195,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
194
195
  return;
195
196
  }
196
197
  const req = firstArg.value;
197
- executeImportValidations( { fileKind, specifier: req, filename } );
198
+ executeImportValidations( { fileKind, specifier: req, filename, emitWarning } );
198
199
 
199
200
  // Collect imported identifiers from require patterns
200
201
  if ( isStringLiteral( firstArg ) ) {
@@ -246,7 +247,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
246
247
  ].find( v => v[1] )?.[0];
247
248
 
248
249
  if ( violation ) {
249
- throw new Error( `Invalid call in ${fileLabel} fn: calling a ${violation} ('${name}') is not allowed in ${filename}` );
250
+ emitWarning( new Error( `Invalid call in ${fileLabel} fn: calling a ${violation} ('${name}') is not allowed in ${filename}` ) );
250
251
  }
251
252
  }
252
253
  }
@@ -6,11 +6,13 @@ import validatorLoader from './index.mjs';
6
6
 
7
7
  function runLoader( filename, source ) {
8
8
  return new Promise( ( resolve, reject ) => {
9
+ const warnings = [];
9
10
  const ctx = {
10
11
  resourcePath: filename,
11
12
  cacheable: () => {},
12
- async: () => ( err, code, map ) => ( err ? reject( err ) : resolve( { code, map } ) ),
13
- callback: ( err, code, map ) => ( err ? reject( err ) : resolve( { code, map } ) )
13
+ emitWarning: err => warnings.push( err ),
14
+ async: () => ( err, code, map ) => ( err ? reject( err ) : resolve( { code, map, warnings } ) ),
15
+ callback: ( err, code, map ) => ( err ? reject( err ) : resolve( { code, map, warnings } ) )
14
16
  };
15
17
  validatorLoader.call( ctx, source, null );
16
18
  } );
@@ -52,10 +54,12 @@ describe( 'workflow_validator loader', () => {
52
54
  rmSync( dir, { recursive: true, force: true } );
53
55
  } );
54
56
 
55
- it( 'steps.js: rejects importing steps/evaluators/workflow', async () => {
57
+ it( 'steps.js: warns when importing steps/evaluators/workflow', async () => {
56
58
  const dir = mkdtempSync( join( tmpdir(), 'steps-reject-' ) );
57
59
  const src = 'import { S } from "./steps.js";';
58
- await expect( runLoader( join( dir, 'steps.js' ), src ) ).rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
60
+ const result = await runLoader( join( dir, 'steps.js' ), src );
61
+ expect( result.warnings ).toHaveLength( 1 );
62
+ expect( result.warnings[0].message ).toMatch( /Invalid (import|imports|dependency) in steps\.js/ );
59
63
  rmSync( dir, { recursive: true, force: true } );
60
64
  } );
61
65
 
@@ -66,14 +70,16 @@ describe( 'workflow_validator loader', () => {
66
70
  rmSync( dir, { recursive: true, force: true } );
67
71
  } );
68
72
 
69
- it( 'evaluators.js: rejects importing evaluators/steps/workflow', async () => {
73
+ it( 'evaluators.js: warns when importing evaluators/steps/workflow', async () => {
70
74
  const dir = mkdtempSync( join( tmpdir(), 'evals-reject-' ) );
71
75
  const src = 'import { E } from "./evaluators.js";';
72
- await expect( runLoader( join( dir, 'evaluators.js' ), src ) ).rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
76
+ const result = await runLoader( join( dir, 'evaluators.js' ), src );
77
+ expect( result.warnings ).toHaveLength( 1 );
78
+ expect( result.warnings[0].message ).toMatch( /Invalid (import|imports|dependency) in evaluators\.js/ );
73
79
  rmSync( dir, { recursive: true, force: true } );
74
80
  } );
75
81
 
76
- it( 'steps.js: rejects calling another step inside fn', async () => {
82
+ it( 'steps.js: warns when calling another step inside fn', async () => {
77
83
  const dir = mkdtempSync( join( tmpdir(), 'steps-call-reject-' ) );
78
84
  // Can only test same-type components since cross-type declarations are now blocked by instantiation validation
79
85
  const src = [
@@ -81,11 +87,13 @@ describe( 'workflow_validator loader', () => {
81
87
  'const B = step({ name: "b" });',
82
88
  'const obj = { fn: function() { B(); } };'
83
89
  ].join( '\n' );
84
- await expect( runLoader( join( dir, 'steps.js' ), src ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
90
+ const result = await runLoader( join( dir, 'steps.js' ), src );
91
+ expect( result.warnings ).toHaveLength( 1 );
92
+ expect( result.warnings[0].message ).toMatch( /Invalid call in .*\.js fn/ );
85
93
  rmSync( dir, { recursive: true, force: true } );
86
94
  } );
87
95
 
88
- it( 'evaluators.js: rejects calling another evaluator inside fn', async () => {
96
+ it( 'evaluators.js: warns when calling another evaluator inside fn', async () => {
89
97
  const dir = mkdtempSync( join( tmpdir(), 'evals-call-reject-' ) );
90
98
  // Can only test same-type components since cross-type declarations are now blocked by instantiation validation
91
99
  const src = [
@@ -93,7 +101,9 @@ describe( 'workflow_validator loader', () => {
93
101
  'const E2 = evaluator({ name: "e2" });',
94
102
  'const obj = { fn: function() { E2(); } };'
95
103
  ].join( '\n' );
96
- await expect( runLoader( join( dir, 'evaluators.js' ), src ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
104
+ const result = await runLoader( join( dir, 'evaluators.js' ), src );
105
+ expect( result.warnings ).toHaveLength( 1 );
106
+ expect( result.warnings[0].message ).toMatch( /Invalid call in .*\.js fn/ );
97
107
  rmSync( dir, { recursive: true, force: true } );
98
108
  } );
99
109
 
@@ -130,21 +140,25 @@ describe( 'workflow_validator loader', () => {
130
140
  rmSync( dir, { recursive: true, force: true } );
131
141
  } );
132
142
 
133
- it( 'steps.js: rejects importing evaluators/workflow variants', async () => {
143
+ it( 'steps.js: warns when importing evaluators/workflow variants', async () => {
134
144
  const dir = mkdtempSync( join( tmpdir(), 'steps-reject2-' ) );
135
- await expect( runLoader( join( dir, 'steps.js' ), 'import { E } from "./evaluators.js";' ) )
136
- .rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
137
- await expect( runLoader( join( dir, 'steps.js' ), 'import WF from "./workflow.js";' ) )
138
- .rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
145
+ const result1 = await runLoader( join( dir, 'steps.js' ), 'import { E } from "./evaluators.js";' );
146
+ expect( result1.warnings ).toHaveLength( 1 );
147
+ expect( result1.warnings[0].message ).toMatch( /Invalid (import|imports|dependency) in steps\.js/ );
148
+ const result2 = await runLoader( join( dir, 'steps.js' ), 'import WF from "./workflow.js";' );
149
+ expect( result2.warnings ).toHaveLength( 1 );
150
+ expect( result2.warnings[0].message ).toMatch( /Invalid (import|imports|dependency) in steps\.js/ );
139
151
  rmSync( dir, { recursive: true, force: true } );
140
152
  } );
141
153
 
142
- it( 'evaluators.js: rejects importing steps/workflow variants', async () => {
154
+ it( 'evaluators.js: warns when importing steps/workflow variants', async () => {
143
155
  const dir = mkdtempSync( join( tmpdir(), 'evals-reject2-' ) );
144
- await expect( runLoader( join( dir, 'evaluators.js' ), 'import { S } from "./steps.js";' ) )
145
- .rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
146
- await expect( runLoader( join( dir, 'evaluators.js' ), 'import WF from "./workflow.js";' ) )
147
- .rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
156
+ const result1 = await runLoader( join( dir, 'evaluators.js' ), 'import { S } from "./steps.js";' );
157
+ expect( result1.warnings ).toHaveLength( 1 );
158
+ expect( result1.warnings[0].message ).toMatch( /Invalid (import|imports|dependency) in evaluators\.js/ );
159
+ const result2 = await runLoader( join( dir, 'evaluators.js' ), 'import WF from "./workflow.js";' );
160
+ expect( result2.warnings ).toHaveLength( 1 );
161
+ expect( result2.warnings[0].message ).toMatch( /Invalid (import|imports|dependency) in evaluators\.js/ );
148
162
  rmSync( dir, { recursive: true, force: true } );
149
163
  } );
150
164
 
@@ -176,27 +190,34 @@ describe( 'workflow_validator loader', () => {
176
190
  rmSync( dir, { recursive: true, force: true } );
177
191
  } );
178
192
 
179
- it( 'steps.js: rejects require of steps/evaluators/workflow; allows other require', async () => {
193
+ it( 'steps.js: warns on require of steps/evaluators/workflow; allows other require', async () => {
180
194
  const dir = mkdtempSync( join( tmpdir(), 'steps-require-' ) );
181
- await expect( runLoader( join( dir, 'steps.js' ), 'const { S } = require("./steps.js");' ) )
182
- .rejects.toThrow( /Invalid (require|dependency) in steps\.js/ );
183
- await expect( runLoader( join( dir, 'steps.js' ), 'const { E } = require("./evaluators.js");' ) )
184
- .rejects.toThrow( /Invalid (require|dependency) in steps\.js/ );
185
- await expect( runLoader( join( dir, 'steps.js' ), 'const W = require("./workflow.js");' ) )
186
- .rejects.toThrow( /Invalid (require|dependency) in steps\.js/ );
195
+ const result1 = await runLoader( join( dir, 'steps.js' ), 'const { S } = require("./steps.js");' );
196
+ expect( result1.warnings ).toHaveLength( 1 );
197
+ expect( result1.warnings[0].message ).toMatch( /Invalid (require|dependency) in steps\.js/ );
198
+ const result2 = await runLoader( join( dir, 'steps.js' ), 'const { E } = require("./evaluators.js");' );
199
+ expect( result2.warnings ).toHaveLength( 1 );
200
+ expect( result2.warnings[0].message ).toMatch( /Invalid (require|dependency) in steps\.js/ );
201
+ const result3 = await runLoader( join( dir, 'steps.js' ), 'const W = require("./workflow.js");' );
202
+ expect( result3.warnings ).toHaveLength( 1 );
203
+ expect( result3.warnings[0].message ).toMatch( /Invalid (require|dependency) in steps\.js/ );
187
204
  const ok = 'const util = require("./util.js");';
188
- await expect( runLoader( join( dir, 'steps.js' ), ok ) ).resolves.toBeTruthy();
205
+ const resultOk = await runLoader( join( dir, 'steps.js' ), ok );
206
+ expect( resultOk.warnings ).toHaveLength( 0 );
189
207
  rmSync( dir, { recursive: true, force: true } );
190
208
  } );
191
209
 
192
- it( 'evaluators.js: rejects require of steps/workflow; allows other require', async () => {
210
+ it( 'evaluators.js: warns on require of steps/workflow; allows other require', async () => {
193
211
  const dir = mkdtempSync( join( tmpdir(), 'evals-require-' ) );
194
- await expect( runLoader( join( dir, 'evaluators.js' ), 'const { S } = require("./steps.js");' ) )
195
- .rejects.toThrow( /Invalid (require|dependency) in evaluators\.js/ );
196
- await expect( runLoader( join( dir, 'evaluators.js' ), 'const W = require("./workflow.js");' ) )
197
- .rejects.toThrow( /Invalid (require|dependency) in evaluators\.js/ );
212
+ const result1 = await runLoader( join( dir, 'evaluators.js' ), 'const { S } = require("./steps.js");' );
213
+ expect( result1.warnings ).toHaveLength( 1 );
214
+ expect( result1.warnings[0].message ).toMatch( /Invalid (require|dependency) in evaluators\.js/ );
215
+ const result2 = await runLoader( join( dir, 'evaluators.js' ), 'const W = require("./workflow.js");' );
216
+ expect( result2.warnings ).toHaveLength( 1 );
217
+ expect( result2.warnings[0].message ).toMatch( /Invalid (require|dependency) in evaluators\.js/ );
198
218
  const ok = 'const util = require("./util.js");';
199
- await expect( runLoader( join( dir, 'evaluators.js' ), ok ) ).resolves.toBeTruthy();
219
+ const resultOk = await runLoader( join( dir, 'evaluators.js' ), ok );
220
+ expect( resultOk.warnings ).toHaveLength( 0 );
200
221
  rmSync( dir, { recursive: true, force: true } );
201
222
  } );
202
223
 
@@ -317,47 +338,51 @@ describe( 'workflow_validator loader', () => {
317
338
  } );
318
339
 
319
340
  describe( 'activity isolation - shared imports', () => {
320
- it( 'steps.ts: rejects imports from ../../shared/steps/common.js (activity isolation)', async () => {
341
+ it( 'steps.ts: warns on imports from ../../shared/steps/common.js (activity isolation)', async () => {
321
342
  const dir = mkdtempSync( join( tmpdir(), 'steps-shared-steps-reject-' ) );
322
343
  mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
323
344
  mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
324
345
  writeFileSync( join( dir, 'shared', 'steps', 'common.js' ), 'export const commonStep = step({ name: "common" });\n' );
325
346
  const src = 'import { commonStep } from "../../shared/steps/common.js";';
326
- await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'steps.js' ), src ) )
327
- .rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
347
+ const result = await runLoader( join( dir, 'workflows', 'my_workflow', 'steps.js' ), src );
348
+ expect( result.warnings ).toHaveLength( 1 );
349
+ expect( result.warnings[0].message ).toMatch( /Invalid (import|imports|dependency) in steps\.js/ );
328
350
  rmSync( dir, { recursive: true, force: true } );
329
351
  } );
330
352
 
331
- it( 'steps.ts: rejects imports from ../../shared/evaluators/quality.js (activity isolation)', async () => {
353
+ it( 'steps.ts: warns on imports from ../../shared/evaluators/quality.js (activity isolation)', async () => {
332
354
  const dir = mkdtempSync( join( tmpdir(), 'steps-shared-evals-reject-' ) );
333
355
  mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
334
356
  mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
335
357
  writeFileSync( join( dir, 'shared', 'evaluators', 'quality.js' ), 'export const qualityEval = evaluator({ name: "quality" });\n' );
336
358
  const src = 'import { qualityEval } from "../../shared/evaluators/quality.js";';
337
- await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'steps.js' ), src ) )
338
- .rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
359
+ const result = await runLoader( join( dir, 'workflows', 'my_workflow', 'steps.js' ), src );
360
+ expect( result.warnings ).toHaveLength( 1 );
361
+ expect( result.warnings[0].message ).toMatch( /Invalid (import|imports|dependency) in steps\.js/ );
339
362
  rmSync( dir, { recursive: true, force: true } );
340
363
  } );
341
364
 
342
- it( 'evaluators.ts: rejects imports from ../../shared/steps/common.js (activity isolation)', async () => {
365
+ it( 'evaluators.ts: warns on imports from ../../shared/steps/common.js (activity isolation)', async () => {
343
366
  const dir = mkdtempSync( join( tmpdir(), 'evals-shared-steps-reject-' ) );
344
367
  mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
345
368
  mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
346
369
  writeFileSync( join( dir, 'shared', 'steps', 'common.js' ), 'export const commonStep = step({ name: "common" });\n' );
347
370
  const src = 'import { commonStep } from "../../shared/steps/common.js";';
348
- await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'evaluators.js' ), src ) )
349
- .rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
371
+ const result = await runLoader( join( dir, 'workflows', 'my_workflow', 'evaluators.js' ), src );
372
+ expect( result.warnings ).toHaveLength( 1 );
373
+ expect( result.warnings[0].message ).toMatch( /Invalid (import|imports|dependency) in evaluators\.js/ );
350
374
  rmSync( dir, { recursive: true, force: true } );
351
375
  } );
352
376
 
353
- it( 'evaluators.ts: rejects imports from ../../shared/evaluators/other.js (activity isolation)', async () => {
377
+ it( 'evaluators.ts: warns on imports from ../../shared/evaluators/other.js (activity isolation)', async () => {
354
378
  const dir = mkdtempSync( join( tmpdir(), 'evals-shared-evals-reject-' ) );
355
379
  mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
356
380
  mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
357
381
  writeFileSync( join( dir, 'shared', 'evaluators', 'other.js' ), 'export const otherEval = evaluator({ name: "other" });\n' );
358
382
  const src = 'import { otherEval } from "../../shared/evaluators/other.js";';
359
- await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'evaluators.js' ), src ) )
360
- .rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
383
+ const result = await runLoader( join( dir, 'workflows', 'my_workflow', 'evaluators.js' ), src );
384
+ expect( result.warnings ).toHaveLength( 1 );
385
+ expect( result.warnings[0].message ).toMatch( /Invalid (import|imports|dependency) in evaluators\.js/ );
361
386
  rmSync( dir, { recursive: true, force: true } );
362
387
  } );
363
388
  } );