@output.ai/core 0.2.4 → 0.3.0-dev.pr263-8f8e94a

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.
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
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 validatorLoader from './index.mjs';
@@ -17,16 +17,14 @@ function runLoader( filename, source ) {
17
17
  }
18
18
 
19
19
  describe( 'workflow_validator loader', () => {
20
- it( 'workflow.js: allows imports from steps/shared_steps/evaluators/workflow', async () => {
20
+ it( 'workflow.js: allows imports from steps/evaluators/workflow', async () => {
21
21
  const dir = mkdtempSync( join( tmpdir(), 'wf-allow-' ) );
22
22
  writeFileSync( join( dir, 'steps.js' ), 'export const S = step({ name: "s" })\n' );
23
- writeFileSync( join( dir, 'shared_steps.js' ), 'export const SS = step({ name: "ss" })\n' );
24
23
  writeFileSync( join( dir, 'evaluators.js' ), 'export const E = evaluator({ name: "e" })\n' );
25
24
  writeFileSync( join( dir, 'workflow.js' ), 'export const W = workflow({ name: "w" })\n' );
26
25
 
27
26
  const src = [
28
27
  'import { S } from "./steps.js";',
29
- 'import { SS } from "./shared_steps.js";',
30
28
  'import { E } from "./evaluators.js";',
31
29
  'import { W } from "./workflow.js";',
32
30
  'const x = 1;'
@@ -36,6 +34,13 @@ describe( 'workflow_validator loader', () => {
36
34
  rmSync( dir, { recursive: true, force: true } );
37
35
  } );
38
36
 
37
+ it( 'workflow.js: allows imports from any non-component file', async () => {
38
+ const dir = mkdtempSync( join( tmpdir(), 'wf-any-allow-' ) );
39
+ const src = 'import x from "./foo.js";';
40
+ await expect( runLoader( join( dir, 'workflow.js' ), src ) ).resolves.toBeTruthy();
41
+ rmSync( dir, { recursive: true, force: true } );
42
+ } );
43
+
39
44
  it( 'workflow.js: allows imports from @output.ai/core and local_core', async () => {
40
45
  const dir = mkdtempSync( join( tmpdir(), 'wf-allow-external-' ) );
41
46
  const src = [
@@ -47,7 +52,7 @@ describe( 'workflow_validator loader', () => {
47
52
  rmSync( dir, { recursive: true, force: true } );
48
53
  } );
49
54
 
50
- it( 'steps.js: rejects importing steps/shared_steps/evaluators/workflow', async () => {
55
+ it( 'steps.js: rejects importing steps/evaluators/workflow', async () => {
51
56
  const dir = mkdtempSync( join( tmpdir(), 'steps-reject-' ) );
52
57
  const src = 'import { S } from "./steps.js";';
53
58
  await expect( runLoader( join( dir, 'steps.js' ), src ) ).rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
@@ -61,44 +66,34 @@ describe( 'workflow_validator loader', () => {
61
66
  rmSync( dir, { recursive: true, force: true } );
62
67
  } );
63
68
 
64
- it( 'evaluators.js: rejects importing evaluators/steps/shared_steps/workflow', async () => {
69
+ it( 'evaluators.js: rejects importing evaluators/steps/workflow', async () => {
65
70
  const dir = mkdtempSync( join( tmpdir(), 'evals-reject-' ) );
66
71
  const src = 'import { E } from "./evaluators.js";';
67
72
  await expect( runLoader( join( dir, 'evaluators.js' ), src ) ).rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
68
73
  rmSync( dir, { recursive: true, force: true } );
69
74
  } );
70
75
 
71
- it( 'steps.js: rejects calling another step/evaluator/workflow inside fn', async () => {
76
+ it( 'steps.js: rejects calling another step inside fn', async () => {
72
77
  const dir = mkdtempSync( join( tmpdir(), 'steps-call-reject-' ) );
78
+ // Can only test same-type components since cross-type declarations are now blocked by instantiation validation
73
79
  const src = [
74
80
  'const A = step({ name: "a" });',
75
81
  'const B = step({ name: "b" });',
76
82
  'const obj = { fn: function() { B(); } };'
77
83
  ].join( '\n' );
78
84
  await expect( runLoader( join( dir, 'steps.js' ), src ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
79
-
80
- const src2 = [
81
- 'const E = evaluator({ name: "e" });',
82
- 'const obj = { fn: () => { E(); } };'
83
- ].join( '\n' );
84
- await expect( runLoader( join( dir, 'steps.js' ), src2 ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
85
85
  rmSync( dir, { recursive: true, force: true } );
86
86
  } );
87
87
 
88
- it( 'evaluators.js: rejects calling another evaluator/step/workflow inside fn', async () => {
88
+ it( 'evaluators.js: rejects calling another evaluator inside fn', async () => {
89
89
  const dir = mkdtempSync( join( tmpdir(), 'evals-call-reject-' ) );
90
+ // Can only test same-type components since cross-type declarations are now blocked by instantiation validation
90
91
  const src = [
91
92
  'const E1 = evaluator({ name: "e1" });',
92
93
  'const E2 = evaluator({ name: "e2" });',
93
94
  'const obj = { fn: function() { E2(); } };'
94
95
  ].join( '\n' );
95
96
  await expect( runLoader( join( dir, 'evaluators.js' ), src ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
96
-
97
- const src2 = [
98
- 'const S = step({ name: "s" });',
99
- 'const obj = { fn: () => { S(); } };'
100
- ].join( '\n' );
101
- await expect( runLoader( join( dir, 'evaluators.js' ), src2 ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
102
97
  rmSync( dir, { recursive: true, force: true } );
103
98
  } );
104
99
 
@@ -118,10 +113,25 @@ describe( 'workflow_validator loader', () => {
118
113
  rmSync( dir, { recursive: true, force: true } );
119
114
  } );
120
115
 
121
- it( 'steps.js: rejects importing shared_steps/evaluators/workflow variants', async () => {
116
+ it( 'workflow.js: allows require from steps/evaluators/workflow; allows other require', async () => {
117
+ const dir = mkdtempSync( join( tmpdir(), 'wf-req-' ) );
118
+ writeFileSync( join( dir, 'steps.js' ), 'export const S = step({ name: "s" })\n' );
119
+ writeFileSync( join( dir, 'evaluators.js' ), 'export const E = evaluator({ name: "e" })\n' );
120
+ writeFileSync( join( dir, 'workflow.js' ), 'export default workflow({ name: "w" })\n' );
121
+ const ok = [
122
+ 'const { S } = require("./steps.js");',
123
+ 'const { E } = require("./evaluators.js");',
124
+ 'const W = require("./workflow.js");'
125
+ ].join( '\n' );
126
+ await expect( runLoader( join( dir, 'workflow.js' ), ok ) ).resolves.toBeTruthy();
127
+ // Also allow random files (not rejected anymore)
128
+ const ok2 = 'const X = require("./random_file.js");';
129
+ await expect( runLoader( join( dir, 'workflow.js' ), ok2 ) ).resolves.toBeTruthy();
130
+ rmSync( dir, { recursive: true, force: true } );
131
+ } );
132
+
133
+ it( 'steps.js: rejects importing evaluators/workflow variants', async () => {
122
134
  const dir = mkdtempSync( join( tmpdir(), 'steps-reject2-' ) );
123
- await expect( runLoader( join( dir, 'steps.js' ), 'import { SS } from "./shared_steps.js";' ) )
124
- .rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
125
135
  await expect( runLoader( join( dir, 'steps.js' ), 'import { E } from "./evaluators.js";' ) )
126
136
  .rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
127
137
  await expect( runLoader( join( dir, 'steps.js' ), 'import WF from "./workflow.js";' ) )
@@ -129,12 +139,10 @@ describe( 'workflow_validator loader', () => {
129
139
  rmSync( dir, { recursive: true, force: true } );
130
140
  } );
131
141
 
132
- it( 'evaluators.js: rejects importing steps/shared_steps/workflow variants', async () => {
142
+ it( 'evaluators.js: rejects importing steps/workflow variants', async () => {
133
143
  const dir = mkdtempSync( join( tmpdir(), 'evals-reject2-' ) );
134
144
  await expect( runLoader( join( dir, 'evaluators.js' ), 'import { S } from "./steps.js";' ) )
135
145
  .rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
136
- await expect( runLoader( join( dir, 'evaluators.js' ), 'import { SS } from "./shared_steps.js";' ) )
137
- .rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
138
146
  await expect( runLoader( join( dir, 'evaluators.js' ), 'import WF from "./workflow.js";' ) )
139
147
  .rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
140
148
  rmSync( dir, { recursive: true, force: true } );
@@ -149,48 +157,29 @@ describe( 'workflow_validator loader', () => {
149
157
  rmSync( dir, { recursive: true, force: true } );
150
158
  } );
151
159
 
152
- it( 'shared_steps.js: rejects importing steps/shared_steps/evaluators/workflow', async () => {
153
- const dir = mkdtempSync( join( tmpdir(), 'shared-imp-reject-' ) );
154
- await expect( runLoader( join( dir, 'shared_steps.js' ), 'import { S } from "./steps.js";' ) )
155
- .rejects.toThrow( /Invalid (import|imports|dependency) in shared_steps\.js/ );
156
- await expect( runLoader( join( dir, 'shared_steps.js' ), 'import { SS } from "./shared_steps.js";' ) )
157
- .rejects.toThrow( /Invalid (import|imports|dependency) in shared_steps\.js/ );
158
- await expect( runLoader( join( dir, 'shared_steps.js' ), 'import { E } from "./evaluators.js";' ) )
159
- .rejects.toThrow( /Invalid (import|imports|dependency) in shared_steps\.js/ );
160
- await expect( runLoader( join( dir, 'shared_steps.js' ), 'import WF from "./workflow.js";' ) )
161
- .rejects.toThrow( /Invalid (import|imports|dependency) in shared_steps\.js/ );
160
+ it( 'workflow.js: allows importing ./types.js and bare types', async () => {
161
+ const dir = mkdtempSync( join( tmpdir(), 'wf-types-allow-' ) );
162
+ writeFileSync( join( dir, 'types.js' ), 'export const T = {}\n' );
163
+ const src1 = 'import { T } from "./types.js";';
164
+ await expect( runLoader( join( dir, 'workflow.js' ), src1 ) ).resolves.toBeTruthy();
162
165
  rmSync( dir, { recursive: true, force: true } );
163
166
  } );
164
167
 
165
- it( 'shared_steps.js: rejects calling step/evaluator/workflow inside fn', async () => {
166
- const dir = mkdtempSync( join( tmpdir(), 'shared-call-reject-' ) );
167
- const src1 = [
168
- 'const A = step({ name: "a" });',
169
- 'const obj = { fn: function() { A(); } };'
170
- ].join( '\n' );
171
- await expect( runLoader( join( dir, 'shared_steps.js' ), src1 ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
172
-
173
- const src2 = [
174
- 'const E = evaluator({ name: "e" });',
175
- 'const obj = { fn: () => { E(); } };'
176
- ].join( '\n' );
177
- await expect( runLoader( join( dir, 'shared_steps.js' ), src2 ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
178
- rmSync( dir, { recursive: true, force: true } );
179
- } );
180
-
181
- it( 'shared_steps.js: top-level calls outside fn are allowed', async () => {
182
- const dir = mkdtempSync( join( tmpdir(), 'shared-toplevel-allowed-' ) );
183
- const src = [ 'const A = step({ name: "a" });', 'A();' ].join( '\n' );
184
- await expect( runLoader( join( dir, 'shared_steps.js' ), src ) ).resolves.toBeTruthy();
168
+ it( 'workflow.js: allows importing any file (consts/constants/vars/variables/random)', async () => {
169
+ const dir = mkdtempSync( join( tmpdir(), 'wf-extra-allow-' ) );
170
+ const bases = [ 'consts', 'constants', 'vars', 'variables', 'random', 'anything' ];
171
+ for ( const base of bases ) {
172
+ writeFileSync( join( dir, `${base}.js` ), 'export const X = 1\n' );
173
+ const src = `import x from "./${base}.js";`;
174
+ await expect( runLoader( join( dir, 'workflow.js' ), src ) ).resolves.toBeTruthy();
175
+ }
185
176
  rmSync( dir, { recursive: true, force: true } );
186
177
  } );
187
178
 
188
- it( 'steps.js: rejects require of steps/shared_steps/evaluators/workflow; allows other require', async () => {
179
+ it( 'steps.js: rejects require of steps/evaluators/workflow; allows other require', async () => {
189
180
  const dir = mkdtempSync( join( tmpdir(), 'steps-require-' ) );
190
181
  await expect( runLoader( join( dir, 'steps.js' ), 'const { S } = require("./steps.js");' ) )
191
182
  .rejects.toThrow( /Invalid (require|dependency) in steps\.js/ );
192
- await expect( runLoader( join( dir, 'steps.js' ), 'const { SS } = require("./shared_steps.js");' ) )
193
- .rejects.toThrow( /Invalid (require|dependency) in steps\.js/ );
194
183
  await expect( runLoader( join( dir, 'steps.js' ), 'const { E } = require("./evaluators.js");' ) )
195
184
  .rejects.toThrow( /Invalid (require|dependency) in steps\.js/ );
196
185
  await expect( runLoader( join( dir, 'steps.js' ), 'const W = require("./workflow.js");' ) )
@@ -200,16 +189,394 @@ describe( 'workflow_validator loader', () => {
200
189
  rmSync( dir, { recursive: true, force: true } );
201
190
  } );
202
191
 
203
- it( 'evaluators.js: rejects require of steps/shared_steps/workflow; allows other require', async () => {
192
+ it( 'evaluators.js: rejects require of steps/workflow; allows other require', async () => {
204
193
  const dir = mkdtempSync( join( tmpdir(), 'evals-require-' ) );
205
194
  await expect( runLoader( join( dir, 'evaluators.js' ), 'const { S } = require("./steps.js");' ) )
206
195
  .rejects.toThrow( /Invalid (require|dependency) in evaluators\.js/ );
207
- await expect( runLoader( join( dir, 'evaluators.js' ), 'const { SS } = require("./shared_steps.js");' ) )
208
- .rejects.toThrow( /Invalid (require|dependency) in evaluators\.js/ );
209
196
  await expect( runLoader( join( dir, 'evaluators.js' ), 'const W = require("./workflow.js");' ) )
210
197
  .rejects.toThrow( /Invalid (require|dependency) in evaluators\.js/ );
211
198
  const ok = 'const util = require("./util.js");';
212
199
  await expect( runLoader( join( dir, 'evaluators.js' ), ok ) ).resolves.toBeTruthy();
213
200
  rmSync( dir, { recursive: true, force: true } );
214
201
  } );
202
+
203
+ // =====================================================
204
+ // Folder-based utilities and shared directory
205
+ // =====================================================
206
+
207
+ describe( 'folder-based utilities', () => {
208
+ it( 'workflow.js: allows imports from ./utils/helper.js (folder-based utils)', async () => {
209
+ const dir = mkdtempSync( join( tmpdir(), 'wf-folder-utils-' ) );
210
+ mkdirSync( join( dir, 'utils' ) );
211
+ writeFileSync( join( dir, 'utils', 'helper.js' ), 'export const helper = () => 1;\n' );
212
+ const src = 'import { helper } from "./utils/helper.js";';
213
+ await expect( runLoader( join( dir, 'workflow.js' ), src ) ).resolves.toBeTruthy();
214
+ rmSync( dir, { recursive: true, force: true } );
215
+ } );
216
+
217
+ it( 'workflow.js: allows imports from ./clients/redis.js (folder-based clients)', async () => {
218
+ const dir = mkdtempSync( join( tmpdir(), 'wf-folder-clients-' ) );
219
+ mkdirSync( join( dir, 'clients' ) );
220
+ writeFileSync( join( dir, 'clients', 'redis.js' ), 'export const client = {};\n' );
221
+ const src = 'import { client } from "./clients/redis.js";';
222
+ await expect( runLoader( join( dir, 'workflow.js' ), src ) ).resolves.toBeTruthy();
223
+ rmSync( dir, { recursive: true, force: true } );
224
+ } );
225
+
226
+ it( 'steps/fetch_data.js: allows imports from ../utils/http.js', async () => {
227
+ const dir = mkdtempSync( join( tmpdir(), 'steps-folder-utils-' ) );
228
+ mkdirSync( join( dir, 'steps' ) );
229
+ mkdirSync( join( dir, 'utils' ) );
230
+ writeFileSync( join( dir, 'utils', 'http.js' ), 'export const get = () => {};\n' );
231
+ const src = 'import { get } from "../utils/http.js";';
232
+ await expect( runLoader( join( dir, 'steps', 'fetch_data.js' ), src ) ).resolves.toBeTruthy();
233
+ rmSync( dir, { recursive: true, force: true } );
234
+ } );
235
+
236
+ it( 'evaluators/quality.js: allows imports from ../utils/metrics.js', async () => {
237
+ const dir = mkdtempSync( join( tmpdir(), 'evals-folder-utils-' ) );
238
+ mkdirSync( join( dir, 'evaluators' ) );
239
+ mkdirSync( join( dir, 'utils' ) );
240
+ writeFileSync( join( dir, 'utils', 'metrics.js' ), 'export const compute = () => {};\n' );
241
+ const src = 'import { compute } from "../utils/metrics.js";';
242
+ await expect( runLoader( join( dir, 'evaluators', 'quality.js' ), src ) ).resolves.toBeTruthy();
243
+ rmSync( dir, { recursive: true, force: true } );
244
+ } );
245
+ } );
246
+
247
+ describe( 'shared directory imports', () => {
248
+ it( 'workflow.js: allows imports from ../../shared/utils/keys.js', async () => {
249
+ const dir = mkdtempSync( join( tmpdir(), 'wf-shared-utils-' ) );
250
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
251
+ mkdirSync( join( dir, 'shared', 'utils' ), { recursive: true } );
252
+ writeFileSync( join( dir, 'shared', 'utils', 'keys.js' ), 'export const KEY = "test";\n' );
253
+ const src = 'import { KEY } from "../../shared/utils/keys.js";';
254
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'workflow.js' ), src ) ).resolves.toBeTruthy();
255
+ rmSync( dir, { recursive: true, force: true } );
256
+ } );
257
+
258
+ it( 'workflow.js: allows imports from ../../shared/steps/common.js', async () => {
259
+ const dir = mkdtempSync( join( tmpdir(), 'wf-shared-steps-' ) );
260
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
261
+ mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
262
+ writeFileSync( join( dir, 'shared', 'steps', 'common.js' ), 'export const commonStep = step({ name: "common" });\n' );
263
+ const src = 'import { commonStep } from "../../shared/steps/common.js";';
264
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'workflow.js' ), src ) ).resolves.toBeTruthy();
265
+ rmSync( dir, { recursive: true, force: true } );
266
+ } );
267
+
268
+ it( 'workflow.js: allows imports from ../../shared/evaluators/quality.js', async () => {
269
+ const dir = mkdtempSync( join( tmpdir(), 'wf-shared-evals-' ) );
270
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
271
+ mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
272
+ writeFileSync( join( dir, 'shared', 'evaluators', 'quality.js' ), 'export const qualityEval = evaluator({ name: "quality" });\n' );
273
+ const src = 'import { qualityEval } from "../../shared/evaluators/quality.js";';
274
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'workflow.js' ), src ) ).resolves.toBeTruthy();
275
+ rmSync( dir, { recursive: true, force: true } );
276
+ } );
277
+
278
+ it( 'workflow.js: allows imports from ../../shared/clients/pokeapi.js', async () => {
279
+ const dir = mkdtempSync( join( tmpdir(), 'wf-shared-clients-' ) );
280
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
281
+ mkdirSync( join( dir, 'shared', 'clients' ), { recursive: true } );
282
+ writeFileSync( join( dir, 'shared', 'clients', 'pokeapi.js' ), 'export const getPokemon = () => {};\n' );
283
+ const src = 'import { getPokemon } from "../../shared/clients/pokeapi.js";';
284
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'workflow.js' ), src ) ).resolves.toBeTruthy();
285
+ rmSync( dir, { recursive: true, force: true } );
286
+ } );
287
+
288
+ it( 'steps.ts: allows imports from ../../shared/utils/keys.js', async () => {
289
+ const dir = mkdtempSync( join( tmpdir(), 'steps-shared-utils-' ) );
290
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
291
+ mkdirSync( join( dir, 'shared', 'utils' ), { recursive: true } );
292
+ writeFileSync( join( dir, 'shared', 'utils', 'keys.js' ), 'export const KEY = "test";\n' );
293
+ const src = 'import { KEY } from "../../shared/utils/keys.js";';
294
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'steps.js' ), src ) ).resolves.toBeTruthy();
295
+ rmSync( dir, { recursive: true, force: true } );
296
+ } );
297
+
298
+ it( 'steps.ts: allows imports from ../../shared/clients/redis.js', async () => {
299
+ const dir = mkdtempSync( join( tmpdir(), 'steps-shared-clients-' ) );
300
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
301
+ mkdirSync( join( dir, 'shared', 'clients' ), { recursive: true } );
302
+ writeFileSync( join( dir, 'shared', 'clients', 'redis.js' ), 'export const client = {};\n' );
303
+ const src = 'import { client } from "../../shared/clients/redis.js";';
304
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'steps.js' ), src ) ).resolves.toBeTruthy();
305
+ rmSync( dir, { recursive: true, force: true } );
306
+ } );
307
+
308
+ it( 'evaluators.ts: allows imports from ../../shared/utils/helpers.js', async () => {
309
+ const dir = mkdtempSync( join( tmpdir(), 'evals-shared-utils-' ) );
310
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
311
+ mkdirSync( join( dir, 'shared', 'utils' ), { recursive: true } );
312
+ writeFileSync( join( dir, 'shared', 'utils', 'helpers.js' ), 'export const helper = () => 1;\n' );
313
+ const src = 'import { helper } from "../../shared/utils/helpers.js";';
314
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'evaluators.js' ), src ) ).resolves.toBeTruthy();
315
+ rmSync( dir, { recursive: true, force: true } );
316
+ } );
317
+ } );
318
+
319
+ describe( 'activity isolation - shared imports', () => {
320
+ it( 'steps.ts: rejects imports from ../../shared/steps/common.js (activity isolation)', async () => {
321
+ const dir = mkdtempSync( join( tmpdir(), 'steps-shared-steps-reject-' ) );
322
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
323
+ mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
324
+ writeFileSync( join( dir, 'shared', 'steps', 'common.js' ), 'export const commonStep = step({ name: "common" });\n' );
325
+ const src = 'import { commonStep } from "../../shared/steps/common.js";';
326
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'steps.js' ), src ) )
327
+ .rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
328
+ rmSync( dir, { recursive: true, force: true } );
329
+ } );
330
+
331
+ it( 'steps.ts: rejects imports from ../../shared/evaluators/quality.js (activity isolation)', async () => {
332
+ const dir = mkdtempSync( join( tmpdir(), 'steps-shared-evals-reject-' ) );
333
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
334
+ mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
335
+ writeFileSync( join( dir, 'shared', 'evaluators', 'quality.js' ), 'export const qualityEval = evaluator({ name: "quality" });\n' );
336
+ const src = 'import { qualityEval } from "../../shared/evaluators/quality.js";';
337
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'steps.js' ), src ) )
338
+ .rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
339
+ rmSync( dir, { recursive: true, force: true } );
340
+ } );
341
+
342
+ it( 'evaluators.ts: rejects imports from ../../shared/steps/common.js (activity isolation)', async () => {
343
+ const dir = mkdtempSync( join( tmpdir(), 'evals-shared-steps-reject-' ) );
344
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
345
+ mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
346
+ writeFileSync( join( dir, 'shared', 'steps', 'common.js' ), 'export const commonStep = step({ name: "common" });\n' );
347
+ const src = 'import { commonStep } from "../../shared/steps/common.js";';
348
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'evaluators.js' ), src ) )
349
+ .rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
350
+ rmSync( dir, { recursive: true, force: true } );
351
+ } );
352
+
353
+ it( 'evaluators.ts: rejects imports from ../../shared/evaluators/other.js (activity isolation)', async () => {
354
+ const dir = mkdtempSync( join( tmpdir(), 'evals-shared-evals-reject-' ) );
355
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
356
+ mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
357
+ writeFileSync( join( dir, 'shared', 'evaluators', 'other.js' ), 'export const otherEval = evaluator({ name: "other" });\n' );
358
+ const src = 'import { otherEval } from "../../shared/evaluators/other.js";';
359
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'evaluators.js' ), src ) )
360
+ .rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
361
+ rmSync( dir, { recursive: true, force: true } );
362
+ } );
363
+ } );
364
+
365
+ describe( 'workflow import scope', () => {
366
+ it( 'workflow.js: allows imports from other workflow directories (cross-workflow)', async () => {
367
+ const dir = mkdtempSync( join( tmpdir(), 'wf-cross-allow-' ) );
368
+ mkdirSync( join( dir, 'workflows', 'workflow_a' ), { recursive: true } );
369
+ mkdirSync( join( dir, 'workflows', 'workflow_b' ), { recursive: true } );
370
+ writeFileSync( join( dir, 'workflows', 'workflow_b', 'steps.js' ), 'export const S = step({ name: "s" });\n' );
371
+ // Cross-workflow imports are allowed for workflows (they can import steps from other workflows)
372
+ const src = 'import { S } from "../workflow_b/steps.js";';
373
+ await expect( runLoader( join( dir, 'workflows', 'workflow_a', 'workflow.js' ), src ) ).resolves.toBeTruthy();
374
+ rmSync( dir, { recursive: true, force: true } );
375
+ } );
376
+
377
+ it( 'workflow.js: allows imports from utils with relative parent paths', async () => {
378
+ // The validator does not enforce strict scope boundaries, it validates by file type patterns
379
+ const dir = mkdtempSync( join( tmpdir(), 'wf-parent-utils-' ) );
380
+ mkdirSync( join( dir, 'src', 'workflows', 'my_workflow' ), { recursive: true } );
381
+ mkdirSync( join( dir, 'utils' ), { recursive: true } );
382
+ writeFileSync( join( dir, 'utils', 'helpers.js' ), 'export const helper = () => 1;\n' );
383
+ const src = 'import { helper } from "../../../utils/helpers.js";';
384
+ // This should pass because it's not a component file
385
+ await expect( runLoader( join( dir, 'src', 'workflows', 'my_workflow', 'workflow.js' ), src ) ).resolves.toBeTruthy();
386
+ rmSync( dir, { recursive: true, force: true } );
387
+ } );
388
+ } );
389
+
390
+ // =====================================================
391
+ // Arbitrary path imports (proving any folder name works)
392
+ // =====================================================
393
+
394
+ describe( 'arbitrary path imports', () => {
395
+ it( 'workflow.js: allows imports from ./foobar.js', async () => {
396
+ const dir = mkdtempSync( join( tmpdir(), 'wf-arbitrary-1-' ) );
397
+ const src = 'import { foo } from "./foobar.js";';
398
+ await expect( runLoader( join( dir, 'workflow.js' ), src ) ).resolves.toBeTruthy();
399
+ rmSync( dir, { recursive: true, force: true } );
400
+ } );
401
+
402
+ it( 'workflow.js: allows imports from ./foobar/baz.js', async () => {
403
+ const dir = mkdtempSync( join( tmpdir(), 'wf-arbitrary-2-' ) );
404
+ mkdirSync( join( dir, 'foobar' ) );
405
+ writeFileSync( join( dir, 'foobar', 'baz.js' ), 'export const baz = 1;\n' );
406
+ const src = 'import { baz } from "./foobar/baz.js";';
407
+ await expect( runLoader( join( dir, 'workflow.js' ), src ) ).resolves.toBeTruthy();
408
+ rmSync( dir, { recursive: true, force: true } );
409
+ } );
410
+
411
+ it( 'workflow.js: allows imports from ../../shared/foobar.js', async () => {
412
+ const dir = mkdtempSync( join( tmpdir(), 'wf-arbitrary-3-' ) );
413
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
414
+ mkdirSync( join( dir, 'shared' ), { recursive: true } );
415
+ writeFileSync( join( dir, 'shared', 'foobar.js' ), 'export const foobar = 1;\n' );
416
+ const src = 'import { foobar } from "../../shared/foobar.js";';
417
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'workflow.js' ), src ) ).resolves.toBeTruthy();
418
+ rmSync( dir, { recursive: true, force: true } );
419
+ } );
420
+
421
+ it( 'workflow.js: allows imports from ../../shared/foobar/baz.js', async () => {
422
+ const dir = mkdtempSync( join( tmpdir(), 'wf-arbitrary-4-' ) );
423
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
424
+ mkdirSync( join( dir, 'shared', 'foobar' ), { recursive: true } );
425
+ writeFileSync( join( dir, 'shared', 'foobar', 'baz.js' ), 'export const baz = 1;\n' );
426
+ const src = 'import { baz } from "../../shared/foobar/baz.js";';
427
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'workflow.js' ), src ) ).resolves.toBeTruthy();
428
+ rmSync( dir, { recursive: true, force: true } );
429
+ } );
430
+
431
+ it( 'steps.js: allows imports from ./helpers/anything.js', async () => {
432
+ const dir = mkdtempSync( join( tmpdir(), 'steps-arbitrary-1-' ) );
433
+ mkdirSync( join( dir, 'helpers' ) );
434
+ writeFileSync( join( dir, 'helpers', 'anything.js' ), 'export const anything = 1;\n' );
435
+ const src = 'import { anything } from "./helpers/anything.js";';
436
+ await expect( runLoader( join( dir, 'steps.js' ), src ) ).resolves.toBeTruthy();
437
+ rmSync( dir, { recursive: true, force: true } );
438
+ } );
439
+
440
+ it( 'evaluators.js: allows imports from ./my_custom_lib/tools.js', async () => {
441
+ const dir = mkdtempSync( join( tmpdir(), 'evals-arbitrary-1-' ) );
442
+ mkdirSync( join( dir, 'my_custom_lib' ) );
443
+ writeFileSync( join( dir, 'my_custom_lib', 'tools.js' ), 'export const tools = {};\n' );
444
+ const src = 'import { tools } from "./my_custom_lib/tools.js";';
445
+ await expect( runLoader( join( dir, 'evaluators.js' ), src ) ).resolves.toBeTruthy();
446
+ rmSync( dir, { recursive: true, force: true } );
447
+ } );
448
+ } );
449
+
450
+ // =====================================================
451
+ // Instantiation location validation
452
+ // =====================================================
453
+
454
+ describe( 'instantiation location tests - correct locations', () => {
455
+ it( 'step() called in steps/fetch_data.js is allowed', async () => {
456
+ const dir = mkdtempSync( join( tmpdir(), 'step-folder-allowed-' ) );
457
+ mkdirSync( join( dir, 'steps' ) );
458
+ const src = [
459
+ 'import { step } from "@output.ai/core";',
460
+ 'export const fetchData = step({ name: "fetch_data", fn: async () => ({}) });'
461
+ ].join( '\n' );
462
+ await expect( runLoader( join( dir, 'steps', 'fetch_data.js' ), src ) ).resolves.toBeTruthy();
463
+ rmSync( dir, { recursive: true, force: true } );
464
+ } );
465
+
466
+ it( 'step() called in src/shared/steps/common.js is allowed', async () => {
467
+ const dir = mkdtempSync( join( tmpdir(), 'step-shared-allowed-' ) );
468
+ mkdirSync( join( dir, 'src', 'shared', 'steps' ), { recursive: true } );
469
+ const src = [
470
+ 'import { step } from "@output.ai/core";',
471
+ 'export const commonStep = step({ name: "common_step", fn: async () => ({}) });'
472
+ ].join( '\n' );
473
+ await expect( runLoader( join( dir, 'src', 'shared', 'steps', 'common.js' ), src ) ).resolves.toBeTruthy();
474
+ rmSync( dir, { recursive: true, force: true } );
475
+ } );
476
+
477
+ it( 'evaluator() called in evaluators/quality.js is allowed', async () => {
478
+ const dir = mkdtempSync( join( tmpdir(), 'eval-folder-allowed-' ) );
479
+ mkdirSync( join( dir, 'evaluators' ) );
480
+ const src = [
481
+ 'import { evaluator } from "@output.ai/core";',
482
+ 'export const quality = evaluator({ name: "quality", fn: async () => ({ value: 1 }) });'
483
+ ].join( '\n' );
484
+ await expect( runLoader( join( dir, 'evaluators', 'quality.js' ), src ) ).resolves.toBeTruthy();
485
+ rmSync( dir, { recursive: true, force: true } );
486
+ } );
487
+
488
+ it( 'evaluator() called in src/shared/evaluators/metrics.js is allowed', async () => {
489
+ const dir = mkdtempSync( join( tmpdir(), 'eval-shared-allowed-' ) );
490
+ mkdirSync( join( dir, 'src', 'shared', 'evaluators' ), { recursive: true } );
491
+ const src = [
492
+ 'import { evaluator } from "@output.ai/core";',
493
+ 'export const metrics = evaluator({ name: "metrics", fn: async () => ({ value: 1 }) });'
494
+ ].join( '\n' );
495
+ await expect( runLoader( join( dir, 'src', 'shared', 'evaluators', 'metrics.js' ), src ) ).resolves.toBeTruthy();
496
+ rmSync( dir, { recursive: true, force: true } );
497
+ } );
498
+ } );
499
+
500
+ describe( 'instantiation location tests - wrong locations', () => {
501
+ it( 'step() called in utils.js is blocked by validator', async () => {
502
+ const dir = mkdtempSync( join( tmpdir(), 'step-utils-fail-' ) );
503
+ const src = [
504
+ 'import { step } from "@output.ai/core";',
505
+ 'export const badStep = step({ name: "bad", fn: async () => ({}) });'
506
+ ].join( '\n' );
507
+ await expect( runLoader( join( dir, 'utils.js' ), src ) )
508
+ .rejects.toThrow( /Invalid instantiation location.*step\(\).*steps/ );
509
+ rmSync( dir, { recursive: true, force: true } );
510
+ } );
511
+
512
+ it( 'step() called in clients/api.js is blocked by validator', async () => {
513
+ const dir = mkdtempSync( join( tmpdir(), 'step-clients-fail-' ) );
514
+ mkdirSync( join( dir, 'clients' ) );
515
+ const src = [
516
+ 'import { step } from "@output.ai/core";',
517
+ 'export const badStep = step({ name: "bad", fn: async () => ({}) });'
518
+ ].join( '\n' );
519
+ await expect( runLoader( join( dir, 'clients', 'api.js' ), src ) )
520
+ .rejects.toThrow( /Invalid instantiation location.*step\(\).*steps/ );
521
+ rmSync( dir, { recursive: true, force: true } );
522
+ } );
523
+
524
+ it( 'evaluator() called in utils.js is blocked by validator', async () => {
525
+ const dir = mkdtempSync( join( tmpdir(), 'eval-utils-fail-' ) );
526
+ const src = [
527
+ 'import { evaluator } from "@output.ai/core";',
528
+ 'export const badEval = evaluator({ name: "bad", fn: async () => ({ value: 1 }) });'
529
+ ].join( '\n' );
530
+ await expect( runLoader( join( dir, 'utils.js' ), src ) )
531
+ .rejects.toThrow( /Invalid instantiation location.*evaluator\(\).*evaluators/ );
532
+ rmSync( dir, { recursive: true, force: true } );
533
+ } );
534
+
535
+ it( 'evaluator() called in clients/api.js is blocked by validator', async () => {
536
+ const dir = mkdtempSync( join( tmpdir(), 'eval-clients-fail-' ) );
537
+ mkdirSync( join( dir, 'clients' ) );
538
+ const src = [
539
+ 'import { evaluator } from "@output.ai/core";',
540
+ 'export const badEval = evaluator({ name: "bad", fn: async () => ({ value: 1 }) });'
541
+ ].join( '\n' );
542
+ await expect( runLoader( join( dir, 'clients', 'api.js' ), src ) )
543
+ .rejects.toThrow( /Invalid instantiation location.*evaluator\(\).*evaluators/ );
544
+ rmSync( dir, { recursive: true, force: true } );
545
+ } );
546
+
547
+ it( 'evaluator() called in helpers.js is blocked by validator', async () => {
548
+ const dir = mkdtempSync( join( tmpdir(), 'eval-helpers-fail-' ) );
549
+ const src = [
550
+ 'import { evaluator } from "@output.ai/core";',
551
+ 'export const badEval = evaluator({ name: "bad", fn: async () => ({ value: 1 }) });'
552
+ ].join( '\n' );
553
+ await expect( runLoader( join( dir, 'helpers.js' ), src ) )
554
+ .rejects.toThrow( /Invalid instantiation location.*evaluator\(\).*evaluators/ );
555
+ rmSync( dir, { recursive: true, force: true } );
556
+ } );
557
+
558
+ it( 'workflow() called in shared/common.js is blocked by validator', async () => {
559
+ const dir = mkdtempSync( join( tmpdir(), 'wf-shared-fail-' ) );
560
+ mkdirSync( join( dir, 'shared' ) );
561
+ const src = [
562
+ 'import { workflow } from "@output.ai/core";',
563
+ 'export const badWf = workflow({ name: "bad", fn: async () => ({}) });'
564
+ ].join( '\n' );
565
+ await expect( runLoader( join( dir, 'shared', 'common.js' ), src ) )
566
+ .rejects.toThrow( /Invalid instantiation location.*workflow\(\).*workflow/ );
567
+ rmSync( dir, { recursive: true, force: true } );
568
+ } );
569
+
570
+ it( 'workflow() called in utils/index.js is blocked by validator', async () => {
571
+ const dir = mkdtempSync( join( tmpdir(), 'wf-utils-fail-' ) );
572
+ mkdirSync( join( dir, 'utils' ) );
573
+ const src = [
574
+ 'import { workflow } from "@output.ai/core";',
575
+ 'export const badWf = workflow({ name: "bad", fn: async () => ({}) });'
576
+ ].join( '\n' );
577
+ await expect( runLoader( join( dir, 'utils', 'index.js' ), src ) )
578
+ .rejects.toThrow( /Invalid instantiation location.*workflow\(\).*workflow/ );
579
+ rmSync( dir, { recursive: true, force: true } );
580
+ } );
581
+ } );
215
582
  } );