@outputai/core 0.7.1-next.ae5bab4.0 → 0.7.1-next.ba2fb0b.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.
Files changed (78) hide show
  1. package/bin/worker.sh +6 -0
  2. package/package.json +1 -1
  3. package/src/consts.js +0 -4
  4. package/src/errors.js +6 -2
  5. package/src/hooks/index.d.ts +10 -0
  6. package/src/interface/evaluator.js +7 -20
  7. package/src/interface/evaluator.spec.js +117 -1
  8. package/src/interface/step.js +8 -9
  9. package/src/interface/step.spec.js +124 -0
  10. package/src/interface/validations/index.js +108 -0
  11. package/src/interface/validations/index.spec.js +182 -0
  12. package/src/interface/validations/schemas.js +113 -0
  13. package/src/interface/validations/schemas.spec.js +209 -0
  14. package/src/interface/webhook.js +1 -1
  15. package/src/interface/webhook.spec.js +1 -1
  16. package/src/interface/workflow.d.ts +10 -9
  17. package/src/interface/workflow.js +76 -164
  18. package/src/interface/workflow.spec.js +637 -521
  19. package/src/interface/workflow_activity_options.js +16 -0
  20. package/src/interface/workflow_utils.js +1 -1
  21. package/src/interface/zod_integration.spec.js +2 -2
  22. package/src/internal_utils/aggregations.js +0 -10
  23. package/src/internal_utils/aggregations.spec.js +1 -48
  24. package/src/internal_utils/errors.js +14 -8
  25. package/src/internal_utils/errors.spec.js +73 -27
  26. package/src/utils/index.d.ts +19 -0
  27. package/src/utils/utils.js +53 -0
  28. package/src/utils/utils.spec.js +105 -1
  29. package/src/worker/bundle.js +26 -0
  30. package/src/worker/bundle.spec.js +53 -0
  31. package/src/worker/bundler_options.js +1 -1
  32. package/src/worker/bundler_options.spec.js +1 -1
  33. package/src/worker/catalog_workflow/catalog_job.js +148 -0
  34. package/src/worker/catalog_workflow/catalog_job.spec.js +232 -0
  35. package/src/worker/check.js +24 -0
  36. package/src/worker/connection_monitor.js +112 -0
  37. package/src/worker/connection_monitor.spec.js +199 -0
  38. package/src/worker/index.js +146 -41
  39. package/src/worker/index.spec.js +281 -109
  40. package/src/worker/interceptors/activity.js +7 -24
  41. package/src/worker/interceptors/activity.spec.js +97 -66
  42. package/src/worker/interceptors/index.js +4 -7
  43. package/src/worker/interceptors/modules.js +15 -0
  44. package/src/worker/interceptors/workflow.js +6 -8
  45. package/src/worker/interceptors/workflow.spec.js +49 -42
  46. package/src/worker/interruption.js +33 -0
  47. package/src/worker/interruption.spec.js +98 -0
  48. package/src/worker/loader/activities.js +75 -0
  49. package/src/worker/loader/activities.spec.js +213 -0
  50. package/src/worker/loader/hooks.js +28 -0
  51. package/src/worker/loader/hooks.spec.js +64 -0
  52. package/src/worker/loader/matchers.js +46 -0
  53. package/src/worker/loader/matchers.spec.js +140 -0
  54. package/src/worker/{loader_tools.js → loader/tools.js} +19 -67
  55. package/src/worker/{loader_tools.spec.js → loader/tools.spec.js} +53 -85
  56. package/src/worker/loader/workflows.js +82 -0
  57. package/src/worker/loader/workflows.spec.js +256 -0
  58. package/src/worker/{setup_telemetry.js → telemetry.js} +9 -4
  59. package/src/worker/{setup_telemetry.spec.js → telemetry.spec.js} +3 -3
  60. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +5 -109
  61. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +31 -103
  62. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +5 -6
  63. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +11 -83
  64. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +8 -11
  65. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +9 -9
  66. package/src/interface/validations/runtime.js +0 -20
  67. package/src/interface/validations/runtime.spec.js +0 -29
  68. package/src/interface/validations/schema_utils.js +0 -8
  69. package/src/interface/validations/schema_utils.spec.js +0 -67
  70. package/src/interface/validations/static.js +0 -137
  71. package/src/interface/validations/static.spec.js +0 -397
  72. package/src/interface/workflow.replay_compatibility.spec.js +0 -254
  73. package/src/worker/loader.js +0 -202
  74. package/src/worker/loader.spec.js +0 -498
  75. package/src/worker/shutdown.js +0 -26
  76. package/src/worker/shutdown.spec.js +0 -82
  77. package/src/worker/start_catalog.js +0 -96
  78. 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 './loader_tools.js';
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( 'staticMatchers', () => {
572
- describe( 'workflowFile', () => {
573
- it( 'matches paths ending with path separator and workflow.js', () => {
574
- expect( staticMatchers.workflowFile( `${sep}x${sep}y${sep}workflow.js` ) ).toBe( true );
575
- } );
576
-
577
- it( 'rejects workflow.ts', () => {
578
- expect( staticMatchers.workflowFile( `${sep}a${sep}workflow.ts` ) ).toBe( false );
579
- } );
580
- } );
581
-
582
- describe( 'workflowPathHasShared', () => {
583
- it( 'matches workflow.js under a shared folder segment', () => {
584
- expect( staticMatchers.workflowPathHasShared( `${sep}foo${sep}shared${sep}workflow.js` ) ).toBe( true );
585
- } );
586
-
587
- it( 'rejects workflow.js not under shared', () => {
588
- expect( staticMatchers.workflowPathHasShared( `${sep}foo${sep}workflow.js` ) ).toBe( false );
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
- it( 'rejects non-.js files inside shared/steps/', () => {
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
- describe( 'sharedEvaluatorsDir', () => {
611
- it( 'matches .js files inside shared/evaluators/', () => {
612
- expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}dist${sep}shared${sep}evaluators${sep}quality.js` ) ).toBe( true );
613
- } );
614
-
615
- it( 'matches .js files in nested subdirectories of shared/evaluators/', () => {
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
- it( 'rejects non-.js files inside shared/evaluators/', () => {
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 > 0 ) {
8
- setInterval( () => {
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
- }, workerTelemetryIntervalMs ).unref();
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( './setup_telemetry.js' );
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/setup_telemetry', () => {
23
+ describe( 'worker/telemetry', () => {
24
24
  const availableMemoryMock = vi.fn();
25
25
  const constrainedMemoryMock = vi.fn();
26
26
  const memoryUsageMock = vi.fn();