@outputai/core 0.7.1-dev.144d64f.0 → 0.7.1-next.0b398c7.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} +18 -66
- package/src/worker/{loader_tools.spec.js → loader/tools.spec.js} +24 -92
- 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,10 +1,24 @@
|
|
|
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,
|
|
@@ -16,12 +30,18 @@ import {
|
|
|
16
30
|
isPathDescendentFromNodeModules,
|
|
17
31
|
resolveNodeModulesPath,
|
|
18
32
|
resolveSymlink,
|
|
19
|
-
staticMatchers,
|
|
20
33
|
packageExposesWorkflows
|
|
21
|
-
} from './
|
|
34
|
+
} from './tools.js';
|
|
22
35
|
|
|
23
36
|
const TEMP_BASE = join( process.cwd(), 'sdk/core/temp_test_modules' );
|
|
24
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
|
+
|
|
25
45
|
afterEach( () => {
|
|
26
46
|
rmSync( TEMP_BASE, { recursive: true, force: true } );
|
|
27
47
|
} );
|
|
@@ -159,36 +179,6 @@ describe( 'node_modules package resource helpers', () => {
|
|
|
159
179
|
} );
|
|
160
180
|
} );
|
|
161
181
|
|
|
162
|
-
describe( 'activityMatchersBuilder', () => {
|
|
163
|
-
const base = `${sep}app${sep}proj`;
|
|
164
|
-
|
|
165
|
-
it( 'stepsFile matches only steps.js at base', () => {
|
|
166
|
-
const m = activityMatchersBuilder( base );
|
|
167
|
-
expect( m.stepsFile( `${base}${sep}steps.js` ) ).toBe( true );
|
|
168
|
-
expect( m.stepsFile( `${base}${sep}nested${sep}steps.js` ) ).toBe( false );
|
|
169
|
-
} );
|
|
170
|
-
|
|
171
|
-
it( 'evaluatorsFile matches only evaluators.js at base', () => {
|
|
172
|
-
const m = activityMatchersBuilder( base );
|
|
173
|
-
expect( m.evaluatorsFile( `${base}${sep}evaluators.js` ) ).toBe( true );
|
|
174
|
-
expect( m.evaluatorsFile( `${base}${sep}sub${sep}evaluators.js` ) ).toBe( false );
|
|
175
|
-
} );
|
|
176
|
-
|
|
177
|
-
it( 'stepsDir matches js under steps/', () => {
|
|
178
|
-
const m = activityMatchersBuilder( base );
|
|
179
|
-
expect( m.stepsDir( `${base}${sep}steps${sep}a.js` ) ).toBe( true );
|
|
180
|
-
expect( m.stepsDir( `${base}${sep}steps${sep}sub${sep}b.js` ) ).toBe( true );
|
|
181
|
-
expect( m.stepsDir( `${base}${sep}other${sep}a.js` ) ).toBe( false );
|
|
182
|
-
} );
|
|
183
|
-
|
|
184
|
-
it( 'evaluatorsDir matches js under evaluators/', () => {
|
|
185
|
-
const m = activityMatchersBuilder( base );
|
|
186
|
-
expect( m.evaluatorsDir( `${base}${sep}evaluators${sep}x.js` ) ).toBe( true );
|
|
187
|
-
expect( m.evaluatorsDir( `${base}${sep}evaluators${sep}y${sep}z.js` ) ).toBe( true );
|
|
188
|
-
expect( m.evaluatorsDir( `${base}${sep}steps${sep}x.js` ) ).toBe( false );
|
|
189
|
-
} );
|
|
190
|
-
} );
|
|
191
|
-
|
|
192
182
|
describe( 'matchFiles', () => {
|
|
193
183
|
it( 'collects files matching matchers', () => {
|
|
194
184
|
const root = join( TEMP_BASE, `fbnr-files-${Date.now()}` );
|
|
@@ -603,61 +593,3 @@ describe( 'hashSourceCode', () => {
|
|
|
603
593
|
expect( await hashSourceCode( after ) ).not.toBe( await hashSourceCode( before ) );
|
|
604
594
|
} );
|
|
605
595
|
} );
|
|
606
|
-
|
|
607
|
-
describe( 'staticMatchers', () => {
|
|
608
|
-
describe( 'workflowFile', () => {
|
|
609
|
-
it( 'matches paths ending with path separator and workflow.js', () => {
|
|
610
|
-
expect( staticMatchers.workflowFile( `${sep}x${sep}y${sep}workflow.js` ) ).toBe( true );
|
|
611
|
-
} );
|
|
612
|
-
|
|
613
|
-
it( 'rejects workflow.ts', () => {
|
|
614
|
-
expect( staticMatchers.workflowFile( `${sep}a${sep}workflow.ts` ) ).toBe( false );
|
|
615
|
-
} );
|
|
616
|
-
} );
|
|
617
|
-
|
|
618
|
-
describe( 'workflowPathHasShared', () => {
|
|
619
|
-
it( 'matches workflow.js under a shared folder segment', () => {
|
|
620
|
-
expect( staticMatchers.workflowPathHasShared( `${sep}foo${sep}shared${sep}workflow.js` ) ).toBe( true );
|
|
621
|
-
} );
|
|
622
|
-
|
|
623
|
-
it( 'rejects workflow.js not under shared', () => {
|
|
624
|
-
expect( staticMatchers.workflowPathHasShared( `${sep}foo${sep}workflow.js` ) ).toBe( false );
|
|
625
|
-
} );
|
|
626
|
-
} );
|
|
627
|
-
|
|
628
|
-
describe( 'sharedStepsDir', () => {
|
|
629
|
-
it( 'matches .js files inside shared/steps/', () => {
|
|
630
|
-
expect( staticMatchers.sharedStepsDir( `${sep}app${sep}dist${sep}shared${sep}steps${sep}tools.js` ) ).toBe( true );
|
|
631
|
-
} );
|
|
632
|
-
|
|
633
|
-
it( 'matches .js files in nested subdirectories of shared/steps/', () => {
|
|
634
|
-
expect( staticMatchers.sharedStepsDir( `${sep}app${sep}dist${sep}shared${sep}steps${sep}utils${sep}helper.js` ) ).toBe( true );
|
|
635
|
-
} );
|
|
636
|
-
|
|
637
|
-
it( 'rejects .ts files inside shared/steps/', () => {
|
|
638
|
-
expect( staticMatchers.sharedStepsDir( `${sep}app${sep}src${sep}shared${sep}steps${sep}tools.ts` ) ).toBe( false );
|
|
639
|
-
} );
|
|
640
|
-
|
|
641
|
-
it( 'rejects non-.js files inside shared/steps/', () => {
|
|
642
|
-
expect( staticMatchers.sharedStepsDir( `${sep}app${sep}dist${sep}shared${sep}steps${sep}readme.md` ) ).toBe( false );
|
|
643
|
-
} );
|
|
644
|
-
} );
|
|
645
|
-
|
|
646
|
-
describe( 'sharedEvaluatorsDir', () => {
|
|
647
|
-
it( 'matches .js files inside shared/evaluators/', () => {
|
|
648
|
-
expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}dist${sep}shared${sep}evaluators${sep}quality.js` ) ).toBe( true );
|
|
649
|
-
} );
|
|
650
|
-
|
|
651
|
-
it( 'matches .js files in nested subdirectories of shared/evaluators/', () => {
|
|
652
|
-
expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}dist${sep}shared${sep}evaluators${sep}utils${sep}helper.js` ) ).toBe( true );
|
|
653
|
-
} );
|
|
654
|
-
|
|
655
|
-
it( 'rejects .ts files inside shared/evaluators/', () => {
|
|
656
|
-
expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}src${sep}shared${sep}evaluators${sep}quality.ts` ) ).toBe( false );
|
|
657
|
-
} );
|
|
658
|
-
|
|
659
|
-
it( 'rejects non-.js files inside shared/evaluators/', () => {
|
|
660
|
-
expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}dist${sep}shared${sep}evaluators${sep}readme.md` ) ).toBe( false );
|
|
661
|
-
} );
|
|
662
|
-
} );
|
|
663
|
-
} );
|
|
@@ -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();
|