@outputai/core 0.10.0 → 0.10.1-next.09ed166.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 (45) hide show
  1. package/package.json +1 -1
  2. package/src/bus.js +58 -20
  3. package/src/bus.spec.js +135 -77
  4. package/src/consts.js +2 -1
  5. package/src/helpers/object.js +48 -35
  6. package/src/helpers/object.spec.js +123 -75
  7. package/src/hooks/index.d.ts +48 -45
  8. package/src/hooks/index.js +49 -44
  9. package/src/hooks/index.spec.js +74 -102
  10. package/src/interface/workflow.js +92 -85
  11. package/src/interface/workflow.spec.js +135 -142
  12. package/src/sdk/helpers/objects.d.ts +24 -19
  13. package/src/sdk/runtime/events.d.ts +4 -6
  14. package/src/sdk/runtime/events.js +2 -15
  15. package/src/sdk/runtime/events.spec.js +14 -92
  16. package/src/worker/global_functions.js +2 -2
  17. package/src/worker/global_functions.spec.js +3 -3
  18. package/src/worker/index.js +3 -3
  19. package/src/worker/index.spec.js +6 -6
  20. package/src/worker/interceptors/activity.js +4 -4
  21. package/src/worker/interceptors/activity.spec.js +9 -9
  22. package/src/worker/interceptors/workflow.js +5 -5
  23. package/src/worker/interceptors/workflow.spec.js +7 -7
  24. package/src/worker/loader/activities.js +56 -40
  25. package/src/worker/loader/activities.spec.js +21 -22
  26. package/src/worker/loader/workflows.spec.js +1 -2
  27. package/src/worker/log_hooks.js +9 -9
  28. package/src/worker/log_hooks.spec.js +2 -2
  29. package/src/worker/sinks.js +8 -8
  30. package/src/worker/sinks.spec.js +9 -9
  31. package/src/worker/webpack_loaders/consts.js +6 -0
  32. package/src/worker/webpack_loaders/tools.js +1 -146
  33. package/src/worker/webpack_loaders/tools.spec.js +0 -23
  34. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +24 -32
  35. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +82 -279
  36. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +6 -7
  37. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +57 -119
  38. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.js +88 -0
  39. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +165 -0
  40. package/src/worker/webpack_loaders/workflow_validator/index.mjs +96 -29
  41. package/src/worker/webpack_loaders/workflow_validator/index.spec.js +110 -583
  42. package/src/worker/webpack_loaders/{npm_workflow_export_resolve.js → workflow_validator/npm_workflow_export_resolve.js} +1 -1
  43. package/src/worker/webpack_loaders/{npm_workflow_export_resolve.spec.js → workflow_validator/npm_workflow_export_resolve.spec.js} +1 -1
  44. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +0 -193
  45. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +0 -123
@@ -1,9 +1,18 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, it, expect, vi } from 'vitest';
2
2
  import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import loader from './index.mjs';
6
6
 
7
+ const { invokeActivitySymbolKey } = vi.hoisted( () => ( {
8
+ invokeActivitySymbolKey: 'test:invoke_activity'
9
+ } ) );
10
+
11
+ vi.mock( '#consts', async importOriginal => ( {
12
+ ...await importOriginal(),
13
+ INVOKE_ACTIVITY_SYMBOL: Symbol.for( invokeActivitySymbolKey )
14
+ } ) );
15
+
7
16
  function runLoader( source, resourcePath ) {
8
17
  return new Promise( ( resolve, reject ) => {
9
18
  const ctx = {
@@ -16,8 +25,12 @@ function runLoader( source, resourcePath ) {
16
25
  } );
17
26
  }
18
27
 
28
+ const escapeRegExp = value => value.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
29
+ const invokeActivityPattern = activityName =>
30
+ new RegExp( `globalThis\\[globalThis\\.Symbol\\.for\\(([\"'])${escapeRegExp( invokeActivitySymbolKey )}\\1\\)\\]\\('${activityName}'` );
31
+
19
32
  describe( 'workflows_rewriter Webpack loader spec', () => {
20
- it( 'rewrites ESM step imports and leaves workflow calls intact', async () => {
33
+ it( 'rewrites ESM activity imports and leaves workflow imports intact', async () => {
21
34
  const dir = mkdtempSync( join( tmpdir(), 'ast-loader-esm-' ) );
22
35
  writeFileSync( join( dir, 'steps.js' ), 'export const StepA = step({ name: \'step.a\' });' );
23
36
  writeFileSync( join( dir, 'workflow.js' ), `
@@ -29,7 +42,7 @@ import { StepA } from './steps.js';
29
42
  import FlowDef, { FlowA } from './workflow.js';
30
43
 
31
44
  const obj = {
32
- fn: async (x) => {
45
+ fn: async x => {
33
46
  StepA(1);
34
47
  FlowA(2);
35
48
  FlowDef(3);
@@ -40,65 +53,14 @@ const obj = {
40
53
 
41
54
  expect( code ).not.toMatch( /from '\.\/steps\.js'/ );
42
55
  expect( code ).toMatch( /from '\.\/workflow\.js'/ );
43
- expect( code ).toMatch( /fn:\s*async function \(x\)/ );
44
- expect( code ).toMatch( /this\.invokeStep\('step\.a',\s*1\)/ );
56
+ expect( code ).toMatch( invokeActivityPattern( 'step\\.a' ) );
57
+ expect( code ).toMatch( /,\s*1\)/ );
45
58
  expect( code ).toMatch( /FlowA\(2\)/ );
46
59
  expect( code ).toMatch( /FlowDef\(3\)/ );
47
-
48
- rmSync( dir, { recursive: true, force: true } );
49
- } );
50
-
51
- it( 'rewrites ESM shared steps imports to invokeSharedStep', async () => {
52
- // Create directory structure: shared/steps/common.js
53
- const dir = mkdtempSync( join( tmpdir(), 'ast-loader-esm-shared-' ) );
54
- mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
55
- mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
56
- writeFileSync( join( dir, 'shared', 'steps', 'common.js' ), 'export const SharedA = step({ name: \'shared.a\' });' );
57
-
58
- const source = `
59
- import { SharedA } from '../../shared/steps/common.js';
60
-
61
- const obj = {
62
- fn: async (x) => {
63
- SharedA(1);
64
- }
65
- }`;
66
-
67
- const { code } = await runLoader( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
68
-
69
- expect( code ).not.toMatch( /from '\.\.\/\.\.\/shared\/steps\/common\.js'/ );
70
- expect( code ).toMatch( /fn:\s*async function \(x\)/ );
71
- expect( code ).toMatch( /this\.invokeSharedStep\('shared\.a',\s*1\)/ );
72
-
73
60
  rmSync( dir, { recursive: true, force: true } );
74
61
  } );
75
62
 
76
- it( 'rewrites CJS shared steps requires to invokeSharedStep', async () => {
77
- // Create directory structure: shared/steps/common.js
78
- const dir = mkdtempSync( join( tmpdir(), 'ast-loader-cjs-shared-' ) );
79
- mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
80
- mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
81
- writeFileSync( join( dir, 'shared', 'steps', 'common.js' ), 'export const SharedB = step({ name: \'shared.b\' });' );
82
-
83
- const source = `
84
- const { SharedB } = require( '../../shared/steps/common.js' );
85
-
86
- const obj = {
87
- fn: async (y) => {
88
- SharedB();
89
- }
90
- }`;
91
-
92
- const { code } = await runLoader( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
93
-
94
- expect( code ).not.toMatch( /require\('\.\.\/\.\.\/shared\/steps\/common\.js'\)/ );
95
- expect( code ).toMatch( /fn:\s*async function \(y\)/ );
96
- expect( code ).toMatch( /this\.invokeSharedStep\('shared\.b'\)/ );
97
-
98
- rmSync( dir, { recursive: true, force: true } );
99
- } );
100
-
101
- it( 'rewrites CJS step requires and leaves workflow calls intact', async () => {
63
+ it( 'rewrites CJS activity requires and leaves workflow requires intact', async () => {
102
64
  const dir = mkdtempSync( join( tmpdir(), 'ast-loader-cjs-' ) );
103
65
  writeFileSync( join( dir, 'steps.js' ), 'export const StepB = step({ name: \'step.b\' });' );
104
66
  writeFileSync( join( dir, 'workflow.js' ), 'export default workflow({ name: \'flow.c\' });' );
@@ -108,8 +70,8 @@ const { StepB } = require( './steps.js' );
108
70
  const FlowDefault = require( './workflow.js' );
109
71
 
110
72
  const obj = {
111
- fn: async (y) => {
112
- StepB();
73
+ fn: async y => {
74
+ StepB(y);
113
75
  FlowDefault();
114
76
  }
115
77
  }`;
@@ -118,97 +80,73 @@ const obj = {
118
80
 
119
81
  expect( code ).not.toMatch( /require\('\.\/steps\.js'\)/ );
120
82
  expect( code ).toMatch( /require\('\.\/workflow\.js'\)/ );
121
- expect( code ).toMatch( /fn:\s*async function \(y\)/ );
122
- expect( code ).toMatch( /this\.invokeStep\('step\.b'\)/ );
83
+ expect( code ).toMatch( invokeActivityPattern( 'step\\.b' ) );
84
+ expect( code ).toMatch( /,\s*y\)/ );
123
85
  expect( code ).toMatch( /FlowDefault\(\)/ );
124
-
125
86
  rmSync( dir, { recursive: true, force: true } );
126
87
  } );
127
88
 
128
- it( 'resolves top-level const name variables', async () => {
129
- const dir = mkdtempSync( join( tmpdir(), 'ast-loader-const-' ) );
130
- writeFileSync( join( dir, 'steps.js' ), `
131
- const NAME = 'step.const';
132
- export const StepC = step({ name: NAME });` );
133
- writeFileSync( join( dir, 'workflow.js' ), `
134
- const WF = 'wf.const';
135
- export const FlowC = workflow({ name: WF });
136
- const D = 'wf.def';
137
- export default workflow({ name: D });` );
138
-
139
- const source = `
140
- import { StepC } from './steps.js';
141
- import FlowDef, { FlowC } from './workflow.js';
142
- const obj = { fn: async () => { StepC(); FlowC(); FlowDef(); } }`;
143
-
144
- const { code } = await runLoader( source, join( dir, 'file.js' ) );
145
- expect( code ).toMatch( /this\.invokeStep\('step\.const'\)/ );
146
- expect( code ).toMatch( /FlowC\(\)/ );
147
- expect( code ).toMatch( /FlowDef\(\)/ );
148
- rmSync( dir, { recursive: true, force: true } );
149
- } );
150
-
151
- it( 'rewrites ESM shared evaluator imports to invokeSharedEvaluator', async () => {
152
- const dir = mkdtempSync( join( tmpdir(), 'ast-loader-esm-shared-eval-' ) );
89
+ it( 'rewrites shared activity imports from helper modules', async () => {
90
+ const dir = mkdtempSync( join( tmpdir(), 'ast-loader-shared-helper-' ) );
153
91
  mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
154
92
  mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
155
- writeFileSync( join( dir, 'shared', 'evaluators', 'common.js' ), 'export const SharedEval = evaluator({ name: \'shared.eval\' });' );
93
+ writeFileSync(
94
+ join( dir, 'shared', 'evaluators', 'common.js' ),
95
+ 'export const SharedEval = evaluator({ name: \'shared.eval\' });'
96
+ );
156
97
 
157
98
  const source = `
158
99
  import { SharedEval } from '../../shared/evaluators/common.js';
159
100
 
160
- const obj = {
161
- fn: async (x) => {
162
- SharedEval(1);
163
- }
164
- }`;
165
-
166
- const { code } = await runLoader( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
101
+ export const helper = async value => SharedEval(value);`;
167
102
 
168
- expect( code ).not.toMatch( /from '\.\.\/\.\.\/shared\/evaluators\/common\.js'/ );
169
- expect( code ).toMatch( /fn:\s*async function \(x\)/ );
170
- expect( code ).toMatch( /this\.invokeSharedEvaluator\('shared\.eval',\s*1\)/ );
103
+ const { code } = await runLoader( source, join( dir, 'workflows', 'my_workflow', 'helper.js' ) );
171
104
 
105
+ expect( code ).not.toMatch( /shared\/evaluators\/common\.js/ );
106
+ expect( code ).toMatch( invokeActivityPattern( 'shared\\.eval' ) );
107
+ expect( code ).toMatch( /,\s*value\)/ );
108
+ expect( code ).toMatch( /export const helper = async value =>/ );
172
109
  rmSync( dir, { recursive: true, force: true } );
173
110
  } );
174
111
 
175
- it( 'rewrites CJS shared evaluator requires to invokeSharedEvaluator', async () => {
176
- const dir = mkdtempSync( join( tmpdir(), 'ast-loader-cjs-shared-eval-' ) );
177
- mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
178
- mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
179
- writeFileSync( join( dir, 'shared', 'evaluators', 'common.js' ), 'export const SharedEval = evaluator({ name: \'shared.eval\' });' );
112
+ it( 'resolves top-level const activity names', async () => {
113
+ const dir = mkdtempSync( join( tmpdir(), 'ast-loader-const-' ) );
114
+ writeFileSync( join( dir, 'steps.js' ), `
115
+ const NAME = 'step.const';
116
+ export const StepC = step({ name: NAME });` );
180
117
 
181
118
  const source = `
182
- const { SharedEval } = require( '../../shared/evaluators/common.js' );
119
+ import { StepC } from './steps.js';
120
+ const obj = { fn: async () => StepC() };`;
183
121
 
184
- const obj = {
185
- fn: async (y) => {
186
- SharedEval();
187
- }
188
- }`;
122
+ const { code } = await runLoader( source, join( dir, 'file.js' ) );
123
+ expect( code ).toMatch( invokeActivityPattern( 'step\\.const' ) );
124
+ rmSync( dir, { recursive: true, force: true } );
125
+ } );
189
126
 
190
- const { code } = await runLoader( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
127
+ it( 'throws when globalThis is shadowed at a rewritten call site', async () => {
128
+ const dir = mkdtempSync( join( tmpdir(), 'ast-loader-globalthis-shadow-' ) );
129
+ writeFileSync( join( dir, 'steps.js' ), 'export const StepA = step({ name: \'step.shadow\' });' );
191
130
 
192
- expect( code ).not.toMatch( /require\('\.\.\/\.\.\/shared\/evaluators\/common\.js'\)/ );
193
- expect( code ).toMatch( /fn:\s*async function \(y\)/ );
194
- expect( code ).toMatch( /this\.invokeSharedEvaluator\('shared\.eval'\)/ );
131
+ const source = `
132
+ import { StepA } from './steps.js';
133
+ export function helper(globalThis) {
134
+ return StepA();
135
+ }`;
195
136
 
137
+ await expect( runLoader( source, join( dir, 'helper.js' ) ) ).rejects.toThrow( /globalThis.*shadowed/ );
196
138
  rmSync( dir, { recursive: true, force: true } );
197
139
  } );
198
140
 
199
- it( 'throws on non-static name', async () => {
141
+ it( 'propagates errors for non-static activity names', async () => {
200
142
  const dir = mkdtempSync( join( tmpdir(), 'ast-loader-error-' ) );
201
143
  writeFileSync( join( dir, 'steps.js' ), `
202
144
  function n() { return 'x'; }
203
145
  export const StepX = step({ name: n() });` );
204
- writeFileSync( join( dir, 'workflow.js' ), `
205
- const base = 'a';
206
- export default workflow({ name: \`\${base}-b\` });` );
207
146
 
208
147
  const source = `
209
148
  import { StepX } from './steps.js';
210
- import WF from './workflow.js';
211
- const obj = { fn: async () => { StepX(); WF(); } }`;
149
+ const obj = { fn: async () => StepX() };`;
212
150
 
213
151
  await expect( runLoader( source, join( dir, 'file.js' ) ) ).rejects.toThrow( /Invalid step name/ );
214
152
  rmSync( dir, { recursive: true, force: true } );
@@ -0,0 +1,88 @@
1
+ import traverseModule from '@babel/traverse';
2
+ import { INVOKE_ACTIVITY_SYMBOL } from '#consts';
3
+ import {
4
+ callExpression,
5
+ identifier,
6
+ isIdentifier,
7
+ memberExpression,
8
+ stringLiteral
9
+ } from '@babel/types';
10
+
11
+ // Handle CJS/ESM interop for Babel packages when executed as a webpack loader
12
+ const traverse = traverseModule.default ?? traverseModule;
13
+ const invokeActivitySymbolKey = Symbol.keyFor( INVOKE_ACTIVITY_SYMBOL );
14
+
15
+ if ( !invokeActivitySymbolKey ) {
16
+ throw new Error( 'INVOKE_ACTIVITY_SYMBOL must be created with Symbol.for().' );
17
+ }
18
+
19
+ // Only direct function calls are rewritten. Member calls and dynamic callees stay untouched.
20
+ const isIdentifierCallee = cPath => isIdentifier( cPath.node.callee );
21
+
22
+ // Collapse collected activity imports into local identifier -> declared activity name.
23
+ const buildActivityNameMap = activityImports =>
24
+ activityImports.reduce( ( map, { localName, activityName } ) => map.set( localName, activityName ), new Map() );
25
+
26
+ // Ignore calls when the imported binding has been shadowed by a live local binding.
27
+ const hasLiveLocalBinding = ( path, name ) => {
28
+ const binding = path.scope.getBinding( name );
29
+ return binding && !binding.path.removed;
30
+ };
31
+
32
+ const assertGlobalThisIsSafe = path => {
33
+ if ( hasLiveLocalBinding( path, 'globalThis' ) ) {
34
+ throw new Error( 'Cannot rewrite activity call because "globalThis" is shadowed in this scope.' );
35
+ }
36
+ };
37
+
38
+ // Build the generated dispatcher call, preserving the original call arguments.
39
+ const createInvokeActivityCall = ( activityName, args ) =>
40
+ callExpression(
41
+ memberExpression(
42
+ identifier( 'globalThis' ),
43
+ callExpression(
44
+ memberExpression(
45
+ memberExpression( identifier( 'globalThis' ), identifier( 'Symbol' ) ),
46
+ identifier( 'for' )
47
+ ),
48
+ [ stringLiteral( invokeActivitySymbolKey ) ]
49
+ ),
50
+ true
51
+ ),
52
+ [ stringLiteral( activityName ), ...args ]
53
+ );
54
+
55
+ // Rewrite valid function-body activity calls; top-level call validation belongs to the validator loader.
56
+ const rewriteActivityCallsInFunctions = ( { ast, activityNames } ) => {
57
+ const state = { rewrote: false };
58
+
59
+ traverse( ast, {
60
+ CallExpression: path => {
61
+ if ( !path.getFunctionParent() || !isIdentifierCallee( path ) ) {
62
+ return;
63
+ }
64
+
65
+ const callee = path.node.callee;
66
+ const activityName = activityNames.get( callee.name );
67
+ if ( !activityName || hasLiveLocalBinding( path, callee.name ) ) {
68
+ return;
69
+ }
70
+
71
+ assertGlobalThisIsSafe( path );
72
+ path.replaceWith( createInvokeActivityCall( activityName, path.node.arguments ) );
73
+ state.rewrote = true;
74
+ }
75
+ } );
76
+
77
+ return state.rewrote;
78
+ };
79
+
80
+ // Entry point: resolve collected component imports into plain dispatcher calls.
81
+ export default function rewriteActivityCalls( { ast, activityImports } ) {
82
+ const activityNames = buildActivityNameMap( activityImports );
83
+ if ( activityNames.size === 0 ) {
84
+ return false;
85
+ }
86
+
87
+ return rewriteActivityCallsInFunctions( { ast, activityNames } );
88
+ };
@@ -0,0 +1,165 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import generatorModule from '@babel/generator';
3
+ import { parse } from '../tools.js';
4
+ import rewriteActivityCalls from './rewrite_activity_calls.js';
5
+
6
+ const { invokeActivitySymbolKey } = vi.hoisted( () => ( {
7
+ invokeActivitySymbolKey: 'test:invoke_activity'
8
+ } ) );
9
+
10
+ vi.mock( '#consts', async importOriginal => ( {
11
+ ...await importOriginal(),
12
+ INVOKE_ACTIVITY_SYMBOL: Symbol.for( invokeActivitySymbolKey )
13
+ } ) );
14
+
15
+ const generate = generatorModule.default ?? generatorModule;
16
+ const escapeRegExp = value => value.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
17
+ const invokeActivityPattern = activityName =>
18
+ new RegExp( `globalThis\\[globalThis\\.Symbol\\.for\\(([\"'])${escapeRegExp( invokeActivitySymbolKey )}\\1\\)\\]\\(([\"'])${activityName}\\2` );
19
+
20
+ describe( 'rewrite_activity_calls', () => {
21
+ it( 'rewrites step calls inside functions without changing function shape', () => {
22
+ const src = `
23
+ const obj = {
24
+ fn: async x => {
25
+ StepA( 1 );
26
+ FlowB( 2 );
27
+ }
28
+ }`;
29
+ const ast = parse( src, 'file.js' );
30
+ const activityImports = [ { localName: 'StepA', activityName: 'step.a' } ];
31
+
32
+ const rewrote = rewriteActivityCalls( { ast, activityImports } );
33
+ expect( rewrote ).toBe( true );
34
+
35
+ const { code } = generate( ast, { quotes: 'single' } );
36
+ expect( code ).not.toMatch( /@outputai\/core\/invoker/ );
37
+ expect( code ).toMatch( /fn:\s*async x =>/ );
38
+ expect( code ).toMatch( invokeActivityPattern( 'step\\.a' ) );
39
+ expect( code ).toMatch( /,\s*1\)/ );
40
+ expect( code ).toMatch( /FlowB\(2\)/ );
41
+ } );
42
+
43
+ it( 'rewrites evaluator calls to the global activity dispatcher', () => {
44
+ const src = `
45
+ const obj = {
46
+ fn: async x => {
47
+ EvalA(3);
48
+ }
49
+ };`;
50
+ const ast = parse( src, 'file.js' );
51
+ const activityImports = [ { localName: 'EvalA', activityName: 'eval.a' } ];
52
+ const rewrote = rewriteActivityCalls( { ast, activityImports } );
53
+ expect( rewrote ).toBe( true );
54
+ const { code } = generate( ast, { quotes: 'single' } );
55
+ expect( code ).toMatch( invokeActivityPattern( 'eval\\.a' ) );
56
+ expect( code ).toMatch( /,\s*3\)/ );
57
+ } );
58
+
59
+ it( 'does nothing when no matching calls are present', () => {
60
+ const src = [ 'const obj = { fn: function() { other(); } }' ].join( '\n' );
61
+ const ast = parse( src, 'file.js' );
62
+ const rewrote = rewriteActivityCalls( { ast, activityImports: [] } );
63
+ expect( rewrote ).toBe( false );
64
+ } );
65
+
66
+ it( 'rewrites imported activity calls in any function body', () => {
67
+ const src = `
68
+ const foo = async () => {
69
+ StepA( 1 );
70
+ };
71
+
72
+ function bar( x ) {
73
+ EvalA( x );
74
+ }
75
+
76
+ const obj = {
77
+ fn: async x => {
78
+ foo();
79
+ bar( 2 );
80
+ }
81
+ }`;
82
+
83
+ const ast = parse( src, 'file.js' );
84
+ const activityImports = [
85
+ { localName: 'StepA', activityName: 'step.a' },
86
+ { localName: 'EvalA', activityName: 'eval.a' }
87
+ ];
88
+
89
+ const rewrote = rewriteActivityCalls( { ast, activityImports } );
90
+ expect( rewrote ).toBe( true );
91
+
92
+ const { code } = generate( ast, { quotes: 'single' } );
93
+
94
+ expect( code ).toMatch( invokeActivityPattern( 'step\\.a' ) );
95
+ expect( code ).toMatch( invokeActivityPattern( 'eval\\.a' ) );
96
+ expect( code ).toMatch( /,\s*1\)/ );
97
+ expect( code ).toMatch( /,\s*x\)/ );
98
+ expect( code ).toMatch( /foo\(\)/ );
99
+ expect( code ).toMatch( /bar\(2\)/ );
100
+ expect( code ).toMatch( /const foo = async \(\) =>/ );
101
+ } );
102
+
103
+ it( 'does not rewrite top-level calls', () => {
104
+ const src = `
105
+ StepA( 1 );
106
+ function helper() {
107
+ StepA( 2 );
108
+ }`;
109
+
110
+ const ast = parse( src, 'file.js' );
111
+ const activityImports = [ { localName: 'StepA', activityName: 'step.a' } ];
112
+ const rewrote = rewriteActivityCalls( { ast, activityImports } );
113
+ expect( rewrote ).toBe( true );
114
+
115
+ const { code } = generate( ast, { quotes: 'single' } );
116
+ expect( code ).toMatch( /StepA\(1\)/ );
117
+ expect( code ).toMatch( invokeActivityPattern( 'step\\.a' ) );
118
+ expect( code ).toMatch( /,\s*2\)/ );
119
+ } );
120
+
121
+ it( 'ignores local invoke activity names because globalThis is explicit', () => {
122
+ const src = `
123
+ const __invokeActivity = () => 'local';
124
+ const _invokeActivity = () => 'local';
125
+ function helper() {
126
+ StepA();
127
+ }`;
128
+ const ast = parse( src, 'file.js' );
129
+ const activityImports = [ { localName: 'StepA', activityName: 'step.a' } ];
130
+ const rewrote = rewriteActivityCalls( { ast, activityImports } );
131
+ expect( rewrote ).toBe( true );
132
+
133
+ const { code } = generate( ast, { quotes: 'single' } );
134
+ expect( code ).not.toMatch( /@outputai\/core\/invoker/ );
135
+ expect( code ).toMatch( /const __invokeActivity = \(\) => 'local'/ );
136
+ expect( code ).toMatch( /const _invokeActivity = \(\) => 'local'/ );
137
+ expect( code ).toMatch( invokeActivityPattern( 'step\\.a' ) );
138
+ } );
139
+
140
+ it( 'reads Symbol through globalThis so local Symbol bindings are safe', () => {
141
+ const src = `
142
+ function helper( Symbol ) {
143
+ StepA();
144
+ }`;
145
+ const ast = parse( src, 'file.js' );
146
+ const activityImports = [ { localName: 'StepA', activityName: 'step.a' } ];
147
+ const rewrote = rewriteActivityCalls( { ast, activityImports } );
148
+ expect( rewrote ).toBe( true );
149
+
150
+ const { code } = generate( ast, { quotes: 'single' } );
151
+ expect( code ).toMatch( /function helper\(Symbol\)/ );
152
+ expect( code ).toMatch( invokeActivityPattern( 'step\\.a' ) );
153
+ } );
154
+
155
+ it( 'throws when globalThis is shadowed at an activity call site', () => {
156
+ const src = `
157
+ function helper( globalThis ) {
158
+ StepA();
159
+ }`;
160
+ const ast = parse( src, 'file.js' );
161
+ const activityImports = [ { localName: 'StepA', activityName: 'step.a' } ];
162
+
163
+ expect( () => rewriteActivityCalls( { ast, activityImports } ) ).toThrow( /globalThis.*shadowed/ );
164
+ } );
165
+ } );