@outputai/core 0.7.1-next.de30052.0 → 0.8.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/bin/worker.sh +6 -0
- package/package.json +1 -1
- package/src/consts.js +0 -4
- package/src/errors.js +6 -2
- package/src/interface/evaluator.js +7 -20
- package/src/interface/evaluator.spec.js +117 -1
- package/src/interface/step.js +8 -9
- package/src/interface/step.spec.js +124 -0
- package/src/interface/validations/index.js +108 -0
- package/src/interface/validations/index.spec.js +182 -0
- package/src/interface/validations/schemas.js +113 -0
- package/src/interface/validations/schemas.spec.js +209 -0
- package/src/interface/webhook.js +1 -1
- package/src/interface/webhook.spec.js +1 -1
- package/src/interface/workflow.d.ts +10 -9
- package/src/interface/workflow.js +76 -164
- package/src/interface/workflow.spec.js +637 -521
- package/src/interface/workflow_activity_options.js +16 -0
- package/src/interface/workflow_utils.js +1 -1
- package/src/interface/zod_integration.spec.js +2 -2
- package/src/internal_utils/aggregations.js +0 -10
- package/src/internal_utils/aggregations.spec.js +1 -48
- package/src/internal_utils/errors.js +14 -8
- package/src/internal_utils/errors.spec.js +73 -27
- package/src/utils/index.d.ts +19 -0
- package/src/utils/utils.js +53 -0
- package/src/utils/utils.spec.js +105 -1
- package/src/worker/bundle.js +26 -0
- package/src/worker/bundle.spec.js +53 -0
- package/src/worker/bundler_options.js +1 -1
- package/src/worker/bundler_options.spec.js +1 -1
- package/src/worker/catalog_workflow/catalog_job.js +148 -0
- package/src/worker/catalog_workflow/catalog_job.spec.js +232 -0
- package/src/worker/check.js +24 -0
- package/src/worker/connection_monitor.js +112 -0
- package/src/worker/connection_monitor.spec.js +199 -0
- package/src/worker/index.js +146 -41
- package/src/worker/index.spec.js +281 -109
- package/src/worker/interceptors/activity.js +7 -24
- package/src/worker/interceptors/activity.spec.js +97 -66
- package/src/worker/interceptors/index.js +4 -7
- package/src/worker/interceptors/modules.js +15 -0
- package/src/worker/interceptors/workflow.js +6 -8
- package/src/worker/interceptors/workflow.spec.js +49 -42
- package/src/worker/interruption.js +33 -0
- package/src/worker/interruption.spec.js +98 -0
- package/src/worker/loader/activities.js +75 -0
- package/src/worker/loader/activities.spec.js +213 -0
- package/src/worker/loader/hooks.js +28 -0
- package/src/worker/loader/hooks.spec.js +64 -0
- package/src/worker/loader/matchers.js +46 -0
- package/src/worker/loader/matchers.spec.js +140 -0
- package/src/worker/{loader_tools.js → loader/tools.js} +19 -67
- package/src/worker/{loader_tools.spec.js → loader/tools.spec.js} +53 -85
- package/src/worker/loader/workflows.js +82 -0
- package/src/worker/loader/workflows.spec.js +256 -0
- package/src/worker/{setup_telemetry.js → telemetry.js} +9 -4
- package/src/worker/{setup_telemetry.spec.js → telemetry.spec.js} +3 -3
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +5 -109
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +31 -103
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +5 -6
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +11 -83
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +8 -11
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +9 -9
- package/src/interface/validations/runtime.js +0 -20
- package/src/interface/validations/runtime.spec.js +0 -29
- package/src/interface/validations/schema_utils.js +0 -8
- package/src/interface/validations/schema_utils.spec.js +0 -67
- package/src/interface/validations/static.js +0 -137
- package/src/interface/validations/static.spec.js +0 -397
- package/src/interface/workflow.replay_compatibility.spec.js +0 -254
- package/src/worker/loader.js +0 -202
- package/src/worker/loader.spec.js +0 -498
- package/src/worker/shutdown.js +0 -26
- package/src/worker/shutdown.spec.js +0 -82
- package/src/worker/start_catalog.js +0 -96
- package/src/worker/start_catalog.spec.js +0 -179
|
@@ -1,26 +1,47 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, sep } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const { workflowFileMatcherMock, sharedStepsDirMatcherMock, sharedEvaluatorsDirMatcherMock } = vi.hoisted( () => ( {
|
|
8
|
+
workflowFileMatcherMock: vi.fn(),
|
|
9
|
+
sharedStepsDirMatcherMock: vi.fn(),
|
|
10
|
+
sharedEvaluatorsDirMatcherMock: vi.fn()
|
|
11
|
+
} ) );
|
|
12
|
+
|
|
13
|
+
vi.mock( './matchers.js', () => ( {
|
|
14
|
+
staticMatchers: {
|
|
15
|
+
workflowFile: workflowFileMatcherMock,
|
|
16
|
+
sharedStepsDir: sharedStepsDirMatcherMock,
|
|
17
|
+
sharedEvaluatorsDir: sharedEvaluatorsDirMatcherMock
|
|
18
|
+
}
|
|
19
|
+
} ) );
|
|
20
|
+
|
|
6
21
|
import {
|
|
7
|
-
activityMatchersBuilder,
|
|
8
22
|
matchFiles,
|
|
9
23
|
findWorkflowsInNodeModules,
|
|
10
24
|
findWorkflowsInPackages,
|
|
11
25
|
findSharedActivitiesFromWorkflows,
|
|
12
26
|
importComponents,
|
|
13
27
|
findPackageRoot,
|
|
28
|
+
hashSourceCode,
|
|
14
29
|
isPackageRoot,
|
|
15
30
|
isPathDescendentFromNodeModules,
|
|
16
31
|
resolveNodeModulesPath,
|
|
17
32
|
resolveSymlink,
|
|
18
|
-
staticMatchers,
|
|
19
33
|
packageExposesWorkflows
|
|
20
|
-
} from './
|
|
34
|
+
} from './tools.js';
|
|
21
35
|
|
|
22
36
|
const TEMP_BASE = join( process.cwd(), 'sdk/core/temp_test_modules' );
|
|
23
37
|
|
|
38
|
+
beforeEach( () => {
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
workflowFileMatcherMock.mockImplementation( path => path.endsWith( `${sep}workflow.js` ) );
|
|
41
|
+
sharedStepsDirMatcherMock.mockImplementation( path => path.includes( `${sep}shared${sep}steps${sep}` ) && path.endsWith( '.js' ) );
|
|
42
|
+
sharedEvaluatorsDirMatcherMock.mockImplementation( path => path.includes( `${sep}shared${sep}evaluators${sep}` ) && path.endsWith( '.js' ) );
|
|
43
|
+
} );
|
|
44
|
+
|
|
24
45
|
afterEach( () => {
|
|
25
46
|
rmSync( TEMP_BASE, { recursive: true, force: true } );
|
|
26
47
|
} );
|
|
@@ -158,36 +179,6 @@ describe( 'node_modules package resource helpers', () => {
|
|
|
158
179
|
} );
|
|
159
180
|
} );
|
|
160
181
|
|
|
161
|
-
describe( 'activityMatchersBuilder', () => {
|
|
162
|
-
const base = `${sep}app${sep}proj`;
|
|
163
|
-
|
|
164
|
-
it( 'stepsFile matches only steps.js at base', () => {
|
|
165
|
-
const m = activityMatchersBuilder( base );
|
|
166
|
-
expect( m.stepsFile( `${base}${sep}steps.js` ) ).toBe( true );
|
|
167
|
-
expect( m.stepsFile( `${base}${sep}nested${sep}steps.js` ) ).toBe( false );
|
|
168
|
-
} );
|
|
169
|
-
|
|
170
|
-
it( 'evaluatorsFile matches only evaluators.js at base', () => {
|
|
171
|
-
const m = activityMatchersBuilder( base );
|
|
172
|
-
expect( m.evaluatorsFile( `${base}${sep}evaluators.js` ) ).toBe( true );
|
|
173
|
-
expect( m.evaluatorsFile( `${base}${sep}sub${sep}evaluators.js` ) ).toBe( false );
|
|
174
|
-
} );
|
|
175
|
-
|
|
176
|
-
it( 'stepsDir matches js under steps/', () => {
|
|
177
|
-
const m = activityMatchersBuilder( base );
|
|
178
|
-
expect( m.stepsDir( `${base}${sep}steps${sep}a.js` ) ).toBe( true );
|
|
179
|
-
expect( m.stepsDir( `${base}${sep}steps${sep}sub${sep}b.js` ) ).toBe( true );
|
|
180
|
-
expect( m.stepsDir( `${base}${sep}other${sep}a.js` ) ).toBe( false );
|
|
181
|
-
} );
|
|
182
|
-
|
|
183
|
-
it( 'evaluatorsDir matches js under evaluators/', () => {
|
|
184
|
-
const m = activityMatchersBuilder( base );
|
|
185
|
-
expect( m.evaluatorsDir( `${base}${sep}evaluators${sep}x.js` ) ).toBe( true );
|
|
186
|
-
expect( m.evaluatorsDir( `${base}${sep}evaluators${sep}y${sep}z.js` ) ).toBe( true );
|
|
187
|
-
expect( m.evaluatorsDir( `${base}${sep}steps${sep}x.js` ) ).toBe( false );
|
|
188
|
-
} );
|
|
189
|
-
} );
|
|
190
|
-
|
|
191
182
|
describe( 'matchFiles', () => {
|
|
192
183
|
it( 'collects files matching matchers', () => {
|
|
193
184
|
const root = join( TEMP_BASE, `fbnr-files-${Date.now()}` );
|
|
@@ -568,60 +559,37 @@ describe( 'findSharedActivitiesFromWorkflows', () => {
|
|
|
568
559
|
} );
|
|
569
560
|
} );
|
|
570
561
|
|
|
571
|
-
describe( '
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
} );
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
} );
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
describe( 'sharedStepsDir', () => {
|
|
593
|
-
it( 'matches .js files inside shared/steps/', () => {
|
|
594
|
-
expect( staticMatchers.sharedStepsDir( `${sep}app${sep}dist${sep}shared${sep}steps${sep}tools.js` ) ).toBe( true );
|
|
595
|
-
} );
|
|
596
|
-
|
|
597
|
-
it( 'matches .js files in nested subdirectories of shared/steps/', () => {
|
|
598
|
-
expect( staticMatchers.sharedStepsDir( `${sep}app${sep}dist${sep}shared${sep}steps${sep}utils${sep}helper.js` ) ).toBe( true );
|
|
599
|
-
} );
|
|
600
|
-
|
|
601
|
-
it( 'rejects .ts files inside shared/steps/', () => {
|
|
602
|
-
expect( staticMatchers.sharedStepsDir( `${sep}app${sep}src${sep}shared${sep}steps${sep}tools.ts` ) ).toBe( false );
|
|
603
|
-
} );
|
|
562
|
+
describe( 'hashSourceCode', () => {
|
|
563
|
+
const writeSource = root => {
|
|
564
|
+
mkdirSync( join( root, 'src' ), { recursive: true } );
|
|
565
|
+
writeFileSync( join( root, 'package.json' ), JSON.stringify( { name: 'proj' } ) );
|
|
566
|
+
writeFileSync( join( root, 'src', 'workflow.js' ), 'export default {};\n' );
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
it( 'ignores excluded folders so accumulated run logs do not change the hash', async () => {
|
|
570
|
+
const baseline = join( TEMP_BASE, `hash-baseline-${Date.now()}` );
|
|
571
|
+
const withCruft = join( TEMP_BASE, `hash-cruft-${Date.now()}` );
|
|
572
|
+
writeSource( baseline );
|
|
573
|
+
writeSource( withCruft );
|
|
574
|
+
|
|
575
|
+
// The cruft tree is identical source plus large excluded artifacts that
|
|
576
|
+
// boot must not walk: local trace dumps under logs/ and build output under dist/.
|
|
577
|
+
for ( const excluded of [ 'logs', 'logs/runs', 'dist', 'node_modules' ] ) {
|
|
578
|
+
const dir = join( withCruft, excluded );
|
|
579
|
+
mkdirSync( dir, { recursive: true } );
|
|
580
|
+
writeFileSync( join( dir, 'dump.json' ), JSON.stringify( { blob: 'x'.repeat( 50_000 ) } ) );
|
|
581
|
+
}
|
|
604
582
|
|
|
605
|
-
|
|
606
|
-
expect( staticMatchers.sharedStepsDir( `${sep}app${sep}dist${sep}shared${sep}steps${sep}readme.md` ) ).toBe( false );
|
|
607
|
-
} );
|
|
583
|
+
expect( await hashSourceCode( withCruft ) ).toBe( await hashSourceCode( baseline ) );
|
|
608
584
|
} );
|
|
609
585
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}dist${sep}shared${sep}evaluators${sep}utils${sep}helper.js` ) ).toBe( true );
|
|
617
|
-
} );
|
|
618
|
-
|
|
619
|
-
it( 'rejects .ts files inside shared/evaluators/', () => {
|
|
620
|
-
expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}src${sep}shared${sep}evaluators${sep}quality.ts` ) ).toBe( false );
|
|
621
|
-
} );
|
|
586
|
+
it( 'changes the hash when actual source changes', async () => {
|
|
587
|
+
const before = join( TEMP_BASE, `hash-src-before-${Date.now()}` );
|
|
588
|
+
const after = join( TEMP_BASE, `hash-src-after-${Date.now()}` );
|
|
589
|
+
writeSource( before );
|
|
590
|
+
writeSource( after );
|
|
591
|
+
writeFileSync( join( after, 'src', 'workflow.js' ), 'export default { changed: true };\n' );
|
|
622
592
|
|
|
623
|
-
|
|
624
|
-
expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}dist${sep}shared${sep}evaluators${sep}readme.md` ) ).toBe( false );
|
|
625
|
-
} );
|
|
593
|
+
expect( await hashSourceCode( after ) ).not.toBe( await hashSourceCode( before ) );
|
|
626
594
|
} );
|
|
627
595
|
} );
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { EOL } from 'node:os';
|
|
2
|
+
import { writeFileInTempDir, findWorkflowsInNodeModules, importComponents, matchFiles } from './tools.js';
|
|
3
|
+
import { staticMatchers } from './matchers.js';
|
|
4
|
+
import { WORKFLOWS_INDEX_FILENAME, WORKFLOW_CATALOG } from '#consts';
|
|
5
|
+
import { createChildLogger } from '#logger';
|
|
6
|
+
import { ValidationError } from '#errors';
|
|
7
|
+
|
|
8
|
+
const log = createChildLogger( 'Workflow Loader' );
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a temporary index file importing all workflows for Temporal.
|
|
12
|
+
* @param {object[]} workflows
|
|
13
|
+
* @returns {string} Filename
|
|
14
|
+
*/
|
|
15
|
+
const createWorkflowsEntrypoint = workflows => {
|
|
16
|
+
// default system catalog workflow
|
|
17
|
+
const catalog = { name: WORKFLOW_CATALOG, path: import.meta.resolve( '../catalog_workflow/workflow.js' ) };
|
|
18
|
+
const aliasExports = workflows.flatMap( ( { aliases = [], path } ) =>
|
|
19
|
+
aliases.map( alias => ( { name: alias, path } ) )
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const content = [ ...workflows, ...aliasExports, catalog ]
|
|
23
|
+
.map( ( { name, path } ) => `export { default as ${name} } from '${path}';` ).join( EOL );
|
|
24
|
+
|
|
25
|
+
return writeFileInTempDir( content, WORKFLOWS_INDEX_FILENAME );
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef Workflow
|
|
30
|
+
* @property {string} path
|
|
31
|
+
* @property {boolean} external
|
|
32
|
+
* @property {string} name
|
|
33
|
+
* @property {string[]} aliases
|
|
34
|
+
* @property {object} inputSchema
|
|
35
|
+
* @property {object} outputSchema
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* @typedef LoadWorkflowsResult
|
|
39
|
+
* @property {Workflow[]} workflows - Loaded workflows
|
|
40
|
+
* @property {string} entrypoint - Index file loading all workflows
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Scan and find workflow.js files and import them.
|
|
44
|
+
* Look into local and external (node_modules) folders.
|
|
45
|
+
* @param {string} rootDir
|
|
46
|
+
* @returns {LoadWorkflowsResult}
|
|
47
|
+
*/
|
|
48
|
+
export async function loadWorkflows( rootDir ) {
|
|
49
|
+
const workflowNames = new Set();
|
|
50
|
+
const workflows = [];
|
|
51
|
+
const localWorkflows = matchFiles( rootDir, [ staticMatchers.workflowFile ] );
|
|
52
|
+
const externalWorkflows = findWorkflowsInNodeModules( rootDir );
|
|
53
|
+
for await ( const { metadata, path } of importComponents( [ ...localWorkflows, ...externalWorkflows ] ) ) {
|
|
54
|
+
const external = externalWorkflows.some( a => a.path === path );
|
|
55
|
+
if ( staticMatchers.workflowPathHasShared( path ) ) {
|
|
56
|
+
throw new ValidationError( 'Workflow directory can\'t be named "shared"' );
|
|
57
|
+
}
|
|
58
|
+
const { name, aliases } = metadata;
|
|
59
|
+
if ( workflowNames.has( name ) ) {
|
|
60
|
+
throw new ValidationError( `Workflow name "${name}" conflicts with another workflow or alias. \
|
|
61
|
+
Workflow names and aliases must be unique.` );
|
|
62
|
+
}
|
|
63
|
+
if ( WORKFLOW_CATALOG === name ) {
|
|
64
|
+
throw new ValidationError( `Workflow name "${name}" is reserved for the internal catalog workflow.` );
|
|
65
|
+
}
|
|
66
|
+
workflowNames.add( name );
|
|
67
|
+
for ( const alias of aliases ?? [] ) {
|
|
68
|
+
if ( workflowNames.has( alias ) ) {
|
|
69
|
+
throw new ValidationError( `Workflow "${name}" alias "${alias}" conflicts with another workflow or alias. \
|
|
70
|
+
Workflow names and aliases must be unique.` );
|
|
71
|
+
}
|
|
72
|
+
if ( WORKFLOW_CATALOG === alias ) {
|
|
73
|
+
throw new ValidationError( `Workflow "${name}" alias "${alias}" is reserved for the internal catalog workflow.` );
|
|
74
|
+
}
|
|
75
|
+
workflowNames.add( alias );
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
log.info( name, { path, aliases, ...( external && { external } ) } );
|
|
79
|
+
workflows.push( { ...metadata, path, external } );
|
|
80
|
+
}
|
|
81
|
+
return { workflows, entrypoint: createWorkflowsEntrypoint( workflows ) };
|
|
82
|
+
};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock( '#consts', () => ( {
|
|
4
|
+
ACTIVITY_SEND_HTTP_REQUEST: '__internal#sendHttpRequest',
|
|
5
|
+
ACTIVITY_GET_TRACE_DESTINATIONS: '__internal#getTraceDestinations',
|
|
6
|
+
WORKFLOWS_INDEX_FILENAME: '__workflows_entrypoint.js',
|
|
7
|
+
WORKFLOW_CATALOG: 'catalog',
|
|
8
|
+
ACTIVITY_OPTIONS_FILENAME: '__activity_options.js',
|
|
9
|
+
SHARED_STEP_PREFIX: '$shared'
|
|
10
|
+
} ) );
|
|
11
|
+
|
|
12
|
+
const { importComponentsMock, findWorkflowsInNodeModulesMock, matchFilesMock, writeFileInTempDirMock } = vi.hoisted( () => ( {
|
|
13
|
+
importComponentsMock: vi.fn(),
|
|
14
|
+
findWorkflowsInNodeModulesMock: vi.fn(),
|
|
15
|
+
matchFilesMock: vi.fn(),
|
|
16
|
+
writeFileInTempDirMock: vi.fn()
|
|
17
|
+
} ) );
|
|
18
|
+
|
|
19
|
+
const { workflowFileMatcherMock, workflowPathHasSharedMock } = vi.hoisted( () => ( {
|
|
20
|
+
workflowFileMatcherMock: vi.fn(),
|
|
21
|
+
workflowPathHasSharedMock: vi.fn()
|
|
22
|
+
} ) );
|
|
23
|
+
|
|
24
|
+
vi.mock( './tools.js', () => ( {
|
|
25
|
+
importComponents: importComponentsMock,
|
|
26
|
+
findWorkflowsInNodeModules: findWorkflowsInNodeModulesMock,
|
|
27
|
+
matchFiles: matchFilesMock,
|
|
28
|
+
writeFileInTempDir: writeFileInTempDirMock
|
|
29
|
+
} ) );
|
|
30
|
+
|
|
31
|
+
vi.mock( './matchers.js', () => ( {
|
|
32
|
+
staticMatchers: {
|
|
33
|
+
workflowFile: workflowFileMatcherMock,
|
|
34
|
+
workflowPathHasShared: workflowPathHasSharedMock
|
|
35
|
+
}
|
|
36
|
+
} ) );
|
|
37
|
+
|
|
38
|
+
describe( 'loadWorkflows', () => {
|
|
39
|
+
beforeEach( () => {
|
|
40
|
+
vi.clearAllMocks();
|
|
41
|
+
importComponentsMock.mockReset();
|
|
42
|
+
importComponentsMock.mockImplementation( async function *() {} );
|
|
43
|
+
findWorkflowsInNodeModulesMock.mockReset();
|
|
44
|
+
findWorkflowsInNodeModulesMock.mockReturnValue( [] );
|
|
45
|
+
matchFilesMock.mockReset();
|
|
46
|
+
matchFilesMock.mockReturnValue( [] );
|
|
47
|
+
writeFileInTempDirMock.mockReset();
|
|
48
|
+
writeFileInTempDirMock.mockReturnValue( '/tmp/__workflows_entrypoint.js' );
|
|
49
|
+
workflowPathHasSharedMock.mockReset();
|
|
50
|
+
workflowPathHasSharedMock.mockReturnValue( false );
|
|
51
|
+
} );
|
|
52
|
+
|
|
53
|
+
it( 'returns local workflows from importComponents with metadata spread onto each entry', async () => {
|
|
54
|
+
const { loadWorkflows } = await import( './workflows.js' );
|
|
55
|
+
const localFiles = [ { path: '/b/workflow.js', url: 'file:///b/workflow.js' } ];
|
|
56
|
+
matchFilesMock.mockReturnValueOnce( localFiles );
|
|
57
|
+
|
|
58
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
59
|
+
yield { metadata: { name: 'Flow1', description: 'd' }, path: '/b/workflow.js' };
|
|
60
|
+
} );
|
|
61
|
+
|
|
62
|
+
const { workflows, entrypoint } = await loadWorkflows( '/root' );
|
|
63
|
+
expect( workflows ).toEqual( [ { name: 'Flow1', description: 'd', path: '/b/workflow.js', external: false } ] );
|
|
64
|
+
expect( entrypoint ).toBe( '/tmp/__workflows_entrypoint.js' );
|
|
65
|
+
expect( importComponentsMock ).toHaveBeenNthCalledWith( 1, localFiles );
|
|
66
|
+
expect( findWorkflowsInNodeModulesMock ).toHaveBeenCalledOnce();
|
|
67
|
+
expect( findWorkflowsInNodeModulesMock ).toHaveBeenCalledWith( '/root' );
|
|
68
|
+
} );
|
|
69
|
+
|
|
70
|
+
it( 'calls matchFiles with rootDir and workflowFile matcher', async () => {
|
|
71
|
+
const { loadWorkflows } = await import( './workflows.js' );
|
|
72
|
+
const localFiles = [ { path: '/my/app/workflow.js', url: 'file:///my/app/workflow.js' } ];
|
|
73
|
+
const externalFiles = [ { path: '/my/app/node_modules/pkg/workflow.js', url: 'file:///my/app/node_modules/pkg/workflow.js' } ];
|
|
74
|
+
matchFilesMock.mockReturnValueOnce( localFiles );
|
|
75
|
+
findWorkflowsInNodeModulesMock.mockReturnValue( externalFiles );
|
|
76
|
+
|
|
77
|
+
await loadWorkflows( '/my/app' );
|
|
78
|
+
|
|
79
|
+
expect( matchFilesMock ).toHaveBeenCalledOnce();
|
|
80
|
+
expect( matchFilesMock ).toHaveBeenCalledWith( '/my/app', [ workflowFileMatcherMock ] );
|
|
81
|
+
expect( importComponentsMock ).toHaveBeenCalledTimes( 1 );
|
|
82
|
+
expect( importComponentsMock ).toHaveBeenNthCalledWith( 1, [ ...localFiles, ...externalFiles ] );
|
|
83
|
+
expect( findWorkflowsInNodeModulesMock ).toHaveBeenCalledOnce();
|
|
84
|
+
expect( findWorkflowsInNodeModulesMock ).toHaveBeenCalledWith( '/my/app' );
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
it( 'appends node_modules workflows after local ones and sets external: true', async () => {
|
|
88
|
+
const { loadWorkflows } = await import( './workflows.js' );
|
|
89
|
+
|
|
90
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
91
|
+
yield { metadata: { name: 'LocalFlow', description: 'local' }, path: '/my/app/workflows/wf/workflow.js' };
|
|
92
|
+
yield {
|
|
93
|
+
metadata: { name: '__sum_numbers', description: 'from catalog' },
|
|
94
|
+
path: '/my/app/node_modules/catalog_pkg/src/w/workflow.js'
|
|
95
|
+
};
|
|
96
|
+
} );
|
|
97
|
+
findWorkflowsInNodeModulesMock.mockReturnValue( [ { path: '/my/app/node_modules/catalog_pkg/src/w/workflow.js' } ] );
|
|
98
|
+
|
|
99
|
+
const { workflows } = await loadWorkflows( '/my/app' );
|
|
100
|
+
expect( workflows ).toEqual( [
|
|
101
|
+
{ name: 'LocalFlow', description: 'local', path: '/my/app/workflows/wf/workflow.js', external: false },
|
|
102
|
+
{
|
|
103
|
+
name: '__sum_numbers',
|
|
104
|
+
description: 'from catalog',
|
|
105
|
+
path: '/my/app/node_modules/catalog_pkg/src/w/workflow.js',
|
|
106
|
+
external: true
|
|
107
|
+
}
|
|
108
|
+
] );
|
|
109
|
+
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
it( 'returns only external workflows when the project root has none', async () => {
|
|
113
|
+
const { loadWorkflows } = await import( './workflows.js' );
|
|
114
|
+
|
|
115
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
116
|
+
yield { metadata: { name: 'PkgFlow', description: 'pkg' }, path: '/proj/node_modules/a/w/workflow.js' };
|
|
117
|
+
} );
|
|
118
|
+
findWorkflowsInNodeModulesMock.mockReturnValue( [ { path: '/proj/node_modules/a/w/workflow.js' } ] );
|
|
119
|
+
|
|
120
|
+
const { workflows } = await loadWorkflows( '/proj' );
|
|
121
|
+
expect( workflows ).toEqual( [
|
|
122
|
+
{
|
|
123
|
+
name: 'PkgFlow',
|
|
124
|
+
description: 'pkg',
|
|
125
|
+
path: '/proj/node_modules/a/w/workflow.js',
|
|
126
|
+
external: true
|
|
127
|
+
}
|
|
128
|
+
] );
|
|
129
|
+
} );
|
|
130
|
+
|
|
131
|
+
it( 'throws when a local workflow path is under a shared directory', async () => {
|
|
132
|
+
const { loadWorkflows } = await import( './workflows.js' );
|
|
133
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
134
|
+
yield { metadata: { name: 'Invalid' }, path: '/root/shared/workflow.js' };
|
|
135
|
+
} );
|
|
136
|
+
workflowPathHasSharedMock.mockReturnValueOnce( true );
|
|
137
|
+
|
|
138
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow( 'Workflow directory can\'t be named "shared"' );
|
|
139
|
+
expect( findWorkflowsInNodeModulesMock ).toHaveBeenCalledOnce();
|
|
140
|
+
expect( workflowPathHasSharedMock ).toHaveBeenCalledWith( '/root/shared/workflow.js' );
|
|
141
|
+
} );
|
|
142
|
+
|
|
143
|
+
it( 'throws when a workflow name conflicts with an earlier workflow name', async () => {
|
|
144
|
+
const { loadWorkflows } = await import( './workflows.js' );
|
|
145
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
146
|
+
yield { metadata: { name: 'duplicate' }, path: '/root/a/workflow.js' };
|
|
147
|
+
yield { metadata: { name: 'duplicate' }, path: '/root/b/workflow.js' };
|
|
148
|
+
} );
|
|
149
|
+
|
|
150
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow(
|
|
151
|
+
'Workflow name "duplicate" conflicts with another workflow or alias. Workflow names and aliases must be unique.'
|
|
152
|
+
);
|
|
153
|
+
} );
|
|
154
|
+
|
|
155
|
+
it( 'throws when a workflow name conflicts with an earlier alias', async () => {
|
|
156
|
+
const { loadWorkflows } = await import( './workflows.js' );
|
|
157
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
158
|
+
yield { metadata: { name: 'alpha', aliases: [ 'legacy' ] }, path: '/root/a/workflow.js' };
|
|
159
|
+
yield { metadata: { name: 'legacy' }, path: '/root/b/workflow.js' };
|
|
160
|
+
} );
|
|
161
|
+
|
|
162
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow(
|
|
163
|
+
'Workflow name "legacy" conflicts with another workflow or alias. Workflow names and aliases must be unique.'
|
|
164
|
+
);
|
|
165
|
+
} );
|
|
166
|
+
|
|
167
|
+
it( 'throws when an alias conflicts with an earlier workflow name', async () => {
|
|
168
|
+
const { loadWorkflows } = await import( './workflows.js' );
|
|
169
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
170
|
+
yield { metadata: { name: 'alpha' }, path: '/root/a/workflow.js' };
|
|
171
|
+
yield { metadata: { name: 'beta', aliases: [ 'alpha' ] }, path: '/root/b/workflow.js' };
|
|
172
|
+
} );
|
|
173
|
+
|
|
174
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow(
|
|
175
|
+
'Workflow "beta" alias "alpha" conflicts with another workflow or alias. Workflow names and aliases must be unique.'
|
|
176
|
+
);
|
|
177
|
+
} );
|
|
178
|
+
|
|
179
|
+
it( 'throws when an alias conflicts with an earlier alias', async () => {
|
|
180
|
+
const { loadWorkflows } = await import( './workflows.js' );
|
|
181
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
182
|
+
yield { metadata: { name: 'alpha', aliases: [ 'shared_alias' ] }, path: '/root/a/workflow.js' };
|
|
183
|
+
yield { metadata: { name: 'beta', aliases: [ 'shared_alias' ] }, path: '/root/b/workflow.js' };
|
|
184
|
+
} );
|
|
185
|
+
|
|
186
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow(
|
|
187
|
+
'Workflow "beta" alias "shared_alias" conflicts with another workflow or alias. Workflow names and aliases must be unique.'
|
|
188
|
+
);
|
|
189
|
+
} );
|
|
190
|
+
|
|
191
|
+
it( 'throws when an alias is identical to its workflow name', async () => {
|
|
192
|
+
const { loadWorkflows } = await import( './workflows.js' );
|
|
193
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
194
|
+
yield { metadata: { name: 'alpha', aliases: [ 'alpha' ] }, path: '/root/a/workflow.js' };
|
|
195
|
+
} );
|
|
196
|
+
|
|
197
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow(
|
|
198
|
+
'Workflow "alpha" alias "alpha" conflicts with another workflow or alias. Workflow names and aliases must be unique.'
|
|
199
|
+
);
|
|
200
|
+
} );
|
|
201
|
+
|
|
202
|
+
it( 'allows workflow names and aliases that only differ by case', async () => {
|
|
203
|
+
const { loadWorkflows } = await import( './workflows.js' );
|
|
204
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
205
|
+
yield { metadata: { name: 'Alpha', aliases: [ 'Legacy' ] }, path: '/root/a/workflow.js' };
|
|
206
|
+
yield { metadata: { name: 'alpha', aliases: [ 'legacy' ] }, path: '/root/b/workflow.js' };
|
|
207
|
+
} );
|
|
208
|
+
|
|
209
|
+
await expect( loadWorkflows( '/root' ) ).resolves.toMatchObject( {
|
|
210
|
+
workflows: [
|
|
211
|
+
{ name: 'Alpha', aliases: [ 'Legacy' ], path: '/root/a/workflow.js', external: false },
|
|
212
|
+
{ name: 'alpha', aliases: [ 'legacy' ], path: '/root/b/workflow.js', external: false }
|
|
213
|
+
]
|
|
214
|
+
} );
|
|
215
|
+
} );
|
|
216
|
+
|
|
217
|
+
it( 'throws when a workflow name is reserved for the internal catalog', async () => {
|
|
218
|
+
const { loadWorkflows } = await import( './workflows.js' );
|
|
219
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
220
|
+
yield { metadata: { name: 'catalog' }, path: '/root/catalog/workflow.js' };
|
|
221
|
+
} );
|
|
222
|
+
|
|
223
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow(
|
|
224
|
+
'Workflow name "catalog" is reserved for the internal catalog workflow.'
|
|
225
|
+
);
|
|
226
|
+
} );
|
|
227
|
+
|
|
228
|
+
it( 'throws when a workflow alias is reserved for the internal catalog', async () => {
|
|
229
|
+
const { loadWorkflows } = await import( './workflows.js' );
|
|
230
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
231
|
+
yield { metadata: { name: 'alpha', aliases: [ 'catalog' ] }, path: '/root/a/workflow.js' };
|
|
232
|
+
} );
|
|
233
|
+
|
|
234
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow(
|
|
235
|
+
'Workflow "alpha" alias "catalog" is reserved for the internal catalog workflow.'
|
|
236
|
+
);
|
|
237
|
+
} );
|
|
238
|
+
|
|
239
|
+
it( 'writes an entrypoint with workflows, aliases, and the catalog workflow', async () => {
|
|
240
|
+
const { loadWorkflows } = await import( './workflows.js' );
|
|
241
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
242
|
+
yield { metadata: { name: 'W', aliases: [ 'W_old', 'W_legacy' ] }, path: '/abs/wf.js' };
|
|
243
|
+
} );
|
|
244
|
+
|
|
245
|
+
const { entrypoint } = await loadWorkflows( '/root' );
|
|
246
|
+
|
|
247
|
+
expect( entrypoint ).toBe( '/tmp/__workflows_entrypoint.js' );
|
|
248
|
+
expect( writeFileInTempDirMock ).toHaveBeenCalledTimes( 1 );
|
|
249
|
+
const [ contents, filename ] = writeFileInTempDirMock.mock.calls[0];
|
|
250
|
+
expect( filename ).toBe( '__workflows_entrypoint.js' );
|
|
251
|
+
expect( contents ).toContain( 'export { default as W } from \'/abs/wf.js\';' );
|
|
252
|
+
expect( contents ).toContain( 'export { default as W_old } from \'/abs/wf.js\';' );
|
|
253
|
+
expect( contents ).toContain( 'export { default as W_legacy } from \'/abs/wf.js\';' );
|
|
254
|
+
expect( contents ).toContain( 'export { default as catalog }' );
|
|
255
|
+
} );
|
|
256
|
+
} );
|
|
@@ -4,8 +4,11 @@ import { workerTelemetryIntervalMs } from './configs.js';
|
|
|
4
4
|
const log = createChildLogger( 'Telemetry' );
|
|
5
5
|
|
|
6
6
|
export const setupTelemetry = ( { worker } ) => {
|
|
7
|
-
if ( workerTelemetryIntervalMs
|
|
8
|
-
|
|
7
|
+
if ( workerTelemetryIntervalMs <= 0 ) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
setInterval( () => {
|
|
11
|
+
try {
|
|
9
12
|
log.info( 'Worker', {
|
|
10
13
|
status: worker.getStatus(),
|
|
11
14
|
memory: {
|
|
@@ -14,6 +17,8 @@ export const setupTelemetry = ( { worker } ) => {
|
|
|
14
17
|
memoryUsage: process.memoryUsage()
|
|
15
18
|
}
|
|
16
19
|
} );
|
|
17
|
-
}
|
|
18
|
-
|
|
20
|
+
} catch ( error ) {
|
|
21
|
+
log.warn( 'Failure', { error: error.message } );
|
|
22
|
+
}
|
|
23
|
+
}, workerTelemetryIntervalMs ).unref();
|
|
19
24
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
3
|
const configMock = vi.hoisted( () => ( { workerTelemetryIntervalMs: 0 } ) );
|
|
4
|
-
const logMock = vi.hoisted( () => ( { info: vi.fn() } ) );
|
|
4
|
+
const logMock = vi.hoisted( () => ( { info: vi.fn(), warn: vi.fn() } ) );
|
|
5
5
|
const createChildLoggerMock = vi.hoisted( () => vi.fn( () => logMock ) );
|
|
6
6
|
|
|
7
7
|
vi.mock( './configs.js', () => ( {
|
|
@@ -14,13 +14,13 @@ vi.mock( '#logger', () => ( { createChildLogger: createChildLoggerMock } ) );
|
|
|
14
14
|
|
|
15
15
|
const loadSetupTelemetry = async () => {
|
|
16
16
|
vi.resetModules();
|
|
17
|
-
return import( './
|
|
17
|
+
return import( './telemetry.js' );
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
const mockSetInterval = unrefMock =>
|
|
21
21
|
vi.spyOn( globalThis, 'setInterval' ).mockReturnValue( { unref: unrefMock } );
|
|
22
22
|
|
|
23
|
-
describe( 'worker/
|
|
23
|
+
describe( 'worker/telemetry', () => {
|
|
24
24
|
const availableMemoryMock = vi.fn();
|
|
25
25
|
const constrainedMemoryMock = vi.fn();
|
|
26
26
|
const memoryUsageMock = vi.fn();
|