@outputai/core 0.7.1-next.db8ddd7.0 → 0.7.1-next.ed233ce.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 +86 -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} +19 -67
  54. package/src/worker/{loader_tools.spec.js → loader/tools.spec.js} +53 -85
  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
@@ -0,0 +1,213 @@
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 sendHttpRequestMock = vi.fn();
13
+ const getTraceDestinationsMock = vi.fn();
14
+ vi.mock( '#internal_activities', () => ( {
15
+ sendHttpRequest: sendHttpRequestMock,
16
+ getTraceDestinations: getTraceDestinationsMock
17
+ } ) );
18
+
19
+ const { importComponentsMock, findSharedActivitiesFromWorkflowsMock, matchFilesMock, writeFileInTempDirMock } = vi.hoisted( () => ( {
20
+ importComponentsMock: vi.fn(),
21
+ findSharedActivitiesFromWorkflowsMock: vi.fn(),
22
+ matchFilesMock: vi.fn(),
23
+ writeFileInTempDirMock: vi.fn()
24
+ } ) );
25
+
26
+ const { buildActivityMatcherMock, activityMatcherMock, sharedStepsDirMock, sharedEvaluatorsDirMock } = vi.hoisted( () => ( {
27
+ buildActivityMatcherMock: vi.fn(),
28
+ activityMatcherMock: vi.fn(),
29
+ sharedStepsDirMock: vi.fn(),
30
+ sharedEvaluatorsDirMock: vi.fn()
31
+ } ) );
32
+
33
+ vi.mock( './tools.js', () => ( {
34
+ importComponents: importComponentsMock,
35
+ findSharedActivitiesFromWorkflows: findSharedActivitiesFromWorkflowsMock,
36
+ matchFiles: matchFilesMock,
37
+ writeFileInTempDir: writeFileInTempDirMock
38
+ } ) );
39
+
40
+ vi.mock( './matchers.js', () => ( {
41
+ buildActivityMatcher: buildActivityMatcherMock,
42
+ staticMatchers: {
43
+ sharedStepsDir: sharedStepsDirMock,
44
+ sharedEvaluatorsDir: sharedEvaluatorsDirMock
45
+ }
46
+ } ) );
47
+
48
+ describe( 'loadActivities', () => {
49
+ beforeEach( () => {
50
+ vi.clearAllMocks();
51
+ importComponentsMock.mockReset();
52
+ importComponentsMock.mockImplementation( async function *() {} );
53
+ findSharedActivitiesFromWorkflowsMock.mockReset();
54
+ findSharedActivitiesFromWorkflowsMock.mockReturnValue( [] );
55
+ matchFilesMock.mockReset();
56
+ matchFilesMock.mockReturnValue( [] );
57
+ writeFileInTempDirMock.mockReset();
58
+ writeFileInTempDirMock.mockReturnValue( '/tmp/__activity_options.js' );
59
+ buildActivityMatcherMock.mockReset();
60
+ buildActivityMatcherMock.mockReturnValue( activityMatcherMock );
61
+ } );
62
+
63
+ it( 'loadActivities returns map including system activity and writes options file', async () => {
64
+ const { loadActivities } = await import( './activities.js' );
65
+
66
+ importComponentsMock.mockImplementationOnce( async function *() {
67
+ yield {
68
+ fn: () => {},
69
+ metadata: { name: 'Act1', options: { activityOptions: { retry: { maximumAttempts: 3 } } } },
70
+ path: '/a/steps.js'
71
+ };
72
+ } );
73
+ importComponentsMock.mockImplementationOnce( async function *() {} );
74
+
75
+ const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
76
+ const { activities, optionsFile } = await loadActivities( '/root', workflows );
77
+ expect( activities['A#Act1'] ).toBeTypeOf( 'function' );
78
+ expect( activities['__internal#sendHttpRequest'] ).toBe( sendHttpRequestMock );
79
+ expect( optionsFile ).toBe( '/tmp/__activity_options.js' );
80
+
81
+ expect( writeFileInTempDirMock ).toHaveBeenCalledTimes( 1 );
82
+ const [ contents, filename ] = writeFileInTempDirMock.mock.calls[0];
83
+ expect( filename ).toBe( '__activity_options.js' );
84
+ expect( contents ).toContain( 'export default' );
85
+ expect( JSON.parse( contents.replace( /^export default\s*/, '' ).replace( /;\s*$/, '' ) ) ).toEqual( {
86
+ 'A#Act1': { retry: { maximumAttempts: 3 } }
87
+ } );
88
+ } );
89
+
90
+ it( 'loadActivities omits activity options when component has no options or no activityOptions', async () => {
91
+ const { loadActivities } = await import( './activities.js' );
92
+ importComponentsMock.mockImplementationOnce( async function *() {
93
+ yield { fn: () => {}, metadata: { name: 'NoOptions' }, path: '/a/steps.js' };
94
+ yield { fn: () => {}, metadata: { name: 'EmptyOptions', options: {} }, path: '/a/steps2.js' };
95
+ } );
96
+ importComponentsMock.mockImplementationOnce( async function *() {} );
97
+
98
+ await loadActivities( '/root', [ { name: 'A', path: '/a/workflow.js' } ] );
99
+ const written = JSON.parse(
100
+ writeFileInTempDirMock.mock.calls[0][0].replace( /^export default\s*/, '' ).replace( /;\s*$/, '' )
101
+ );
102
+ expect( written['A#NoOptions'] ).toBeUndefined();
103
+ expect( written['A#EmptyOptions'] ).toBeUndefined();
104
+ } );
105
+
106
+ it( 'loadActivities throws when two activities in the same workflow share a name', async () => {
107
+ const { loadActivities } = await import( './activities.js' );
108
+ importComponentsMock.mockImplementationOnce( async function *() {
109
+ yield { fn: () => {}, metadata: { name: 'DuplicateActivity' }, path: '/a/steps.js' };
110
+ yield { fn: () => {}, metadata: { name: 'DuplicateActivity' }, path: '/a/evaluators.js' };
111
+ } );
112
+
113
+ await expect( loadActivities( '/root', [ { name: 'A', path: '/a/workflow.js' } ] ) ).rejects.toThrow(
114
+ 'Activity "DuplicateActivity" in workflow "A" conflicts with another activity in the same workflow. \
115
+ Activity names must be unique within a workflow.'
116
+ );
117
+ } );
118
+
119
+ it( 'loadActivities throws when two shared activities share a name', async () => {
120
+ const { loadActivities } = await import( './activities.js' );
121
+ importComponentsMock.mockImplementationOnce( async function *() {} );
122
+ importComponentsMock.mockImplementationOnce( async function *() {
123
+ yield { fn: () => {}, metadata: { name: 'DuplicateShared' }, path: '/root/shared/steps/a.js' };
124
+ yield { fn: () => {}, metadata: { name: 'DuplicateShared' }, path: '/root/shared/evaluators/a.js' };
125
+ } );
126
+
127
+ await expect( loadActivities( '/root', [ { name: 'A', path: '/a/workflow.js' } ] ) ).rejects.toThrow(
128
+ 'Shared activity "DuplicateShared" conflicts with another shared activity. Shared activity names must be unique.'
129
+ );
130
+ } );
131
+
132
+ it( 'loadActivities uses activity and shared matchers for workflow and shared scans', async () => {
133
+ const { loadActivities } = await import( './activities.js' );
134
+ const workflowFiles = [ { path: '/a/steps/foo.js' } ];
135
+ const sharedFiles = [ { path: '/root/shared/steps/baz.js' } ];
136
+ matchFilesMock.mockReturnValueOnce( workflowFiles );
137
+ matchFilesMock.mockReturnValueOnce( sharedFiles );
138
+ importComponentsMock.mockImplementationOnce( async function *() {} );
139
+ importComponentsMock.mockImplementationOnce( async function *() {} );
140
+
141
+ const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
142
+ await loadActivities( '/root', workflows );
143
+
144
+ expect( matchFilesMock ).toHaveBeenCalledTimes( 2 );
145
+ expect( buildActivityMatcherMock ).toHaveBeenCalledWith( '/a' );
146
+ expect( matchFilesMock ).toHaveBeenNthCalledWith( 1, '/a', [ activityMatcherMock ] );
147
+ expect( matchFilesMock ).toHaveBeenNthCalledWith( 2, '/root', [ sharedStepsDirMock, sharedEvaluatorsDirMock ] );
148
+
149
+ expect( importComponentsMock ).toHaveBeenCalledTimes( 2 );
150
+ expect( importComponentsMock ).toHaveBeenNthCalledWith( 1, workflowFiles );
151
+ expect( importComponentsMock ).toHaveBeenNthCalledWith( 2, sharedFiles );
152
+ } );
153
+
154
+ it( 'loads shared activities from external workflow packages', async () => {
155
+ const { loadActivities } = await import( './activities.js' );
156
+ const externalSharedFiles = [ { path: '/root/node_modules/pkg/shared/steps/prepare.js' } ];
157
+ const localWorkflow = { name: 'Local', path: '/root/workflows/local/workflow.js' };
158
+ const externalWorkflow = { name: 'External', path: '/root/node_modules/pkg/workflows/a/workflow.js', external: true };
159
+ findSharedActivitiesFromWorkflowsMock.mockReturnValue( externalSharedFiles );
160
+ importComponentsMock.mockImplementationOnce( async function *() {} );
161
+ importComponentsMock.mockImplementationOnce( async function *() {} );
162
+ importComponentsMock.mockImplementationOnce( async function *() {
163
+ yield {
164
+ fn: () => {},
165
+ metadata: { name: 'ExternalShared', options: { activityOptions: { retry: { maximumAttempts: 2 } } } },
166
+ path: '/root/node_modules/pkg/shared/steps/prepare.js'
167
+ };
168
+ } );
169
+
170
+ const { activities } = await loadActivities( '/root', [ localWorkflow, externalWorkflow ] );
171
+
172
+ expect( findSharedActivitiesFromWorkflowsMock ).toHaveBeenCalledWith( [ externalWorkflow ] );
173
+ expect( importComponentsMock ).toHaveBeenNthCalledWith( 3, externalSharedFiles );
174
+ expect( activities['$shared#ExternalShared'] ).toBeTypeOf( 'function' );
175
+ const written = JSON.parse(
176
+ writeFileInTempDirMock.mock.calls[0][0].replace( /^export default\s*/, '' ).replace( /;\s*$/, '' )
177
+ );
178
+ expect( written['$shared#ExternalShared'] ).toEqual( { retry: { maximumAttempts: 2 } } );
179
+ } );
180
+
181
+ it( 'loadActivities includes nested workflow steps and shared evaluators', async () => {
182
+ const { loadActivities } = await import( './activities.js' );
183
+ importComponentsMock.mockImplementationOnce( async function *() {
184
+ yield { fn: () => {}, metadata: { name: 'ActNested' }, path: '/a/steps/foo.js' };
185
+ } );
186
+ importComponentsMock.mockImplementationOnce( async function *() {
187
+ yield { fn: () => {}, metadata: { name: 'SharedEval' }, path: '/root/shared/evaluators/bar.js' };
188
+ } );
189
+
190
+ const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
191
+ const { activities } = await loadActivities( '/root', workflows );
192
+ expect( activities['A#ActNested'] ).toBeTypeOf( 'function' );
193
+ expect( activities['$shared#SharedEval'] ).toBeTypeOf( 'function' );
194
+ } );
195
+
196
+ it( 'collects shared nested steps and evaluators across multiple subfolders', async () => {
197
+ const { loadActivities } = await import( './activities.js' );
198
+ importComponentsMock.mockImplementationOnce( async function *() {} );
199
+ importComponentsMock.mockImplementationOnce( async function *() {
200
+ yield { fn: () => {}, metadata: { name: 'SharedStepPrimary' }, path: '/root/shared/steps/primary/a.js' };
201
+ yield { fn: () => {}, metadata: { name: 'SharedStepSecondary' }, path: '/root/shared/steps/secondary/b.js' };
202
+ yield { fn: () => {}, metadata: { name: 'SharedEvalPrimary' }, path: '/root/shared/evaluators/primary/c.js' };
203
+ yield { fn: () => {}, metadata: { name: 'SharedEvalSecondary' }, path: '/root/shared/evaluators/secondary/d.js' };
204
+ } );
205
+
206
+ const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
207
+ const { activities } = await loadActivities( '/root', workflows );
208
+ expect( activities['$shared#SharedStepPrimary'] ).toBeTypeOf( 'function' );
209
+ expect( activities['$shared#SharedStepSecondary'] ).toBeTypeOf( 'function' );
210
+ expect( activities['$shared#SharedEvalPrimary'] ).toBeTypeOf( 'function' );
211
+ expect( activities['$shared#SharedEvalSecondary'] ).toBeTypeOf( 'function' );
212
+ } );
213
+ } );
@@ -0,0 +1,28 @@
1
+ import { join } from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { createChildLogger } from '#logger';
4
+
5
+ const log = createChildLogger( 'Hooks Loader' );
6
+
7
+ /**
8
+ * Loads the hook files from package.json's "outputai" section.
9
+ *
10
+ * @param {string} rootDir
11
+ * @returns {void}
12
+ */
13
+ export async function loadHooks( rootDir ) {
14
+ const packageFile = join( rootDir, 'package.json' );
15
+ if ( existsSync( packageFile ) ) {
16
+ const pkg = await import( packageFile, { with: { type: 'json' } } );
17
+ const content = pkg.default;
18
+ const hooks = [];
19
+ // @DEPRECATED: "output" is the legacy namespace for configs, can be removed after couple version (this is being added in 0.3.x)
20
+ hooks.push( ...( content['output']?.hookFiles ?? [] ) );
21
+ hooks.push( ...( content['outputai']?.hookFiles ?? [] ) );
22
+ for ( const path of hooks ) {
23
+ const hookFile = join( rootDir, path );
24
+ await import( hookFile );
25
+ log.info( 'Hook file loaded', { path } );
26
+ }
27
+ }
28
+ };
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+
5
+ const fsMocks = vi.hoisted( () => ( {
6
+ existsSync: vi.fn().mockReturnValue( false )
7
+ } ) );
8
+ vi.mock( 'node:fs', () => ( {
9
+ existsSync: fsMocks.existsSync
10
+ } ) );
11
+
12
+ describe( 'loadHooks', () => {
13
+ beforeEach( () => {
14
+ vi.clearAllMocks();
15
+ fsMocks.existsSync.mockReturnValue( false );
16
+ } );
17
+
18
+ it( 'resolves without importing when package.json does not exist', async () => {
19
+ fsMocks.existsSync.mockReturnValue( false );
20
+ const { loadHooks } = await import( './hooks.js' );
21
+ await expect( loadHooks( '/root' ) ).resolves.toBeUndefined();
22
+ expect( fsMocks.existsSync ).toHaveBeenCalledWith( join( '/root', 'package.json' ) );
23
+ } );
24
+
25
+ it( 'imports hook files listed in package.json outputai.hookFiles', async () => {
26
+ vi.doUnmock( 'node:fs' );
27
+ vi.resetModules();
28
+ const fs = await import( 'node:fs' );
29
+ const tmpDir = fs.mkdtempSync( join( tmpdir(), 'loader-spec-' ) );
30
+ try {
31
+ fs.writeFileSync( join( tmpDir, 'package.json' ), JSON.stringify( {
32
+ outputai: { hookFiles: [ 'hook.js' ] }
33
+ } ) );
34
+ fs.writeFileSync( join( tmpDir, 'hook.js' ), 'globalThis.__loadHooksTestLoaded = true;' );
35
+
36
+ const { loadHooks } = await import( './hooks.js' );
37
+ await loadHooks( tmpDir );
38
+ expect( globalThis.__loadHooksTestLoaded ).toBe( true );
39
+ } finally {
40
+ delete globalThis.__loadHooksTestLoaded;
41
+ fs.rmSync( tmpDir, { recursive: true, force: true } );
42
+ }
43
+ } );
44
+
45
+ it( 'imports hook files from legacy package.json output.hookFiles', async () => {
46
+ vi.doUnmock( 'node:fs' );
47
+ vi.resetModules();
48
+ const fs = await import( 'node:fs' );
49
+ const tmpDir = fs.mkdtempSync( join( tmpdir(), 'loader-spec-' ) );
50
+ try {
51
+ fs.writeFileSync( join( tmpDir, 'package.json' ), JSON.stringify( {
52
+ output: { hookFiles: [ 'legacy_hook.js' ] }
53
+ } ) );
54
+ fs.writeFileSync( join( tmpDir, 'legacy_hook.js' ), 'globalThis.__loadHooksLegacyTestLoaded = true;' );
55
+
56
+ const { loadHooks } = await import( './hooks.js' );
57
+ await loadHooks( tmpDir );
58
+ expect( globalThis.__loadHooksLegacyTestLoaded ).toBe( true );
59
+ } finally {
60
+ delete globalThis.__loadHooksLegacyTestLoaded;
61
+ fs.rmSync( tmpDir, { recursive: true, force: true } );
62
+ }
63
+ } );
64
+ } );
@@ -0,0 +1,46 @@
1
+ import { sep } from 'node:path';
2
+ import { rxEscape } from '#utils';
3
+
4
+ /**
5
+ * Creates a matcher function that based on "path", matches:
6
+ * - path/steps.js
7
+ * - path/evaluators.js
8
+ * - path/steps/*
9
+ * - path/evaluators/*
10
+ * @param {string} path
11
+ * @returns {function(string): boolean}
12
+ */
13
+ export const buildActivityMatcher = path => {
14
+ const exp = new RegExp( `^${rxEscape( `${path}${sep}` )}(?:steps|evaluators)(?:\\.js$|${rxEscape( sep )})` );
15
+ return v => exp.test( v );
16
+ };
17
+
18
+ /**
19
+ * Matchers that can be used to access conditions without initializing them
20
+ */
21
+ export const staticMatchers = {
22
+ /**
23
+ * Matches a workflow.js file
24
+ * @param {string} path - Path to test
25
+ * @returns {boolean}
26
+ */
27
+ workflowFile: v => v.endsWith( `${sep}workflow.js` ),
28
+ /**
29
+ * Matches a workflow.js that is inside a shared folder: eg foo/shared/workflow.js
30
+ * @param {string} path - Path to test
31
+ * @returns {boolean}
32
+ */
33
+ workflowPathHasShared: v => v.endsWith( `${sep}shared${sep}workflow.js` ),
34
+ /**
35
+ * Matches the shared folder for steps src/shared/steps/../step_file.js
36
+ * @param {string} path - Path to test
37
+ * @returns {boolean}
38
+ */
39
+ sharedStepsDir: v => v.includes( `${sep}shared${sep}steps${sep}` ) && v.endsWith( '.js' ),
40
+ /**
41
+ * Matches the shared folder for evaluators src/shared/evaluators/../evaluator_file.js
42
+ * @param {string} path - Path to test
43
+ * @returns {boolean}
44
+ */
45
+ sharedEvaluatorsDir: v => v.includes( `${sep}shared${sep}evaluators${sep}` ) && v.endsWith( '.js' )
46
+ };
@@ -0,0 +1,140 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { sep } from 'node:path';
3
+ import { buildActivityMatcher, staticMatchers } from './matchers.js';
4
+
5
+ describe( 'buildActivityMatcher', () => {
6
+ const base = `${sep}app${sep}proj`;
7
+
8
+ it( 'matches steps.js at the base path', () => {
9
+ const matchActivity = buildActivityMatcher( base );
10
+
11
+ expect( matchActivity( `${base}${sep}steps.js` ) ).toBe( true );
12
+ } );
13
+
14
+ it( 'matches evaluators.js at the base path', () => {
15
+ const matchActivity = buildActivityMatcher( base );
16
+
17
+ expect( matchActivity( `${base}${sep}evaluators.js` ) ).toBe( true );
18
+ } );
19
+
20
+ it( 'matches files under the steps directory', () => {
21
+ const matchActivity = buildActivityMatcher( base );
22
+
23
+ expect( matchActivity( `${base}${sep}steps${sep}a.js` ) ).toBe( true );
24
+ expect( matchActivity( `${base}${sep}steps${sep}sub${sep}b.js` ) ).toBe( true );
25
+ } );
26
+
27
+ it( 'matches files under the evaluators directory', () => {
28
+ const matchActivity = buildActivityMatcher( base );
29
+
30
+ expect( matchActivity( `${base}${sep}evaluators${sep}x.js` ) ).toBe( true );
31
+ expect( matchActivity( `${base}${sep}evaluators${sep}y${sep}z.js` ) ).toBe( true );
32
+ } );
33
+
34
+ it( 'rejects activity filenames outside the base path', () => {
35
+ const matchActivity = buildActivityMatcher( base );
36
+
37
+ expect( matchActivity( `${base}${sep}nested${sep}steps.js` ) ).toBe( false );
38
+ expect( matchActivity( `${base}${sep}sub${sep}evaluators.js` ) ).toBe( false );
39
+ expect( matchActivity( `${sep}app${sep}other${sep}steps.js` ) ).toBe( false );
40
+ } );
41
+
42
+ it( 'rejects non-activity names at the base path', () => {
43
+ const matchActivity = buildActivityMatcher( base );
44
+
45
+ expect( matchActivity( `${base}${sep}workflow.js` ) ).toBe( false );
46
+ expect( matchActivity( `${base}${sep}shared${sep}steps${sep}a.js` ) ).toBe( false );
47
+ expect( matchActivity( `${base}${sep}other${sep}a.js` ) ).toBe( false );
48
+ } );
49
+
50
+ it( 'rejects similarly named files and directories', () => {
51
+ const matchActivity = buildActivityMatcher( base );
52
+
53
+ expect( matchActivity( `${base}${sep}stepsXjs` ) ).toBe( false );
54
+ expect( matchActivity( `${base}${sep}evaluatorsXjs` ) ).toBe( false );
55
+ expect( matchActivity( `${base}${sep}steps-extra.js` ) ).toBe( false );
56
+ expect( matchActivity( `${base}${sep}evaluators-extra.js` ) ).toBe( false );
57
+ expect( matchActivity( `${base}${sep}steps_extra${sep}a.js` ) ).toBe( false );
58
+ expect( matchActivity( `${base}${sep}evaluators_extra${sep}a.js` ) ).toBe( false );
59
+ } );
60
+
61
+ it( 'rejects directory names without a nested path', () => {
62
+ const matchActivity = buildActivityMatcher( base );
63
+
64
+ expect( matchActivity( `${base}${sep}steps` ) ).toBe( false );
65
+ expect( matchActivity( `${base}${sep}evaluators` ) ).toBe( false );
66
+ } );
67
+
68
+ it( 'rejects exact files when additional path segments follow', () => {
69
+ const matchActivity = buildActivityMatcher( base );
70
+
71
+ expect( matchActivity( `${base}${sep}steps.js${sep}extra.js` ) ).toBe( false );
72
+ expect( matchActivity( `${base}${sep}evaluators.js${sep}extra.js` ) ).toBe( false );
73
+ } );
74
+
75
+ it( 'escapes regular expression characters in the base path', () => {
76
+ const specialBase = `${sep}app${sep}proj.with-symbols+(test)`;
77
+ const matchActivity = buildActivityMatcher( specialBase );
78
+
79
+ expect( matchActivity( `${specialBase}${sep}steps.js` ) ).toBe( true );
80
+ expect( matchActivity( `${sep}app${sep}projXwith-symbols+(test)${sep}steps.js` ) ).toBe( false );
81
+ } );
82
+ } );
83
+
84
+ describe( 'staticMatchers', () => {
85
+ describe( 'workflowFile', () => {
86
+ it( 'matches paths ending with path separator and workflow.js', () => {
87
+ expect( staticMatchers.workflowFile( `${sep}x${sep}y${sep}workflow.js` ) ).toBe( true );
88
+ } );
89
+
90
+ it( 'rejects workflow.ts', () => {
91
+ expect( staticMatchers.workflowFile( `${sep}a${sep}workflow.ts` ) ).toBe( false );
92
+ } );
93
+ } );
94
+
95
+ describe( 'workflowPathHasShared', () => {
96
+ it( 'matches workflow.js under a shared folder segment', () => {
97
+ expect( staticMatchers.workflowPathHasShared( `${sep}foo${sep}shared${sep}workflow.js` ) ).toBe( true );
98
+ } );
99
+
100
+ it( 'rejects workflow.js not under shared', () => {
101
+ expect( staticMatchers.workflowPathHasShared( `${sep}foo${sep}workflow.js` ) ).toBe( false );
102
+ } );
103
+ } );
104
+
105
+ describe( 'sharedStepsDir', () => {
106
+ it( 'matches .js files inside shared/steps/', () => {
107
+ expect( staticMatchers.sharedStepsDir( `${sep}app${sep}dist${sep}shared${sep}steps${sep}tools.js` ) ).toBe( true );
108
+ } );
109
+
110
+ it( 'matches .js files in nested subdirectories of shared/steps/', () => {
111
+ expect( staticMatchers.sharedStepsDir( `${sep}app${sep}dist${sep}shared${sep}steps${sep}utils${sep}helper.js` ) ).toBe( true );
112
+ } );
113
+
114
+ it( 'rejects .ts files inside shared/steps/', () => {
115
+ expect( staticMatchers.sharedStepsDir( `${sep}app${sep}src${sep}shared${sep}steps${sep}tools.ts` ) ).toBe( false );
116
+ } );
117
+
118
+ it( 'rejects non-.js files inside shared/steps/', () => {
119
+ expect( staticMatchers.sharedStepsDir( `${sep}app${sep}dist${sep}shared${sep}steps${sep}readme.md` ) ).toBe( false );
120
+ } );
121
+ } );
122
+
123
+ describe( 'sharedEvaluatorsDir', () => {
124
+ it( 'matches .js files inside shared/evaluators/', () => {
125
+ expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}dist${sep}shared${sep}evaluators${sep}quality.js` ) ).toBe( true );
126
+ } );
127
+
128
+ it( 'matches .js files in nested subdirectories of shared/evaluators/', () => {
129
+ expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}dist${sep}shared${sep}evaluators${sep}utils${sep}helper.js` ) ).toBe( true );
130
+ } );
131
+
132
+ it( 'rejects .ts files inside shared/evaluators/', () => {
133
+ expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}src${sep}shared${sep}evaluators${sep}quality.ts` ) ).toBe( false );
134
+ } );
135
+
136
+ it( 'rejects non-.js files inside shared/evaluators/', () => {
137
+ expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}dist${sep}shared${sep}evaluators${sep}readme.md` ) ).toBe( false );
138
+ } );
139
+ } );
140
+ } );
@@ -1,8 +1,9 @@
1
- import { dirname, resolve, sep } from 'path';
2
- import { pathToFileURL } from 'url';
1
+ import { join, dirname, resolve } from 'node:path';
2
+ import { fileURLToPath, pathToFileURL } from 'node:url';
3
3
  import { METADATA_ACCESS_SYMBOL } from '#consts';
4
- import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from 'fs';
4
+ import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, mkdirSync, writeFileSync } from 'node:fs';
5
5
  import { hashElement } from 'folder-hash';
6
+ import { staticMatchers } from './matchers.js';
6
7
 
7
8
  /**
8
9
  * Returns the real path for symlink
@@ -20,69 +21,6 @@ export const resolveSymlink = link => {
20
21
  }
21
22
  };
22
23
 
23
- /**
24
- * Returns matchers that need to be built using a relative path
25
- *
26
- * @param {string} path
27
- * @returns {object} The object containing the matchers
28
- */
29
- export const activityMatchersBuilder = path => ( {
30
- /**
31
- * Matches a file called steps.js, located at the path
32
- * @param {string} path - Path to test
33
- * @returns {boolean}
34
- */
35
- stepsFile: v => v === `${path}${sep}steps.js`,
36
- /**
37
- * Matches a file called evaluators.js, located at the path
38
- * @param {string} path - Path to test
39
- * @returns {boolean}
40
- */
41
- evaluatorsFile: v => v === `${path}${sep}evaluators.js`,
42
- /**
43
- * Matches all files on any levels inside a folder called steps/, located at the path
44
- * @param {string} path - Path to test
45
- * @returns {boolean}
46
- */
47
- stepsDir: v => v.startsWith( `${path}${sep}steps${sep}` ),
48
- /**
49
- * Matches all files on any levels inside a folder called evaluators/, located at the path
50
- * @param {string} path - Path to test
51
- * @returns {boolean}
52
- */
53
- evaluatorsDir: v => v.startsWith( `${path}${sep}evaluators${sep}` )
54
- } );
55
-
56
- /**
57
- * Matchers that can be used to access conditions without initializing them
58
- */
59
- export const staticMatchers = {
60
- /**
61
- * Matches a workflow.js file
62
- * @param {string} path - Path to test
63
- * @returns {boolean}
64
- */
65
- workflowFile: v => v.endsWith( `${sep}workflow.js` ),
66
- /**
67
- * Matches a workflow.js that is inside a shared folder: eg foo/shared/workflow.js
68
- * @param {string} path - Path to test
69
- * @returns {boolean}
70
- */
71
- workflowPathHasShared: v => v.endsWith( `${sep}shared${sep}workflow.js` ),
72
- /**
73
- * Matches the shared folder for steps src/shared/steps/../step_file.js
74
- * @param {string} path - Path to test
75
- * @returns {boolean}
76
- */
77
- sharedStepsDir: v => v.includes( `${sep}shared${sep}steps${sep}` ) && v.endsWith( '.js' ),
78
- /**
79
- * Matches the shared folder for evaluators src/shared/evaluators/../evaluator_file.js
80
- * @param {string} path - Path to test
81
- * @returns {boolean}
82
- */
83
- sharedEvaluatorsDir: v => v.includes( `${sep}shared${sep}evaluators${sep}` ) && v.endsWith( '.js' )
84
- };
85
-
86
24
  /**
87
25
  * @typedef {object} File
88
26
  * @property {string} path - The file path
@@ -314,7 +252,7 @@ export const hashSourceCode = async rootDir => {
314
252
  try {
315
253
  const { hash } = await hashElement( rootDir, {
316
254
  folders: {
317
- exclude: [ '.*', 'node_modules', 'test_coverage', 'vendor', 'test' ],
255
+ exclude: [ '.*', 'node_modules', 'test_coverage', 'vendor', 'test', 'logs', 'dist' ],
318
256
  ignoreRootName: true
319
257
  },
320
258
  files: {
@@ -327,3 +265,17 @@ export const hashSourceCode = async rootDir => {
327
265
  throw new Error( `Error calculating hash from "${error}": ${error.message}`, { cause: error } );
328
266
  }
329
267
  };
268
+
269
+ /**
270
+ * Creates a file in the temp folder and returns its path
271
+ * @returns {string} Folder
272
+ */
273
+ export const writeFileInTempDir = ( content, name ) => {
274
+ const here = dirname( fileURLToPath( import.meta.url ) );
275
+ const tempDir = join( here, '..', 'temp' );
276
+ mkdirSync( tempDir, { recursive: true } );
277
+
278
+ const filename = join( tempDir, name );
279
+ writeFileSync( filename, content, 'utf-8' );
280
+ return filename;
281
+ };