@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.
Files changed (77) 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/interface/evaluator.js +7 -20
  6. package/src/interface/evaluator.spec.js +117 -1
  7. package/src/interface/step.js +8 -9
  8. package/src/interface/step.spec.js +124 -0
  9. package/src/interface/validations/index.js +108 -0
  10. package/src/interface/validations/index.spec.js +182 -0
  11. package/src/interface/validations/schemas.js +113 -0
  12. package/src/interface/validations/schemas.spec.js +209 -0
  13. package/src/interface/webhook.js +1 -1
  14. package/src/interface/webhook.spec.js +1 -1
  15. package/src/interface/workflow.d.ts +10 -9
  16. package/src/interface/workflow.js +76 -164
  17. package/src/interface/workflow.spec.js +637 -521
  18. package/src/interface/workflow_activity_options.js +16 -0
  19. package/src/interface/workflow_utils.js +1 -1
  20. package/src/interface/zod_integration.spec.js +2 -2
  21. package/src/internal_utils/aggregations.js +0 -10
  22. package/src/internal_utils/aggregations.spec.js +1 -48
  23. package/src/internal_utils/errors.js +14 -8
  24. package/src/internal_utils/errors.spec.js +73 -27
  25. package/src/utils/index.d.ts +19 -0
  26. package/src/utils/utils.js +53 -0
  27. package/src/utils/utils.spec.js +105 -1
  28. package/src/worker/bundle.js +26 -0
  29. package/src/worker/bundle.spec.js +53 -0
  30. package/src/worker/bundler_options.js +1 -1
  31. package/src/worker/bundler_options.spec.js +1 -1
  32. package/src/worker/catalog_workflow/catalog_job.js +148 -0
  33. package/src/worker/catalog_workflow/catalog_job.spec.js +232 -0
  34. package/src/worker/check.js +24 -0
  35. package/src/worker/connection_monitor.js +112 -0
  36. package/src/worker/connection_monitor.spec.js +199 -0
  37. package/src/worker/index.js +146 -41
  38. package/src/worker/index.spec.js +281 -109
  39. package/src/worker/interceptors/activity.js +7 -24
  40. package/src/worker/interceptors/activity.spec.js +97 -66
  41. package/src/worker/interceptors/index.js +4 -7
  42. package/src/worker/interceptors/modules.js +15 -0
  43. package/src/worker/interceptors/workflow.js +6 -8
  44. package/src/worker/interceptors/workflow.spec.js +49 -42
  45. package/src/worker/interruption.js +33 -0
  46. package/src/worker/interruption.spec.js +98 -0
  47. package/src/worker/loader/activities.js +75 -0
  48. package/src/worker/loader/activities.spec.js +213 -0
  49. package/src/worker/loader/hooks.js +28 -0
  50. package/src/worker/loader/hooks.spec.js +64 -0
  51. package/src/worker/loader/matchers.js +46 -0
  52. package/src/worker/loader/matchers.spec.js +140 -0
  53. package/src/worker/{loader_tools.js → loader/tools.js} +18 -66
  54. package/src/worker/{loader_tools.spec.js → loader/tools.spec.js} +24 -92
  55. package/src/worker/loader/workflows.js +82 -0
  56. package/src/worker/loader/workflows.spec.js +256 -0
  57. package/src/worker/{setup_telemetry.js → telemetry.js} +9 -4
  58. package/src/worker/{setup_telemetry.spec.js → telemetry.spec.js} +3 -3
  59. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +5 -109
  60. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +31 -103
  61. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +5 -6
  62. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +11 -83
  63. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +8 -11
  64. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +9 -9
  65. package/src/interface/validations/runtime.js +0 -20
  66. package/src/interface/validations/runtime.spec.js +0 -29
  67. package/src/interface/validations/schema_utils.js +0 -8
  68. package/src/interface/validations/schema_utils.spec.js +0 -67
  69. package/src/interface/validations/static.js +0 -137
  70. package/src/interface/validations/static.spec.js +0 -397
  71. package/src/interface/workflow.replay_compatibility.spec.js +0 -254
  72. package/src/worker/loader.js +0 -202
  73. package/src/worker/loader.spec.js +0 -498
  74. package/src/worker/shutdown.js +0 -26
  75. package/src/worker/shutdown.spec.js +0 -82
  76. package/src/worker/start_catalog.js +0 -96
  77. 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 './loader_tools.js';
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 > 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();